Skip to content

Commit

Permalink
feat(ADR-028): refunding transaction fee for certain txs (#125)
Browse files Browse the repository at this point in the history
Resolves babylonlabs-io/pm#64

This PR introduces the functionality of refunding fee for certain txs,
including BTC headers/checkpoints in BTC timestamping protocol, and
finality signature, BTC delegation inclusion proof, covenant signatures,
undelegation, selective slashing evidence in BTC staking protocol.

The implementation leverages a new key-only KV store in the incentive
module for storing refundable messages, and a new PostHandler for
refunding a tx if all msgs in it are refundable.

Along the way, this PR also adds dependency from BTC light client / BTC
staking modules to the incentive module, and adds relevant e2e tests to
ensure that the tx fee refunding indeed works.

- RFC:
https://github.com/babylonlabs-io/pm/blob/main/rfc/rfc-010-transaction-fee-refund-protocol.md
- ADR:
https://github.com/babylonlabs-io/pm/blob/main/adr/adr-028-transaction-fee-refund-protocol.md
  • Loading branch information
SebastianElvis authored Oct 4, 2024
1 parent b8bcb89 commit 6e5210d
Show file tree
Hide file tree
Showing 36 changed files with 428 additions and 96 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

### State Machine Breaking

* [#125](https://github.com/babylonlabs-io/babylon/pull/125) Implement ADR-028 and
refund transaction fee for certain transactions from protocol stakeholders
* [#107](https://github.com/babylonlabs-io/babylon/pull/107) Implement ADR-027 and
enable in-protocol minimum gas price
* [#103](https://github.com/babylonlabs-io/babylon/pull/103) Add token distribution
Expand Down
1 change: 0 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,6 @@ mockgen_cmd=go run github.com/golang/mock/[email protected]
mocks: $(MOCKS_DIR) ## Generate mock objects for testing
$(mockgen_cmd) -source=x/checkpointing/types/expected_keepers.go -package mocks -destination testutil/mocks/checkpointing_expected_keepers.go
$(mockgen_cmd) -source=x/checkpointing/keeper/bls_signer.go -package mocks -destination testutil/mocks/bls_signer.go
$(mockgen_cmd) -source=x/zoneconcierge/types/expected_keepers.go -package types -destination x/zoneconcierge/types/mocked_keepers.go
$(mockgen_cmd) -source=x/btcstaking/types/expected_keepers.go -package types -destination x/btcstaking/types/mocked_keepers.go
$(mockgen_cmd) -source=x/finality/types/expected_keepers.go -package types -destination x/finality/types/mocked_keepers.go
$(mockgen_cmd) -source=x/incentive/types/expected_keepers.go -package types -destination x/incentive/types/mocked_keepers.go
Expand Down
7 changes: 7 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ import (
"github.com/babylonlabs-io/babylon/x/finality"
finalitytypes "github.com/babylonlabs-io/babylon/x/finality/types"
"github.com/babylonlabs-io/babylon/x/incentive"
incentivekeeper "github.com/babylonlabs-io/babylon/x/incentive/keeper"
incentivetypes "github.com/babylonlabs-io/babylon/x/incentive/types"
"github.com/babylonlabs-io/babylon/x/monitor"
monitortypes "github.com/babylonlabs-io/babylon/x/monitor/types"
Expand Down Expand Up @@ -513,6 +514,12 @@ func NewBabylonApp(
app.SetEndBlocker(app.EndBlocker)
app.SetAnteHandler(anteHandler)

// set postHandler
postHandler := sdk.ChainPostDecorators(
incentivekeeper.NewRefundTxDecorator(&app.IncentiveKeeper),
)
app.SetPostHandler(postHandler)

// must be before Loading version
// requires the snapshot store to be created and registered as a BaseAppOption
// see cmd/wasmd/root.go: 206 - 214 approx
Expand Down
2 changes: 2 additions & 0 deletions app/keepers/keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,7 @@ func (ak *AppKeepers) InitKeepers(
appCodec,
runtime.NewKVStoreService(keys[btclightclienttypes.StoreKey]),
*btcConfig,
&ak.IncentiveKeeper,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)

Expand Down Expand Up @@ -498,6 +499,7 @@ func (ak *AppKeepers) InitKeepers(
// setting the finality keeper as nil for now
// need to set it after finality keeper is initiated
nil,
&ak.IncentiveKeeper,
btcNetParams,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)
Expand Down
56 changes: 32 additions & 24 deletions test/e2e/btc_staking_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,18 @@ func (s *BTCStakingTestSuite) Test2SubmitCovenantSignature() {
s.NoError(err)

for i := 0; i < int(s.covenantQuorum); i++ {
nonValidatorNode.AddCovenantSigs(
covenantSlashingSigs[i].CovPk,
stakingTxHash,
covenantSlashingSigs[i].AdaptorSigs,
bbn.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]),
covenantUnbondingSlashingSigs[i].AdaptorSigs,
)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
// add covenant sigs
nonValidatorNode.AddCovenantSigs(
covenantSlashingSigs[i].CovPk,
stakingTxHash,
covenantSlashingSigs[i].AdaptorSigs,
bbn.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]),
covenantUnbondingSlashingSigs[i].AdaptorSigs,
)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
}, true)
}

// wait for a block so that above txs take effect
Expand Down Expand Up @@ -345,18 +348,21 @@ func (s *BTCStakingTestSuite) Test3CommitPublicRandomnessAndSubmitFinalitySignat
sig, err := eots.Sign(s.fptBTCSK, randListInfo.SRList[idx], msgToSign)
s.NoError(err)
eotsSig := bbn.NewSchnorrEOTSSigFromModNScalar(sig)
// submit finality signature
nonValidatorNode.AddFinalitySig(s.cacheFP.BtcPk, activatedHeight, &randListInfo.PRList[idx], *randListInfo.ProofList[idx].ToProto(), appHash, eotsSig)

// ensure vote is eventually cast
var finalizedBlocks []*ftypes.IndexedBlock
s.Eventually(func() bool {
finalizedBlocks = nonValidatorNode.QueryListBlocks(ftypes.QueriedBlockStatus_FINALIZED)
return len(finalizedBlocks) > 0
}, time.Minute, time.Millisecond*50)
s.Equal(activatedHeight, finalizedBlocks[0].Height)
s.Equal(appHash.Bytes(), finalizedBlocks[0].AppHash)
s.T().Logf("the block %d is finalized", activatedHeight)
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
// submit finality signature
nonValidatorNode.AddFinalitySig(s.cacheFP.BtcPk, activatedHeight, &randListInfo.PRList[idx], *randListInfo.ProofList[idx].ToProto(), appHash, eotsSig)

// ensure vote is eventually cast
var finalizedBlocks []*ftypes.IndexedBlock
s.Eventually(func() bool {
finalizedBlocks = nonValidatorNode.QueryListBlocks(ftypes.QueriedBlockStatus_FINALIZED)
return len(finalizedBlocks) > 0
}, time.Minute, time.Millisecond*50)
s.Equal(activatedHeight, finalizedBlocks[0].Height)
s.Equal(appHash.Bytes(), finalizedBlocks[0].AppHash)
s.T().Logf("the block %d is finalized", activatedHeight)
}, true)

