diff --git a/beacon-chain/core/gloas/BUILD.bazel b/beacon-chain/core/gloas/BUILD.bazel new file mode 100644 index 0000000000..4adf805aa9 --- /dev/null +++ b/beacon-chain/core/gloas/BUILD.bazel @@ -0,0 +1,42 @@ +load("@prysm//tools/go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = ["payload.go"], + importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas", + visibility = ["//visibility:public"], + deps = [ + "//beacon-chain/core/electra:go_default_library", + "//beacon-chain/core/signing:go_default_library", + "//beacon-chain/state:go_default_library", + "//config/params:go_default_library", + "//consensus-types/interfaces:go_default_library", + "//crypto/bls:go_default_library", + "//encoding/ssz:go_default_library", + "//proto/engine/v1:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//time/slots:go_default_library", + "@com_github_pkg_errors//:go_default_library", + ], +) + +go_test( + name = "go_default_test", + srcs = ["payload_test.go"], + embed = [":go_default_library"], + deps = [ + "//beacon-chain/core/signing:go_default_library", + "//beacon-chain/state:go_default_library", + "//beacon-chain/state/state-native:go_default_library", + "//config/params:go_default_library", + "//consensus-types/blocks:go_default_library", + "//consensus-types/interfaces:go_default_library", + "//consensus-types/primitives:go_default_library", + "//crypto/bls:go_default_library", + "//encoding/ssz:go_default_library", + "//proto/engine/v1:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//testing/require:go_default_library", + "//time/slots:go_default_library", + ], +) diff --git a/beacon-chain/core/gloas/payload.go b/beacon-chain/core/gloas/payload.go new file mode 100644 index 0000000000..2dfc2280fc --- /dev/null +++ b/beacon-chain/core/gloas/payload.go @@ -0,0 +1,336 @@ +package gloas + +import ( + "bytes" + "context" + "fmt" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/electra" + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing" + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + "github.com/OffchainLabs/prysm/v7/crypto/bls" + "github.com/OffchainLabs/prysm/v7/encoding/ssz" + enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/time/slots" + "github.com/pkg/errors" +) + +// ProcessExecutionPayload processes the signed execution payload envelope for the Gloas fork. +// Spec v1.6.1 (pseudocode): +// def process_execution_payload(state, signed_envelope, execution_engine, verify=True): +// +// envelope = signed_envelope.message +// payload = envelope.payload +// +// if verify: assert verify_execution_payload_envelope_signature(state, signed_envelope) +// +// prev_root = hash_tree_root(state) +// if state.latest_block_header.state_root == Root(): +// state.latest_block_header.state_root = prev_root +// +// assert envelope.beacon_block_root == hash_tree_root(state.latest_block_header) +// assert envelope.slot == state.slot +// +// committed_bid = state.latest_execution_payload_bid +// assert envelope.builder_index == committed_bid.builder_index +// assert committed_bid.blob_kzg_commitments_root == hash_tree_root(envelope.blob_kzg_commitments) +// assert committed_bid.prev_randao == payload.prev_randao +// +// assert hash_tree_root(payload.withdrawals) == hash_tree_root(state.payload_expected_withdrawals) +// +// assert committed_bid.gas_limit == payload.gas_limit +// assert committed_bid.block_hash == payload.block_hash +// assert payload.parent_hash == state.latest_block_hash +// assert payload.timestamp == compute_time_at_slot(state, state.slot) +// assert ( +// len(envelope.blob_kzg_commitments) +// <= get_blob_parameters(get_current_epoch(state)).max_blobs_per_block +// ) +// versioned_hashes = [ +// kzg_commitment_to_versioned_hash(commitment) for commitment in envelope.blob_kzg_commitments +// ] +// requests = envelope.execution_requests +// assert execution_engine.verify_and_notify_new_payload( +// NewPayloadRequest( +// execution_payload=payload, +// versioned_hashes=versioned_hashes, +// parent_beacon_block_root=state.latest_block_header.parent_root, +// execution_requests=requests, +// ) +// ) +// +// for op in requests.deposits: process_deposit_request(state, op) +// for op in requests.withdrawals: process_withdrawal_request(state, op) +// for op in requests.consolidations: process_consolidation_request(state, op) +// +// payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] +// amount = payment.withdrawal.amount +// if amount > 0: +// exit_queue_epoch = compute_exit_epoch_and_update_churn(state, amount) +// payment.withdrawal.withdrawable_epoch = Epoch(exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) +// state.builder_pending_withdrawals.append(payment.withdrawal) +// state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = BuilderPendingPayment() +// +// state.execution_payload_availability[state.slot % SLOTS_PER_HISTORICAL_ROOT] = 0b1 +// state.latest_block_hash = payload.block_hash +// +// if verify: assert envelope.state_root == h +func ProcessExecutionPayload( + ctx context.Context, + st state.BeaconState, + signedEnvelope interfaces.ROSignedExecutionPayloadEnvelope, +) error { + // Verify the signature on the signed execution payload envelope + if err := verifyExecutionPayloadEnvelopeSignature(st, signedEnvelope); err != nil { + return errors.Wrap(err, "signature verification failed") + } + + envelope, err := signedEnvelope.Envelope() + if err != nil { + return errors.Wrap(err, "could not get envelope from signed envelope") + } + payload, err := envelope.Execution() + if err != nil { + return errors.Wrap(err, "could not get execution payload from envelope") + } + + latestHeader := st.LatestBlockHeader() + if len(latestHeader.StateRoot) == 0 || bytes.Equal(latestHeader.StateRoot, make([]byte, 32)) { + previousStateRoot, err := st.HashTreeRoot(ctx) + if err != nil { + return errors.Wrap(err, "could not compute state root") + } + latestHeader.StateRoot = previousStateRoot[:] + if err := st.SetLatestBlockHeader(latestHeader); err != nil { + return errors.Wrap(err, "could not set latest block header") + } + } + + blockHeaderRoot, err := latestHeader.HashTreeRoot() + if err != nil { + return errors.Wrap(err, "could not compute block header root") + } + beaconBlockRoot := envelope.BeaconBlockRoot() + if !bytes.Equal(beaconBlockRoot[:], blockHeaderRoot[:]) { + return errors.Errorf("envelope beacon block root does not match state latest block header root: envelope=%#x, header=%#x", beaconBlockRoot, blockHeaderRoot) + } + + if envelope.Slot() != st.Slot() { + return errors.Errorf("envelope slot does not match state slot: envelope=%d, state=%d", envelope.Slot(), st.Slot()) + } + + latestBid, err := st.LatestExecutionPayloadBid() + if err != nil { + return errors.Wrap(err, "could not get latest execution payload bid") + } + if latestBid == nil { + return errors.New("latest execution payload bid is nil") + } + if envelope.BuilderIndex() != latestBid.BuilderIndex() { + return errors.Errorf("envelope builder index does not match committed bid builder index: envelope=%d, bid=%d", envelope.BuilderIndex(), latestBid.BuilderIndex()) + } + + envelopeBlobCommitments := envelope.BlobKzgCommitments() + envelopeBlobRoot, err := ssz.KzgCommitmentsRoot(envelopeBlobCommitments) + if err != nil { + return errors.Wrap(err, "could not compute envelope blob KZG commitments root") + } + committedBlobRoot := latestBid.BlobKzgCommitmentsRoot() + if !bytes.Equal(committedBlobRoot[:], envelopeBlobRoot[:]) { + return errors.Errorf("committed bid blob KZG commitments root does not match envelope: bid=%#x, envelope=%#x", committedBlobRoot, envelopeBlobRoot) + } + + withdrawals, err := payload.Withdrawals() + if err != nil { + return errors.Wrap(err, "could not get withdrawals from payload") + } + cfg := params.BeaconConfig() + withdrawalsRoot, err := ssz.WithdrawalSliceRoot(withdrawals, cfg.MaxWithdrawalsPerPayload) + if err != nil { + return errors.Wrap(err, "could not compute withdrawals root") + } + + latestWithdrawalsRoot, err := st.LatestWithdrawalsRoot() + if err != nil { + return errors.Wrap(err, "could not get latest withdrawals root") + } + if !bytes.Equal(withdrawalsRoot[:], latestWithdrawalsRoot[:]) { + return errors.Errorf("payload withdrawals root does not match state latest withdrawals root: payload=%#x, state=%#x", withdrawalsRoot, latestWithdrawalsRoot) + } + + if latestBid.GasLimit() != payload.GasLimit() { + return errors.Errorf("committed bid gas limit does not match payload gas limit: bid=%d, payload=%d", latestBid.GasLimit(), payload.GasLimit()) + } + + latestBidPrevRandao := latestBid.PrevRandao() + if !bytes.Equal(payload.PrevRandao(), latestBidPrevRandao[:]) { + return errors.Errorf("payload prev randao does not match committed bid prev randao: payload=%#x, bid=%#x", payload.PrevRandao(), latestBidPrevRandao) + } + + bidBlockHash := latestBid.BlockHash() + payloadBlockHash := payload.BlockHash() + if !bytes.Equal(bidBlockHash[:], payloadBlockHash) { + return errors.Errorf("committed bid block hash does not match payload block hash: bid=%#x, payload=%#x", bidBlockHash, payloadBlockHash) + } + + latestBlockHash, err := st.LatestBlockHash() + if err != nil { + return errors.Wrap(err, "could not get latest block hash") + } + if !bytes.Equal(payload.ParentHash(), latestBlockHash[:]) { + return errors.Errorf("payload parent hash does not match state latest block hash: payload=%#x, state=%#x", payload.ParentHash(), latestBlockHash) + } + + t, err := slots.StartTime(st.GenesisTime(), st.Slot()) + if err != nil { + return errors.Wrap(err, "could not compute timestamp") + } + if payload.Timestamp() != uint64(t.Unix()) { + return errors.Errorf("payload timestamp does not match expected timestamp: payload=%d, expected=%d", payload.Timestamp(), uint64(t.Unix())) + } + + maxBlobsPerBlock := cfg.MaxBlobsPerBlock(envelope.Slot()) + if len(envelopeBlobCommitments) > maxBlobsPerBlock { + return errors.Errorf("too many blob KZG commitments: got=%d, max=%d", len(envelopeBlobCommitments), maxBlobsPerBlock) + } + + if err := processExecutionRequests(ctx, st, envelope.ExecutionRequests()); err != nil { + return errors.Wrap(err, "could not process execution requests") + } + + if err := queueBuilderPayment(ctx, st); err != nil { + return errors.Wrap(err, "could not queue builder payment") + } + + if err := st.SetExecutionPayloadAvailability(st.Slot(), true); err != nil { + return errors.Wrap(err, "could not set execution payload availability") + } + + if err := st.SetLatestBlockHash([32]byte(payload.BlockHash())); err != nil { + return errors.Wrap(err, "could not set latest block hash") + } + + r, err := st.HashTreeRoot(ctx) + if err != nil { + return errors.Wrap(err, "could not get hash tree root") + } + if r != envelope.StateRoot() { + return fmt.Errorf("state root mismatch: expected %#x, got %#x", envelope.StateRoot(), r) + } + + return nil +} + +// processExecutionRequests processes deposits, withdrawals, and consolidations from execution requests. +// Spec v1.6.1 (pseudocode): +// for op in requests.deposits: process_deposit_request(state, op) +// for op in requests.withdrawals: process_withdrawal_request(state, op) +// for op in requests.consolidations: process_consolidation_request(state, op) +func processExecutionRequests(ctx context.Context, st state.BeaconState, requests *enginev1.ExecutionRequests) error { + var err error + st, err = electra.ProcessDepositRequests(ctx, st, requests.Deposits) + if err != nil { + return errors.Wrap(err, "could not process deposit requests") + } + + st, err = electra.ProcessWithdrawalRequests(ctx, st, requests.Withdrawals) + if err != nil { + return errors.Wrap(err, "could not process withdrawal requests") + } + err = electra.ProcessConsolidationRequests(ctx, st, requests.Consolidations) + if err != nil { + return errors.Wrap(err, "could not process consolidation requests") + } + return nil +} + +// queueBuilderPayment implements the builder payment queuing logic for Gloas. +// Spec v1.6.1 (pseudocode): +// payment = state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] +// amount = payment.withdrawal.amount +// if amount > 0: +// +// exit_queue_epoch = compute_exit_epoch_and_update_churn(state, amount) +// payment.withdrawal.withdrawable_epoch = Epoch(exit_queue_epoch + MIN_VALIDATOR_WITHDRAWABILITY_DELAY) +// state.builder_pending_withdrawals.append(payment.withdrawal) +// +// state.builder_pending_payments[SLOTS_PER_EPOCH + state.slot % SLOTS_PER_EPOCH] = BuilderPendingPayment() +func queueBuilderPayment(ctx context.Context, st state.BeaconState) error { + payment, err := st.BuilderPendingPayment(st.Slot()) + if err != nil { + return errors.Wrap(err, "could not get builder pending payment") + } + + amount := payment.Withdrawal.Amount + if amount > 0 { + exitQueueEpoch, err := st.ExitEpochAndUpdateChurn(amount) + if err != nil { + return errors.Wrap(err, "could not compute exit epoch and update churn") + } + + minValidatorWithdrawabilityDelay := params.BeaconConfig().MinValidatorWithdrawabilityDelay + payment.Withdrawal.WithdrawableEpoch = exitQueueEpoch + minValidatorWithdrawabilityDelay + + if err := st.AppendBuilderPendingWithdrawal(payment.Withdrawal); err != nil { + return errors.Wrap(err, "could not append builder pending withdrawal") + } + } + + emptyPayment := ðpb.BuilderPendingPayment{ + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + }, + } + if err := st.SetBuilderPendingPayment(st.Slot(), emptyPayment); err != nil { + return errors.Wrap(err, "could not set builder pending payment") + } + + return nil +} + +// verifyExecutionPayloadEnvelopeSignature verifies the BLS signature on a signed execution payload envelope. +// Spec v1.6.1 (pseudocode): +// if verify: assert verify_execution_payload_envelope_signature(state, signed_envelope) +func verifyExecutionPayloadEnvelopeSignature(st state.BeaconState, signedEnvelope interfaces.ROSignedExecutionPayloadEnvelope) error { + envelope, err := signedEnvelope.Envelope() + if err != nil { + return fmt.Errorf("failed to get envelope: %w", err) + } + + builderPubkey := st.PubkeyAtIndex(envelope.BuilderIndex()) + publicKey, err := bls.PublicKeyFromBytes(builderPubkey[:]) + if err != nil { + return fmt.Errorf("invalid builder public key: %w", err) + } + + signatureBytes := signedEnvelope.Signature() + signature, err := bls.SignatureFromBytes(signatureBytes[:]) + if err != nil { + return fmt.Errorf("invalid signature format: %w", err) + } + + currentEpoch := slots.ToEpoch(envelope.Slot()) + domain, err := signing.Domain( + st.Fork(), + currentEpoch, + params.BeaconConfig().DomainBeaconBuilder, + st.GenesisValidatorsRoot(), + ) + if err != nil { + return fmt.Errorf("failed to compute signing domain: %w", err) + } + + signingRoot, err := signedEnvelope.SigningRoot(domain) + if err != nil { + return fmt.Errorf("failed to compute signing root: %w", err) + } + + if !signature.Verify(publicKey, signingRoot[:]) { + return fmt.Errorf("signature verification failed: %w", signing.ErrSigFailedToVerify) + } + + return nil +} diff --git a/beacon-chain/core/gloas/payload_test.go b/beacon-chain/core/gloas/payload_test.go new file mode 100644 index 0000000000..ec0246aa6e --- /dev/null +++ b/beacon-chain/core/gloas/payload_test.go @@ -0,0 +1,276 @@ +package gloas + +import ( + "bytes" + "context" + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing" + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/blocks" + "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + "github.com/OffchainLabs/prysm/v7/crypto/bls" + "github.com/OffchainLabs/prysm/v7/encoding/ssz" + enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/OffchainLabs/prysm/v7/time/slots" +) + +type payloadFixture struct { + state state.BeaconState + signed interfaces.ROSignedExecutionPayloadEnvelope + signedProto *ethpb.SignedExecutionPayloadEnvelope + envelope *ethpb.ExecutionPayloadEnvelope + payload *enginev1.ExecutionPayloadDeneb + slot primitives.Slot +} + +func buildPayloadFixture(t *testing.T, mutate func(payload *enginev1.ExecutionPayloadDeneb, bid *ethpb.ExecutionPayloadBid, envelope *ethpb.ExecutionPayloadEnvelope)) payloadFixture { + t.Helper() + + cfg := params.BeaconConfig() + slot := primitives.Slot(5) + builderIdx := primitives.ValidatorIndex(0) + + sk, err := bls.RandKey() + require.NoError(t, err) + pk := sk.PublicKey().Marshal() + + randao := bytes.Repeat([]byte{0xAA}, 32) + parentHash := bytes.Repeat([]byte{0xBB}, 32) + blockHash := bytes.Repeat([]byte{0xCC}, 32) + + withdrawals := []*enginev1.Withdrawal{ + {Index: 0, ValidatorIndex: 1, Address: bytes.Repeat([]byte{0x01}, 20), Amount: 0}, + } + withdrawalsRoot, err := ssz.WithdrawalSliceRoot(withdrawals, cfg.MaxWithdrawalsPerPayload) + require.NoError(t, err) + + blobCommitments := [][]byte{} + blobRoot, err := ssz.KzgCommitmentsRoot(blobCommitments) + require.NoError(t, err) + + payload := &enginev1.ExecutionPayloadDeneb{ + ParentHash: parentHash, + FeeRecipient: bytes.Repeat([]byte{0x01}, 20), + StateRoot: bytes.Repeat([]byte{0x02}, 32), + ReceiptsRoot: bytes.Repeat([]byte{0x03}, 32), + LogsBloom: bytes.Repeat([]byte{0x04}, 256), + PrevRandao: randao, + BlockNumber: 1, + GasLimit: 1, + GasUsed: 0, + Timestamp: 100, + ExtraData: []byte{}, + BaseFeePerGas: bytes.Repeat([]byte{0x05}, 32), + BlockHash: blockHash, + Transactions: [][]byte{}, + Withdrawals: withdrawals, + BlobGasUsed: 0, + ExcessBlobGas: 0, + } + + bid := ðpb.ExecutionPayloadBid{ + ParentBlockHash: parentHash, + ParentBlockRoot: bytes.Repeat([]byte{0xDD}, 32), + BlockHash: blockHash, + PrevRandao: randao, + GasLimit: 1, + BuilderIndex: builderIdx, + Slot: slot, + Value: 0, + ExecutionPayment: 0, + BlobKzgCommitmentsRoot: blobRoot[:], + FeeRecipient: bytes.Repeat([]byte{0xEE}, 20), + } + + header := ðpb.BeaconBlockHeader{ + Slot: slot, + ParentRoot: bytes.Repeat([]byte{0x11}, 32), + StateRoot: bytes.Repeat([]byte{0x22}, 32), + BodyRoot: bytes.Repeat([]byte{0x33}, 32), + } + headerRoot, err := header.HashTreeRoot() + require.NoError(t, err) + + envelope := ðpb.ExecutionPayloadEnvelope{ + Slot: slot, + BuilderIndex: builderIdx, + BeaconBlockRoot: headerRoot[:], + Payload: payload, + BlobKzgCommitments: blobCommitments, + ExecutionRequests: &enginev1.ExecutionRequests{}, + } + + if mutate != nil { + mutate(payload, bid, envelope) + } + + withdrawalsRoot, err = ssz.WithdrawalSliceRoot(payload.Withdrawals, cfg.MaxWithdrawalsPerPayload) + require.NoError(t, err) + blobRoot, err = ssz.KzgCommitmentsRoot(envelope.BlobKzgCommitments) + require.NoError(t, err) + + genesisRoot := bytes.Repeat([]byte{0xAB}, 32) + blockRoots := make([][]byte, cfg.SlotsPerHistoricalRoot) + stateRoots := make([][]byte, cfg.SlotsPerHistoricalRoot) + for i := range blockRoots { + blockRoots[i] = bytes.Repeat([]byte{0x44}, 32) + stateRoots[i] = bytes.Repeat([]byte{0x55}, 32) + } + randaoMixes := make([][]byte, cfg.EpochsPerHistoricalVector) + for i := range randaoMixes { + randaoMixes[i] = randao + } + + withdrawalCreds := make([]byte, 32) + withdrawalCreds[0] = cfg.ETH1AddressWithdrawalPrefixByte + + eth1Data := ðpb.Eth1Data{ + DepositRoot: bytes.Repeat([]byte{0x66}, 32), + DepositCount: 0, + BlockHash: bytes.Repeat([]byte{0x77}, 32), + } + + vals := []*ethpb.Validator{ + { + PublicKey: pk, + WithdrawalCredentials: withdrawalCreds, + EffectiveBalance: cfg.MinActivationBalance + 1_000, + }, + } + balances := []uint64{cfg.MinActivationBalance + 1_000} + + payments := make([]*ethpb.BuilderPendingPayment, cfg.SlotsPerEpoch*2) + for i := range payments { + payments[i] = ðpb.BuilderPendingPayment{ + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: make([]byte, 20), + }, + } + } + + executionPayloadAvailability := make([]byte, cfg.SlotsPerHistoricalRoot/8) + + genesisTime := uint64(0) + slotSeconds := cfg.SecondsPerSlot * uint64(slot) + if payload.Timestamp > slotSeconds { + genesisTime = payload.Timestamp - slotSeconds + } + + stProto := ðpb.BeaconStateGloas{ + Slot: slot, + GenesisTime: genesisTime, + GenesisValidatorsRoot: genesisRoot, + Fork: ðpb.Fork{ + CurrentVersion: bytes.Repeat([]byte{0x01}, 4), + PreviousVersion: bytes.Repeat([]byte{0x01}, 4), + Epoch: 0, + }, + LatestBlockHeader: header, + BlockRoots: blockRoots, + StateRoots: stateRoots, + RandaoMixes: randaoMixes, + Eth1Data: eth1Data, + Validators: vals, + Balances: balances, + LatestBlockHash: payload.ParentHash, + LatestWithdrawalsRoot: withdrawalsRoot[:], + LatestExecutionPayloadBid: bid, + BuilderPendingPayments: payments, + ExecutionPayloadAvailability: executionPayloadAvailability, + BuilderPendingWithdrawals: []*ethpb.BuilderPendingWithdrawal{}, + } + + st, err := state_native.InitializeFromProtoGloas(stProto) + require.NoError(t, err) + + expected := st.Copy() + ctx := context.Background() + require.NoError(t, processExecutionRequests(ctx, expected, envelope.ExecutionRequests)) + require.NoError(t, queueBuilderPayment(ctx, expected)) + require.NoError(t, expected.SetExecutionPayloadAvailability(slot, true)) + var blockHashArr [32]byte + copy(blockHashArr[:], payload.BlockHash) + require.NoError(t, expected.SetLatestBlockHash(blockHashArr)) + expectedRoot, err := expected.HashTreeRoot(ctx) + require.NoError(t, err) + envelope.StateRoot = expectedRoot[:] + + epoch := slots.ToEpoch(slot) + domain, err := signing.Domain(st.Fork(), epoch, cfg.DomainBeaconBuilder, st.GenesisValidatorsRoot()) + require.NoError(t, err) + signingRoot, err := signing.ComputeSigningRoot(envelope, domain) + require.NoError(t, err) + signature := sk.Sign(signingRoot[:]).Marshal() + + signedProto := ðpb.SignedExecutionPayloadEnvelope{ + Message: envelope, + Signature: signature, + } + signed, err := blocks.WrappedROSignedExecutionPayloadEnvelope(signedProto) + require.NoError(t, err) + + return payloadFixture{ + state: st, + signed: signed, + signedProto: signedProto, + envelope: envelope, + payload: payload, + slot: slot, + } +} + +func TestProcessExecutionPayload_Success(t *testing.T) { + fixture := buildPayloadFixture(t, nil) + require.NoError(t, ProcessExecutionPayload(t.Context(), fixture.state, fixture.signed)) + + latestHash, err := fixture.state.LatestBlockHash() + require.NoError(t, err) + var expectedHash [32]byte + copy(expectedHash[:], fixture.payload.BlockHash) + require.Equal(t, expectedHash, latestHash) + + payment, err := fixture.state.BuilderPendingPayment(fixture.slot) + require.NoError(t, err) + require.NotNil(t, payment) + require.Equal(t, primitives.Gwei(0), payment.Withdrawal.Amount) +} + +func TestProcessExecutionPayload_PrevRandaoMismatch(t *testing.T) { + fixture := buildPayloadFixture(t, func(_ *enginev1.ExecutionPayloadDeneb, bid *ethpb.ExecutionPayloadBid, _ *ethpb.ExecutionPayloadEnvelope) { + bid.PrevRandao = bytes.Repeat([]byte{0xFF}, 32) + }) + + err := ProcessExecutionPayload(t.Context(), fixture.state, fixture.signed) + require.ErrorContains(t, "prev randao", err) +} + +func TestQueueBuilderPayment_ZeroAmountClearsSlot(t *testing.T) { + fixture := buildPayloadFixture(t, nil) + + require.NoError(t, queueBuilderPayment(t.Context(), fixture.state)) + + payment, err := fixture.state.BuilderPendingPayment(fixture.slot) + require.NoError(t, err) + require.NotNil(t, payment) + require.Equal(t, primitives.Gwei(0), payment.Withdrawal.Amount) +} + +func TestVerifyExecutionPayloadEnvelopeSignature_InvalidSig(t *testing.T) { + fixture := buildPayloadFixture(t, nil) + + signedProto := ðpb.SignedExecutionPayloadEnvelope{ + Message: fixture.signedProto.Message, + Signature: bytes.Repeat([]byte{0xFF}, 96), + } + badSigned, err := blocks.WrappedROSignedExecutionPayloadEnvelope(signedProto) + require.NoError(t, err) + + err = verifyExecutionPayloadEnvelopeSignature(fixture.state, badSigned) + require.ErrorContains(t, "invalid signature format", err) +} diff --git a/beacon-chain/state/BUILD.bazel b/beacon-chain/state/BUILD.bazel index af0a4bc5ae..82b53b59b8 100644 --- a/beacon-chain/state/BUILD.bazel +++ b/beacon-chain/state/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "error.go", "interfaces.go", + "interfaces_gloas.go", "prometheus.go", ], importpath = "github.com/OffchainLabs/prysm/v7/beacon-chain/state", diff --git a/beacon-chain/state/interfaces.go b/beacon-chain/state/interfaces.go index c884a92d50..f12043bd4c 100644 --- a/beacon-chain/state/interfaces.go +++ b/beacon-chain/state/interfaces.go @@ -63,6 +63,7 @@ type ReadOnlyBeaconState interface { ReadOnlyDeposits ReadOnlyConsolidations ReadOnlyProposerLookahead + ReadOnlyGloasFields ToProtoUnsafe() any ToProto() any GenesisTime() time.Time @@ -99,6 +100,7 @@ type WriteOnlyBeaconState interface { WriteOnlyDeposits WriteOnlyProposerLookahead SetGenesisTime(val time.Time) error + WriteOnlyGloasFields SetGenesisValidatorsRoot(val []byte) error SetSlot(val primitives.Slot) error SetFork(val *ethpb.Fork) error diff --git a/beacon-chain/state/interfaces_gloas.go b/beacon-chain/state/interfaces_gloas.go new file mode 100644 index 0000000000..c557b12414 --- /dev/null +++ b/beacon-chain/state/interfaces_gloas.go @@ -0,0 +1,25 @@ +package state + +import ( + "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +type WriteOnlyGloasFields interface { + SetExecutionPayloadBid(h interfaces.ROExecutionPayloadBid) error + SetBuilderPendingPayment(index primitives.Slot, payment *ethpb.BuilderPendingPayment) error + SetLatestBlockHash(hash [32]byte) error + SetExecutionPayloadAvailability(index primitives.Slot, available bool) error + SetBuilderPendingPayments(payments []*ethpb.BuilderPendingPayment) error + AppendBuilderPendingWithdrawal(withdrawal *ethpb.BuilderPendingWithdrawal) error +} + +type ReadOnlyGloasFields interface { + LatestBlockHash() ([32]byte, error) + BuilderPendingPayment(slot primitives.Slot) (*ethpb.BuilderPendingPayment, error) + BuilderPendingPayments() ([]*ethpb.BuilderPendingPayment, error) + BuilderPendingWithdrawals() ([]*ethpb.BuilderPendingWithdrawal, error) + LatestExecutionPayloadBid() (interfaces.ROExecutionPayloadBid, error) + LatestWithdrawalsRoot() ([32]byte, error) +} diff --git a/beacon-chain/state/state-native/BUILD.bazel b/beacon-chain/state/state-native/BUILD.bazel index 597d3e6496..67d06d831d 100644 --- a/beacon-chain/state/state-native/BUILD.bazel +++ b/beacon-chain/state/state-native/BUILD.bazel @@ -14,6 +14,7 @@ go_library( "getters_deposits.go", "getters_eth1.go", "getters_exit.go", + "getters_gloas.go", "getters_misc.go", "getters_participation.go", "getters_payload_header.go", @@ -36,6 +37,7 @@ go_library( "setters_deposit_requests.go", "setters_deposits.go", "setters_eth1.go", + "setters_gloas.go", "setters_misc.go", "setters_participation.go", "setters_payload_header.go", @@ -97,6 +99,7 @@ go_test( "getters_deposit_requests_test.go", "getters_deposits_test.go", "getters_exit_test.go", + "getters_gloas_test.go", "getters_participation_test.go", "getters_setters_lookahead_test.go", "getters_test.go", @@ -113,6 +116,7 @@ go_test( "setters_deposit_requests_test.go", "setters_deposits_test.go", "setters_eth1_test.go", + "setters_gloas_test.go", "setters_misc_test.go", "setters_participation_test.go", "setters_payload_header_test.go", diff --git a/beacon-chain/state/state-native/getters_gloas.go b/beacon-chain/state/state-native/getters_gloas.go new file mode 100644 index 0000000000..c028ebca50 --- /dev/null +++ b/beacon-chain/state/state-native/getters_gloas.go @@ -0,0 +1,80 @@ +package state_native + +import ( + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/blocks" + "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +// LatestWithdrawalsRoot returns the root of the latest withdrawals. +func (b *BeaconState) LatestWithdrawalsRoot() ([32]byte, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if b.latestWithdrawalsRoot == nil { + return [32]byte{}, nil + } + + return [32]byte(b.latestWithdrawalsRoot), nil +} + +// LatestBlockHash returns the hash of the latest execution block. +func (b *BeaconState) LatestBlockHash() ([32]byte, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if b.latestBlockHash == nil { + return [32]byte{}, nil + } + + return [32]byte(b.latestBlockHash), nil +} + +// BuilderPendingPayment returns a specific builder pending payment for the given slot. +func (b *BeaconState) BuilderPendingPayment(slot primitives.Slot) (*ethpb.BuilderPendingPayment, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch + paymentIndex := slotsPerEpoch + (slot % slotsPerEpoch) + + return ethpb.CopyBuilderPendingPayment(b.builderPendingPayments[paymentIndex]), nil +} + +// BuilderPendingPayments returns the builder pending payments. +func (b *BeaconState) BuilderPendingPayments() ([]*ethpb.BuilderPendingPayment, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if b.builderPendingPayments == nil { + return make([]*ethpb.BuilderPendingPayment, 0), nil + } + + return ethpb.CopyBuilderPendingPaymentSlice(b.builderPendingPayments), nil +} + +// BuilderPendingWithdrawals returns the builder pending withdrawals. +func (b *BeaconState) BuilderPendingWithdrawals() ([]*ethpb.BuilderPendingWithdrawal, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if b.builderPendingWithdrawals == nil { + return make([]*ethpb.BuilderPendingWithdrawal, 0), nil + } + + return ethpb.CopyBuilderPendingWithdrawalSlice(b.builderPendingWithdrawals), nil +} + +// LatestExecutionPayloadBid returns the cached latest execution payload bid for Gloas. +func (b *BeaconState) LatestExecutionPayloadBid() (interfaces.ROExecutionPayloadBid, error) { + b.lock.RLock() + defer b.lock.RUnlock() + + if b.latestExecutionPayloadBid == nil { + return nil, nil + } + + return blocks.WrappedROExecutionPayloadBid(b.latestExecutionPayloadBid.Copy()) +} diff --git a/beacon-chain/state/state-native/getters_gloas_test.go b/beacon-chain/state/state-native/getters_gloas_test.go new file mode 100644 index 0000000000..9b020d1fb4 --- /dev/null +++ b/beacon-chain/state/state-native/getters_gloas_test.go @@ -0,0 +1,93 @@ +package state_native_test + +import ( + "testing" + + state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/assert" + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestLatestWithdrawalsRoot(t *testing.T) { + t.Run("nil root returns zero", func(t *testing.T) { + s, err := state_native.InitializeFromProtoGloas(ðpb.BeaconStateGloas{}) + require.NoError(t, err) + + root, err := s.LatestWithdrawalsRoot() + require.NoError(t, err) + assert.Equal(t, [32]byte{}, root) + }) + + t.Run("returns value", func(t *testing.T) { + var want [32]byte + copy(want[:], []byte("withdrawals-root")) + + s, err := state_native.InitializeFromProtoGloas(ðpb.BeaconStateGloas{ + LatestWithdrawalsRoot: want[:], + }) + require.NoError(t, err) + + root, err := s.LatestWithdrawalsRoot() + require.NoError(t, err) + assert.Equal(t, want, root) + }) +} + +func TestLatestBlockHash(t *testing.T) { + t.Run("nil hash returns zero", func(t *testing.T) { + s, err := state_native.InitializeFromProtoGloas(ðpb.BeaconStateGloas{}) + require.NoError(t, err) + + hash, err := s.LatestBlockHash() + require.NoError(t, err) + assert.Equal(t, [32]byte{}, hash) + }) + + t.Run("returns value", func(t *testing.T) { + var want [32]byte + copy(want[:], []byte("latest-block-hash")) + + s, err := state_native.InitializeFromProtoGloas(ðpb.BeaconStateGloas{ + LatestBlockHash: want[:], + }) + require.NoError(t, err) + + hash, err := s.LatestBlockHash() + require.NoError(t, err) + assert.Equal(t, want, hash) + }) +} + +func TestBuilderPendingPayment(t *testing.T) { + t.Run("returns payment for slot", func(t *testing.T) { + slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch + slot := primitives.Slot(7) + paymentIndex := slotsPerEpoch + (slot % slotsPerEpoch) + payments := make([]*ethpb.BuilderPendingPayment, slotsPerEpoch*2) + payment := ðpb.BuilderPendingPayment{ + Weight: 11, + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, + Amount: 99, + BuilderIndex: 3, + WithdrawableEpoch: 5, + }, + } + payments[paymentIndex] = payment + + s, err := state_native.InitializeFromProtoGloas(ðpb.BeaconStateGloas{ + BuilderPendingPayments: payments, + }) + require.NoError(t, err) + + got, err := s.BuilderPendingPayment(slot) + require.NoError(t, err) + require.DeepEqual(t, payment, got) + + got.Withdrawal.FeeRecipient[0] = 9 + require.Equal(t, byte(1), payment.Withdrawal.FeeRecipient[0]) + }) +} diff --git a/beacon-chain/state/state-native/setters_gloas.go b/beacon-chain/state/state-native/setters_gloas.go new file mode 100644 index 0000000000..d32dbdee14 --- /dev/null +++ b/beacon-chain/state/state-native/setters_gloas.go @@ -0,0 +1,101 @@ +package state_native + +import ( + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native/types" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +func (b *BeaconState) SetExecutionPayloadBid(h interfaces.ROExecutionPayloadBid) error { + b.lock.Lock() + defer b.lock.Unlock() + + parentBlockHash := h.ParentBlockHash() + parentBlockRoot := h.ParentBlockRoot() + blockHash := h.BlockHash() + randao := h.PrevRandao() + blobKzgCommitmentsRoot := h.BlobKzgCommitmentsRoot() + feeRecipient := h.FeeRecipient() + b.latestExecutionPayloadBid = ðpb.ExecutionPayloadBid{ + ParentBlockHash: parentBlockHash[:], + ParentBlockRoot: parentBlockRoot[:], + BlockHash: blockHash[:], + PrevRandao: randao[:], + GasLimit: h.GasLimit(), + BuilderIndex: h.BuilderIndex(), + Slot: h.Slot(), + Value: h.Value(), + ExecutionPayment: h.ExecutionPayment(), + BlobKzgCommitmentsRoot: blobKzgCommitmentsRoot[:], + FeeRecipient: feeRecipient[:], + } + b.markFieldAsDirty(types.LatestExecutionPayloadBid) + + return nil +} + +// SetBuilderPendingPayment sets a builder pending payment for the specified slot. +func (b *BeaconState) SetBuilderPendingPayment(slot primitives.Slot, payment *ethpb.BuilderPendingPayment) error { + b.lock.Lock() + defer b.lock.Unlock() + + slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch + paymentIndex := slotsPerEpoch + (slot % slotsPerEpoch) + + b.builderPendingPayments[paymentIndex] = ethpb.CopyBuilderPendingPayment(payment) + + b.markFieldAsDirty(types.BuilderPendingPayments) + return nil +} + +// SetLatestBlockHash sets the latest execution block hash. +func (b *BeaconState) SetLatestBlockHash(hash [32]byte) error { + b.lock.Lock() + defer b.lock.Unlock() + + b.latestBlockHash = hash[:] + b.markFieldAsDirty(types.LatestBlockHash) + return nil +} + +// SetExecutionPayloadAvailability sets the execution payload availability bit for a specific slot. +func (b *BeaconState) SetExecutionPayloadAvailability(index primitives.Slot, available bool) error { + b.lock.Lock() + defer b.lock.Unlock() + + bitIndex := index % params.BeaconConfig().SlotsPerHistoricalRoot + byteIndex := bitIndex / 8 + bitPosition := bitIndex % 8 + + // Set or clear the bit + if available { + b.executionPayloadAvailability[byteIndex] |= 1 << bitPosition + } else { + b.executionPayloadAvailability[byteIndex] &^= 1 << bitPosition + } + + b.markFieldAsDirty(types.ExecutionPayloadAvailability) + return nil +} + +// SetBuilderPendingPayments sets the entire builder pending payments array. +func (b *BeaconState) SetBuilderPendingPayments(payments []*ethpb.BuilderPendingPayment) error { + b.lock.Lock() + defer b.lock.Unlock() + + b.builderPendingPayments = ethpb.CopyBuilderPendingPaymentSlice(payments) + b.markFieldAsDirty(types.BuilderPendingPayments) + return nil +} + +// AppendBuilderPendingWithdrawal appends a builder pending withdrawal to the list. +func (b *BeaconState) AppendBuilderPendingWithdrawal(withdrawal *ethpb.BuilderPendingWithdrawal) error { + b.lock.Lock() + defer b.lock.Unlock() + + b.builderPendingWithdrawals = append(b.builderPendingWithdrawals, ethpb.CopyBuilderPendingWithdrawal(withdrawal)) + b.markFieldAsDirty(types.BuilderPendingWithdrawals) + return nil +} diff --git a/beacon-chain/state/state-native/setters_gloas_test.go b/beacon-chain/state/state-native/setters_gloas_test.go new file mode 100644 index 0000000000..65b9bc7712 --- /dev/null +++ b/beacon-chain/state/state-native/setters_gloas_test.go @@ -0,0 +1,125 @@ +package state_native + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native/types" + "github.com/OffchainLabs/prysm/v7/config/params" + "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" + "github.com/OffchainLabs/prysm/v7/testing/require" +) + +func TestSetBuilderPendingPayment(t *testing.T) { + slotsPerEpoch := params.BeaconConfig().SlotsPerEpoch + slot := primitives.Slot(7) + paymentIndex := slotsPerEpoch + (slot % slotsPerEpoch) + payment := ðpb.BuilderPendingPayment{ + Weight: 11, + Withdrawal: ðpb.BuilderPendingWithdrawal{ + FeeRecipient: []byte{1, 2, 3, 4, 5}, + Amount: 99, + BuilderIndex: 3, + WithdrawableEpoch: 5, + }, + } + + state := &BeaconState{ + builderPendingPayments: make([]*ethpb.BuilderPendingPayment, slotsPerEpoch*2), + dirtyFields: make(map[types.FieldIndex]bool), + } + + require.NoError(t, state.SetBuilderPendingPayment(slot, payment)) + require.Equal(t, true, state.dirtyFields[types.BuilderPendingPayments]) + require.DeepEqual(t, payment, state.builderPendingPayments[paymentIndex]) + + state.builderPendingPayments[paymentIndex].Withdrawal.FeeRecipient[0] = 9 + require.Equal(t, byte(1), payment.Withdrawal.FeeRecipient[0]) +} + +func TestSetLatestBlockHash(t *testing.T) { + var hash [32]byte + copy(hash[:], []byte("latest-block-hash")) + + state := &BeaconState{ + dirtyFields: make(map[types.FieldIndex]bool), + } + + require.NoError(t, state.SetLatestBlockHash(hash)) + require.Equal(t, true, state.dirtyFields[types.LatestBlockHash]) + require.DeepEqual(t, hash[:], state.latestBlockHash) +} + +func TestSetExecutionPayloadAvailability(t *testing.T) { + state := &BeaconState{ + executionPayloadAvailability: make([]byte, params.BeaconConfig().SlotsPerHistoricalRoot/8), + dirtyFields: make(map[types.FieldIndex]bool), + } + + slot := primitives.Slot(10) + bitIndex := slot % params.BeaconConfig().SlotsPerHistoricalRoot + byteIndex := bitIndex / 8 + bitPosition := bitIndex % 8 + + require.NoError(t, state.SetExecutionPayloadAvailability(slot, true)) + require.Equal(t, true, state.dirtyFields[types.ExecutionPayloadAvailability]) + require.Equal(t, byte(1< size || o9 > o11 { + if o11 = ssz.ReadOffset(buf[700:704]); o11 > size || o9 > o11 { return ssz.ErrOffset } @@ -1105,7 +1134,7 @@ func (b *BeaconBlockBodyGloas) UnmarshalSSZ(buf []byte) error { // SizeSSZ returns the ssz encoded size in bytes for the BeaconBlockBodyGloas object func (b *BeaconBlockBodyGloas) SizeSSZ() (size int) { - size = 664 + size = 704 // Field (3) 'ProposerSlashings' size += len(b.ProposerSlashings) * 416 @@ -1408,7 +1437,7 @@ func (b *BeaconStateGloas) MarshalSSZ() ([]byte, error) { // MarshalSSZTo ssz marshals the BeaconStateGloas object to a target array func (b *BeaconStateGloas) MarshalSSZTo(buf []byte) (dst []byte, err error) { dst = buf - offset := int(2741821) + offset := int(2741861) // Field (0) 'GenesisTime' dst = ssz.MarshalUint64(dst, b.GenesisTime) @@ -1795,7 +1824,7 @@ func (b *BeaconStateGloas) MarshalSSZTo(buf []byte) (dst []byte, err error) { func (b *BeaconStateGloas) UnmarshalSSZ(buf []byte) error { var err error size := uint64(len(buf)) - if size < 2741821 { + if size < 2741861 { return ssz.ErrSize } @@ -1853,7 +1882,7 @@ func (b *BeaconStateGloas) UnmarshalSSZ(buf []byte) error { return ssz.ErrOffset } - if o7 != 2741821 { + if o7 != 2741861 { return ssz.ErrInvalidVariableOffset } @@ -1963,65 +1992,65 @@ func (b *BeaconStateGloas) UnmarshalSSZ(buf []byte) error { if b.LatestExecutionPayloadBid == nil { b.LatestExecutionPayloadBid = new(ExecutionPayloadBid) } - if err = b.LatestExecutionPayloadBid.UnmarshalSSZ(buf[2736629:2736809]); err != nil { + if err = b.LatestExecutionPayloadBid.UnmarshalSSZ(buf[2736629:2736849]); err != nil { return err } // Field (25) 'NextWithdrawalIndex' - b.NextWithdrawalIndex = ssz.UnmarshallUint64(buf[2736809:2736817]) + b.NextWithdrawalIndex = ssz.UnmarshallUint64(buf[2736849:2736857]) // Field (26) 'NextWithdrawalValidatorIndex' - b.NextWithdrawalValidatorIndex = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.ValidatorIndex(ssz.UnmarshallUint64(buf[2736817:2736825])) + b.NextWithdrawalValidatorIndex = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.ValidatorIndex(ssz.UnmarshallUint64(buf[2736857:2736865])) // Offset (27) 'HistoricalSummaries' - if o27 = ssz.ReadOffset(buf[2736825:2736829]); o27 > size || o21 > o27 { + if o27 = ssz.ReadOffset(buf[2736865:2736869]); o27 > size || o21 > o27 { return ssz.ErrOffset } // Field (28) 'DepositRequestsStartIndex' - b.DepositRequestsStartIndex = ssz.UnmarshallUint64(buf[2736829:2736837]) + b.DepositRequestsStartIndex = ssz.UnmarshallUint64(buf[2736869:2736877]) // Field (29) 'DepositBalanceToConsume' - b.DepositBalanceToConsume = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Gwei(ssz.UnmarshallUint64(buf[2736837:2736845])) + b.DepositBalanceToConsume = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Gwei(ssz.UnmarshallUint64(buf[2736877:2736885])) // Field (30) 'ExitBalanceToConsume' - b.ExitBalanceToConsume = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Gwei(ssz.UnmarshallUint64(buf[2736845:2736853])) + b.ExitBalanceToConsume = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Gwei(ssz.UnmarshallUint64(buf[2736885:2736893])) // Field (31) 'EarliestExitEpoch' - b.EarliestExitEpoch = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch(ssz.UnmarshallUint64(buf[2736853:2736861])) + b.EarliestExitEpoch = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch(ssz.UnmarshallUint64(buf[2736893:2736901])) // Field (32) 'ConsolidationBalanceToConsume' - b.ConsolidationBalanceToConsume = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Gwei(ssz.UnmarshallUint64(buf[2736861:2736869])) + b.ConsolidationBalanceToConsume = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Gwei(ssz.UnmarshallUint64(buf[2736901:2736909])) // Field (33) 'EarliestConsolidationEpoch' - b.EarliestConsolidationEpoch = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch(ssz.UnmarshallUint64(buf[2736869:2736877])) + b.EarliestConsolidationEpoch = github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Epoch(ssz.UnmarshallUint64(buf[2736909:2736917])) // Offset (34) 'PendingDeposits' - if o34 = ssz.ReadOffset(buf[2736877:2736881]); o34 > size || o27 > o34 { + if o34 = ssz.ReadOffset(buf[2736917:2736921]); o34 > size || o27 > o34 { return ssz.ErrOffset } // Offset (35) 'PendingPartialWithdrawals' - if o35 = ssz.ReadOffset(buf[2736881:2736885]); o35 > size || o34 > o35 { + if o35 = ssz.ReadOffset(buf[2736921:2736925]); o35 > size || o34 > o35 { return ssz.ErrOffset } // Offset (36) 'PendingConsolidations' - if o36 = ssz.ReadOffset(buf[2736885:2736889]); o36 > size || o35 > o36 { + if o36 = ssz.ReadOffset(buf[2736925:2736929]); o36 > size || o35 > o36 { return ssz.ErrOffset } // Field (37) 'ProposerLookahead' b.ProposerLookahead = ssz.ExtendUint64(b.ProposerLookahead, 64) for ii := 0; ii < 64; ii++ { - b.ProposerLookahead[ii] = ssz.UnmarshallUint64(buf[2736889:2737401][ii*8 : (ii+1)*8]) + b.ProposerLookahead[ii] = ssz.UnmarshallUint64(buf[2736929:2737441][ii*8 : (ii+1)*8]) } // Field (38) 'ExecutionPayloadAvailability' if cap(b.ExecutionPayloadAvailability) == 0 { - b.ExecutionPayloadAvailability = make([]byte, 0, len(buf[2737401:2738425])) + b.ExecutionPayloadAvailability = make([]byte, 0, len(buf[2737441:2738465])) } - b.ExecutionPayloadAvailability = append(b.ExecutionPayloadAvailability, buf[2737401:2738425]...) + b.ExecutionPayloadAvailability = append(b.ExecutionPayloadAvailability, buf[2737441:2738465]...) // Field (39) 'BuilderPendingPayments' b.BuilderPendingPayments = make([]*BuilderPendingPayment, 64) @@ -2029,27 +2058,27 @@ func (b *BeaconStateGloas) UnmarshalSSZ(buf []byte) error { if b.BuilderPendingPayments[ii] == nil { b.BuilderPendingPayments[ii] = new(BuilderPendingPayment) } - if err = b.BuilderPendingPayments[ii].UnmarshalSSZ(buf[2738425:2741753][ii*52 : (ii+1)*52]); err != nil { + if err = b.BuilderPendingPayments[ii].UnmarshalSSZ(buf[2738465:2741793][ii*52 : (ii+1)*52]); err != nil { return err } } // Offset (40) 'BuilderPendingWithdrawals' - if o40 = ssz.ReadOffset(buf[2741753:2741757]); o40 > size || o36 > o40 { + if o40 = ssz.ReadOffset(buf[2741793:2741797]); o40 > size || o36 > o40 { return ssz.ErrOffset } // Field (41) 'LatestBlockHash' if cap(b.LatestBlockHash) == 0 { - b.LatestBlockHash = make([]byte, 0, len(buf[2741757:2741789])) + b.LatestBlockHash = make([]byte, 0, len(buf[2741797:2741829])) } - b.LatestBlockHash = append(b.LatestBlockHash, buf[2741757:2741789]...) + b.LatestBlockHash = append(b.LatestBlockHash, buf[2741797:2741829]...) // Field (42) 'LatestWithdrawalsRoot' if cap(b.LatestWithdrawalsRoot) == 0 { - b.LatestWithdrawalsRoot = make([]byte, 0, len(buf[2741789:2741821])) + b.LatestWithdrawalsRoot = make([]byte, 0, len(buf[2741829:2741861])) } - b.LatestWithdrawalsRoot = append(b.LatestWithdrawalsRoot, buf[2741789:2741821]...) + b.LatestWithdrawalsRoot = append(b.LatestWithdrawalsRoot, buf[2741829:2741861]...) // Field (7) 'HistoricalRoots' { @@ -2247,7 +2276,7 @@ func (b *BeaconStateGloas) UnmarshalSSZ(buf []byte) error { // SizeSSZ returns the ssz encoded size in bytes for the BeaconStateGloas object func (b *BeaconStateGloas) SizeSSZ() (size int) { - size = 2741821 + size = 2741861 // Field (7) 'HistoricalRoots' size += len(b.HistoricalRoots) * 32 diff --git a/testing/spectest/mainnet/BUILD.bazel b/testing/spectest/mainnet/BUILD.bazel index 75d3340e6a..f26abdd623 100644 --- a/testing/spectest/mainnet/BUILD.bazel +++ b/testing/spectest/mainnet/BUILD.bazel @@ -200,6 +200,7 @@ go_test( "fulu__sanity__blocks_test.go", "fulu__sanity__slots_test.go", "fulu__ssz_static__ssz_static_test.go", + "gloas__operations__execution_payload_test.go", "gloas__ssz_static__ssz_static_test.go", "phase0__epoch_processing__effective_balance_updates_test.go", "phase0__epoch_processing__epoch_processing_test.go", @@ -278,6 +279,7 @@ go_test( "//testing/spectest/shared/fulu/rewards:go_default_library", "//testing/spectest/shared/fulu/sanity:go_default_library", "//testing/spectest/shared/fulu/ssz_static:go_default_library", + "//testing/spectest/shared/gloas/operations:go_default_library", "//testing/spectest/shared/gloas/ssz_static:go_default_library", "//testing/spectest/shared/phase0/epoch_processing:go_default_library", "//testing/spectest/shared/phase0/finality:go_default_library", diff --git a/testing/spectest/mainnet/gloas__operations__execution_payload_test.go b/testing/spectest/mainnet/gloas__operations__execution_payload_test.go new file mode 100644 index 0000000000..a6305f1103 --- /dev/null +++ b/testing/spectest/mainnet/gloas__operations__execution_payload_test.go @@ -0,0 +1,11 @@ +package mainnet + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations" +) + +func TestMainnet_Gloas_Operations_ExecutionPayloadEnvelope(t *testing.T) { + operations.RunExecutionPayloadTest(t, "mainnet") +} diff --git a/testing/spectest/shared/gloas/operations/BUILD.bazel b/testing/spectest/shared/gloas/operations/BUILD.bazel new file mode 100644 index 0000000000..345494d6f1 --- /dev/null +++ b/testing/spectest/shared/gloas/operations/BUILD.bazel @@ -0,0 +1,29 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "execution_payload.go", + "helpers.go", + ], + importpath = "github.com/OffchainLabs/prysm/v7/testing/spectest/shared/gloas/operations", + visibility = ["//visibility:public"], + deps = [ + "//beacon-chain/core/gloas:go_default_library", + "//beacon-chain/core/helpers:go_default_library", + "//beacon-chain/state:go_default_library", + "//beacon-chain/state/state-native:go_default_library", + "//consensus-types/blocks:go_default_library", + "//consensus-types/interfaces:go_default_library", + "//proto/engine/v1:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//testing/require:go_default_library", + "//testing/spectest/utils:go_default_library", + "//testing/util:go_default_library", + "@com_github_golang_snappy//:go_default_library", + "@com_github_google_go_cmp//cmp:go_default_library", + "@io_bazel_rules_go//go/tools/bazel:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", + "@org_golang_google_protobuf//testing/protocmp:go_default_library", + ], +) diff --git a/testing/spectest/shared/gloas/operations/execution_payload.go b/testing/spectest/shared/gloas/operations/execution_payload.go new file mode 100644 index 0000000000..77c52619c8 --- /dev/null +++ b/testing/spectest/shared/gloas/operations/execution_payload.go @@ -0,0 +1,119 @@ +package operations + +import ( + "context" + "os" + "path" + "strings" + "testing" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/gloas" + "github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers" + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + "github.com/OffchainLabs/prysm/v7/consensus-types/blocks" + "github.com/OffchainLabs/prysm/v7/consensus-types/interfaces" + enginev1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1" + "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/OffchainLabs/prysm/v7/testing/spectest/utils" + "github.com/OffchainLabs/prysm/v7/testing/util" + "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/golang/snappy" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" +) + +type ExecutionConfig struct { + Valid bool `json:"execution_valid"` +} + +func sszToSignedExecutionPayloadEnvelope(b []byte) (interfaces.ROSignedExecutionPayloadEnvelope, error) { + envelope := &enginev1.SignedExecutionPayloadEnvelope{} + if err := envelope.UnmarshalSSZ(b); err != nil { + return nil, err + } + return blocks.WrappedROSignedExecutionPayloadEnvelope(envelope) +} + +func RunExecutionPayloadTest(t *testing.T, config string) { + require.NoError(t, utils.SetConfig(t, config)) + testFolders, testsFolderPath := utils.TestFolders(t, config, "gloas", "operations/execution_payload/pyspec_tests") + if len(testFolders) == 0 { + t.Fatalf("No test folders found for %s/%s/%s", config, "gloas", "operations/execution_payload/pyspec_tests") + } + for _, folder := range testFolders { + t.Run(folder.Name(), func(t *testing.T) { + helpers.ClearCache() + + // Check if signed_envelope.ssz_snappy exists, skip if not + _, err := bazel.Runfile(path.Join(testsFolderPath, folder.Name(), "signed_envelope.ssz_snappy")) + if err != nil && strings.Contains(err.Error(), "could not locate file") { + t.Skipf("Skipping test %s: signed_envelope.ssz_snappy not found", folder.Name()) + return + } + + // Read the signed execution payload envelope + envelopeFile, err := util.BazelFileBytes(testsFolderPath, folder.Name(), "signed_envelope.ssz_snappy") + require.NoError(t, err) + envelopeSSZ, err := snappy.Decode(nil /* dst */, envelopeFile) + require.NoError(t, err, "Failed to decompress envelope") + signedEnvelope, err := sszToSignedExecutionPayloadEnvelope(envelopeSSZ) + require.NoError(t, err, "Failed to unmarshal signed envelope") + + preBeaconStateFile, err := util.BazelFileBytes(testsFolderPath, folder.Name(), "pre.ssz_snappy") + require.NoError(t, err) + preBeaconStateSSZ, err := snappy.Decode(nil /* dst */, preBeaconStateFile) + require.NoError(t, err, "Failed to decompress") + preBeaconState, err := sszToState(preBeaconStateSSZ) + require.NoError(t, err) + + postSSZFilepath, err := bazel.Runfile(path.Join(testsFolderPath, folder.Name(), "post.ssz_snappy")) + postSSZExists := true + if err != nil && strings.Contains(err.Error(), "could not locate file") { + postSSZExists = false + } else { + require.NoError(t, err) + } + + file, err := util.BazelFileBytes(testsFolderPath, folder.Name(), "execution.yaml") + require.NoError(t, err) + config := &ExecutionConfig{} + require.NoError(t, utils.UnmarshalYaml(file, config), "Failed to Unmarshal") + if !config.Valid { + t.Skip("Skipping invalid execution engine test as it's never supported") + } + + err = gloas.ProcessExecutionPayload(context.Background(), preBeaconState, signedEnvelope) + if postSSZExists { + require.NoError(t, err) + comparePostState(t, postSSZFilepath, preBeaconState) + } else if config.Valid { + // Note: This doesn't test anything worthwhile. It essentially tests + // that *any* error has occurred, not any specific error. + if err == nil { + t.Fatal("Did not fail when expected") + } + t.Logf("Expected failure; failure reason = %v", err) + return + } + }) + } +} + +func comparePostState(t *testing.T, postSSZFilepath string, want state.BeaconState) { + postBeaconStateFile, err := os.ReadFile(postSSZFilepath) // #nosec G304 + require.NoError(t, err) + postBeaconStateSSZ, err := snappy.Decode(nil /* dst */, postBeaconStateFile) + require.NoError(t, err, "Failed to decompress") + postBeaconState, err := sszToState(postBeaconStateSSZ) + require.NoError(t, err) + postBeaconStatePb, ok := postBeaconState.ToProtoUnsafe().(proto.Message) + require.Equal(t, true, ok, "post beacon state did not return a proto.Message") + pbState, ok := want.ToProtoUnsafe().(proto.Message) + require.Equal(t, true, ok, "beacon state did not return a proto.Message") + + if !proto.Equal(postBeaconStatePb, pbState) { + diff := cmp.Diff(pbState, postBeaconStatePb, protocmp.Transform()) + t.Fatalf("Post state does not match expected state, diff: %s", diff) + } +} diff --git a/testing/spectest/shared/gloas/operations/helpers.go b/testing/spectest/shared/gloas/operations/helpers.go new file mode 100644 index 0000000000..13becc60b9 --- /dev/null +++ b/testing/spectest/shared/gloas/operations/helpers.go @@ -0,0 +1,15 @@ +package operations + +import ( + "github.com/OffchainLabs/prysm/v7/beacon-chain/state" + state_native "github.com/OffchainLabs/prysm/v7/beacon-chain/state/state-native" + ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" +) + +func sszToState(b []byte) (state.BeaconState, error) { + base := ðpb.BeaconStateGloas{} + if err := base.UnmarshalSSZ(b); err != nil { + return nil, err + } + return state_native.InitializeFromProtoGloas(base) +}