Skip to content

Commit 6b0405c

Browse files
authored
core/stateless: fix witness RLP backward compat with WIT/0 peers (#2068)
* core/stateless: fix witness RLP backward compat with WIT/0 peers Nodes upgraded from WIT/0 to WIT/1 were incorrectly jailing WIT/0 peers with "rlp: too few elements for stateless.ExtWitness". A 5-field ExtWitness encoding (Context, Headers, Codes, State, Keys) was accidentally introduced during an upstream geth merge, replacing the original 3-field wire format (Context, Headers, State) that WIT/0 peers still use. Introduce BorWitness as the canonical 3-field wire format. EncodeRLP now encodes using BorWitness (State only, codes excluded). DecodeRLP tries BorWitness first and falls back to ExtWitness for backward compatibility with peers that temporarily ran the extended encoding. ExtWitness is retained as the representation for the JSON debug API (ExecutionWitness, ExecutionWitnessByHash) and as the fallback decode target for the transitional period. * Add test
1 parent 6f2d79b commit 6b0405c

File tree

2 files changed

+248
-15
lines changed

2 files changed

+248
-15
lines changed

core/stateless/encoding.go

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,28 @@ import (
2424
"github.com/ethereum/go-ethereum/rlp"
2525
)
2626

27-
// ToExtWitness converts our internal witness representation to the consensus one.
27+
// BorWitness is the canonical 3-field RLP encoding used for network
28+
// transmission in Bor. The State field carries all proof data — both
29+
// contract bytecodes and MPT state trie nodes — as a flat list of byte slices.
30+
type BorWitness struct {
31+
Context *types.Header
32+
Headers []*types.Header
33+
State [][]byte
34+
}
35+
36+
// ExtWitness is a witness representation used by the JSON debug API and for
37+
// backward-compatible RLP decoding of peers that temporarily used the
38+
// extended 5-field format. It is not the canonical wire format.
39+
type ExtWitness struct {
40+
Context *types.Header
41+
Headers []*types.Header `json:"headers"`
42+
Codes []hexutil.Bytes `json:"codes"`
43+
State []hexutil.Bytes `json:"state"`
44+
Keys []hexutil.Bytes `json:"keys"`
45+
}
46+
47+
// ToExtWitness converts our internal witness representation to the ExtWitness
48+
// form used by the JSON debug API.
2849
func (w *Witness) ToExtWitness() *ExtWitness {
2950
w.lock.RLock()
3051
defer w.lock.RUnlock()
@@ -44,7 +65,7 @@ func (w *Witness) ToExtWitness() *ExtWitness {
4465
return ext
4566
}
4667

47-
// fromExtWitness converts the consensus witness format into our internal one.
68+
// fromExtWitness converts the ExtWitness format into our internal representation.
4869
func (w *Witness) fromExtWitness(ext *ExtWitness) error {
4970
w.context = ext.Context
5071
w.Headers = ext.Headers
@@ -60,25 +81,55 @@ func (w *Witness) fromExtWitness(ext *ExtWitness) error {
6081
return nil
6182
}
6283

63-
// EncodeRLP serializes a witness as RLP.
84+
// EncodeRLP serializes a witness as RLP using the canonical BorWitness 3-field
85+
// format. Only state trie nodes are encoded; contract bytecodes are not
86+
// included in the wire format.
6487
func (w *Witness) EncodeRLP(wr io.Writer) error {
65-
return rlp.Encode(wr, w.ToExtWitness())
88+
w.lock.RLock()
89+
defer w.lock.RUnlock()
90+
91+
bw := &BorWitness{
92+
Context: w.context,
93+
Headers: w.Headers,
94+
State: make([][]byte, 0, len(w.State)),
95+
}
96+
for node := range w.State {
97+
bw.State = append(bw.State, []byte(node))
98+
}
99+
return rlp.Encode(wr, bw)
66100
}
67101

68-
// DecodeRLP decodes a witness from RLP.
102+
// DecodeRLP decodes a witness from RLP. It first attempts the canonical
103+
// BorWitness 3-field format. If that fails (e.g. the peer sent the legacy
104+
// 5-field ExtWitness encoding), it falls back to ExtWitness for backward
105+
// compatibility.
106+
//
107+
// When decoding BorWitness, State items are placed into w.State and w.Codes
108+
// is left empty, since codes are not part of the BorWitness wire format.
69109
func (w *Witness) DecodeRLP(s *rlp.Stream) error {
110+
raw, err := s.Raw()
111+
if err != nil {
112+
return err
113+
}
114+
115+
// Try BorWitness first. A 5-field list will fail here with "too many
116+
// elements", routing cleanly to the ExtWitness fallback.
117+
var bw BorWitness
118+
if err := rlp.DecodeBytes(raw, &bw); err == nil {
119+
w.context = bw.Context
120+
w.Headers = bw.Headers
121+
w.Codes = make(map[string]struct{})
122+
w.State = make(map[string]struct{}, len(bw.State))
123+
for _, node := range bw.State {
124+
w.State[string(node)] = struct{}{}
125+
}
126+
return nil
127+
}
128+
129+
// Fall back to the legacy 5-field ExtWitness format.
70130
var ext ExtWitness
71-
if err := s.Decode(&ext); err != nil {
131+
if err := rlp.DecodeBytes(raw, &ext); err != nil {
72132
return err
73133
}
74134
return w.fromExtWitness(&ext)
75135
}
76-
77-
// ExtWitness is a witness RLP encoding for transferring across clients.
78-
type ExtWitness struct {
79-
Context *types.Header
80-
Headers []*types.Header `json:"headers"`
81-
Codes []hexutil.Bytes `json:"codes"`
82-
State []hexutil.Bytes `json:"state"`
83-
Keys []hexutil.Bytes `json:"keys"`
84-
}

core/stateless/encoding_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package stateless
2+
3+
import (
4+
"math/big"
5+
"testing"
6+
7+
"github.com/ethereum/go-ethereum/common/hexutil"
8+
"github.com/ethereum/go-ethereum/core/types"
9+
"github.com/ethereum/go-ethereum/rlp"
10+
)
11+
12+
// TestDecodeRLP_BorWitnessFormat verifies that a witness RLP-encoded in the
13+
// canonical 3-field BorWitness format is correctly decoded.
14+
func TestDecodeRLP_BorWitnessFormat(t *testing.T) {
15+
contextHeader := &types.Header{Number: big.NewInt(100)}
16+
parentHeader := &types.Header{Number: big.NewInt(99)}
17+
node1 := []byte("statenode1")
18+
node2 := []byte("statenode2")
19+
20+
bw := &BorWitness{
21+
Context: contextHeader,
22+
Headers: []*types.Header{parentHeader},
23+
State: [][]byte{node1, node2},
24+
}
25+
26+
encoded, err := rlp.EncodeToBytes(bw)
27+
if err != nil {
28+
t.Fatalf("failed to encode BorWitness: %v", err)
29+
}
30+
31+
var w Witness
32+
if err := rlp.DecodeBytes(encoded, &w); err != nil {
33+
t.Fatalf("DecodeRLP failed for BorWitness input: %v", err)
34+
}
35+
36+
if w.context.Number.Cmp(contextHeader.Number) != 0 {
37+
t.Errorf("context number: got %v, want %v", w.context.Number, contextHeader.Number)
38+
}
39+
if len(w.Headers) != 1 || w.Headers[0].Number.Cmp(parentHeader.Number) != 0 {
40+
t.Errorf("headers mismatch after decode")
41+
}
42+
43+
// All State items from BorWitness should land in w.State.
44+
if len(w.State) != 2 {
45+
t.Errorf("state len: got %d, want 2", len(w.State))
46+
}
47+
for _, node := range [][]byte{node1, node2} {
48+
if _, ok := w.State[string(node)]; !ok {
49+
t.Errorf("state node %q missing after decode", node)
50+
}
51+
}
52+
53+
// Codes are not part of the BorWitness wire format.
54+
if len(w.Codes) != 0 {
55+
t.Errorf("Codes should be empty after BorWitness decode, got %d entries", len(w.Codes))
56+
}
57+
}
58+
59+
// TestDecodeRLP_ExtWitnessFormat verifies backward compatibility: a witness
60+
// encoded in the legacy 5-field ExtWitness format is correctly decoded via the
61+
// fallback path, with Codes and State placed in their respective internal maps.
62+
func TestDecodeRLP_ExtWitnessFormat(t *testing.T) {
63+
contextHeader := &types.Header{Number: big.NewInt(100)}
64+
parentHeader := &types.Header{Number: big.NewInt(99)}
65+
code1 := hexutil.Bytes("contractbytecode")
66+
node1 := hexutil.Bytes("statetrienode")
67+
68+
ext := &ExtWitness{
69+
Context: contextHeader,
70+
Headers: []*types.Header{parentHeader},
71+
Codes: []hexutil.Bytes{code1},
72+
State: []hexutil.Bytes{node1},
73+
Keys: nil,
74+
}
75+
76+
encoded, err := rlp.EncodeToBytes(ext)
77+
if err != nil {
78+
t.Fatalf("failed to encode ExtWitness: %v", err)
79+
}
80+
81+
var w Witness
82+
if err := rlp.DecodeBytes(encoded, &w); err != nil {
83+
t.Fatalf("DecodeRLP failed for ExtWitness input: %v", err)
84+
}
85+
86+
if w.context.Number.Cmp(contextHeader.Number) != 0 {
87+
t.Errorf("context number: got %v, want %v", w.context.Number, contextHeader.Number)
88+
}
89+
if len(w.Headers) != 1 || w.Headers[0].Number.Cmp(parentHeader.Number) != 0 {
90+
t.Errorf("headers mismatch after decode")
91+
}
92+
93+
if len(w.Codes) != 1 {
94+
t.Errorf("Codes len: got %d, want 1", len(w.Codes))
95+
}
96+
if _, ok := w.Codes[string(code1)]; !ok {
97+
t.Errorf("code %q missing from Codes after ExtWitness decode", code1)
98+
}
99+
100+
if len(w.State) != 1 {
101+
t.Errorf("State len: got %d, want 1", len(w.State))
102+
}
103+
if _, ok := w.State[string(node1)]; !ok {
104+
t.Errorf("node %q missing from State after ExtWitness decode", node1)
105+
}
106+
}
107+
108+
// TestEncodeRLP_UsesBorWitnessFormat verifies that EncodeRLP produces the
109+
// canonical 3-field BorWitness format and that codes are excluded from it.
110+
func TestEncodeRLP_UsesBorWitnessFormat(t *testing.T) {
111+
w := &Witness{
112+
context: &types.Header{Number: big.NewInt(100)},
113+
Headers: []*types.Header{{Number: big.NewInt(99)}},
114+
Codes: map[string]struct{}{"contractcode": {}},
115+
State: map[string]struct{}{"statenode": {}},
116+
}
117+
118+
encoded, err := rlp.EncodeToBytes(w)
119+
if err != nil {
120+
t.Fatalf("EncodeRLP failed: %v", err)
121+
}
122+
123+
// The output must be decodable as BorWitness (3 fields).
124+
var bw BorWitness
125+
if err := rlp.DecodeBytes(encoded, &bw); err != nil {
126+
t.Fatalf("encoded output is not a valid BorWitness: %v", err)
127+
}
128+
129+
// Only State nodes should be present — codes are not encoded.
130+
if len(bw.State) != 1 || string(bw.State[0]) != "statenode" {
131+
t.Errorf("BorWitness.State = %v, want [statenode]", bw.State)
132+
}
133+
134+
// The output must NOT be decodable as ExtWitness (5 fields).
135+
var ext ExtWitness
136+
if err := rlp.DecodeBytes(encoded, &ext); err == nil {
137+
t.Error("expected ExtWitness decode to fail for BorWitness output, but it succeeded")
138+
}
139+
}
140+
141+
// TestRoundtrip_BorWitnessFormat verifies the full encode→decode cycle using
142+
// EncodeRLP and DecodeRLP (BorWitness path).
143+
func TestRoundtrip_BorWitnessFormat(t *testing.T) {
144+
original := &Witness{
145+
context: &types.Header{Number: big.NewInt(200)},
146+
Headers: []*types.Header{{Number: big.NewInt(199)}, {Number: big.NewInt(198)}},
147+
Codes: map[string]struct{}{"code": {}},
148+
State: map[string]struct{}{"node1": {}, "node2": {}},
149+
}
150+
151+
encoded, err := rlp.EncodeToBytes(original)
152+
if err != nil {
153+
t.Fatalf("EncodeRLP failed: %v", err)
154+
}
155+
156+
var decoded Witness
157+
if err := rlp.DecodeBytes(encoded, &decoded); err != nil {
158+
t.Fatalf("DecodeRLP failed: %v", err)
159+
}
160+
161+
if decoded.context.Number.Cmp(original.context.Number) != 0 {
162+
t.Errorf("context number: got %v, want %v", decoded.context.Number, original.context.Number)
163+
}
164+
if len(decoded.Headers) != len(original.Headers) {
165+
t.Errorf("headers count: got %d, want %d", len(decoded.Headers), len(original.Headers))
166+
}
167+
168+
// State nodes are preserved across the roundtrip.
169+
for node := range original.State {
170+
if _, ok := decoded.State[node]; !ok {
171+
t.Errorf("state node %q missing after roundtrip", node)
172+
}
173+
}
174+
if len(decoded.State) != len(original.State) {
175+
t.Errorf("state len: got %d, want %d", len(decoded.State), len(original.State))
176+
}
177+
178+
// Codes are not in the BorWitness wire format and will be empty after decode.
179+
if len(decoded.Codes) != 0 {
180+
t.Errorf("Codes should be empty after BorWitness roundtrip, got %d", len(decoded.Codes))
181+
}
182+
}

0 commit comments

Comments
 (0)