Skip to content

Commit e7318dc

Browse files
authored
Merge branch 'master' into feat/api-accounts-star-limit-max-data-size
2 parents b294891 + 16cf4e9 commit e7318dc

File tree

19 files changed

+701
-70
lines changed

19 files changed

+701
-70
lines changed

block/raw_block.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright (c) 2026 The VeChainThor developers
2+
3+
// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
4+
// file LICENSE or <https://www.gnu.org/licenses/lgpl-3.0.html>
5+
6+
package block
7+
8+
import (
9+
"github.com/ethereum/go-ethereum/rlp"
10+
11+
"github.com/vechain/thor/v2/tx"
12+
)
13+
14+
// RawBlock holds a decoded header and raw (undecoded) transaction bytes.
15+
// It implements a two-phase decode pattern: Phase 1 (DecodeRLP) validates the
16+
// block's RLP structure and decodes only the header; Phase 2 (Decode) performs
17+
// the expensive transaction deserialization.
18+
//
19+
// This allows callers to inspect the header (e.g. verify block ID, check sequence
20+
// number) before committing to the costly transaction decode, and to reject
21+
// malformed or unexpected blocks cheaply.
22+
type RawBlock struct {
23+
header *Header
24+
rawTxs rlp.RawValue
25+
txCount int
26+
size uint64
27+
}
28+
29+
// DecodeRLP implements rlp.Decoder. It performs Phase 1 of the two-phase decode:
30+
// validates the block RLP structure (outer list with exactly 2 items), decodes the
31+
// header, and counts transactions via rlp.CountValues without decoding them.
32+
func (rb *RawBlock) DecodeRLP(s *rlp.Stream) error {
33+
contentSize, err := s.List()
34+
if err != nil {
35+
return err
36+
}
37+
38+
var header Header
39+
if err := s.Decode(&header); err != nil {
40+
return err
41+
}
42+
43+
rawTxs, err := s.Raw()
44+
if err != nil {
45+
return err
46+
}
47+
48+
if err := s.ListEnd(); err != nil {
49+
return err
50+
}
51+
52+
txContent, _, err := rlp.SplitList(rawTxs)
53+
if err != nil {
54+
return err
55+
}
56+
57+
var txCount int
58+
if len(txContent) > 0 {
59+
txCount, err = rlp.CountValues(txContent)
60+
if err != nil {
61+
return err
62+
}
63+
}
64+
65+
*rb = RawBlock{
66+
header: &header,
67+
rawTxs: rawTxs,
68+
txCount: txCount,
69+
size: rlp.ListSize(contentSize),
70+
}
71+
return nil
72+
}
73+
74+
// Header returns the already-decoded block header.
75+
func (rb *RawBlock) Header() *Header {
76+
return rb.header
77+
}
78+
79+
// Decode performs Phase 2: decodes the raw transaction bytes into a fully
80+
// materialized Block. This is the expensive step that should only be called
81+
// after Phase 1 checks (ID verification, sequence checks, etc.) have passed.
82+
func (rb *RawBlock) Decode() (*Block, error) {
83+
var txs tx.Transactions
84+
if err := rlp.DecodeBytes(rb.rawTxs, &txs); err != nil {
85+
return nil, err
86+
}
87+
88+
b := &Block{
89+
header: rb.header,
90+
txs: txs,
91+
}
92+
b.cache.size.Store(rb.size)
93+
return b, nil
94+
}
95+
96+
// DecodeRawBlock decodes raw RLP bytes into a RawBlock using the two-phase
97+
// approach. Only the header is decoded; transactions remain as raw bytes until
98+
// Decode() is called.
99+
func DecodeRawBlock(data []byte) (*RawBlock, error) {
100+
var rb RawBlock
101+
if err := rlp.DecodeBytes(data, &rb); err != nil {
102+
return nil, err
103+
}
104+
return &rb, nil
105+
}

