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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions beacon-chain/core/electra/deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,14 +470,15 @@ func ApplyPendingDeposit(ctx context.Context, st state.BeaconState, deposit *eth
// set_or_append_list(state.previous_epoch_participation, index, ParticipationFlags(0b0000_0000))
// set_or_append_list(state.current_epoch_participation, index, ParticipationFlags(0b0000_0000))
// set_or_append_list(state.inactivity_scores, index, uint64(0))
//
// # [New in EIP8148]
// set_or_append_list(
// state.validator_sweep_thresholds,
// index,
// MAX_EFFECTIVE_BALANCE_ELECTRA
// if has_compounding_withdrawal_credential(validator)
// else Gwei(0),
// )
// threshold = Gwei(0)
// if has_compounding_withdrawal_credential(validator):
// threshold = get_credential_sweep_threshold(withdrawal_credentials)
// if threshold < amount:
// threshold = MAX_EFFECTIVE_BALANCE_ELECTRA
//
// set_or_append_list(state.validator_sweep_thresholds, index, threshold)
func AddValidatorToRegistry(beaconState state.BeaconState, pubKey []byte, withdrawalCredentials []byte, amount uint64) error {
val, err := GetValidatorFromDeposit(pubKey, withdrawalCredentials, amount)
if err != nil {
Expand All @@ -503,13 +504,18 @@ func AddValidatorToRegistry(beaconState state.BeaconState, pubKey []byte, withdr
}
}

// [New in EIP-8148] Compounding validators start out at the default 2048 ETH sweep
// threshold; everyone else gets 0, which falls back to get_max_effective_balance.
// [New in EIP-8148] Compounding validators may request a threshold in the withdrawal
// credentials of the deposit creating them, otherwise they start out at the default
// 2048 ETH sweep threshold. Everyone else gets 0, which falls back to
// get_max_effective_balance.
config := params.BeaconConfig()
if beaconState.Version() >= version.Gloas {
threshold := uint64(0)
if len(val.WithdrawalCredentials) > 0 && val.WithdrawalCredentials[0] == config.CompoundingWithdrawalPrefixByte {
threshold = config.MaxEffectiveBalanceElectra
threshold = helpers.CredentialSweepThreshold(val.WithdrawalCredentials)
if threshold < amount {
threshold = config.MaxEffectiveBalanceElectra
}
}

if err := beaconState.AppendValidatorSweepThreshold(threshold); err != nil {
Expand Down
90 changes: 90 additions & 0 deletions beacon-chain/core/electra/deposits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package electra_test

import (
"context"
"encoding/binary"
"testing"

"github.com/OffchainLabs/prysm/v7/beacon-chain/core/electra"
Expand Down Expand Up @@ -589,3 +590,92 @@ func TestApplyPendingDeposit_InvalidSignature(t *testing.T) {
// no topup either
require.Equal(t, 0, len(st.Balances()))
}

func TestAddValidatorToRegistry_SweepThreshold(t *testing.T) {
const gwei = 1_000_000_000

// compoundingCredentialsWithIncrements builds 0x02 credentials carrying the given
// threshold, in EFFECTIVE_BALANCE_INCREMENT units, at SweepThresholdCredentialOffset.
compoundingCredentialsWithIncrements := func(increments uint16) []byte {
creds := make([]byte, 32)
creds[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte
binary.BigEndian.PutUint16(creds[helpers.SweepThresholdCredentialOffset:], increments)
return creds
}

eth1Credentials := func() []byte {
creds := make([]byte, 32)
creds[0] = params.BeaconConfig().ETH1AddressWithdrawalPrefixByte
binary.BigEndian.PutUint16(creds[helpers.SweepThresholdCredentialOffset:], 100)
return creds
}

tests := []struct {
name string
credentials []byte
amount uint64
want uint64
}{
{
name: "Compounding with no embedded threshold gets the default",
credentials: compoundingCredentialsWithIncrements(0),
amount: params.BeaconConfig().MinActivationBalance,
want: params.BeaconConfig().MaxEffectiveBalanceElectra,
},
{
// The eth1 bridge deposit path registers the validator with an amount of 0 and
// credits the balance later, so the fallback to the default does not trigger. A
// stored 0 resolves to the same 2048 ETH default in get_effective_sweep_threshold.
name: "Compounding with no embedded threshold and a zero amount stores zero",
credentials: compoundingCredentialsWithIncrements(0),
amount: 0,
want: 0,
},
{
name: "Compounding with an embedded threshold",
credentials: compoundingCredentialsWithIncrements(100),
amount: params.BeaconConfig().MinActivationBalance,
want: 100 * gwei,
},
{
name: "Embedded threshold out of range falls back to the default",
credentials: compoundingCredentialsWithIncrements(2049),
amount: params.BeaconConfig().MinActivationBalance,
want: params.BeaconConfig().MaxEffectiveBalanceElectra,
},
{
name: "Embedded threshold at least the deposited amount is kept",
credentials: compoundingCredentialsWithIncrements(100),
amount: 100 * gwei,
want: 100 * gwei,
},
{
name: "Embedded threshold below the deposited amount falls back to the default",
credentials: compoundingCredentialsWithIncrements(100),
amount: 100*gwei + 1,
want: params.BeaconConfig().MaxEffectiveBalanceElectra,
},
{
name: "Non-compounding credentials ignore the embedded threshold",
credentials: eth1Credentials(),
amount: params.BeaconConfig().MinActivationBalance,
want: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
st, err := util.NewBeaconStateGloas()
require.NoError(t, err)

sk, err := bls.RandKey()
require.NoError(t, err)

require.NoError(t, electra.AddValidatorToRegistry(st, sk.PublicKey().Marshal(), tt.credentials, tt.amount))

threshold, err := st.ValidatorSweepThreshold(0)
require.NoError(t, err)
require.Equal(t, tt.want, threshold)
})
}
}
28 changes: 28 additions & 0 deletions beacon-chain/core/helpers/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto"
)

// SweepThresholdCredentialOffset is the byte offset, inside `0x02` compounding withdrawal
// credentials, of the sweep threshold a validator asks for at deposit time (EIP-8148). The
// two bytes there hold the threshold in EFFECTIVE_BALANCE_INCREMENT units, big-endian:
//
// byte 0 COMPOUNDING_WITHDRAWAL_PREFIX (0x02)
// bytes 1..9 reserved, must be zero
// bytes 10..11 threshold in EFFECTIVE_BALANCE_INCREMENT units, big-endian
// bytes 12..31 execution address
const SweepThresholdCredentialOffset = 10

var CommitteeCacheInProgressHit = promauto.NewCounter(prometheus.CounterOpts{
Name: "committee_cache_in_progress_hit",
Help: "The number of committee requests that are present in the cache.",
Expand Down Expand Up @@ -509,6 +519,24 @@ func EffectiveSweepThreshold(val state.ReadOnlyValidator, sweepThreshold uint64)
return ValidatorMaxEffectiveBalance(val)
}

// CredentialSweepThreshold gets the sweep threshold embedded in compounding withdrawalCredentials, or 0 if it is unset or out of range.
// https://github.com/nalepae/consensus-specs/blob/master/specs/_features/eip8148/beacon-chain.md#new-get_credential_sweep_threshold
func CredentialSweepThreshold(withdrawalCredentials []byte) uint64 {
if len(withdrawalCredentials) < SweepThresholdCredentialOffset+2 {
return 0
}

increments := binary.BigEndian.Uint16(withdrawalCredentials[SweepThresholdCredentialOffset : SweepThresholdCredentialOffset+2])

cfg := params.BeaconConfig()
threshold := uint64(increments) * cfg.EffectiveBalanceIncrement
if threshold < cfg.MinSweepThreshold() || threshold > cfg.MaxEffectiveBalanceElectra {
return 0
}

return threshold
}

// isPartiallyWithdrawableValidatorElectra implements is_partially_withdrawable_validator in the
// electra fork.
//
Expand Down
73 changes: 73 additions & 0 deletions beacon-chain/core/helpers/validators_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package helpers_test

import (
"bytes"
"encoding/binary"
"errors"
"testing"

Expand Down Expand Up @@ -1057,6 +1059,77 @@ func TestIsPartiallyWithdrawableValidator(t *testing.T) {
}
}

func TestCredentialSweepThreshold(t *testing.T) {
const gwei = 1_000_000_000

// credentialsWithIncrements builds 0x02 credentials carrying the given threshold, in
// EFFECTIVE_BALANCE_INCREMENT units, at SweepThresholdCredentialOffset.
credentialsWithIncrements := func(increments uint16) []byte {
creds := make([]byte, 32)
creds[0] = params.BeaconConfig().CompoundingWithdrawalPrefixByte
binary.BigEndian.PutUint16(creds[helpers.SweepThresholdCredentialOffset:], increments)
return creds
}

tests := []struct {
name string
credentials []byte
want uint64
}{
{
name: "Unset",
credentials: credentialsWithIncrements(0),
want: 0,
},
{
name: "MIN_SWEEP_THRESHOLD",
credentials: credentialsWithIncrements(33),
want: 33 * gwei,
},
{
name: "Just below MIN_SWEEP_THRESHOLD",
credentials: credentialsWithIncrements(32),
want: 0,
},
{
name: "Big-endian byte order",
credentials: credentialsWithIncrements(256),
want: 256 * gwei,
},
{
name: "MAX_EFFECTIVE_BALANCE_ELECTRA",
credentials: credentialsWithIncrements(2048),
want: 2048 * gwei,
},
{
name: "Above MAX_EFFECTIVE_BALANCE_ELECTRA",
credentials: credentialsWithIncrements(2049),
want: 0,
},
{
name: "Largest encodable value",
credentials: credentialsWithIncrements(65535),
want: 0,
},
{
name: "Reserved bytes are not read",
credentials: append(append([]byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte}, bytes.Repeat([]byte{0xFF}, 9)...), make([]byte, 22)...),
want: 0,
},
{
name: "Truncated credentials",
credentials: []byte{params.BeaconConfig().CompoundingWithdrawalPrefixByte, 0xCC},
want: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, helpers.CredentialSweepThreshold(tt.credentials))
})
}
}

func TestValidatorMaxEffectiveBalance(t *testing.T) {
tests := []struct {
name string
Expand Down
3 changes: 2 additions & 1 deletion changelog/manu_eip8148-custom-sweep-threshold.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
### Added

- Implement EIP-8148 (custom sweep threshold for validators) as part of the Gloas fork rather than Heze.
- Implement EIP-8148 (custom sweep threshold for validators) as part of the Gloas fork rather than Heze.
- EIP-8148: Allow a `0x02` validator to be created with a custom sweep threshold, encoded in bytes 10-11 of the withdrawal credentials of the deposit creating it.
Loading