다중 서명 값으로 FeeDelegatedValueTransfer 오류

안녕하세요.
멀티시그 지갑에서 FeeDelegatedValueTransfer 로 전송 코드를 작성중 오류가 발생해서 질문드립니다.

아래 코드를 실행하면

    String ownerPrivateKey = "";
    String op1PrivateKey = "";
    String op2PrivateKey = "";

    String feePayer = "0x5e3d89dd8ad65129b340e73c92d51d2213213c37";
    String feePayerKey = "";

    SingleKeyring keyring1 = KeyringFactory.createFromPrivateKey(ownerPrivateKey);      caver.wallet.add(keyring1);
    SingleKeyring keyring2 = KeyringFactory.createFromPrivateKey(op1PrivateKey);        caver.wallet.add(keyring2);
    SingleKeyring keyring3 = KeyringFactory.createFromPrivateKey(op2PrivateKey);        caver.wallet.add(keyring3);
    SingleKeyring feePayerKeyring = KeyringFactory.createFromPrivateKey(feePayerKey);   caver.wallet.add(feePayerKeyring);

    FeeDelegatedValueTransfer feeDelegatedValueTransfer = new FeeDelegatedValueTransfer.Builder()
            .setKlaytnCall(caver.rpc.klay)
            .setFrom(keyring1.getAddress())
            .setTo("0xc41ae0a358f069a934bdc383c854aac4ea28f946")
            .setValue(BigInteger.valueOf(1))
            .setGas(BigInteger.valueOf(30000))
            .setFeePayer(feePayer)
            .build();

    AbstractTransaction sign1 = caver.wallet.sign(keyring1.getAddress(), feeDelegatedValueTransfer);
    AbstractTransaction sign2 = caver.wallet.sign(keyring2.getAddress(), sign1);
    AbstractTransaction sign3 = caver.wallet.sign(keyring3.getAddress(), sign2);

    String rlpEncoding = sign3.getRLPEncoding();

    Bytes32 sendResult = caver.rpc.klay.sendRawTransaction(rlpEncoding).send();
    String result = sendResult.getResult();
    System.out.println("result = " + result);

아래와 같은 오류가 나옵니다.

java.lang.IllegalArgumentException: The from address of the transaction is different with the address of the keyring to use

at com.klaytn.caver.transaction.AbstractTransaction.sign(AbstractTransaction.java:279)
at com.klaytn.caver.transaction.AbstractTransaction.sign(AbstractTransaction.java:259)

혹시 원인을 알수있을까요?
감사합니다.

안녕하세요 질문 올려주셔서 감사합니다.

KeyringFactory.createFromPrivateKey 함수를 사용해서 3개의 다른 private key를 각각 사용해서 키링을 생성하고 이를 caver.wallet에 추가하셨는데요, 이럴 경우 각각의 private key와 매핑되는 address를 가진 키링이 생성되므로 멀티시그 지갑의 시나리오와 맞지 않습니다.

해당 에러는 트랜잭션에 정의된 from의 주소와 서명에 사용하고자 하는 키링의 주소가 다를 경우에 발생되는 에러입니다.

멀티시그 계정인 경우 하나의 주소에 여러 개의 private key를 사용할 수 있는 아래의 구조입니다.
MultiSigKeyringA ( addressA - private B, private C, private D )

위에 작성해 주신 예제는 아래와 같이 서로 각기 다른 계정을 사용하여 트랜잭션에 서명을 시도하였기 때문에 에러가 발생한 것입니다.
Keyring 1 ( address A - private A )
Keyring 2 ( address B - private B )
Keyring 3 ( address C - private C )

아래의 코드 예제를 참고해 주시기 바랍니다.

String address = "0x{address in hex}";
String[] privateKeyArray = new String[] {"0x{private key#1}", "0x{private key#2}", "0x{private key#3}"};
MultipleKeyring multisigKeyring = KeyringFactory.createWithMultipleKey(address, privateKeyArray);
caver.wallet.add(multisigKeyring);

... (수수료 대납 키링 생성 및 트랜잭션 생성부 생략)

AbstractTransaction signed = caver.wallet.sign(multisigKeyring.getAddress(), feeDelegatedValueTransfer);

멀티시그 키링을 생성하는 방법은 여기를 확인해 주세요.
감사합니다 :slight_smile:

네, 답변감사합니다.

feePayer가 서명하는 부분은 어떤 방식으로 이뤄지나요?

