Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth: Add tests of Beacon API response structs & update Beacon API client #9077

Merged
merged 6 commits into from
Jan 18, 2024
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
12 changes: 8 additions & 4 deletions op-e2e/e2eutils/fakebeacon/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,23 @@ func (f *FakeBeacon) Start(addr string) error {
var mockBeaconBlockRoot [32]byte
mockBeaconBlockRoot[0] = 42
binary.LittleEndian.PutUint64(mockBeaconBlockRoot[32-8:], slot)
sidecars := make([]*eth.BlobSidecar, len(indices))
sidecars := make([]*eth.APIBlobSidecar, len(indices))
for i, ix := range indices {
if ix >= uint64(len(bundle.Blobs)) {
f.log.Error("blob index from request is out of range", "url", r.URL)
w.WriteHeader(http.StatusBadRequest)
return
}
sidecars[i] = &eth.BlobSidecar{
BlockRoot: mockBeaconBlockRoot,
Slot: eth.Uint64String(slot),
sidecars[i] = &eth.APIBlobSidecar{
Index: eth.Uint64String(i),
KZGCommitment: eth.Bytes48(bundle.Commitments[ix]),
KZGProof: eth.Bytes48(bundle.Proofs[ix]),
SignedBlockHeader: eth.SignedBeaconBlockHeader{
Message: eth.BeaconBlockHeader{
StateRoot: mockBeaconBlockRoot,
Slot: eth.Uint64String(slot),
},
},
sebastianst marked this conversation as resolved.
Show resolved Hide resolved
}
copy(sidecars[i].Blob[:], bundle.Blobs[ix])
}
Expand Down
36 changes: 34 additions & 2 deletions op-service/eth/blobs_api.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,48 @@
package eth

type BlobSidecar struct {
BlockRoot Bytes32 `json:"block_root"`
Slot Uint64String `json:"slot"`
Blob Blob `json:"blob"`
Index Uint64String `json:"index"`
KZGCommitment Bytes48 `json:"kzg_commitment"`
KZGProof Bytes48 `json:"kzg_proof"`
}

type APIBlobSidecar struct {
Index Uint64String `json:"index"`
Blob Blob `json:"blob"`
KZGCommitment Bytes48 `json:"kzg_commitment"`
KZGProof Bytes48 `json:"kzg_proof"`
SignedBlockHeader SignedBeaconBlockHeader `json:"signed_block_header"`
// The inclusion-proof of the blob-sidecar into the beacon-block is ignored,
// since we verify blobs by their versioned hashes against the execution-layer block instead.
}

func (sc *APIBlobSidecar) BlobSidecar() *BlobSidecar {
return &BlobSidecar{
Slot: sc.SignedBlockHeader.Message.Slot,
Blob: sc.Blob,
Index: sc.Index,
KZGCommitment: sc.KZGCommitment,
KZGProof: sc.KZGProof,
}
}

type SignedBeaconBlockHeader struct {
Message BeaconBlockHeader `json:"message"`
// signature is ignored, since we verify blobs against EL versioned-hashes
}

type BeaconBlockHeader struct {
Slot Uint64String `json:"slot"`
ProposerIndex Uint64String `json:"proposer_index"`
ParentRoot Bytes32 `json:"parent_root"`
StateRoot Bytes32 `json:"state_root"`
BodyRoot Bytes32 `json:"body_root"`
}

type APIGetBlobSidecarsResponse struct {
Data []*BlobSidecar `json:"data"`
Data []*APIBlobSidecar `json:"data"`
}

type ReducedGenesisData struct {
Expand Down
97 changes: 97 additions & 0 deletions op-service/eth/blobs_api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package eth_test

import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"testing"

"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/stretchr/testify/require"
)

type dataJson struct {
Data map[string]any `json:"data"`
}

// TestAPIGenesisResponse tests that json unmarshaling a json response from a
// eth/v1/beacon/genesis beacon node call into a APIGenesisResponse object
// fills all exising fields with the expected values, thereby confirming that
// APIGenesisResponse is compatible with the current beacon node API.
// This also confirms that the [sources.L1BeaconClient] correctly parses
// responses from a real beacon node.
func TestAPIGenesisResponse(t *testing.T) {
require := require.New(t)
var resp eth.APIGenesisResponse
require.Equal(1, reflect.TypeOf(resp.Data).NumField(), "APIGenesisResponse changed, adjust test")

path := filepath.Join("testdata", "eth_v1_beacon_genesis_goerli.json")
jsonStr, err := os.ReadFile(path)
require.NoError(err)

require.NoError(json.Unmarshal(jsonStr, &resp))
require.NotZero(resp.Data.GenesisTime)

jsonMap := &dataJson{Data: make(map[string]any)}
require.NoError(json.Unmarshal(jsonStr, jsonMap))
genesisTime, err := resp.Data.GenesisTime.MarshalText()
require.NoError(err)
require.Equal(jsonMap.Data["genesis_time"].(string), string(genesisTime))
}

// TestAPIConfigResponse tests that json unmarshaling a json response from a
// eth/v1/config/spec beacon node call into a APIConfigResponse object
// fills all exising fields with the expected values, thereby confirming that
// APIGenesisResponse is compatible with the current beacon node API.
// This also confirms that the [sources.L1BeaconClient] correctly parses
// responses from a real beacon node.
func TestAPIConfigResponse(t *testing.T) {
require := require.New(t)
var resp eth.APIConfigResponse
require.Equal(1, reflect.TypeOf(resp.Data).NumField(), "APIConfigResponse changed, adjust test")

path := filepath.Join("testdata", "eth_v1_config_spec_goerli.json")
jsonStr, err := os.ReadFile(path)
require.NoError(err)

require.NoError(json.Unmarshal(jsonStr, &resp))
require.NotZero(resp.Data.SecondsPerSlot)

jsonMap := &dataJson{Data: make(map[string]any)}
require.NoError(json.Unmarshal(jsonStr, jsonMap))
secPerSlot, err := resp.Data.SecondsPerSlot.MarshalText()
require.NoError(err)
require.Equal(jsonMap.Data["SECONDS_PER_SLOT"].(string), string(secPerSlot))
}

// TestAPIGetBlobSidecarsResponse tests that json unmarshaling a json response from a
// eth/v1/beacon/blob_sidecars/<X> beacon node call into a APIGetBlobSidecarsResponse object
// fills all exising fields with the expected values, thereby confirming that
// APIGenesisResponse is compatible with the current beacon node API.
// This also confirms that the [sources.L1BeaconClient] correctly parses
// responses from a real beacon node.
func TestAPIGetBlobSidecarsResponse(t *testing.T) {
require := require.New(t)

path := filepath.Join("testdata", "eth_v1_beacon_blob_sidecars_7422094_goerli.json")
jsonStr, err := os.ReadFile(path)
require.NoError(err)

var resp eth.APIGetBlobSidecarsResponse
require.NoError(json.Unmarshal(jsonStr, &resp))
require.NotEmpty(resp.Data)
require.Equal(5, reflect.TypeOf(*resp.Data[0]).NumField(), "APIBlobSidecar changed, adjust test")
require.Equal(1, reflect.TypeOf(resp.Data[0].SignedBlockHeader).NumField(), "SignedBeaconBlockHeader changed, adjust test")
require.Equal(5, reflect.TypeOf(resp.Data[0].SignedBlockHeader.Message).NumField(), "BeaconBlockHeader changed, adjust test")

require.NotZero(resp.Data[0].Blob)
require.NotZero(resp.Data[1].Index)
require.NotZero(resp.Data[0].KZGCommitment)
require.NotZero(resp.Data[0].KZGProof)
require.NotZero(resp.Data[0].SignedBlockHeader.Message.Slot)
require.NotZero(resp.Data[0].SignedBlockHeader.Message.ParentRoot)
require.NotZero(resp.Data[0].SignedBlockHeader.Message.BodyRoot)
require.NotZero(resp.Data[0].SignedBlockHeader.Message.ProposerIndex)
require.NotZero(resp.Data[0].SignedBlockHeader.Message.StateRoot)
}

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions op-service/eth/testdata/eth_v1_beacon_genesis_goerli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"data":{"genesis_time":"1606824023","genesis_validators_root":"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95","genesis_fork_version":"0x00000000"}}
1 change: 1 addition & 0 deletions op-service/eth/testdata/eth_v1_config_spec_goerli.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"data":{"CONFIG_NAME":"mainnet","PRESET_BASE":"mainnet","TERMINAL_TOTAL_DIFFICULTY":"58750000000000000000000","TERMINAL_BLOCK_HASH":"0x0000000000000000000000000000000000000000000000000000000000000000","TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH":"18446744073709551615","SAFE_SLOTS_TO_IMPORT_OPTIMISTICALLY":"128","MIN_GENESIS_ACTIVE_VALIDATOR_COUNT":"16384","MIN_GENESIS_TIME":"1606824000","GENESIS_FORK_VERSION":"0x00000000","GENESIS_DELAY":"604800","ALTAIR_FORK_VERSION":"0x01000000","ALTAIR_FORK_EPOCH":"74240","BELLATRIX_FORK_VERSION":"0x02000000","BELLATRIX_FORK_EPOCH":"144896","CAPELLA_FORK_VERSION":"0x03000000","CAPELLA_FORK_EPOCH":"194048","SECONDS_PER_SLOT":"12","SECONDS_PER_ETH1_BLOCK":"14","MIN_VALIDATOR_WITHDRAWABILITY_DELAY":"256","SHARD_COMMITTEE_PERIOD":"256","ETH1_FOLLOW_DISTANCE":"2048","SUBNETS_PER_NODE":"2","INACTIVITY_SCORE_BIAS":"4","INACTIVITY_SCORE_RECOVERY_RATE":"16","EJECTION_BALANCE":"16000000000","MIN_PER_EPOCH_CHURN_LIMIT":"4","CHURN_LIMIT_QUOTIENT":"65536","PROPOSER_SCORE_BOOST":"40","DEPOSIT_CHAIN_ID":"1","DEPOSIT_NETWORK_ID":"1","DEPOSIT_CONTRACT_ADDRESS":"0x00000000219ab540356cbb839cbe05303d7705fa","MAX_COMMITTEES_PER_SLOT":"64","TARGET_COMMITTEE_SIZE":"128","MAX_VALIDATORS_PER_COMMITTEE":"2048","SHUFFLE_ROUND_COUNT":"90","HYSTERESIS_QUOTIENT":"4","HYSTERESIS_DOWNWARD_MULTIPLIER":"1","HYSTERESIS_UPWARD_MULTIPLIER":"5","SAFE_SLOTS_TO_UPDATE_JUSTIFIED":"8","MIN_DEPOSIT_AMOUNT":"1000000000","MAX_EFFECTIVE_BALANCE":"32000000000","EFFECTIVE_BALANCE_INCREMENT":"1000000000","MIN_ATTESTATION_INCLUSION_DELAY":"1","SLOTS_PER_EPOCH":"32","MIN_SEED_LOOKAHEAD":"1","MAX_SEED_LOOKAHEAD":"4","EPOCHS_PER_ETH1_VOTING_PERIOD":"64","SLOTS_PER_HISTORICAL_ROOT":"8192","MIN_EPOCHS_TO_INACTIVITY_PENALTY":"4","EPOCHS_PER_HISTORICAL_VECTOR":"65536","EPOCHS_PER_SLASHINGS_VECTOR":"8192","HISTORICAL_ROOTS_LIMIT":"16777216","VALIDATOR_REGISTRY_LIMIT":"1099511627776","BASE_REWARD_FACTOR":"64","WHISTLEBLOWER_REWARD_QUOTIENT":"512","PROPOSER_REWARD_QUOTIENT":"8","INACTIVITY_PENALTY_QUOTIENT":"67108864","MIN_SLASHING_PENALTY_QUOTIENT":"128","PROPORTIONAL_SLASHING_MULTIPLIER":"1","MAX_PROPOSER_SLASHINGS":"16","MAX_ATTESTER_SLASHINGS":"2","MAX_ATTESTATIONS":"128","MAX_DEPOSITS":"16","MAX_VOLUNTARY_EXITS":"16","INACTIVITY_PENALTY_QUOTIENT_ALTAIR":"50331648","MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR":"64","PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR":"2","SYNC_COMMITTEE_SIZE":"512","EPOCHS_PER_SYNC_COMMITTEE_PERIOD":"256","MIN_SYNC_COMMITTEE_PARTICIPANTS":"1","INACTIVITY_PENALTY_QUOTIENT_BELLATRIX":"16777216","MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX":"32","PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX":"3","MAX_BYTES_PER_TRANSACTION":"1073741824","MAX_TRANSACTIONS_PER_PAYLOAD":"1048576","BYTES_PER_LOGS_BLOOM":"256","MAX_EXTRA_DATA_BYTES":"32","MAX_BLS_TO_EXECUTION_CHANGES":"16","MAX_WITHDRAWALS_PER_PAYLOAD":"16","MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP":"16384","DOMAIN_DEPOSIT":"0x03000000","DOMAIN_SELECTION_PROOF":"0x05000000","SYNC_COMMITTEE_SUBNET_COUNT":"4","DOMAIN_AGGREGATE_AND_PROOF":"0x06000000","TARGET_AGGREGATORS_PER_COMMITTEE":"16","BLS_WITHDRAWAL_PREFIX":"0x00","DOMAIN_SYNC_COMMITTEE":"0x07000000","DOMAIN_BEACON_PROPOSER":"0x00000000","DOMAIN_BEACON_ATTESTER":"0x01000000","DOMAIN_VOLUNTARY_EXIT":"0x04000000","DOMAIN_RANDAO":"0x02000000","DOMAIN_APPLICATION_MASK":"0x00000001","DOMAIN_CONTRIBUTION_AND_PROOF":"0x09000000","TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE":"16","DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF":"0x08000000"}}
28 changes: 25 additions & 3 deletions op-service/eth/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ const (
InvalidPayloadAttributes ErrorCode = -38003 // Payload attributes are invalid / inconsistent.
)

