diff --git a/test/e2e/btc_staking_e2e_test.go b/test/e2e/btc_staking_e2e_test.go index d8b733515..714ef0bca 100644 --- a/test/e2e/btc_staking_e2e_test.go +++ b/test/e2e/btc_staking_e2e_test.go @@ -244,21 +244,6 @@ func (s *BTCStakingTestSuite) Test2SubmitCovenantSignature() { activeDel := activeDels.Dels[0] s.True(activeDel.HasCovenantQuorums(covenantQuorum)) - - // wait for a block so that above txs take effect and the voting power table - // is updated in the next block's BeginBlock - nonValidatorNode.WaitForNextBlock() - - // ensure BTC staking is activated - activatedHeight := nonValidatorNode.QueryActivatedHeight() - s.Positive(activatedHeight) - // ensure finality provider has voting power at activated height - currentBtcTip, err := nonValidatorNode.QueryTip() - s.NoError(err) - activeFps := nonValidatorNode.QueryActiveFinalityProvidersAtHeight(activatedHeight) - s.Len(activeFps, 1) - s.Equal(activeFps[0].VotingPower, activeDels.VotingPower(currentBtcTip.Height, initialization.BabylonBtcFinalizationPeriod, params.CovenantQuorum)) - s.Equal(activeFps[0].VotingPower, activeDel.VotingPower(currentBtcTip.Height, initialization.BabylonBtcFinalizationPeriod, params.CovenantQuorum)) } // Test2CommitPublicRandomnessAndSubmitFinalitySignature is an end-to-end @@ -271,10 +256,11 @@ func (s *BTCStakingTestSuite) Test3CommitPublicRandomnessAndSubmitFinalitySignat s.NoError(err) // get activated height - activatedHeight := nonValidatorNode.QueryActivatedHeight() - s.Positive(activatedHeight) - _, err = nonValidatorNode.QueryCurrentHeight() - s.NoError(err) + activatedHeight, err := nonValidatorNode.QueryActivatedHeight() + s.ErrorIs(err, bstypes.ErrBTCStakingNotActivated) + fps := nonValidatorNode.QueryFinalityProviders() + s.Len(fps, 1) + s.Zero(fps[0].VotingPower) /* commit a number of public randomness since activatedHeight diff --git a/test/e2e/configurer/chain/queries_btcstaking.go b/test/e2e/configurer/chain/queries_btcstaking.go index f7a735c8f..861ac6a69 100644 --- a/test/e2e/configurer/chain/queries_btcstaking.go +++ b/test/e2e/configurer/chain/queries_btcstaking.go @@ -94,15 +94,19 @@ func (n *NodeConfig) QueryUnbondedDelegations() []*bstypes.BTCDelegationResponse return resp.BtcDelegations } -func (n *NodeConfig) QueryActivatedHeight() uint64 { +func (n *NodeConfig) QueryActivatedHeight() (uint64, error) { bz, err := n.QueryGRPCGateway("/babylon/btcstaking/v1/activated_height", url.Values{}) - require.NoError(n.t, err) + if err != nil { + return 0, err + } var resp bstypes.QueryActivatedHeightResponse err = util.Cdc.UnmarshalJSON(bz, &resp) - require.NoError(n.t, err) + if err != nil { + return 0, err + } - return resp.Height + return resp.Height, nil } // TODO: pagination support diff --git a/test/e2e/upgrades/signet-launch.json b/test/e2e/upgrades/signet-launch.json index f1be8ba79..3c958c90e 100644 --- a/test/e2e/upgrades/signet-launch.json +++ b/test/e2e/upgrades/signet-launch.json @@ -6,7 +6,7 @@ "plan": { "name": "signet-launch", "time": "0001-01-01T00:00:00Z", - "height": "23", + "height": "22", "info": "Msg info", "upgraded_client_state": null } diff --git a/x/btcstaking/keeper/power_dist_change.go b/x/btcstaking/keeper/power_dist_change.go index 2b42b1742..8b31133e7 100644 --- a/x/btcstaking/keeper/power_dist_change.go +++ b/x/btcstaking/keeper/power_dist_change.go @@ -35,7 +35,7 @@ func (k Keeper) UpdatePowerDist(ctx context.Context) { if len(events) == 0 { if dc != nil { // map everything in prev height to this height - k.recordVotingPowerAndCache(ctx, dc) + k.recordVotingPowerAndCache(ctx, dc, dc, maxActiveFps) } return } @@ -56,31 +56,44 @@ func (k Keeper) UpdatePowerDist(ctx context.Context) { // to construct the new distribution newDc := k.ProcessAllPowerDistUpdateEvents(ctx, dc, events, maxActiveFps) - // find newly bonded finality providers and execute the hooks - newBondedFinalityProviders := newDc.FindNewActiveFinalityProviders(dc) - for _, fp := range newBondedFinalityProviders { - if err := k.hooks.AfterFinalityProviderActivated(ctx, fp.BtcPk); err != nil { - panic(fmt.Errorf("failed to execute after finality provider %s bonded", fp.BtcPk.MarshalHex())) - } - } - // record voting power and cache for this height - k.recordVotingPowerAndCache(ctx, newDc) + k.recordVotingPowerAndCache(ctx, dc, newDc, maxActiveFps) // record metrics k.recordMetrics(newDc) } -func (k Keeper) recordVotingPowerAndCache(ctx context.Context, dc *types.VotingPowerDistCache) { +func (k Keeper) recordVotingPowerAndCache(ctx context.Context, prevDc, newDc *types.VotingPowerDistCache, maxActiveFps uint32) *types.VotingPowerDistCache { babylonTipHeight := uint64(sdk.UnwrapSDKContext(ctx).HeaderInfo().Height) + // label fps with whether it has timestamped pub rand + for _, fp := range newDc.FinalityProviders { + // TODO calling HasTimestampedPubRand potentially iterates + // all the pub rand committed by the fp, which might slow down + // the process, need optimization + fp.IsTimestamped = k.FinalityKeeper.HasTimestampedPubRand(ctx, fp.BtcPk, babylonTipHeight) + } + + // filter out the top N finality providers and their total voting power, and + // record them in the new cache + newDc.ApplyActiveFinalityProviders(maxActiveFps) // set voting power table for this height - for i := uint32(0); i < dc.NumActiveFps; i++ { - fp := dc.FinalityProviders[i] + for i := uint32(0); i < newDc.NumActiveFps; i++ { + fp := newDc.FinalityProviders[i] k.SetVotingPower(ctx, fp.BtcPk.MustMarshal(), babylonTipHeight, fp.TotalVotingPower) } + // find newly bonded finality providers and execute the hooks + newBondedFinalityProviders := newDc.FindNewActiveFinalityProviders(prevDc) + for _, fp := range newBondedFinalityProviders { + if err := k.hooks.AfterFinalityProviderActivated(ctx, fp.BtcPk); err != nil { + panic(fmt.Errorf("failed to execute after finality provider %s bonded", fp.BtcPk.MarshalHex())) + } + } + // set the voting power distribution cache of the current height - k.setVotingPowerDistCache(ctx, babylonTipHeight, dc) + k.setVotingPowerDistCache(ctx, babylonTipHeight, newDc) + + return newDc } func (k Keeper) recordMetrics(dc *types.VotingPowerDistCache) { @@ -112,7 +125,6 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents( events []*types.EventPowerDistUpdate, maxActiveFps uint32, ) *types.VotingPowerDistCache { - height := uint64(sdk.UnwrapSDKContext(ctx).HeaderInfo().Height) // a map where key is finality provider's BTC PK hex and value is a list // of BTC delegations that newly become active under this provider activeBTCDels := map[string][]*types.BTCDelegation{} @@ -236,18 +248,6 @@ func (k Keeper) ProcessAllPowerDistUpdateEvents( } } - // label fps that does not have timestamped pub rand - for _, fp := range newDc.FinalityProviders { - // TODO calling HasTimestampedPubRand potentially iterates - // all the pub rand committed by the fp, which might slow down - // the process, need optimization - fp.IsTimestamped = k.FinalityKeeper.HasTimestampedPubRand(ctx, fp.BtcPk, height) - } - - // filter out the top N finality providers and their total voting power, and - // record them in the new cache - newDc.ApplyActiveFinalityProviders(maxActiveFps) - return newDc } diff --git a/x/btcstaking/keeper/power_dist_change_test.go b/x/btcstaking/keeper/power_dist_change_test.go index 0016ca9c6..0277ad28c 100644 --- a/x/btcstaking/keeper/power_dist_change_test.go +++ b/x/btcstaking/keeper/power_dist_change_test.go @@ -157,7 +157,6 @@ func FuzzBTCDelegationEvents(f *testing.F) { btclcKeeper := types.NewMockBTCLightClientKeeper(ctrl) btccKeeper := types.NewMockBtcCheckpointKeeper(ctrl) finalityKeeper := types.NewMockFinalityKeeper(ctrl) - finalityKeeper.EXPECT().HasTimestampedPubRand(gomock.Any(), gomock.Any(), gomock.Any()).Return(true).AnyTimes() h := NewHelper(t, btclcKeeper, btccKeeper, finalityKeeper) // set all parameters @@ -221,10 +220,21 @@ func FuzzBTCDelegationEvents(f *testing.F) { require.Equal(t, expectedStakingTxHash, btcDelStateUpdate.StakingTxHash) require.Equal(t, types.BTCDelegationStatus_ACTIVE, btcDelStateUpdate.NewState) - // ensure this finality provider has voting power at the current height + // ensure this finality provider does not have voting power at the current height + // due to no timestamped randomness + babylonHeight += 1 + h.SetCtxHeight(babylonHeight) + h.BTCLightClientKeeper.EXPECT().GetTipInfo(gomock.Eq(h.Ctx)).Return(btcTip).AnyTimes() + finalityKeeper.EXPECT().HasTimestampedPubRand(gomock.Any(), gomock.Any(), gomock.Eq(babylonHeight)).Return(false).AnyTimes() + err = h.BTCStakingKeeper.BeginBlocker(h.Ctx) + h.NoError(err) + require.Zero(t, h.BTCStakingKeeper.GetVotingPower(h.Ctx, *fp.BtcPk, babylonHeight)) + + // ensure this finality provider has voting power at the current height after having timestamped pub rand babylonHeight += 1 h.SetCtxHeight(babylonHeight) h.BTCLightClientKeeper.EXPECT().GetTipInfo(gomock.Eq(h.Ctx)).Return(btcTip).AnyTimes() + finalityKeeper.EXPECT().HasTimestampedPubRand(gomock.Any(), gomock.Any(), gomock.Eq(babylonHeight)).Return(true).AnyTimes() err = h.BTCStakingKeeper.BeginBlocker(h.Ctx) h.NoError(err) require.Equal(t, uint64(stakingValue), h.BTCStakingKeeper.GetVotingPower(h.Ctx, *fp.BtcPk, babylonHeight)) diff --git a/x/btcstaking/types/incentive_test.go b/x/btcstaking/types/incentive_test.go new file mode 100644 index 000000000..950467b7e --- /dev/null +++ b/x/btcstaking/types/incentive_test.go @@ -0,0 +1,93 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestVotingPowerDistCache(t *testing.T) { + tests := []struct { + desc string + maxActiveFPs uint32 + numActiveFps uint32 + totalVotingPower uint64 + fps []*FinalityProviderDistInfo + }{ + { + desc: "all not timestamped", + maxActiveFPs: 80, + numActiveFps: 0, + totalVotingPower: 0, + fps: []*FinalityProviderDistInfo{ + { + TotalVotingPower: 1000, + IsTimestamped: false, + }, + { + TotalVotingPower: 2000, + IsTimestamped: false, + }, + }, + }, + { + desc: "all timestamped", + maxActiveFPs: 80, + numActiveFps: 2, + totalVotingPower: 3000, + fps: []*FinalityProviderDistInfo{ + { + TotalVotingPower: 1000, + IsTimestamped: true, + }, + { + TotalVotingPower: 2000, + IsTimestamped: true, + }, + }, + }, + { + desc: "partly timestamped", + maxActiveFPs: 80, + numActiveFps: 1, + totalVotingPower: 1000, + fps: []*FinalityProviderDistInfo{ + { + TotalVotingPower: 1000, + IsTimestamped: true, + }, + { + TotalVotingPower: 2000, + IsTimestamped: false, + }, + }, + }, + { + desc: "small max active fps", + maxActiveFPs: 1, + numActiveFps: 1, + totalVotingPower: 2000, + fps: []*FinalityProviderDistInfo{ + { + TotalVotingPower: 1000, + IsTimestamped: true, + }, + { + TotalVotingPower: 2000, + IsTimestamped: true, + }, + }, + }, + } + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + dc := NewVotingPowerDistCache() + for _, fp := range tc.fps { + dc.AddFinalityProviderDistInfo(fp) + } + dc.ApplyActiveFinalityProviders(tc.maxActiveFPs) + require.Equal(t, tc.totalVotingPower, dc.TotalVotingPower) + require.Equal(t, tc.numActiveFps, dc.NumActiveFps) + }) + } +} diff --git a/x/finality/keeper/public_randomness.go b/x/finality/keeper/public_randomness.go index 470e747ef..6508118f1 100644 --- a/x/finality/keeper/public_randomness.go +++ b/x/finality/keeper/public_randomness.go @@ -45,7 +45,7 @@ func (k Keeper) GetTimestampedPubRandCommitForHeight(ctx context.Context, fpBtcP // ensure the finality provider's last randomness commit is already finalised by BTC timestamping finalizedEpoch := k.GetLastFinalizedEpoch(ctx) if finalizedEpoch == 0 { - return nil, fmt.Errorf("no finalized epoch yet") + return nil, types.ErrPubRandCommitNotBTCTimestamped.Wrapf("no finalized epoch yet") } if finalizedEpoch < prCommit.EpochNum { return nil, types.ErrPubRandCommitNotBTCTimestamped.