// ensure finality provider has received rewards after the block is finalised
fpRewardGauges, err := nonValidatorNode.QueryRewardGauge(fpBabylonAddr)
Expand Down Expand Up @@ -463,10 +469,12 @@ func (s *BTCStakingTestSuite) Test5SubmitStakerUnbonding() {
delUnbondingSig, err := activeDel.SignUnbondingTx(params, s.net, s.delBTCSK)
s.NoError(err)

// submit the message for creating BTC undelegation
nonValidatorNode.BTCUndelegate(&stakingTxHash, delUnbondingSig)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
// submit the message for creating BTC undelegation
nonValidatorNode.BTCUndelegate(&stakingTxHash, delUnbondingSig)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
}, true)

// Wait for unbonded delegations to be created
var unbondedDelsResp []*bstypes.BTCDelegationResponse
Expand Down
78 changes: 47 additions & 31 deletions test/e2e/btc_staking_pre_approval_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,17 @@ func (s *BTCStakingPreApprovalTestSuite) Test2SubmitCovenantSignature() {
s.NoError(err)

for i := 0; i < int(s.covenantQuorum); i++ {
nonValidatorNode.AddCovenantSigs(
covenantSlashingSigs[i].CovPk,
stakingTxHash,
covenantSlashingSigs[i].AdaptorSigs,
bbn.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]),
covenantUnbondingSlashingSigs[i].AdaptorSigs,
)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
nonValidatorNode.AddCovenantSigs(
covenantSlashingSigs[i].CovPk,
stakingTxHash,
covenantSlashingSigs[i].AdaptorSigs,
bbn.NewBIP340SignatureFromBTCSig(covUnbondingSigs[i]),
covenantUnbondingSlashingSigs[i].AdaptorSigs,
)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
}, true)
}