var (
ErrBedrockScalarPaddingNotEmpty = errors.New("version 0 scalar value has non-empty padding")
)
var ErrBedrockScalarPaddingNotEmpty = errors.New("version 0 scalar value has non-empty padding")

// InputError distinguishes an user-input error from regular rpc errors,
// to help the (Engine) API user divert from accidental input mistakes.
Expand Down Expand Up @@ -76,6 +74,30 @@ func (b Bytes32) TerminalString() string {
return fmt.Sprintf("%x..%x", b[:3], b[29:])
}

type Bytes96 [96]byte

func (b *Bytes96) UnmarshalJSON(text []byte) error {
return hexutil.UnmarshalFixedJSON(reflect.TypeOf(b), text, b[:])
}

func (b *Bytes96) UnmarshalText(text []byte) error {
return hexutil.UnmarshalFixedText("Bytes96", text, b[:])
}

func (b Bytes96) MarshalText() ([]byte, error) {
return hexutil.Bytes(b[:]).MarshalText()
}

func (b Bytes96) String() string {
return hexutil.Encode(b[:])
}

// TerminalString implements log.TerminalStringer, formatting a string for console
// output during logging.
func (b Bytes96) TerminalString() string {
return fmt.Sprintf("%x..%x", b[:3], b[93:])
}

type Bytes256 [256]byte

func (b *Bytes256) UnmarshalJSON(text []byte) error {
Expand Down
7 changes: 6 additions & 1 deletion op-service/sources/l1_beacon_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,12 @@ func (cl *L1BeaconClient) GetBlobSidecars(ctx context.Context, ref eth.L1BlockRe
return nil, fmt.Errorf("expected %v sidecars but got %v", len(hashes), len(resp.Data))
}

return resp.Data, nil
bscs := make([]*eth.BlobSidecar, 0, len(hashes))
for _, apisc := range resp.Data {
bscs = append(bscs, apisc.BlobSidecar())
}

return bscs, nil
}

// GetBlobs fetches blobs that were confirmed in the specified L1 block with the given indexed
Expand Down