Utxo

花一个晚上看完了btc白皮书,借助AI理解了一下btc转账。

1.png

第一个框的交易指定了Owner0 将一笔UTXO(Unspent Transaction Output)指定给了Owner 1的公钥,这个时候如果Owner1想要使用这笔UTXO,则需要使用自己的私钥生成一段签名,整个网络可以验证这个签名是否正确。 结合代码看,首先将UTXO作为输入,验证UTXO签名是否正确(即Owner 0 的签名),将接收BTC的地址作为输出,使用Owner的私钥对其进行签名,再将其放到最后一个区块内即可。 基于非对称加密,可以很容易使用公钥验证一个签名是否是由正确的私钥签发的,进而使得私钥的所有人证明其所有权,

   const utxos = await getUTXOs(senderAddress);
...
    const keyPair = ECPair.fromWIF(senderPrivateKey, network);
...
    let inputAmount = BigInt(0);
    const psbt = new bitcoin.Psbt({ network });
    psbt.setVersion(2);
    psbt.setLocktime(0);

    const utxo = utxos[0];
    inputAmount += BigInt(utxo.value);

    const txid = utxo.tx_hash_big_endian;
    const rawTxHex = await getRawTxHex(txid);

    if (rawTxHex) {
        // 非 witness 输入:需要传入整个前置交易的 raw hex
        psbt.addInput({
            hash: txid,
            index: utxo.tx_output_n,
            nonWitnessUtxo: Buffer.from(rawTxHex, 'hex'),
        });
    } else {
        // 如果无法获取 raw tx,可以尝试用 witnessUtxo(仅对 segwit 输出有效)
        psbt.addInput({
            hash: txid,
            index: utxo.tx_output_n,
            witnessUtxo: {
                script: Buffer.from(utxo.script, 'hex'),
                value: Number(utxo.value), // witnessUtxo.value should be a number (satoshis)
            },
        });
    }

    psbt.addOutput({
        script: bitcoin.address.toOutputScript(recipientAddress, network),
        value: amountToSend, // must be BigInt
    });

    psbt.addOutput({
        script: bitcoin.address.toOutputScript(senderAddress, network),
        value: inputAmount - amountToSend - BigInt(500), // 找零,减去手续费(假设1000聪)
    });

    // 签名并 finalise
    const validator = (
        pubkey,
        msghash,
        signature,
    ) => ECPair.fromPublicKey(pubkey, network).verify(msghash, signature);
    psbt.signInput(0, keyPair);
    try {
        for (let i = 0; i < psbt.inputCount; i++) {
            psbt.validateSignaturesOfInput(i, validator);
            console.log(`Input ${i} signature valid`);
        }

    } catch (e) {
        console.warn('Signature validation failed or not applicable:', e.message || e);
    }

    try {
        psbt.finalizeAllInputs();
    } catch (e) {
        console.error('Failed to finalize inputs:', e.message || e);
        return;
    }

    const txHex = psbt.extractTransaction().toHex();

UTXO模型是BTC网络用来间接计算账户余额的方式,传统中心化交易依赖于一个中心实体记录每一个账户的消费,入账及余额,通过这个中心化实体用户才能知道自己的消费记录情况,用户之间是看不到对方的消费等记录的,对于去中心化的区块链来说一大要点就是透明:每个人都可以在区块上面验证某个信息是否正确,这也就要求不能依赖于中心化实体,在BTC中UTXO首次被实现。

[
  {
    tx_hash_big_endian: '9bd090ad9a846428f4d060463ca13f878fb78ab0d66d7019941fda6ae7e74f4e',
    tx_hash: '4e4fe7e76ada1f9419706dd6b08ab78f873fa13c4660d0f42864849aad90d09b',
    tx_output_n: 28,
    script: '76a9146b12dbf14da1e874dc8d962d661bd3abe5f910a588ac',
    value: 17000,
    value_hex: '4268',
    confirmations: 160,
    tx_index: 2755363202751299
  }
]

UTXO结构

UTXO全称Unspent Transaction Output,未花费交易输出,在BTC网络中每个地址(钱包)没有余额的概念,那么想要花一笔钱该如何做呢。