block/raw_block_test.go

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// Copyright (c) 2026 The VeChainThor developers
2+
3+
// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
4+
// file LICENSE or <https://www.gnu.org/licenses/lgpl-3.0.html>
5+
6+
package block
7+
8+
import (
9+
"math/big"
10+
"testing"
11+
12+
"github.com/ethereum/go-ethereum/rlp"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
16+
"github.com/vechain/thor/v2/thor"
17+
"github.com/vechain/thor/v2/tx"
18+
)
19+
20+
func buildTestBlock(txCount int) *Block {
21+
builder := new(Builder).
22+
ParentID(thor.Bytes32{1, 2, 3}).
23+
Timestamp(1234567890).
24+
GasLimit(10000).
25+
GasUsed(500).
26+
TotalScore(100).
27+
StateRoot(thor.Bytes32{4, 5, 6}).
28+
ReceiptsRoot(thor.Bytes32{7, 8, 9}).
29+
BaseFee(big.NewInt(1000)).
30+
Alpha([]byte{0xaa, 0xbb})
31+
32+
for range txCount {
33+
trx := tx.NewBuilder(tx.TypeLegacy).
34+
Clause(tx.NewClause(&thor.Address{})).
35+
Build()
36+
builder.Transaction(trx)
37+
}
38+
39+
return builder.Build()
40+
}
41+
42+
func TestDecodeRawBlock_RoundTrip(t *testing.T) {
43+
blk := buildTestBlock(3)
44+
45+
data, err := rlp.EncodeToBytes(blk)
46+
require.NoError(t, err)
47+
48+
rb, err := DecodeRawBlock(data)
49+
require.NoError(t, err)
50+
51+
assert.Equal(t, blk.Header().ParentID(), rb.Header().ParentID())
52+
assert.Equal(t, blk.Header().Timestamp(), rb.Header().Timestamp())
53+
assert.Equal(t, blk.Header().GasLimit(), rb.Header().GasLimit())
54+
assert.Equal(t, blk.Header().GasUsed(), rb.Header().GasUsed())
55+
assert.Equal(t, blk.Header().TotalScore(), rb.Header().TotalScore())
56+
assert.Equal(t, blk.Header().StateRoot(), rb.Header().StateRoot())
57+
assert.Equal(t, blk.Header().ReceiptsRoot(), rb.Header().ReceiptsRoot())
58+
assert.Equal(t, blk.Header().Number(), rb.Header().Number())
59+
60+
decoded, err := rb.Decode()
61+
require.NoError(t, err)
62+
63+
assert.Equal(t, len(blk.Transactions()), len(decoded.Transactions()))
64+
assert.Equal(t, blk.Header().TxsRoot(), decoded.Header().TxsRoot())
65+
assert.Equal(t, blk.Size(), decoded.Size())
66+
}
67+
68+
func TestDecodeRawBlock_NoTransactions(t *testing.T) {
69+
blk := buildTestBlock(0)
70+
71+
data, err := rlp.EncodeToBytes(blk)
72+
require.NoError(t, err)
73+
74+
rb, err := DecodeRawBlock(data)
75+
require.NoError(t, err)
76+
77+
decoded, err := rb.Decode()
78+
require.NoError(t, err)
79+
assert.Empty(t, decoded.Transactions())
80+
}
81+
82+
func TestDecodeRawBlock_HeaderIDMatchesOriginal(t *testing.T) {
83+
blk := buildTestBlock(2)
84+
85+
data, err := rlp.EncodeToBytes(blk)
86+
require.NoError(t, err)
87+
88+
rb, err := DecodeRawBlock(data)
89+
require.NoError(t, err)
90+
91+
assert.Equal(t, blk.Header().SigningHash(), rb.Header().SigningHash())
92+
}
93+
94+
func TestDecodeRawBlock_MalformedNotAList(t *testing.T) {
95+
// A plain string instead of a list
96+
data := []byte{0x83, 0x01, 0x02, 0x03}
97+
98+
_, err := DecodeRawBlock(data)
99+
assert.Error(t, err)
100+
}
101+
102+
func TestDecodeRawBlock_MalformedTruncated(t *testing.T) {
103+
blk := buildTestBlock(1)
104+
data, err := rlp.EncodeToBytes(blk)
105+
require.NoError(t, err)
106+
107+
// Truncate the data
108+
_, err = DecodeRawBlock(data[:len(data)/2])
109+
assert.Error(t, err)
110+
}
111+
112+
func TestDecodeRawBlock_MalformedExtraFields(t *testing.T) {
113+
blk := buildTestBlock(1)
114+
115+
// Encode block as a list with 3 items (header, txs, extra)
116+
data, err := rlp.EncodeToBytes([]any{
117+
blk.Header(),
118+
blk.Transactions(),
119+
[]byte("extra garbage"),
120+
})
121+
require.NoError(t, err)
122+
123+
_, err = DecodeRawBlock(data)
124+
assert.Error(t, err, "should reject block with extra fields")
125+
}
126+
127+
func TestDecodeRawBlock_MalformedTxsNotAList(t *testing.T) {
128+
blk := buildTestBlock(0)
129+
130+
// Encode with txs as a string instead of a list
131+
data, err := rlp.EncodeToBytes([]any{
132+
blk.Header(),
133+
[]byte("not a list"),
134+
})
135+
require.NoError(t, err)
136+
137+
_, err = DecodeRawBlock(data)
138+
assert.Error(t, err, "should reject block where txs field is not a list")
139+
}
140+
141+
func TestDecodeRawBlock_ViaStream(t *testing.T) {
142+
blk := buildTestBlock(2)
143+
144+
data, err := rlp.EncodeToBytes(blk)
145+
require.NoError(t, err)
146+
147+
// Decode via rlp.DecodeBytes which uses Stream internally
148+
var rb RawBlock
149+
err = rlp.DecodeBytes(data, &rb)
150+
require.NoError(t, err)
151+
152+
assert.Equal(t, blk.Header().ParentID(), rb.Header().ParentID())
153+
154+
decoded, err := rb.Decode()
155+
require.NoError(t, err)
156+
assert.Equal(t, len(blk.Transactions()), len(decoded.Transactions()))
157+
}
158+
159+
func TestDecodeRawBlock_MatchesFullDecode(t *testing.T) {
160+
blk := buildTestBlock(3)
161+
162+
data, err := rlp.EncodeToBytes(blk)
163+
require.NoError(t, err)
164+
165+
// Full decode (original path)
166+
var fullBlock Block
167+
require.NoError(t, rlp.DecodeBytes(data, &fullBlock))
168+
169+
// Two-phase decode
170+
rb, err := DecodeRawBlock(data)
171+
require.NoError(t, err)
172+
twoPhaseBlock, err := rb.Decode()
173+
require.NoError(t, err)
174+
175+
assert.Equal(t, fullBlock.Header().ParentID(), twoPhaseBlock.Header().ParentID())
176+
assert.Equal(t, fullBlock.Header().Timestamp(), twoPhaseBlock.Header().Timestamp())
177+
assert.Equal(t, fullBlock.Header().GasLimit(), twoPhaseBlock.Header().GasLimit())
178+
assert.Equal(t, fullBlock.Header().StateRoot(), twoPhaseBlock.Header().StateRoot())
179+
assert.Equal(t, fullBlock.Header().TxsRoot(), twoPhaseBlock.Header().TxsRoot())
180+
assert.Equal(t, len(fullBlock.Transactions()), len(twoPhaseBlock.Transactions()))
181+
assert.Equal(t, fullBlock.Size(), twoPhaseBlock.Size())
182+
183+
for i, ftx := range fullBlock.Transactions() {
184+
assert.Equal(t, ftx.ID(), twoPhaseBlock.Transactions()[i].ID())
185+
}
186+
}
187+
188+
func benchmarkBlockDecode(b *testing.B, txCount int) {
189+
blk := buildTestBlock(txCount)
190+
data, err := rlp.EncodeToBytes(blk)
191+
if err != nil {
192+
b.Fatal(err)
193+
}
194+
b.ResetTimer()
195+
196+
b.Run("SinglePhase", func(b *testing.B) {
197+
b.ReportAllocs()
198+
for b.Loop() {
199+
var block Block
200+
if err := rlp.DecodeBytes(data, &block); err != nil {
201+
b.Fatal(err)
202+
}
203+
}
204+
})
205+
206+
b.Run("TwoPhase_Full", func(b *testing.B) {
207+
b.ReportAllocs()
208+
for b.Loop() {
209+
rb, err := DecodeRawBlock(data)
210+
if err != nil {
211+
b.Fatal(err)
212+
}
213+
if _, err := rb.Decode(); err != nil {
214+
b.Fatal(err)
215+
}
216+
}
217+
})
218+
219+
b.Run("TwoPhase_HeaderOnly", func(b *testing.B) {
220+
b.ReportAllocs()
221+
for b.Loop() {
222+
rb, err := DecodeRawBlock(data)
223+
if err != nil {
224+
b.Fatal(err)
225+
}
226+
_ = rb.Header()
227+
}
228+
})
229+
}
230+
231+
func BenchmarkBlockDecode_0Txs(b *testing.B) { benchmarkBlockDecode(b, 0) }
232+
func BenchmarkBlockDecode_10Txs(b *testing.B) { benchmarkBlockDecode(b, 10) }
233+
func BenchmarkBlockDecode_100Txs(b *testing.B) { benchmarkBlockDecode(b, 100) }
234+
func BenchmarkBlockDecode_500Txs(b *testing.B) { benchmarkBlockDecode(b, 500) }

