Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions api/v1/forkchoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type ForkChoice struct {
FinalizedCheckpoint phase0.Checkpoint
// ForkChoiceNodes contains the fork choice nodes.
ForkChoiceNodes []*ForkChoiceNode
// ExtraData is the optional, client-specific extra data of the fork choice store.
ExtraData map[string]any
}

// MarshalJSON implements json.Marshaler.
Expand All @@ -40,6 +42,7 @@ func (f *ForkChoice) MarshalJSON() ([]byte, error) {
JustifiedCheckpoint: &f.JustifiedCheckpoint,
FinalizedCheckpoint: &f.FinalizedCheckpoint,
ForkChoiceNodes: f.ForkChoiceNodes,
ExtraData: f.ExtraData,
})
}

Expand Down Expand Up @@ -75,6 +78,7 @@ func (f *ForkChoice) UnmarshalJSON(input []byte) error {
}

f.ForkChoiceNodes = forkChoiceJSON.ForkChoiceNodes
f.ExtraData = forkChoiceJSON.ExtraData

return nil
}
Expand All @@ -94,6 +98,7 @@ type forkChoiceJSON struct {
JustifiedCheckpoint *phase0.Checkpoint `json:"justified_checkpoint"`
FinalizedCheckpoint *phase0.Checkpoint `json:"finalized_checkpoint"`
ForkChoiceNodes []*ForkChoiceNode `json:"fork_choice_nodes"`
ExtraData map[string]any `json:"extra_data,omitempty"`
}

