Skip to content

Commit

Permalink
Add add-finality-sig cmd (ethereum-optimism#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
gitferry authored Aug 1, 2023
1 parent a5697d3 commit b3c9c9a
Show file tree
Hide file tree
Showing 18 changed files with 713 additions and 186 deletions.
26 changes: 23 additions & 3 deletions bbnclient/bbncontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package babylonclient

import (
"context"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"strings"
"time"

bbnapp "github.com/babylonchain/babylon/app"
Expand All @@ -12,6 +15,7 @@ import (
btclctypes "github.com/babylonchain/babylon/x/btclightclient/types"
btcstakingtypes "github.com/babylonchain/babylon/x/btcstaking/types"
finalitytypes "github.com/babylonchain/babylon/x/finality/types"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil"
ctypes "github.com/cometbft/cometbft/rpc/core/types"
sdkclient "github.com/cosmos/cosmos-sdk/client"
Expand Down Expand Up @@ -217,7 +221,7 @@ func (bc *BabylonController) SubmitJurySig(btcPubKey *types.BIP340PubKey, delPub
}

// SubmitFinalitySig submits the finality signature via a MsgAddVote to Babylon
func (bc *BabylonController) SubmitFinalitySig(btcPubKey *types.BIP340PubKey, blockHeight uint64, blockHash []byte, sig *types.SchnorrEOTSSig) ([]byte, error) {
func (bc *BabylonController) SubmitFinalitySig(btcPubKey *types.BIP340PubKey, blockHeight uint64, blockHash []byte, sig *types.SchnorrEOTSSig) ([]byte, *btcec.PrivateKey, error) {
msg := &finalitytypes.MsgAddFinalitySig{
Signer: bc.MustGetTxSigner(),
ValBtcPk: btcPubKey,
Expand All @@ -228,10 +232,26 @@ func (bc *BabylonController) SubmitFinalitySig(btcPubKey *types.BIP340PubKey, bl

res, _, err := bc.provider.SendMessage(context.Background(), cosmos.NewCosmosMessage(msg), "")
if err != nil {
return nil, err
return nil, nil, err
}

var privKey *btcec.PrivateKey
for _, ev := range res.Events {
if strings.Contains(ev.EventType, "EventSlashedBTCValidator") {
// add this trim because the attribute is a string with quotation marks
extractedBtcSk := strings.Trim(ev.Attributes["extracted_btc_sk"], `'"`)
privKeyBytes, err := base64.StdEncoding.DecodeString(extractedBtcSk)
if err != nil {
bc.logger.Errorf("failed to decode extracted BTC SK: %s", err.Error())
break
}
bc.logger.Debugf("extracted BTC SK: %s", hex.EncodeToString(privKeyBytes))
privKey, _ = btcec.PrivKeyFromBytes(privKeyBytes)
break
}
}

return []byte(res.TxHash), nil
return []byte(res.TxHash), privKey, nil
}

// Currently this is only used for e2e tests, probably does not need to add it into the interface
Expand Down
3 changes: 2 additions & 1 deletion bbnclient/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ type BabylonClient interface {
// it returns tx hash and error
SubmitJurySig(btcPubKey *types.BIP340PubKey, delPubKey *types.BIP340PubKey, sig *types.BIP340Signature) ([]byte, error)
// SubmitFinalitySig submits the finality signature via a MsgAddVote to Babylon
SubmitFinalitySig(btcPubKey *types.BIP340PubKey, blockHeight uint64, blockHash []byte, sig *types.SchnorrEOTSSig) ([]byte, error)
// validator's BTC private key will be returned if the validator is slashed due to double signing
SubmitFinalitySig(btcPubKey *types.BIP340PubKey, blockHeight uint64, blockHash []byte, sig *types.SchnorrEOTSSig) ([]byte, *btcec.PrivateKey, error)

// Note: the following queries are only for PoC

Expand Down
66 changes: 66 additions & 0 deletions cmd/valcli/daemoncmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"fmt"
"strconv"

"github.com/babylonchain/babylon/x/checkpointing/types"
"github.com/urfave/cli"

"github.com/babylonchain/btc-validator/proto"
dc "github.com/babylonchain/btc-validator/service/client"
"github.com/babylonchain/btc-validator/valcfg"
)
Expand All @@ -22,17 +24,22 @@ var daemonCommands = []cli.Command{
createValDaemonCmd,
lsValDaemonCmd,
registerValDaemonCmd,
addFinalitySigDaemonCmd,
},
},
}

const (
valdDaemonAddressFlag = "daemon-address"
keyNameFlag = "key-name"
valBabylonPkFlag = "babylon-pk"
blockHeightFlag = "height"
lastCommitHashFlag = "last-commit-hash"
)

var (
defaultValdDaemonAddress = "127.0.0.1:" + strconv.Itoa(valcfg.DefaultRPCPort)
defaultLastCommitHashStr = "fd903d9baeb3ab1c734ee003de75f676c5a9a8d0574647e5385834d57d3e79ec"
)

var getDaemonInfoCmd = cli.Command{
Expand Down Expand Up @@ -178,3 +185,62 @@ func registerVal(ctx *cli.Context) error {

return nil
}

var addFinalitySigDaemonCmd = cli.Command{
Name: "add-finality-sig",
ShortName: "afs",
Usage: "Send a finality signature to Babylon.",
UsageText: fmt.Sprintf("add-finality-sig --%s []", keyNameFlag),
Flags: []cli.Flag{
cli.StringFlag{
Name: valdDaemonAddressFlag,
Usage: "Full address of the validator daemon in format tcp://<host>:<port>",
Value: defaultValdDaemonAddress,
},
cli.StringFlag{
Name: valBabylonPkFlag,
Usage: "The hex string of the Babylon public key",
Required: true,
},
cli.Uint64Flag{
Name: blockHeightFlag,
Usage: "The height of the Babylon block",
Required: true,
},
cli.StringFlag{
Name: lastCommitHashFlag,
Usage: "The last commit hash of the Babylon block",
Value: defaultLastCommitHashStr,
},
},
Action: addFinalitySig,
}

func addFinalitySig(ctx *cli.Context) error {
daemonAddress := ctx.String(valdDaemonAddressFlag)
rpcClient, cleanUp, err := dc.NewValidatorServiceGRpcClient(daemonAddress)
if err != nil {
return err
}
defer cleanUp()

bbnPk, err := proto.NewBabylonPkFromHex(ctx.String(valBabylonPkFlag))
if err != nil {
return err
}

lch, err := types.NewLastCommitHashFromHex(ctx.String(lastCommitHashFlag))
if err != nil {
return err
}

res, err := rpcClient.AddFinalitySignature(
context.Background(), bbnPk.Key, ctx.Uint64(blockHeightFlag), lch)
if err != nil {
return err
}

printRespJSON(res)

return nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
go.etcd.io/bbolt v1.3.7
go.uber.org/zap v1.24.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
golang.org/x/sync v0.1.0
google.golang.org/grpc v1.56.1
google.golang.org/protobuf v1.31.0
)
Expand Down Expand Up @@ -267,7 +268,6 @@ require (
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
Expand Down
119 changes: 119 additions & 0 deletions itest/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,24 @@
package e2etest

import (
"math/rand"
"os"
"testing"
"time"

"github.com/babylonchain/babylon/testutil/datagen"
btcstakingtypes "github.com/babylonchain/babylon/x/btcstaking/types"
"github.com/babylonchain/babylon/x/finality/types"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"

babylonclient "github.com/babylonchain/btc-validator/bbnclient"
"github.com/babylonchain/btc-validator/proto"
"github.com/babylonchain/btc-validator/service"
"github.com/babylonchain/btc-validator/val"
"github.com/babylonchain/btc-validator/valcfg"
)

Expand Down Expand Up @@ -232,3 +239,115 @@ func TestJurySigSubmission(t *testing.T) {
}, eventuallyWaitTimeOut, eventuallyPollTime)
require.True(t, dels[0].BabylonPk.Equals(delData.DelegatorBabylonKey))
}

// TestDoubleSigning tests the attack scenario where the validator
// sends a finality vote over a conflicting block
// in this case, the BTC private key should be extracted by Babylon
func TestDoubleSigning(t *testing.T) {
tm := StartManager(t, false)
defer tm.Stop(t)

app := tm.Va
newValName := "testingValidator"

// create a validator object
valResult, err := app.CreateValidator(newValName)
require.NoError(t, err)
validator, err := app.GetValidator(valResult.BabylonValidatorPk.Key)
require.NoError(t, err)
require.Equal(t, newValName, validator.KeyName)

// register the validator to Babylon
_, err = app.RegisterValidator(validator.KeyName)
require.NoError(t, err)
validatorAfterReg, err := app.GetValidator(valResult.BabylonValidatorPk.Key)
require.NoError(t, err)
require.Equal(t, validatorAfterReg.Status, proto.ValidatorStatus_REGISTERED)
var queriedValidators []*btcstakingtypes.BTCValidator
require.Eventually(t, func() bool {
queriedValidators, err = tm.BabylonClient.QueryValidators()
if err != nil {
return false
}
return len(queriedValidators) == 1
}, eventuallyWaitTimeOut, eventuallyPollTime)
require.True(t, queriedValidators[0].BabylonPk.Equals(validator.GetBabylonPK()))

// check the public randomness is committed
require.Eventually(t, func() bool {
randPairs, err := app.GetCommittedPubRandPairList(validator.BabylonPk)
if err != nil {
return false
}
return int(tm.Config.NumPubRand) == len(randPairs)
}, eventuallyWaitTimeOut, eventuallyPollTime)

// send a BTC delegation
delData := tm.InsertBTCDelegation(t, validator.MustGetBTCPK(), stakingTime, stakingAmount)

// check the BTC delegation is pending
var dels []*btcstakingtypes.BTCDelegation
require.Eventually(t, func() bool {
dels, err = tm.BabylonClient.QueryPendingBTCDelegations()
if err != nil {
return false
}
return len(dels) == 1
}, eventuallyWaitTimeOut, eventuallyPollTime)
require.True(t, dels[0].BabylonPk.Equals(delData.DelegatorBabylonKey))

// submit Jury sig
_ = tm.AddJurySignature(t, dels[0])

// check the BTC delegation is active
require.Eventually(t, func() bool {
dels, err = tm.BabylonClient.QueryActiveBTCValidatorDelegations(validator.MustGetBIP340BTCPK())
if err != nil {
return false
}
return len(dels) == 1
}, eventuallyWaitTimeOut, eventuallyPollTime)
require.True(t, dels[0].BabylonPk.Equals(delData.DelegatorBabylonKey))

// check there's a block finalized
var blocks []*types.IndexedBlock
require.Eventually(t, func() bool {
blocks, err = tm.BabylonClient.QueryLatestFinalisedBlocks(100)
if err != nil {
return false
}
if len(blocks) == 1 {
return true
}
return false
}, eventuallyWaitTimeOut, eventuallyPollTime)

// attack: manually submit a finality vote over a conflicting block
// to trigger the extraction of validator's private key
r := rand.New(rand.NewSource(time.Now().UnixNano()))
b := &service.BlockInfo{
Height: blocks[0].Height,
LastCommitHash: datagen.GenRandomLastCommitHash(r),
}
_, extractedKey, err := app.SubmitFinalitySignatureForValidator(b, validator)
require.NoError(t, err)
require.NotNil(t, extractedKey)
localKey, err := getBtcPrivKey(app.GetKeyring(), val.KeyName(validator.KeyName))
require.NoError(t, err)
require.True(t, localKey.Key.Equals(&extractedKey.Key) || localKey.Key.Negate().Equals(&extractedKey.Key))
}

func getBtcPrivKey(kr keyring.Keyring, keyName val.KeyName) (*btcec.PrivateKey, error) {
k, err := kr.Key(keyName.GetBtcKeyName())
if err != nil {
return nil, err
}
localKey := k.GetLocal().PrivKey.GetCachedValue()
switch v := localKey.(type) {
case *secp256k1.PrivKey:
privKey, _ := btcec.PrivKeyFromBytes(v.Key)
return privKey, nil
default:
return nil, err
}
}
2 changes: 2 additions & 0 deletions itest/test_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ func (tm *TestManager) InsertBTCDelegation(t *testing.T, valBtcPk *btcec.PublicK

func defaultValidatorConfig(keyringDir, testDir string, isJury bool) *valcfg.Config {
cfg := valcfg.DefaultConfig()

cfg.ValidatorModeConfig.AutoChainScanningMode = false
// babylon configs for sending transactions
cfg.BabylonConfig.KeyDirectory = keyringDir
// need to use this one to send otherwise we will have account sequence mismatch
Expand Down
9 changes: 9 additions & 0 deletions proto/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ func (v *Validator) GetBabylonPK() *secp256k1.PubKey {
}
}

func NewBabylonPkFromHex(hexStr string) (*secp256k1.PubKey, error) {
pkBytes, err := hex.DecodeString(hexStr)
if err != nil {
return nil, err
}

return &secp256k1.PubKey{Key: pkBytes}, nil
}

func (v *Validator) GetBabylonPkHexString() string {
return hex.EncodeToString(v.BabylonPk)
}
Expand Down
Loading

0 comments on commit b3c9c9a

Please sign in to comment.