comm/announcement_loop.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
package comm
77

88
import (
9-
"github.com/ethereum/go-ethereum/rlp"
10-
119
"github.com/vechain/thor/v2/block"
1210
"github.com/vechain/thor/v2/comm/proto"
1311
"github.com/vechain/thor/v2/p2p/discover"
@@ -79,15 +77,26 @@ func (c *Communicator) fetchBlockByID(peer *Peer, newBlockID thor.Bytes32) {
7977
return
8078
}
8179

82-
var blk block.Block
83-
if err := rlp.DecodeBytes(result, &blk); err != nil {
84-
peer.logger.Debug("failed to decode block got by id", "err", err)
80+
rawBlk, err := block.DecodeRawBlock(result)
81+
if err != nil {
82+
peer.logger.Debug("failed to decode block structure", "err", err)
83+
return
84+
}
85+
86+
if rawBlk.Header().ID() != newBlockID {
87+
peer.logger.Debug("block ID mismatch", "expected", newBlockID, "got", rawBlk.Header().ID())
88+
return
89+
}
90+
91+
blk, err := rawBlk.Decode()
92+
if err != nil {
93+
peer.logger.Debug("failed to decode block body", "err", err)
8594
return
8695
}
8796

8897
metricFetchedBlockCount().Add(1)
8998

9099
c.newBlockFeed.Send(&NewBlockEvent{
91-
Block: &blk,
100+
Block: blk,
92101
})
93102
}

comm/handle_rpc.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,11 @@ func (c *Communicator) handleRPC(peer *Peer, msg *p2p.Msg, write func(any), txsT
117117
return errors.WithMessage(err, "decode msg")
118118
}
119119

120-
const maxBlocks = 1024
121120
const maxSize = 512 * 1024
122-
result := make([]rlp.RawValue, 0, maxBlocks)
121+
result := make([]rlp.RawValue, 0, proto.MaxBlocksFromNumber)
123122
var size thor.StorageSize
124123
chain := c.repo.NewBestChain()
125-
for size < maxSize && len(result) < maxBlocks {
124+
for size < maxSize && len(result) < proto.MaxBlocksFromNumber {
126125
b, err := chain.GetBlock(num)
127126
if err != nil {
128127
if !c.repo.IsNotFound(err) {

0 commit comments

Comments
 (0)