From 3cf385fe91aea87436957e81f48b85de34d98c5a Mon Sep 17 00:00:00 2001 From: Potuz Date: Fri, 27 May 2022 13:38:00 -0300 Subject: [PATCH] Unrealized justification (#10659) * unrealized justification API * Add time elapse logging * add unrealized justification checkpoint * Use UnrealizedJustificationCheckpoint * Refactor unrealized checkpoints * Move logic to state package * do not use ctx on a sum * fix ctx * add tests * fix conflicts * unhandled error * Fix ordering in computing checkpoints * gaz * keep finalized checkpoint if nothing justified * gaz * copy checkpoint * fix check for nil * Add state package tests * Add tests * Radek's review * add more tests * Update beacon-chain/core/epoch/precompute/justification_finalization.go Co-authored-by: terencechain * deduplicate to stateutil * missing file * Add stateutil test * Minor refactor, don't export certain things * Fix exports in tests * remove unused error Co-authored-by: terence tsao Co-authored-by: prylabs-bulldozer[bot] <58059840+prylabs-bulldozer[bot]@users.noreply.github.com> --- beacon-chain/core/altair/epoch_precompute.go | 2 +- .../core/epoch/precompute/BUILD.bazel | 3 + .../precompute/justification_finalization.go | 145 +++++++++++------- .../justification_finalization_test.go | 127 +++++++++++++++ beacon-chain/state/interfaces.go | 1 + beacon-chain/state/state-native/BUILD.bazel | 3 + beacon-chain/state/state-native/error.go | 5 + .../state-native/getters_participation.go | 24 +++ .../getters_participation_test.go | 64 ++++++++ beacon-chain/state/stateutil/BUILD.bazel | 2 + .../stateutil/unrealized_justification.go | 43 ++++++ .../unrealized_justification_test.go | 95 ++++++++++++ beacon-chain/state/v1/getters_test.go | 2 + beacon-chain/state/v1/unsupported_getters.go | 5 + beacon-chain/state/v2/BUILD.bazel | 3 + beacon-chain/state/v2/error.go | 5 + .../state/v2/getters_participation.go | 25 +++ .../state/v2/getters_participation_test.go | 64 ++++++++ beacon-chain/state/v2/getters_test.go | 2 + beacon-chain/state/v3/BUILD.bazel | 3 + beacon-chain/state/v3/error.go | 5 + .../state/v3/getters_participation.go | 25 +++ .../state/v3/getters_participation_test.go | 64 ++++++++ beacon-chain/state/v3/getters_test.go | 2 + 24 files changed, 661 insertions(+), 58 deletions(-) create mode 100644 beacon-chain/state/state-native/error.go create mode 100644 beacon-chain/state/state-native/getters_participation_test.go create mode 100644 beacon-chain/state/stateutil/unrealized_justification.go create mode 100644 beacon-chain/state/stateutil/unrealized_justification_test.go create mode 100644 beacon-chain/state/v2/error.go create mode 100644 beacon-chain/state/v2/getters_participation_test.go create mode 100644 beacon-chain/state/v3/error.go create mode 100644 beacon-chain/state/v3/getters_participation_test.go diff --git a/beacon-chain/core/altair/epoch_precompute.go b/beacon-chain/core/altair/epoch_precompute.go index 7e8bcfd0f..b137dedf7 100644 --- a/beacon-chain/core/altair/epoch_precompute.go +++ b/beacon-chain/core/altair/epoch_precompute.go @@ -47,7 +47,7 @@ func InitializePrecomputeValidators(ctx context.Context, beaconState state.Beaco return err } } - // Set validator's active status for preivous epoch. + // Set validator's active status for previous epoch. if helpers.IsActiveValidatorUsingTrie(val, prevEpoch) { v.IsActivePrevEpoch = true bal.ActivePrevEpoch, err = math.Add64(bal.ActivePrevEpoch, val.EffectiveBalance()) diff --git a/beacon-chain/core/epoch/precompute/BUILD.bazel b/beacon-chain/core/epoch/precompute/BUILD.bazel index e9e44a112..11c59d82c 100644 --- a/beacon-chain/core/epoch/precompute/BUILD.bazel +++ b/beacon-chain/core/epoch/precompute/BUILD.bazel @@ -28,6 +28,7 @@ go_library( "//runtime/version:go_default_library", "//time/slots:go_default_library", "@com_github_pkg_errors//:go_default_library", + "@com_github_prysmaticlabs_go_bitfield//:go_default_library", "@io_opencensus_go//trace:go_default_library", ], ) @@ -43,11 +44,13 @@ go_test( ], embed = [":go_default_library"], deps = [ + "//beacon-chain/core/altair:go_default_library", "//beacon-chain/core/epoch:go_default_library", "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/core/time:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/v1:go_default_library", + "//beacon-chain/state/v2:go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/primitives:go_default_library", diff --git a/beacon-chain/core/epoch/precompute/justification_finalization.go b/beacon-chain/core/epoch/precompute/justification_finalization.go index 0f19cdb67..0f01a027f 100644 --- a/beacon-chain/core/epoch/precompute/justification_finalization.go +++ b/beacon-chain/core/epoch/precompute/justification_finalization.go @@ -2,6 +2,7 @@ package precompute import ( "github.com/pkg/errors" + "github.com/prysmaticlabs/go-bitfield" "github.com/prysmaticlabs/prysm/beacon-chain/core/helpers" "github.com/prysmaticlabs/prysm/beacon-chain/core/time" "github.com/prysmaticlabs/prysm/beacon-chain/state" @@ -9,6 +10,24 @@ import ( "github.com/prysmaticlabs/prysm/time/slots" ) +var errNilState = errors.New("nil state") + +// UnrealizedCheckpoints returns the justification and finalization checkpoints of the +// given state as if it was progressed with empty slots until the next epoch. +func UnrealizedCheckpoints(st state.BeaconState) (*ethpb.Checkpoint, *ethpb.Checkpoint, error) { + if st == nil || st.IsNil() { + return nil, nil, errNilState + } + + activeBalance, prevTarget, currentTarget, err := st.UnrealizedCheckpointBalances() + if err != nil { + return nil, nil, err + } + + justification := processJustificationBits(st, activeBalance, prevTarget, currentTarget) + return computeCheckpoints(st, justification) +} + // ProcessJustificationAndFinalizationPreCompute processes justification and finalization during // epoch processing. This is where a beacon node can justify and finalize a new epoch. // Note: this is an optimized version by passing in precomputed total and attesting balances. @@ -34,12 +53,55 @@ func ProcessJustificationAndFinalizationPreCompute(state state.BeaconState, pBal return state, nil } - return weighJustificationAndFinalization(state, pBal.ActiveCurrentEpoch, pBal.PrevEpochTargetAttested, pBal.CurrentEpochTargetAttested) + newBits := processJustificationBits(state, pBal.ActiveCurrentEpoch, pBal.PrevEpochTargetAttested, pBal.CurrentEpochTargetAttested) + + return weighJustificationAndFinalization(state, newBits) } -// weighJustificationAndFinalization processes justification and finalization during +// processJustificationBits processes the justification bits during epoch processing. +func processJustificationBits(state state.BeaconState, totalActiveBalance, prevEpochTargetBalance, currEpochTargetBalance uint64) bitfield.Bitvector4 { + newBits := state.JustificationBits() + newBits.Shift(1) + // If 2/3 or more of total balance attested in the previous epoch. + if 3*prevEpochTargetBalance >= 2*totalActiveBalance { + newBits.SetBitAt(1, true) + } + + if 3*currEpochTargetBalance >= 2*totalActiveBalance { + newBits.SetBitAt(0, true) + } + + return newBits +} + +// updateJustificationAndFinalization processes justification and finalization during // epoch processing. This is where a beacon node can justify and finalize a new epoch. -// +func weighJustificationAndFinalization(state state.BeaconState, newBits bitfield.Bitvector4) (state.BeaconState, error) { + jc, fc, err := computeCheckpoints(state, newBits) + if err != nil { + return nil, err + } + + if err := state.SetPreviousJustifiedCheckpoint(state.CurrentJustifiedCheckpoint()); err != nil { + return nil, err + } + + if err := state.SetCurrentJustifiedCheckpoint(jc); err != nil { + return nil, err + } + + if err := state.SetJustificationBits(newBits); err != nil { + return nil, err + } + + if err := state.SetFinalizedCheckpoint(fc); err != nil { + return nil, err + } + return state, nil +} + +// computeCheckpoints computes the new Justification and Finalization +// checkpoints at epoch transition // Spec pseudocode definition: // def weigh_justification_and_finalization(state: BeaconState, // total_active_balance: Gwei, @@ -77,88 +139,57 @@ func ProcessJustificationAndFinalizationPreCompute(state state.BeaconState, pBal // # The 1st/2nd most recent epochs are justified, the 1st using the 2nd as source // if all(bits[0:2]) and old_current_justified_checkpoint.epoch + 1 == current_epoch: // state.finalized_checkpoint = old_current_justified_checkpoint -func weighJustificationAndFinalization(state state.BeaconState, - totalActiveBalance, prevEpochTargetBalance, currEpochTargetBalance uint64) (state.BeaconState, error) { +func computeCheckpoints(state state.BeaconState, newBits bitfield.Bitvector4) (*ethpb.Checkpoint, *ethpb.Checkpoint, error) { prevEpoch := time.PrevEpoch(state) currentEpoch := time.CurrentEpoch(state) oldPrevJustifiedCheckpoint := state.PreviousJustifiedCheckpoint() oldCurrJustifiedCheckpoint := state.CurrentJustifiedCheckpoint() - // Process justifications - if err := state.SetPreviousJustifiedCheckpoint(state.CurrentJustifiedCheckpoint()); err != nil { - return nil, err - } - newBits := state.JustificationBits() - newBits.Shift(1) - if err := state.SetJustificationBits(newBits); err != nil { - return nil, err - } - - // Note: the spec refers to the bit index position starting at 1 instead of starting at zero. - // We will use that paradigm here for consistency with the godoc spec definition. - - // If 2/3 or more of total balance attested in the previous epoch. - if 3*prevEpochTargetBalance >= 2*totalActiveBalance { - blockRoot, err := helpers.BlockRoot(state, prevEpoch) - if err != nil { - return nil, errors.Wrapf(err, "could not get block root for previous epoch %d", prevEpoch) - } - if err := state.SetCurrentJustifiedCheckpoint(ðpb.Checkpoint{Epoch: prevEpoch, Root: blockRoot}); err != nil { - return nil, err - } - newBits = state.JustificationBits() - newBits.SetBitAt(1, true) - if err := state.SetJustificationBits(newBits); err != nil { - return nil, err - } - } + justifiedCheckpoint := state.CurrentJustifiedCheckpoint() + finalizedCheckpoint := state.FinalizedCheckpoint() // If 2/3 or more of the total balance attested in the current epoch. - if 3*currEpochTargetBalance >= 2*totalActiveBalance { + if newBits.BitAt(0) { blockRoot, err := helpers.BlockRoot(state, currentEpoch) if err != nil { - return nil, errors.Wrapf(err, "could not get block root for current epoch %d", prevEpoch) + return nil, nil, errors.Wrapf(err, "could not get block root for current epoch %d", currentEpoch) } - if err := state.SetCurrentJustifiedCheckpoint(ðpb.Checkpoint{Epoch: currentEpoch, Root: blockRoot}); err != nil { - return nil, err - } - newBits = state.JustificationBits() - newBits.SetBitAt(0, true) - if err := state.SetJustificationBits(newBits); err != nil { - return nil, err + justifiedCheckpoint.Epoch = currentEpoch + justifiedCheckpoint.Root = blockRoot + } else if newBits.BitAt(1) { + // If 2/3 or more of total balance attested in the previous epoch. + blockRoot, err := helpers.BlockRoot(state, prevEpoch) + if err != nil { + return nil, nil, errors.Wrapf(err, "could not get block root for previous epoch %d", prevEpoch) } + justifiedCheckpoint.Epoch = prevEpoch + justifiedCheckpoint.Root = blockRoot } // Process finalization according to Ethereum Beacon Chain specification. - justification := state.JustificationBits().Bytes()[0] + if len(newBits) == 0 { + return nil, nil, errors.New("empty justification bits") + } + justification := newBits.Bytes()[0] // 2nd/3rd/4th (0b1110) most recent epochs are justified, the 2nd using the 4th as source. if justification&0x0E == 0x0E && (oldPrevJustifiedCheckpoint.Epoch+3) == currentEpoch { - if err := state.SetFinalizedCheckpoint(oldPrevJustifiedCheckpoint); err != nil { - return nil, err - } + finalizedCheckpoint = oldPrevJustifiedCheckpoint } // 2nd/3rd (0b0110) most recent epochs are justified, the 2nd using the 3rd as source. if justification&0x06 == 0x06 && (oldPrevJustifiedCheckpoint.Epoch+2) == currentEpoch { - if err := state.SetFinalizedCheckpoint(oldPrevJustifiedCheckpoint); err != nil { - return nil, err - } + finalizedCheckpoint = oldPrevJustifiedCheckpoint } // 1st/2nd/3rd (0b0111) most recent epochs are justified, the 1st using the 3rd as source. if justification&0x07 == 0x07 && (oldCurrJustifiedCheckpoint.Epoch+2) == currentEpoch { - if err := state.SetFinalizedCheckpoint(oldCurrJustifiedCheckpoint); err != nil { - return nil, err - } + finalizedCheckpoint = oldCurrJustifiedCheckpoint } // The 1st/2nd (0b0011) most recent epochs are justified, the 1st using the 2nd as source if justification&0x03 == 0x03 && (oldCurrJustifiedCheckpoint.Epoch+1) == currentEpoch { - if err := state.SetFinalizedCheckpoint(oldCurrJustifiedCheckpoint); err != nil { - return nil, err - } + finalizedCheckpoint = oldCurrJustifiedCheckpoint } - - return state, nil + return justifiedCheckpoint, finalizedCheckpoint, nil } diff --git a/beacon-chain/core/epoch/precompute/justification_finalization_test.go b/beacon-chain/core/epoch/precompute/justification_finalization_test.go index e2065120e..e8cdc2b1f 100644 --- a/beacon-chain/core/epoch/precompute/justification_finalization_test.go +++ b/beacon-chain/core/epoch/precompute/justification_finalization_test.go @@ -1,11 +1,14 @@ package precompute_test import ( + "context" "testing" "github.com/prysmaticlabs/go-bitfield" + "github.com/prysmaticlabs/prysm/beacon-chain/core/altair" "github.com/prysmaticlabs/prysm/beacon-chain/core/epoch/precompute" v1 "github.com/prysmaticlabs/prysm/beacon-chain/state/v1" + v2 "github.com/prysmaticlabs/prysm/beacon-chain/state/v2" fieldparams "github.com/prysmaticlabs/prysm/config/fieldparams" "github.com/prysmaticlabs/prysm/config/params" types "github.com/prysmaticlabs/prysm/consensus-types/primitives" @@ -123,3 +126,127 @@ func TestProcessJustificationAndFinalizationPreCompute_JustifyPrevEpoch(t *testi assert.DeepEqual(t, params.BeaconConfig().ZeroHash[:], newState.FinalizedCheckpoint().Root) assert.Equal(t, types.Epoch(0), newState.FinalizedCheckpointEpoch(), "Unexpected finalized epoch") } + +func TestUnrealizedCheckpoints(t *testing.T) { + validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount) + balances := make([]uint64, len(validators)) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + } + balances[i] = params.BeaconConfig().MaxEffectiveBalance + } + pjr := [32]byte{'p'} + cjr := [32]byte{'c'} + je := types.Epoch(3) + fe := types.Epoch(2) + pjcp := ðpb.Checkpoint{Root: pjr[:], Epoch: fe} + cjcp := ðpb.Checkpoint{Root: cjr[:], Epoch: je} + fcp := ðpb.Checkpoint{Root: pjr[:], Epoch: fe} + tests := []struct { + name string + slot types.Slot + prevVals, currVals int + expectedJustified, expectedFinalized types.Epoch // The expected unrealized checkpoint epochs + }{ + { + "Not enough votes, keep previous justification", + 129, + len(validators) / 3, + len(validators) / 3, + je, + fe, + }, + { + "Not enough votes, keep previous justification, N+2", + 161, + len(validators) / 3, + len(validators) / 3, + je, + fe, + }, + { + "Enough to justify previous epoch but not current", + 129, + 2*len(validators)/3 + 3, + len(validators) / 3, + je, + fe, + }, + { + "Enough to justify previous epoch but not current, N+2", + 161, + 2*len(validators)/3 + 3, + len(validators) / 3, + je + 1, + fe, + }, + { + "Enough to justify current epoch", + 129, + len(validators) / 3, + 2*len(validators)/3 + 3, + je + 1, + fe, + }, + { + "Enough to justify current epoch, but not previous", + 161, + len(validators) / 3, + 2*len(validators)/3 + 3, + je + 2, + fe, + }, + { + "Enough to justify current and previous", + 161, + 2*len(validators)/3 + 3, + 2*len(validators)/3 + 3, + je + 2, + fe, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + base := ðpb.BeaconStateAltair{ + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + + Validators: validators, + Slot: test.slot, + CurrentEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + PreviousEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + Balances: balances, + PreviousJustifiedCheckpoint: pjcp, + CurrentJustifiedCheckpoint: cjcp, + FinalizedCheckpoint: fcp, + InactivityScores: make([]uint64, len(validators)), + JustificationBits: make(bitfield.Bitvector4, 1), + } + for i := 0; i < test.prevVals; i++ { + base.PreviousEpochParticipation[i] = 0xFF + } + for i := 0; i < test.currVals; i++ { + base.CurrentEpochParticipation[i] = 0xFF + } + if test.slot > 130 { + base.JustificationBits.SetBitAt(2, true) + base.JustificationBits.SetBitAt(3, true) + } else { + base.JustificationBits.SetBitAt(1, true) + base.JustificationBits.SetBitAt(2, true) + } + + state, err := v2.InitializeFromProto(base) + require.NoError(t, err) + + _, _, err = altair.InitializePrecomputeValidators(context.Background(), state) + require.NoError(t, err) + + jc, fc, err := precompute.UnrealizedCheckpoints(state) + require.NoError(t, err) + require.DeepEqual(t, test.expectedJustified, jc.Epoch) + require.DeepEqual(t, test.expectedFinalized, fc.Epoch) + }) + } +} diff --git a/beacon-chain/state/interfaces.go b/beacon-chain/state/interfaces.go index 99ff0f007..26b4f4319 100644 --- a/beacon-chain/state/interfaces.go +++ b/beacon-chain/state/interfaces.go @@ -223,6 +223,7 @@ type FutureForkStub interface { AppendInactivityScore(s uint64) error CurrentEpochParticipation() ([]byte, error) PreviousEpochParticipation() ([]byte, error) + UnrealizedCheckpointBalances() (uint64, uint64, uint64, error) InactivityScores() ([]uint64, error) SetInactivityScores(val []uint64) error CurrentSyncCommittee() (*ethpb.SyncCommittee, error) diff --git a/beacon-chain/state/state-native/BUILD.bazel b/beacon-chain/state/state-native/BUILD.bazel index 8044c9a58..0d25317e8 100644 --- a/beacon-chain/state/state-native/BUILD.bazel +++ b/beacon-chain/state/state-native/BUILD.bazel @@ -4,6 +4,7 @@ go_library( name = "go_default_library", srcs = [ "doc.go", + "error.go", "getters_attestation.go", "getters_block.go", "getters_checkpoint.go", @@ -54,6 +55,7 @@ go_library( "//tools/pcli:__pkg__", ], deps = [ + "//beacon-chain/core/time:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/fieldtrie:go_default_library", "//beacon-chain/state/state-native/custom-types:go_default_library", @@ -83,6 +85,7 @@ go_test( "getters_attestation_test.go", "getters_block_test.go", "getters_checkpoint_test.go", + "getters_participation_test.go", "getters_test.go", "getters_validator_test.go", "hasher_test.go", diff --git a/beacon-chain/state/state-native/error.go b/beacon-chain/state/state-native/error.go new file mode 100644 index 000000000..ac8dde525 --- /dev/null +++ b/beacon-chain/state/state-native/error.go @@ -0,0 +1,5 @@ +package state_native + +import "errors" + +var ErrNilParticipation = errors.New("Nil epoch participation in state") diff --git a/beacon-chain/state/state-native/getters_participation.go b/beacon-chain/state/state-native/getters_participation.go index c81a97028..e58e58a4f 100644 --- a/beacon-chain/state/state-native/getters_participation.go +++ b/beacon-chain/state/state-native/getters_participation.go @@ -1,6 +1,8 @@ package state_native import ( + "github.com/prysmaticlabs/prysm/beacon-chain/core/time" + "github.com/prysmaticlabs/prysm/beacon-chain/state/stateutil" "github.com/prysmaticlabs/prysm/runtime/version" ) @@ -36,6 +38,28 @@ func (b *BeaconState) PreviousEpochParticipation() ([]byte, error) { return b.previousEpochParticipationVal(), nil } +// UnrealizedCheckpointBalances returns the total balances: active, target attested in +// current epoch and target attested in previous epoch. This function is used to +// compute the "unrealized justification" that a synced Beacon Block will have. +func (b *BeaconState) UnrealizedCheckpointBalances() (uint64, uint64, uint64, error) { + if b.version == version.Phase0 { + return 0, 0, 0, errNotSupported("UnrealizedCheckpointBalances", b.version) + } + + b.lock.RLock() + defer b.lock.RUnlock() + + cp := b.currentEpochParticipation + pp := b.previousEpochParticipation + if cp == nil || pp == nil { + return 0, 0, 0, ErrNilParticipation + } + + currentEpoch := time.CurrentEpoch(b) + return stateutil.UnrealizedCheckpointBalances(cp, pp, b.validators, currentEpoch) + +} + // currentEpochParticipationVal corresponding to participation bits on the beacon chain. // This assumes that a lock is already held on BeaconState. func (b *BeaconState) currentEpochParticipationVal() []byte { diff --git a/beacon-chain/state/state-native/getters_participation_test.go b/beacon-chain/state/state-native/getters_participation_test.go new file mode 100644 index 000000000..30fbdd647 --- /dev/null +++ b/beacon-chain/state/state-native/getters_participation_test.go @@ -0,0 +1,64 @@ +package state_native + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/config/params" + ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/testing/require" +) + +func TestState_UnrealizedCheckpointBalances(t *testing.T) { + validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount) + balances := make([]uint64, params.BeaconConfig().MinGenesisActiveValidatorCount) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + } + balances[i] = params.BeaconConfig().MaxEffectiveBalance + } + base := ðpb.BeaconStateAltair{ + Slot: 2, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + + Validators: validators, + CurrentEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + PreviousEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + Balances: balances, + } + state, err := InitializeFromProtoAltair(base) + require.NoError(t, err) + + // No one voted in the last two epochs + allActive := params.BeaconConfig().MinGenesisActiveValidatorCount * params.BeaconConfig().MaxEffectiveBalance + active, previous, current, err := state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive, active) + require.Equal(t, uint64(0), current) + require.Equal(t, uint64(0), previous) + + // Add some votes in the last two epochs: + base.CurrentEpochParticipation[0] = 0xFF + base.PreviousEpochParticipation[0] = 0xFF + base.PreviousEpochParticipation[1] = 0xFF + + state, err = InitializeFromProtoAltair(base) + require.NoError(t, err) + active, previous, current, err = state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive, active) + require.Equal(t, params.BeaconConfig().MaxEffectiveBalance, current) + require.Equal(t, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + + // Slash some validators + validators[0].Slashed = true + state, err = InitializeFromProtoAltair(base) + require.NoError(t, err) + active, previous, current, err = state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive-params.BeaconConfig().MaxEffectiveBalance, active) + require.Equal(t, uint64(0), current) + require.Equal(t, params.BeaconConfig().MaxEffectiveBalance, previous) + +} diff --git a/beacon-chain/state/stateutil/BUILD.bazel b/beacon-chain/state/stateutil/BUILD.bazel index 2b61856f0..a2952c6be 100644 --- a/beacon-chain/state/stateutil/BUILD.bazel +++ b/beacon-chain/state/stateutil/BUILD.bazel @@ -15,6 +15,7 @@ go_library( "state_hasher.go", "sync_committee.root.go", "trie_helpers.go", + "unrealized_justification.go", "validator_map_handler.go", "validator_root.go", ], @@ -57,6 +58,7 @@ go_test( "reference_bench_test.go", "state_root_test.go", "trie_helpers_test.go", + "unrealized_justification_test.go", "validator_root_test.go", ], embed = [":go_default_library"], diff --git a/beacon-chain/state/stateutil/unrealized_justification.go b/beacon-chain/state/stateutil/unrealized_justification.go new file mode 100644 index 000000000..78dd0e6a5 --- /dev/null +++ b/beacon-chain/state/stateutil/unrealized_justification.go @@ -0,0 +1,43 @@ +package stateutil + +import ( + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/config/params" + types "github.com/prysmaticlabs/prysm/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/math" + ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" +) + +func UnrealizedCheckpointBalances(cp, pp []byte, validators []*ethpb.Validator, currentEpoch types.Epoch) (uint64, uint64, uint64, error) { + targetIdx := params.BeaconConfig().TimelyTargetFlagIndex + activeBalance := uint64(0) + currentTarget := uint64(0) + prevTarget := uint64(0) + if len(cp) < len(validators) || len(pp) < len(validators) { + return 0, 0, 0, errors.New("participation does not match validator set") + } + + var err error + for i, v := range validators { + active := v.ActivationEpoch <= currentEpoch && currentEpoch < v.ExitEpoch + if active && !v.Slashed { + activeBalance, err = math.Add64(activeBalance, v.EffectiveBalance) + if err != nil { + return 0, 0, 0, err + } + if ((cp[i] >> targetIdx) & 1) == 1 { + currentTarget, err = math.Add64(currentTarget, v.EffectiveBalance) + if err != nil { + return 0, 0, 0, err + } + } + if ((pp[i] >> targetIdx) & 1) == 1 { + prevTarget, err = math.Add64(prevTarget, v.EffectiveBalance) + if err != nil { + return 0, 0, 0, err + } + } + } + } + return activeBalance, prevTarget, currentTarget, nil +} diff --git a/beacon-chain/state/stateutil/unrealized_justification_test.go b/beacon-chain/state/stateutil/unrealized_justification_test.go new file mode 100644 index 000000000..de4f5341b --- /dev/null +++ b/beacon-chain/state/stateutil/unrealized_justification_test.go @@ -0,0 +1,95 @@ +package stateutil + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/config/params" + ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/testing/require" +) + +func TestState_UnrealizedCheckpointBalances(t *testing.T) { + validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount) + targetFlag := params.BeaconConfig().TimelyTargetFlagIndex + expectedActive := params.BeaconConfig().MinGenesisActiveValidatorCount * params.BeaconConfig().MaxEffectiveBalance + + balances := make([]uint64, params.BeaconConfig().MinGenesisActiveValidatorCount) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + } + balances[i] = params.BeaconConfig().MaxEffectiveBalance + } + cp := make([]byte, len(validators)) + pp := make([]byte, len(validators)) + + t.Run("No one voted last two epochs", func(tt *testing.T) { + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 0) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, uint64(0), current) + require.Equal(tt, uint64(0), previous) + }) + + t.Run("bad votes in last two epochs", func(tt *testing.T) { + copy(cp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00}) + copy(pp, []byte{0x00, 0x00, 0x00, 0x00}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, uint64(0), current) + require.Equal(tt, uint64(0), previous) + }) + + t.Run("two votes in last epoch", func(tt *testing.T) { + copy(cp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 1 << targetFlag, 1 << targetFlag}) + copy(pp, []byte{0x00, 0x00, 0x00, 0x00, 0xFF ^ (1 << targetFlag)}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, current) + require.Equal(tt, uint64(0), previous) + }) + + t.Run("two votes in previous epoch", func(tt *testing.T) { + copy(cp, []byte{0x00, 0x00, 0x00, 0x00, 0xFF ^ (1 << targetFlag), 0x00}) + copy(pp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 1 << targetFlag, 1 << targetFlag}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + require.NoError(tt, err) + require.Equal(tt, expectedActive, active) + require.Equal(tt, uint64(0), current) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + }) + + t.Run("votes in both epochs, decreased balance in first validator", func(tt *testing.T) { + validators[0].EffectiveBalance = params.BeaconConfig().MaxEffectiveBalance - params.BeaconConfig().MinDepositAmount + copy(cp, []byte{0xFF, 0xFF, 0x00, 0x00, 0xFF ^ (1 << targetFlag), 0}) + copy(pp, []byte{0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0xFF ^ (1 << targetFlag), 0x00, 0xFF, 0xFF}) + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + require.NoError(tt, err) + expectedActive -= params.BeaconConfig().MinDepositAmount + require.Equal(tt, expectedActive, active) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance-params.BeaconConfig().MinDepositAmount, current) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + }) + + t.Run("slash a validator", func(tt *testing.T) { + validators[1].Slashed = true + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 1) + require.NoError(tt, err) + expectedActive -= params.BeaconConfig().MaxEffectiveBalance + require.Equal(tt, expectedActive, active) + require.Equal(tt, params.BeaconConfig().MaxEffectiveBalance-params.BeaconConfig().MinDepositAmount, current) + require.Equal(tt, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + }) + t.Run("Exit a validator", func(tt *testing.T) { + validators[4].ExitEpoch = 1 + active, previous, current, err := UnrealizedCheckpointBalances(cp, pp, validators, 2) + require.NoError(tt, err) + expectedActive -= params.BeaconConfig().MaxEffectiveBalance + require.Equal(tt, expectedActive, active) + require.Equal(tt, params.BeaconConfig().MaxEffectiveBalance-params.BeaconConfig().MinDepositAmount, current) + require.Equal(tt, params.BeaconConfig().MaxEffectiveBalance, previous) + }) +} diff --git a/beacon-chain/state/v1/getters_test.go b/beacon-chain/state/v1/getters_test.go index 474dbc503..c2bd2138c 100644 --- a/beacon-chain/state/v1/getters_test.go +++ b/beacon-chain/state/v1/getters_test.go @@ -63,6 +63,8 @@ func TestNilState_NoPanic(t *testing.T) { _ = st.PreviousJustifiedCheckpoint() _ = st.CurrentJustifiedCheckpoint() _ = st.FinalizedCheckpoint() + _, _, _, err = st.UnrealizedCheckpointBalances() + _ = err } func TestBeaconState_MatchCurrentJustifiedCheckpt(t *testing.T) { diff --git a/beacon-chain/state/v1/unsupported_getters.go b/beacon-chain/state/v1/unsupported_getters.go index 3717aab45..010807f4c 100644 --- a/beacon-chain/state/v1/unsupported_getters.go +++ b/beacon-chain/state/v1/unsupported_getters.go @@ -15,6 +15,11 @@ func (*BeaconState) PreviousEpochParticipation() ([]byte, error) { return nil, errors.New("PreviousEpochParticipation is not supported for phase 0 beacon state") } +// UnrealizedCheckpointBalances is not supported for phase 0 beacon state. +func (*BeaconState) UnrealizedCheckpointBalances() (uint64, uint64, uint64, error) { + return 0, 0, 0, errors.New("UnrealizedCheckpointBalances is not supported for phase0 beacon state") +} + // InactivityScores is not supported for phase 0 beacon state. func (*BeaconState) InactivityScores() ([]uint64, error) { return nil, errors.New("InactivityScores is not supported for phase 0 beacon state") diff --git a/beacon-chain/state/v2/BUILD.bazel b/beacon-chain/state/v2/BUILD.bazel index 8bf49c9f9..d92b4cce9 100644 --- a/beacon-chain/state/v2/BUILD.bazel +++ b/beacon-chain/state/v2/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "deprecated_getters.go", "deprecated_setters.go", + "error.go", "field_roots.go", "getters_block.go", "getters_checkpoint.go", @@ -32,6 +33,7 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/beacon-chain/state/v2", visibility = ["//visibility:public"], deps = [ + "//beacon-chain/core/time:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/fieldtrie:go_default_library", "//beacon-chain/state/state-native:go_default_library", @@ -62,6 +64,7 @@ go_test( "deprecated_setters_test.go", "getters_block_test.go", "getters_checkpoint_test.go", + "getters_participation_test.go", "getters_test.go", "getters_validator_test.go", "proofs_test.go", diff --git a/beacon-chain/state/v2/error.go b/beacon-chain/state/v2/error.go new file mode 100644 index 000000000..7ab7be4dc --- /dev/null +++ b/beacon-chain/state/v2/error.go @@ -0,0 +1,5 @@ +package v2 + +import "errors" + +var ErrNilParticipation = errors.New("Nil epoch participation in state") diff --git a/beacon-chain/state/v2/getters_participation.go b/beacon-chain/state/v2/getters_participation.go index 6c90e12cb..d5de0b4b1 100644 --- a/beacon-chain/state/v2/getters_participation.go +++ b/beacon-chain/state/v2/getters_participation.go @@ -1,5 +1,10 @@ package v2 +import ( + "github.com/prysmaticlabs/prysm/beacon-chain/core/time" + "github.com/prysmaticlabs/prysm/beacon-chain/state/stateutil" +) + // CurrentEpochParticipation corresponding to participation bits on the beacon chain. func (b *BeaconState) CurrentEpochParticipation() ([]byte, error) { if !b.hasInnerState() { @@ -30,6 +35,26 @@ func (b *BeaconState) PreviousEpochParticipation() ([]byte, error) { return b.previousEpochParticipation(), nil } +// UnrealizedCheckpointBalances returns the total balances: active, target attested in +// current epoch and target attested in previous epoch. This function is used to +// compute the "unrealized justification" that a synced Beacon Block will have. +func (b *BeaconState) UnrealizedCheckpointBalances() (uint64, uint64, uint64, error) { + if !b.hasInnerState() { + return 0, 0, 0, ErrNilInnerState + } + b.lock.RLock() + defer b.lock.RUnlock() + + cp := b.state.CurrentEpochParticipation + pp := b.state.PreviousEpochParticipation + if cp == nil || pp == nil { + return 0, 0, 0, ErrNilParticipation + } + currentEpoch := time.CurrentEpoch(b) + return stateutil.UnrealizedCheckpointBalances(cp, pp, b.state.Validators, currentEpoch) + +} + // currentEpochParticipation corresponding to participation bits on the beacon chain. // This assumes that a lock is already held on BeaconState. func (b *BeaconState) currentEpochParticipation() []byte { diff --git a/beacon-chain/state/v2/getters_participation_test.go b/beacon-chain/state/v2/getters_participation_test.go new file mode 100644 index 000000000..bbe574e52 --- /dev/null +++ b/beacon-chain/state/v2/getters_participation_test.go @@ -0,0 +1,64 @@ +package v2 + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/config/params" + ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/testing/require" +) + +func TestState_UnrealizedCheckpointBalances(t *testing.T) { + validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount) + balances := make([]uint64, params.BeaconConfig().MinGenesisActiveValidatorCount) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + } + balances[i] = params.BeaconConfig().MaxEffectiveBalance + } + base := ðpb.BeaconStateAltair{ + Slot: 2, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + + Validators: validators, + CurrentEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + PreviousEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + Balances: balances, + } + state, err := InitializeFromProto(base) + require.NoError(t, err) + + // No one voted in the last two epochs + allActive := params.BeaconConfig().MinGenesisActiveValidatorCount * params.BeaconConfig().MaxEffectiveBalance + active, previous, current, err := state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive, active) + require.Equal(t, uint64(0), current) + require.Equal(t, uint64(0), previous) + + // Add some votes in the last two epochs: + base.CurrentEpochParticipation[0] = 0xFF + base.PreviousEpochParticipation[0] = 0xFF + base.PreviousEpochParticipation[1] = 0xFF + + state, err = InitializeFromProto(base) + require.NoError(t, err) + active, previous, current, err = state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive, active) + require.Equal(t, params.BeaconConfig().MaxEffectiveBalance, current) + require.Equal(t, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + + // Slash some validators + validators[0].Slashed = true + state, err = InitializeFromProto(base) + require.NoError(t, err) + active, previous, current, err = state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive-params.BeaconConfig().MaxEffectiveBalance, active) + require.Equal(t, uint64(0), current) + require.Equal(t, params.BeaconConfig().MaxEffectiveBalance, previous) + +} diff --git a/beacon-chain/state/v2/getters_test.go b/beacon-chain/state/v2/getters_test.go index 8ccff4e62..d11641527 100644 --- a/beacon-chain/state/v2/getters_test.go +++ b/beacon-chain/state/v2/getters_test.go @@ -74,6 +74,8 @@ func TestNilState_NoPanic(t *testing.T) { require.ErrorIs(t, ErrNilInnerState, err) _, err = st.NextSyncCommittee() require.ErrorIs(t, ErrNilInnerState, err) + _, _, _, err = st.UnrealizedCheckpointBalances() + require.ErrorIs(t, ErrNilInnerState, err) } func TestBeaconState_MatchCurrentJustifiedCheckpt(t *testing.T) { diff --git a/beacon-chain/state/v3/BUILD.bazel b/beacon-chain/state/v3/BUILD.bazel index ac9cd76d5..973cc0a9c 100644 --- a/beacon-chain/state/v3/BUILD.bazel +++ b/beacon-chain/state/v3/BUILD.bazel @@ -5,6 +5,7 @@ go_library( srcs = [ "deprecated_getters.go", "deprecated_setters.go", + "error.go", "field_roots.go", "getters_block.go", "getters_checkpoint.go", @@ -34,6 +35,7 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/beacon-chain/state/v3", visibility = ["//visibility:public"], deps = [ + "//beacon-chain/core/time:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/fieldtrie:go_default_library", "//beacon-chain/state/state-native:go_default_library", @@ -64,6 +66,7 @@ go_test( "deprecated_setters_test.go", "getters_block_test.go", "getters_checkpoint_test.go", + "getters_participation_test.go", "getters_test.go", "getters_validator_test.go", "proofs_test.go", diff --git a/beacon-chain/state/v3/error.go b/beacon-chain/state/v3/error.go new file mode 100644 index 000000000..f7773f8a6 --- /dev/null +++ b/beacon-chain/state/v3/error.go @@ -0,0 +1,5 @@ +package v3 + +import "errors" + +var ErrNilParticipation = errors.New("Nil epoch participation in state") diff --git a/beacon-chain/state/v3/getters_participation.go b/beacon-chain/state/v3/getters_participation.go index 48acdbde5..c1f62383b 100644 --- a/beacon-chain/state/v3/getters_participation.go +++ b/beacon-chain/state/v3/getters_participation.go @@ -1,5 +1,10 @@ package v3 +import ( + "github.com/prysmaticlabs/prysm/beacon-chain/core/time" + "github.com/prysmaticlabs/prysm/beacon-chain/state/stateutil" +) + // CurrentEpochParticipation corresponding to participation bits on the beacon chain. func (b *BeaconState) CurrentEpochParticipation() ([]byte, error) { if !b.hasInnerState() { @@ -30,6 +35,26 @@ func (b *BeaconState) PreviousEpochParticipation() ([]byte, error) { return b.previousEpochParticipation(), nil } +// UnrealizedCheckpointBalances returns the total balances: active, target attested in +// current epoch and target attested in previous epoch. This function is used to +// compute the "unrealized justification" that a synced Beacon Block will have. +func (b *BeaconState) UnrealizedCheckpointBalances() (uint64, uint64, uint64, error) { + if !b.hasInnerState() { + return 0, 0, 0, ErrNilInnerState + } + b.lock.RLock() + defer b.lock.RUnlock() + + cp := b.state.CurrentEpochParticipation + pp := b.state.PreviousEpochParticipation + if cp == nil || pp == nil { + return 0, 0, 0, ErrNilParticipation + } + currentEpoch := time.CurrentEpoch(b) + return stateutil.UnrealizedCheckpointBalances(cp, pp, b.state.Validators, currentEpoch) + +} + // currentEpochParticipation corresponding to participation bits on the beacon chain. // This assumes that a lock is already held on BeaconState. func (b *BeaconState) currentEpochParticipation() []byte { diff --git a/beacon-chain/state/v3/getters_participation_test.go b/beacon-chain/state/v3/getters_participation_test.go new file mode 100644 index 000000000..1b92ef04a --- /dev/null +++ b/beacon-chain/state/v3/getters_participation_test.go @@ -0,0 +1,64 @@ +package v3 + +import ( + "testing" + + "github.com/prysmaticlabs/prysm/config/params" + ethpb "github.com/prysmaticlabs/prysm/proto/prysm/v1alpha1" + "github.com/prysmaticlabs/prysm/testing/require" +) + +func TestState_UnrealizedCheckpointBalances(t *testing.T) { + validators := make([]*ethpb.Validator, params.BeaconConfig().MinGenesisActiveValidatorCount) + balances := make([]uint64, params.BeaconConfig().MinGenesisActiveValidatorCount) + for i := 0; i < len(validators); i++ { + validators[i] = ðpb.Validator{ + ExitEpoch: params.BeaconConfig().FarFutureEpoch, + EffectiveBalance: params.BeaconConfig().MaxEffectiveBalance, + } + balances[i] = params.BeaconConfig().MaxEffectiveBalance + } + base := ðpb.BeaconStateBellatrix{ + Slot: 2, + RandaoMixes: make([][]byte, params.BeaconConfig().EpochsPerHistoricalVector), + + Validators: validators, + CurrentEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + PreviousEpochParticipation: make([]byte, params.BeaconConfig().MinGenesisActiveValidatorCount), + Balances: balances, + } + state, err := InitializeFromProto(base) + require.NoError(t, err) + + // No one voted in the last two epochs + allActive := params.BeaconConfig().MinGenesisActiveValidatorCount * params.BeaconConfig().MaxEffectiveBalance + active, previous, current, err := state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive, active) + require.Equal(t, uint64(0), current) + require.Equal(t, uint64(0), previous) + + // Add some votes in the last two epochs: + base.CurrentEpochParticipation[0] = 0xFF + base.PreviousEpochParticipation[0] = 0xFF + base.PreviousEpochParticipation[1] = 0xFF + + state, err = InitializeFromProto(base) + require.NoError(t, err) + active, previous, current, err = state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive, active) + require.Equal(t, params.BeaconConfig().MaxEffectiveBalance, current) + require.Equal(t, 2*params.BeaconConfig().MaxEffectiveBalance, previous) + + // Slash some validators + validators[0].Slashed = true + state, err = InitializeFromProto(base) + require.NoError(t, err) + active, previous, current, err = state.UnrealizedCheckpointBalances() + require.NoError(t, err) + require.Equal(t, allActive-params.BeaconConfig().MaxEffectiveBalance, active) + require.Equal(t, uint64(0), current) + require.Equal(t, params.BeaconConfig().MaxEffectiveBalance, previous) + +} diff --git a/beacon-chain/state/v3/getters_test.go b/beacon-chain/state/v3/getters_test.go index 58928b423..f84cec720 100644 --- a/beacon-chain/state/v3/getters_test.go +++ b/beacon-chain/state/v3/getters_test.go @@ -75,6 +75,8 @@ func TestNilState_NoPanic(t *testing.T) { require.ErrorIs(t, ErrNilInnerState, err) _, err = st.LatestExecutionPayloadHeader() require.ErrorIs(t, ErrNilInnerState, err) + _, _, _, err = st.UnrealizedCheckpointBalances() + require.ErrorIs(t, ErrNilInnerState, err) }