WeightedMultiSig 지갑으로 만든 계정에 관한 질문입니다

안녕하세요.

caver-java (v1.3.2) 를 통해서 테스트 중에 있는데 질문이 있습니다.

테스트 중인 내용은 아래와 같습니다.

테스트 1 멀티시그로 생성된 계정에서 오너 자신의 서명을 통한 transfer 가 가능하다.
테스트 2 멀티시그로 생성된 계정에서 오퍼레이터들의 서명을 통한 transfer 가 가능하다.

위 상황을 테스트 하기위해서 멀티시그 월렛을 생성하였습니다.

이때 사용된 키는 오너의 키를 포함한 3개의 키로 생성되었습니다.

각각의 weight 는 아래와 같습니다.

  • 오너 계정: 2
  • 오퍼레이터 1계정: 1
  • 오퍼레이터 2계정: 1

즉 weight 의 총합은 4이고 Threshold 는 2를 할당하였습니다.

테스트1은 무난히 성공되었으나 테스트2를 진행하다가 막혔습니다.

테스트2가 성공하기 위해서는 오너의 서명이 필요없다는 가정인데요.

질문1 이게 가능한건지요?
질문2 가능하다면 어떤 방식으로 가능한가요? (cavar-java 로 된 예제 필요)

감사합니다.

안녕하세요, 먼저 질문 주셔서 감사합니다 :slight_smile:

답변을 드리기 전에 조금 더 명확한 답변을 위해서 질문 드릴 것이 있는데요,

이 글에서의 오너 계정과 오퍼레이터 계정의 차이점이 무엇인가요?
멀티시그를 사용하는 계정에서, weight가 2인 키를 소유한 주체가 오너이고, weight가 1인 키를 소유한 주체가 오퍼레이터가 맞나요?
위의 내용이 아니라면 다른 차이점이 있는건가요?

네, 맞습니다.

멀티시그 지갑을 생성하기 위해서 아래와 같은 코드를 작성해서 사용하고 있는데요.

먼저 EOA 계정 한개를 생성한 뒤에 아래처럼 계정을 업데이트 하기 위해서 AccountkeyWeightedMultiSig 를 리턴하는 메소드를 작성해서 리턴된 계정이 되겠습니다.

public AccountKeyWeightedMultiSig multiSigWalletThreshold2WithMe(String ownerWalletKey) {
    List<AccountKeyWeightedMultiSig.WeightedPublicKey> weightedPublicKeys = new ArrayList<>();

    {
        KlayCredentials operator = KlayWalletUtils.loadCredentials(ownerWalletKey);
        AccountKeyPublic operatorAccountKeyPublic = AccountKeyPublic.create(operator.getEcKeyPair().getPublicKey());
        weightedPublicKeys.add(AccountKeyWeightedMultiSig.WeightedPublicKey.create(BigInteger.valueOf(2), operatorAccountKeyPublic));
    }

    {
        KlayCredentials operator = KlayWalletUtils.loadCredentials("오퍼레이터1의 키"); // op1
        AccountKeyPublic operatorAccountKeyPublic = AccountKeyPublic.create(operator.getEcKeyPair().getPublicKey());
        weightedPublicKeys.add(AccountKeyWeightedMultiSig.WeightedPublicKey.create(BigInteger.valueOf(1), operatorAccountKeyPublic));
    }

    {
        KlayCredentials operator = KlayWalletUtils.loadCredentials("오퍼레이터2의 키"); // op2
        AccountKeyPublic operatorAccountKeyPublic = AccountKeyPublic.create(operator.getEcKeyPair().getPublicKey());
        weightedPublicKeys.add(AccountKeyWeightedMultiSig.WeightedPublicKey.create(BigInteger.valueOf(1), operatorAccountKeyPublic));
    }

    return AccountKeyWeightedMultiSig.create(BigInteger.valueOf(2), weightedPublicKeys);
}

네, 안녕하세요 :slight_smile:

그럼 아래의 상황을 가정으로 답변 드리겠습니다.

  1. test account의 accountKey가 AccountKeyWeightedMultiSig로 업데이트 되었다.
    test account ===> (privateKey1, publicKey1) -> address 1 에서
    test account ===> [(privateKey2, publicKey2), (privateKey3, publicKey3), (privateKey4, publicKey4)] -> address 1으로 업데이트 된 상황. (threshold: 2)

  2. AccountKeyWeightedMultiSig는 3 개의 키로 업데이트 되었으며 각 키의 weight가 다르며 소유자도 다르다.
    owner ===> [ address1, (privateKey2, publicKey2) ] wieght 2
    operator1 ===> [address1, (privateKey3, publicKey3) ] weight 1
    operator2 ===> [address1, (privateKey4, publicKey4) ] weight 1

위의 상황에서 operator들이 소유한 credential들을 사용하여 트랜잭션에 서명하고 전송하는 방법에 대해서 말씀드리겠습니다.

    // ValueTransferTransaction 생성. multiSigAccount의 threshold는 2 임.
    ValueTransferTransaction valueTransferTransaction = ValueTransferTransaction.create(
            multiSigAccountAddress,
            "0xe97f27e9a5765ce36a7b919b1cb6004c7209217e",
            BigInteger.ONE,
            BigInteger.valueOf(100_000)
    );

    // operator1이 소유한 credential로 서명. 이는 weight 1 이므로 아직 threshold에 충족되지 않음.
    TransactionManager transactionManagerForOperator1 = new TransactionManager.Builder(caver, operator1Credential).setChaindId(chainId.getValue().intValue()).build();
    KlayRawTransaction rawTransactionForOperator1 = transactionManagerForOperator1.sign(valueTransferTransaction);

    // operator2가 소유한 credential로 서명. 여기서는 operator1이 서명한 결과에 이어서 서명하므로 기존에 operator1이 서명한 weight  1에 operator2가 서명 함으로써 weight 1이 추가 됨.
    TransactionManager transactionManagerForOperator2 = new TransactionManager.Builder(caver, operator2Credential).setChaindId(chainId.getValue().intValue()).build();
    KlayRawTransaction rawTransactionForOperator2 = transactionManagerForOperator2.sign(rawTransactionForOperator1.getValueAsString());

    // threshold를 만족했으므로 서명된 결과를 네트워크로 전송
    KlayTransactionReceipt.TransactionReceipt receiptWithOperators = transactionManagerForOperator2.executeTransaction(rawTransactionForOperator2.getValueAsString());
    System.out.println(receiptWithOperators);

위의 코드와 같이, valueTransfer 트랜잭션에 operator1이 소유한 credential로 서명하고, operator1이 서명한 결과를 사용하여 operator2가 자신의 credential로 서명할 수 있습니다.

이렇게 진행하면 operator2가 서명한 결과인 rawTransactionForOperator2는 2개의 signatures를 소유하게 됩니다.