// wait for a block so that above txs take effect
Expand Down Expand Up @@ -266,13 +268,14 @@ func (s *BTCStakingPreApprovalTestSuite) Test3SendStakingTransctionInclusionProo
s.NoError(err)
stakingTxHash := stakingMsgTx.TxHash()

nonValidatorNode.AddBTCDelegationInclusionProof(
&stakingTxHash,
s.cachedInclusionProof,
)

nonValidatorNode.WaitForNextBlock()
nonValidatorNode.WaitForNextBlock()
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
nonValidatorNode.AddBTCDelegationInclusionProof(
&stakingTxHash,
s.cachedInclusionProof,
)
nonValidatorNode.WaitForNextBlock()
nonValidatorNode.WaitForNextBlock()
}, true)

activeBTCDelegations := nonValidatorNode.QueryActiveDelegations()
s.Len(activeBTCDelegations, 1)
Expand Down Expand Up @@ -366,17 +369,28 @@ func (s *BTCStakingPreApprovalTestSuite) Test4CommitPublicRandomnessAndSubmitFin
s.NoError(err)
eotsSig := bbn.NewSchnorrEOTSSigFromModNScalar(sig)
// submit finality signature
nonValidatorNode.AddFinalitySig(s.cacheFP.BtcPk, activatedHeight, &randListInfo.PRList[idx], *randListInfo.ProofList[idx].ToProto(), appHash, eotsSig)

// ensure vote is eventually cast
var finalizedBlocks []*ftypes.IndexedBlock
s.Eventually(func() bool {
finalizedBlocks = nonValidatorNode.QueryListBlocks(ftypes.QueriedBlockStatus_FINALIZED)
return len(finalizedBlocks) > 0
}, time.Minute, time.Millisecond*50)
s.Equal(activatedHeight, finalizedBlocks[0].Height)
s.Equal(appHash.Bytes(), finalizedBlocks[0].AppHash)
s.T().Logf("the block %d is finalized", activatedHeight)
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
nonValidatorNode.AddFinalitySig(s.cacheFP.BtcPk, activatedHeight, &randListInfo.PRList[idx], *randListInfo.ProofList[idx].ToProto(), appHash, eotsSig)

// ensure vote is eventually cast
var finalizedBlocks []*ftypes.IndexedBlock
s.Eventually(func() bool {
finalizedBlocks = nonValidatorNode.QueryListBlocks(ftypes.QueriedBlockStatus_FINALIZED)
return len(finalizedBlocks) > 0
}, time.Minute, time.Millisecond*50)
s.Equal(activatedHeight, finalizedBlocks[0].Height)
s.Equal(appHash.Bytes(), finalizedBlocks[0].AppHash)
s.T().Logf("the block %d is finalized", activatedHeight)
}, true)

// submit an invalid finality signature, and tx should NOT be refunded
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
_, pk, err := datagen.GenRandomBTCKeyPair(s.r)
s.NoError(err)
btcPK := bbn.NewBIP340PubKeyFromBTCPK(pk)
nonValidatorNode.AddFinalitySig(btcPK, activatedHeight, &randListInfo.PRList[idx], *randListInfo.ProofList[idx].ToProto(), appHash, eotsSig)
nonValidatorNode.WaitForNextBlock()
}, false)