// ForkChoiceNodeValidity represents the validity of a fork choice node.
Expand Down Expand Up @@ -125,6 +130,8 @@ var ForkChoiceNodeValidityStrings = [...]string{
// ForkChoiceNodeValidityFromString converts a string input to a fork choice.
func ForkChoiceNodeValidityFromString(input string) (ForkChoiceNodeValidity, error) {
switch strings.ToLower(input) {
case "unknown":
return ForkChoiceNodeValidityUnknown, nil
case "invalid":
return ForkChoiceNodeValidityInvalid, nil
case "valid":
Expand All @@ -144,6 +151,9 @@ func (d *ForkChoiceNodeValidity) MarshalJSON() ([]byte, error) {
}

// UnmarshalJSON implements json.Unmarshaler.
// Unlike fork choice node decoding, which keeps an unrecognised validity in
// the node's ExtraData, a lone validity has nowhere to keep the original
// value, so an unrecognised one is an error rather than silently unknown.
func (d *ForkChoiceNodeValidity) UnmarshalJSON(input []byte) error {
var err error

Expand Down Expand Up @@ -171,6 +181,7 @@ type ForkChoiceNode struct {
// BlockRoot is the block root of the node.
BlockRoot phase0.Root
// ParentRoot is the parent root of the node.
// A zero root means none: some clients return a null parent root for the oldest retained node.
ParentRoot phase0.Root
// JustifiedEpcoh is the justified epoch of the node.
JustifiedEpoch phase0.Epoch
Expand Down Expand Up @@ -246,6 +257,13 @@ func (f *ForkChoiceNode) UnmarshalJSON(input []byte) error {
return errors.Wrap(err, fmt.Sprintf("invalid value for parent root: %s", forkChoiceNodeJSON.ParentRoot))
}

// A null parent root, which some clients return for the oldest retained
// node, decodes as the zero root.
if len(parentRoot) != 0 && len(parentRoot) != rootLength {
return fmt.Errorf("incorrect length %d for parent root", len(parentRoot))
}

f.ParentRoot = phase0.Root{}
copy(f.ParentRoot[:], parentRoot)

justifiedEpoch, err := strconv.ParseUint(forkChoiceNodeJSON.JustifiedEpoch, 10, 64)
Expand All @@ -269,9 +287,16 @@ func (f *ForkChoiceNode) UnmarshalJSON(input []byte) error {

f.Weight = weight

// Validities this package does not recognise (e.g. a value a client adds
// in future) decode as unknown, keeping the original in ExtraData, rather
// than failing the whole fork choice.
if forkChoiceNodeJSON.Validity == "" {
return errors.New("validity missing")
}
validity, err := ForkChoiceNodeValidityFromString(forkChoiceNodeJSON.Validity)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("invalid value for validity: %s", forkChoiceNodeJSON.Validity))
unrecognisedValidity := err != nil
Comment thread
Savid marked this conversation as resolved.
if unrecognisedValidity {
validity = ForkChoiceNodeValidityUnknown
}

f.Validity = validity
Expand All @@ -288,6 +313,9 @@ func (f *ForkChoiceNode) UnmarshalJSON(input []byte) error {
copy(f.ExecutionBlockHash[:], executionBlockHash)

f.ExtraData = forkChoiceNodeJSON.ExtraData
if unrecognisedValidity {
f.ExtraData = keepUnrecognisedValue(f.ExtraData, "validity", forkChoiceNodeJSON.Validity)
}

return nil
}
Expand All @@ -301,3 +329,24 @@ func (f *ForkChoiceNode) String() string {

return string(data)
}

// keepUnrecognisedValue records a value this package does not recognise, which
// decodes as unknown, under extraData[key] (unless the client already uses that
// key), so callers can still tell such values apart and re-encoding keeps them.
func keepUnrecognisedValue(extraData map[string]any, key string, value string) map[string]any {
if value == "" {
return extraData
}

if _, exists := extraData[key]; exists {
return extraData
}

if extraData == nil {
extraData = make(map[string]any)
}

extraData[key] = value

return extraData
}
26 changes: 22 additions & 4 deletions api/v1/forkchoice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ func TestForkChoiceJSON(t *testing.T) {
input: []byte(`{"justified_checkpoint":{"epoch":"1","root":"0x0000000000000000000000000000000000000000000000000000000000000000"},"finalized_checkpoint":{"epoch":"2","root":"0x0100000000000000000000000000000000000000000000000000000000000000"},"fork_choice_nodes":[{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"valid","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7"}}]}`),
expected: `{"justified_checkpoint":{"epoch":"1","root":"0x0000000000000000000000000000000000000000000000000000000000000000"},"finalized_checkpoint":{"epoch":"2","root":"0x0100000000000000000000000000000000000000000000000000000000000000"},"fork_choice_nodes":[{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"valid","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7"}}]}`,
},
{
name: "GoodExtraData",
input: []byte(`{"justified_checkpoint":{"epoch":"1","root":"0x0000000000000000000000000000000000000000000000000000000000000000"},"finalized_checkpoint":{"epoch":"2","root":"0x0100000000000000000000000000000000000000000000000000000000000000"},"fork_choice_nodes":[{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"valid","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9"}],"extra_data":{"proposer_boost_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","unrealized_justified_checkpoint":{"epoch":"61323","root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6"}}}`),
},
{
name: "JustifiedCheckpointMissing",
input: []byte(`{"finalized_checkpoint":{"epoch":"2","root":"0x0100000000000000000000000000000000000000000000000000000000000000"},"fork_choice_nodes":[]}`),
Expand Down Expand Up @@ -123,6 +127,11 @@ func TestForkChoiceNodeJSON(t *testing.T) {
input: []byte(`{"slot":"1962336","block_root":"","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"valid","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7"}}`),
err: "incorrect length 0 for block root",
},
{
name: "ParentRootShort",
input: []byte(`{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0x1234","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"valid","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7"}}`),
err: "incorrect length 2 for parent root",
},
{
name: "JustifiedEpochInvalid",
input: []byte(`{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":-1,"finalized_epoch":"61321","weight":"57481550000000","validity":"valid","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7"}}`),
Expand All @@ -139,9 +148,14 @@ func TestForkChoiceNodeJSON(t *testing.T) {
err: "invalid JSON: json: cannot unmarshal number into Go struct field forkChoiceNodeJSON.weight of type string",
},
{
name: "ValidityInvalid",
input: []byte(`{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"NOT_A_VALID_VALIDITY","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7"}}`),
err: "invalid value for validity: NOT_A_VALID_VALIDITY: unrecognised fork choice validity: NOT_A_VALID_VALIDITY",
name: "ValidityUnrecognised",
input: []byte(`{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"NOT_A_VALID_VALIDITY","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7"}}`),
expected: `{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","validity":"unknown","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9","extra_data":{"justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","state_root":"0x4bcecf56081291ab95df1dba25b0f83343d38217e9cec198510b61f6f35afdb3","unrealised_finalized_epoch":"61321","unrealised_justified_epoch":"61322","unrealized_finalized_root":"0x57a41f26678190d3e319c19fe9f4ea3830c4b21710a1e1ae41adcc23d0f030a2","unrealized_justified_root":"0xdee6c83ee7dc6c0916a8d43c4e7cda93655857da0487f193a62852699e5c39f7","validity":"NOT_A_VALID_VALIDITY"}}`,
},
{
name: "ValidityMissing",
input: []byte(`{"slot":"1962336","block_root":"0x0f61e82f7b51f41fcd552cbdc64547bd9e1ba54b8404482732927645c2e13ec6","parent_root":"0xe399a2ee74cf0570b4f980772983bdc5cfbfde1f87f3ab395f2bee96103978c7","justified_epoch":"61322","finalized_epoch":"61321","weight":"57481550000000","execution_block_hash":"0x06a0277e02eae44c332bcec82d6715c3113dddce427982014cf5f43432f479e9"}`),
err: "validity missing",
},
{
name: "ExecutionBlockHashInvalid",
Expand All @@ -166,7 +180,11 @@ func TestForkChoiceNodeJSON(t *testing.T) {
require.NoError(t, err)
rt, err := json.Marshal(&fc)
require.NoError(t, err)
assert.Equal(t, string(test.input), string(rt))
expected := test.expected
if expected == "" {
expected = string(test.input)
}
assert.Equal(t, expected, string(rt))
assert.Equal(t, string(rt), fc.String())
}
})
Expand Down
Loading
Loading