7. Coinstack Multisig

Last updated 2 months ago

1. 개요

비트코인 계정은 공개 키 암호 방식으로 관리되므로 안전하고 편리하지만, 개인키를 분실하는 경우 해당 주소에 가지고 있는 비트코인을 사용할 수 없게 된다는 위험성이 있습니다. 실제로 상당량의 비트코인이 개인키의 분실로 인해 특정 주소에서 더이상 사용되지 못하고 잠들어 있을 것으로 추정되고 있습니다. 또한 허술한 관리로 인해 개인키가 유출되는 경우 비트코인을 쉽게 도난당할 수 있습니다.

만약 비트코인의 특정 주소에 여러 개의 키를 설정할 수 있고, 그 중 몇 개의 키가 있어야 해당 주소의 비트코인을 사용할 수 있게 한다면 단 한 개뿐인 개인키의 분실이나 유출만으로 비트코인을 사용할 수 없게 되거나 도난당하는 문제를 완화할 수 있습니다.

예를 들어 2개의 키를 설정하고 그 중 하나만으로도 특정 주소의 비트코인을 사용할 수 있게 해 두면, 사용자는 2개의 키를 각각 다른 공간에 보관할 수 있습니다. 만약 한 개의 키를 잃어버리더라도 별도로 보관하고 있던 다른 키를 사용하여 해당 비트코인을 사용할 수 있게 됩니다.

또 다른 활용은 3개의 키를 설정하고 그 중 두 개가 있어야 비트코인을 사용할 수 있게 하는 방식입니다. 3개의 키 중 하나는 거래소나 지갑 같은 서비스 회사에서 관리하게 하면 사용자는 항상 두 개의 키를 준비해야 하는 불편 없이 서비스를 이용할 수 있습니다. 하지만 평소 사용하던 키를 분실하더라도 별도로 보관하던 키를 이용해 비트코인을 안전하게 옮길 수 있고, 서비스 회사 입장에서는 키가 하나밖에 없기 때문에 사용자를 속일 수 없으며, 해킹을 당하더라도 사용자의 지갑을 안전하게 보호할 수 있습니다.

이처럼 특정 비트코인 주소에 n개(n>1)의 개인 키를 설정해 두고, 해당 주소의 비트코인을 사용할 때는 그 중 m개(n>=m>=1)의 개인 키가 필요하도록 설정하는 방식을 Multisig라고 합니다.

2. Multisig Funding

  1. P2SH 주소 생성

  2. P2SH 주소에 Bitcoin 전송하는 Transaction 생성

Multisig 주소에 Bitcoin을 전송하기 위해서는 우선 Multisig 주소를 생성해야 합니다. 일반적으로 많이 쓰이는 Multisig 주소라는 용어는 사실 P2SH 주소입니다. P2SH 의 Pubkey script는 일반적으로 다음 형태를 가집니다.

Pubkey script: OP_HASH160 <Hash160(redeemScript)> OP_EQUAL

위 Pubkey script의 input으로 사용되는 redeemScript는 다음과 같은 구성 요소를 가집니다.

Pubkey script: <m> <A pubkey> [B pubkey] [C pubkey...] <n> OP_CHECKMULTISIG

m of n multisig의 경우에 n개의 pubkey와 이 트랜잭션을 사용하기 위해 필수적으로 제출해야 하는 signature 수 m을 input으로 가지는 것을 알 수 있습니다.

다음은 Coinstack에서 제공하는 Multisig 주소 관련 함수입니다.

//Redeemscript 생성
public void testRedeemScript() throws UnsupportedEncodingException, DecoderException {
String publickey1 = "04a93d29a957d3e0064f6fde7a6c296e1ab2643f877d98ca5f52bdb9df43d3f70dfc040ee212b9bae63b2cf266ca677d31d72db53d1e928851d131d1cb9d9bde25";
String publickey2 = "04c30518fb1d5f0e96ba9f262f31260c8ce02e322494436e4ccb0981ba687a1a2626ab30911ddad22023bcbdac1329fc73e999ff2836c608e9c8e405f4e6c0b615";
String publickey3 = "04486a7c8754177b488982dcb13bd37fa6005a439dce55101c35ba3c5f60928036920e72a59429f0425967b735a1c52c9d2018a5ef4f63b8a8473ded4a29d5bad0";
List<byte[]> pubkeys = new ArrayList<byte[]>(3);
pubkeys.add(Hex.decodeHex(publickey1.toCharArray()));
pubkeys.add(Hex.decodeHex(publickey2.toCharArray()));
pubkeys.add(Hex.decodeHex(publickey3.toCharArray()));
String redeemScript = MultiSig.createRedeemScript(2, pubkeys);
}

