Stacks Raw Tx Breakdown

Table of Contents

1. Stacks 交易

在 Stacks 区块链中,通过 RPC /v2/transactions 可以把签名后的 Tx 提交到链上。本文介绍签名打包 Stacks 交易的细节。

2. Tx 解析实例:原生币转帐

测试网上交易 0x605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883 的功能是原生币 STX 转帐:

ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA       ---- 0.000123 STX --->      STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z

这个交易是通过下面方式提交到 Stacks 测试网的(注:要在 JS 代码中提交交易上链可直接使用 stacks.js 库中的 broadcastTransaction 函数,这里仅仅为了演示 RPC 的使用):

$ xxd raw_tx.bin                                        # 查看交易原始文件 raw_tx.bin 的内容
00000000: 8080 0000 0004 0089 480e 8142 160c 42ad  ........H..B..B.
00000010: 05b5 aea9 388a bcba 56e5 1c00 0000 0000  ....8...V.......
00000020: 0000 0100 0000 0000 0008 b900 00fa c7b6  ................
00000030: f6a9 7d51 1d89 d72e 1fa4 9662 1c6d 95ee  ..}Q.......b.m..
00000040: 77d2 12a0 57a2 3669 adc7 6ff2 5d69 f93d  w...W.6i..o.]i.=
00000050: 90c5 7d16 c360 8f7a 603f 3f55 880d db25  ..}..`.z`??U...%
00000060: 6034 72ca 8476 f2cc 98aa ccb6 da03 0200  `4r..v..........
00000070: 0000 0000 051a 2a30 0cab e023 89be 4e1b  ......*0...#..N.
00000080: 8958 d448 31f5 fa98 754f 0000 0000 0000  .X.H1...uO......
00000090: 007b 7465 7374 206d 656d 6f00 0000 0000  .{test memo.....
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
000000b0: 0000 0000                                ....
$ curl --verbose -L \
  -X POST \
  -H 'Content-Type: application/octet-stream' \
  --data-binary @raw_tx.bin \
  'https://api.testnet.hiro.so/v2/transactions'         # 提交交易到链上
"605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883"

其中交易原始文件 raw_tx.bin 是通过节 4.1 中的代码生成的,我们把它的内容转换为 Hex String 形式:

8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000100000000000008b90000fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da03020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000

上面的数据可以分解为下面这种更清晰的表达方式:

 version number                           80                                                                     主网为 00,测试网为 80                                                   
 chain id                                 80000000                                                               主网为 00000001,测试网为 80000000                                       
                
                
                
                
                
                
                
                
 authorization  
                
                
                
                
                
                
                
                
 auth type               04                                                                     04 表示 standard,05 表示 sponsored                                      
 hash mode               00                                                                     00/01/02/03 分别表示 P2PKH/P2SH/P2WPKH-P2SH/P2WSH-P2SH                   
 public key hash         89480e8142160c42ad05b5aea9388abcba56e51c                               发送者公钥的哈希 sha256(ripemd160(pubkey))                               
 nonce                   0000000000000001                                                       发送者地址关联的 nonce 值                                                
 fee                     00000000000008b9                                                       手续费。这里是 2233 micro-STX                                            
 pub key encoding        00                                                                     00 表示 compressed 公钥,01 表示 uncompressed 公钥                       
            
            
 ECDSA sign 
            
            
 v          00                                                                     recovery ID                                                              
 r          fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d       签名 r 值                                                                
 s          69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da       签名 s 值                                                                
 anchor mode                              03                                                                     01/02/03 分别表示 anchored block/microblock/leader can choose            
                
 post-condition 
                
 post-condition mode     02                                                                     可选值有 01/02,这里不介绍                                               
 post-condition length   00000000                                                               post-condition 长度                                                      
                
                
                
                
                
 tx payload     
                
                
                
                
                
 payload type ID         00                                                                     可选值有 00/01/02/03/04/05,其中 00 表示 token-transfer                  
 recipient type          05                                                                     接收者类型。05 表示 recipient address,06 表示 contract recipient        
 address version         1a                                                                     地址版本。1a 表示测试网 P2PKH,16 表示主网 P2PKH                         
 20-byte hash            2a300cabe02389be4e1b8958d44831f5fa98754f                               接收者公钥哈希,从地址 STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z 解码得到 
 transfer amount         000000000000007b                                                       转帐金额。这里是 123 micro-STX                                           
 memo (34 bytes)         74657374206d656d6f00000000000000000000000000000000000000000000000000   转帐备注。这里是 test memo 的 hexadecimal 编码。如 74 编码字符 t         

2.1. 签名计算

上一节的例子中,ECDSA 的签名 rs 值(即 fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da)是如何计算的呢?

首先,要计算“待签名数据”,其过程如下:

  1. 把 nonce 值、fee 以及签名数据全部设置为 0,即得到:

     8080000000040089480e8142160c42ad05b5aea9388abcba56e51c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000
    
  2. 把上面结果计算 SHA512_256 哈希,即得到:855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec
  3. 上面数据的后面再加上 [auth type][fee][nonce],即得到“待签名数据”:855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec0400000000000008b90000000000000001

然后,进行 ECDSA 签名,对“待签名数据”所使用的哈希函数为 SHA512_256(它是 SHA512 的截断版本),这一步的细节可以参考节 4.2,最终可以得到结果:

r=fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d
s=69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da

3. Tx 解析实例:SIP10 代币转帐

合约 STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z.ambitious-olive-capybara 是 Stacks 测试网上的 SIP10 代币,交易 0x3bd122ed6e2c8db4da436a9ae3a36ce465337ccd383a9a546193eaa81f6682a5 的功能就是对这个 SIP10 代币进行转账。

交易 0x3bd122ed6e2c8db4da436a9ae3a36ce465337ccd383a9a546193eaa81f6682a5 的原始 Tx 数据(它由节 4.3 的代码产生)的 Hex String 形式为:

8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000200000000000001050000d58550986a2040495ca86b2ca7c0fd58e9b62f53f27848bf6a07c1568d769bdb6e9b4ad52cb2f5e390ad8d7d0b1f08a03fbb94520eaf98c9736960c080849b4d030200000000021a2a300cabe02389be4e1b8958d44831f5fa98754f18616d626974696f75732d6f6c6976652d6361707962617261087472616e73666572000000040100000000000000000000000000000078051a89480e8142160c42ad05b5aea9388abcba56e51c051a2a300cabe02389be4e1b8958d44831f5fa98754f0a02000000137369703130207472616e736665722074657374

上面的数据可以分解为下面这种更清晰的表达方式:

 version number                           80                                                                 主网为 00,测试网为 80                                        
 chain id                                 80000000                                                           主网为 00000001,测试网为 80000000                            
                
                
                
                
                
                
                
                
 authorization  
                
                
                
                
                
                
                
                
 auth type               04                                                                 04 表示 standard,05 表示 sponsored                           
 hash mode               00                                                                 00/01/02/03 分别表示 P2PKH/P2SH/P2WPKH-P2SH/P2WSH-P2SH        
 public key hash         89480e8142160c42ad05b5aea9388abcba56e51c                           发送者公钥的哈希 sha256(ripemd160(pubkey))                    
 nonce                   0000000000000002                                                   发送者地址关联的 nonce 值                                     
 fee                     0000000000000105                                                   手续费。这里是 261 micro-STX                                  
 pub key encoding        00                                                                 00 表示 compressed 公钥,01 表示 uncompressed 公钥            
            
            
 ECDSA sign 
            
            
 v          00                                                                 recovery ID                                                   
 r          d58550986a2040495ca86b2ca7c0fd58e9b62f53f27848bf6a07c1568d769bdb   签名 r 值                                                     
 s          6e9b4ad52cb2f5e390ad8d7d0b1f08a03fbb94520eaf98c9736960c080849b4d   签名 s 值                                                     
 anchor mode                              03                                                                 01/02/03 分别表示 anchored block/microblock/leader can choose 
                
 post-condition 
                
 post-condition mode     02                                                                 可选值有 01/02,这里不介绍                                    
 post-condition length   00000000                                                           post-condition 长度                                           
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
  tx payload    
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
                
 payload type ID         02                                                                 可选值有 00/01/02/03/04/05,其中 02 表示调用合约函数          
 address version         1a                                                                 地址版本。1a 表示测试网 P2PKH,16 表示主网 P2PKH              
 20-byte hash            2a300cabe02389be4e1b8958d44831f5fa98754f                           合约部署者的公钥哈希,从合约部署者地址中可以解码得到          
 contract name length    18                                                                 合约名称的长度,这里是 0x18(24)字节                         
 contract name           616d626974696f75732d6f6c6976652d6361707962617261                   合约名称 ambitious-olive-capybara 的编码                      
 function name length    08                                                                 合约函数名称的长度                                            
 function name           7472616e73666572                                                   合约函数名称 transfer 的编码                                  
 arguments length        00000004                                                           合约函数的参数个数,这里是 4 个参数                           
       
 arg 0 
       
 type            01                                                                 01 表示 Clarity type: 128-bit unsigned integer                
 value           00000000000000000000000000000078                                   表示转帐数量 120                                              
       
       
 arg 1 
       
       
 type            05                                                                 05 表示 Clarity type: standard principal                      
 addr version    1a                                                                 1a 表示测试网 P2PKH,16 表示主网 P2PKH                        
 20-byte hash    89480e8142160c42ad05b5aea9388abcba56e51c                           代币发出方的公钥哈希                                          
       
       
 arg 2 
       
       
 type            05                                                                 05 表示 Clarity type: standard principal                      
 addr version    1a                                                                 1a 表示测试网 P2PKH,16 表示主网 P2PKH                        
 20-byte hash    2a300cabe02389be4e1b8958d44831f5fa98754f                           代币接收方的公钥哈希                                          
       
       
       
 arg 3 
       
       
       
 type            0a                                                                 0a 表示 Clarity type: Some option                             
 type            02                                                                 02 表示 Clarity type: buffer                                  
 buffer length   00000013                                                           转帐备注长度                                                  
 value           7369703130207472616e736665722074657374                             转帐备注 sip10 transfer test 的编码                           

关于函数参数的 Clarity type,可以参考:https://github.com/stacksgov/sips/blob/main/sips/sip-005/sip-005-blocks-and-transactions.md#clarity-value-representation

4. 附录

4.1. 构造转帐交易(JS)

下面 JS 代码可以构造节 2 中所演示的交易:

import {bytesToHex, hexToBytes, intToBigInt, intToBytes} from '@stacks/common';
import {StacksTestnet, StacksMainnet} from "@stacks/network";
import {
    AddressHashMode, AddressVersion, AnchorMode, AuthType, createMemoString, createSingleSigSpendingCondition,
    createStacksPrivateKey, estimateTransactionFeeWithFallback, getNonce, getPublicKey, PayloadType,
    principalCV, publicKeyToString, signWithKey, StacksMessageType, StacksTransaction, TransactionVersion,
    broadcastTransaction, makeSTXTokenTransfer
} from "@stacks/transactions";
import {c32address} from "c32check";
import {sha512_256} from "@noble/hashes/sha512";
import {writeFile} from 'node:fs';

// 没有必要自己实现 buildTx 函数,因为 stacks.js 中已经有了 makeSTXTokenTransfer 可以转帐 STX
// 这里仅仅是为了演示交易的具体构造过程才自己实现
async function buildTx(network, senderKey, recipient, amount, memo) {
    const publicKey = publicKeyToString(getPublicKey(createStacksPrivateKey(senderKey)))
    console.log("publicKey", publicKey)  // 03a1328ef6068af52aea4c09f1a31627017acd2ea15a3e23df2760ff1457f77165

    const spendingCondition = createSingleSigSpendingCondition(
        AddressHashMode.SerializeP2PKH,
        publicKey,
        0,
        0
    );

    const authorization = {
        authType: AuthType.Standard,
        spendingCondition
    };

    const payload = {
        type: StacksMessageType.Payload,
        payloadType: PayloadType.TokenTransfer,
        recipient: principalCV(recipient),
        amount: intToBigInt(amount, false),
        memo: createMemoString(memo),
    };

    const transaction = new StacksTransaction(
        network.version,
        authorization,
        payload,
        undefined, // no post conditions on STX transfers (see SIP-005)
        undefined, // no post conditions on STX transfers (see SIP-005)
        AnchorMode.Any,
        network.chainId
    );

    // Estimate the fee
    // const fee = await estimateTransactionFeeWithFallback(transaction, network);
    const fee = 2233;  // 应该使用上一行代码从链上获得,这里为了演示方便直接写死
    transaction.setFee(fee);

    // Set the nonce
    const addressVersion = network.version === TransactionVersion.Mainnet ? AddressVersion.MainnetSingleSig : AddressVersion.TestnetSingleSig;
    const senderAddress = c32address(addressVersion, spendingCondition.signer);
    console.log("senderAddress", senderAddress);   // ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA
    // const nonce = await getNonce(senderAddress, network);
    const nonce = 1;  // 应该使用上一行代码从链上获得,这里为了演示方便直接写死
    transaction.setNonce(nonce);

    // Sign the transaction, set the signature on the spending condition
    const privateKey = createStacksPrivateKey(senderKey);
    const msg =
        transaction.signBegin() +
        bytesToHex(new Uint8Array([transaction.auth.authType])) +
        bytesToHex(intToBytes(fee, false, 8)) +
        bytesToHex(intToBytes(nonce, false, 8));
    console.log("msg", msg);  // 855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec0400000000000008b90000000000000001
    const msgHash = sha512_256(hexToBytes(msg));
    console.log("msgHash", bytesToHex(msgHash))  // 05733e861a2a10b4b389dd9b9cb82a348ebd442e7015f19026efa0e2d48db5ab
    const signature = signWithKey(privateKey, bytesToHex(msgHash)); // VRS signature
    console.log("signature", signature.data);  // 00fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da
    transaction.auth.spendingCondition.signature = signature;

    return transaction
}

// 构造 tx
const tx = await buildTx(
    new StacksTestnet(),  // 如果是主网交易,则改为 StacksMainnet()
    '53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a01', // 发送方私钥(注:最后多一个字节 01 表示使用对应 compressed 公钥推导发送方地址)
    'STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z', // 接收方地址
    123, // 单位为 micro-STX
    'test memo')

const rawTx = tx.serialize();
console.log("txHex", bytesToHex(rawTx)) // 8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000100000000000008b90000fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da03020000000000051a2a300cabe02389be4e1b8958d44831f5fa98754f000000000000007b74657374206d656d6f00000000000000000000000000000000000000000000000000
console.log("txId", bytesToHex(sha512_256(rawTx)));  // 605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883

writeFile('raw_tx.bin', rawTx, (err) => {
    if (err) throw err;
    console.log('The raw tx has been saved!');
});

4.1.1. TxId 计算

Stacks 中,TxId 就是签名后交易的 SHA-512/256 哈希(它是 SHA512 的截断版本)。 在上面例子中,TxId 就是 605c958a9ddcf38ac36f0f6f44c02d3d3cf5602ef3d3ad517e4756bc2d1c0883。

4.2. 计算签名(Python)

下面是计算节 2 中所演示的交易的 ECDSA 签名数据:

#!/usr/bin/env python3

import hashlib
import ecdsa                      # pip install ecdsa
from Crypto.Hash import SHA512    # pip install pycryptodome


# SHA512/256(https://eprint.iacr.org/2010/548.pdf )是 SHA512 的截断版本,截断后的哈希值长度为 256 位
class SHA512_256(SHA512.SHA512Hash):
    def __init__(self, data=None):
        super().__init__(data, truncate='256')


priv_key_hex = '53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a'
priv_key = int(priv_key_hex, 16)

msg = bytes.fromhex('855f926c9893082213fd2d8bdd100dbbe98422b918959579a29a09dc290d6dec0400000000000008b90000000000000001')  # 前面有介绍它是怎么计算的
msgHash = SHA512_256(msg).digest()                # Stacks 区块链要求待签名消息的哈希值必须是 SHA512/256 的结果
print("msgHash:                ", msgHash.hex())  # 05733e861a2a10b4b389dd9b9cb82a348ebd442e7015f19026efa0e2d48db5ab

sk: ecdsa.SigningKey = ecdsa.SigningKey.from_secret_exponent(priv_key, curve=ecdsa.SECP256k1)
# 下面生成确定性签名(RFC6979),设置计算 k 时所用的 HMAC 中的哈希函数为 sha256
# 当然我们设置计算 k 时所用的 HMAC 中的哈希函数和对 msg 采用的哈希函数一致(即也设置为 SHA512_256)也可以正常产生签名
# 这里之所以采用 sha256,是为了和 stacks.js 中的 makeSTXTokenTransfer 的设置一样
deterministic_signature: bytes = sk.sign_digest_deterministic(msgHash, hashfunc=hashlib.sha256)

r = deterministic_signature.hex()[0:64]
s = deterministic_signature.hex()[64:]
print("RFC6979 signature (r):  ", r)  # fac7b6f6a97d511d89d72e1fa496621c6d95ee77d212a057a23669adc76ff25d
print("RFC6979 signature (s):  ", s)  # 9606c26f3a82e93c9f70859fc0c0aa76acd3b7867ad5d5b748df91f425698a67

# bip62: enforce low S values
if int(s, 16) > ecdsa.SECP256k1.order // 2:
    s = hex(ecdsa.SECP256k1.order - int(s, 16))[2:]
    assert len(s) % 2 == 0
    if len(s) < 64:
        s = '0' * (64 - len(s)) + s
    assert len(s) == 64
print("Low S signature (s):    ", s)  # 69f93d90c57d16c3608f7a603f3f55880ddb25603472ca8476f2cc98aaccb6da

vk: ecdsa.VerifyingKey = sk.verifying_key
print("Public key (compressed):",
      vk.to_string('compressed').hex())  # 03a1328ef6068af52aea4c09f1a31627017acd2ea15a3e23df2760ff1457f77165
assert vk.verify_digest(deterministic_signature, msgHash)
assert vk.verify_digest(bytes.fromhex(r+s), msgHash)

4.3. 构造 SIP10 转帐交易(JS)

下面 JS 代码可以构造节 3 中所演示的交易:

import {broadcastTransaction, makeContractCall, uintCV, standardPrincipalCV, bufferCVFromString, optionalCVOf} from '@stacks/transactions';
import {StacksTestnet, StacksMainnet} from '@stacks/network';
import {bytesToHex} from "@stacks/common";

const txOptions = {
    contractAddress: 'STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z',
    contractName: 'ambitious-olive-capybara',
    functionName: 'transfer',
    functionArgs: [uintCV(120),
        standardPrincipalCV('ST24MG3M188B0RGND0PTTXA9RHAYBMNQ53GE6WRQA'),
        standardPrincipalCV('STN3035BW0HRKFJE3E4NHN2867TZN63N9XCGSM1Z'),
        optionalCVOf(bufferCVFromString('sip10 transfer test'))],
    senderKey: '53e804fec042f8fa21c6ab9e8c00d2c879aed9c8737601a8888602848471869a01',
    // attempt to fetch this contracts interface and validate the provided functionArgs
    validateWithAbi: true,
    network: new StacksTestnet(), // for mainnet, use `StacksMainnet()`
};

const tx = await makeContractCall(txOptions);
console.log("txHex", bytesToHex(tx.serialize())) // 8080000000040089480e8142160c42ad05b5aea9388abcba56e51c000000000000000200000000000001050000d58550986a2040495ca86b2ca7c0fd58e9b62f53f27848bf6a07c1568d769bdb6e9b4ad52cb2f5e390ad8d7d0b1f08a03fbb94520eaf98c9736960c080849b4d030200000000021a2a300cabe02389be4e1b8958d44831f5fa98754f18616d626974696f75732d6f6c6976652d6361707962617261087472616e73666572000000040100000000000000000000000000000078051a89480e8142160c42ad05b5aea9388abcba56e51c051a2a300cabe02389be4e1b8958d44831f5fa98754f0a02000000137369703130207472616e736665722074657374
console.log("txId", tx.txid());  // 3bd122ed6e2c8db4da436a9ae3a36ce465337ccd383a9a546193eaa81f6682a5

const broadcastResponse = await broadcastTransaction(tx);
console.log("broadcastResponse", broadcastResponse)

注:重复运行上面代码时,输出的信息会不一样,这是因为 nonce 值和 fee 会发现变化。

5. 参考

Author: cig01

Created: <2024-04-14 Sun>

Last updated: <2024-04-22 Mon>

Creator: Emacs 27.1 (Org mode 9.4)