// ensure finality provider has received rewards after the block is finalised
fpRewardGauges, err := nonValidatorNode.QueryRewardGauge(fpBabylonAddr)
Expand Down Expand Up @@ -483,10 +497,12 @@ func (s *BTCStakingPreApprovalTestSuite) Test5SubmitStakerUnbonding() {
delUnbondingSig, err := activeDel.SignUnbondingTx(params, s.net, s.delBTCSK)
s.NoError(err)

// submit the message for creating BTC undelegation
nonValidatorNode.BTCUndelegate(&stakingTxHash, delUnbondingSig)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
nonValidatorNode.SubmitRefundableTxWithAssertion(func() {
// submit the message for creating BTC undelegation
nonValidatorNode.BTCUndelegate(&stakingTxHash, delUnbondingSig)
// wait for a block so that above txs take effect
nonValidatorNode.WaitForNextBlock()
}, true)

// Wait for unbonded delegations to be created
var unbondedDelsResp []*bstypes.BTCDelegationResponse
Expand Down
33 changes: 30 additions & 3 deletions test/e2e/configurer/chain/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,13 @@ func (n *NodeConfig) FinalizeSealedEpochs(startEpoch uint64, lastEpoch uint64) {
tx2 := datagen.CreatOpReturnTransaction(r, p2)
opReturn2 := datagen.CreateBlockWithTransaction(r, opReturn1.HeaderBytes.ToBlockHeader(), tx2)

n.InsertHeader(&opReturn1.HeaderBytes)
n.InsertHeader(&opReturn2.HeaderBytes)
n.InsertProofs(opReturn1.SpvProof, opReturn2.SpvProof)
n.SubmitRefundableTxWithAssertion(func() {
n.InsertHeader(&opReturn1.HeaderBytes)
n.InsertHeader(&opReturn2.HeaderBytes)
}, true)
n.SubmitRefundableTxWithAssertion(func() {
n.InsertProofs(opReturn1.SpvProof, opReturn2.SpvProof)
}, true)

n.WaitForCondition(func() bool {
ckpt, err := n.QueryRawCheckpoint(checkpoint.Ckpt.EpochNum)
Expand Down Expand Up @@ -444,3 +448,26 @@ func (n *NodeConfig) TxGovVote(from string, propID int, option govv1.VoteOption,

n.LogActionF("successfully submitted vote %s to prop %d", option, propID)
}

// submitRefundableTxWithAssertion submits a refundable transaction,
// and asserts that the tx fee is refunded
func (n *NodeConfig) SubmitRefundableTxWithAssertion(
f func(),
shouldBeRefunded bool,
) {
// balance before submitting the refundable tx
submitterBalanceBefore, err := n.QueryBalances(n.PublicAddress)
require.NoError(n.t, err)

// submit refundable tx
f()

// ensure the tx fee is refunded and the balance is not changed
submitterBalanceAfter, err := n.QueryBalances(n.PublicAddress)
require.NoError(n.t, err)
if shouldBeRefunded {
require.Equal(n.t, submitterBalanceBefore, submitterBalanceAfter)
} else {
require.True(n.t, submitterBalanceBefore.IsAllGT(submitterBalanceAfter))
}
}
2 changes: 1 addition & 1 deletion test/e2e/configurer/chain/commands_btcstaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ func (n *NodeConfig) AddCovenantSigs(covPK *bbn.BIP340PubKey, stakingTxHash stri
// used key
cmd = append(cmd, "--from=val")
// gas
cmd = append(cmd, "--gas=auto", "--gas-adjustment=1.3")
cmd = append(cmd, "--gas=auto", "--gas-adjustment=2")

_, _, err := n.containerManager.ExecTxCmd(n.t, n.chainId, n.Name, cmd)
require.NoError(n.t, err)
Expand Down
12 changes: 10 additions & 2 deletions testutil/keeper/btclightclient.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
"context"
"testing"

"cosmossdk.io/core/header"
Expand All @@ -26,6 +27,10 @@ import (
btclightclientt "github.com/babylonlabs-io/babylon/x/btclightclient/types"
)

type MockIncentiveKeeper struct{}

func (mik MockIncentiveKeeper) IndexRefundableMsg(ctx context.Context, msg sdk.Msg) {}

func BTCLightClientKeeper(t testing.TB) (*btclightclientk.Keeper, sdk.Context) {
k, ctx, _ := BTCLightClientKeeperWithCustomParams(t, btclightclientt.DefaultParams())
return k, ctx
Expand All @@ -40,7 +45,10 @@ func NewBTCHeaderBytesList(chain []*wire.BlockHeader) []bbn.BTCHeaderBytes {
return chainBytes
}

func BTCLightClientKeeperWithCustomParams(t testing.TB, p btclightclientt.Params) (*btclightclientk.Keeper, sdk.Context, corestore.KVStoreService) {
func BTCLightClientKeeperWithCustomParams(
t testing.TB,
p btclightclientt.Params,
) (*btclightclientk.Keeper, sdk.Context, corestore.KVStoreService) {
storeKey := storetypes.NewKVStoreKey(btclightclientt.StoreKey)

db := dbm.NewMemDB()
Expand All @@ -52,12 +60,12 @@ func BTCLightClientKeeperWithCustomParams(t testing.TB, p btclightclientt.Params
cdc := codec.NewProtoCodec(registry)

testCfg := bbn.ParseBtcOptionsFromConfig(bapp.TmpAppOptions())

stServ := runtime.NewKVStoreService(storeKey)
k := btclightclientk.NewKeeper(
cdc,
stServ,
testCfg,
&MockIncentiveKeeper{},
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)

Expand Down
2 changes: 2 additions & 0 deletions testutil/keeper/btcstaking.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func BTCStakingKeeper(
btclcKeeper types.BTCLightClientKeeper,
btccKeeper types.BtcCheckpointKeeper,
finalityKeeper types.FinalityKeeper,
iKeeper types.IncentiveKeeper,
) (*keeper.Keeper, sdk.Context) {
storeKey := storetypes.NewKVStoreKey(types.StoreKey)

Expand All @@ -45,6 +46,7 @@ func BTCStakingKeeper(
btclcKeeper,
btccKeeper,
finalityKeeper,
iKeeper,
&chaincfg.SimNetParams,
authtypes.NewModuleAddress(govtypes.ModuleName).String(),
)
Expand Down
7 changes: 6 additions & 1 deletion x/btccheckpoint/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func NewMsgServerImpl(keeper Keeper) types.MsgServer {
}

// TODO at some point add proper logging of error
// TODO emit some events for external consumers. Those should be probably emited
// TODO emit some events for external consumers. Those should be probably emitted
// at EndBlockerCallback
func (ms msgServer) InsertBTCSpvProof(ctx context.Context, req *types.MsgInsertBTCSpvProof) (*types.MsgInsertBTCSpvProofResponse, error) {

Expand Down Expand Up @@ -92,6 +92,11 @@ func (ms msgServer) InsertBTCSpvProof(ctx context.Context, req *types.MsgInsertB
return nil, err
}

// At this point, the BTC checkpoint is considered the first valid submission
// for the epoch.
// Thus, we can safely consider this message as refundable
ms.k.incentiveKeeper.IndexRefundableMsg(sdkCtx, req)

return &types.MsgInsertBTCSpvProofResponse{}, nil
}

Expand Down
3 changes: 3 additions & 0 deletions x/btccheckpoint/types/expected_keepers.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package types

import (
"context"

txformat "github.com/babylonlabs-io/babylon/btctxformatter"
bbn "github.com/babylonlabs-io/babylon/types"
sdk "github.com/cosmos/cosmos-sdk/types"
)

type BTCLightClientKeeper interface {
Expand Down Expand Up @@ -40,4 +42,5 @@ type CheckpointingKeeper interface {

type IncentiveKeeper interface {
RewardBTCTimestamping(ctx context.Context, epoch uint64, rewardDistInfo *RewardDistInfo)
IndexRefundableMsg(ctx context.Context, msg sdk.Msg)
}
Loading

0 comments on commit 6e5210d

Please sign in to comment.