最近參與了有關區塊鏈相關的專案,系統內部的服務會去調 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 鏈路創建交易的部分在主要第21
–29
行,它的上面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
變數裡面的v
、r
、s
、yParity
、hash
變得不一樣了。而那個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
分別使用掉了0
、1
、2
。
// 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 的那些系統分析設計概念,整個軟體工程處理問題的解法還是換湯不換藥,是不是很巧妙呢。