fee payer 서명의 경우 signAsFeePayer로 아래와 같이 하시면 됩니다.

caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), feeDelegatedValueTransfer);

문서는 여기를 참고해 주세요.

답변감사합니다.

String ownerAddress = "0x7dd06d34595a95845ea025e13bc3b1c3b5da4c59";
    String ownerPrivateKey = "";
    String op1PrivateKey = "";
    String op2PrivateKey = "";
    String feePayer = "0x5e3d89dd8ad65129b340e73c92d51d2213213c37";
    String feePayerKey = "";

    String[] privateKeyArray = new String[] {ownerPrivateKey, op1PrivateKey, op2PrivateKey, feePayerKey};
    MultipleKeyring multiSigKeyring = KeyringFactory.createWithMultipleKey(ownerAddress, privateKeyArray);
    caver.wallet.add(multiSigKeyring);

    FeeDelegatedValueTransfer feeDelegatedValueTransfer = new FeeDelegatedValueTransfer.Builder()
            .setKlaytnCall(caver.rpc.klay)
            .setFrom(multiSigKeyring.getAddress())
            .setTo("0xc41ae0a358f069a934bdc383c854aac4ea28f946")
            .setValue(BigInteger.valueOf(1))
            .setGas(BigInteger.valueOf(30000))
            .setFeePayer(feePayer)
            .build();

    AbstractTransaction signed = caver.wallet.sign(multiSigKeyring.getAddress(), feeDelegatedValueTransfer);
    String rlpEncoded = signed.getRLPEncoding();

    SingleKeyring feePayerKeyring = KeyringFactory.createWithSingleKey(feePayer, feePayerKey);
    FeeDelegatedValueTransfer decoded = FeeDelegatedValueTransfer.decode(rlpEncoded);
    decoded.setFeePayer(feePayerKeyring.getAddress());

    caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), decoded);
    System.out.println(feeDelegatedValueTransfer.getRLPEncoding());

이렇게 했을 때

java.lang.NullPointerException: Failed to find keyring from wallet with address

at com.klaytn.caver.wallet.KeyringContainer.signAsFeePayer(KeyringContainer.java:306)
at com.klaytn.caver.wallet.KeyringContainer.signAsFeePayer(KeyringContainer.java:293)

이런 오류가 나고 있는데요.
확인가능하실까요?

위 코드에서 아래 부분에서 오류가 납니다. feePayer 의 키를 키링에 등록했는데 오류가 나는것 같습니다.
caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), decoded);

해당 에러는 caver.wallet에서 서명에 사용할 키링을 찾지 못했을 때에 발생하는 에러입니다.

caver.wallet.add(feePayerKeyring); 으로 기생성한 feePayerKeyring 키링을 추가해 보시겠어요?

답변 감사합니다.
해당 부분 말씀하신대로 했는데요.

다른 오류가 발생하네요. ;

String[] privateKeyArray = new String[] {ownerPrivateKey, op1PrivateKey, op2PrivateKey};
MultipleKeyring multiSigKeyring = KeyringFactory.createWithMultipleKey(ownerAddress, privateKeyArray);
caver.wallet.add(multiSigKeyring);

    FeeDelegatedValueTransfer feeDelegatedValueTransfer = new FeeDelegatedValueTransfer.Builder()
            .setKlaytnCall(caver.rpc.klay)
            .setFrom(multiSigKeyring.getAddress())
            .setTo("0xc41ae0a358f069a934bdc383c854aac4ea28f946")
            .setValue(BigInteger.valueOf(1))
            .setGas(BigInteger.valueOf(30000))
            .setFeePayer(feePayer)
            .build();

    AbstractTransaction signed = caver.wallet.sign(multiSigKeyring.getAddress(), feeDelegatedValueTransfer);
    String rlpEncoded = signed.getRLPEncoding();

    SingleKeyring feePayerKeyring = KeyringFactory.createWithSingleKey(feePayer, feePayerKey);
    caver.wallet.add(feePayerKeyring);

    FeeDelegatedValueTransfer decoded = FeeDelegatedValueTransfer.decode(rlpEncoded);
    decoded.setFeePayer(feePayerKeyring.getAddress());

    caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), decoded);
    System.out.println(feeDelegatedValueTransfer.getRLPEncoding());

오류내용은 caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), decoded); 에서 발생합니다.

