比較ERC與TRC的創建交易流程是如何做到防止重複交易

分散式系統區塊鏈

最近參與了有關區塊鏈相關的專案,系統內部的服務會去調 web3 的 API 做 ERC 或 TRC 的鏈路的 USDT 轉帳。實際下去開發後才發現在這兩個鏈路上創建交易的流程雖然有一些不一樣,但是卻都又能解決了重複交易的問題。因此趁著記憶猶新時寫了這篇:比較 ERC 與 TRC 的創建交易流程以及它們是怎麼防止同一筆交易被上鏈多次導致重複交易的。

什麼是重複交易

重複交易的概念是如果我們將相同的交易的請求原封不動提交了多次,server 方收到後就無腦的將請求處理了多次,而不是將相同的請求視為同筆。

在區塊鏈的領域上如果要創建一筆轉帳,需要將該筆轉帳交易廣播至區塊鏈上,轉帳就會成立了,例如有一筆交易叫小明轉帳給小美100塊,當區塊鏈節點收到這個交易並廣播給其它的節點後,小明轉帳給小美 100 塊這件交易的事實就成立並且永久留存在區塊鏈上了,就跟平常寫的程式碼上了 git 最後被 merge 進 master 一樣。

假如說今天網路抖動、或者程式碼中有 bug 導致不小心將小明轉帳給小美100塊這筆相同的交易廣播 2 次會發生什麼事呢,會發生重複交易導致小美最後收到 200 塊嗎?先公布答案是不會的。具體會在哪個環節被檔下來我們首先要先從區塊鏈的交易生命週期開始說起。

一筆區塊鏈轉帳交易的生命週期

不管是 ERC 或 TRC 鏈路,一筆交易從創建到上鏈總共會經過三個步驟:創建尚未被簽名的交易、對交易簽名、廣播簽名後的交易至區塊鏈網路。

創建尚未被簽名的交易

這邊我們來實際走一遍看看程式對 ERC 及 TRC 鏈路創建轉帳交易需要哪些參數及回傳值。為了實驗方便, ERC 我們使用 go-ethereum 這個套件,而 TRC 則使用 tronWeb 這個套件。

ERC

ERC 鏈路創建交易的部分在主要第2129行,它的上面11行與16行先去取得即時的 gas fee。比較特別的是第6行的nonce,它是 ERC 鏈路防止重複交易的核心,後續我們會詳細說明這個欄位。

附帶一提 ERC 鏈路創建交易時不需要填發送者地址這個參數,因為從之後的簽名就可以判斷出誰是發送者,就像我們去銀行填匯款單後,行員可以從蓋的章判斷出你叫什麼名字。

senderAddress := common.HexToAddress("0x3bBAC4C7323b24A37ab3cCC0fCF5a44EDC56C269")
receiverAddress := common.HexToAddress("0xB9338f331eD6957319f9f531c0EAA71bCe3dfD99")
amount := big.NewInt(0.0002 * params.Ether)
gasLimit := uint64(21000)

nonce, err := client.PendingNonceAt(context.Background(), senderAddress)
if err != nil {
    log.Fatal(err)
}

tipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
    log.Fatal(err)
}

feeCap, err := client.SuggestGasPrice(context.Background())
if err != nil {
    log.Fatal(err)
}

unsignedTx := types.NewTx(&types.DynamicFeeTx{
    ChainID:   chainID,
    Nonce:     nonce,
    GasTipCap: tipCap,
    GasFeeCap: feeCap,
    Gas:       gasLimit,
    To:        &receiverAddress,
    Value:     amount,
})

以下是unsignedTx變數的內容,與我們上面初始化的 struct 大同小異。

{
    "type": "0x2",
    "chainId": "0xaa36a7",
    "nonce": "0x22",
    "to": "0xb9338f331ed6957319f9f531c0eaa71bce3dfd99",
    "gas": "0x5208",
    "gasPrice": null,
    "maxPriorityFeePerGas": "0x5f5e100",
    "maxFeePerGas": "0x1cba75345",
    "value": "0xb5e620f48000",
    "input": "0x",
    "accessList": [],
    "v": "0x0",
    "r": "0x0",
    "s": "0x0",
    "yParity": "0x0",
    "hash": "0x7a77a23ffc36ec97a0f6565241c797b3ae8c794e906d8a61c754aa433f25996b"
}

TRC