public key 3개와 이 UTXO를 사용하기 위해 2개의 pubkey를 제출해야 하는 redeemscript를 생성하는 예시입니다.

// P2SH 주소 생성
public static String createAddressFromRedeemScript(String redeemScript, boolean isMainNet) throws MalformedInputException {
Script redeem = null;
String from = null;
try {
redeem = new Script(Hex.decodeHex(redeemScript.toCharArray()));
} catch (ScriptException e) {
throw new MalformedInputException("Invalid redeem script", "Parsing redeem script failed");
} catch (DecoderException e) {
throw new MalformedInputException("Invalid redeem script", "Parsing redeem script failed");
}
from = createAddressFromRedeemScript(redeem, isMainNet);
return from.toString();
}

Redeemscript를 이용하여 P2SH 주소를 생성하는 함수입니다.

최종적으로 생성되는 P2SH 주소는 일반적으로 사용되는 P2PKH 주소와 동일하게 사용할 수 있습니다. 즉, P2SH 주소에 Bitcoin을 전송하면 Multisig Transaction을 생성한 것이고 이 주소를 계속적으로 Funding을 하거나 Spending을 하면서 사용할 수 있습니다. Funding은 단순히 이 주소에 Bitcoin을 보내기만 하면 되고 Spending은 m개(m of n Multisig)의 Private key로 서명한 signature script가 제출되어야만 가능합니다.

3. Multisig Spending

위 장에서 Funding한 Multisig transaction UTXO를 사용하기 위해서는 m개의 Private key가 필요합니다. m개의 private key를 array list로 전달하고 수신 주소를 입력하면 Multisig spending transaction을 생성할 수 있습니다.

@Test
public void testMultiSigTransaction() throws Exception {
String privateKey1 = "5Jh46BhaJJeiW8C9tTz6ockGEgYnLYfrJmGnwYdcDpBbAvWvCbv";
// String privateKey2 = "5KF55BbKeZZqmAmpQAovn7KoBRjVdW4UN9uPGZoK1y9RrkPhnhA";
String privateKey3 = "5JSad8KB82c3XW69e1hz8g1YFFts4GTdjHuWHkh4d4A8MZWw12N";
String redeemScript = "52410468806910b7a3589f40c09092d3a45c64f1ef950e23d4b5aa92ad4c3de7804ed95f0f50aca9ae928fb6e00223fad667693bf3e2b716dd6c9d474ad79f5b7a107e410494bae4aa9a4c2ca6103899098ca0867f62ca24af02fee2d6473a698d92fbc8c449aa2e236c0684ebb9e0fbb23d847d4624fd8ca4a1fdc940df432c6e312e18e84104cbc882d221f567005ea61aa45d5414f25371472f6ab5973e13a39a9edc26359b6980aa4f6f34cea62e82bbe13adc7fde9fc26bba2be2e7c5f8011a68bea39bae53ae";
List<String> prikeys = new ArrayList<String>();
prikeys.add(privateKey1);
prikeys.add(privateKey3);
String to = "1F444Loh6KzUQ8u8mAsz5upBtQ356vN95s";
long amount = Math.convertToSatoshi("0.0039");
long fee = Math.convertToSatoshi("0.0001");
TransactionBuilder builder = new TransactionBuilder();
builder.addOutput(to, amount);
builder.setFee(fee);
String signedTx = coinStackClient.createMultiSigTransaction(builder, prikeys, redeemScript);
assertNotNull(signedTx);
coinStackClient.sendTransaction(signedTx);
}

4. Multisig Partial Sign

위에 설명한 Multisig Spending은 실제 use case에는 그 쓰임이 한정적일 수 밖에 없습니다. 설명에서 보듯이 제출하는 Private key들을 모두 알고 있어야 하고 그것을 이용하여 한번에 sign을 해야만 Spending이 가능하기 때문입니다. 하지만, 실제 사용에서는 개인들 간에, 또는 기관과 개인, machine과 개인 등등이 각각 private key를 나눠 갖고 자신이 가진 private key를 자신만이 보관하고 유지해야 하는 경우가 대부분입니다. 따라서, 자신이 가지고 있는 private key로 sign을 추가하고 incomplete한 transaction을 또 다른 개인에게 전달할 수 있는 기능이 필요합니다. 이것이 Multisig partial sign입니다.

@Test
public void testPartialSignTransaction() throws Exception {
String privateKey3 = "5JiqywVBWDphZbR2UWnUtj3yTX52LmGhnBy8gGED7GDdxzPuRaZ";
String privateKey2 = "5J4ZadEdMs3zaqTutP1eQoKnCGKSYrUgkPwBZrk3hNmFiz7B6Ke";
//String privateKey1 = "5JKhaPecauUSKKZTJ2R8zhNZqFxLSDu3Q5dPU3ijSqkf2WGVekn";
String redeemScript = "524104162a5b6239e12d3d52f2c880555934525dbb014dae7165380f77dcbf58b121b8033f59a1f7a4dcea589fc4405ac756542dfa393d53f7a559038f59b8d1084de541046a8fca1041f6ecf55aaa4e431b6c4ee72b51492330e777f2967697eb633e277eabf5d6e2ab3132b218a2d03b013ac90a80a4a2b5a27d1fa2a78cccad64d43b6f4104e850211b270fe7c97335411fcb774f6c7af0a8dd2e3360ba577e0c2979c51a375f5c256e2c8701d1b9777c15b7fc8b42af435977fe338e4a4e19683c884ad0fd53ae";
String to = "1F444Loh6KzUQ8u8mAsz5upBtQ356vN95s";
long amount = Math.convertToSatoshi("0.0001");
long fee = Math.convertToSatoshi("0.0001");
TransactionBuilder builder = new TransactionBuilder();
builder.addOutput(to, amount);
builder.setFee(fee);
String signedTx = coinStackClient.createMultiSigTransactionWithPartialSign(builder, privateKey3, redeemScript);
signedTx = coinStackClient.signMultiSigTransaction(signedTx, privateKey2, redeemScript);
assertNotNull(signedTx);
coinStackClient.sendTransaction(signedTx);
}

위 예제는 2 of 3 Multisig spending transaction을 partial sign을 통해 생성하는 예제입니다. createMultiSigTransactionWithPartialSign 메소드는 bitcoin 수량, 수수료, 수신자 주소와 같은 Transaction 생성을 위한 기본적인 정보를 활용하여 incomplete transaction을 만들어 냅니다. 실제로 이 메소드를 활용한 결과값 transaction을 broadcast할 수 없습니다. 왜냐하면 최소 요구 signature 수량이 2인데 privateKey3을 활용하여 1개의 signature만을 추가한 transaction이기 때문입니다. 이 임시 Transaction을 완료하기 위해서는 1개의 signature를 추가해야 하는데 이때 사용하는 메소드가 signMultiSigTransaction입니다. 만약 signature 최소 요구 수량을 만족하면 완성된 transaction이 return되고 여전히 부족하면 1개의 signature가 추가된 incomplete transaction이 return됩니다.

5. Assets Multisig Sign

다음 장에서 설명할 Open Assets Protocol 또한 Multisig Spending 의 적용이 가능합니다. Assets 의 소유자와 Assets 를 관리하는 시스템이 서로 권한을 나눠 운영 할 수 있게 됩니다. 1 of 2 Multisig 를 사용하면 운영 시스템에서의 Assets 에 대한 관리가 가능하며 Assets spending transaction 에 대한 서명 주체 또한 명확하게 구분됩니다. 아래 예제와 같은 방법으로 Assets 에 Multisig 를 적용할 수 있습니다.

@Test
public void testTransferAssetMultisig() throws Exception {
String privateKey1 = "5JpxHsHWAFDJmGhQTypRf62s4UQYf59a2BnzDD2P7YxhXywraNF";
String privateKey2 = "5JMH6KfwymcUXo1FgLJS2hC8NwicLWwTMVdHkiEgkwzvuymbast";
String redeemScript = "5241047f78d43ff2db4c6b5e46bbc7ee6a9a253b1e66c2aece8422bd1412c6997acd8cf8561dc5ff3dbaa8a4edd94cd270204db833e0db3d6ee53276736b30184fe57741049de5e58cb6f2a3e059958a69934618b6d02277926b2d3fcf5ad8b387ffc54119ff3bd8369c893455ff7f06e1304252ccd19efb3198f1f8feb94710173123fcdd4104bc61f87c32d0262c81a0fe95979fc54133c1c97f1da7882e586728fa0e7743863841a0a09e02ac1e13ac022e0d66ac84a42aae90c9ef96866e663234dceab49353ae";
List<String> prikeys = new ArrayList<String>();
prikeys.add(privateKey1);
prikeys.add(privateKey2);
String assetID = "AKdiAtZGMuEUnZXm9tA3TPN6YJHzUarxRn";
long assetAmount = 114;
String to = "akDQMunRtK15fVpBR83Jnv4ezQLtyerTJTB";
long fee = Math.convertToSatoshi("0.0001");
String rawTx = coloringEngine.transferMultisigAsset(prikeys, redeemScript, assetID, assetAmount, to, fee);
assertNotNull(rawTx);
System.out.println(rawTx);
assertNotNull(TransactionUtil.getTransactionHash(rawTx));
coinstackClient.sendTransaction(rawTx);
}