위의 테스트를 위한 전체 코드를 아래에 첨부하겠습니다 .참고 부탁드립니다.

    // caver 연결 with Klaytn Node
    Caver caver = Caver.build("http://52.78.238.123:8551/");
    Quantity chainId = caver.klay().getChainID().send();

    // 테스트 계정 생성을 위한 작업
    String walletKey = "c126e2987dc32e96b0ce795614836a381e225975b7e312567fc87b75be87fa440x000x7ea9b9015ac473cdfa9c231e7bb939c02571fa22";
    KlayCredentials credentials = KlayCredentials.createWithKlaytnWalletKey(walletKey);

    TransactionManager transactionManager = new TransactionManager.Builder(caver, credentials).setChaindId(chainId.getValue().intValue()).build();

    // update를 하기 위한 계정. KLAY가 필요하므로 아래는 테스트에 필요한 KLAY를 전송하는 과정
    KlayCredentials updateCredential = KlayCredentials.create(Keys.createEcKeyPair());
    String toAddress = updateCredential.getAddress();

    KlayTransactionReceipt.TransactionReceipt receipt = ValueTransfer.create(caver, credentials, chainId.getValue().intValue())
            .sendFunds(credentials.getAddress(), toAddress, BigDecimal.ONE, Convert.Unit.KLAY, BigInteger.valueOf(100_000)).send();
    System.out.println(receipt);


    // 실제 AccountKeyWeightedMultiSig를 위한 테스트 시작

    // AccountKeyWeightedMultiSig를 위한 WeightedPublicKey 생성. 각 weight와 publicKey를 통해서 생성
    String multiSigAccountAddress = updateCredential.getAddress();
    ECKeyPair ownerKey = Keys.createEcKeyPair();
    KlayCredentials ownerCredential = KlayCredentials.create(ownerKey, multiSigAccountAddress); // owner가 소유할 credential
    AccountKeyWeightedMultiSig.WeightedPublicKey pubKey1 = AccountKeyWeightedMultiSig.WeightedPublicKey.create(
        BigInteger.valueOf(2), // weight 2
        AccountKeyPublic.create(ownerKey.getPublicKey())
    );
    ECKeyPair operator1Key = Keys.createEcKeyPair();
    KlayCredentials operator1Credential = KlayCredentials.create(operator1Key, multiSigAccountAddress); // operator1이 소유할 credential
    AccountKeyWeightedMultiSig.WeightedPublicKey pubKey2 = AccountKeyWeightedMultiSig.WeightedPublicKey.create(
        BigInteger.valueOf(1), // weight 1
        AccountKeyPublic.create(operator1Key.getPublicKey())
    );
    ECKeyPair operator2Key = Keys.createEcKeyPair();
    KlayCredentials operator2Credential = KlayCredentials.create(operator2Key, multiSigAccountAddress); // operator2가 소유할 credential
    AccountKeyWeightedMultiSig.WeightedPublicKey pubKey3 = AccountKeyWeightedMultiSig.WeightedPublicKey.create(
            BigInteger.valueOf(1), // weight 1
            AccountKeyPublic.create(operator2Key.getPublicKey())
    );

    // 여러 개의 WeightedPublicKey를 포함하는 리스트 생성
    List<AccountKeyWeightedMultiSig.WeightedPublicKey> weightedKeys = new ArrayList<>();
    weightedKeys.add(pubKey1);
    weightedKeys.add(pubKey2);
    weightedKeys.add(pubKey3);

    // AccountKeyWeightedMultiSig 생성. threshold와 weightedPublicKey들을 포함하는 리스트를 통하여 생성.
    AccountKeyWeightedMultiSig newAccountKey = AccountKeyWeightedMultiSig.create(BigInteger.valueOf(2), weightedKeys); // threshold 2

    // Account update 트랜잭션 생성.
    AccountUpdateTransaction updateTx = AccountUpdateTransaction.create(
            multiSigAccountAddress,
            newAccountKey,
            BigInteger.valueOf(300_000)
    );

    // accountkey를 업데이트 하기 위하여 account update transaction을 서명 및 전송
    transactionManager = new TransactionManager.Builder(caver, updateCredential).setChaindId(chainId.getValue().intValue()).build();
    KlayRawTransaction klayRawTransaction = transactionManager.sign(updateTx);
    KlayTransactionReceipt.TransactionReceipt transactionReceipt = transactionManager.executeTransaction(klayRawTransaction.getValueAsString());
    System.out.println(transactionReceipt);

    // ValueTransferTransaction 생성. multiSigAccount의 threshold는 2 임.
    ValueTransferTransaction valueTransferTransaction = ValueTransferTransaction.create(
            multiSigAccountAddress,
            "0xe97f27e9a5765ce36a7b919b1cb6004c7209217e",
            BigInteger.ONE,
            BigInteger.valueOf(100_000)
    );

    // operator1이 소유한 credential로 서명. 이는 weight 1 이므로 아직 threshold에 충족되지 않음.
    TransactionManager transactionManagerForOperator1 = new TransactionManager.Builder(caver, operator1Credential).setChaindId(chainId.getValue().intValue()).build();
    KlayRawTransaction rawTransactionForOperator1 = transactionManagerForOperator1.sign(valueTransferTransaction);

    // operator2가 소유한 credential로 서명. 여기서는 operator1이 서명한 결과에 이어서 서명하므로 기존에 operator1이 서명한 weight  1에 operator2가 서명 함으로써 weight 1이 추가 됨.
    TransactionManager transactionManagerForOperator2 = new TransactionManager.Builder(caver, operator2Credential).setChaindId(chainId.getValue().intValue()).build();
    KlayRawTransaction rawTransactionForOperator2 = transactionManagerForOperator2.sign(rawTransactionForOperator1.getValueAsString());

    // threshold를 만족했으므로 서명된 결과를 네트워크로 전송
    KlayTransactionReceipt.TransactionReceipt receiptWithOperators = transactionManagerForOperator2.executeTransaction(rawTransactionForOperator2.getValueAsString());
    System.out.println(receiptWithOperators);