TRC 鏈路在創建交易的第一步跟 ERC 有點不太一樣,只要填發送者、接收者、及金額就好,之後調用sendTrx()這個函式取得交易 ID這個交易 ID 是 TRC 鏈路防止重複交易的核心,後續我們也會拉出來獨立說明這個欄位。

至於交易 ID 是怎麼產生的呢,sendTrx()這個函式其實是對區塊鏈節點發送 http 請求的一個封裝,其對應的 http 請求是這隻,所以這個交易 ID 其實是由區塊鏈網路創建後回傳回來的,並不是在本地自己創建。

const unsignedTx = await tronWeb.transactionBuilder.sendTrx("TVDGpn4hCSzJ5nkHPLetk8KQBtwaTppnkr", 100, "TNPeeaaFB7K9cmo4uQpcU32zGK8G1NYqeL");

這是 TRC 鏈路返回的unsignedTx內容,看到那個txID就是我們要找的值。

{
    "visible": false,
    "txID": "9f62a65d0616c749643c4e2620b7877efd0f04dd5b2b4cd14004570d39858d7e",
    "raw_data": {
        "contract": [
            {
                "parameter": {
                    "value": {
                        "amount": 100,
                        "owner_address": "418840e6c55b9ada326d211d818c34a994aeced808",
                        "to_address": "41d3136787e667d1e055d2cd5db4b5f6c880563049"
                    },
                    "type_url": "type.googleapis.com/protocol.TransferContract"
                },
                "type": "TransferContract"
            }
        ],
        "ref_block_bytes": "0add",
        "ref_block_hash": "6c2763abadf9ed29",
        "expiration": 1581308685000,
        "timestamp": 1581308626092
    },
    "raw_data_hex": "0a020add22086c2763abadf9ed2940c8d5deea822e5a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a15418840e6c55b9ada326d211d818c34a994aeced808121541d3136787e667d1e055d2cd5db4b5f6c880563049186470ac89dbea822e"
}

交易簽名

創建完畢後下一步是要幫交易用發送者的私鑰做簽名,就像我們去銀行填寫完匯款單後,行員會要求要蓋章核對身份證確認我們是真正的轉帳者。

ERC

我們把unsignedTx變數與我們的私鑰privateKey直接餵進去做簽名。

signedTx, err := types.SignTx(unsignedTx, types.NewLondonSigner(chainID), privateKey)
if err != nil {
    log.Fatal(err)
}

可以看到簽完名後的signedTx變數裡面的vrsyParityhash變得不一樣了。而那個hash就是最終這筆交易的唯一 ID,做完下一步廣播上鏈後就可以拿這個hash去 etherscan 之類的區塊鏈瀏覽器查詢到交易。

{
    "type": "0x2",
    "chainId": "0xaa36a7",
    "nonce": "0x22",
    "to": "0xb9338f331ed6957319f9f531c0eaa71bce3dfd99",
    "gas": "0x5208",
    "gasPrice": null,
    "maxPriorityFeePerGas": "0x5f5e100",
    "maxFeePerGas": "0x1cba75345",
    "value": "0xb5e620f48000",
    "input": "0x",
    "accessList": [],
    "v": "0x1",
    "r": "0x49d0fea662e23b25525f9048d0d7c91c8380fefab73ee969c375a297da43f093",
    "s": "0x1396092f57176b08104020d4d5cc6dbd884203dd511a4cfd1eca207bc42b8a76",
    "yParity": "0x1",
    "hash": "0x41e992daed3a6acd6920f6762336ddbc1f09b8288530c040ef02d0bc44286ca0"
}

TRC

TRC 鏈路也是大同小異,把unsignedTx變數與我們的私鑰privateKey餵進去做簽名。

const signedTx = await tronWeb.trx.sign(unsignedTxn, privateKey);

簽名後出來的signedTx變數多了signature這個欄位,這也是各區塊鏈節點拿來驗證這筆轉帳交易是不是真的由發送者發起並簽名的。

{
    "visible": false,
    "txID":"9f62a65d0616c749643c4e2620b7877efd0f04dd5b2b4cd14004570d39858d7e",
    "raw_data":
    {
        "contract": [{<-->}],
        "ref_block_bytes": "0add",
        "ref_block_hash": "6c2763abadf9ed29",
        "expiration": 1581308685000,
        "timestamp": 1581308626092 
    },
    "raw_data_hex": "0a020add22086c2763abadf9ed2940c8d5deea822e5a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a15418840e6c55b9ada326d211d818c34a994aeced808121541d3136787e667d1e055d2cd5db4b5f6c880563049186470ac89dbea822e",
    "signature": [ "47b1f77b3e30cfbbfa41d795dd34475865240617dd1c5a7bad526f5fd89e52cd057c80b665cc2431efab53520e2b1b92a0425033baee915df858ca1c588b0a1800" ]
 }

