diff --git a/bindings/megapool/megapool-contract.go b/bindings/megapool/megapool-contract.go index add563694..7d71b8ef9 100644 --- a/bindings/megapool/megapool-contract.go +++ b/bindings/megapool/megapool-contract.go @@ -18,14 +18,14 @@ import ( ) type SlotProof struct { - Slot uint64 - Witnesses [][32]byte + Slot uint64 `json:"slot"` + Witnesses [][32]byte `json:"witnesses"` } type ValidatorProof struct { - ValidatorIndex *big.Int - Validator ProvedValidator - Witnesses [][32]byte + ValidatorIndex *big.Int `json:"validatorIndex"` + Validator ProvedValidator `json:"validator"` + Witnesses [][32]byte `json:"witnesses"` } type ProvedValidator struct { diff --git a/bindings/megapool/megapool-manager.go b/bindings/megapool/megapool-manager.go index 1e17f33b0..921edebdc 100644 --- a/bindings/megapool/megapool-manager.go +++ b/bindings/megapool/megapool-manager.go @@ -21,13 +21,22 @@ type ExitChallenge struct { } type WithdrawalProof struct { - Slot uint64 `json:"slot"` WithdrawalSlot uint64 `json:"withdrawalSlot"` WithdrawalNum uint16 `json:"withdrawalNum"` Withdrawal Withdrawal `json:"withdrawal"` Witnesses [][32]byte `json:"witnesses"` } +type NextWithdrawalIndexProof struct { + NextWithdrawalIndex uint64 `json:"nextWithdrawalIndex"` + Witnesses [][32]byte `json:"witnesses"` +} + +type ValidatorBalanceProof struct { + BalanceChunk [32]byte `json:"balanceChunk"` + Witnesses [][32]byte `json:"witnesses"` +} + func GetValidatorCount(rp *rocketpool.RocketPool, opts *bind.CallOpts) (uint32, error) { megapoolManager, err := getRocketMegapoolManager(rp, opts) if err != nil { @@ -111,21 +120,21 @@ func GetValidatorInfo(rp *rocketpool.RocketPool, index uint32, opts *bind.CallOp } // Estimate the gas of Stake -func EstimateStakeGas(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (gaslimit.Limits, error) { +func EstimateStakeGas(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (gaslimit.Limits, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return gaslimit.Limits{}, err } - return megapoolManager.GetTransactionGasInfo(opts, "stake", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + return megapoolManager.GetTransactionGasInfo(opts, "stake", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) } // Progress the prelaunch megapool to staking -func Stake(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (*types.Transaction, error) { +func Stake(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (*types.Transaction, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return nil, err } - tx, err := megapoolManager.Transact(opts, "stake", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + tx, err := megapoolManager.Transact(opts, "stake", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) if err != nil { return nil, fmt.Errorf("error staking megapool %s: %w", megapoolAddress, err) } @@ -133,21 +142,21 @@ func Stake(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorI } // Estimate the gas to call NotifyExit -func EstimateNotifyExitGas(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (gaslimit.Limits, error) { +func EstimateNotifyExitGas(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (gaslimit.Limits, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return gaslimit.Limits{}, err } - return megapoolManager.GetTransactionGasInfo(opts, "notifyExit", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + return megapoolManager.GetTransactionGasInfo(opts, "notifyExit", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) } // Notify the megapool that one of its validators is exiting -func NotifyExit(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (*types.Transaction, error) { +func NotifyExit(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (*types.Transaction, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return nil, err } - tx, err := megapoolManager.Transact(opts, "notifyExit", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + tx, err := megapoolManager.Transact(opts, "notifyExit", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) if err != nil { return nil, fmt.Errorf("error calling notify exit: %w", err) } @@ -155,21 +164,21 @@ func NotifyExit(rp *rocketpool.RocketPool, megapoolAddress common.Address, valid } // Estimate the gas to call NotifyNotExit -func EstimateNotifyNotExitGas(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (gaslimit.Limits, error) { +func EstimateNotifyNotExitGas(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (gaslimit.Limits, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return gaslimit.Limits{}, err } - return megapoolManager.GetTransactionGasInfo(opts, "notifyNotExit", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + return megapoolManager.GetTransactionGasInfo(opts, "notifyNotExit", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) } // Used to prove a validator is not exiting after a challenge-exit -func NotifyNotExit(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (*types.Transaction, error) { +func NotifyNotExit(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (*types.Transaction, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return nil, err } - tx, err := megapoolManager.Transact(opts, "notifyNotExit", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + tx, err := megapoolManager.Transact(opts, "notifyNotExit", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) if err != nil { return nil, fmt.Errorf("error calling notify not exit: %w", err) } @@ -199,21 +208,21 @@ func ChallengeExit(rp *rocketpool.RocketPool, exitChallenge []ExitChallenge, opt } // Estimate the gas to call NotifyFinalBalance -func EstimateNotifyFinalBalance(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, withdrawalProof WithdrawalProof, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (gaslimit.Limits, error) { +func EstimateNotifyFinalBalance(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (gaslimit.Limits, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return gaslimit.Limits{}, err } - return megapoolManager.GetTransactionGasInfo(opts, "notifyFinalBalance", megapoolAddress, validatorId, slotTimestamp, withdrawalProof, validatorProof, slotProof) + return megapoolManager.GetTransactionGasInfo(opts, "notifyFinalBalance", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) } // Notify the megapool of the final balance of an exited validator -func NotifyFinalBalance(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, withdrawalProof WithdrawalProof, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (*types.Transaction, error) { +func NotifyFinalBalance(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (*types.Transaction, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return nil, err } - tx, err := megapoolManager.Transact(opts, "notifyFinalBalance", megapoolAddress, validatorId, slotTimestamp, withdrawalProof, validatorProof, slotProof) + tx, err := megapoolManager.Transact(opts, "notifyFinalBalance", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) if err != nil { return nil, fmt.Errorf("error calling notify final balance: %w", err) } @@ -221,23 +230,23 @@ func NotifyFinalBalance(rp *rocketpool.RocketPool, megapoolAddress common.Addres } // Estimate the gas to call DissolveWithProof -func EstimateDissolveWithProof(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (gaslimit.Limits, error) { +func EstimateDissolveWithProof(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (gaslimit.Limits, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return gaslimit.Limits{}, err } - return megapoolManager.GetTransactionGasInfo(opts, "dissolve", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + return megapoolManager.GetTransactionGasInfo(opts, "dissolve", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) } // Dissolve a validator using a proof that it used wrong credentials -func DissolveWithProof(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, validatorProof ValidatorProof, slotProof SlotProof, opts *bind.TransactOpts) (*types.Transaction, error) { +func DissolveWithProof(rp *rocketpool.RocketPool, megapoolAddress common.Address, validatorId uint32, slotTimestamp uint64, proofVersion *big.Int, proofData []byte, opts *bind.TransactOpts) (*types.Transaction, error) { megapoolManager, err := getRocketMegapoolManager(rp, nil) if err != nil { return nil, err } - tx, err := megapoolManager.Transact(opts, "dissolve", megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof) + tx, err := megapoolManager.Transact(opts, "dissolve", megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData) if err != nil { - return nil, fmt.Errorf("error calling notify final balance: %w", err) + return nil, fmt.Errorf("error calling dissolve with proof: %w", err) } return tx, nil } diff --git a/bindings/megapool/proof-bundle.go b/bindings/megapool/proof-bundle.go new file mode 100644 index 000000000..5830997c5 --- /dev/null +++ b/bindings/megapool/proof-bundle.go @@ -0,0 +1,132 @@ +package megapool + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +// Proof versions accepted by BeaconStateVerifier on v1.4.1-dev. +var ( + ValidatorProofVersion1 = big.NewInt(1) + FinalBalanceProofVersion1 = big.NewInt(1) + FinalBalanceProofVersion2 = big.NewInt(2) +) + +type ValidatorProofBundleV1 struct { + ValidatorProof ValidatorProof `json:"validatorProof"` + SlotProof SlotProof `json:"slotProof"` +} + +type FinalBalanceProofBundleV1 struct { + WithdrawalProof WithdrawalProof `json:"withdrawalProof"` + ValidatorProof ValidatorProof `json:"validatorProof"` + SlotProof SlotProof `json:"slotProof"` +} + +type FinalBalanceProofBundleV2 struct { + WithdrawalProof WithdrawalProof `json:"withdrawalProof"` + ValidatorProof ValidatorProof `json:"validatorProof"` + SlotProof SlotProof `json:"slotProof"` + PreviousNextWithdrawalIndexProof NextWithdrawalIndexProof `json:"previousNextWithdrawalIndexProof"` + ValidatorBalanceProof ValidatorBalanceProof `json:"validatorBalanceProof"` +} + +var ( + validatorComponents = []abi.ArgumentMarshaling{ + {Name: "pubkey", Type: "bytes"}, + {Name: "withdrawalCredentials", Type: "bytes32"}, + {Name: "effectiveBalance", Type: "uint64"}, + {Name: "slashed", Type: "bool"}, + {Name: "activationEligibilityEpoch", Type: "uint64"}, + {Name: "activationEpoch", Type: "uint64"}, + {Name: "exitEpoch", Type: "uint64"}, + {Name: "withdrawableEpoch", Type: "uint64"}, + } + validatorProofComponents = []abi.ArgumentMarshaling{ + {Name: "validatorIndex", Type: "uint40"}, + {Name: "validator", Type: "tuple", Components: validatorComponents}, + {Name: "witnesses", Type: "bytes32[]"}, + } + slotProofComponents = []abi.ArgumentMarshaling{ + {Name: "slot", Type: "uint64"}, + {Name: "witnesses", Type: "bytes32[]"}, + } + withdrawalComponents = []abi.ArgumentMarshaling{ + {Name: "index", Type: "uint64"}, + {Name: "validatorIndex", Type: "uint64"}, + {Name: "withdrawalCredentials", Type: "bytes20"}, + {Name: "amountInGwei", Type: "uint64"}, + } + withdrawalProofComponents = []abi.ArgumentMarshaling{ + {Name: "withdrawalSlot", Type: "uint64"}, + {Name: "withdrawalNum", Type: "uint16"}, + {Name: "withdrawal", Type: "tuple", Components: withdrawalComponents}, + {Name: "witnesses", Type: "bytes32[]"}, + } + nextWithdrawalIndexProofComponents = []abi.ArgumentMarshaling{ + {Name: "nextWithdrawalIndex", Type: "uint64"}, + {Name: "witnesses", Type: "bytes32[]"}, + } + validatorBalanceProofComponents = []abi.ArgumentMarshaling{ + {Name: "balanceChunk", Type: "bytes32"}, + {Name: "witnesses", Type: "bytes32[]"}, + } +) + +func EncodeValidatorProofBundleV1(validatorProof ValidatorProof, slotProof SlotProof) ([]byte, error) { + return encodeTuple([]abi.ArgumentMarshaling{ + {Name: "validatorProof", Type: "tuple", Components: validatorProofComponents}, + {Name: "slotProof", Type: "tuple", Components: slotProofComponents}, + }, ValidatorProofBundleV1{ + ValidatorProof: validatorProof, + SlotProof: slotProof, + }) +} + +func EncodeFinalBalanceProofBundleV1(withdrawalProof WithdrawalProof, validatorProof ValidatorProof, slotProof SlotProof) ([]byte, error) { + return encodeTuple([]abi.ArgumentMarshaling{ + {Name: "withdrawalProof", Type: "tuple", Components: withdrawalProofComponents}, + {Name: "validatorProof", Type: "tuple", Components: validatorProofComponents}, + {Name: "slotProof", Type: "tuple", Components: slotProofComponents}, + }, FinalBalanceProofBundleV1{ + WithdrawalProof: withdrawalProof, + ValidatorProof: validatorProof, + SlotProof: slotProof, + }) +} + +func EncodeFinalBalanceProofBundleV2( + withdrawalProof WithdrawalProof, + validatorProof ValidatorProof, + slotProof SlotProof, + previousNextWithdrawalIndexProof NextWithdrawalIndexProof, + validatorBalanceProof ValidatorBalanceProof, +) ([]byte, error) { + return encodeTuple([]abi.ArgumentMarshaling{ + {Name: "withdrawalProof", Type: "tuple", Components: withdrawalProofComponents}, + {Name: "validatorProof", Type: "tuple", Components: validatorProofComponents}, + {Name: "slotProof", Type: "tuple", Components: slotProofComponents}, + {Name: "previousNextWithdrawalIndexProof", Type: "tuple", Components: nextWithdrawalIndexProofComponents}, + {Name: "validatorBalanceProof", Type: "tuple", Components: validatorBalanceProofComponents}, + }, FinalBalanceProofBundleV2{ + WithdrawalProof: withdrawalProof, + ValidatorProof: validatorProof, + SlotProof: slotProof, + PreviousNextWithdrawalIndexProof: previousNextWithdrawalIndexProof, + ValidatorBalanceProof: validatorBalanceProof, + }) +} + +func encodeTuple(components []abi.ArgumentMarshaling, value any) ([]byte, error) { + typ, err := abi.NewType("tuple", "", components) + if err != nil { + return nil, fmt.Errorf("error creating proof bundle tuple type: %w", err) + } + encoded, err := abi.Arguments{{Type: typ}}.Pack(value) + if err != nil { + return nil, fmt.Errorf("error encoding proof bundle: %w", err) + } + return encoded, nil +} diff --git a/bindings/megapool/proof-bundle_test.go b/bindings/megapool/proof-bundle_test.go new file mode 100644 index 000000000..b781a9fec --- /dev/null +++ b/bindings/megapool/proof-bundle_test.go @@ -0,0 +1,86 @@ +package megapool + +import ( + "math/big" + "testing" +) + +func sampleValidatorProof() ValidatorProof { + return ValidatorProof{ + ValidatorIndex: big.NewInt(7), + Validator: ProvedValidator{ + Pubkey: make([]byte, 48), + WithdrawalCredentials: [32]byte{0x01}, + EffectiveBalance: 32_000_000_000, + Slashed: false, + ActivationEligibilityEpoch: 1, + ActivationEpoch: 2, + ExitEpoch: 3, + WithdrawableEpoch: 4, + }, + Witnesses: [][32]byte{{0x11}, {0x22}}, + } +} + +func sampleSlotProof() SlotProof { + return SlotProof{ + Slot: 99, + Witnesses: [][32]byte{{0x33}}, + } +} + +func sampleWithdrawalProof() WithdrawalProof { + return WithdrawalProof{ + WithdrawalSlot: 50, + WithdrawalNum: 1, + Withdrawal: Withdrawal{ + Index: 10, + ValidatorIndex: 7, + WithdrawalCredentials: [20]byte{0xaa}, + AmountInGwei: 32_000_000_000, + }, + Witnesses: [][32]byte{{0x44}, {0x55}}, + } +} + +func TestEncodeValidatorProofBundleV1(t *testing.T) { + encoded, err := EncodeValidatorProofBundleV1(sampleValidatorProof(), sampleSlotProof()) + if err != nil { + t.Fatalf("encode: %v", err) + } + if len(encoded) == 0 { + t.Fatal("expected non-empty encoding") + } +} + +func TestEncodeFinalBalanceProofBundleV1(t *testing.T) { + encoded, err := EncodeFinalBalanceProofBundleV1(sampleWithdrawalProof(), sampleValidatorProof(), sampleSlotProof()) + if err != nil { + t.Fatalf("encode: %v", err) + } + if len(encoded) == 0 { + t.Fatal("expected non-empty encoding") + } +} + +func TestEncodeFinalBalanceProofBundleV2(t *testing.T) { + encoded, err := EncodeFinalBalanceProofBundleV2( + sampleWithdrawalProof(), + sampleValidatorProof(), + sampleSlotProof(), + NextWithdrawalIndexProof{ + NextWithdrawalIndex: 9, + Witnesses: [][32]byte{{0x66}}, + }, + ValidatorBalanceProof{ + BalanceChunk: [32]byte{0x77}, + Witnesses: [][32]byte{{0x88}}, + }, + ) + if err != nil { + t.Fatalf("encode: %v", err) + } + if len(encoded) == 0 { + t.Fatal("expected non-empty encoding") + } +} diff --git a/rocketpool/api/megapool/dissolve-with-proof.go b/rocketpool/api/megapool/dissolve-with-proof.go index 6913981fd..65e10b7c7 100644 --- a/rocketpool/api/megapool/dissolve-with-proof.go +++ b/rocketpool/api/megapool/dissolve-with-proof.go @@ -78,6 +78,10 @@ func canDissolveWithProof(c *cli.Command, validatorId uint32) (*api.CanDissolveW if err != nil { return nil, err } + proofData, err := megapool.EncodeValidatorProofBundleV1(proof, slotProof) + if err != nil { + return nil, err + } if !validatorInfo.InPrestake { response.CanDissolve = false @@ -101,7 +105,7 @@ func canDissolveWithProof(c *cli.Command, validatorId uint32) (*api.CanDissolveW if err != nil { return nil, err } - gasLimits, err := megapool.EstimateDissolveWithProof(rp, megapoolAddress, validatorId, slotTimestamp, proof, slotProof, opts) + gasLimits, err := megapool.EstimateDissolveWithProof(rp, megapoolAddress, validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return nil, err } @@ -167,9 +171,13 @@ func dissolveWithProof(c *cli.Command, validatorId uint32, t *snroute.TransactOp if err != nil { return nil, err } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + return nil, err + } // Dissolve - tx, err := megapool.DissolveWithProof(rp, megapoolAddress, validatorId, slotTimestamp, validatorProof, slotProof, opts) + tx, err := megapool.DissolveWithProof(rp, megapoolAddress, validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return nil, err } diff --git a/rocketpool/api/megapool/notify-final-balance.go b/rocketpool/api/megapool/notify-final-balance.go index b12af9d06..8c89c5cf6 100644 --- a/rocketpool/api/megapool/notify-final-balance.go +++ b/rocketpool/api/megapool/notify-final-balance.go @@ -12,7 +12,6 @@ import ( "github.com/rocket-pool/smartnode/rocketpool/api/snroute" "github.com/rocket-pool/smartnode/shared/services" "github.com/rocket-pool/smartnode/shared/types/api" - cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" ) func canNotifyFinalBalance(c *cli.Command, validatorId uint32, withdrawalSlot uint64) (*api.CanNotifyFinalBalanceResponse, error) { @@ -59,14 +58,6 @@ func canNotifyFinalBalance(c *cli.Command, validatorId uint32, withdrawalSlot ui return nil, err } - cfg, err := services.GetConfig(c) - if err != nil { - return nil, err - } - - // Get the network - network := cfg.Smartnode.Network.Value.(cfgtypes.Network) - validatorStatus, err := bc.GetValidatorStatus(types.ValidatorPubkey(validatorInfo.Pubkey), nil) if err != nil { return nil, fmt.Errorf("Error getting validator status from beacon chain: %w", err) @@ -80,55 +71,18 @@ func canNotifyFinalBalance(c *cli.Command, validatorId uint32, withdrawalSlot ui withdrawalSlot = validatorStatus.WithdrawableEpoch * 32 } - beaconHead, err := bc.GetBeaconHead() + proofVersion, proofData, slotTimestamp, err := services.GetFinalBalanceProofBundle(c, withdrawalSlot, validatorIndex, types.ValidatorPubkey(validatorInfo.Pubkey), megapoolAddress, w) if err != nil { return nil, err } - finalizedSlot := beaconHead.FinalizedEpoch * 32 - - withdrawalProof, slotUsed, stateUsed, err := services.GetWithdrawalProofForSlot(c, withdrawalSlot, validatorIndex) - if err != nil { - fmt.Printf("An error occurred while getting the withdrawal proof: %s\n", err) - // try to fetch the withdrawal proof from the Rocket Pool API - withdrawalProof, slotUsed, err = services.GetWithdrawalProofForSlotFromAPI(c, finalizedSlot, withdrawalSlot, validatorIndex, network) - if err != nil { - fmt.Printf("An error occurred while getting the withdrawal proof from the Rocket Pool API: %s\n", err) - return nil, err - } - } opts, err := w.GetNodeAccountTransactor() if err != nil { return nil, err } - withdrawal := megapool.Withdrawal{ - Index: withdrawalProof.WithdrawalIndex, - ValidatorIndex: validatorIndex, - WithdrawalCredentials: withdrawalProof.WithdrawalAddress, - AmountInGwei: withdrawalProof.Amount.Uint64(), - } - - finalBalanceProof := megapool.WithdrawalProof{ - WithdrawalSlot: withdrawalProof.WithdrawalSlot, - WithdrawalNum: uint16(withdrawalProof.IndexInWithdrawalsArray), - Withdrawal: withdrawal, - Witnesses: withdrawalProof.Witnesses, - } - - // Get the eth2Config and validatorProof - eth2Config, err := bc.GetEth2Config() - if err != nil { - return nil, err - } - - validatorProof, slotTimestamp, slotProof, err := services.GetValidatorProof(c, slotUsed, w, eth2Config, megapoolAddress, types.ValidatorPubkey(validatorInfo.Pubkey), stateUsed) - if err != nil { - return nil, err - } - // Notify the validator exit - gasLimits, err := megapool.EstimateNotifyFinalBalance(rp, megapoolAddress, validatorId, slotTimestamp, finalBalanceProof, validatorProof, slotProof, opts) + gasLimits, err := megapool.EstimateNotifyFinalBalance(rp, megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData, opts) if err != nil { return nil, err } @@ -163,14 +117,6 @@ func notifyFinalBalance(c *cli.Command, validatorId uint32, withdrawalSlot uint6 return nil, err } - cfg, err := services.GetConfig(c) - if err != nil { - return nil, err - } - - // Get the network - network := cfg.Smartnode.Network.Value.(cfgtypes.Network) - // Validate minipool owner nodeAccount, err := w.GetNodeAccount() if err != nil { @@ -211,49 +157,13 @@ func notifyFinalBalance(c *cli.Command, validatorId uint32, withdrawalSlot uint6 withdrawalSlot = validatorStatus.WithdrawableEpoch * 32 } - beaconHead, err := bc.GetBeaconHead() - if err != nil { - return nil, err - } - finalizedSlot := beaconHead.FinalizedEpoch * 32 - - withdrawalProof, proofSlot, stateUsed, err := services.GetWithdrawalProofForSlot(c, withdrawalSlot, validatorIndex) - if err != nil { - fmt.Printf("An error occurred while getting the withdrawal proof: %s\n", err) - // try to fetch the withdrawal proof from the Rocket Pool API - withdrawalProof, proofSlot, err = services.GetWithdrawalProofForSlotFromAPI(c, finalizedSlot, withdrawalSlot, validatorIndex, network) - if err != nil { - fmt.Printf("An error occurred while getting the withdrawal proof from the Rocket Pool API: %s\n", err) - return nil, err - } - } - - withdrawal := megapool.Withdrawal{ - Index: withdrawalProof.WithdrawalIndex, - ValidatorIndex: validatorIndex, - WithdrawalCredentials: withdrawalProof.WithdrawalAddress, - AmountInGwei: withdrawalProof.Amount.Uint64(), - } - - finalBalanceProof := megapool.WithdrawalProof{ - WithdrawalSlot: withdrawalProof.WithdrawalSlot, - WithdrawalNum: uint16(withdrawalProof.IndexInWithdrawalsArray), - Withdrawal: withdrawal, - Witnesses: withdrawalProof.Witnesses, - } - - // Get the eth2Config and validatorProof - eth2Config, err := bc.GetEth2Config() - if err != nil { - return nil, err - } - validatorProof, slotTimestamp, slotProof, err := services.GetValidatorProof(c, proofSlot, w, eth2Config, megapoolAddress, types.ValidatorPubkey(validatorInfo.Pubkey), stateUsed) + proofVersion, proofData, slotTimestamp, err := services.GetFinalBalanceProofBundle(c, withdrawalSlot, validatorIndex, types.ValidatorPubkey(validatorInfo.Pubkey), megapoolAddress, w) if err != nil { return nil, err } // Notify the validator exit - tx, err := megapool.NotifyFinalBalance(rp, megapoolAddress, validatorId, slotTimestamp, finalBalanceProof, validatorProof, slotProof, opts) + tx, err := megapool.NotifyFinalBalance(rp, megapoolAddress, validatorId, slotTimestamp, proofVersion, proofData, opts) if err != nil { return nil, err } diff --git a/rocketpool/api/megapool/notify-validator-exit.go b/rocketpool/api/megapool/notify-validator-exit.go index 66f14e04f..b7d945c86 100644 --- a/rocketpool/api/megapool/notify-validator-exit.go +++ b/rocketpool/api/megapool/notify-validator-exit.go @@ -126,6 +126,10 @@ func canNotifyValidatorExit(c *cli.Command, validatorId uint32) (*api.CanNotifyV if err != nil { return nil, err } + proofData, err := megapool.EncodeValidatorProofBundleV1(proof, slotProof) + if err != nil { + return nil, err + } opts, err := w.GetNodeAccountTransactor() if err != nil { @@ -133,7 +137,7 @@ func canNotifyValidatorExit(c *cli.Command, validatorId uint32) (*api.CanNotifyV } // Notify the validator exit - gasLimits, err := megapool.EstimateNotifyExitGas(rp, megapoolAddress, validatorId, slotTimestamp, proof, slotProof, opts) + gasLimits, err := megapool.EstimateNotifyExitGas(rp, megapoolAddress, validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return nil, err } @@ -211,9 +215,13 @@ func notifyValidatorExit(c *cli.Command, validatorId uint32, t *snroute.Transact if err != nil { return nil, err } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + return nil, err + } // Notify the validator exit - tx, err := megapool.NotifyExit(rp, megapoolAddress, validatorId, slotTimetamp, validatorProof, slotProof, opts) + tx, err := megapool.NotifyExit(rp, megapoolAddress, validatorId, slotTimetamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return nil, err } diff --git a/rocketpool/api/megapool/stake.go b/rocketpool/api/megapool/stake.go index 0b6d57528..9b99eca28 100644 --- a/rocketpool/api/megapool/stake.go +++ b/rocketpool/api/megapool/stake.go @@ -99,13 +99,17 @@ func canStake(c *cli.Command, validatorId uint64) (*api.CanStakeResponse, error) } return nil, err } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + return nil, err + } // Get gas estimate opts, err := w.GetNodeAccountTransactor() if err != nil { return nil, err } - gasLimits, err := megapool.EstimateStakeGas(rp, megapoolAddress, uint32(validatorId), slotTimestamp, validatorProof, slotProof, opts) + gasLimits, err := megapool.EstimateStakeGas(rp, megapoolAddress, uint32(validatorId), slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return nil, err } @@ -171,9 +175,13 @@ func stake(c *cli.Command, validatorId uint64, t *snroute.TransactOpts) (*api.St if err != nil { return nil, err } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + return nil, err + } // Stake - tx, err := megapool.Stake(rp, megapoolAddress, uint32(validatorId), slotTimestamp, validatorProof, slotProof, opts) + tx, err := megapool.Stake(rp, megapoolAddress, uint32(validatorId), slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return nil, err } diff --git a/rocketpool/node/defend-challenge-exit.go b/rocketpool/node/defend-challenge-exit.go index 548553b35..b3f9feff1 100644 --- a/rocketpool/node/defend-challenge-exit.go +++ b/rocketpool/node/defend-challenge-exit.go @@ -167,18 +167,22 @@ func (t *defendChallengeExit) defendChallenge(rp *rocketpool.RocketPool, mp mega t.log.Printlnf("[ERROR] There was an error during the proof creation process: %w", err) return err } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + return err + } t.log.Printlnf("[FINISHED] The beacon state proof has been successfully created.") var gasLimits gaslimit.Limits if !exiting { // Get the gas limit - gasLimits, err = megapool.EstimateNotifyNotExitGas(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + gasLimits, err = megapool.EstimateNotifyNotExitGas(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return err } } else { - gasLimits, err = megapool.EstimateNotifyExitGas(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + gasLimits, err = megapool.EstimateNotifyExitGas(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return err } @@ -206,13 +210,13 @@ func (t *defendChallengeExit) defendChallenge(rp *rocketpool.RocketPool, mp mega var tx *coretypes.Transaction if !exiting { t.log.Printlnf("Notifying that validator %d is not exiting.", validatorId) - tx, err = megapool.NotifyNotExit(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + tx, err = megapool.NotifyNotExit(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return err } } else { t.log.Printlnf("Notifying that validator %d is exiting.", validatorId) - tx, err = megapool.NotifyExit(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + tx, err = megapool.NotifyExit(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return err } diff --git a/rocketpool/node/notify-final-balance.go b/rocketpool/node/notify-final-balance.go index 68977e84e..0b0f6ffba 100644 --- a/rocketpool/node/notify-final-balance.go +++ b/rocketpool/node/notify-final-balance.go @@ -183,36 +183,15 @@ func (t *notifyFinalBalance) createFinalBalanceProof(rp *rocketpool.RocketPool, slot := validatorDetails.WithdrawableEpoch * 32 - withdrawalProof, proofSlot, stateUsed, err := services.GetWithdrawalProofForSlot(t.c, slot, validatorIndex) + proofVersion, proofData, slotTimestamp, err := services.GetFinalBalanceProofBundle(t.c, slot, validatorIndex, validatorDetails.Pubkey, mp.GetAddress(), t.w) if err != nil { return fmt.Errorf("error getting withdrawal proof for validator 0x%s (index: %d): %w", validatorDetails.Pubkey.String(), validatorIndex, err) } - t.log.Printlnf("The Beacon WithdrawalSlot for validator index %d is: %d", validatorDetails.Index, withdrawalProof.WithdrawalSlot) - - validatorProof, slotTimestamp, slotProof, err := services.GetValidatorProof(t.c, proofSlot, t.w, state.BeaconConfig, mp.GetAddress(), validatorDetails.Pubkey, stateUsed) - if err != nil { - t.log.Printlnf("There was an error during the proof creation process: %w", err) - return err - } - - withdrawal := megapool.Withdrawal{ - Index: withdrawalProof.WithdrawalIndex, - ValidatorIndex: validatorIndex, - WithdrawalCredentials: withdrawalProof.WithdrawalAddress, - AmountInGwei: withdrawalProof.Amount.Uint64(), - } - - finalBalanceProof := megapool.WithdrawalProof{ - WithdrawalSlot: withdrawalProof.WithdrawalSlot, - WithdrawalNum: uint16(withdrawalProof.IndexInWithdrawalsArray), - Withdrawal: withdrawal, - Witnesses: withdrawalProof.Witnesses, - } t.log.Printlnf("The validator final balance proof has been successfully created.") // Get the gas limit - gasLimits, err := megapool.EstimateNotifyFinalBalance(rp, mp.GetAddress(), validatorId, slotTimestamp, finalBalanceProof, validatorProof, slotProof, opts) + gasLimits, err := megapool.EstimateNotifyFinalBalance(rp, mp.GetAddress(), validatorId, slotTimestamp, proofVersion, proofData, opts) if err != nil { t.log.Printlnf("Could not estimate the gas required to notify final balance on megapool validator %d: %w", validatorId, err) return err @@ -237,7 +216,7 @@ func (t *notifyFinalBalance) createFinalBalanceProof(rp *rocketpool.RocketPool, opts.GasLimit = gas.Uint64() // Call Notify Final Balance - tx, err := megapool.NotifyFinalBalance(rp, mp.GetAddress(), validatorId, slotTimestamp, finalBalanceProof, validatorProof, slotProof, opts) + tx, err := megapool.NotifyFinalBalance(rp, mp.GetAddress(), validatorId, slotTimestamp, proofVersion, proofData, opts) if err != nil { return err } diff --git a/rocketpool/node/notify-validator-exit.go b/rocketpool/node/notify-validator-exit.go index f5f6f0bb2..569ec8ff3 100644 --- a/rocketpool/node/notify-validator-exit.go +++ b/rocketpool/node/notify-validator-exit.go @@ -207,11 +207,15 @@ func (t *notifyValidatorExit) createExitProof(rp *rocketpool.RocketPool, beaconS t.log.Printlnf("[ERROR] There was an error during the proof creation process: %w", err) return err } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + return err + } t.log.Printlnf("[FINISHED] The validator exit proof has been successfully created.") // Get the gas limit - gasLimits, err := megapool.EstimateNotifyExitGas(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + gasLimits, err := megapool.EstimateNotifyExitGas(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { t.log.Printlnf("Could not estimate the gas required to notify exit on megapool validator %d: %w", validatorId, err) return err @@ -236,7 +240,7 @@ func (t *notifyValidatorExit) createExitProof(rp *rocketpool.RocketPool, beaconS opts.GasLimit = gas.Uint64() // Call Notify Exit - tx, err := megapool.NotifyExit(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + tx, err := megapool.NotifyExit(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return err } diff --git a/rocketpool/node/stake-megapool-validator.go b/rocketpool/node/stake-megapool-validator.go index 694b5d4b1..12cd70c8c 100644 --- a/rocketpool/node/stake-megapool-validator.go +++ b/rocketpool/node/stake-megapool-validator.go @@ -209,11 +209,15 @@ func (t *stakeMegapoolValidator) stakeValidator(rp *rocketpool.RocketPool, beaco t.log.Printlnf("There was an error during the proof creation process: %w", err) return err } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + return err + } t.log.Printlnf("The beacon state proof has been successfully created.") // Get the gas limit - gasLimits, err := megapool.EstimateStakeGas(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + gasLimits, err := megapool.EstimateStakeGas(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { t.log.Printlnf("Could not estimate the gas required to stake megapool validator %d: %w", validatorId, err) return err @@ -238,7 +242,7 @@ func (t *stakeMegapoolValidator) stakeValidator(rp *rocketpool.RocketPool, beaco opts.GasLimit = gas.Uint64() // Call stake - tx, err := megapool.Stake(rp, mp.GetAddress(), validatorId, slotTimestamp, validatorProof, slotProof, opts) + tx, err := megapool.Stake(rp, mp.GetAddress(), validatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { return err } diff --git a/rocketpool/watchtower/dissolve-invalid-credentials.go b/rocketpool/watchtower/dissolve-invalid-credentials.go index 12e801892..4123df1b8 100644 --- a/rocketpool/watchtower/dissolve-invalid-credentials.go +++ b/rocketpool/watchtower/dissolve-invalid-credentials.go @@ -166,9 +166,14 @@ func (t *dissolveInvalidCredentials) dissolveMegapoolValidator(validator megapoo t.log.Printlnf("error getting validator proof: %v", err) return } + proofData, err := megapool.EncodeValidatorProofBundleV1(validatorProof, slotProof) + if err != nil { + t.log.Printlnf("error encoding validator proof: %v", err) + return + } // Get the gas limit - gasLimits, err := megapool.EstimateDissolveWithProof(t.rp, validator.MegapoolAddress, validator.ValidatorId, slotTimestamp, validatorProof, slotProof, opts) + gasLimits, err := megapool.EstimateDissolveWithProof(t.rp, validator.MegapoolAddress, validator.ValidatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { t.log.Printlnf("error estimating the gas required to dissolve the validator: %v", err) return @@ -186,7 +191,7 @@ func (t *dissolveInvalidCredentials) dissolveMegapoolValidator(validator megapoo opts.GasLimit = gasLimits.Safe // Dissolve - tx, err := megapool.DissolveWithProof(t.rp, validator.MegapoolAddress, validator.ValidatorId, slotTimestamp, validatorProof, slotProof, opts) + tx, err := megapool.DissolveWithProof(t.rp, validator.MegapoolAddress, validator.ValidatorId, slotTimestamp, megapool.ValidatorProofVersion1, proofData, opts) if err != nil { t.log.Printlnf("error dissolving the validator: %v", err) return diff --git a/shared/services/beacon/client/std-http-client.go b/shared/services/beacon/client/std-http-client.go index 5f93bec98..60e67d5ad 100644 --- a/shared/services/beacon/client/std-http-client.go +++ b/shared/services/beacon/client/std-http-client.go @@ -144,6 +144,10 @@ func (c *StandardHttpClient) GetEth2Config() (beacon.Eth2Config, error) { if eth2Config.Data.GloasForkEpoch != nil { gloasForkEpoch = uint64(*eth2Config.Data.GloasForkEpoch) } + var capellaForkEpoch uint64 + if eth2Config.Data.CapellaForkEpoch != nil { + capellaForkEpoch = uint64(*eth2Config.Data.CapellaForkEpoch) + } out := beacon.Eth2Config{ GenesisForkVersion: genesis.Data.GenesisForkVersion, GenesisValidatorsRoot: genesis.Data.GenesisValidatorsRoot, @@ -154,6 +158,7 @@ func (c *StandardHttpClient) GetEth2Config() (beacon.Eth2Config, error) { SecondsPerEpoch: uint64(eth2Config.Data.SecondsPerSlot * eth2Config.Data.SlotsPerEpoch), EpochsPerSyncCommitteePeriod: uint64(eth2Config.Data.EpochsPerSyncCommitteePeriod), ShardCommitteePeriod: shardCommitteePeriod, + CapellaForkEpoch: capellaForkEpoch, GloasForkEpoch: gloasForkEpoch, } eth2ConfigCache.Store(&out) diff --git a/shared/services/beacon/client/types.go b/shared/services/beacon/client/types.go index 8f6e8c46c..799ac3415 100644 --- a/shared/services/beacon/client/types.go +++ b/shared/services/beacon/client/types.go @@ -44,6 +44,7 @@ type Eth2ConfigResponse struct { SecondsPerSlot uinteger `json:"SECONDS_PER_SLOT"` SlotsPerEpoch uinteger `json:"SLOTS_PER_EPOCH"` CapellaForkVersion byteArray `json:"CAPELLA_FORK_VERSION"` + CapellaForkEpoch *uinteger `json:"CAPELLA_FORK_EPOCH"` EpochsPerSyncCommitteePeriod uinteger `json:"EPOCHS_PER_SYNC_COMMITTEE_PERIOD"` ShardCommitteePeriod uinteger `json:"SHARD_COMMITTEE_PERIOD"` GloasForkEpoch *uinteger `json:"GLOAS_FORK_EPOCH"` diff --git a/shared/services/beacon/config.go b/shared/services/beacon/config.go index d50349fbb..7675e2fcf 100644 --- a/shared/services/beacon/config.go +++ b/shared/services/beacon/config.go @@ -21,6 +21,7 @@ type Eth2Config struct { SecondsPerEpoch uint64 `json:"seconds_per_epoch"` EpochsPerSyncCommitteePeriod uint64 `json:"epochs_per_sync_committee_period"` ShardCommitteePeriod uint64 `json:"shard_committee_period"` + CapellaForkEpoch uint64 `json:"capella_fork_epoch"` GloasForkEpoch uint64 `json:"gloas_fork_epoch"` } @@ -74,6 +75,16 @@ func (c *Eth2Config) GloasActivationSlot() uint64 { return c.GloasForkEpoch * c.SlotsPerEpoch } +// HistoricalSummaryOffset is the Capella-era index subtracted when proving +// historical_summaries[n]. Matches BeaconStateVerifier: +// slotCapella / SLOTS_PER_HISTORICAL_ROOT. +func (c *Eth2Config) HistoricalSummaryOffset() uint64 { + if c.SlotsPerEpoch == 0 { + return 0 + } + return (c.CapellaForkEpoch * c.SlotsPerEpoch) / 8192 +} + // GetSlotTime returns the time of a given slot for the network described by Eth2Config. func (c *Eth2Config) GetSlotTime(slot uint64) time.Time { // In the interest of keeping this pure, we'll just return genesis time for slots before genesis diff --git a/shared/services/megapools.go b/shared/services/megapools.go index 06aee6fa4..ddb7b9b0b 100644 --- a/shared/services/megapools.go +++ b/shared/services/megapools.go @@ -10,7 +10,6 @@ import ( "math/big" "net/http" "strconv" - "strings" "sync" "time" @@ -18,7 +17,6 @@ import ( "github.com/ethereum/go-ethereum/common" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/pk910/dynamic-ssz/treeproof" "github.com/urfave/cli/v3" "golang.org/x/sync/errgroup" @@ -35,6 +33,7 @@ import ( "github.com/rocket-pool/smartnode/shared/types/api" cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" "github.com/rocket-pool/smartnode/shared/types/eth2" + "github.com/rocket-pool/smartnode/shared/types/eth2/fork/electra" "github.com/rocket-pool/smartnode/shared/types/eth2/fork/fulu" "github.com/rocket-pool/smartnode/shared/types/eth2/fork/gloas" "github.com/rocket-pool/smartnode/shared/types/eth2/generic" @@ -42,10 +41,6 @@ import ( const MAX_WITHDRAWAL_SLOT_DISTANCE = 144000 // 20 days. -// gloasForkName is the Eth-Consensus-Version reported for the Gloas fork -// (ePBS, EIP-7732). -const gloasForkName = "gloas" - // API URL for the withdrawal proofs (base URL + network + withdrawal slot + validator index) const apiURL = "https://api.rocketpool.net/%s/withdrawals/proofs/%d/%d/%d" @@ -712,17 +707,11 @@ func GetWithdrawalProofForSlot(c *cli.Command, slot uint64, validatorIndex uint6 return megapool.FinalBalanceProof{}, 0, nil, err } - // Gloas (ePBS) removed the execution payload from beacon blocks, so - // withdrawals can no longer be found in, or proven against, a beacon block. - // Resolve the fork at the finalized checkpoint and use the state-based proof - // flow when Gloas is active. - finalizedSlot, finalizedFork, err := GetFinalizedBlockSlotAndFork(bc) + eth2Config, err := bc.GetEth2Config() if err != nil { return megapool.FinalBalanceProof{}, 0, nil, err } - if strings.EqualFold(finalizedFork, gloasForkName) { - return getGloasWithdrawalProofForSlot(c, bc, slot, validatorIndex, finalizedSlot) - } + capellaOffset := eth2Config.HistoricalSummaryOffset() withdrawalSlot, block, indexInWithdrawalsArray, withdrawal, finalizedBlock, err := FindWithdrawalBlockAndArrayPosition(slot, validatorIndex, bc) if err != nil { @@ -753,90 +742,17 @@ func GetWithdrawalProofForSlot(c *cli.Command, slot uint64, validatorIndex uint6 return megapool.FinalBalanceProof{}, 0, nil, err } - fuluState, ok := beaconState.(*fulu.BeaconState) - if !ok { - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("expected fulu.BeaconState, got %T", beaconState) + pastRootProof, err := pastRootWitnesses(bc, beaconState, withdrawalSlot, false, capellaOffset) + if err != nil { + return megapool.FinalBalanceProof{}, 0, nil, err } - - // Generate proofs separately - // 1. Withdrawal proof (withdrawal -> block_root) - // 2. Block roots proof (block_root -> state) - // 3. Block header proof (state_root in block header) - // Final order: [withdrawal, block_roots, block_header] - - var blockRootsProof [][]byte - var blockHeaderProof [][]byte - var summaryProof [][]byte - var historicalSummaryProof [][]byte - var finalProof [][]byte - - if response.WithdrawalSlot+generic.SlotsPerHistoricalRoot > finalizedBlock.Slot { - // Recent slot: use block_roots - // Get the block_roots proof separately - blockRootsProof, err = beaconState.BlockRootProof(response.WithdrawalSlot) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - blockHeaderProof, err = beaconState.BlockHeaderProof() - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - - finalProof = append(finalProof, withdrawalProof...) - finalProof = append(finalProof, blockRootsProof...) - finalProof = append(finalProof, blockHeaderProof...) - - } else { - // Historical slot: use historical_summaries - // Get historical summary block root proof - blockRootsStateSlot := generic.SlotsPerHistoricalRoot + ((response.WithdrawalSlot / generic.SlotsPerHistoricalRoot) * generic.SlotsPerHistoricalRoot) - blockRootsStateResponse, err := bc.GetBeaconStateSSZ(blockRootsStateSlot) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - blockRootsState, err := eth2.NewBeaconState(blockRootsStateResponse.Data, blockRootsStateResponse.Size, blockRootsStateResponse.Fork) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - summaryProof, err = blockRootsState.HistoricalSummaryBlockRootProof(int(response.WithdrawalSlot)) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - - // Get historical summary proof - var tree *treeproof.Node - tree, err = generic.SSZ.GetTree(fuluState) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("could not get state tree: %w", err) - } - - // Navigate to the historical_summaries (matching HistoricalSummaryProof logic) - beaconStateChunkCeil := uint64(64) - gid := uint64(1) - gid = gid*beaconStateChunkCeil + generic.BeaconStateHistoricalSummariesFieldIndex - arrayIndex := (response.WithdrawalSlot / generic.SlotsPerHistoricalRoot) - gid = gid*2*generic.BeaconStateHistoricalSummariesMaxLength + arrayIndex - - proof, err := tree.Prove(int(gid)) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("could not get proof for historical summary: %w", err) - } - historicalSummaryProof = proof.Hashes - - blockHeaderProof, err = fuluState.BlockHeaderProof() - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - // Concatenate in order: [withdrawal, summary_block_root, historical_summary, block_header] - finalProof = append(finalProof, withdrawalProof...) - finalProof = append(finalProof, summaryProof...) - finalProof = append(finalProof, historicalSummaryProof...) - finalProof = append(finalProof, blockHeaderProof...) + blockHeaderProof, err := beaconState.BlockHeaderProof() + if err != nil { + return megapool.FinalBalanceProof{}, 0, nil, err } - // Convert [][]byte to [][32]byte - proofWithFixedSize := ConvertToFixedSize(finalProof) - response.Witnesses = proofWithFixedSize + finalProof := concatProofs(withdrawalProof, pastRootProof, blockHeaderProof) + response.Witnesses = ConvertToFixedSize(finalProof) return response, finalizedBlock.Slot, beaconState, nil } @@ -875,166 +791,304 @@ func GetFinalizedBlockSlotAndFork(bc beacon.Client) (uint64, string, error) { return 0, "", fmt.Errorf("failed to find a finalized beacon block within %d slots of finalized epoch %d", maxAttempts, head.FinalizedEpoch) } -// getGloasWithdrawalProofForSlot builds a final-balance withdrawal proof for a -// Gloas (post-ePBS) chain. Pre-Gloas, a withdrawal is proven against the -// execution payload in the beacon block that carried it, and that block's root -// is anchored in a finalized state. In Gloas the payload is no longer part of -// the block; instead, the post-state of the withdrawal slot commits to the -// payload's withdrawals in payload_expected_withdrawals (see -// process_withdrawals / update_payload_expected_withdrawals in EIP-7732). The -// proof therefore chains: -// -// 1. withdrawal -> payload_expected_withdrawals -> state root of the withdrawal slot -// 2. state root of the withdrawal slot -> finalized state root, via -// state_roots (recent slots) or historical_summaries (historical slots) -// 3. finalized state root -> finalized block root (block header proof), which -// is what the contract checks against the EIP-4788 beacon roots oracle -// -// The witness layout mirrors the pre-Gloas one: -// -// recent: [expected_withdrawal, state_roots, block_header] -// historical: [expected_withdrawal, summary_state_root, historical_summary, block_header] -func getGloasWithdrawalProofForSlot(c *cli.Command, bc beacon.Client, slot uint64, validatorIndex uint64, finalizedSlot uint64) (megapool.FinalBalanceProof, uint64, eth2.BeaconState, error) { - // Create a new response - response := megapool.FinalBalanceProof{} - response.ValidatorIndex = validatorIndex - - ec, err := GetEthClient(c) +// GetFinalBalanceProofBundle builds the versioned proof payload for +// RocketMegapoolManager.notifyFinalBalance. Version 1 is used for pre-Gloas +// withdrawals; version 2 for post-Gloas withdrawals. +func GetFinalBalanceProofBundle(c *cli.Command, slotHint uint64, validatorIndex uint64, validatorPubkey types.ValidatorPubkey, megapoolAddress common.Address, w wallet.Wallet) (*big.Int, []byte, uint64, error) { + bc, err := GetBeaconClient(c) if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err } eth2Config, err := bc.GetEth2Config() if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err } - // Find the payload that carried the withdrawal on the execution layer and - // map it back to the consensus slot whose state committed to it. - withdrawalSlot, indexInWithdrawalsArray, withdrawal, err := FindGloasWithdrawalSlotAndArrayPosition(slot, validatorIndex, ec, eth2Config) + withdrawalProof, proofSlot, stateUsed, err := GetWithdrawalProofForSlot(c, slotHint, validatorIndex) if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + if errors.Is(err, ErrGloasBoundaryReached) { + return getGloasFinalBalanceProofBundle(c, bc, eth2Config, slotHint, validatorIndex, validatorPubkey, megapoolAddress, w) + } + cfg, cfgErr := GetConfig(c) + if cfgErr != nil { + return nil, nil, 0, err + } + fmt.Printf("An error occurred while getting the withdrawal proof: %s\n", err) + head, headErr := bc.GetBeaconHead() + if headErr != nil { + return nil, nil, 0, err + } + finalizedSlot := head.FinalizedEpoch * eth2Config.SlotsPerEpoch + network := cfg.Smartnode.Network.Value.(cfgtypes.Network) + var apiErr error + withdrawalProof, proofSlot, apiErr = GetWithdrawalProofForSlotFromAPI(c, finalizedSlot, slotHint, validatorIndex, network) + if apiErr != nil { + fmt.Printf("An error occurred while getting the withdrawal proof from the Rocket Pool API: %s\n", apiErr) + return nil, nil, 0, err + } + stateUsed = nil } - response.WithdrawalSlot = withdrawalSlot - response.Amount = big.NewInt(0).SetUint64(withdrawal.Amount) - response.IndexInWithdrawalsArray = uint(indexInWithdrawalsArray) - response.WithdrawalIndex = withdrawal.Index - response.WithdrawalAddress = withdrawal.Address + validatorProof, slotTimestamp, slotProof, err := GetValidatorProof(c, proofSlot, w, eth2Config, megapoolAddress, validatorPubkey, stateUsed) + if err != nil { + return nil, nil, 0, err + } + + encoded, err := megapool.EncodeFinalBalanceProofBundleV1( + megapool.WithdrawalProof{ + WithdrawalSlot: withdrawalProof.WithdrawalSlot, + WithdrawalNum: uint16(withdrawalProof.IndexInWithdrawalsArray), + Withdrawal: megapool.Withdrawal{ + Index: withdrawalProof.WithdrawalIndex, + ValidatorIndex: validatorIndex, + WithdrawalCredentials: withdrawalProof.WithdrawalAddress, + AmountInGwei: withdrawalProof.Amount.Uint64(), + }, + Witnesses: withdrawalProof.Witnesses, + }, + validatorProof, + slotProof, + ) + if err != nil { + return nil, nil, 0, err + } + return megapool.FinalBalanceProofVersion1, encoded, slotTimestamp, nil +} + +func getGloasFinalBalanceProofBundle(c *cli.Command, bc beacon.Client, eth2Config beacon.Eth2Config, slotHint uint64, validatorIndex uint64, validatorPubkey types.ValidatorPubkey, megapoolAddress common.Address, w wallet.Wallet) (*big.Int, []byte, uint64, error) { + ec, err := GetEthClient(c) + if err != nil { + return nil, nil, 0, err + } + finalizedSlot, _, err := GetFinalizedBlockSlotAndFork(bc) + if err != nil { + return nil, nil, 0, err + } + capellaOffset := eth2Config.HistoricalSummaryOffset() + + withdrawalSlot, indexInWithdrawalsArray, withdrawal, err := FindGloasWithdrawalSlotAndArrayPosition(slotHint, validatorIndex, ec, eth2Config) + if err != nil { + return nil, nil, 0, err + } + if withdrawalSlot == 0 { + return nil, nil, 0, fmt.Errorf("withdrawal slot must be greater than zero") + } - // The state at the withdrawal slot commits to the payload's withdrawals in - // payload_expected_withdrawals; prove the withdrawal against its root. withdrawalStateResponse, err := bc.GetBeaconStateSSZ(withdrawalSlot) if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err } withdrawalState, err := eth2.NewBeaconState(withdrawalStateResponse.Data, withdrawalStateResponse.Size, withdrawalStateResponse.Fork) if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err } gloasWithdrawalState, ok := withdrawalState.(*gloas.BeaconState) if !ok { - // TODO: Check when the withdrawal predates the Gloas fork while finality is past it. - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("withdrawal slot %d is in the %s fork but finality is in gloas; cross-fork withdrawal proofs are not supported yet", withdrawalSlot, withdrawalStateResponse.Fork) + return nil, nil, 0, fmt.Errorf("withdrawal slot %d is in the %s fork; version 2 final balance proofs require Gloas", withdrawalSlot, withdrawalStateResponse.Fork) } - - // Sanity check that the withdrawal found on the EL matches the entry the - // beacon state committed to at the same position. if err := verifyExpectedWithdrawal(gloasWithdrawalState, indexInWithdrawalsArray, withdrawal); err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err + } + if uint64(len(gloasWithdrawalState.Balances)) <= validatorIndex { + return nil, nil, 0, fmt.Errorf("validator index %d is outside balances length %d", validatorIndex, len(gloasWithdrawalState.Balances)) + } + if gloasWithdrawalState.Balances[validatorIndex] != 0 { + return nil, nil, 0, fmt.Errorf("validator %d balance is %d gwei at withdrawal slot %d; expected zero", validatorIndex, gloasWithdrawalState.Balances[validatorIndex], withdrawalSlot) } - withdrawalProof, err := gloasWithdrawalState.ProveExpectedWithdrawal(uint64(indexInWithdrawalsArray)) + matching := 0 + for _, expected := range gloasWithdrawalState.PayloadExpectedWithdrawals { + if expected.ValidatorIndex == validatorIndex { + matching++ + } + } + if matching != 1 { + return nil, nil, 0, fmt.Errorf("expected exactly one payload_expected_withdrawals entry for validator %d at slot %d, found %d", validatorIndex, withdrawalSlot, matching) + } + + withdrawalLeafProof, err := gloasWithdrawalState.ProveExpectedWithdrawal(uint64(indexInWithdrawalsArray)) if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err + } + balanceLeafProof, balanceChunk, err := gloasWithdrawalState.ProveValidatorBalanceChunk(validatorIndex) + if err != nil { + return nil, nil, 0, err + } + if !gloas.IsZeroValidatorBalance(balanceChunk, validatorIndex) { + return nil, nil, 0, fmt.Errorf("validator %d balance chunk is not zero at withdrawal slot %d", validatorIndex, withdrawalSlot) } - // Get the finalized beacon state to anchor the withdrawal slot's state root. - stateResponse, err := bc.GetBeaconStateSSZ(finalizedSlot) + previousSlot := withdrawalSlot - 1 + previousStateResponse, err := bc.GetBeaconStateSSZ(previousSlot) if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err } - beaconState, err := eth2.NewBeaconState(stateResponse.Data, stateResponse.Size, stateResponse.Fork) + previousState, err := eth2.NewBeaconState(previousStateResponse.Data, previousStateResponse.Size, previousStateResponse.Fork) if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + return nil, nil, 0, err } - gloasState, ok := beaconState.(*gloas.BeaconState) - if !ok { - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("expected gloas.BeaconState, got %T", beaconState) + nextWithdrawalIndex, nextWithdrawalIndexLeafProof, err := proveNextWithdrawalIndex(previousState) + if err != nil { + return nil, nil, 0, err + } + if withdrawal.Index != nextWithdrawalIndex+uint64(indexInWithdrawalsArray) { + return nil, nil, 0, fmt.Errorf("withdrawal index %d is stale relative to preceding next_withdrawal_index %d and withdrawal number %d", withdrawal.Index, nextWithdrawalIndex, indexInWithdrawalsArray) } - var finalProof [][]byte - if withdrawalSlot+generic.SlotsPerHistoricalRoot > finalizedSlot { - // Recent slot: the finalized state's state_roots ring still holds the - // withdrawal slot's post-state root. - stateRootsProof, err := gloasState.StateRootProof(withdrawalSlot) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - blockHeaderProof, err := gloasState.BlockHeaderProof() - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } + proofStateResponse, err := bc.GetBeaconStateSSZ(finalizedSlot) + if err != nil { + return nil, nil, 0, err + } + proofState, err := eth2.NewBeaconState(proofStateResponse.Data, proofStateResponse.Size, proofStateResponse.Fork) + if err != nil { + return nil, nil, 0, err + } + if _, ok := proofState.(*gloas.BeaconState); !ok { + return nil, nil, 0, fmt.Errorf("expected gloas.BeaconState for the proof slot, got %T", proofState) + } - finalProof = append(finalProof, withdrawalProof...) - finalProof = append(finalProof, stateRootsProof...) - finalProof = append(finalProof, blockHeaderProof...) - } else { - // Historical slot: use historical_summaries. The era-aligned state's - // state_roots ring holds the whole era containing the withdrawal slot. - blockRootsStateSlot := generic.SlotsPerHistoricalRoot + ((withdrawalSlot / generic.SlotsPerHistoricalRoot) * generic.SlotsPerHistoricalRoot) - blockRootsStateResponse, err := bc.GetBeaconStateSSZ(blockRootsStateSlot) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - blockRootsState, err := eth2.NewBeaconState(blockRootsStateResponse.Data, blockRootsStateResponse.Size, blockRootsStateResponse.Fork) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } - gloasBlockRootsState, ok := blockRootsState.(*gloas.BeaconState) - if !ok { - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("expected gloas.BeaconState for the era-aligned state, got %T", blockRootsState) - } - summaryStateRootProof, err := gloasBlockRootsState.HistoricalSummaryStateRootProof(int(withdrawalSlot)) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err - } + withdrawalPastRoot, err := pastRootWitnesses(bc, proofState, withdrawalSlot, true, capellaOffset) + if err != nil { + return nil, nil, 0, err + } + previousPastRoot, err := pastRootWitnesses(bc, proofState, previousSlot, true, capellaOffset) + if err != nil { + return nil, nil, 0, err + } + blockHeaderProof, err := proofState.BlockHeaderProof() + if err != nil { + return nil, nil, 0, err + } - // Prove the HistoricalSummary itself in the finalized state, navigating - // the EIP-7688 ProgressiveContainer with the Gloas g-indices. As in the - // pre-Gloas flow above, the block-header proof stays a separate final - // segment (the state's latest block header has a zeroed state root at - // the block's own slot, so it must be recomputed). - tree, err := generic.SSZ.GetTree(gloasState) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("could not get state tree: %w", err) - } - arrayIndex := withdrawalSlot / generic.SlotsPerHistoricalRoot - gid := generic.GetGeneralizedIndexForListElement( - gloas.GetGeneralizedIndexForHistoricalSummaries(), - generic.BeaconStateHistoricalSummariesMaxLength, - arrayIndex, - ) - proof, err := tree.Prove(int(gid)) - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, fmt.Errorf("could not get proof for historical summary: %w", err) + validatorProof, slotTimestamp, slotProof, err := GetValidatorProof(c, finalizedSlot, w, eth2Config, megapoolAddress, validatorPubkey, proofState) + if err != nil { + return nil, nil, 0, err + } + + withdrawalEpoch := withdrawalSlot / eth2Config.SlotsPerEpoch + validators := proofState.GetValidators() + if validatorIndex >= uint64(len(validators)) { + return nil, nil, 0, fmt.Errorf("validator index %d is outside validators length %d", validatorIndex, len(validators)) + } + if withdrawalEpoch < validators[validatorIndex].WithdrawableEpoch { + return nil, nil, 0, fmt.Errorf("withdrawal epoch %d is earlier than validator withdrawable epoch %d", withdrawalEpoch, validators[validatorIndex].WithdrawableEpoch) + } + + encoded, err := megapool.EncodeFinalBalanceProofBundleV2( + megapool.WithdrawalProof{ + WithdrawalSlot: withdrawalSlot, + WithdrawalNum: uint16(indexInWithdrawalsArray), + Withdrawal: megapool.Withdrawal{ + Index: withdrawal.Index, + ValidatorIndex: withdrawal.ValidatorIndex, + WithdrawalCredentials: withdrawal.Address, + AmountInGwei: withdrawal.Amount, + }, + Witnesses: ConvertToFixedSize(concatProofs(withdrawalLeafProof, withdrawalPastRoot, blockHeaderProof)), + }, + validatorProof, + slotProof, + megapool.NextWithdrawalIndexProof{ + NextWithdrawalIndex: nextWithdrawalIndex, + Witnesses: ConvertToFixedSize(concatProofs(nextWithdrawalIndexLeafProof, previousPastRoot, blockHeaderProof)), + }, + megapool.ValidatorBalanceProof{ + BalanceChunk: balanceChunk, + Witnesses: ConvertToFixedSize(concatProofs(balanceLeafProof, withdrawalPastRoot, blockHeaderProof)), + }, + ) + if err != nil { + return nil, nil, 0, err + } + return megapool.FinalBalanceProofVersion2, encoded, slotTimestamp, nil +} + +func concatProofs(parts ...[][]byte) [][]byte { + n := 0 + for _, part := range parts { + n += len(part) + } + out := make([][]byte, 0, n) + for _, part := range parts { + out = append(out, part...) + } + return out +} + +func pastRootWitnesses(bc beacon.Client, proofState eth2.BeaconState, targetSlot uint64, useStateRoot bool, capellaOffset uint64) ([][]byte, error) { + proofSlot := proofState.GetSlot() + if proofSlot <= targetSlot { + return nil, fmt.Errorf("proof slot %d must be later than target slot %d", proofSlot, targetSlot) + } + if !generic.IsHistoricalProof(proofSlot, targetSlot) { + if useStateRoot { + gloasState, ok := proofState.(*gloas.BeaconState) + if !ok { + return nil, fmt.Errorf("state_roots past-root path requires a Gloas proof state, got %T", proofState) + } + return gloasState.StateRootProof(targetSlot) } - historicalSummaryProof := proof.Hashes + return proofState.BlockRootProof(targetSlot) + } - blockHeaderProof, err := gloasState.BlockHeaderProof() - if err != nil { - return megapool.FinalBalanceProof{}, 0, nil, err + eraEndSlot := generic.SlotsPerHistoricalRoot + (targetSlot/generic.SlotsPerHistoricalRoot)*generic.SlotsPerHistoricalRoot + eraStateResponse, err := bc.GetBeaconStateSSZ(eraEndSlot) + if err != nil { + return nil, err + } + eraState, err := eth2.NewBeaconState(eraStateResponse.Data, eraStateResponse.Size, eraStateResponse.Fork) + if err != nil { + return nil, err + } + + var summaryRootProof [][]byte + if useStateRoot { + gloasEraState, ok := eraState.(*gloas.BeaconState) + if !ok { + return nil, fmt.Errorf("expected gloas.BeaconState for the era-aligned state, got %T", eraState) } + summaryRootProof, err = gloasEraState.HistoricalSummaryStateRootProof(int(targetSlot)) + } else { + summaryRootProof, err = eraState.HistoricalSummaryBlockRootProof(int(targetSlot)) + } + if err != nil { + return nil, err + } - finalProof = append(finalProof, withdrawalProof...) - finalProof = append(finalProof, summaryStateRootProof...) - finalProof = append(finalProof, historicalSummaryProof...) - finalProof = append(finalProof, blockHeaderProof...) + summaryProof, err := historicalSummariesElementProof(proofState, targetSlot, capellaOffset) + if err != nil { + return nil, err } + return concatProofs(summaryRootProof, summaryProof), nil +} - // Convert [][]byte to [][32]byte - response.Witnesses = ConvertToFixedSize(finalProof) +func historicalSummariesElementProof(state eth2.BeaconState, slot uint64, capellaOffset uint64) ([][]byte, error) { + switch s := state.(type) { + case *gloas.BeaconState: + return s.HistoricalSummariesElementProof(slot, capellaOffset) + case *fulu.BeaconState: + return s.HistoricalSummariesElementProof(slot, capellaOffset) + case *electra.BeaconState: + return s.HistoricalSummariesElementProof(slot, capellaOffset) + default: + return nil, fmt.Errorf("unsupported beacon state type %T", state) + } +} - return response, finalizedSlot, beaconState, nil +func proveNextWithdrawalIndex(state eth2.BeaconState) (uint64, [][]byte, error) { + switch s := state.(type) { + case *gloas.BeaconState: + proof, err := s.ProveNextWithdrawalIndex() + return s.NextWithdrawalIndex, proof, err + case *fulu.BeaconState: + proof, err := s.ProveNextWithdrawalIndex() + return s.NextWithdrawalIndex, proof, err + case *electra.BeaconState: + proof, err := s.ProveNextWithdrawalIndex() + return s.NextWithdrawalIndex, proof, err + default: + return 0, nil, fmt.Errorf("unsupported beacon state type %T for next_withdrawal_index", state) + } } // verifyExpectedWithdrawal checks that the withdrawal found on the execution diff --git a/shared/types/eth2/fork/electra/state_electra.go b/shared/types/eth2/fork/electra/state_electra.go index d32b8639e..d711cef65 100644 --- a/shared/types/eth2/fork/electra/state_electra.go +++ b/shared/types/eth2/fork/electra/state_electra.go @@ -2,6 +2,7 @@ package electra import ( "bytes" + "encoding/binary" "errors" "fmt" "reflect" @@ -80,6 +81,10 @@ func GetGeneralizedIndexForSlot() uint64 { return generic.ContainerFieldGindex(getStateChunkSize(), generic.BeaconStateSlotIndex) } +func GetGeneralizedIndexForNextWithdrawalIndex() uint64 { + return generic.ContainerFieldGindex(getStateChunkSize(), generic.BeaconStateNextWithdrawalIndexFieldIndex) +} + // ValidatorAndSlotProof produces both the validator proof and the slot proof // for the state's current slot lowering the memory cost for building the proofs. func (state *BeaconState) ValidatorAndSlotProof(validatorIndex uint64) ([][]byte, [][]byte, error) { @@ -149,9 +154,8 @@ func (state *BeaconState) blockHeaderToStateProof(blockHeader *generic.BeaconBlo return blockHeaderProof.Hashes, nil } -func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint64) ([][]byte, error) { - isHistorical := slot+generic.SlotsPerHistoricalRoot <= state.Slot - if !isHistorical { +func (state *BeaconState) HistoricalSummariesElementProof(slot uint64, capellaOffset uint64) ([][]byte, error) { + if !generic.IsHistoricalProof(state.Slot, slot) { return nil, fmt.Errorf("slot %d is less than %d slots in the past from the state at slot %d, you must build a proof from the block_roots field instead", slot, generic.SlotsPerHistoricalRoot, state.Slot) } tree, err := generic.SSZ.GetTree(state) @@ -163,20 +167,28 @@ func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint gid := uint64(1) gid = gid*beaconStateChunkCeil + generic.BeaconStateHistoricalSummariesFieldIndex // Navigate into the historical summaries vector. - arrayIndex := (slot / generic.SlotsPerHistoricalRoot) + arrayIndex := (slot / generic.SlotsPerHistoricalRoot) - capellaOffset gid = gid*2*generic.BeaconStateHistoricalSummariesMaxLength + arrayIndex proof, err := tree.Prove(int(gid)) if err != nil { return nil, fmt.Errorf("could not get proof for historical block root: %w", err) } + return proof.Hashes, nil +} + +func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint64) ([][]byte, error) { + proof, err := state.HistoricalSummariesElementProof(slot, capellaOffset) + if err != nil { + return nil, err + } // The EL proves against BeaconBlockHeader root, so we need to merge the state proof with that. blockHeaderProof, err := state.blockHeaderToStateProof(state.LatestBlockHeader) if err != nil { return nil, fmt.Errorf("could not get block header proof: %w", err) } - return append(proof.Hashes, blockHeaderProof...), nil + return append(proof, blockHeaderProof...), nil } func (state *BeaconState) HistoricalSummaryBlockRootProof(slot int) ([][]byte, error) { @@ -210,8 +222,7 @@ func (state *BeaconState) HistoricalSummaryBlockRootProof(slot int) ([][]byte, e } func (state *BeaconState) BlockRootProof(slot uint64) ([][]byte, error) { - isHistorical := slot+generic.SlotsPerHistoricalRoot <= state.Slot - if isHistorical { + if generic.IsHistoricalProof(state.Slot, slot) { return nil, fmt.Errorf("slot %d is more than %d slots in the past from the state at slot %d, you must build a proof from the historical_summaries instead", slot, generic.SlotsPerHistoricalRoot, state.Slot) } @@ -243,6 +254,23 @@ func (state *BeaconState) BlockRootProof(slot uint64) ([][]byte, error) { return append(proof.Hashes, blockHeaderProof...), nil } +func (state *BeaconState) ProveNextWithdrawalIndex() ([][]byte, error) { + stateTree, err := generic.SSZ.GetTree(state) + if err != nil { + return nil, fmt.Errorf("could not get state tree: %w", err) + } + proof, err := stateTree.Prove(int(GetGeneralizedIndexForNextWithdrawalIndex())) + if err != nil { + return nil, fmt.Errorf("could not get proof for next_withdrawal_index: %w", err) + } + var expected [32]byte + binary.LittleEndian.PutUint64(expected[:8], state.NextWithdrawalIndex) + if !bytes.Equal(proof.Leaf, expected[:]) { + return nil, fmt.Errorf("proof leaf does not match next_withdrawal_index") + } + return proof.Hashes, nil +} + func (state *BeaconState) GetValidators() []*generic.Validator { return state.Validators } diff --git a/shared/types/eth2/fork/fulu/state_fulu.go b/shared/types/eth2/fork/fulu/state_fulu.go index 258708bf5..d28338ba3 100644 --- a/shared/types/eth2/fork/fulu/state_fulu.go +++ b/shared/types/eth2/fork/fulu/state_fulu.go @@ -2,6 +2,7 @@ package fulu import ( "bytes" + "encoding/binary" "errors" "fmt" "reflect" @@ -83,6 +84,10 @@ func GetGeneralizedIndexForSlot() uint64 { return generic.ContainerFieldGindex(getStateChunkSize(), generic.BeaconStateSlotIndex) } +func GetGeneralizedIndexForNextWithdrawalIndex() uint64 { + return generic.ContainerFieldGindex(getStateChunkSize(), generic.BeaconStateNextWithdrawalIndexFieldIndex) +} + // ValidatorAndSlotProof produces both the validator proof and the slot proof // for the state's current slot func (state *BeaconState) ValidatorAndSlotProof(validatorIndex uint64) ([][]byte, [][]byte, error) { @@ -152,9 +157,8 @@ func (state *BeaconState) blockHeaderToStateProof(blockHeader *generic.BeaconBlo return blockHeaderProof.Hashes, nil } -func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint64) ([][]byte, error) { - isHistorical := slot+generic.SlotsPerHistoricalRoot <= state.Slot - if !isHistorical { +func (state *BeaconState) HistoricalSummariesElementProof(slot uint64, capellaOffset uint64) ([][]byte, error) { + if !generic.IsHistoricalProof(state.Slot, slot) { return nil, fmt.Errorf("slot %d is less than %d slots in the past from the state at slot %d, you must build a proof from the block_roots field instead", slot, generic.SlotsPerHistoricalRoot, state.Slot) } tree, err := generic.SSZ.GetTree(state) @@ -174,13 +178,21 @@ func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint if err != nil { return nil, fmt.Errorf("could not get proof for historical block root: %w", err) } + return proof.Hashes, nil +} + +func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint64) ([][]byte, error) { + proof, err := state.HistoricalSummariesElementProof(slot, capellaOffset) + if err != nil { + return nil, err + } // The EL proves against BeaconBlockHeader root, so we need to merge the state proof with that. blockHeaderProof, err := state.blockHeaderToStateProof(state.LatestBlockHeader) if err != nil { return nil, fmt.Errorf("could not get block header proof: %w", err) } - return append(proof.Hashes, blockHeaderProof...), nil + return append(proof, blockHeaderProof...), nil } func (state *BeaconState) HistoricalSummaryBlockRootProof(slot int) ([][]byte, error) { @@ -214,8 +226,7 @@ func (state *BeaconState) HistoricalSummaryBlockRootProof(slot int) ([][]byte, e } func (state *BeaconState) BlockRootProof(slot uint64) ([][]byte, error) { - isHistorical := slot+generic.SlotsPerHistoricalRoot <= state.Slot - if isHistorical { + if generic.IsHistoricalProof(state.Slot, slot) { return nil, fmt.Errorf("slot %d is more than %d slots in the past from the state at slot %d, you must build a proof from the historical_summaries instead", slot, generic.SlotsPerHistoricalRoot, state.Slot) } @@ -241,6 +252,23 @@ func (state *BeaconState) BlockRootProof(slot uint64) ([][]byte, error) { return proof.Hashes, nil } +func (state *BeaconState) ProveNextWithdrawalIndex() ([][]byte, error) { + stateTree, err := generic.SSZ.GetTree(state) + if err != nil { + return nil, fmt.Errorf("could not get state tree: %w", err) + } + proof, err := stateTree.Prove(int(GetGeneralizedIndexForNextWithdrawalIndex())) + if err != nil { + return nil, fmt.Errorf("could not get proof for next_withdrawal_index: %w", err) + } + var expected [32]byte + binary.LittleEndian.PutUint64(expected[:8], state.NextWithdrawalIndex) + if !bytes.Equal(proof.Leaf, expected[:]) { + return nil, fmt.Errorf("proof leaf does not match next_withdrawal_index") + } + return proof.Hashes, nil +} + func (state *BeaconState) BlockHeaderProof() ([][]byte, error) { // Construct block header with state root stateRoot, err := generic.SSZ.HashTreeRoot(state) diff --git a/shared/types/eth2/fork/gloas/gloas_proofs_test.go b/shared/types/eth2/fork/gloas/gloas_proofs_test.go index f809807d5..5dbd8bbe4 100644 --- a/shared/types/eth2/fork/gloas/gloas_proofs_test.go +++ b/shared/types/eth2/fork/gloas/gloas_proofs_test.go @@ -195,6 +195,10 @@ func TestGloasWithdrawalProofGindices(t *testing.T) { want uint64 }{ {"state_roots", GetGeneralizedIndexForStateRoots(), 353}, + {"balances", GetGeneralizedIndexForBalances(), 359}, + {"balances chunk 0", GetGeneralizedIndexForBalanceChunk(0), 1436}, + {"balances chunk 1", GetGeneralizedIndexForBalanceChunk(4), 11496}, + {"next_withdrawal_index", GetGeneralizedIndexForNextWithdrawalIndex(), 2948}, {"payload_expected_withdrawals", GetGeneralizedIndexForPayloadExpectedWithdrawals(), 2967}, {"payload_expected_withdrawals[0]", GetGeneralizedIndexForExpectedWithdrawal(0), 11868}, {"payload_expected_withdrawals[1]", GetGeneralizedIndexForExpectedWithdrawal(1), 94952}, @@ -308,6 +312,12 @@ func TestStateRootProofProgressive(t *testing.T) { if _, err := state.StateRootProof(0); err == nil { t.Fatalf("expected historical-slot error") } + + // Distance equal to SLOTS_PER_HISTORICAL_ROOT is still the direct ring. + state.Slot = generic.SlotsPerHistoricalRoot + if _, err := state.StateRootProof(0); err != nil { + t.Fatalf("distance 8192 should use the direct state_roots path: %v", err) + } } func TestHistoricalSummaryStateRootProof(t *testing.T) { @@ -363,3 +373,72 @@ func TestHistoricalSummaryStateRootProof(t *testing.T) { t.Fatalf("expected non-aligned state error") } } + +func TestProveNextWithdrawalIndex(t *testing.T) { + state := minimalBeaconState() + state.NextWithdrawalIndex = 42 + + stateTree, err := generic.SSZ.GetTree(state) + if err != nil { + t.Fatalf("GetTree: %v", err) + } + stateRoot := stateTree.Hash() + + proofHashes, err := state.ProveNextWithdrawalIndex() + if err != nil { + t.Fatalf("ProveNextWithdrawalIndex: %v", err) + } + direct, err := stateTree.Prove(int(GetGeneralizedIndexForNextWithdrawalIndex())) + if err != nil { + t.Fatalf("direct Prove: %v", err) + } + if ok, err := treeproof.VerifyProof(stateRoot, direct); err != nil || !ok { + t.Fatalf("verify next_withdrawal_index: ok=%v err=%v", ok, err) + } + if len(proofHashes) != len(direct.Hashes) { + t.Fatalf("helper proof length %d != direct %d", len(proofHashes), len(direct.Hashes)) + } +} + +func TestProveValidatorBalanceChunk(t *testing.T) { + state := minimalBeaconState() + state.Balances = []uint64{1, 0, 3, 4, 5} + + _, chunk, err := state.ProveValidatorBalanceChunk(1) + if err != nil { + t.Fatalf("ProveValidatorBalanceChunk: %v", err) + } + if !IsZeroValidatorBalance(chunk, 1) { + t.Fatal("expected validator 1 lane to be zero") + } + if IsZeroValidatorBalance(chunk, 0) { + t.Fatal("expected validator 0 lane to be non-zero") + } + + stateTree, err := generic.SSZ.GetTree(state) + if err != nil { + t.Fatalf("GetTree: %v", err) + } + direct, err := stateTree.Prove(int(GetGeneralizedIndexForBalanceChunk(1))) + if err != nil { + t.Fatalf("direct Prove: %v", err) + } + if ok, err := treeproof.VerifyProof(stateTree.Hash(), direct); err != nil || !ok { + t.Fatalf("verify balance chunk: ok=%v err=%v", ok, err) + } + + if _, _, err := state.ProveValidatorBalanceChunk(uint64(len(state.Balances))); err == nil { + t.Fatal("expected out-of-bounds error") + } +} + +func TestHistoricalBoundarySplit(t *testing.T) { + const w uint64 = 100 + proofSlot := w + generic.SlotsPerHistoricalRoot + if generic.IsHistoricalProof(proofSlot, w) { + t.Fatalf("W at distance 8192 should use the direct path") + } + if !generic.IsHistoricalProof(proofSlot, w-1) { + t.Fatalf("W-1 at distance 8193 should use the historical path") + } +} diff --git a/shared/types/eth2/fork/gloas/state_gloas.go b/shared/types/eth2/fork/gloas/state_gloas.go index d79ab4070..621af530d 100644 --- a/shared/types/eth2/fork/gloas/state_gloas.go +++ b/shared/types/eth2/fork/gloas/state_gloas.go @@ -2,8 +2,10 @@ package gloas import ( "bytes" + "encoding/binary" "errors" "fmt" + "math/big" "github.com/rocket-pool/smartnode/shared/types/eth2/generic" ) @@ -15,6 +17,8 @@ const ( beaconStateBlockRootsFieldIndex = generic.BeaconStateBlockRootsFieldIndex // 5 beaconStateStateRootsFieldIndex = generic.BeaconStateStateRootsFieldIndex // 6 beaconStateValidatorsFieldIndex = generic.BeaconStateValidatorsIndex // 11 + beaconStateBalancesFieldIndex = generic.BeaconStateBalancesFieldIndex // 12 + beaconStateNextWithdrawalIndexFieldIndex = generic.BeaconStateNextWithdrawalIndexFieldIndex // 25 beaconStateHistoricalSummariesFieldIndex = generic.BeaconStateHistoricalSummariesFieldIndex // 27 // New in Gloas (EIP-7732): payload_expected_withdrawals commits to the // withdrawals of the execution payload bid on at this slot. @@ -163,6 +167,27 @@ func GetGeneralizedIndexForValidator(validatorIndex uint64) uint64 { ) } +// GetGeneralizedIndexForNextWithdrawalIndex returns the gindex of +// next_withdrawal_index inside the Gloas ProgressiveContainer BeaconState. +func GetGeneralizedIndexForNextWithdrawalIndex() uint64 { + return generic.ProgressiveContainerFieldGindex(beaconStateNextWithdrawalIndexFieldIndex) +} + +// GetGeneralizedIndexForBalances returns the gindex of the balances field root +// inside the Gloas ProgressiveContainer BeaconState. +func GetGeneralizedIndexForBalances() uint64 { + return generic.ProgressiveContainerFieldGindex(beaconStateBalancesFieldIndex) +} + +// GetGeneralizedIndexForBalanceChunk returns the gindex of the packed +// balances leaf that contains validatorIndex (4 uint64s per 32-byte chunk). +func GetGeneralizedIndexForBalanceChunk(validatorIndex uint64) uint64 { + return generic.GetGeneralizedIndexForProgressiveListElement( + GetGeneralizedIndexForBalances(), + validatorIndex/4, + ) +} + // ValidatorAndSlotProof produces both the validator proof and the slot proof // for the state's current slot, using EIP-7688 progressive g-indices. func (state *BeaconState) ValidatorAndSlotProof(validatorIndex uint64) ([][]byte, [][]byte, error) { @@ -232,9 +257,8 @@ func (state *BeaconState) blockHeaderToStateProof(blockHeader *generic.BeaconBlo return blockHeaderProof.Hashes, nil } -func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint64) ([][]byte, error) { - isHistorical := slot+generic.SlotsPerHistoricalRoot <= state.Slot - if !isHistorical { +func (state *BeaconState) HistoricalSummariesElementProof(slot uint64, capellaOffset uint64) ([][]byte, error) { + if !generic.IsHistoricalProof(state.Slot, slot) { return nil, fmt.Errorf("slot %d is less than %d slots in the past from the state at slot %d, you must build a proof from the block_roots field instead", slot, generic.SlotsPerHistoricalRoot, state.Slot) } tree, err := generic.SSZ.GetTree(state) @@ -254,13 +278,21 @@ func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint if err != nil { return nil, fmt.Errorf("could not get proof for historical block root: %w", err) } + return proof.Hashes, nil +} + +func (state *BeaconState) HistoricalSummaryProof(slot uint64, capellaOffset uint64) ([][]byte, error) { + proof, err := state.HistoricalSummariesElementProof(slot, capellaOffset) + if err != nil { + return nil, err + } // The EL proves against BeaconBlockHeader root, so we need to merge the state proof with that. blockHeaderProof, err := state.blockHeaderToStateProof(state.LatestBlockHeader) if err != nil { return nil, fmt.Errorf("could not get block header proof: %w", err) } - return append(proof.Hashes, blockHeaderProof...), nil + return append(proof, blockHeaderProof...), nil } func (state *BeaconState) HistoricalSummaryBlockRootProof(slot int) ([][]byte, error) { @@ -294,8 +326,7 @@ func (state *BeaconState) HistoricalSummaryBlockRootProof(slot int) ([][]byte, e } func (state *BeaconState) BlockRootProof(slot uint64) ([][]byte, error) { - isHistorical := slot+generic.SlotsPerHistoricalRoot <= state.Slot - if isHistorical { + if generic.IsHistoricalProof(state.Slot, slot) { return nil, fmt.Errorf("slot %d is more than %d slots in the past from the state at slot %d, you must build a proof from the historical_summaries instead", slot, generic.SlotsPerHistoricalRoot, state.Slot) } @@ -324,8 +355,7 @@ func (state *BeaconState) BlockRootProof(slot uint64) ([][]byte, error) { // used to anchor the post-state root of a recent slot inside a later // (finalized) state. Used for Gloas withdrawal proofs. func (state *BeaconState) StateRootProof(slot uint64) ([][]byte, error) { - isHistorical := slot+generic.SlotsPerHistoricalRoot <= state.Slot - if isHistorical { + if generic.IsHistoricalProof(state.Slot, slot) { return nil, fmt.Errorf("slot %d is more than %d slots in the past from the state at slot %d, you must build a proof from the historical_summaries instead", slot, generic.SlotsPerHistoricalRoot, state.Slot) } @@ -421,6 +451,56 @@ func (state *BeaconState) ProveExpectedWithdrawal(index uint64) ([][]byte, error return proof.Hashes, nil } +// ProveNextWithdrawalIndex proves BeaconState.next_withdrawal_index against +// this state's root (no block-header extension). +func (state *BeaconState) ProveNextWithdrawalIndex() ([][]byte, error) { + stateTree, err := generic.SSZ.GetTree(state) + if err != nil { + return nil, fmt.Errorf("could not get state tree: %w", err) + } + proof, err := stateTree.Prove(int(GetGeneralizedIndexForNextWithdrawalIndex())) + if err != nil { + return nil, fmt.Errorf("could not get proof for next_withdrawal_index: %w", err) + } + var expected [32]byte + binary.LittleEndian.PutUint64(expected[:8], state.NextWithdrawalIndex) + if !bytes.Equal(proof.Leaf, expected[:]) { + return nil, fmt.Errorf("proof leaf does not match next_withdrawal_index") + } + return proof.Hashes, nil +} + +// ProveValidatorBalanceChunk proves the packed balances leaf containing +// validatorIndex against this state's root (no block-header extension). +func (state *BeaconState) ProveValidatorBalanceChunk(validatorIndex uint64) ([][]byte, [32]byte, error) { + var chunk [32]byte + if validatorIndex >= uint64(len(state.Balances)) { + return nil, chunk, fmt.Errorf("validator index %d out of bounds: state at slot %d has %d balances", validatorIndex, state.Slot, len(state.Balances)) + } + stateTree, err := generic.SSZ.GetTree(state) + if err != nil { + return nil, chunk, fmt.Errorf("could not get state tree: %w", err) + } + proof, err := stateTree.Prove(int(GetGeneralizedIndexForBalanceChunk(validatorIndex))) + if err != nil { + return nil, chunk, fmt.Errorf("could not get proof for validator balance chunk: %w", err) + } + if len(proof.Leaf) != 32 { + return nil, chunk, fmt.Errorf("balance chunk leaf is %d bytes, expected 32", len(proof.Leaf)) + } + copy(chunk[:], proof.Leaf) + return proof.Hashes, chunk, nil +} + +// IsZeroValidatorBalance reports whether the validator's uint64 lane in a +// packed SSZ balances chunk is zero. Matches BeaconStateVerifier: +// shift = (3 - validatorIndex%4) * 64. +func IsZeroValidatorBalance(balanceChunk [32]byte, validatorIndex uint64) bool { + shift := (3 - validatorIndex%4) * 64 + value := new(big.Int).SetBytes(balanceChunk[:]) + return new(big.Int).Rsh(value, uint(shift)).Uint64() == 0 +} + func (state *BeaconState) BlockHeaderProof() ([][]byte, error) { // Construct block header with state root stateRoot, err := generic.SSZ.HashTreeRoot(state) diff --git a/shared/types/eth2/generic/gindex_test.go b/shared/types/eth2/generic/gindex_test.go index 6ef05b26d..928ec08a9 100644 --- a/shared/types/eth2/generic/gindex_test.go +++ b/shared/types/eth2/generic/gindex_test.go @@ -2,6 +2,18 @@ package generic import "testing" +func TestIsHistoricalProof(t *testing.T) { + if IsHistoricalProof(8192, 0) { + t.Fatal("distance 8192 must be direct") + } + if !IsHistoricalProof(8193, 0) { + t.Fatal("distance 8193 must be historical") + } + if IsHistoricalProof(100, 100) { + t.Fatal("equal slots are not historical") + } +} + func TestContainerFieldGindex(t *testing.T) { // Pre-Gloas Deneb-style: 28 fields → width 32; validators at index 11 → 43 if got := ContainerFieldGindex(28, BeaconStateValidatorsIndex); got != 43 { diff --git a/shared/types/eth2/generic/state.go b/shared/types/eth2/generic/state.go index 01b3ea414..2e9cc82a8 100644 --- a/shared/types/eth2/generic/state.go +++ b/shared/types/eth2/generic/state.go @@ -14,6 +14,8 @@ const BeaconStateSlotIndex uint64 = 2 // used by pre-Gloas fixed-capacity List[Validator, N] merkleization. const beaconStateValidatorsMaxLength uint64 = 1 << 40 +const BeaconStateBalancesFieldIndex uint64 = 12 +const BeaconStateNextWithdrawalIndexFieldIndex uint64 = 25 const BeaconStateHistoricalSummariesFieldIndex uint64 = 27 const BeaconStateHistoricalSummariesMaxLength uint64 = 1 << 24 const BeaconStateBlockRootsMaxLength uint64 = 1 << 13 diff --git a/shared/types/eth2/generic/types.go b/shared/types/eth2/generic/types.go index 101993f10..a51a4f6d1 100644 --- a/shared/types/eth2/generic/types.go +++ b/shared/types/eth2/generic/types.go @@ -2,6 +2,10 @@ package generic const SlotsPerHistoricalRoot uint64 = 8192 +func IsHistoricalProof(proofSlot, targetSlot uint64) bool { + return targetSlot+SlotsPerHistoricalRoot < proofSlot +} + // Deposit data (with no signature field) type DepositDataNoSignature struct { PublicKey []byte `json:"pubkey" ssz-size:"48"`