All Articles

하드웨어 웰렛으로 비트코인 서명 구현하기 #1


안녕하세요. 안피곤입니다.

개발자로서 블로그를 운영하며 매일 매일 글쓰기는 정말 어렵습니다. 그래서 개발 관련한 노하우나 기술라도 블로그에 자주 올리도록 노력해야겠습니다.

다음은 비트코인 트랜잭션에서 계산된 시그해시를 외부에서 서명하여 트랜잭션에 다시 추가하는 방법입니다. SDK 라이브러리를 사용하면 지갑 생성과 서명을 한번에 다 처리해줍니다. 하지만 서명만 처리하는 서버가 별도로 있거나 지갑키(서명키)가 하드웨어 월렛 또는 모바일 디바이스에 있는 경우는 대부분의 SDK에서 지원하지 않는 것 같습니다. 이번에 하드웨어 웰렛을 개발하면서 삽질한(?) 노하우를 공유합니다.


UTXO 조회하기

서명에 필요한 utxo를 테스트넷에서 가져옵니다. 가져온 utxo에서 전송에 필요한 금액 만큼만 사용하면 됩니다.

const Client = axios.create({
  baseURL: 'https://test-insight.bitpay.com/api',
  headers: { "Content-Type": "application/json" },
  timeout: 10000
});

const fromAddress = "1J5RoyfyjLBcdGc2PCnN8wkfEUJms13vv8";
const utxos = await Client.post(`/addrs/utxo`, { addrs: fromAddress }).then(r => r.data);

실제 비트코인 주소입니다. 메인넷에서 테스트로 송금하셔도 됩니다.


트랜잭션 생성하기

트랜잭션 생성에는 비트코인(bitcore-lib) 라이브러리를 사용하였습니다. 그리고 일단은 P2PKH 주소 서명만 예제 코드로 사용하였습니다. 주소의 prefix로 스크립트 유형을 알아낼 수 있습니다. 비트코인 주소 유형은 이곳 위키 를 참고하세요.

const bitcore = require('bitcore-lib');
const tx = new bitcore.Transaction();
if ( ["1", "m", "n"].includes(fromAddress.slice(0, 1)) ) { // P2PKH
  tx.from(utxos); 
} else {
  // P2PK, P2SH, P2WPKH, Etc ...
}

tx.to(to, amount.toNumber());
tx.feePerKb(20000); // TX KB별 필요한 수수료 입력
tx.change(fromAddress); // 잔금 받을 주소 입력(HD지갑이라면 change 주소 사용)

const transaction = tx.uncheckedSerialize();

주소 prefix가 1이면(테스트넷은 m or n) P2PKH 입니다.


서명할 시그해시 계산하기

const hash = tx.inputs.map((input, index) => {
  let sighash = bitcore.Transaction.Sighash.sighash(tx, 0x01, index, input.output.script);
  sighash = bitcore.encoding.BufferReader(sighash).readReverse();
  return sighash.toString("hex");
});

bitcore 라이브러리는 서명에 little-endian을 사용하고 있습니다. 그래서 계산된 hash를 다시 reverse 합니다.


트랜잭션에 서명값 추가하기

외부 서버나 디바이스에서 받아온 서명을 다시 트랜잭션에 추가합니다.

// sign 서버에 서명 요청하기
const signatures = await SignService.requestSignature(hash); 

signatures.forEach((signature, index) => {
  // signature 객체 생성
  let sig = bitcore.crypto.Signature.fromCompact(
    buffer.Buffer(signature, "hex")
  );

  // 시그해시 계산
  const input = tx.inputs[index];
  let hashbuf = bitcore.Transaction.Sighash.sighash(tx, 0x01, index, input.output.script);
  hashbuf = bitcore.encoding.BufferReader(hashbuf).readReverse();

  // 서명 검증
  const ecdsa = bitcore.crypto.ECDSA({ hashbuf, sig });
  ecdsa.set({ pubkey: ecdsa.toPublicKey() });
  if (!ecdsa.verify().verified) {
    return Errors.BAD_SIGNATURES;
  }

  // tx에 서명 추가
  const signatureObj = new bitcore.Transaction.Signature({
    publicKey: ecdsa.pubkey, 
    prevTxId: input.prevTxId,
    outputIndex: input.outputIndex,
    inputIndex: index,
    signature: sig,
    sigtype: 0x01 // SIGHASH_ALL.
  });

  tx.applySignature(signatureObj);
});

// 서명된 tx를 serialize 하기
const serializeTx = tx.serialize();


이제 마지막으로 serialized된 tx를 비트코인 노드서버로 send 하면 됩니다.

비트코인이 동작은 단순하지만 구현하는 부분에 있어서는 매우 어렵고 복잡합니다.


해피 코딩하세요~!


![](https://steemitimages.com/400x0/https://cdn.steemitimages.com/DQmQmWhMN6zNrLmKJRKhvSScEgWZmpb8zCeE2Gray1krbv6/BC054B6E-6F73-46D0-88E4-C88EB8167037.jpeg)
Published 12 Jun 2019

안피곤의 블로그입니다.