廣播簽名後的交易至區塊鏈網路

到了這一步就可以將交易廣播上鏈了。其實背後的細節還有節點驗證交易、進入交易池等待、礦工打包區塊、廣播打包完的區塊,但這個不是今天本篇的重點,所以我們先統稱廣播上鏈就好。

ERC

ERC 鏈路調用上鏈後會得到請求是否有發生錯誤的err變數。

err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
    log.Fatal(err)
}

txID := signedTx.Hash().Hex()
0x41e992daed3a6acd6920f6762336ddbc1f09b8288530c040ef02d0bc44286ca0

TRC

TRC 鏈路的話返回值仍然是個物件,且剛剛的signedTx會被多包一層在transaction內,外加多了一個欄位result說明成功與否。

const receipt = await tronWeb.trx.sendRawTransaction(signedTx);

receipt變數內容:

{ 
    "result": true,
    "transaction":
    { 
        "visible": false,
        "txID": "9f62a65d0616c749643c4e2620b7877efd0f04dd5b2b4cd14004570d39858d7e",
        "raw_data":
        {
            "contract": [{<-->}],
            "ref_block_bytes": "0add",
            "ref_block_hash": "6c2763abadf9ed29",
            "expiration": 1581308685000,
            "timestamp": 1581308626092
        },
        "raw_data_hex": "0a020add22086c2763abadf9ed2940c8d5deea822e5a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a15418840e6c55b9ada326d211d818c34a994aeced808121541d3136787e667d1e055d2cd5db4b5f6c880563049186470ac89dbea822e",
        "signature": [ "47b1f77b3e30cfbbfa41d795dd34475865240617dd1c5a7bad526f5fd89e52cd057c80b665cc2431efab53520e2b1b92a0425033baee915df858ca1c588b0a1800" ]
    }
 }

到這邊大家應該都理解如何在 ERC 或 TRC 鏈路創建一筆交易到上鏈了。終於我們要來講到 ERC 鏈路的nonce變數及 TRC 鏈路的txID是怎麼防止重複交易。

ERC 交易中的 nonce

ERC 鏈路中的nonce說白了其實就是借用了 DB 中的版本號樂觀鎖的概念。發送者每次創建一筆新的交易時nonce都要比前一次交易多加1,如此一來就可以確保當有多筆交易使用相同的nonce時,保證只會有其中一筆被成立,防止signedTx被處理多次造成重複交易。

我們可以回到 ERC 創建鏈路的第6行的PendingNonceAt()細看實作它的程式碼,其實就是在取得發送者的總交易筆數。因為nonce是從0開始數,所以假如說發送者的地址先前已經操作過3筆交易了,那再下一筆交易的nonce就會是3,因為前面三筆的nonce分別使用掉了012

// PendingNonceAt returns the account nonce of the given account in the pending state.
// This is the nonce that should be used for the next transaction.
func (ec *Client) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) {
    var result hexutil.Uint64
    err := ec.c.CallContext(ctx, &result, "eth_getTransactionCount", account, "pending")
    return uint64(result), err
}

實驗同個 nonce 提交多次

為了發揮實驗精神,我們來試試看如果使用相同的nonce發送了多筆不同的交易會發生什麼事。以下是我們測試使用的完整的 code。

client, err := ethclient.Dial("https://rpc.sepolia.org")
if err != nil {
    log.Fatal(err)
}

chainID := big.NewInt(11155111)
secret := crypto.ToECDSAUnsafe(common.FromHex("🫣"))
receiverAddress := common.HexToAddress("0xB9338f331eD6957319f9f531c0EAA71bCe3dfD99")
amount := big.NewInt(0.0002 * params.Ether)
gasLimit := uint64(21000)

var nonce uint64 = 41
log.Printf("nonce: %v\n", nonce)

tipCap, err := client.SuggestGasTipCap(context.Background())
if err != nil {
    log.Fatal(err)
}
log.Printf("tipCap: %v\n", tipCap)

feeCap, err := client.SuggestGasPrice(context.Background())
if err != nil {
    log.Fatal(err)
}
log.Printf("feeCap: %v\n", feeCap)