혹시 추가적으로 더 궁금한 부분이나 이해가 되지 않는 부분이 있다면 말씀해 주세요 :slight_smile:

안녕하세요.
문제가 말끔히 해결되었습니다.
저희 문제는 EOA계정을 멀티시그로 업데이트한 이후에
완전 별도의 프로세스로 해당 계정을 다시 로딩하고
멀티시그 오퍼레이터들로 서명해서 보내는 부분으로 기획을 하고 있었는데 바로 이부분에서 오류가 있었던 것을 발견했습니다.
도움주셔서 감사합니다.
클레이튼 기반 서비스를 만들고 있는데요.
앞으로 자주 질문 올리도록 하겠습니다.
:slight_smile: 행복한 주말보내시길 ~

1 Like

엇 한가지 더 궁금한게 있습니다.

FeeDeleate() 트랜잭션으로 보내는 경우에도 위 예제에 추가해주실 수 있으신가요?

감사합니다.

테스트 상황을 좀 더 설명드리겠습니다.

보내주신 코드에서
ValueTransferTransaction 객체를 생성할 때 .feeDelegate() 를 추가 한뒤에

오퍼레이터 2가 sign 만 하고 executeTransacion() 을 하지 않고

아래 코드로 feePayer 가 execution 하도록 수정했습니다.

// fee payer
ECKeyPair feePayer = ECKeyPair.create(Numeric.toBigInt(“FeePayer프라이빗키”));
KlayCredentials feePayerCredential = KlayCredentials.create(feePayer, multiSigAccountAddress); // operator2가 소유할 credential
TransactionManager transactionManagerForFeePayer = new TransactionManager.Builder(caver, feePayerCredential).setChaindId(chainId.getValue().intValue()).build();
KlayTransactionReceipt.TransactionReceipt transactionReceipt = transactionManagerForFeePayer.executeTransaction(rawTransactionForOperator2.getValueAsString());
System.out.println("transactionReceipt = " + transactionReceipt);

그런데 HttpService 의 디버그로그에서

{“jsonrpc”:“2.0”,“id”:2,“error”:{“code”:-32000,“message”:“rlp: input string too short for common.Address, decoding into (types.Transaction)(types.TxInternalDataSerializer)(types.TxInternalDataFeeDelegatedValueTransfer).FeePayer”}}

와 같이 뜨고 있어서 질문 드렸습니다.

감사합니다.

자문 자답합니다.
문제가 해결되었습니다.
감사합니다. :slight_smile:

1 Like

안녕하세요.
작성해 주신 예제대 대한 1.5.6 버전으로 변경했을 때의 예제도 어디에서 볼 수 있을까요?

  • Keyring 방식의 서명 방식이 사용하도록.

https://ko.docs.klaytn.com/bapp/sdk/caver-java/v1.4.0/getting-started_1.4.0#sending-a-transaction-with-multiple-signers

여기 있는 내용을 1.5.6 으로 변경한 예제이면 될것같습니다.