java.lang.RuntimeException: Cannot fill transaction data.(nonce, chainId, gasPrice

at com.klaytn.caver.transaction.AbstractTransaction.fillTransaction(AbstractTransaction.java:456)
at com.klaytn.caver.transaction.AbstractFeeDelegatedTransaction.signAsFeePayer(AbstractFeeDelegatedTransaction.java:181)
at com.klaytn.caver.wallet.KeyringContainer.signAsFeePayer(KeyringContainer.java:309)
at com.klaytn.caver.wallet.KeyringContainer.signAsFeePayer(KeyringContainer.java:293)

안녕하세요.

RuntimeException이 발생한 이유는 RLP Encoding한 Transaction을 decoding한 Transaction instance는 Klay instance와 chainId가 들어있지 않은 상태입니다.
하지만 signAsFeePayer()시 chainID값이 없어서, 이 값을 klay 객체를 사용하여 얻어오는 과정에서 발생하게 되는데요.

2가지 방법으로 해결할 수가 있을 것 같습니다.

  1. Decoding하지 않고 sign(), signAsFeePayer()를 수행

         Caver caver = new Caver(Caver.DEFAULT_URL);
         String ownerAddress = KeyringFactory.generate().getAddress();
         SingleKeyring feePayerKeyring = KeyringFactory.generate();
    
         String[] privateKeyArray = new String[] {PrivateKey.generate().getPrivateKey(), PrivateKey.generate().getPrivateKey(), PrivateKey.generate().getPrivateKey()};
         MultipleKeyring multiSigKeyring = KeyringFactory.createWithMultipleKey(ownerAddress, privateKeyArray);
         caver.wallet.add(multiSigKeyring);
         caver.wallet.add(feePayerKeyring);
    
         FeeDelegatedValueTransfer feeDelegatedValueTransfer = new FeeDelegatedValueTransfer.Builder()
                 .setKlaytnCall(caver.rpc.klay)
                 .setFrom(multiSigKeyring.getAddress())
                 .setTo("0xc41ae0a358f069a934bdc383c854aac4ea28f946")
                 .setValue(BigInteger.valueOf(1))
                 .setGas(BigInteger.valueOf(30000))
                 .setFeePayer(feePayerKeyring.getAddress())
                 .build();
    
         caver.wallet.sign(multiSigKeyring.getAddress(), feeDelegatedValueTransfer);
         caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), feeDelegatedValueTransfer);
         System.out.println(feeDelegatedValueTransfer.getRLPEncoding());
    
  2. Decoding된 Transaction instance에 Klay객체를 다시 넣어주는 방법

     Caver caver = new Caver(Caver.DEFAULT_URL);
     String ownerAddress = KeyringFactory.generate().getAddress();
     SingleKeyring feePayerKeyring = KeyringFactory.generate();
    
     String[] privateKeyArray = new String[] {PrivateKey.generate().getPrivateKey(), PrivateKey.generate().getPrivateKey(), PrivateKey.generate().getPrivateKey()};
     MultipleKeyring multiSigKeyring = KeyringFactory.createWithMultipleKey(ownerAddress, privateKeyArray);
     caver.wallet.add(multiSigKeyring);
     caver.wallet.add(feePayerKeyring);
    
     FeeDelegatedValueTransfer feeDelegatedValueTransfer = new FeeDelegatedValueTransfer.Builder()
             .setKlaytnCall(caver.rpc.klay)
             .setFrom(multiSigKeyring.getAddress())
             .setTo("0xc41ae0a358f069a934bdc383c854aac4ea28f946")
             .setValue(BigInteger.valueOf(1))
             .setGas(BigInteger.valueOf(30000))
             .build();
    
     AbstractTransaction signed = caver.wallet.sign(multiSigKeyring.getAddress(), feeDelegatedValueTransfer);
     String rlpEncoded = signed.getRLPEncoding();
    
     FeeDelegatedValueTransfer decoded = FeeDelegatedValueTransfer.decode(rlpEncoded);
     decoded.setFeePayer(feePayerKeyring.getAddress());
     decoded.setKlaytnCall(caver.rpc.klay);
    
     caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), decoded);
     System.out.println(feeDelegatedValueTransfer.getRLPEncoding());
    

추가질문이 있으면 댓글 부탁드리겠습니다.

감사합니다.