unsignedTx := types.NewTx(&types.DynamicFeeTx{
    ChainID:   chainID,
    Nonce:     nonce,
    GasTipCap: tipCap,
    GasFeeCap: feeCap,
    Gas:       gasLimit,
    To:        &receiverAddress,
    Value:     amount,
})

b, err := unsignedTx.MarshalJSON()
if err != nil {
    log.Fatal(err)
}
log.Printf("unsignedTx: %s\n", b)

signedTx, err := types.SignTx(unsignedTx, types.NewLondonSigner(chainID), secret)
if err != nil {
    log.Fatal(err)
}

b, err = signedTx.MarshalJSON()
if err != nil {
    log.Fatal(err)
}
log.Printf("signedTx: %s\n", b)

err = client.SendTransaction(context.Background(), signedTx)
if err != nil {
    log.Fatal(err)
}
log.Printf("txID: %s\n", signedTx.Hash().Hex())

這是第一次執行完的 log,程式正常結束,並且可以在鏈上查到這筆交易的紀錄。

> go run .

2024/08/02 00:02:43 nonce: 41
2024/08/02 00:02:44 tipCap: +200974356
2024/08/02 00:02:44 feeCap: +11948192534
2024/08/02 00:02:44 unsignedTx: {"type":"0x2","chainId":"0xaa36a7","nonce":"0x29","to":"0xb9338f331ed6957319f9f531c0eaa71bce3dfd99","gas":"0x5208","gasPrice":null,"maxPriorityFeePerGas":"0xbfaa014","maxFeePerGas":"0x2c82af316","value":"0xb5e620f48000","input":"0x","accessList":[],"v":"0x0","r":"0x0","s":"0x0","yParity":"0x0","hash":"0x040edacc07d69e08e6e48b3cd6b24bc9c23dbf420ea20b643f8ca153e923ce61"}
2024/08/02 00:02:44 signedTx: {"type":"0x2","chainId":"0xaa36a7","nonce":"0x29","to":"0xb9338f331ed6957319f9f531c0eaa71bce3dfd99","gas":"0x5208","gasPrice":null,"maxPriorityFeePerGas":"0xbfaa014","maxFeePerGas":"0x2c82af316","value":"0xb5e620f48000","input":"0x","accessList":[],"v":"0x0","r":"0x33a27f08150c951eeb4bf91c3ad82410104991d4071ad7f7d73ebd6a636298b0","s":"0x671088c34e3ef6eec46e8334866f58632e0a8ce3906eed495b16d9703f82ac06","yParity":"0x0","hash":"0x630c5e2884e4d9124a937baef40e1d5defa55476e53ae38ce621fa36e2d9d314"}
2024/08/02 00:02:45 txID: 0x630c5e2884e4d9124a937baef40e1d5defa55476e53ae38ce621fa36e2d9d314

相同的 code 我們來執行第二次。從 log 中可以看到在最後一步廣播上鏈調用SendTransaction()時就拋錯被擋了下來,並且明確給出指示說我們指定的nonce太低了,跟前一筆發生碰撞。

> go run .

2024/08/02 00:05:26 nonce: 41
2024/08/02 00:05:28 tipCap: +200974356
2024/08/02 00:05:28 feeCap: +11687703098
2024/08/02 00:05:28 unsignedTx: {"type":"0x2","chainId":"0xaa36a7","nonce":"0x29","to":"0xb9338f331ed6957319f9f531c0eaa71bce3dfd99","gas":"0x5208","gasPrice":null,"maxPriorityFeePerGas":"0xbfaa014","maxFeePerGas":"0x2b8a4323a","value":"0xb5e620f48000","input":"0x","accessList":[],"v":"0x0","r":"0x0","s":"0x0","yParity":"0x0","hash":"0x4b4f5840394db1ac03c43187168c35c525ae003db7d87b818252dc1964c2a20d"}
2024/08/02 00:05:28 signedTx: {"type":"0x2","chainId":"0xaa36a7","nonce":"0x29","to":"0xb9338f331ed6957319f9f531c0eaa71bce3dfd99","gas":"0x5208","gasPrice":null,"maxPriorityFeePerGas":"0xbfaa014","maxFeePerGas":"0x2b8a4323a","value":"0xb5e620f48000","input":"0x","accessList":[],"v":"0x1","r":"0xafefc39eac889c29880fbc64ed2fea4bf8e42f638e4eb65d362a0c034f67ac80","s":"0x5c76194534dac5c9982dd1f430998cae65b3d676ebdd27854c54270618778b1d","yParity":"0x1","hash":"0x22028cb477d88c3ee688872ecbd8ff9c1c0923bc2de4e09c95a6af2ee6483794"}
2024/08/02 00:05:29 nonce too low: next nonce 42, tx nonce 41
exit status 1

