diff --git a/common/common.go b/common/common.go index 21bce2f1..acd50737 100644 --- a/common/common.go +++ b/common/common.go @@ -17,6 +17,8 @@ var ( SlotsPerEpoch = uint64(cli.GetEnvInt("SLOTS_PER_EPOCH", 32)) DurationPerEpoch = DurationPerSlot * time.Duration(SlotsPerEpoch) + + EmptyTxRoot = "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1" ) func SlotToEpoch(slot uint64) uint64 { @@ -38,10 +40,10 @@ type BuilderStatus struct { IsOptimistic bool } -// Profile captures performance metrics for the block submission handler. Each +// BlockSubmissionProfile captures performance metrics for the block submission handler. Each // field corresponds to the number of microseconds in each stage. The `Total` // field is the number of microseconds taken for entire flow. -type Profile struct { +type BlockSubmissionProfile struct { PayloadLoad uint64 Decode uint64 Prechecks uint64 @@ -50,6 +52,22 @@ type Profile struct { Total uint64 } -func (p *Profile) String() string { +func (p *BlockSubmissionProfile) String() string { return fmt.Sprintf("%v,%v,%v,%v,%v", p.Decode, p.Prechecks, p.Simulation, p.RedisUpdate, p.Total) } + +// HeaderSubmissionProfile captures performance metrics for the header submission handler. Each +// field corresponds to the number of microseconds at the start of each stage. +type HeaderSubmissionProfile struct { + PayloadLoad uint64 + Decode uint64 + Prechecks uint64 + Signature uint64 + RedisChecks uint64 + RedisUpdate uint64 + Total uint64 +} + +func (p *HeaderSubmissionProfile) String() string { + return fmt.Sprintf("%v,%v,%v,%v,%v,%v,%v", p.PayloadLoad, p.Decode, p.Prechecks, p.Signature, p.RedisChecks, p.RedisUpdate, p.Total) +} diff --git a/common/test_utils.go b/common/test_utils.go index 410f0000..9f0e2f8a 100644 --- a/common/test_utils.go +++ b/common/test_utils.go @@ -186,9 +186,14 @@ func CreateTestBlockSubmission(t *testing.T, builderPubkey string, value *uint25 Message: bidTrace, ExecutionPayload: &deneb.ExecutionPayload{ //nolint:exhaustruct BaseFeePerGas: uint256.NewInt(0), + ExtraData: make([]byte, 32), + Transactions: make([]bellatrix.Transaction, 0), + Withdrawals: make([]*capella.Withdrawal, 0), }, BlobsBundle: &builderApiDeneb.BlobsBundle{ //nolint:exhaustruct Commitments: make([]deneb.KZGCommitment, 0), + Proofs: make([]deneb.KZGProof, 0), + Blobs: make([]deneb.Blob, 0), }, Signature: phase0.BLSSignature{}, }, diff --git a/database/database.go b/database/database.go index 704d8a08..0419967d 100644 --- a/database/database.go +++ b/database/database.go @@ -25,7 +25,7 @@ type IDatabaseService interface { GetValidatorRegistration(pubkey string) (*ValidatorRegistrationEntry, error) GetValidatorRegistrationsForPubkeys(pubkeys []string) ([]*ValidatorRegistrationEntry, error) - SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) + SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.BlockSubmissionProfile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) GetBlockSubmissionEntry(slot uint64, proposerPubkey, blockHash string) (entry *BuilderBlockSubmissionEntry, err error) GetBuilderSubmissions(filters GetBuilderSubmissionsFilters) ([]*BuilderBlockSubmissionEntry, error) GetBuilderSubmissionsBySlots(slotFrom, slotTo uint64) (entries []*BuilderBlockSubmissionEntry, err error) @@ -175,7 +175,7 @@ func (s *DatabaseService) GetLatestValidatorRegistrations(timestampOnly bool) ([ return registrations, err } -func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { +func (s *DatabaseService) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.BlockSubmissionProfile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { // Save execution_payload: insert, or if already exists update to be able to return the id ('on conflict do nothing' doesn't return an id) execPayloadEntry, err := PayloadToExecPayloadEntry(payload) if err != nil { diff --git a/database/database_test.go b/database/database_test.go index e8c5fb00..a3e97fb2 100644 --- a/database/database_test.go +++ b/database/database_test.go @@ -39,7 +39,7 @@ var ( feeRecipient = bellatrix.ExecutionAddress{0x02} blockHashStr = "0xa645370cc112c2e8e3cce121416c7dc849e773506d4b6fb9b752ada711355369" testDBDSN = common.GetEnv("TEST_DB_DSN", "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable") - profile = common.Profile{ + profile = common.BlockSubmissionProfile{ Decode: 42, Prechecks: 43, Simulation: 44, diff --git a/database/mockdb.go b/database/mockdb.go index fe65645e..121cca14 100644 --- a/database/mockdb.go +++ b/database/mockdb.go @@ -36,7 +36,7 @@ func (db MockDB) GetLatestValidatorRegistrations(timestampOnly bool) ([]*Validat return nil, nil } -func (db MockDB) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.Profile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { +func (db MockDB) SaveBuilderBlockSubmission(payload *common.VersionedSubmitBlockRequest, requestError, validationError error, receivedAt, eligibleAt time.Time, wasSimulated, saveExecPayload bool, profile common.BlockSubmissionProfile, optimisticSubmission bool) (entry *BuilderBlockSubmissionEntry, err error) { return nil, nil } diff --git a/datastore/redis.go b/datastore/redis.go index 02b307f8..b11a42b0 100644 --- a/datastore/redis.go +++ b/datastore/redis.go @@ -12,6 +12,7 @@ import ( builderApi "github.com/attestantio/go-builder-client/api" builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb" + builderApiV1 "github.com/attestantio/go-builder-client/api/v1" builderSpec "github.com/attestantio/go-builder-client/spec" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/capella" @@ -496,31 +497,26 @@ type SaveBidAndUpdateTopBidResponse struct { PrevTopBidValue *big.Int TimePrep time.Duration - TimeSavePayload time.Duration TimeSaveBid time.Duration TimeSaveTrace time.Duration + TimeSavePayload time.Duration TimeUpdateTopBid time.Duration TimeUpdateFloor time.Duration } -func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, pipeliner redis.Pipeliner, trace *common.BidTraceV2WithBlobFields, payload *common.VersionedSubmitBlockRequest, getPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse, getHeaderResponse *builderSpec.VersionedSignedBuilderBid, reqReceivedAt time.Time, isCancellationEnabled bool, floorValue *big.Int) (state SaveBidAndUpdateTopBidResponse, err error) { +func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, pipeliner redis.Pipeliner, trace *builderApiV1.BidTrace, blockSubmission *common.BlockSubmissionInfo, getPayloadResponse *builderApi.VersionedSubmitBlindedBlockResponse, getHeaderResponse *builderSpec.VersionedSignedBuilderBid, reqReceivedAt time.Time, isCancellationEnabled bool, floorValue *big.Int) (state SaveBidAndUpdateTopBidResponse, err error) { var prevTime, nextTime time.Time prevTime = time.Now() - submission, err := common.GetBlockSubmissionInfo(payload) - if err != nil { - return state, err - } - // Load latest bids for a given slot+parent+proposer - builderBids, err := NewBuilderBidsFromRedis(ctx, r, pipeliner, submission.BidTrace.Slot, submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String()) + builderBids, err := NewBuilderBidsFromRedis(ctx, r, pipeliner, trace.Slot, trace.ParentHash.String(), trace.ProposerPubkey.String()) if err != nil { return state, err } // Load floor value (if not passed in already) if floorValue == nil { - floorValue, err = r.GetFloorBidValue(ctx, pipeliner, submission.BidTrace.Slot, submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String()) + floorValue, err = r.GetFloorBidValue(ctx, pipeliner, trace.Slot, trace.ParentHash.String(), trace.ProposerPubkey.String()) if err != nil { return state, err } @@ -534,7 +530,7 @@ func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, pipeliner redis state.PrevTopBidValue = state.TopBidValue // Abort now if non-cancellation bid is lower than floor value - isBidAboveFloor := submission.BidTrace.Value.ToBig().Cmp(floorValue) == 1 + isBidAboveFloor := trace.Value.ToBig().Cmp(floorValue) == 1 if !isCancellationEnabled && !isBidAboveFloor { return state, nil } @@ -547,49 +543,61 @@ func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, pipeliner redis // // Time to save things in Redis // - // 1. Save the execution payload - switch payload.Version { - case spec.DataVersionCapella: - err = r.SaveExecutionPayloadCapella(ctx, pipeliner, submission.BidTrace.Slot, submission.BidTrace.ProposerPubkey.String(), submission.BidTrace.BlockHash.String(), getPayloadResponse.Capella) - if err != nil { - return state, err - } - case spec.DataVersionDeneb: - err = r.SavePayloadContentsDeneb(ctx, pipeliner, submission.BidTrace.Slot, submission.BidTrace.ProposerPubkey.String(), submission.BidTrace.BlockHash.String(), getPayloadResponse.Deneb) - if err != nil { - return state, err - } - case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: - return state, fmt.Errorf("unsupported payload version: %s", payload.Version) //nolint:goerr113 - } - - // Record time needed to save payload - nextTime = time.Now().UTC() - state.TimeSavePayload = nextTime.Sub(prevTime) - prevTime = nextTime - - // 2. Save latest bid for this builder - err = r.SaveBuilderBid(ctx, pipeliner, submission.BidTrace.Slot, submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String(), submission.BidTrace.BuilderPubkey.String(), reqReceivedAt, getHeaderResponse) + // 1. Save latest bid for this builder + err = r.SaveBuilderBid(ctx, pipeliner, trace.Slot, trace.ParentHash.String(), trace.ProposerPubkey.String(), trace.BuilderPubkey.String(), reqReceivedAt, getHeaderResponse) if err != nil { return state, err } - builderBids.bidValues[submission.BidTrace.BuilderPubkey.String()] = submission.BidTrace.Value.ToBig() + builderBids.bidValues[trace.BuilderPubkey.String()] = trace.Value.ToBig() // Record time needed to save bid nextTime = time.Now().UTC() state.TimeSaveBid = nextTime.Sub(prevTime) prevTime = nextTime - // 3. Save the bid trace - err = r.SaveBidTrace(ctx, pipeliner, trace) - if err != nil { - return state, err + // 2. Save the bid trace + if blockSubmission != nil { + bidTrace := common.BidTraceV2WithBlobFields{ + BidTrace: *trace, + BlockNumber: blockSubmission.BlockNumber, + NumTx: uint64(len(blockSubmission.Transactions)), + NumBlobs: uint64(len(blockSubmission.Blobs)), + BlobGasUsed: blockSubmission.BlobGasUsed, + ExcessBlobGas: blockSubmission.ExcessBlobGas, + } + err = r.SaveBidTrace(ctx, pipeliner, &bidTrace) + if err != nil { + return state, err + } + + // Record time needed to save trace + nextTime = time.Now().UTC() + state.TimeSaveTrace = nextTime.Sub(prevTime) + prevTime = nextTime } - // Record time needed to save trace - nextTime = time.Now().UTC() - state.TimeSaveTrace = nextTime.Sub(prevTime) - prevTime = nextTime + // 3. Save the execution payload + if getPayloadResponse != nil { + switch getPayloadResponse.Version { + case spec.DataVersionCapella: + err = r.SaveExecutionPayloadCapella(ctx, pipeliner, trace.Slot, trace.ProposerPubkey.String(), trace.BlockHash.String(), getPayloadResponse.Capella) + if err != nil { + return state, err + } + case spec.DataVersionDeneb: + err = r.SavePayloadContentsDeneb(ctx, pipeliner, trace.Slot, trace.ProposerPubkey.String(), trace.BlockHash.String(), getPayloadResponse.Deneb) + if err != nil { + return state, err + } + case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: + return state, fmt.Errorf("unsupported payload version: %s", getPayloadResponse.Version) //nolint:goerr113 + } + + // Record time needed to save payload + nextTime = time.Now().UTC() + state.TimeSavePayload = nextTime.Sub(prevTime) + prevTime = nextTime + } // If top bid value hasn't change, abort now _, state.TopBidValue = builderBids.getTopBid() @@ -597,11 +605,11 @@ func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, pipeliner redis return state, nil } - state, err = r._updateTopBid(ctx, pipeliner, state, builderBids, submission.BidTrace.Slot, submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String(), floorValue) + state, err = r._updateTopBid(ctx, pipeliner, state, builderBids, trace.Slot, trace.ParentHash.String(), trace.ProposerPubkey.String(), floorValue) if err != nil { return state, err } - state.IsNewTopBid = submission.BidTrace.Value.ToBig().Cmp(state.TopBidValue) == 0 + state.IsNewTopBid = trace.Value.ToBig().Cmp(state.TopBidValue) == 0 // An Exec happens in _updateTopBid. state.WasBidSaved = true @@ -615,8 +623,8 @@ func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, pipeliner redis } // Non-cancellable bid above floor should set new floor - keyBidSource := r.keyLatestBidByBuilder(submission.BidTrace.Slot, submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String(), submission.BidTrace.BuilderPubkey.String()) - keyFloorBid := r.keyFloorBid(submission.BidTrace.Slot, submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String()) + keyBidSource := r.keyLatestBidByBuilder(trace.Slot, trace.ParentHash.String(), trace.ProposerPubkey.String(), trace.BuilderPubkey.String()) + keyFloorBid := r.keyFloorBid(trace.Slot, trace.ParentHash.String(), trace.ProposerPubkey.String()) c := pipeliner.Copy(ctx, keyBidSource, keyFloorBid, 0, true) _, err = pipeliner.Exec(ctx) if err != nil { @@ -634,8 +642,8 @@ func (r *RedisCache) SaveBidAndUpdateTopBid(ctx context.Context, pipeliner redis return state, err } - keyFloorBidValue := r.keyFloorBidValue(submission.BidTrace.Slot, submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String()) - err = pipeliner.Set(ctx, keyFloorBidValue, submission.BidTrace.Value.Dec(), expiryBidCache).Err() + keyFloorBidValue := r.keyFloorBidValue(trace.Slot, trace.ParentHash.String(), trace.ProposerPubkey.String()) + err = pipeliner.Set(ctx, keyFloorBidValue, trace.Value.Dec(), expiryBidCache).Err() if err != nil { return state, err } diff --git a/datastore/redis_test.go b/datastore/redis_test.go index fdada757..52e87de2 100644 --- a/datastore/redis_test.go +++ b/datastore/redis_test.go @@ -21,6 +21,15 @@ import ( "github.com/stretchr/testify/require" ) +const ( + bApubkey = "0xfa1ed37c3553d0ce1e9349b2c5063cf6e394d231c8d3e0df75e9462257c081543086109ffddaacc0aa76f33dc9661c83" + bBpubkey = "0x2e02be2c9f9eccf9856478fdb7876598fed2da09f45c233969ba647a250231150ecf38bce5771adb6171c86b79a92f16" + + slot = uint64(2) + parentHash = "0x13e606c7b3d1faad7e83503ce3dedce4c6bb89b0c28ffb240d713c7b110b9747" + proposerPubkey = "0x6ae5932d1e248d987d51b58665b81848814202d7b23b343d20f2a167d12f07dcb01ca41c42fdd60b7fca9c4b90890792" +) + func setupTestRedis(t *testing.T) *RedisCache { t.Helper() var err error @@ -113,6 +122,59 @@ func TestRedisProposerDuties(t *testing.T) { require.Equal(t, duties[0].Entry.Message.FeeRecipient, duties2[0].Entry.Message.FeeRecipient) } +func TestRedisBidUpdate(t *testing.T) { + opts := common.CreateTestBlockSubmissionOpts{ + Slot: slot, + ParentHash: parentHash, + ProposerPubkey: proposerPubkey, + Version: spec.DataVersionDeneb, + } + + t.Run("Saves bid trace and payload into the cache", func(t *testing.T) { + cache := setupTestRedis(t) + payload, getPayloadResp, getHeaderResp := common.CreateTestBlockSubmission(t, bApubkey, uint256.NewInt(10), &opts) + submission, err := common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err := cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), false, nil) + require.NoError(t, err) + require.True(t, resp.WasBidSaved, resp) + require.True(t, resp.WasTopBidUpdated) + require.True(t, resp.IsNewTopBid) + require.Equal(t, big.NewInt(10), resp.TopBidValue) + trace, err := cache.GetBidTrace(slot, proposerPubkey, submission.ExecutionPayloadBlockHash.String()) + require.NoError(t, err) + bidTrace := &common.BidTraceV2WithBlobFields{ + BidTrace: *submission.BidTrace, + BlockNumber: submission.BlockNumber, + NumTx: uint64(len(submission.Transactions)), + NumBlobs: uint64(len(submission.Blobs)), + BlobGasUsed: submission.BlobGasUsed, + ExcessBlobGas: submission.ExcessBlobGas, + } + require.Equal(t, bidTrace, trace) + payloadContents, err := cache.GetPayloadContents(slot, proposerPubkey, submission.ExecutionPayloadBlockHash.String()) + require.NoError(t, err) + require.Equal(t, getPayloadResp, payloadContents) + }) + + t.Run("Does not save bid trace and payload in cache if not provided", func(t *testing.T) { + cache := setupTestRedis(t) + payload, _, getHeaderResp := common.CreateTestBlockSubmission(t, bApubkey, uint256.NewInt(10), &opts) + submission, err := common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err := cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, nil, nil, getHeaderResp, time.Now(), false, nil) + require.NoError(t, err) + require.True(t, resp.WasBidSaved, resp) + require.True(t, resp.WasTopBidUpdated) + require.True(t, resp.IsNewTopBid) + require.Equal(t, big.NewInt(10), resp.TopBidValue) + _, err = cache.GetBidTrace(slot, proposerPubkey, submission.ExecutionPayloadBlockHash.String()) + require.Error(t, err, redis.Nil) + _, err = cache.GetPayloadContents(slot, proposerPubkey, submission.ExecutionPayloadBlockHash.String()) + require.Error(t, err, redis.Nil) + }) +} + func TestBuilderBids(t *testing.T) { versions := []spec.DataVersion{ spec.DataVersionCapella, @@ -120,30 +182,18 @@ func TestBuilderBids(t *testing.T) { } for _, version := range versions { - slot := uint64(2) - parentHash := "0x13e606c7b3d1faad7e83503ce3dedce4c6bb89b0c28ffb240d713c7b110b9747" - proposerPubkey := "0x6ae5932d1e248d987d51b58665b81848814202d7b23b343d20f2a167d12f07dcb01ca41c42fdd60b7fca9c4b90890792" opts := common.CreateTestBlockSubmissionOpts{ - Slot: 2, + Slot: slot, ParentHash: parentHash, ProposerPubkey: proposerPubkey, Version: version, } - trace := &common.BidTraceV2WithBlobFields{ - BidTrace: builderApiV1.BidTrace{ - Value: uint256.NewInt(123), - }, - } - // Notation: // - ba1: builder A, bid 1 // - ba1c: builder A, bid 1, cancellation enabled // // test 1: ba1=10 -> ba2=5 -> ba3c=5 -> bb1=20 -> ba4c=3 -> bb2c=2 - // - bApubkey := "0xfa1ed37c3553d0ce1e9349b2c5063cf6e394d231c8d3e0df75e9462257c081543086109ffddaacc0aa76f33dc9661c83" - bBpubkey := "0x2e02be2c9f9eccf9856478fdb7876598fed2da09f45c233969ba647a250231150ecf38bce5771adb6171c86b79a92f16" // Setup redis instance cache := setupTestRedis(t) @@ -179,7 +229,9 @@ func TestBuilderBids(t *testing.T) { // submit ba1=10 payload, getPayloadResp, getHeaderResp := common.CreateTestBlockSubmission(t, bApubkey, uint256.NewInt(10), &opts) - resp, err := cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), false, nil) + submission, err := common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err := cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), false, nil) require.NoError(t, err) require.True(t, resp.WasBidSaved, resp) require.True(t, resp.WasTopBidUpdated) @@ -198,7 +250,9 @@ func TestBuilderBids(t *testing.T) { // submit ba2=5 (should not update, because floor is 10) payload, getPayloadResp, getHeaderResp = common.CreateTestBlockSubmission(t, bApubkey, uint256.NewInt(5), &opts) - resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), false, nil) + submission, err = common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), false, nil) require.NoError(t, err) require.False(t, resp.WasBidSaved, resp) require.False(t, resp.WasTopBidUpdated) @@ -209,7 +263,9 @@ func TestBuilderBids(t *testing.T) { // submit ba3c=5 (should not update, because floor is 10) payload, getPayloadResp, getHeaderResp = common.CreateTestBlockSubmission(t, bApubkey, uint256.NewInt(5), &opts) - resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), true, nil) + submission, err = common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), true, nil) require.NoError(t, err) require.True(t, resp.WasBidSaved) require.False(t, resp.WasTopBidUpdated) @@ -221,7 +277,9 @@ func TestBuilderBids(t *testing.T) { // submit bb1=20 payload, getPayloadResp, getHeaderResp = common.CreateTestBlockSubmission(t, bBpubkey, uint256.NewInt(20), &opts) - resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), false, nil) + submission, err = common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), false, nil) require.NoError(t, err) require.True(t, resp.WasBidSaved) require.True(t, resp.WasTopBidUpdated) @@ -232,7 +290,9 @@ func TestBuilderBids(t *testing.T) { // submit bb2c=22 payload, getPayloadResp, getHeaderResp = common.CreateTestBlockSubmission(t, bBpubkey, uint256.NewInt(22), &opts) - resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), true, nil) + submission, err = common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), true, nil) require.NoError(t, err) require.True(t, resp.WasBidSaved) require.True(t, resp.WasTopBidUpdated) @@ -243,7 +303,9 @@ func TestBuilderBids(t *testing.T) { // submit bb3c=12 (should update top bid, using floor at 20) payload, getPayloadResp, getHeaderResp = common.CreateTestBlockSubmission(t, bBpubkey, uint256.NewInt(12), &opts) - resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), true, nil) + submission, err = common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + resp, err = cache.SaveBidAndUpdateTopBid(context.Background(), cache.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), true, nil) require.NoError(t, err) require.True(t, resp.WasBidSaved) require.True(t, resp.WasTopBidUpdated) diff --git a/services/api/service.go b/services/api/service.go index 402ae06c..c0bace73 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -69,6 +69,7 @@ var ( // Block builder API pathBuilderGetValidators = "/relay/v1/builder/validators" pathSubmitNewBlock = "/relay/v1/builder/blocks" + pathSubmitHeader = "/relay/v1/builder/headers" // Data API pathDataProposerPayloadDelivered = "/relay/v1/data/bidtraces/proposer_payload_delivered" @@ -347,6 +348,7 @@ func (api *RelayAPI) getRouter() http.Handler { api.log.Info("block builder API enabled") r.HandleFunc(pathBuilderGetValidators, api.handleBuilderGetValidators).Methods(http.MethodGet) r.HandleFunc(pathSubmitNewBlock, api.handleSubmitNewBlock).Methods(http.MethodPost) + r.HandleFunc(pathSubmitHeader, api.handleSubmitNewHeader).Methods(http.MethodPost) } // Data API @@ -1615,35 +1617,28 @@ func (api *RelayAPI) checkSubmissionFeeRecipient(w http.ResponseWriter, log *log return slotDuty.Entry.Message.GasLimit, true } -func (api *RelayAPI) checkSubmissionPayloadAttrs(w http.ResponseWriter, log *logrus.Entry, submission *common.BlockSubmissionInfo) (payloadAttributesHelper, bool) { +func (api *RelayAPI) checkSubmissionPayloadAttrs(w http.ResponseWriter, log *logrus.Entry, bidTrace *builderApiV1.BidTrace, prevRandao phase0.Hash32, withdrawalsRoot phase0.Root) (payloadAttributesHelper, bool) { api.payloadAttributesLock.RLock() - attrs, ok := api.payloadAttributes[getPayloadAttributesKey(submission.BidTrace.ParentHash.String(), submission.BidTrace.Slot)] + attrs, ok := api.payloadAttributes[getPayloadAttributesKey(bidTrace.ParentHash.String(), bidTrace.Slot)] api.payloadAttributesLock.RUnlock() - if !ok || submission.BidTrace.Slot != attrs.slot { + if !ok || bidTrace.Slot != attrs.slot { log.WithFields(logrus.Fields{ "attributesFound": ok, - "payloadSlot": submission.BidTrace.Slot, + "payloadSlot": bidTrace.Slot, "attrsSlot": attrs.slot, }).Warn("payload attributes not (yet) known") api.RespondError(w, http.StatusBadRequest, "payload attributes not (yet) known") return attrs, false } - if submission.PrevRandao.String() != attrs.payloadAttributes.PrevRandao { - msg := fmt.Sprintf("incorrect prev_randao - got: %s, expected: %s", submission.PrevRandao.String(), attrs.payloadAttributes.PrevRandao) + if prevRandao.String() != attrs.payloadAttributes.PrevRandao { + msg := fmt.Sprintf("incorrect prev_randao - got: %s, expected: %s", prevRandao.String(), attrs.payloadAttributes.PrevRandao) log.Info(msg) api.RespondError(w, http.StatusBadRequest, msg) return attrs, false } - if hasReachedFork(submission.BidTrace.Slot, api.capellaEpoch) { // Capella requires correct withdrawals - withdrawalsRoot, err := ComputeWithdrawalsRoot(submission.Withdrawals) - if err != nil { - log.WithError(err).Warn("could not compute withdrawals root from payload") - api.RespondError(w, http.StatusBadRequest, "could not compute withdrawals root") - return attrs, false - } - + if hasReachedFork(bidTrace.Slot, api.capellaEpoch) { // Capella requires correct withdrawals if withdrawalsRoot != attrs.withdrawalsRoot { msg := fmt.Sprintf("incorrect withdrawals root - got: %s, expected: %s", withdrawalsRoot.String(), attrs.withdrawalsRoot.String()) log.Info(msg) @@ -1655,30 +1650,30 @@ func (api *RelayAPI) checkSubmissionPayloadAttrs(w http.ResponseWriter, log *log return attrs, true } -func (api *RelayAPI) checkSubmissionSlotDetails(w http.ResponseWriter, log *logrus.Entry, headSlot uint64, payload *common.VersionedSubmitBlockRequest, submission *common.BlockSubmissionInfo) bool { - if api.isDeneb(submission.BidTrace.Slot) && payload.Deneb == nil { +func (api *RelayAPI) checkSubmissionSlotDetails(w http.ResponseWriter, log *logrus.Entry, headSlot uint64, payloadVersion spec.DataVersion, payloadTimestamp uint64, bidTrace *builderApiV1.BidTrace) bool { + if api.isDeneb(bidTrace.Slot) && payloadVersion != spec.DataVersionDeneb { log.Info("rejecting submission - non deneb payload for deneb fork") api.RespondError(w, http.StatusBadRequest, "not deneb payload") return false } - if api.isCapella(submission.BidTrace.Slot) && payload.Capella == nil { + if api.isCapella(bidTrace.Slot) && payloadVersion != spec.DataVersionCapella { log.Info("rejecting submission - non capella payload for capella fork") api.RespondError(w, http.StatusBadRequest, "not capella payload") return false } - if submission.BidTrace.Slot <= headSlot { + if bidTrace.Slot <= headSlot { log.Info("submitNewBlock failed: submission for past slot") api.RespondError(w, http.StatusBadRequest, "submission for past slot") return false } // Timestamp check - expectedTimestamp := api.genesisInfo.Data.GenesisTime + (submission.BidTrace.Slot * common.SecondsPerSlot) - if submission.Timestamp != expectedTimestamp { - log.Warnf("incorrect timestamp. got %d, expected %d", submission.Timestamp, expectedTimestamp) - api.RespondError(w, http.StatusBadRequest, fmt.Sprintf("incorrect timestamp. got %d, expected %d", submission.Timestamp, expectedTimestamp)) + expectedTimestamp := api.genesisInfo.Data.GenesisTime + (bidTrace.Slot * common.SecondsPerSlot) + if payloadTimestamp != expectedTimestamp { + log.Warnf("incorrect timestamp. got %d, expected %d", payloadTimestamp, expectedTimestamp) + api.RespondError(w, http.StatusBadRequest, fmt.Sprintf("incorrect timestamp. got %d, expected %d", payloadTimestamp, expectedTimestamp)) return false } @@ -1717,13 +1712,28 @@ func (api *RelayAPI) checkBuilderEntry(w http.ResponseWriter, log *logrus.Entry, return builderEntry, true } +func (api *RelayAPI) checkBuilderBid(w http.ResponseWriter, log *logrus.Entry, bidTrace *builderApiV1.BidTrace, transactionsRoot phase0.Root, builderEntry *blockBuilderCacheEntry) bool { + // Check bid value + if bidTrace.Value.ToBig().Cmp(ZeroU256.BigInt()) == 0 || transactionsRoot.String() == common.EmptyTxRoot { + log.Info("rejecting bid: block with 0 value or empty tx root") + api.RespondError(w, http.StatusBadRequest, "block with 0 value") + return false + } + // Check bid has enough collateral if optimistic + if builderEntry.status.IsOptimistic && builderEntry.collateral.Cmp(bidTrace.Value.ToBig()) == -1 { + log.Info("rejecting bid: insufficient collateral") + api.RespondError(w, http.StatusBadRequest, "builder has insufficient collateral to cover bid") + return false + } + return true +} + type bidFloorOpts struct { w http.ResponseWriter tx redis.Pipeliner log *logrus.Entry cancellationsEnabled bool - simResultC chan *blockSimResult - submission *common.BlockSubmissionInfo + bidTrace *builderApiV1.BidTrace } func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, bool) { @@ -1731,14 +1741,14 @@ func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, bool) { slotLastPayloadDelivered, err := api.redis.GetLastSlotDelivered(context.Background(), opts.tx) if err != nil && !errors.Is(err, redis.Nil) { opts.log.WithError(err).Error("failed to get delivered payload slot from redis") - } else if opts.submission.BidTrace.Slot <= slotLastPayloadDelivered { + } else if opts.bidTrace.Slot <= slotLastPayloadDelivered { opts.log.Info("rejecting submission because payload for this slot was already delivered") api.RespondError(opts.w, http.StatusBadRequest, "payload for this slot was already delivered") return nil, false } // Grab floor bid value - floorBidValue, err := api.redis.GetFloorBidValue(context.Background(), opts.tx, opts.submission.BidTrace.Slot, opts.submission.BidTrace.ParentHash.String(), opts.submission.BidTrace.ProposerPubkey.String()) + floorBidValue, err := api.redis.GetFloorBidValue(context.Background(), opts.tx, opts.bidTrace.Slot, opts.bidTrace.ParentHash.String(), opts.bidTrace.ProposerPubkey.String()) if err != nil { opts.log.WithError(err).Error("failed to get floor bid value from redis") } else { @@ -1748,12 +1758,11 @@ func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, bool) { // -------------------------------------------- // Skip submission if below the floor bid value // -------------------------------------------- - isBidBelowFloor := floorBidValue != nil && opts.submission.BidTrace.Value.ToBig().Cmp(floorBidValue) == -1 - isBidAtOrBelowFloor := floorBidValue != nil && opts.submission.BidTrace.Value.ToBig().Cmp(floorBidValue) < 1 + isBidBelowFloor := floorBidValue != nil && opts.bidTrace.Value.ToBig().Cmp(floorBidValue) == -1 + isBidAtOrBelowFloor := floorBidValue != nil && opts.bidTrace.Value.ToBig().Cmp(floorBidValue) < 1 if opts.cancellationsEnabled && isBidBelowFloor { // with cancellations: if below floor -> delete previous bid - opts.simResultC <- &blockSimResult{false, false, nil, nil} opts.log.Info("submission below floor bid value, with cancellation") - err := api.redis.DelBuilderBid(context.Background(), opts.tx, opts.submission.BidTrace.Slot, opts.submission.BidTrace.ParentHash.String(), opts.submission.BidTrace.ProposerPubkey.String(), opts.submission.BidTrace.BuilderPubkey.String()) + err := api.redis.DelBuilderBid(context.Background(), opts.tx, opts.bidTrace.Slot, opts.bidTrace.ParentHash.String(), opts.bidTrace.ProposerPubkey.String(), opts.bidTrace.BuilderPubkey.String()) if err != nil { opts.log.WithError(err).Error("failed processing cancellable bid below floor") api.RespondError(opts.w, http.StatusInternalServerError, "failed processing cancellable bid below floor") @@ -1762,7 +1771,6 @@ func (api *RelayAPI) checkFloorBidValue(opts bidFloorOpts) (*big.Int, bool) { api.Respond(opts.w, http.StatusAccepted, "accepted bid below floor, skipped validation") return nil, false } else if !opts.cancellationsEnabled && isBidAtOrBelowFloor { // without cancellations: if at or below floor -> ignore - opts.simResultC <- &blockSimResult{false, false, nil, nil} opts.log.Info("submission at or below floor bid value, without cancellation") api.RespondMsg(opts.w, http.StatusAccepted, "accepted bid below floor, skipped validation") return nil, false @@ -1777,45 +1785,37 @@ type redisUpdateBidOpts struct { cancellationsEnabled bool receivedAt time.Time floorBidValue *big.Int - payload *common.VersionedSubmitBlockRequest + block *common.VersionedSubmitBlockRequest + header *common.VersionedSubmitHeaderOptimistic } func (api *RelayAPI) updateRedisBid(opts redisUpdateBidOpts) (*datastore.SaveBidAndUpdateTopBidResponse, *builderApi.VersionedSubmitBlindedBlockResponse, bool) { // Prepare the response data - getHeaderResponse, err := common.BuildGetHeaderResponse(opts.payload, api.blsSk, api.publicKey, api.opts.EthNetDetails.DomainBuilder) + getHeaderResponse, err := common.BuildGetHeaderResponse(opts.block, api.blsSk, api.publicKey, api.opts.EthNetDetails.DomainBuilder) if err != nil { opts.log.WithError(err).Error("could not sign builder bid") api.RespondError(opts.w, http.StatusBadRequest, err.Error()) return nil, nil, false } - getPayloadResponse, err := common.BuildGetPayloadResponse(opts.payload) + getPayloadResponse, err := common.BuildGetPayloadResponse(opts.block) if err != nil { opts.log.WithError(err).Error("could not build getPayload response") api.RespondError(opts.w, http.StatusBadRequest, err.Error()) return nil, nil, false } - submission, err := common.GetBlockSubmissionInfo(opts.payload) + submission, err := common.GetBlockSubmissionInfo(opts.block) if err != nil { opts.log.WithError(err).Error("could not get block submission info") api.RespondError(opts.w, http.StatusBadRequest, err.Error()) return nil, nil, false } - bidTrace := common.BidTraceV2WithBlobFields{ - BidTrace: *submission.BidTrace, - BlockNumber: submission.BlockNumber, - NumTx: uint64(len(submission.Transactions)), - NumBlobs: uint64(len(submission.Blobs)), - BlobGasUsed: submission.BlobGasUsed, - ExcessBlobGas: submission.ExcessBlobGas, - } - // // Save to Redis // - updateBidResult, err := api.redis.SaveBidAndUpdateTopBid(context.Background(), opts.tx, &bidTrace, opts.payload, getPayloadResponse, getHeaderResponse, opts.receivedAt, opts.cancellationsEnabled, opts.floorBidValue) + updateBidResult, err := api.redis.SaveBidAndUpdateTopBid(context.Background(), opts.tx, submission.BidTrace, submission, getPayloadResponse, getHeaderResponse, opts.receivedAt, opts.cancellationsEnabled, opts.floorBidValue) if err != nil { opts.log.WithError(err).Error("could not save bid and update top bids") api.RespondError(opts.w, http.StatusInternalServerError, "failed saving and updating bid") @@ -1824,8 +1824,36 @@ func (api *RelayAPI) updateRedisBid(opts redisUpdateBidOpts) (*datastore.SaveBid return &updateBidResult, getPayloadResponse, true } +func (api *RelayAPI) updateRedisBidForHeader(opts redisUpdateBidOpts) (*datastore.SaveBidAndUpdateTopBidResponse, bool) { + // Prepare the response data + getHeaderResponse, err := common.BuildGetHeaderResponseOptimistic(opts.header, api.blsSk, api.publicKey, api.opts.EthNetDetails.DomainBuilder) + if err != nil { + opts.log.WithError(err).Error("could not sign builder bid") + api.RespondError(opts.w, http.StatusBadRequest, err.Error()) + return nil, false + } + + submission, err := common.GetHeaderSubmissionInfo(opts.header) + if err != nil { + opts.log.WithError(err).Error("could not get header submission info") + api.RespondError(opts.w, http.StatusBadRequest, err.Error()) + return nil, false + } + + // + // Save to Redis + // + updateBidResult, err := api.redis.SaveBidAndUpdateTopBid(context.Background(), opts.tx, submission.BidTrace, nil, nil, getHeaderResponse, opts.receivedAt, opts.cancellationsEnabled, opts.floorBidValue) + if err != nil { + opts.log.WithError(err).Error("could not save bid and update top bids") + api.RespondError(opts.w, http.StatusInternalServerError, "failed saving and updating bid") + return nil, false + } + return &updateBidResult, true +} + func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Request) { - var pf common.Profile + var pf common.BlockSubmissionProfile var prevTime, nextTime time.Time headSlot := api.headSlot.Load() @@ -1945,7 +1973,7 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque }) } - ok := api.checkSubmissionSlotDetails(w, log, headSlot, payload, submission) + ok := api.checkSubmissionSlotDetails(w, log, headSlot, payload.Version, submission.Timestamp, submission.BidTrace) if !ok { return } @@ -1978,7 +2006,14 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque return } - attrs, ok := api.checkSubmissionPayloadAttrs(w, log, submission) + withdrawalsRoot, err := ComputeWithdrawalsRoot(submission.Withdrawals) + if err != nil { + log.WithError(err).Warn("could not compute withdrawals root from payload") + api.RespondError(w, http.StatusBadRequest, "could not compute withdrawals root") + return + } + + attrs, ok := api.checkSubmissionPayloadAttrs(w, log, submission.BidTrace, submission.PrevRandao, withdrawalsRoot) if !ok { return } @@ -2019,8 +2054,7 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque tx: tx, log: log, cancellationsEnabled: isCancellationEnabled, - simResultC: simResultC, - submission: submission, + bidTrace: submission.BidTrace, } floorBidValue, ok := api.checkFloorBidValue(bfOpts) if !ok { @@ -2159,7 +2193,7 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque cancellationsEnabled: isCancellationEnabled, receivedAt: receivedAt, floorBidValue: floorBidValue, - payload: payload, + block: payload, } updateBidResult, getPayloadResponse, ok := api.updateRedisBid(redisOpts) if !ok { @@ -2209,6 +2243,258 @@ func (api *RelayAPI) handleSubmitNewBlock(w http.ResponseWriter, req *http.Reque w.WriteHeader(http.StatusOK) } +func (api *RelayAPI) handleSubmitNewHeader(w http.ResponseWriter, req *http.Request) { + var pf common.HeaderSubmissionProfile + var prevTime, nextTime time.Time + + receivedAt := time.Now().UTC() + prevTime = receivedAt + + headSlot := api.headSlot.Load() + args := req.URL.Query() + isCancellationEnabled := args.Get("cancellations") == "1" + + log := api.log.WithFields(logrus.Fields{ + "method": "submitNewHeader", + "contentLength": req.ContentLength, + "headSlot": headSlot, + "cancellationEnabled": isCancellationEnabled, + "timestampRequestStart": receivedAt.UnixMilli(), + }) + + // Log at start and end of request + log.Info("request initiated") + defer func() { + log.WithFields(logrus.Fields{ + "timestampRequestFin": time.Now().UTC().UnixMilli(), + "requestDurationMs": time.Since(receivedAt).Milliseconds(), + }).Info("request finished") + }() + + // If cancellations are disabled but builder requested it, return error + if isCancellationEnabled && !api.ffEnableCancellations { + log.Info("builder submitted with cancellations enabled, but feature flag is disabled") + api.RespondError(w, http.StatusBadRequest, "cancellations are disabled") + return + } + + var err error + var r io.Reader = req.Body + isGzip := req.Header.Get("Content-Encoding") == "gzip" + log = log.WithField("reqIsGzip", isGzip) + if isGzip { + r, err = gzip.NewReader(req.Body) + if err != nil { + log.WithError(err).Warn("could not create gzip reader") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + } + + requestPayloadBytes, err := io.ReadAll(r) + if err != nil { + log.WithError(err).Warn("could not read payload") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + + nextTime = time.Now().UTC() + pf.PayloadLoad = uint64(nextTime.Sub(prevTime).Microseconds()) + prevTime = nextTime + + payload := new(common.VersionedSubmitHeaderOptimistic) + + // Check for SSZ encoding + contentType := req.Header.Get("Content-Type") + if contentType == "application/octet-stream" { + log = log.WithField("reqContentType", "ssz") + if err = payload.UnmarshalSSZ(requestPayloadBytes); err != nil { + log.WithError(err).Warn("could not decode payload - SSZ") + + // SSZ decoding failed. try JSON as fallback (some builders used octet-stream for json before) + if err2 := json.Unmarshal(requestPayloadBytes, payload); err2 != nil { + log.WithError(fmt.Errorf("%w / %w", err, err2)).Warn("could not decode payload - SSZ or JSON") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + log = log.WithField("reqContentType", "json") + } else { + log.Debug("received ssz-encoded payload") + } + } else { + log = log.WithField("reqContentType", "json") + if err := json.Unmarshal(requestPayloadBytes, payload); err != nil { + log.WithError(err).Warn("could not decode payload - JSON") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + } + + nextTime = time.Now().UTC() + pf.Decode = uint64(nextTime.Sub(prevTime).Microseconds()) + prevTime = nextTime + + submission, err := common.GetHeaderSubmissionInfo(payload) + if err != nil { + log.WithError(err).Warn("missing fields in submit header request") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + + log = log.WithFields(logrus.Fields{ + "slot": submission.BidTrace.Slot, + "builderPubkey": submission.BidTrace.BuilderPubkey.String(), + "blockHash": submission.BidTrace.BlockHash.String(), + "proposerPubkey": submission.BidTrace.ProposerPubkey.String(), + "parentHash": submission.BidTrace.ParentHash.String(), + "value": submission.BidTrace.Value.Dec(), + "payloadBytes": len(requestPayloadBytes), + }) + + ok := api.checkSubmissionSlotDetails(w, log, headSlot, payload.Version, submission.Timestamp, submission.BidTrace) + if !ok { + return + } + + builderPubkey := submission.BidTrace.BuilderPubkey + builderEntry, ok := api.checkBuilderEntry(w, log, builderPubkey) + if !ok { + return + } + + log = log.WithField("builderIsHighPrio", builderEntry.status.IsHighPrio) + + if !builderEntry.status.IsOptimistic { + log.Info("builder is not optimistic") + api.RespondError(w, http.StatusBadRequest, "builder is not optimistic") + return + } + ok = api.checkBuilderBid(w, log, submission.BidTrace, submission.TransactionsRoot, builderEntry) + if !ok { + return + } + + _, ok = api.checkSubmissionFeeRecipient(w, log, submission.BidTrace) + if !ok { + return + } + + _, ok = api.checkSubmissionPayloadAttrs(w, log, submission.BidTrace, submission.PrevRandao, submission.WithdrawalsRoot) + if !ok { + return + } + + nextTime = time.Now().UTC() + pf.Prechecks = uint64(nextTime.Sub(prevTime).Microseconds()) + prevTime = nextTime + + // Verify the signature + ok, err = ssz.VerifySignature(submission.BidTrace, api.opts.EthNetDetails.DomainBuilder, builderPubkey[:], submission.Signature[:]) + if err != nil { + log.WithError(err).Warn("failed verifying builder signature") + api.RespondError(w, http.StatusBadRequest, "failed verifying builder signature") + return + } else if !ok { + log.Warn("invalid builder signature") + api.RespondError(w, http.StatusBadRequest, "invalid signature") + return + } + + nextTime = time.Now().UTC() + pf.Signature = uint64(nextTime.Sub(prevTime).Microseconds()) + prevTime = nextTime + + // Create the redis pipeline tx + tx := api.redis.NewTxPipeline() + + // If cancellations are enabled, then abort now if this submission is not the latest one + if isCancellationEnabled { + // Ensure this request is still the latest one. This logic intentionally ignores the value of the bids and makes the current active bid the one + // that arrived at the relay last. This allows for builders to reduce the value of their bid (effectively cancel a high bid) by ensuring a lower + // bid arrives later. Even if the higher bid takes longer to simulate, by checking the receivedAt timestamp, this logic ensures that the low bid + // is not overwritten by the high bid. + // + // NOTE: this can lead to a rather tricky race condition. If a builder submits two blocks to the relay concurrently, then the randomness of network + // latency will make it impossible to predict which arrives first. Thus a high bid could unintentionally be overwritten by a low bid that happened + // to arrive a few microseconds later. If builders are submitting blocks at a frequency where they cannot reliably predict which bid will arrive at + // the relay first, they should instead use multiple pubkeys to avoid uninitentionally overwriting their own bids. + latestPayloadReceivedAt, err := api.redis.GetBuilderLatestPayloadReceivedAt(context.Background(), tx, submission.BidTrace.Slot, submission.BidTrace.BuilderPubkey.String(), submission.BidTrace.ParentHash.String(), submission.BidTrace.ProposerPubkey.String()) + if err != nil { + log.WithError(err).Error("failed getting latest payload receivedAt from redis") + } else if receivedAt.UnixMilli() < latestPayloadReceivedAt { + log.Infof("already have a newer payload: now=%d / prev=%d", receivedAt.UnixMilli(), latestPayloadReceivedAt) + api.RespondError(w, http.StatusBadRequest, "already using a newer payload") + return + } + } + + bfOpts := bidFloorOpts{ + w: w, + tx: tx, + log: log, + cancellationsEnabled: isCancellationEnabled, + bidTrace: submission.BidTrace, + } + floorBidValue, ok := api.checkFloorBidValue(bfOpts) + if !ok { + return + } + + nextTime = time.Now().UTC() + pf.RedisChecks = uint64(nextTime.Sub(prevTime).Microseconds()) + pf.Total = uint64(nextTime.Sub(receivedAt).Microseconds()) + + redisOpts := redisUpdateBidOpts{ + w: w, + tx: tx, + log: log, + cancellationsEnabled: isCancellationEnabled, + receivedAt: receivedAt, + floorBidValue: floorBidValue, + header: payload, + } + updateBidResult, ok := api.updateRedisBidForHeader(redisOpts) + if !ok { + return + } + + // Add fields to logs + log = log.WithFields(logrus.Fields{ + "timestampAfterBidUpdate": time.Now().UTC().UnixMilli(), + "wasBidSavedInRedis": updateBidResult.WasBidSaved, + "wasTopBidUpdated": updateBidResult.WasTopBidUpdated, + "topBidValue": updateBidResult.TopBidValue, + "prevTopBidValue": updateBidResult.PrevTopBidValue, + "profileRedisSavePayloadUs": updateBidResult.TimeSavePayload.Microseconds(), + "profileRedisUpdateTopBidUs": updateBidResult.TimeUpdateTopBid.Microseconds(), + "profileRedisUpdateFloorUs": updateBidResult.TimeUpdateFloor.Microseconds(), + }) + + var eligibleAt time.Time // will be set once the bid is ready + if updateBidResult.WasBidSaved { + // Bid is eligible to win the auction + eligibleAt = time.Now().UTC() + log = log.WithField("timestampEligibleAt", eligibleAt.UnixMilli()) + } + + nextTime = time.Now().UTC() + pf.RedisUpdate = uint64(nextTime.Sub(prevTime).Microseconds()) + pf.Total = uint64(nextTime.Sub(receivedAt).Microseconds()) + + // All done, log with profiling information + log.WithFields(logrus.Fields{ + "profileDecodeUs": pf.Decode, + "profilePrechecksUs": pf.Prechecks, + "profileSignatureUs": pf.Signature, + "profileRedisUpdateUs": pf.RedisUpdate, + "profileRedisChecksUs": pf.RedisChecks, + "profileTotalUs": pf.Total, + }).Info("received block from builder") + w.WriteHeader(http.StatusOK) + + // TODO: Save header submission to db +} + // --------------- // // INTERNAL APIS diff --git a/services/api/service_test.go b/services/api/service_test.go index 81779185..33a392e1 100644 --- a/services/api/service_test.go +++ b/services/api/service_test.go @@ -9,6 +9,7 @@ import ( "math/big" "net/http" "net/http/httptest" + "os" "testing" "time" @@ -34,12 +35,13 @@ import ( ) const ( - testGasLimit = uint64(30000000) - testSlot = uint64(42) - testParentHash = "0xbd3291854dc822b7ec585925cda0e18f06af28fa2886e15f52d52dd4b6f94ed6" - testWithdrawalsRoot = "0x7f6d156912a4cb1e74ee37e492ad883f7f7ac856d987b3228b517e490aa0189e" - testPrevRandao = "0x9962816e9d0a39fd4c80935338a741dc916d1545694e41eb5a505e1a3098f9e4" - testBuilderPubkey = "0xfa1ed37c3553d0ce1e9349b2c5063cf6e394d231c8d3e0df75e9462257c081543086109ffddaacc0aa76f33dc9661c83" + testGasLimit = uint64(30000000) + testSlot = uint64(42) + testParentHash = "0xbd3291854dc822b7ec585925cda0e18f06af28fa2886e15f52d52dd4b6f94ed6" + testWithdrawalsRoot = "0x7f6d156912a4cb1e74ee37e492ad883f7f7ac856d987b3228b517e490aa0189e" + testTransactionsRoot = "0x7f6d156912a4cb1e74ee37e492ad883f7f7ac856d987b3228b517e490aa0189e" + testPrevRandao = "0x9962816e9d0a39fd4c80935338a741dc916d1545694e41eb5a505e1a3098f9e4" + testBuilderPubkey = "0xfa1ed37c3553d0ce1e9349b2c5063cf6e394d231c8d3e0df75e9462257c081543086109ffddaacc0aa76f33dc9661c83" ) var ( @@ -218,11 +220,6 @@ func TestGetHeader(t *testing.T) { proposerPubkey := "0x6ae5932d1e248d987d51b58665b81848814202d7b23b343d20f2a167d12f07dcb01ca41c42fdd60b7fca9c4b90890792" builderPubkey := "0xfa1ed37c3553d0ce1e9349b2c5063cf6e394d231c8d3e0df75e9462257c081543086109ffddaacc0aa76f33dc9661c83" bidValue := uint256.NewInt(99) - trace := &common.BidTraceV2WithBlobFields{ - BidTrace: builderApiV1.BidTrace{ - Value: bidValue, - }, - } // request path path := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", slot, parentHash, proposerPubkey) @@ -235,7 +232,9 @@ func TestGetHeader(t *testing.T) { Version: spec.DataVersionCapella, } payload, getPayloadResp, getHeaderResp := common.CreateTestBlockSubmission(t, builderPubkey, bidValue, &opts) - _, err := backend.redis.SaveBidAndUpdateTopBid(context.Background(), backend.redis.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), false, nil) + submission, err := common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + _, err = backend.redis.SaveBidAndUpdateTopBid(context.Background(), backend.redis.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), false, nil) require.NoError(t, err) // Check 1: regular capella request works and returns a bid @@ -258,7 +257,9 @@ func TestGetHeader(t *testing.T) { Version: spec.DataVersionDeneb, } payload, getPayloadResp, getHeaderResp = common.CreateTestBlockSubmission(t, builderPubkey, bidValue, &opts) - _, err = backend.redis.SaveBidAndUpdateTopBid(context.Background(), backend.redis.NewPipeline(), trace, payload, getPayloadResp, getHeaderResp, time.Now(), false, nil) + submission, err = common.GetBlockSubmissionInfo(payload) + require.NoError(t, err) + _, err = backend.redis.SaveBidAndUpdateTopBid(context.Background(), backend.redis.NewPipeline(), submission.BidTrace, submission, getPayloadResp, getHeaderResp, time.Now(), false, nil) require.NoError(t, err) // Check 2: regular deneb request works and returns a bid @@ -530,6 +531,154 @@ func TestBuilderSubmitBlock(t *testing.T) { } } +func TestBuilderSubmitHeader(t *testing.T) { + type testHelper struct { + headSlot uint64 + submissionTimestamp int + parentHash string + feeRecipient string + withdrawalRoot string + prevRandao string + jsonReqSize int + sszReqSize int + jsonGzipReqSize int + sszGzipReqSize int + } + + testCases := []struct { + name string + filepath string + data testHelper + }{ + { + name: "Deneb", + filepath: "../../testdata/submitHeaderPayloadDeneb_Goerli.json", + data: testHelper{ + headSlot: 86, + submissionTimestamp: 1606825067, + parentHash: "0xb1bd772f909db1b6cbad8cf31745d3f2d692294998161369a5709c17a71f630f", + feeRecipient: "0x455E5AA18469bC6ccEF49594645666C587A3a71B", + withdrawalRoot: "0x3cb816ccf6bb079b4f462e81db1262064f321a4afa4ff32c1f7e0a1c603836af", + prevRandao: "0x6d414d3ffba7ba51155c3528739102c2889005940913b5d4c8031eed30764d4d", + jsonReqSize: 2859, + sszReqSize: 1243, + jsonGzipReqSize: 1461, + sszGzipReqSize: 1144, + }, + }, + } + path := "/relay/v1/builder/headers" + backend := newTestBackend(t, 1) + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + headSlot := testCase.data.headSlot + submissionSlot := headSlot + 1 + submissionTimestamp := testCase.data.submissionTimestamp + + // Payload attributes + payloadJSONFilename := testCase.filepath + parentHash := testCase.data.parentHash + feeRec, err := utils.HexToAddress(testCase.data.feeRecipient) + require.NoError(t, err) + withdrawalsRoot, err := utils.HexToHash(testCase.data.withdrawalRoot) + require.NoError(t, err) + prevRandao := testCase.data.prevRandao + + // Setup the test relay backend + backend.relay.headSlot.Store(headSlot) + backend.relay.denebEpoch = 2 + backend.relay.proposerDutiesMap = make(map[uint64]*common.BuilderGetValidatorsResponseEntry) + backend.relay.proposerDutiesMap[headSlot+1] = &common.BuilderGetValidatorsResponseEntry{ + Slot: headSlot, + Entry: &builderApiV1.SignedValidatorRegistration{ + Message: &builderApiV1.ValidatorRegistration{ + FeeRecipient: feeRec, + }, + }, + } + backend.relay.payloadAttributes = make(map[string]payloadAttributesHelper) + backend.relay.payloadAttributes[getPayloadAttributesKey(parentHash, submissionSlot)] = payloadAttributesHelper{ + slot: submissionSlot, + parentHash: parentHash, + payloadAttributes: beaconclient.PayloadAttributes{ + PrevRandao: prevRandao, + }, + withdrawalsRoot: phase0.Root(withdrawalsRoot), + } + + // Prepare the request payload + req := new(common.VersionedSubmitHeaderOptimistic) + requestPayloadJSONBytes, err := os.ReadFile(payloadJSONFilename) + require.NoError(t, err) + err = json.Unmarshal(requestPayloadJSONBytes, req) + require.NoError(t, err) + submission, err := common.GetHeaderSubmissionInfo(req) + require.NoError(t, err) + + // Update + switch req.Version { //nolint:exhaustive + case spec.DataVersionDeneb: + req.Deneb.Message.Slot = submissionSlot + req.Deneb.ExecutionPayloadHeader.Timestamp = uint64(submissionTimestamp) + default: + require.Fail(t, "unknown data version") + } + + backend.relay.blockBuildersCache = make(map[string]*blockBuilderCacheEntry) + backend.relay.blockBuildersCache[submission.BidTrace.BuilderPubkey.String()] = &blockBuilderCacheEntry{ + status: common.BuilderStatus{ + IsOptimistic: true, + }, + collateral: submission.BidTrace.Value.ToBig(), + } + + // Send JSON encoded request + reqJSONBytes, err := json.Marshal(req) + require.NoError(t, err) + require.Len(t, reqJSONBytes, testCase.data.jsonReqSize) + reqJSONBytes2, err := json.Marshal(req) + require.NoError(t, err) + require.Equal(t, reqJSONBytes, reqJSONBytes2) + rr := backend.requestBytes(http.MethodPost, path, reqJSONBytes, nil) + require.Contains(t, rr.Body.String(), "invalid signature") + require.Equal(t, http.StatusBadRequest, rr.Code) + + // Send SSZ encoded request + reqSSZBytes, err := req.MarshalSSZ() + require.NoError(t, err) + require.Len(t, reqSSZBytes, testCase.data.sszReqSize) + rr = backend.requestBytes(http.MethodPost, path, reqSSZBytes, map[string]string{ + "Content-Type": "application/octet-stream", + }) + require.Contains(t, rr.Body.String(), "invalid signature") + require.Equal(t, http.StatusBadRequest, rr.Code) + + // Send JSON+GZIP encoded request + headers := map[string]string{ + "Content-Encoding": "gzip", + } + jsonGzip := gzipBytes(t, reqJSONBytes) + require.Len(t, jsonGzip, testCase.data.jsonGzipReqSize) + rr = backend.requestBytes(http.MethodPost, path, jsonGzip, headers) + require.Contains(t, rr.Body.String(), "invalid signature") + require.Equal(t, http.StatusBadRequest, rr.Code) + + // Send SSZ+GZIP encoded request + headers = map[string]string{ + "Content-Type": "application/octet-stream", + "Content-Encoding": "gzip", + } + + sszGzip := gzipBytes(t, reqSSZBytes) + require.Len(t, sszGzip, testCase.data.sszGzipReqSize) + rr = backend.requestBytes(http.MethodPost, path, sszGzip, headers) + require.Contains(t, rr.Body.String(), "invalid signature") + require.Equal(t, http.StatusBadRequest, rr.Code) + }) + } +} + func TestCheckSubmissionFeeRecipient(t *testing.T) { cases := []struct { description string @@ -788,30 +937,8 @@ func TestCheckSubmissionPayloadAttrs(t *testing.T) { Message: &builderApiV1.BidTrace{ Slot: testSlot + 1, // submission for a future slot }, - ExecutionPayload: &capella.ExecutionPayload{}, - }, - }, - }, - expectOk: false, - }, - { - description: "failure_wrong_prev_randao", - attrs: payloadAttributesHelper{ - slot: testSlot, - payloadAttributes: beaconclient.PayloadAttributes{ - PrevRandao: testPrevRandao, - }, - }, - payload: &common.VersionedSubmitBlockRequest{ - VersionedSubmitBlockRequest: builderSpec.VersionedSubmitBlockRequest{ - Version: spec.DataVersionCapella, - Capella: &builderApiCapella.SubmitBlockRequest{ - Message: &builderApiV1.BidTrace{ - Slot: testSlot, - ParentHash: parentHash, - }, ExecutionPayload: &capella.ExecutionPayload{ - PrevRandao: [32]byte(parentHash), // use a different hash to cause an error + Withdrawals: []*capella.Withdrawal{}, }, }, }, @@ -819,7 +946,7 @@ func TestCheckSubmissionPayloadAttrs(t *testing.T) { expectOk: false, }, { - description: "failure_nil_withdrawals", + description: "failure_wrong_prev_randao", attrs: payloadAttributesHelper{ slot: testSlot, payloadAttributes: beaconclient.PayloadAttributes{ @@ -835,8 +962,8 @@ func TestCheckSubmissionPayloadAttrs(t *testing.T) { ParentHash: parentHash, }, ExecutionPayload: &capella.ExecutionPayload{ - PrevRandao: [32]byte(prevRandao), - Withdrawals: nil, // set to nil to cause an error + PrevRandao: [32]byte(parentHash), // use a different hash to cause an error + Withdrawals: []*capella.Withdrawal{}, }, }, }, @@ -887,7 +1014,9 @@ func TestCheckSubmissionPayloadAttrs(t *testing.T) { log := logrus.NewEntry(logger) submission, err := common.GetBlockSubmissionInfo(tc.payload) require.NoError(t, err) - _, ok := backend.relay.checkSubmissionPayloadAttrs(w, log, submission) + withdrawalsRoot, err := ComputeWithdrawalsRoot(submission.Withdrawals) + require.NoError(t, err) + _, ok := backend.relay.checkSubmissionPayloadAttrs(w, log, submission.BidTrace, submission.PrevRandao, withdrawalsRoot) require.Equal(t, tc.expectOk, ok) }) } @@ -995,7 +1124,7 @@ func TestCheckSubmissionSlotDetails(t *testing.T) { log := logrus.NewEntry(logger) submission, err := common.GetBlockSubmissionInfo(tc.payload) require.NoError(t, err) - ok := backend.relay.checkSubmissionSlotDetails(w, log, headSlot, tc.payload, submission) + ok := backend.relay.checkSubmissionSlotDetails(w, log, headSlot, tc.payload.Version, submission.Timestamp, submission.BidTrace) require.Equal(t, tc.expectOk, ok) }) } @@ -1063,6 +1192,93 @@ func TestCheckBuilderEntry(t *testing.T) { } } +func TestCheckBuilderBid(t *testing.T) { + testTransactionsRoot, err := utils.HexToHash(testTransactionsRoot) + require.NoError(t, err) + transactionsRoot := phase0.Root(testTransactionsRoot) + emptyRoot, err := utils.HexToHash(common.EmptyTxRoot) + require.NoError(t, err) + emptyTxRoot := phase0.Root(emptyRoot) + cases := []struct { + description string + bidTrace *builderApiV1.BidTrace + transactionsRoot phase0.Root + entry *blockBuilderCacheEntry + expectOk bool + }{ + { + description: "success", + entry: &blockBuilderCacheEntry{ + status: common.BuilderStatus{ + IsOptimistic: true, + }, + collateral: big.NewInt(100), + }, + transactionsRoot: transactionsRoot, + bidTrace: &builderApiV1.BidTrace{ + Slot: testSlot, + Value: uint256.NewInt(100), + }, + expectOk: true, + }, + { + description: "failure_zero_value", + entry: &blockBuilderCacheEntry{ + status: common.BuilderStatus{ + IsOptimistic: true, + }, + collateral: big.NewInt(100), + }, + transactionsRoot: transactionsRoot, + bidTrace: &builderApiV1.BidTrace{ + Slot: testSlot, + Value: uint256.NewInt(0), + }, + expectOk: false, + }, + { + description: "failure_empty_tx_root", + entry: &blockBuilderCacheEntry{ + status: common.BuilderStatus{ + IsOptimistic: true, + }, + collateral: big.NewInt(100), + }, + transactionsRoot: emptyTxRoot, + bidTrace: &builderApiV1.BidTrace{ + Slot: testSlot, + Value: uint256.NewInt(100), + }, + expectOk: false, + }, + { + description: "failure_below_collateral", + entry: &blockBuilderCacheEntry{ + status: common.BuilderStatus{ + IsOptimistic: true, + }, + collateral: big.NewInt(100), + }, + transactionsRoot: transactionsRoot, + bidTrace: &builderApiV1.BidTrace{ + Slot: testSlot, + Value: uint256.NewInt(101), + }, + expectOk: false, + }, + } + for _, tc := range cases { + t.Run(tc.description, func(t *testing.T) { + backend := newTestBackend(t, 1) + w := httptest.NewRecorder() + logger := logrus.New() + log := logrus.NewEntry(logger) + ok := backend.relay.checkBuilderBid(w, log, tc.bidTrace, tc.transactionsRoot, tc.entry) + require.Equal(t, tc.expectOk, ok) + }) + } +} + func TestCheckFloorBidValue(t *testing.T) { cases := []struct { description string @@ -1149,7 +1365,6 @@ func TestCheckFloorBidValue(t *testing.T) { logger := logrus.New() log := logrus.NewEntry(logger) tx := backend.redis.NewTxPipeline() - simResultC := make(chan *blockSimResult, 1) submission, err = common.GetBlockSubmissionInfo(tc.payload) require.NoError(t, err) bfOpts := bidFloorOpts{ @@ -1157,8 +1372,7 @@ func TestCheckFloorBidValue(t *testing.T) { tx: tx, log: log, cancellationsEnabled: tc.cancellationsEnabled, - simResultC: simResultC, - submission: submission, + bidTrace: submission.BidTrace, } floor, ok := backend.relay.checkFloorBidValue(bfOpts) require.Equal(t, tc.expectOk, ok) @@ -1238,7 +1452,7 @@ func TestUpdateRedis(t *testing.T) { log: log, cancellationsEnabled: tc.cancellationsEnabled, floorBidValue: floorValue, - payload: tc.payload, + block: tc.payload, } updateResp, getPayloadResp, ok := backend.relay.updateRedisBid(rOpts) require.Equal(t, tc.expectOk, ok) @@ -1250,6 +1464,67 @@ func TestUpdateRedis(t *testing.T) { } } +func TestUpdateHeaderRedis(t *testing.T) { + cases := []struct { + description string + cancellationsEnabled bool + floorValue string + header *common.VersionedSubmitHeaderOptimistic + expectOk bool + }{ + { + description: "success", + floorValue: "10", + header: &common.VersionedSubmitHeaderOptimistic{ + Version: spec.DataVersionDeneb, + Deneb: &common.DenebSubmitHeaderOptimistic{ + Message: &builderApiV1.BidTrace{ + Slot: testSlot, + Value: uint256.NewInt(1), + }, + BlobKZGCommitments: make([]deneb.KZGCommitment, 0), + ExecutionPayloadHeader: &deneb.ExecutionPayloadHeader{ + BaseFeePerGas: uint256.NewInt(1), + }, + }, + }, + expectOk: true, + }, + { + description: "failure_no_payload", + floorValue: "10", + header: nil, + expectOk: false, + }, + } + for _, tc := range cases { + t.Run(tc.description, func(t *testing.T) { + _, _, backend := startTestBackend(t) + w := httptest.NewRecorder() + logger := logrus.New() + log := logrus.NewEntry(logger) + tx := backend.redis.NewTxPipeline() + + floorValue := new(big.Int) + floorValue, ok := floorValue.SetString(tc.floorValue, 10) + require.True(t, ok) + rOpts := redisUpdateBidOpts{ + w: w, + tx: tx, + log: log, + cancellationsEnabled: tc.cancellationsEnabled, + floorBidValue: floorValue, + header: tc.header, + } + updateResp, ok := backend.relay.updateRedisBidForHeader(rOpts) + require.Equal(t, tc.expectOk, ok) + if ok { + require.NotNil(t, updateResp) + } + }) + } +} + func TestCheckProposerSignature(t *testing.T) { t.Run("Unsupported version", func(t *testing.T) { _, _, backend := startTestBackend(t) diff --git a/testdata/submitHeaderPayloadDeneb_Goerli.json b/testdata/submitHeaderPayloadDeneb_Goerli.json new file mode 100644 index 00000000..165b21ff --- /dev/null +++ b/testdata/submitHeaderPayloadDeneb_Goerli.json @@ -0,0 +1,41 @@ +{ + "message": { + "slot": "7433483", + "parent_hash": "0xb1bd772f909db1b6cbad8cf31745d3f2d692294998161369a5709c17a71f630f", + "block_hash": "0x195e2aac0a52cf26428336142e74eafd55d9228f315c2f2fe9253406ef9ef544", + "builder_pubkey": "0xb67a5148a03229926e34b190af81a82a81c4df66831c98c03a139778418dd09a3b542ced0022620d19f35781ece6dc36", + "proposer_pubkey": "0xb4095e6bb559d86bc9ea100566d1312bae955a4c83f86b6ac95cfb855987ce40e5bc6f702b1de035dedbed878cbe1ae9", + "proposer_fee_recipient": "0x455E5AA18469bC6ccEF49594645666C587A3a71B", + "gas_limit": "30000000", + "gas_used": "11203793", + "value": "11365972990140730" + }, + "header": { + "parent_hash": "0xb1bd772f909db1b6cbad8cf31745d3f2d692294998161369a5709c17a71f630f", + "fee_recipient": "0xb64a30399f7F6b0C154c2E7Af0a3ec7B0A5b131a", + "state_root": "0x353d2c98f202ca4959db5337cf8e6f4c8c79819498a2297332432796c5d8eef9", + "receipts_root": "0x25a5ced5930cfcc6e725796a9161586e6e086fc9dee37ef1d42987b484f335da", + "logs_bloom": "0x0000000200030018102004000201020000880800200010484000010201040000021080000500000040000b000000200820420200800860000018870212250040200a0000c40040090000022e0200800102100000204420008400040480000068200a00000600018010109880080018000020200820000d080102001000480982800000100102040002002101030000008001b202801400c20081000001400000022090c004602004020208c2800002000c040003008000c000400060280080000000600210000200001a20000014030884402100408000001400040240c468004010820000100021080000000000005142000408220848010008200408000004", + "prev_randao": "0x6d414d3ffba7ba51155c3528739102c2889005940913b5d4c8031eed30764d4d", + "block_number": "10402105", + "gas_limit": "30000000", + "gas_used": "11203793", + "timestamp": "1705709796", + "extra_data": "0x496c6c756d696e61746520446d6f63726174697a6520447374726962757465", + "base_fee_per_gas": "764", + "block_hash": "0x195e2aac0a52cf26428336142e74eafd55d9228f315c2f2fe9253406ef9ef544", + "transactions_root": "0x297d8366756cec251945166e19d1f54e67002a28422558e76ead5db3677798f9", + "withdrawals_root": "0x3cb816ccf6bb079b4f462e81db1262064f321a4afa4ff32c1f7e0a1c603836af", + "blob_gas_used": "786432", + "excess_blob_gas": "86507520" + }, + "blob_kzg_commitments": [ + "0x81a51fd61ec96b1aea13f02cd97f1f9fee16c9cde185913753a3f5a5041cee44a15ee8d44ea9dc91721fc7fc660bbdb4", + "0x94b5bad83bfc5ae5497642091c7b7772e3093dde8b08e1f82c9b6038088a8f8c31e5cc4d6e791a2ff1f6d68d69454641", + "0xaa61b98d8d0712768c4cd135592d59ac78c74e0a4af9a6ef15930dfc79956aea82dbe5678076a3864741be4bcd85bac2", + "0x856e43253dab176c81619eb4e559b2f7d609b2fed3cb1d14fe310e9d57fc32b1c155128a328e09b22eaf6599857ce6af", + "0x9314c48e5550108cbb324ad4c977162df4c352ead7f7622ddffdfac2d6ead14c78c5809a6f3e03563e9892c4c7bffdb4", + "0x88d99876da8d398e067c25612b5154deec197b5b88d7f0acee3f977ddc992aa5fdf59e8d126ef4fad8545fbb671aa5d5" + ], + "signature": "0x8432b14420a052d669dfb9b9431b1be6286f25e30e201f5430966e51a4f6a33cab3985f56204416a37d40f41879f68d406e9f3be3f8914c0cd289bdcb37e95ff1716c710ac6bd5fd59315ff483bd3bff1a59368868c01b3432e7b3ca4002c3f5" +}