Skip to content

Commit 38d524c

Browse files
committed
fix(consensus): fix error invalid merkle root on pre-tip-signing replay
Symptom: nodes that resumed syncing from local state could fail checkpoint validation with an invalid merkle root while replaying pre-TIPSigning blocks after restart. Cause: the non-TIP signing transaction replay path rebuilt signer data from raw receipts, but raw receipts may not carry TxHash metadata. As a result, signing transactions could be dropped and checkpoint rewards recalculated with totalSigner=0. Fix: match non-TIP signing transactions to receipts by transaction index when receipt metadata is unavailable, keep the failed-receipt filter intact, and add regression tests covering raw-receipt replay.
1 parent 146252a commit 38d524c

3 files changed

Lines changed: 262 additions & 13 deletions

File tree

consensus/XDPoS/XDPoS.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@ func (x *XDPoS) VerifyHeaders(chain consensus.ChainReader, headers []*types.Head
234234
}
235235
}
236236

237-
238237
if v1headers != nil {
239238
x.EngineV1.VerifyHeaders(chain, v1headers, v1fullVerifies, abort, results)
240239
}
@@ -514,21 +513,18 @@ Caching
514513
// Cache signing transaction data into BlockSingers cache object
515514
func (x *XDPoS) CacheNoneTIPSigningTxs(header *types.Header, txs []*types.Transaction, receipts []*types.Receipt) []*types.Transaction {
516515
signTxs := []*types.Transaction{}
517-
for _, tx := range txs {
516+
for txIndex, tx := range txs {
518517
if tx.IsSigningTransaction() {
519-
var b uint64
520-
for _, r := range receipts {
521-
if r.TxHash == tx.Hash() {
522-
if len(r.PostState) > 0 {
523-
b = types.ReceiptStatusSuccessful
524-
} else {
525-
b = r.Status
526-
}
527-
break
528-
}
518+
receipt := findTransactionReceipt(txIndex, tx.Hash(), receipts)
519+
if receipt == nil {
520+
continue
529521
}
530522

531-
if b == types.ReceiptStatusFailed {
523+
status := receipt.Status
524+
if len(receipt.PostState) > 0 {
525+
status = types.ReceiptStatusSuccessful
526+
}
527+
if status == types.ReceiptStatusFailed {
532528
continue
533529
}
534530

@@ -542,6 +538,21 @@ func (x *XDPoS) CacheNoneTIPSigningTxs(header *types.Header, txs []*types.Transa
542538
return signTxs
543539
}
544540

541+
func findTransactionReceipt(txIndex int, txHash common.Hash, receipts []*types.Receipt) *types.Receipt {
542+
if txIndex < len(receipts) {
543+
receipt := receipts[txIndex]
544+
if receipt != nil && (receipt.TxHash == (common.Hash{}) || receipt.TxHash == txHash) {
545+
return receipt
546+
}
547+
}
548+
for _, receipt := range receipts {
549+
if receipt != nil && receipt.TxHash == txHash {
550+
return receipt
551+
}
552+
}
553+
return nil
554+
}
555+
545556
// Cache
546557
func (x *XDPoS) CacheSigningTxs(hash common.Hash, txs []*types.Transaction) []*types.Transaction {
547558
signTxs := []*types.Transaction{}

consensus/XDPoS/XDPoS_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package XDPoS
22

33
import (
4+
"math/big"
45
"testing"
56

7+
"github.com/XinFinOrg/XDPoSChain/common"
68
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
9+
"github.com/XinFinOrg/XDPoSChain/core/types"
710
"github.com/XinFinOrg/XDPoSChain/params"
811
"github.com/stretchr/testify/assert"
912
)
@@ -16,3 +19,88 @@ func TestAdaptorShouldShareDbWithV1Engine(t *testing.T) {
1619
assert := assert.New(t)
1720
assert.Equal(engine.EngineV1.GetDb(), engine.GetDb())
1821
}
22+
23+
func TestCacheNoneTIPSigningTxsSupportsRawReceiptsWithoutTxHash(t *testing.T) {
24+
database := rawdb.NewMemoryDatabase()
25+
config := params.TestXDPoSMockChainConfig
26+
engine := New(config, database)
27+
28+
signingTx := types.NewTransaction(
29+
0,
30+
common.BlockSignersBinary,
31+
big.NewInt(0),
32+
200000,
33+
big.NewInt(0),
34+
append(common.Hex2Bytes(common.HexSignMethod), make([]byte, 64)...),
35+
)
36+
normalTx := types.NewTransaction(
37+
1,
38+
common.Address{0x1},
39+
big.NewInt(0),
40+
21000,
41+
big.NewInt(0),
42+
nil,
43+
)
44+
receipts := []*types.Receipt{
45+
{Status: types.ReceiptStatusSuccessful},
46+
{Status: types.ReceiptStatusSuccessful},
47+
}
48+
49+
cached := engine.CacheNoneTIPSigningTxs(&types.Header{Number: big.NewInt(1)}, []*types.Transaction{signingTx, normalTx}, receipts)
50+
51+
assert.Len(t, cached, 1)
52+
assert.Equal(t, signingTx.Hash(), cached[0].Hash())
53+
}
54+
55+
func TestCacheNoneTIPSigningTxsSkipsFailedSigningReceiptByIndex(t *testing.T) {
56+
database := rawdb.NewMemoryDatabase()
57+
config := params.TestXDPoSMockChainConfig
58+
engine := New(config, database)
59+
60+
signingTx := types.NewTransaction(
61+
0,
62+
common.BlockSignersBinary,
63+
big.NewInt(0),
64+
200000,
65+
big.NewInt(0),
66+
append(common.Hex2Bytes(common.HexSignMethod), make([]byte, 64)...),
67+
)
68+
receipts := []*types.Receipt{{Status: types.ReceiptStatusFailed}}
69+
70+
cached := engine.CacheNoneTIPSigningTxs(&types.Header{Number: big.NewInt(1)}, []*types.Transaction{signingTx}, receipts)
71+
72+
assert.Empty(t, cached)
73+
}
74+
75+
func TestCacheNoneTIPSigningTxsWithRawReceiptRoundTrip(t *testing.T) {
76+
database := rawdb.NewMemoryDatabase()
77+
config := params.TestXDPoSMockChainConfig
78+
engine := New(config, database)
79+
blockHash := common.HexToHash("0x1234")
80+
blockNumber := uint64(1)
81+
82+
signingTx := types.NewTransaction(
83+
0,
84+
common.BlockSignersBinary,
85+
big.NewInt(0),
86+
200000,
87+
big.NewInt(0),
88+
append(common.Hex2Bytes(common.HexSignMethod), make([]byte, 64)...),
89+
)
90+
receipts := []*types.Receipt{{
91+
Status: types.ReceiptStatusSuccessful,
92+
CumulativeGasUsed: 200000,
93+
TxHash: signingTx.Hash(),
94+
}}
95+
96+
rawdb.WriteReceipts(database, blockHash, blockNumber, receipts)
97+
rawReceipts := rawdb.ReadRawReceipts(database, blockHash, blockNumber)
98+
99+
assert.Len(t, rawReceipts, 1)
100+
assert.Equal(t, common.Hash{}, rawReceipts[0].TxHash)
101+
102+
cached := engine.CacheNoneTIPSigningTxs(&types.Header{Number: big.NewInt(int64(blockNumber))}, []*types.Transaction{signingTx}, rawReceipts)
103+
104+
assert.Len(t, cached, 1)
105+
assert.Equal(t, signingTx.Hash(), cached[0].Hash())
106+
}

contracts/utils_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ import (
2727
"github.com/XinFinOrg/XDPoSChain/accounts/abi/bind"
2828
"github.com/XinFinOrg/XDPoSChain/accounts/abi/bind/backends"
2929
"github.com/XinFinOrg/XDPoSChain/common"
30+
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS"
3031
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
3132
"github.com/XinFinOrg/XDPoSChain/contracts/blocksigner"
33+
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
3234
"github.com/XinFinOrg/XDPoSChain/core/types"
3335
"github.com/XinFinOrg/XDPoSChain/crypto"
3436
"github.com/XinFinOrg/XDPoSChain/params"
37+
"github.com/XinFinOrg/XDPoSChain/trie"
3538
)
3639

3740
var (
@@ -53,6 +56,50 @@ func getCommonBackend() *backends.SimulatedBackend {
5356
return backend
5457
}
5558

59+
type rewardReplayChain struct {
60+
config *params.ChainConfig
61+
current *types.Header
62+
headers map[uint64]*types.Header
63+
blocks map[uint64]*types.Block
64+
}
65+
66+
func (c *rewardReplayChain) Config() *params.ChainConfig {
67+
return c.config
68+
}
69+
70+
func (c *rewardReplayChain) CurrentHeader() *types.Header {
71+
return c.current
72+
}
73+
74+
func (c *rewardReplayChain) GetHeader(hash common.Hash, number uint64) *types.Header {
75+
header := c.headers[number]
76+
if header == nil || header.Hash() != hash {
77+
return nil
78+
}
79+
return header
80+
}
81+
82+
func (c *rewardReplayChain) GetHeaderByNumber(number uint64) *types.Header {
83+
return c.headers[number]
84+
}
85+
86+
func (c *rewardReplayChain) GetHeaderByHash(hash common.Hash) *types.Header {
87+
for _, header := range c.headers {
88+
if header.Hash() == hash {
89+
return header
90+
}
91+
}
92+
return nil
93+
}
94+
95+
func (c *rewardReplayChain) GetBlock(hash common.Hash, number uint64) *types.Block {
96+
block := c.blocks[number]
97+
if block == nil || block.Hash() != hash {
98+
return nil
99+
}
100+
return block
101+
}
102+
56103
func TestSendTxSign(t *testing.T) {
57104
accounts := []common.Address{acc2Addr, acc3Addr, acc4Addr}
58105
keys := []*ecdsa.PrivateKey{acc2Key, acc3Key, acc4Key}
@@ -116,6 +163,109 @@ func TestSendTxSign(t *testing.T) {
116163
}
117164
}
118165

166+
func TestGetRewardForCheckpointReplaysSigningTxsFromRawReceipts(t *testing.T) {
167+
database := rawdb.NewMemoryDatabase()
168+
config := params.TestXDPoSMockChainConfig
169+
engine := XDPoS.New(config, database)
170+
171+
checkpointExtra := append(bytes.Repeat([]byte{0x00}, utils.ExtraVanity), acc1Addr.Bytes()...)
172+
checkpointExtra = append(checkpointExtra, make([]byte, utils.ExtraSeal)...)
173+
174+
checkpointHeader := types.NewBlock(&types.Header{
175+
Number: big.NewInt(14),
176+
Extra: checkpointExtra,
177+
}, nil, nil, nil, trie.NewStackTrie(nil))
178+
block15 := types.NewBlock(&types.Header{
179+
Number: big.NewInt(15),
180+
ParentHash: checkpointHeader.Hash(),
181+
}, nil, nil, nil, trie.NewStackTrie(nil))
182+
block16 := types.NewBlock(&types.Header{
183+
Number: big.NewInt(16),
184+
ParentHash: block15.Hash(),
185+
}, nil, nil, nil, trie.NewStackTrie(nil))
186+
187+
signer := types.MakeSigner(config, big.NewInt(17))
188+
signingTx1, err := types.SignTx(CreateTxSign(big.NewInt(15), block15.Hash(), 0, common.BlockSignersBinary), signer, acc1Key)
189+
if err != nil {
190+
t.Fatalf("failed to sign first replay tx: %v", err)
191+
}
192+
receipts := []*types.Receipt{
193+
{Status: types.ReceiptStatusSuccessful, CumulativeGasUsed: 200000, TxHash: signingTx1.Hash()},
194+
}
195+
replayBlock := types.NewBlock(&types.Header{
196+
Number: big.NewInt(17),
197+
ParentHash: block16.Hash(),
198+
}, []*types.Transaction{signingTx1}, nil, receipts, trie.NewStackTrie(nil))
199+
rawdb.WriteReceipts(database, replayBlock.Hash(), replayBlock.NumberU64(), receipts)
200+
201+
rawReceipts := rawdb.ReadRawReceipts(database, replayBlock.Hash(), replayBlock.NumberU64())
202+
if len(rawReceipts) != 1 {
203+
t.Fatalf("unexpected raw receipt count: have %d want 1", len(rawReceipts))
204+
}
205+
if rawReceipts[0].TxHash != (common.Hash{}) {
206+
t.Fatalf("expected raw receipt without TxHash metadata, got %s", rawReceipts[0].TxHash)
207+
}
208+
precheckCached := engine.CacheNoneTIPSigningTxs(replayBlock.Header(), replayBlock.Transactions(), rawReceipts)
209+
if len(precheckCached) != 1 {
210+
t.Fatalf("unexpected cached signing tx count from raw receipts: have %d want 1", len(precheckCached))
211+
}
212+
if from := precheckCached[0].From(); from == nil || *from != acc1Addr {
213+
t.Fatalf("unexpected signer recovered from replay tx: have %v want %s", from, acc1Addr)
214+
}
215+
if got := common.BytesToHash(precheckCached[0].Data()[len(precheckCached[0].Data())-32:]); got != block15.Hash() {
216+
t.Fatalf("unexpected first replay target hash: have %s want %s", got, block15.Hash())
217+
}
218+
masternodes := engine.GetMasternodesFromCheckpointHeader(checkpointHeader.Header())
219+
if len(masternodes) != 1 || masternodes[0] != acc1Addr {
220+
t.Fatalf("unexpected checkpoint masternodes: have %v want [%s]", masternodes, acc1Addr)
221+
}
222+
223+
checkpointBlock := types.NewBlock(&types.Header{
224+
Number: big.NewInt(18),
225+
ParentHash: replayBlock.Hash(),
226+
}, nil, nil, nil, trie.NewStackTrie(nil))
227+
engine = XDPoS.New(config, database)
228+
chain := &rewardReplayChain{
229+
config: config,
230+
current: checkpointBlock.Header(),
231+
headers: map[uint64]*types.Header{
232+
14: checkpointHeader.Header(),
233+
15: block15.Header(),
234+
16: block16.Header(),
235+
17: replayBlock.Header(),
236+
18: checkpointBlock.Header(),
237+
},
238+
blocks: map[uint64]*types.Block{
239+
15: block15,
240+
16: block16,
241+
17: replayBlock,
242+
18: checkpointBlock,
243+
},
244+
}
245+
if _, ok := engine.GetCachedSigningTxs(replayBlock.Hash()); ok {
246+
t.Fatal("expected empty signing cache before restart replay")
247+
}
248+
249+
totalSigner := uint64(0)
250+
signers, err := GetRewardForCheckpoint(engine, chain, checkpointBlock.Header(), 2, &totalSigner)
251+
if err != nil {
252+
t.Fatalf("GetRewardForCheckpoint returned error: %v", err)
253+
}
254+
if cached, ok := engine.GetCachedSigningTxs(replayBlock.Hash()); !ok || len(cached) != 1 {
255+
t.Fatalf("expected replay block signing txs to be cached during reward replay, got ok=%v len=%d", ok, len(cached))
256+
}
257+
if totalSigner != 1 {
258+
t.Fatalf("unexpected total signer count: have %d want 1", totalSigner)
259+
}
260+
rewardLog := signers[acc1Addr]
261+
if rewardLog == nil {
262+
t.Fatalf("expected signer %s to be reconstructed from replay", acc1Addr)
263+
}
264+
if rewardLog.Sign != 1 {
265+
t.Fatalf("unexpected signer count for %s: have %d want 1", acc1Addr, rewardLog.Sign)
266+
}
267+
}
268+
119269
// Generate random string.
120270
func randomHash() common.Hash {
121271
letterBytes := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"

0 commit comments

Comments
 (0)