透過實驗我們驗證了多筆交易使用相同的nonce時,保證只會有其中一筆被成立,在 ERC 鏈路中不會發生signedTx被處理多次造成重複交易。接下來我們來看看 TRC 鏈路。

TRC 交易中的 txID

TRC 鏈路就不是使用nonce樂觀鎖的概念了,取而代之的是餐廳抽號碼牌機制。還記得我們在 TRC 鏈路創建交易的第一步調用了sendTrx()這個函式取得txID嗎,它其實就是一個交易的唯一號碼牌,每次廣播上鏈時必須帶上一個未使用的號碼牌 TRC 鏈路才會允許這個請求。

這就好像我們去餐廳吃飯前要在門口先抽號碼牌等待叫號,當要入座時則將號碼牌還給服務員。如果其他人拿著我們已經使用過的號碼牌給店員,店員會指著我們的桌子說這個號碼牌使用過了,必須重新抽號碼牌一樣。

除此之外,txID跟號碼牌一樣都會過期(過號),在 TRC 鏈路中一個txID預設的有效期限是 60 秒。所以說從呼叫完sendTrx()到交易簽名、最後廣播上鏈這整個流程必須壓在 60 秒內做完。

實驗同個 txID 提交多次

講完理論的部分我們也要一如往常地發揮實驗精神,試試看如果使用相同的txID發送了多筆不同的交易會發生什麼事。我們先取得txID然後提交第一筆。

const TRON_DECIMAL_PLACE = 1e6;

const senderPrivateKey = '🫣';
const senderAddress = 'TWxZ3FtvPM8xT7wccBDdEXRsna6cECcSAn';
const receiverAddress = 'TCyppmEcoz9NrWceoTX1y5KNiN1uiKdhyd';
const amount = 8 * TRON_DECIMAL_PLACE;

const tronWeb = new TronWeb({fullHost: 'https://api.nileex.io'});

const unsignedTx = await tronWeb.transactionBuilder.sendTrx(
    receiverAddress,
    amount,
    senderAddress,
);
console.log('unsignedTx:', unsignedTx);

const signedTx = await tronWeb.trx.sign(unsignedTx, senderPrivateKey);
console.log('signedTx:', signedTx);

const receipt = await tronWeb.trx.sendRawTransaction(signedTx);
console.log('receipt:', receipt);

這是第一次執行完的 log,程式如期結束,一樣也可以在鏈上查詢得到交易紀錄。

> node send_trc.js