当想花费时需要将一笔或者多笔未花费的UTXO可以作为新交易中的输入,一笔交易可能有多个输入和多个输出,由于没有余额的概念那么自然没有找零的概念,假设我想买价值0.5BTC的东西,我有一笔1btc的UTXO,那么此时应该如何做呢。

对于不能一笔花完的开销来说,需要添加一个输出,里面放入自己钱包的公钥,完成交易后,自己钱包会多出一笔未花费的UTXO,实现了间接找零。

以链上一笔交易来说明,tx:77e6773b1cfc38bca5c6ea649ba0e0c1985dc55c00f65284362f0ad79b741ecc,使用代码对其解码,其拥有一笔输入,和多个输出,输入减去输出就是给矿工的fee。

{
  "network": "bitcoin mainnet",
  "txid": "77e6773b1cfc38bca5c6ea649ba0e0c1985dc55c00f65284362f0ad79b741ecc",
  "version": 1,
  "size_bytes": 391,
  "weight": 802,
  "vsize": 201,
  "locktime": {
    "type": "blockHeight",
    "value": 0
  },
  "vin_count": 1,
  "vout_count": 2,
  "inputs": [
    {
      "index": 0,
      "prevout": {
        "txid": "b145aad702e266d74dffa743a54639101e265b9df9fcfae3d8fce1c194a282e7",
        "vout": 2
      },
      "sequence": 4294967293,
      "rbfLikely": true,
      "scriptSigHex": "",
      "witness": [
        {
          "index": 0,
          "hex": "",
          "len": 0
        },
        {
          "index": 1,
          "hex": "48,68,2,32,99,127,45,212,211,58,92,53,118,21,122,22,113,230,97,197,89,110,44,164,32,109,37,188,205,218,99,170,193,108,162,54,2,32,122,235,76,115,81,62,230,176,199,161,166,251,224,187,129,123,29,161,18,81,102,224,26,218,140,64,193,187,6,37,207,203,1",
          "len": 71
        },
        {
          "index": 2,
          "hex": "48,68,2,32,70,149,209,74,159,228,5,72,211,191,0,158,60,171,152,217,118,153,162,205,30,30,28,118,191,252,14,30,240,182,250,120,2,32,64,136,99,29,211,184,215,38,52,63,46,87,204,194,28,210,168,97,161,180,143,43,86,151,21,97,199,117,239,108,44,221,1",
          "len": 71
        },
        {
          "index": 3,
          "hex": "82,33,3,120,197,249,69,7,142,221,73,97,2,213,147,83,214,1,126,126,212,23,234,187,117,42,58,229,164,228,155,20,44,85,166,33,2,237,87,136,14,74,15,140,231,227,169,247,56,151,44,25,1,112,203,115,84,222,17,214,109,63,136,147,52,197,220,63,62,33,2,225,38,37,255,92,147,211,55,179,187,185,201,80,106,241,81,50,45,45,115,46,219,98,200,255,88,79,42,196,92,250,252,83,174",
          "len": 105
        }
      ]
    }
  ],
  "outputs": [
    {
      "index": 0,
      "value_sats": "162510",
      "value_btc": 0.0016251,
      "scriptPubKeyHex": "0,32,123,184,245,128,42,228,70,190,78,95,125,222,56,124,161,98,79,174,224,154,134,67,168,193,111,248,125,244,35,94,37,132",
      "scriptType": "p2wsh",
      "address": "bc1q0wu0tqp2u3rtunjl0h0rsl9pvf86acy6sep63st0lp7lgg67ykzqeq89pn"
    },
    {
      "index": 1,
      "value_sats": "1610937",
      "value_btc": 0.01610937,
      "scriptPubKeyHex": "0,32,26,66,192,48,236,160,69,158,169,57,88,142,36,24,145,133,81,169,4,166,150,159,112,105,52,139,246,54,80,174,22,75",
      "scriptType": "p2wsh",
      "address": "bc1qrfpvqv8v5pzea2fetz8zgxy3s4g6jp9xj60hq6f530mrv59wze9se9a3md"
    }
  ]
}

代码: https://github.com/Chestnuts4/web3learning

创建于:Wednesday, October 15,2025
最后修改于: Thursday, October 16,2025