From e6e82f0275936b391dfc36213780e71d41e6b360 Mon Sep 17 00:00:00 2001 From: Cirrus Gai Date: Mon, 24 Jul 2023 19:05:07 +0800 Subject: [PATCH] Add Jury sig submission into the main event loop (#31) --- bbnclient/bbncontroller.go | 23 +++++- bbnclient/interface.go | 5 +- go.mod | 4 +- go.sum | 4 +- service/app.go | 143 +++++++++++++++++++++++++++++++++--- service/app_test.go | 35 ++++----- service/chain_poller.go | 2 - testutil/mocks/bbnclient.go | 19 +++-- valcfg/jury.go | 13 +++- 9 files changed, 194 insertions(+), 54 deletions(-) diff --git a/bbnclient/bbncontroller.go b/bbnclient/bbncontroller.go index a7f967cc5444d..f95ca100eb891 100644 --- a/bbnclient/bbncontroller.go +++ b/bbnclient/bbncontroller.go @@ -167,11 +167,26 @@ func (bc *BabylonController) QueryHeightWithLastPubRand(btcPubKey *types.BIP340P return ks[0], nil } -// QueryShouldSubmitJurySigs queries if there's a list of delegations that the Jury should submit Jury sigs to +// QueryPendingBTCDelegations queries BTC delegations that need a Jury sig // it is only used when the program is running in Jury mode -// it returns a list of public keys used for delegations -func (bc *BabylonController) QueryShouldSubmitJurySigs(btcPubKey *types.BIP340PubKey) (bool, []*types.BIP340PubKey, error) { - panic("implement me") +func (bc *BabylonController) QueryPendingBTCDelegations() ([]*btcstakingtypes.BTCDelegation, error) { + var delegations []*btcstakingtypes.BTCDelegation + + ctx, cancel := getQueryContext(bc.timeout) + defer cancel() + + clientCtx := sdkclient.Context{Client: bc.rpcClient.QueryClient.RPCClient} + queryClient := btcstakingtypes.NewQueryClient(clientCtx) + + // query all the unsigned delegations + queryRequest := &btcstakingtypes.QueryPendingBTCDelegationsRequest{} + res, err := queryClient.PendingBTCDelegations(ctx, queryRequest) + if err != nil { + return nil, fmt.Errorf("failed to query BTC delegations") + } + delegations = append(delegations, res.BtcDelegations...) + + return delegations, nil } // QueryShouldValidatorVote asks Babylon if the validator should submit a finality sig for the given block height diff --git a/bbnclient/interface.go b/bbnclient/interface.go index 2a2f90f6a4350..ded710acecf29 100644 --- a/bbnclient/interface.go +++ b/bbnclient/interface.go @@ -23,10 +23,9 @@ type BabylonClient interface { // Note: the following queries are only for PoC // QueryHeightWithLastPubRand queries the height of the last block with public randomness QueryHeightWithLastPubRand(btcPubKey *types.BIP340PubKey) (uint64, error) - // QueryShouldSubmitJurySigs queries if there's a list of delegations that the Jury should submit Jury sigs to + // QueryShouldSubmitJurySigs queries BTC delegations that need a Jury signature // it is only used when the program is running in Jury mode - // it returns a list of public keys used for delegations - QueryShouldSubmitJurySigs(btcPubKey *types.BIP340PubKey) (bool, []*types.BIP340PubKey, error) + QueryPendingBTCDelegations() ([]*btcstakingtypes.BTCDelegation, error) // QueryShouldValidatorVote asks Babylon if the validator should submit a finality sig for the given block height QueryShouldValidatorVote(btcPubKey *types.BIP340PubKey, blockHeight uint64) (bool, error) diff --git a/go.mod b/go.mod index 7c9f04c854311..6f39aa3a07562 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/stretchr/testify v1.8.4 github.com/urfave/cli v1.22.14 go.etcd.io/bbolt v1.3.7 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 google.golang.org/grpc v1.56.1 google.golang.org/protobuf v1.31.0 ) @@ -257,7 +258,6 @@ require ( go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.24.0 // indirect golang.org/x/crypto v0.7.0 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect golang.org/x/mod v0.8.0 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/oauth2 v0.8.0 // indirect @@ -296,7 +296,7 @@ require ( ) replace ( - github.com/babylonchain/babylon => github.com/babylonchain/babylon-private v0.0.0-20230722140238-aaa0aad2ae01 + github.com/babylonchain/babylon => github.com/babylonchain/babylon-private v0.0.0-20230724085949-744ada452cc6 github.com/cosmos/ibc-go/v7 => github.com/babylonchain/ibc-go/v7 v7.0.0-20230324085744-4d6a0d2c0fcf github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 diff --git a/go.sum b/go.sum index a9a91e0aecc38..8faef418c5180 100644 --- a/go.sum +++ b/go.sum @@ -267,8 +267,8 @@ github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX github.com/aws/aws-sdk-go v1.44.203 h1:pcsP805b9acL3wUqa4JR2vg1k2wnItkDYNvfmcy6F+U= github.com/aws/aws-sdk-go v1.44.203/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/babylonchain/babylon-private v0.0.0-20230722140238-aaa0aad2ae01 h1:uQtvrEjoeVtkiehFxHhgXzoEH3m4hc0to7NitVo9anQ= -github.com/babylonchain/babylon-private v0.0.0-20230722140238-aaa0aad2ae01/go.mod h1:xH1wcwOFLiu5nCZeaVDnG82ALnNEId9A/GBJeDdRckg= +github.com/babylonchain/babylon-private v0.0.0-20230724085949-744ada452cc6 h1:FioqR0PGxhpkRJmCvJePm/A/Z5MisaKcYj4Tjf2imgw= +github.com/babylonchain/babylon-private v0.0.0-20230724085949-744ada452cc6/go.mod h1:xH1wcwOFLiu5nCZeaVDnG82ALnNEId9A/GBJeDdRckg= github.com/babylonchain/ibc-go/v7 v7.0.0-20230324085744-4d6a0d2c0fcf h1:NJU3YuruPqV8w6/y45Zsb8FudcWSkTBugdpTT7kJmjw= github.com/babylonchain/ibc-go/v7 v7.0.0-20230324085744-4d6a0d2c0fcf/go.mod h1:BFh8nKWjr5zeR2OZfhkzdgDzj1+KjRn3aJLpwapStj8= github.com/babylonchain/rpc-client v0.7.0 h1:koA7evcI39BjCgq+fu9MAIs+TQYCnEg56iPeL78Mihw= diff --git a/service/app.go b/service/app.go index fd847b20bc3ef..9e36d3e0c6db7 100644 --- a/service/app.go +++ b/service/app.go @@ -12,7 +12,6 @@ import ( btcstakingtypes "github.com/babylonchain/babylon/x/btcstaking/types" ftypes "github.com/babylonchain/babylon/x/finality/types" "github.com/btcsuite/btcd/btcec/v2" - "github.com/btcsuite/btcd/chaincfg" "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" "github.com/sirupsen/logrus" @@ -40,9 +39,11 @@ type ValidatorApp struct { createValidatorRequestChan chan *createValidatorRequest registerValidatorRequestChan chan *registerValidatorRequest commitPubRandRequestChan chan *commitPubRandRequest + addJurySigRequestChan chan *addJurySigRequest validatorRegisteredEventChan chan *validatorRegisteredEvent pubRandCommittedEventChan chan *pubRandCommittedEvent + jurySigAddedEventChan chan *jurySigAddedEvent } func NewValidatorAppFromConfig( @@ -83,8 +84,10 @@ func NewValidatorAppFromConfig( createValidatorRequestChan: make(chan *createValidatorRequest), registerValidatorRequestChan: make(chan *registerValidatorRequest), commitPubRandRequestChan: make(chan *commitPubRandRequest), + addJurySigRequestChan: make(chan *addJurySigRequest), pubRandCommittedEventChan: make(chan *pubRandCommittedEvent), validatorRegisteredEventChan: make(chan *validatorRegisteredEvent), + jurySigAddedEventChan: make(chan *jurySigAddedEvent), }, nil } @@ -143,6 +146,25 @@ type pubRandCommittedEvent struct { successResponse chan *commitPubRandResponse } +type addJurySigRequest struct { + bbnPubKey *secp256k1.PubKey + valBtcPk *types.BIP340PubKey + delBtcPk *types.BIP340PubKey + sig *types.BIP340Signature + errResponse chan error + successResponse chan *addJurySigResponse +} + +type addJurySigResponse struct { + txHash []byte +} + +type jurySigAddedEvent struct { + bbnPubKey *secp256k1.PubKey + txHash []byte + successResponse chan *addJurySigResponse +} + type CreateValidatorResult struct { BtcValidatorPk btcec.PublicKey BabylonValidatorPk secp256k1.PubKey @@ -245,13 +267,31 @@ func (app *ValidatorApp) AddJurySignature(btcDel *bstypes.BTCDelegation) ([]byte stakingMsgTx, stakingTx.StakingScript, juryPrivKey, - &chaincfg.SimNetParams, + &app.config.JuryModeConfig.ActiveNetParams, ) if err != nil { return nil, err } - return app.bc.SubmitJurySig(btcDel.ValBtcPk, btcDel.BtcPk, jurySig) + request := &addJurySigRequest{ + bbnPubKey: btcDel.BabylonPk, + valBtcPk: btcDel.ValBtcPk, + delBtcPk: btcDel.BtcPk, + sig: jurySig, + errResponse: make(chan error), + successResponse: make(chan *addJurySigResponse), + } + + app.addJurySigRequestChan <- request + + select { + case err := <-request.errResponse: + return nil, err + case successResponse := <-request.successResponse: + return successResponse.txHash, nil + case <-app.quit: + return nil, fmt.Errorf("validator app is shutting down") + } } func (app *ValidatorApp) getJuryPrivKey() (*btcec.PrivateKey, error) { @@ -392,14 +432,14 @@ func (app *ValidatorApp) Start() error { return } - app.wg.Add(2) + app.wg.Add(3) go app.handleSentToBabylonLoop() go app.eventLoop() - if !app.config.JuryMode { - app.wg.Add(1) + if !app.IsJury() { go app.validatorSubmissionLoop() + } else { + go app.jurySigSubmissionLoop() } - // TODO add another loop in which the app asks Babylon whether there are any delegations need jury sig if the program is running in Jury mode }) return startErr @@ -442,6 +482,10 @@ func (app *ValidatorApp) CreateValidator(keyName string) (*CreateValidatorResult } } +func (app *ValidatorApp) IsJury() bool { + return app.config.JuryMode +} + func (app *ValidatorApp) ListValidators() ([]*proto.Validator, error) { return app.vs.ListValidators() } @@ -484,7 +528,7 @@ func (app *ValidatorApp) handleCreateValidatorRequest(req *createValidatorReques babylonPubKey := validator.GetBabylonPK() app.logger.Info("successfully created validator") - app.logger.WithFields(logrus.Fields{ // TODO: use hex format + app.logger.WithFields(logrus.Fields{ "btc_pub_key": hex.EncodeToString(btcPubKey.SerializeCompressed()), "babylon_pub_key": hex.EncodeToString(babylonPubKey.Key), }).Debug("created validator") @@ -495,6 +539,54 @@ func (app *ValidatorApp) handleCreateValidatorRequest(req *createValidatorReques }, nil } +func (app *ValidatorApp) getPendingDelegationsForAll() ([]*btcstakingtypes.BTCDelegation, error) { + var delegations []*btcstakingtypes.BTCDelegation + + dels, err := app.bc.QueryPendingBTCDelegations() + if err != nil { + return nil, fmt.Errorf("failed to get pending BTC delegations: %w", err) + } + delegations = append(delegations, dels...) + + return delegations, nil +} + +// jurySigSubmissionLoop is the reactor to submit Jury signature for pending BTC delegations +func (app *ValidatorApp) jurySigSubmissionLoop() { + defer app.wg.Done() + + interval := app.config.JuryModeConfig.QueryInterval + jurySigTicker := time.NewTicker(interval) + + for { + select { + case <-jurySigTicker.C: + dels, err := app.getPendingDelegationsForAll() + if err != nil { + app.logger.WithFields(logrus.Fields{ + "err": err, + }).Error("failed to get pending delegations") + continue + } + + for _, d := range dels { + _, err := app.AddJurySignature(d) + if err != nil { + app.logger.WithFields(logrus.Fields{ + "err": err, + "del_btc_pk": d.BtcPk, + }).Error("failed to submit Jury sig to the Bitcoin delegation") + } + } + + case <-app.quit: + return + } + } + +} + +// validatorSubmissionLoop is the reactor to submit finality signature and public randomness func (app *ValidatorApp) validatorSubmissionLoop() { defer app.wg.Done() @@ -502,6 +594,8 @@ func (app *ValidatorApp) validatorSubmissionLoop() { for { select { + case <-app.poller.GetBlockInfoChan(): + // TODO finality sig submission case <-commitRandTicker.C: lastBlock, err := app.GetCurrentBbnBlock() if err != nil { @@ -611,6 +705,15 @@ func (app *ValidatorApp) eventLoop() { ev.successResponse <- &commitPubRandResponse{ txHash: ev.txHash, } + case ev := <-app.jurySigAddedEventChan: + // TODO do we assume the delegator is also a BTC validator? + // if so, do we want to change its status to ACTIVE here? + // if not, maybe we can remove the handler of this event + + // return to the caller + ev.successResponse <- &addJurySigResponse{ + txHash: ev.txHash, + } case <-app.quit: return @@ -665,7 +768,7 @@ func (app *ValidatorApp) handleSentToBabylonLoop() { "err": err, "btcPubKey": req.valBtcPk, "startHeight": req.startingHeight, - }) + }).Error("failed to commit public randomness") req.errResponse <- err continue } @@ -683,6 +786,28 @@ func (app *ValidatorApp) handleSentToBabylonLoop() { // the commit successResponse: req.successResponse, } + case req := <-app.addJurySigRequestChan: + // TODO: we should add some retry mechanism or we can have a health checker to check the connection periodically + tx, err := app.bc.SubmitJurySig(req.valBtcPk, req.delBtcPk, req.sig) + if err != nil { + app.logger.WithFields(logrus.Fields{ + "err": err, + "valBtcPubKey": req.valBtcPk, + "delBtcPubKey": req.delBtcPk, + }).Error("failed to submit Jury signature") + req.errResponse <- err + continue + } + + app.logger.WithField("delBtcPk", req.delBtcPk).Info("successfully submit Jury sig over Bitcoin delegation to Babylon") + + app.jurySigAddedEventChan <- &jurySigAddedEvent{ + bbnPubKey: req.bbnPubKey, + txHash: tx, + // pass the channel to the event so that we can send the response to the user which requested + // the registration + successResponse: req.successResponse, + } case <-app.quit: return diff --git a/service/app_test.go b/service/app_test.go index 7991d19ef320e..3a8126ba854f5 100644 --- a/service/app_test.go +++ b/service/app_test.go @@ -217,6 +217,13 @@ func FuzzAddJurySig(f *testing.F) { app, err := service.NewValidatorAppFromConfig(&cfg, logrus.New(), mockBabylonClient) require.NoError(t, err) + err = app.Start() + require.NoError(t, err) + defer func() { + err = app.Stop() + require.NoError(t, err) + }() + // create a validator object and save it to db keyName := testutil.GenRandomHexStr(r, 4) kc, err := val.NewKeyringControllerWithKeyring(app.GetKeyring(), keyName) @@ -233,14 +240,12 @@ func FuzzAddJurySig(f *testing.F) { require.NoError(t, err) // create a Jury key pair in the keyring - juryKeyName := testutil.GenRandomHexStr(r, 4) - juryKc, err := val.NewKeyringControllerWithKeyring(app.GetKeyring(), juryKeyName) + juryKc, err := val.NewKeyringControllerWithKeyring(app.GetKeyring(), cfg.JuryModeConfig.JuryKeyName) require.NoError(t, err) jurPk, err := juryKc.CreateJuryKey() require.NoError(t, err) require.NotNil(t, jurPk) cfg.JuryMode = true - cfg.JuryModeConfig.JuryKeyName = juryKeyName // generate BTC delegation slashingAddr, err := datagen.GenRandomBTCAddress(r, &chaincfg.SimNetParams) @@ -251,31 +256,23 @@ func FuzzAddJurySig(f *testing.F) { stakingValue := int64(2 * 10e8) stakingTx, slashingTx, err := datagen.GenBTCStakingSlashingTx(r, delSK, btcPk, jurPk, stakingTimeBlocks, stakingValue, slashingAddr) require.NoError(t, err) - stakingMsgTx, err := stakingTx.ToMsgTx() - require.NoError(t, err) - // random Babylon SK delBabylonSK, delBabylonPK, err := datagen.GenRandomSecp256k1KeyPair(r) require.NoError(t, err) pop, err := bstypes.NewPoP(delBabylonSK, delSK) require.NoError(t, err) - delegatorSig, err := slashingTx.Sign( - stakingMsgTx, - stakingTx.StakingScript, - delSK, - &chaincfg.SimNetParams, - ) require.NoError(t, err) delegation := &bstypes.BTCDelegation{ - ValBtcPk: btcPkBIP340, - BtcPk: types.NewBIP340PubKeyFromBTCPK(delPK), - BabylonPk: delBabylonPK.(*secp256k1.PubKey), - Pop: pop, - StakingTx: stakingTx, - SlashingTx: slashingTx, - DelegatorSig: delegatorSig, + ValBtcPk: btcPkBIP340, + BtcPk: types.NewBIP340PubKeyFromBTCPK(delPK), + BabylonPk: delBabylonPK.(*secp256k1.PubKey), + Pop: pop, + StakingTx: stakingTx, + SlashingTx: slashingTx, } expectedTxHash := testutil.GenRandomByteArray(r, 32) + mockBabylonClient.EXPECT().QueryPendingBTCDelegations(). + Return([]*bstypes.BTCDelegation{delegation}, nil).AnyTimes() mockBabylonClient.EXPECT().SubmitJurySig(delegation.ValBtcPk, delegation.BtcPk, gomock.Any()). Return(expectedTxHash, nil).AnyTimes() txHash, err := app.AddJurySignature(delegation) diff --git a/service/chain_poller.go b/service/chain_poller.go index 929db21a686ef..5be1f1d475ae9 100644 --- a/service/chain_poller.go +++ b/service/chain_poller.go @@ -177,7 +177,6 @@ func (cp *ChainPoller) pollChain(initialState PollerState) { var state = initialState ticker := time.NewTicker(cp.cfg.PollInterval) - defer ticker.Stop() for { // TODO: Handlig of request cancellation, as otherwise shutdown will be blocked @@ -228,7 +227,6 @@ func (cp *ChainPoller) pollChain(initialState PollerState) { select { case <-ticker.C: - case <-cp.quit: return } diff --git a/testutil/mocks/bbnclient.go b/testutil/mocks/bbnclient.go index 47472eda107ae..54cfc84c21765 100644 --- a/testutil/mocks/bbnclient.go +++ b/testutil/mocks/bbnclient.go @@ -112,20 +112,19 @@ func (mr *MockBabylonClientMockRecorder) QueryNodeStatus() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryNodeStatus", reflect.TypeOf((*MockBabylonClient)(nil).QueryNodeStatus)) } -// QueryShouldSubmitJurySigs mocks base method. -func (m *MockBabylonClient) QueryShouldSubmitJurySigs(btcPubKey *types.BIP340PubKey) (bool, []*types.BIP340PubKey, error) { +// QueryPendingBTCDelegations mocks base method. +func (m *MockBabylonClient) QueryPendingBTCDelegations() ([]*types0.BTCDelegation, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "QueryShouldSubmitJurySigs", btcPubKey) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].([]*types.BIP340PubKey) - ret2, _ := ret[2].(error) - return ret0, ret1, ret2 + ret := m.ctrl.Call(m, "QueryPendingBTCDelegations") + ret0, _ := ret[0].([]*types0.BTCDelegation) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// QueryShouldSubmitJurySigs indicates an expected call of QueryShouldSubmitJurySigs. -func (mr *MockBabylonClientMockRecorder) QueryShouldSubmitJurySigs(btcPubKey interface{}) *gomock.Call { +// QueryPendingBTCDelegations indicates an expected call of QueryPendingBTCDelegations. +func (mr *MockBabylonClientMockRecorder) QueryPendingBTCDelegations() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryShouldSubmitJurySigs", reflect.TypeOf((*MockBabylonClient)(nil).QueryShouldSubmitJurySigs), btcPubKey) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryPendingBTCDelegations", reflect.TypeOf((*MockBabylonClient)(nil).QueryPendingBTCDelegations)) } // QueryShouldValidatorVote mocks base method. diff --git a/valcfg/jury.go b/valcfg/jury.go index 44e5ba73e7b93..4e05258004292 100644 --- a/valcfg/jury.go +++ b/valcfg/jury.go @@ -1,16 +1,22 @@ package valcfg -import "github.com/btcsuite/btcd/chaincfg" +import ( + "time" + + "github.com/btcsuite/btcd/chaincfg" +) var ( defaultBitcoinNetwork = "simnet" defaultJuryKeyName = "jury-key" defaultActiveNetParams = chaincfg.SimNetParams + defaultQueryInterval = 5 * time.Second ) type JuryConfig struct { - JuryKeyName string `long:"jurykeyname" description:"The key name of the Jury if the program is running in Jury mode"` - BitcoinNetwork string `long:"bitcoinnetwork" description:"Bitcoin network to run on" choice:"regtest" choice:"testnet" choice:"simnet" choice:"signet"` + JuryKeyName string `long:"jurykeyname" description:"The key name of the Jury if the program is running in Jury mode"` + BitcoinNetwork string `long:"bitcoinnetwork" description:"Bitcoin network to run on" choice:"regtest" choice:"testnet" choice:"simnet" choice:"signet"` + QueryInterval time.Duration `long:"queryinterval" description:"The interval between each query for pending BTC delegations"` ActiveNetParams chaincfg.Params } @@ -20,5 +26,6 @@ func DefaultJuryConfig() JuryConfig { JuryKeyName: defaultJuryKeyName, BitcoinNetwork: defaultBitcoinNetwork, ActiveNetParams: defaultActiveNetParams, + QueryInterval: defaultQueryInterval, } }