Skip to content

Commit 685780e

Browse files
Merge pull request #464 from kaelabbott/fix/pool-id-zero-indexer-blob
fix(fsm/indexer): handle proto3 zero-value Pool.Id / Account.Address
2 parents 38c0cfa + 544b16f commit 685780e

3 files changed

Lines changed: 54 additions & 8 deletions

File tree

fsm/indexer.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package fsm
22

33
import (
44
"bytes"
5+
"errors"
56

67
"github.com/canopy-network/canopy/lib"
78
"github.com/canopy-network/canopy/lib/codec"
@@ -314,20 +315,27 @@ func entriesByKey(entries [][]byte, keyExtractor func([]byte) (string, error)) (
314315
return out, entryMap, nil
315316
}
316317

317-
func accountEntryKey(entry []byte) (string, error) {
318-
field, err := codec.GetRawProtoField(entry, 1) // Account.address
318+
// entryKeyOrZero extracts field #1 as a map key. In proto3, absent scalar
319+
// fields represent the zero value on the wire (Pool{Id:0}, Account{Address:nil}),
320+
// so codec.ErrFieldNotFound is a legal zero-value signal, not a parse error.
321+
// Real proto-parse errors (invalid tags, buffer overruns) still surface.
322+
func entryKeyOrZero(entry []byte) (string, error) {
323+
field, err := codec.GetRawProtoField(entry, 1)
319324
if err != nil {
325+
if errors.Is(err, codec.ErrFieldNotFound) {
326+
return "", nil
327+
}
320328
return "", err
321329
}
322330
return string(field), nil
323331
}
324332

333+
func accountEntryKey(entry []byte) (string, error) {
334+
return entryKeyOrZero(entry) // Account.address
335+
}
336+
325337
func poolEntryKey(entry []byte) (string, error) {
326-
field, err := codec.GetRawProtoField(entry, 1) // Pool.id
327-
if err != nil {
328-
return "", err
329-
}
330-
return string(field), nil
338+
return entryKeyOrZero(entry) // Pool.id
331339
}
332340

333341
func changedBlobKeys(current, previous map[string][]byte) (map[string]struct{}, map[string]struct{}) {

fsm/indexer_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,33 @@ func requireEntriesAsSet(t *testing.T, got [][]byte, expected ...[]byte) {
150150
}
151151
require.Equal(t, expSet, gotSet)
152152
}
153+
154+
// TestDeltaIndexerBlobs_ZeroValuePoolAndAccount is a regression for the
155+
// proto3-zero-value bug that permanently 400s /v1/query/indexer-blobs when
156+
// chain state contains a Pool{Id:0} or Account{Address:nil}. Because proto3
157+
// omits zero-valued scalar fields from the wire, marshalling Pool{Id:0,Amount:5000}
158+
// produces bytes with only field #2 (Amount). Pre-fix, poolEntryKey called
159+
// codec.GetRawProtoField(entry, 1) and returned "field number 1 not found",
160+
// which bubbled up as lib.ErrUnmarshal and HTTP 400 on every affected height.
161+
// Post-fix, the missing field is treated as an empty (zero-value) key.
162+
func TestDeltaIndexerBlobs_ZeroValuePoolAndAccount(t *testing.T) {
163+
zeroPool := mustMarshalProto(t, &Pool{Id: 0, Amount: 5000})
164+
normalPool := mustMarshalProto(t, &Pool{Id: 32768, Amount: 25_000_000_000_000})
165+
zeroAcc := mustMarshalProto(t, &Account{Address: nil, Amount: 1})
166+
normalAcc := mustMarshalProto(t, &Account{Address: bytes.Repeat([]byte{7}, 20), Amount: 42})
167+
168+
curr := &IndexerBlob{
169+
Block: mustMarshalProto(t, &lib.BlockResult{}),
170+
Accounts: [][]byte{zeroAcc, normalAcc},
171+
Pools: [][]byte{zeroPool, normalPool},
172+
}
173+
prev := &IndexerBlob{
174+
Block: mustMarshalProto(t, &lib.BlockResult{}),
175+
Accounts: [][]byte{zeroAcc, normalAcc},
176+
Pools: [][]byte{zeroPool, normalPool},
177+
}
178+
179+
delta, err := DeltaIndexerBlobs(&IndexerBlobs{Current: curr, Previous: prev})
180+
require.NoError(t, err, "zero-value Pool.Id / Account.Address must not error")
181+
require.NotNil(t, delta)
182+
}

lib/codec/codec.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@ package codec
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"google.golang.org/protobuf/encoding/protowire"
78
"google.golang.org/protobuf/proto"
89
"google.golang.org/protobuf/types/known/anypb"
910
)
1011

12+
// ErrFieldNotFound is returned by GetRawProtoField when the requested field
13+
// number is absent from the wire bytes. In proto3, absent scalar fields
14+
// represent the zero value, so callers that expect zero-valued fields
15+
// (e.g. Pool{Id:0}, Account{Address:nil}) should detect this via errors.Is
16+
// and treat the field as empty rather than as a parse error.
17+
var ErrFieldNotFound = errors.New("field number not found")
18+
1119
// BinaryCodec is an interface model that defines the requirements for binary encoding and decoding
1220
// A binary encoder converts data into a compact, non-human-readable binary format, which is highly
1321
// efficient in terms of both storage size and speed for serialization and deserialization
@@ -107,7 +115,7 @@ func GetRawProtoField(protoBytes []byte, fieldNumber int) ([]byte, error) {
107115
offset += skipLen
108116
}
109117
}
110-
return nil, fmt.Errorf("field number %d not found", fieldNumber)
118+
return nil, fmt.Errorf("%w: %d", ErrFieldNotFound, fieldNumber)
111119
}
112120

113121
// NullifyProtoField removes a field from protobytes without unmarshalling

0 commit comments

Comments
 (0)