unsignedTx: {
  visible: false,
  txID: '5857a39d5d19511940f78d47a02b5603c786e1409005e98732c4f2bc6efdbc71',
  raw_data_hex: '0a02414a2208abdae327ccff29044090cfeef690325a68080112640a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412330a1541e63af3c9f3e6056c4ed93690435f55b1973a75ae121541210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b1880a4e80370b0faeaf69032',
  raw_data: {
    contract: [
      {
        parameter: {
          value: {
            to_address: '41210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b',
            owner_address: '41e63af3c9f3e6056c4ed93690435f55b1973a75ae',
            amount: 8000000
          },
          type_url: 'type.googleapis.com/protocol.TransferContract'
        },
        type: 'TransferContract'
      }
    ],
    ref_block_bytes: '414a',
    ref_block_hash: 'abdae327ccff2904',
    expiration: 1722531162000,
    timestamp: 1722531102000
  }
}
signedTx: {
  visible: false,
  txID: '5857a39d5d19511940f78d47a02b5603c786e1409005e98732c4f2bc6efdbc71',
  raw_data_hex: '0a02414a2208abdae327ccff29044090cfeef690325a68080112640a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412330a1541e63af3c9f3e6056c4ed93690435f55b1973a75ae121541210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b1880a4e80370b0faeaf69032',
  raw_data: {
    contract: [
      {
        parameter: {
          value: {
            to_address: '41210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b',
            owner_address: '41e63af3c9f3e6056c4ed93690435f55b1973a75ae',
            amount: 8000000
          },
          type_url: 'type.googleapis.com/protocol.TransferContract'
        },
        type: 'TransferContract'
      }
    ],
    ref_block_bytes: '414a',
    ref_block_hash: 'abdae327ccff2904',
    expiration: 1722531162000,
    timestamp: 1722531102000
  },
  signature: [
    '0293478511324ce022919ccfa593ee7b1500615385d5513f4893e25995b176412d9dc290d3149be34bd03222ad36846e76d35cf5e401d3a64ee23f04fe9c65a11C'
  ]
}
receipt: {
  result: true,
  txid: '5857a39d5d19511940f78d47a02b5603c786e1409005e98732c4f2bc6efdbc71',
  transaction: {
    visible: false,
    txID: '5857a39d5d19511940f78d47a02b5603c786e1409005e98732c4f2bc6efdbc71',
    raw_data_hex: '0a02414a2208abdae327ccff29044090cfeef690325a68080112640a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412330a1541e63af3c9f3e6056c4ed93690435f55b1973a75ae121541210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b1880a4e80370b0faeaf69032',
    raw_data: {
      contract: [
        {
          parameter: {
            value: {
              to_address: '41210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b',
              owner_address: '41e63af3c9f3e6056c4ed93690435f55b1973a75ae',
              amount: 8000000
            },
            type_url: 'type.googleapis.com/protocol.TransferContract'
          },
          type: 'TransferContract'
        }
      ],
      ref_block_bytes: '414a',
      ref_block_hash: 'abdae327ccff2904',
      expiration: 1722531162000,
      timestamp: 1722531102000
    },
    signature: [
      '0293478511324ce022919ccfa593ee7b1500615385d5513f4893e25995b176412d9dc290d3149be34bd03222ad36846e76d35cf5e401d3a64ee23f04fe9c65a11C'
    ]
  }
}

接下來實驗執行第二次。這次就直接拿剛剛前一次的signedTx來用。

const tronWeb = new TronWeb({fullHost: 'https://api.nileex.io'});

const signedTx = {
    visible: false,
    txID: '5857a39d5d19511940f78d47a02b5603c786e1409005e98732c4f2bc6efdbc71',
    raw_data_hex: '0a02414a2208abdae327ccff29044090cfeef690325a68080112640a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412330a1541e63af3c9f3e6056c4ed93690435f55b1973a75ae121541210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b1880a4e80370b0faeaf69032',
    raw_data: {
        contract: [
        {
            parameter: {
            value: {
                to_address: '41210647cc4b2d1a4dc2e80340bcfe7b3e895d5a4b',
                owner_address: '41e63af3c9f3e6056c4ed93690435f55b1973a75ae',
                amount: 8000000
            },
            type_url: 'type.googleapis.com/protocol.TransferContract'
            },
            type: 'TransferContract'
        }
        ],
        ref_block_bytes: '414a',
        ref_block_hash: 'abdae327ccff2904',
        expiration: 1722531162000,
        timestamp: 1722531102000
    },
    signature: [
        '0293478511324ce022919ccfa593ee7b1500615385d5513f4893e25995b176412d9dc290d3149be34bd03222ad36846e76d35cf5e401d3a64ee23f04fe9c65a11C'
    ]
};

const receipt = await tronWeb.trx.sendRawTransaction(signedTx);
console.log('receipt:', receipt);

可以看到 TRC 鏈路也在最後一步調用sendRawTransaction()廣播上鏈的前一刻告訴我們交易重複了,這個txID號碼牌已經被使用過請重新抽號碼牌。

> node send_trc.js

receipt: {
  code: 'DUP_TRANSACTION_ERROR',
  txid: '5857a39d5d19511940f78d47a02b5603c786e1409005e98732c4f2bc6efdbc71',
  message: '5472616e73616374696f6e20616c7265616479206578697374732e'
}

TRC 鏈路實驗的結果也是不會發生signedTx被處理多次造成重複交易。

總結

ERC 鏈路及 TRC 鏈路這兩個鏈路上創建交易的流程雖然有一些不一樣,但是各別借用了樂觀鎖nonce及抽號碼牌機制的txID來解決了避免重複交易的問題。

這也是剛好最近有幸參與到了公司內區塊鏈專案的心得:原來 web3 不過是借鏡了 web2 的那些系統分析設計概念,整個軟體工程處理問題的解法還是換湯不換藥,是不是很巧妙呢。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *