Bitcoin Transaction -- Part One

955 阅读7分钟

Transaction是什么?

我们随意看一个最简单的Transaction,看看什么是Transaction。在block exploper中经过简单的查询一个经典的txid 7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18,可以看到,他到底是什么。

0100000001524d288f25cada331c298e21995ad070e1d1a0793e818f2f7cfb5f6122ef3e71000000008c493046022100a59e516883459706ac2e6ed6a97ef9788942d3c96a0108f2699fa48d9a5725d1022100f9bb4434943e87901c0c96b5f3af4e7ba7b83e12c69b1edbfe6965f933fcd17d014104e5a0b4de6c09bd9d3f730ce56ff42657da3a7ec4798c0ace2459fb007236bc3249f70170509ed663da0300023a5de700998bfec49d4da4c66288a58374626c8dffffffff0180969800000000001976a9147f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a888ac00000000

上边是一个Transaction的hex,但是到底是什么意思呢?如何将其decode,可以看的比较明白。

{
    hash 7957a35fe64f80d234d76d83a2a8f1a0d8149a41d81de548f0a65a8a999f6f18
    inputs
    {
        input
        {
            address_hash 54a28c6ba2bebdb694fe487a87e3e8ed4eab1502
            previous_output
            {
                hash 713eef22615ffb7c2f8f813e79a0d1e170d05a99218e291c33daca258f284d52
                index 0
            }
            script "[3046022100a59e516883459706ac2e6ed6a97ef9788942d3c96a0108f2699fa48d9a5725d1022100f9bb4434943e87901c0c96b5f3af4e7ba7b83e12c69b1edbfe6965f933fcd17d01] [04e5a0b4de6c09bd9d3f730ce56ff42657da3a7ec4798c0ace2459fb007236bc3249f70170509ed663da0300023a5de700998bfec49d4da4c66288a58374626c8d]"
            sequence 4294967295
        }
    }
    lock_time 0
    outputs
    {
        output
        {
            address_hash 7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8
            script "dup hash160 [7f9b1a7fb68d60c536c2fd8aeaa53a8f3cc025a8] equalverify checksig"
            value 10000000
        }
    }
    version 1
}

一个典型的bitcoin transaction由两部分组成,input和output,input是这笔transaction的输入,output是输出。首先看input,因为bitcoin是utxo model所以一个的输出是另一transaction的输出。仔细看上边的trasaction,input中有一个script,output中另外一个script,那么script是什么呢?

input中的script是unlocking script, output中的script是locking script。为什么说input中的script是unlocking script呢?因为他是解锁上一笔transaction的output。也就是说矿工在verify transaction的时候,会找到utxo(上一笔交易的output中的locking script)如果unlocking script + locking script 返回的结果是true那么这个钱就能花了。具体来看看是怎么回事。

下边是一个例子是典型的一个P2PKH Transaction的unlocking script + locking script,假设Alice在第n个transaction中,有一笔utxo,然后她在第n+1笔transaction中将其花出。那么她的unlocking script是在第n+1笔 transaction的input中,locking script是在第n的output中。这一点一定要注意一下。

unlocking + locking

在图中的PubkeyHash是Alice的公钥Hash,Sig是Alice的签名,Pubkey是Alice的公钥。所以如果手动运行一下这个script,那么得到的结果是什么呢?

执行结果

如果手动运行一下得到的结果是True。所以那么Alice就可以用这笔钱了。所以现在比较明确了为什么output中的script是locking script,因为这个transaction将这个笔钱lock在一个publicKey中,只有你证明你是这个私钥的owner你才能动这笔钱,反过来这个key的owner因为有私钥,通过签名可以解锁这笔钱。所以通过这样的方式把钱lock了起来,Locking script由此得名。Brilliant!

是不是有另一问题?

上边介绍transaction的基本的结构,那么这里有一个问题是,那么签名到底签的是什么,如果可以随意签名,例如签名一个hello world,miner如何验证呢?第二如果签名数据可以是一个任意的数据那么如果hacker or other任意修改locking script呢?本来是给A的,hacker or others给了B?这样是否可能?或者如何应对这个问题?

第一个问题,首先签名签的是transaction 或者说是transaction的一部分 or hash,这样miner就可以知道如何验证签名了。

第二问题,这就引出了sign的type。bitcoin中的sign hash Type有这几种。

SIGHASH flag Value Description 类比
ALL 0x01 Signature applies to all inputs and outputs 基本类型,from 和 to都签名
NONE 0x02 Signature applies to all inputs, none of the outputs 空白支票,收款人随便填
SINGLE 0x03 Signature applies to all inputs, none of the outputs inputs全签,output只签一个

另外还有其他的modifier

