diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ac9b5..03153b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) * [#83](https://github.com/babylonlabs-io/covenant-emulator/pull/83) covenant-signer: remove go.mod * [#95](https://github.com/babylonlabs-io/covenant-emulator/pull/95) removed local signer option as the covenant emulator should only connect to a remote signer +* [#96](https://github.com/babylonlabs-io/covenant-emulator/pull/96) add pagination to `queryDelegationsWithStatus` ## v0.11.3 diff --git a/clientcontroller/babylon.go b/clientcontroller/babylon.go index 2b7efaa..9e9d700 100644 --- a/clientcontroller/babylon.go +++ b/clientcontroller/babylon.go @@ -27,7 +27,10 @@ import ( "github.com/babylonlabs-io/covenant-emulator/types" ) -var _ ClientController = &BabylonController{} +var ( + _ ClientController = &BabylonController{} + MaxPaginationLimit = uint64(1000) +) type BabylonController struct { bbnClient *bbnclient.Client @@ -183,39 +186,67 @@ func (bc *BabylonController) SubmitCovenantSigs(covSigs []*types.CovenantSigs) ( return &types.TxResponse{TxHash: res.TxHash, Events: res.Events}, nil } -func (bc *BabylonController) QueryPendingDelegations(limit uint64) ([]*types.Delegation, error) { - return bc.queryDelegationsWithStatus(btcstakingtypes.BTCDelegationStatus_PENDING, limit) +func (bc *BabylonController) QueryPendingDelegations(limit uint64, filter FilterFn) ([]*types.Delegation, error) { + return bc.queryDelegationsWithStatus(btcstakingtypes.BTCDelegationStatus_PENDING, limit, filter) } func (bc *BabylonController) QueryActiveDelegations(limit uint64) ([]*types.Delegation, error) { - return bc.queryDelegationsWithStatus(btcstakingtypes.BTCDelegationStatus_ACTIVE, limit) + return bc.queryDelegationsWithStatus(btcstakingtypes.BTCDelegationStatus_ACTIVE, limit, nil) } func (bc *BabylonController) QueryVerifiedDelegations(limit uint64) ([]*types.Delegation, error) { - return bc.queryDelegationsWithStatus(btcstakingtypes.BTCDelegationStatus_VERIFIED, limit) + return bc.queryDelegationsWithStatus(btcstakingtypes.BTCDelegationStatus_VERIFIED, limit, nil) } // queryDelegationsWithStatus queries BTC delegations that need a Covenant signature // with the given status (either pending or unbonding) // it is only used when the program is running in Covenant mode -func (bc *BabylonController) queryDelegationsWithStatus(status btcstakingtypes.BTCDelegationStatus, limit uint64) ([]*types.Delegation, error) { +func (bc *BabylonController) queryDelegationsWithStatus(status btcstakingtypes.BTCDelegationStatus, delsLimit uint64, filter FilterFn) ([]*types.Delegation, error) { + pgLimit := min(MaxPaginationLimit, delsLimit) pagination := &sdkquery.PageRequest{ - Limit: limit, + Limit: pgLimit, } - res, err := bc.bbnClient.QueryClient.BTCDelegations(status, pagination) - if err != nil { - return nil, fmt.Errorf("failed to query BTC delegations: %v", err) - } + dels := make([]*types.Delegation, 0, delsLimit) + indexDels := uint64(0) - dels := make([]*types.Delegation, 0, len(res.BtcDelegations)) - for _, delResp := range res.BtcDelegations { - del, err := DelegationRespToDelegation(delResp) + for indexDels < delsLimit { + res, err := bc.bbnClient.QueryClient.BTCDelegations(status, pagination) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to query BTC delegations: %v", err) } - dels = append(dels, del) + for _, delResp := range res.BtcDelegations { + del, err := DelegationRespToDelegation(delResp) + if err != nil { + return nil, err + } + + if filter != nil { + accept, err := filter(del) + if err != nil { + return nil, err + } + + if !accept { + continue + } + } + + dels = append(dels, del) + indexDels++ + + if indexDels == delsLimit { + return dels, nil + } + } + + // if returned a different number of btc delegations than the pagination limit + // it means that there is no more delegations at the store + if uint64(len(res.BtcDelegations)) != pgLimit { + return dels, nil + } + pagination.Key = res.Pagination.NextKey } return dels, nil diff --git a/clientcontroller/interface.go b/clientcontroller/interface.go index 7d15314..53e0e8d 100644 --- a/clientcontroller/interface.go +++ b/clientcontroller/interface.go @@ -14,19 +14,22 @@ const ( babylonConsumerChainName = "babylon" ) -type ClientController interface { - // SubmitCovenantSigs submits Covenant signatures to the consumer chain, each corresponding to - // a finality provider that the delegation is (re-)staked to - // it returns tx hash and error - SubmitCovenantSigs(covSigMsgs []*types.CovenantSigs) (*types.TxResponse, error) +type ( + FilterFn func(del *types.Delegation) (accept bool, err error) + ClientController interface { + // SubmitCovenantSigs submits Covenant signatures to the consumer chain, each corresponding to + // a finality provider that the delegation is (re-)staked to + // it returns tx hash and error + SubmitCovenantSigs(covSigMsgs []*types.CovenantSigs) (*types.TxResponse, error) - // QueryPendingDelegations queries BTC delegations that are in status of pending - QueryPendingDelegations(limit uint64) ([]*types.Delegation, error) + // QueryPendingDelegations queries BTC delegations that are in status of pending + QueryPendingDelegations(limit uint64, filter FilterFn) ([]*types.Delegation, error) - QueryStakingParamsByVersion(version uint32) (*types.StakingParams, error) + QueryStakingParamsByVersion(version uint32) (*types.StakingParams, error) - Close() error -} + Close() error + } +) func NewClientController(chainName string, bbnConfig *config.BBNConfig, netParams *chaincfg.Params, logger *zap.Logger) (ClientController, error) { var ( diff --git a/covenant-signer/keystore/cosmos/keyringcontroller.go b/covenant-signer/keystore/cosmos/keyringcontroller.go index c3bfcbf..ad3fe1a 100644 --- a/covenant-signer/keystore/cosmos/keyringcontroller.go +++ b/covenant-signer/keystore/cosmos/keyringcontroller.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/crypto/keyring" sdksecp256k1 "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/go-bip39" ) @@ -18,6 +19,7 @@ const ( type ChainKeyInfo struct { Name string + Address sdk.AccAddress Mnemonic string PublicKey *btcec.PublicKey PrivateKey *btcec.PrivateKey @@ -98,6 +100,10 @@ func (kc *ChainKeyringController) CreateChainKey(passphrase, hdPath string) (*Ch return nil, err } + addr, err := record.GetAddress() + if err != nil { + return nil, err + } privKey := record.GetLocal().PrivKey.GetCachedValue() switch v := privKey.(type) { @@ -105,6 +111,7 @@ func (kc *ChainKeyringController) CreateChainKey(passphrase, hdPath string) (*Ch sk, pk := btcec.PrivKeyFromBytes(v.Key) return &ChainKeyInfo{ Name: kc.keyName, + Address: addr, PublicKey: pk, PrivateKey: sk, Mnemonic: mnemonic, diff --git a/covenant/covenant.go b/covenant/covenant.go index 5d0409d..f97ec09 100644 --- a/covenant/covenant.go +++ b/covenant/covenant.go @@ -471,40 +471,36 @@ func CovenantAlreadySigned(covenantSerializedPk []byte, del *types.Delegation) b return false } -// sanitizeDelegations removes any delegations that have already been signed by the covenant and -// remove delegations that were not constructed with this covenant public key -func (ce *CovenantEmulator) sanitizeDelegations(dels []*types.Delegation) ([]*types.Delegation, error) { - return SanitizeDelegations(ce.pk, ce.paramCache, dels) +// acceptDelegationToSign verifies if the delegation should be accepted to sign. +func (ce *CovenantEmulator) acceptDelegationToSign(del *types.Delegation) (accept bool, err error) { + return AcceptDelegationToSign(ce.pk, ce.paramCache, del) } -// SanitizeDelegations remove the delegations in which the covenant public key already signed -// or the delegation was not constructed with that covenant public key -func SanitizeDelegations( +// AcceptDelegationToSign returns true if the delegation should be accepted to be signed. +// Returns false if the covenant public key already signed +// or if the delegation was not constructed with that covenant public key. +func AcceptDelegationToSign( pk *btcec.PublicKey, paramCache ParamsGetter, - dels []*types.Delegation, -) ([]*types.Delegation, error) { + del *types.Delegation, +) (accept bool, err error) { covenantSerializedPk := schnorr.SerializePubKey(pk) + // 1. Check if the delegation does not need the covenant's signature because + // this covenant already signed + if CovenantAlreadySigned(covenantSerializedPk, del) { + return false, nil + } - sanitized := make([]*types.Delegation, 0, len(dels)) - for _, del := range dels { - // 1. Remove delegations that do not need the covenant's signature because - // this covenant already signed - if CovenantAlreadySigned(covenantSerializedPk, del) { - continue - } - // 2. Remove delegations that were not constructed with this covenant public key - isInCommittee, err := IsKeyInCommittee(paramCache, covenantSerializedPk, del) - if err != nil { - return nil, fmt.Errorf("unable to verify if covenant key is in committee: %w", err) - } - if !isInCommittee { - continue - } - sanitized = append(sanitized, del) + // 2. Check if the delegation was not constructed with this covenant public key + isInCommittee, err := IsKeyInCommittee(paramCache, covenantSerializedPk, del) + if err != nil { + return false, fmt.Errorf("unable to verify if covenant key is in committee: %w", err) + } + if !isInCommittee { + return false, nil } - return sanitized, nil + return true, nil } // covenantSigSubmissionLoop is the reactor to submit Covenant signature for BTC delegations @@ -522,7 +518,7 @@ func (ce *CovenantEmulator) covenantSigSubmissionLoop() { select { case <-covenantSigTicker.C: // 1. Get all pending delegations - dels, err := ce.cc.QueryPendingDelegations(limit) + dels, err := ce.cc.QueryPendingDelegations(limit, ce.acceptDelegationToSign) if err != nil { ce.logger.Debug("failed to get pending delegations", zap.Error(err)) continue @@ -532,31 +528,13 @@ func (ce *CovenantEmulator) covenantSigSubmissionLoop() { // record delegation metrics ce.recordMetricsCurrentPendingDelegations(pendingDels) - if len(dels) == 0 { + if pendingDels == 0 { ce.logger.Debug("no pending delegations are found") continue } - // 2. Remove delegations that do not need the covenant's signature - sanitizedDels, err := ce.sanitizeDelegations(dels) - if err != nil { - ce.logger.Error( - "error sanitizing delegations", - zap.Error(err), - ) - continue - } - - if len(sanitizedDels) == 0 { - ce.logger.Debug( - "no new delegations to sign", - zap.Int("pending_dels_len", pendingDels), - ) - continue - } - - // 3. Split delegations into batches for submission - batches := ce.delegationsToBatches(sanitizedDels) + // 2. Split delegations into batches for submission + batches := ce.delegationsToBatches(dels) for _, delBatch := range batches { _, err := ce.AddCovenantSignatures(delBatch) if err != nil { diff --git a/covenant/covenant_test.go b/covenant/covenant_test.go index edb9cab..087416a 100644 --- a/covenant/covenant_test.go +++ b/covenant/covenant_test.go @@ -276,27 +276,26 @@ func TestDeduplicationWithOddKey(t *testing.T) { randomKey, err := btcec.NewPrivateKey() require.NoError(t, err) - pubKey := randomKey.PubKey() + randPubKey := randomKey.PubKey() paramVersion := uint32(2) - delegations := []*types.Delegation{ - &types.Delegation{ - CovenantSigs: []*types.CovenantAdaptorSigInfo{ - &types.CovenantAdaptorSigInfo{ - // 3. Delegation is already signed by the public key with odd y coordinate - Pk: pubKeyFromSchnorr, - }, + delAlreadySigned := &types.Delegation{ + CovenantSigs: []*types.CovenantAdaptorSigInfo{ + &types.CovenantAdaptorSigInfo{ + // 3. Delegation is already signed by the public key with odd y coordinate + Pk: pubKeyFromSchnorr, }, - ParamsVersion: paramVersion, }, - &types.Delegation{ - CovenantSigs: []*types.CovenantAdaptorSigInfo{ - &types.CovenantAdaptorSigInfo{ - Pk: pubKey, - }, + ParamsVersion: paramVersion, + } + + delNotSigned := &types.Delegation{ + CovenantSigs: []*types.CovenantAdaptorSigInfo{ + &types.CovenantAdaptorSigInfo{ + Pk: randPubKey, }, - ParamsVersion: paramVersion, }, + ParamsVersion: paramVersion, } paramsGet := NewMockParam(map[uint32]*types.StakingParams{ @@ -306,9 +305,13 @@ func TestDeduplicationWithOddKey(t *testing.T) { }) // 4. After removing the already signed delegation, the list should have only one element - sanitized, err := covenant.SanitizeDelegations(oddKeyPub, paramsGet, delegations) - require.Equal(t, 1, len(sanitized)) + accept, err := covenant.AcceptDelegationToSign(oddKeyPub, paramsGet, delAlreadySigned) + require.NoError(t, err) + require.False(t, accept) + + accept, err = covenant.AcceptDelegationToSign(oddKeyPub, paramsGet, delNotSigned) require.NoError(t, err) + require.True(t, accept) } func TestIsKeyInCommittee(t *testing.T) { @@ -355,51 +358,50 @@ func TestIsKeyInCommittee(t *testing.T) { actual, err := covenant.IsKeyInCommittee(paramsGet, covenantSerializedPk, delNoCovenant) require.False(t, actual) require.NoError(t, err) - emptyDels, err := covenant.SanitizeDelegations(covKeyPair.PublicKey, paramsGet, []*types.Delegation{delNoCovenant, delNoCovenant}) + + accept, err := covenant.AcceptDelegationToSign(covKeyPair.PublicKey, paramsGet, delNoCovenant) require.NoError(t, err) - require.Len(t, emptyDels, 0) + require.False(t, accept) // checks the case where the covenant is in the committee actual, err = covenant.IsKeyInCommittee(paramsGet, covenantSerializedPk, delWithCovenant) require.True(t, actual) require.NoError(t, err) - dels, err := covenant.SanitizeDelegations(covKeyPair.PublicKey, paramsGet, []*types.Delegation{delWithCovenant, delNoCovenant}) - require.NoError(t, err) - require.Len(t, dels, 1) - dels, err = covenant.SanitizeDelegations(covKeyPair.PublicKey, paramsGet, []*types.Delegation{delWithCovenant}) - require.NoError(t, err) - require.Len(t, dels, 1) - - amtSatFirst := btcutil.Amount(100) - amtSatSecond := btcutil.Amount(150) - amtSatThird := btcutil.Amount(200) - lastUnsanitizedDels := []*types.Delegation{ - &types.Delegation{ - ParamsVersion: pVersionWithCovenant, - TotalSat: amtSatFirst, - }, - delNoCovenant, - &types.Delegation{ - ParamsVersion: pVersionWithCovenant, - TotalSat: amtSatSecond, - }, - delNoCovenant, - &types.Delegation{ - ParamsVersion: pVersionWithCovenant, - TotalSat: amtSatThird, - }, - } - sanitizedDels, err := covenant.SanitizeDelegations(covKeyPair.PublicKey, paramsGet, lastUnsanitizedDels) + accept, err = covenant.AcceptDelegationToSign(covKeyPair.PublicKey, paramsGet, delWithCovenant) require.NoError(t, err) - require.Len(t, sanitizedDels, 3) - require.Equal(t, amtSatFirst, sanitizedDels[0].TotalSat) - require.Equal(t, amtSatSecond, sanitizedDels[1].TotalSat) - require.Equal(t, amtSatThird, sanitizedDels[2].TotalSat) + require.True(t, accept) + + // amtSatFirst := btcutil.Amount(100) + // amtSatSecond := btcutil.Amount(150) + // amtSatThird := btcutil.Amount(200) + // lastUnsanitizedDels := []*types.Delegation{ + // &types.Delegation{ + // ParamsVersion: pVersionWithCovenant, + // TotalSat: amtSatFirst, + // }, + // delNoCovenant, + // &types.Delegation{ + // ParamsVersion: pVersionWithCovenant, + // TotalSat: amtSatSecond, + // }, + // delNoCovenant, + // &types.Delegation{ + // ParamsVersion: pVersionWithCovenant, + // TotalSat: amtSatThird, + // }, + // } + + // sanitizedDels, err := covenant.SanitizeDelegations(covKeyPair.PublicKey, paramsGet, lastUnsanitizedDels) + // require.NoError(t, err) + // require.Len(t, sanitizedDels, 3) + // require.Equal(t, amtSatFirst, sanitizedDels[0].TotalSat) + // require.Equal(t, amtSatSecond, sanitizedDels[1].TotalSat) + // require.Equal(t, amtSatThird, sanitizedDels[2].TotalSat) errParamGet := fmt.Errorf("dumbErr") - sanitizedDels, err = covenant.SanitizeDelegations(covKeyPair.PublicKey, NewMockParamError(errParamGet), lastUnsanitizedDels) - require.Nil(t, sanitizedDels) + accept, err = covenant.AcceptDelegationToSign(covKeyPair.PublicKey, NewMockParamError(errParamGet), delWithCovenant) + require.False(t, accept) errKeyIsInCommittee := fmt.Errorf("unable to get the param version: %d, reason: %s", pVersionWithCovenant, errParamGet.Error()) expErr := fmt.Errorf("unable to verify if covenant key is in committee: %s", errKeyIsInCommittee.Error()) diff --git a/itest/babylon_node_handler.go b/itest/babylon_node_handler.go index 7f7e5bf..d56616a 100644 --- a/itest/babylon_node_handler.go +++ b/itest/babylon_node_handler.go @@ -23,14 +23,16 @@ type babylonNode struct { cmd *exec.Cmd pidFile string dataDir string + nodeHome string chainID string slashingAddr string covenantPk *types.BIP340PubKey } -func newBabylonNode(dataDir string, cmd *exec.Cmd, chainID string, slashingAddr string, covenantPk *types.BIP340PubKey) *babylonNode { +func newBabylonNode(dataDir, nodeHome string, cmd *exec.Cmd, chainID string, slashingAddr string, covenantPk *types.BIP340PubKey) *babylonNode { return &babylonNode{ dataDir: dataDir, + nodeHome: nodeHome, cmd: cmd, chainID: chainID, slashingAddr: slashingAddr, @@ -122,7 +124,7 @@ func NewBabylonNodeHandler(t *testing.T, covenantPk *types.BIP340PubKey) *Babylo } }() - nodeDataDir := filepath.Join(testDir, "node0", "babylond") + nodeHome := filepath.Join(testDir, "node0", "babylond") slashingAddr := "SZtRT4BySL3o4efdGLh3k7Kny8GAnsBrSW" decodedAddr, err := btcutil.DecodeAddress(slashingAddr, &chaincfg.SimNetParams) @@ -164,14 +166,14 @@ func NewBabylonNodeHandler(t *testing.T, covenantPk *types.BIP340PubKey) *Babylo startCmd := exec.Command( "babylond", "start", - fmt.Sprintf("--home=%s", nodeDataDir), + fmt.Sprintf("--home=%s", nodeHome), "--log_level=debug", ) startCmd.Stdout = f return &BabylonNodeHandler{ - babylonNode: newBabylonNode(testDir, startCmd, chainID, slashingAddr, covenantPk), + babylonNode: newBabylonNode(testDir, nodeHome, startCmd, chainID, slashingAddr, covenantPk), } } diff --git a/itest/e2e_test.go b/itest/e2e_test.go index 0ce387d..b2ba621 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/babylonlabs-io/covenant-emulator/clientcontroller" "github.com/stretchr/testify/require" ) @@ -48,3 +49,20 @@ func TestCovenantEmulatorLifeCycle(t *testing.T) { require.NoError(t, err) require.Empty(t, res) } + +func TestQueryPendingDelegations(t *testing.T) { + tm, btcPks := StartManagerWithFinalityProvider(t, 1) + defer tm.Stop(t) + + // manually sets the pg to a low value + clientcontroller.MaxPaginationLimit = 2 + + numDels := 3 + for i := 0; i < numDels; i++ { + _ = tm.InsertBTCDelegation(t, btcPks, stakingTime, stakingAmount, false) + } + + dels, err := tm.CovBBNClient.QueryPendingDelegations(uint64(numDels), nil) + require.NoError(t, err) + require.Len(t, dels, numDels) +} diff --git a/itest/test_manager.go b/itest/test_manager.go index dd6b364..8a913aa 100644 --- a/itest/test_manager.go +++ b/itest/test_manager.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "os" + "os/exec" "sync" "testing" "time" @@ -169,6 +170,7 @@ func StartManager(t *testing.T) *TestManager { } tm.WaitForServicesStart(t) + tm.SendToAddr(t, keyInfo.Address.String(), "100000ubbn") return tm } @@ -187,6 +189,23 @@ func (tm *TestManager) WaitForServicesStart(t *testing.T) { t.Logf("Babylon node is started") } +func (tm *TestManager) SendToAddr(t *testing.T, toAddr, amount string) { + sendTx := exec.Command( + "babylond", + "tx", + "bank", + "send", + "node0", + toAddr, + amount, + "--keyring-backend=test", + "--chain-id=chain-test", + fmt.Sprintf("--home=%s", tm.BabylonHandler.babylonNode.nodeHome), + ) + err := sendTx.Start() + require.NoError(t, err) +} + func StartManagerWithFinalityProvider(t *testing.T, n int) (*TestManager, []*btcec.PublicKey) { tm := StartManager(t) @@ -254,6 +273,7 @@ func (tm *TestManager) WaitForNPendingDels(t *testing.T, n int) []*types.Delegat require.Eventually(t, func() bool { dels, err = tm.CovBBNClient.QueryPendingDelegations( tm.CovenanConfig.DelegationLimit, + nil, ) if err != nil { return false diff --git a/testutil/mocks/babylon.go b/testutil/mocks/babylon.go index 6fd3173..1644332 100644 --- a/testutil/mocks/babylon.go +++ b/testutil/mocks/babylon.go @@ -7,6 +7,7 @@ package mocks import ( reflect "reflect" + clientcontroller "github.com/babylonlabs-io/covenant-emulator/clientcontroller" types "github.com/babylonlabs-io/covenant-emulator/types" gomock "github.com/golang/mock/gomock" ) @@ -49,18 +50,18 @@ func (mr *MockClientControllerMockRecorder) Close() *gomock.Call { } // QueryPendingDelegations mocks base method. -func (m *MockClientController) QueryPendingDelegations(limit uint64) ([]*types.Delegation, error) { +func (m *MockClientController) QueryPendingDelegations(limit uint64, filter clientcontroller.FilterFn) ([]*types.Delegation, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryPendingDelegations", limit) + ret := m.ctrl.Call(m, "QueryPendingDelegations", limit, filter) ret0, _ := ret[0].([]*types.Delegation) ret1, _ := ret[1].(error) return ret0, ret1 } // QueryPendingDelegations indicates an expected call of QueryPendingDelegations. -func (mr *MockClientControllerMockRecorder) QueryPendingDelegations(limit interface{}) *gomock.Call { +func (mr *MockClientControllerMockRecorder) QueryPendingDelegations(limit, filter interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryPendingDelegations", reflect.TypeOf((*MockClientController)(nil).QueryPendingDelegations), limit) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryPendingDelegations", reflect.TypeOf((*MockClientController)(nil).QueryPendingDelegations), limit, filter) } // QueryStakingParamsByVersion mocks base method.