네, 친절한 답변 감사합니다.
2번 방법으로 signAsFeePayer() 는 성공을 했습니다만 send() 결과가 null 이 나와서 다시 질문드립니다.

        FeeDelegatedValueTransfer decoded = FeeDelegatedValueTransfer.decode(rlpEncoded);
    decoded.setFeePayer(feePayerKeyring.getAddress());
    decoded.setKlaytnCall(caver.rpc.klay);

    caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), decoded);
    System.out.println(feeDelegatedValueTransfer.getRLPEncoding());

    String rlpEncodedFinal = feeDelegatedValueTransfer.getRLPEncoding();
    Bytes32 sendResult = caver.rpc.klay.sendRawTransaction(rlpEncodedFinal).send();
    String txHash = sendResult.getResult();
    System.out.println("txHash = " + txHash); // null 이 나옴.

결과가
{“jsonrpc”:“2.0”,“id”:4,“error”:{“code”:-32000,“message”:“invalid fee payer”}}

이렇게 나오는걸보니 fee payer 가 왜 invalid 한지 잘 모르겠네요.;

참고로 해당 Caver 를 퍼블릭 EN 이 아니라 KAS api 쪽으로 설정이 되어 있습니다.
HttpService httpService = new HttpService(“https://node-api.klaytnapi.com/v1/klaytn”);

    String auth = Credentials.basic(accessKeyId, secretAccessKey, StandardCharsets.UTF_8);
    httpService.addHeader("Authorization", auth);
    httpService.addHeader("x-chain-id", ChainId.BAOBAB_TESTNET +"");
    Caver caver = new Caver(httpService);

해결되어서 답변 안해주셔도 될 것 같습니다.
caver.rpc.klay.sendRawTransaction(rlpEncodedFinal).send();
==>
caver.rpc.klay.sendRawTransaction(decoded).send();

감사합니다.

1개의 좋아요

안녕하세요.
비슷한 유형이나 이번에는 FeeDelegatedContractExecution 관련한 오류가 있습니다.
트랜잭션이 정상적으로 생성되지 않으며 아래와 같은 오류가 나옵니다.
{“jsonrpc”:“2.0”,“id”:4,“error”:{“code”:-32000,“message”:“not a program account (e.g., an account having code and storage)”}}

소스코드.

Contract contract = new Contract(caver, abi, contractAddress);

    String[] privateKeyArray = new String[] {op1PrivateKey, op2PrivateKey};
    MultipleKeyring multiSigKeyring = KeyringFactory.createWithMultipleKey(ownerAddress, privateKeyArray);
    caver.wallet.add(multiSigKeyring);

    SendOptions sendOptions = new SendOptions();
    sendOptions.setFrom(multiSigKeyring.getAddress());  // executor address
    sendOptions.setGas(BigInteger.valueOf(500000));

    String encodedParams = contract.encodeABI("safeTransfer", new Address(toAddress), Convert.toPeb(amount, KLAY).toBigInteger());

    FeeDelegatedSmartContractExecution feeDelegatedSmartContractExecution = new FeeDelegatedSmartContractExecution.Builder()
            .setKlaytnCall(caver.rpc.klay)
            .setFrom(sendOptions.getFrom())
            .setTo(toAddress)
            .setGas(sendOptions.getGas())
            .setInput(encodedParams)
            .setFeePayer(feePayer)
            .build();

    AbstractTransaction signed = caver.wallet.sign(multiSigKeyring.getAddress(), feeDelegatedSmartContractExecution);
    String rlpEncoded = signed.getRLPEncoding();
    System.out.println("rlpEncoded = " + rlpEncoded);

    SingleKeyring feePayerKeyring = KeyringFactory.createWithSingleKey(feePayer, feePayerKey);
    caver.wallet.add(feePayerKeyring);

    FeeDelegatedSmartContractExecution decoded = FeeDelegatedSmartContractExecution.decode(rlpEncoded);
    decoded.setFeePayer(feePayerKeyring.getAddress());
    decoded.setKlaytnCall(caver.rpc.klay);

    caver.wallet.signAsFeePayer(feePayerKeyring.getAddress(), decoded);
    String rlpEncodedFinal = feeDelegatedSmartContractExecution.getRLPEncoding();
    System.out.println("rlpEncodedFinal = " + rlpEncodedFinal);

    Bytes32 sendResult = caver.rpc.klay.sendRawTransaction(decoded).send();
    String txHash = sendResult.getResult();
    System.out.println("txHash = " + txHash);

txHash가 null 로 나옵니다.

확인 부탁드립니다.

아 위 to 주소가 잘못되었던 거라서 해결되었습니다.
감사합니다.