SIGHASH flag Value Description 类比
ALL_ANYONECANPAY 0x81 Signature applies to one input and all outputs 募资transaction,outputs锁定
NONE_ANYONECANPAY 0x82 Signature applies to one input, none of the outputs 空白支票,output随便
SINGLE_ANYONECANPAY 0x83 Signature applies to one input and the output with the same index number inputs全签,output只签一个

一般说最近基本的transaction sign hash type 就是ALL,看一下具体的代码中的实现。

// TODO: remove keyPair.network matching in 4.0.0
  if (keyPair.network && keyPair.network !== network)
    throw new TypeError('Inconsistent network');
  if (!inputs[vin]) throw new Error('No input at index: ' + vin);

  hashType = hashType || Transaction.SIGHASH_ALL;
  if (needsOutputs(hashType)) throw new Error('Transaction needs outputs');

  const input = inputs[vin];

  // if redeemScript was previously provided, enforce consistency
  if (
    input.redeemScript !== undefined &&
    redeemScript &&
    !input.redeemScript.equals(redeemScript)
  ) {
    throw new Error('Inconsistent redeemScript');
  }

tx_builder

    const txTmp = this.clone();

    // SIGHASH_NONE: ignore all outputs? (wildcard payee)
    if ((hashType & 0x1f) === Transaction.SIGHASH_NONE) {
      txTmp.outs = [];

      // ignore sequence numbers (except at inIndex)
      txTmp.ins.forEach((input, i) => {
        if (i === inIndex) return;

        input.sequence = 0;
      });

      // SIGHASH_SINGLE: ignore all outputs, except at the same index?
    } else if ((hashType & 0x1f) === Transaction.SIGHASH_SINGLE) {
      // https://github.com/bitcoin/bitcoin/blob/master/src/test/sighash_tests.cpp#L60
      if (inIndex >= this.outs.length) return ONE;

      // truncate outputs after
      txTmp.outs.length = inIndex + 1;

      // "blank" outputs before
      for (let i = 0; i < inIndex; i++) {
        txTmp.outs[i] = BLANK_OUTPUT;
      }

      // ignore sequence numbers (except at inIndex)
      txTmp.ins.forEach((input, y) => {
        if (y === inIndex) return;

        input.sequence = 0;
      });
    }

    // SIGHASH_ANYONECANPAY: ignore inputs entirely?
    if (hashType & Transaction.SIGHASH_ANYONECANPAY) {
      txTmp.ins = [txTmp.ins[inIndex]];
      txTmp.ins[0].script = ourScript;

      // SIGHASH_ALL: only ignore input scripts, leave all the outputs
    } else {
      // "blank" others input scripts 
      txTmp.ins.forEach(input => {
        input.script = EMPTY_SCRIPT;
      });
      txTmp.ins[inIndex].script = ourScript;
    }

    // serialize and hash
    const buffer: Buffer = Buffer.allocUnsafe(txTmp.__byteLength(false) + 4);
    buffer.writeInt32LE(hashType, buffer.length - 4);
    txTmp.__toBuffer(buffer, 0, false);

tx_build_sign

private __byteLength(_ALLOW_WITNESS: boolean): number {
    const hasWitnesses = _ALLOW_WITNESS && this.hasWitnesses();

    return (
      (hasWitnesses ? 10 : 8) +
      varuint.encodingLength(this.ins.length) +
      varuint.encodingLength(this.outs.length) +
      this.ins.reduce((sum, input) => {
        return sum + 40 + varSliceSize(input.script);
      }, 0) +
      this.outs.reduce((sum, output) => {
        return sum + 8 + varSliceSize(output.script);
      }, 0) +
      (hasWitnesses
        ? this.ins.reduce((sum, input) => {
            return sum + vectorSize(input.witness);
          }, 0)
        : 0)
    );
  }

tx, outs

上述的的代码实现中,可以看出默认的hashType是ALL,所以上边提的问题,就可以通过这样的方式解决,但是这么看hash Type中各种形式可以用于实现不同的功能。

最后看看unlocking scirpt的中签名是什么。

3045022100884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb02204b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e381301

签名是DER格式的。

  • 0x30—indicating the start of a DER sequence

  • 0x45—the length of the sequence (69 bytes)

  • 0x02—an integer value follows

  • 0x21—the length of the integer (33 bytes)

  • R—00884d142d86652a3f47ba4746ec719bbfbd040a570b1deccbb6498c75c4ae24cb

  • 0x02—another integer follows

  • 0x20—the length of the integer (32 bytes)

  • S—4b9f039ff08df09cbe9f6addac960298cad530a863ea8f53982c09db8f6e3813

  • A suffix (0x01) indicating the type of hash used (SIGHASH_ALL)