diff --git a/CHANGELOG.md b/CHANGELOG.md index 56bf1350..8d2adaf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ## Unreleased +### Bug Fixes + +* [#199](https://github.com/babylonlabs-io/finality-provider/pull/199) EOTS signing for multiple finality providers + ## v0.13.0 ### Improvements diff --git a/eotsmanager/localmanager.go b/eotsmanager/localmanager.go index 45bd5ee5..7cd66c70 100644 --- a/eotsmanager/localmanager.go +++ b/eotsmanager/localmanager.go @@ -4,10 +4,11 @@ import ( "bytes" "encoding/hex" "fmt" - "github.com/babylonlabs-io/finality-provider/metrics" "strings" "sync" + "github.com/babylonlabs-io/finality-provider/metrics" + "github.com/babylonlabs-io/babylon/crypto/eots" bbntypes "github.com/babylonlabs-io/babylon/types" "github.com/btcsuite/btcd/btcec/v2" @@ -193,22 +194,32 @@ func (lm *LocalEOTSManager) CreateRandomnessPairList(fpPk []byte, chainID []byte return prList, nil } -func (lm *LocalEOTSManager) SignEOTS(fpPk []byte, chainID []byte, msg []byte, height uint64, passphrase string) (*btcec.ModNScalar, error) { - record, found, err := lm.es.GetSignRecord(height) +func (lm *LocalEOTSManager) SignEOTS(eotsPk []byte, chainID []byte, msg []byte, height uint64, passphrase string) (*btcec.ModNScalar, error) { + record, found, err := lm.es.GetSignRecord(eotsPk, chainID, height) if err != nil { return nil, fmt.Errorf("error getting sign record: %w", err) - } else if found { - if bytes.Equal(msg, record.BlockHash) { + } + + if found { + if bytes.Equal(msg, record.Msg) { var s btcec.ModNScalar s.SetByteSlice(record.Signature) + lm.logger.Warn( + "duplicate sign requested", + zap.String("eots_pk", hex.EncodeToString(eotsPk)), + zap.String("hash", hex.EncodeToString(msg)), + zap.Uint64("height", height), + zap.String("chainID", string(chainID)), + ) + return &s, nil } lm.logger.Error( - "double sign error protection", - zap.String("fp", hex.EncodeToString(fpPk)), - zap.String("msg", hex.EncodeToString(msg)), + "double sign requested", + zap.String("eots_pk", hex.EncodeToString(eotsPk)), + zap.String("hash", hex.EncodeToString(msg)), zap.Uint64("height", height), zap.String("chainID", string(chainID)), ) @@ -216,27 +227,27 @@ func (lm *LocalEOTSManager) SignEOTS(fpPk []byte, chainID []byte, msg []byte, he return nil, eotstypes.ErrDoubleSign } - privRand, _, err := lm.getRandomnessPair(fpPk, chainID, height, passphrase) + privRand, _, err := lm.getRandomnessPair(eotsPk, chainID, height, passphrase) if err != nil { return nil, fmt.Errorf("failed to get private randomness: %w", err) } - privKey, err := lm.getEOTSPrivKey(fpPk, passphrase) + privKey, err := lm.getEOTSPrivKey(eotsPk, passphrase) if err != nil { return nil, fmt.Errorf("failed to get EOTS private key: %w", err) } // Update metrics - lm.metrics.IncrementEotsFpTotalEotsSignCounter(hex.EncodeToString(fpPk)) - lm.metrics.SetEotsFpLastEotsSignHeight(hex.EncodeToString(fpPk), float64(height)) + lm.metrics.IncrementEotsFpTotalEotsSignCounter(hex.EncodeToString(eotsPk)) + lm.metrics.SetEotsFpLastEotsSignHeight(hex.EncodeToString(eotsPk), float64(height)) signedBytes, err := eots.Sign(privKey, privRand, msg) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to sign eots") } b := signedBytes.Bytes() - if err := lm.es.SaveSignRecord(height, msg, fpPk, b[:]); err != nil { + if err := lm.es.SaveSignRecord(height, chainID, msg, eotsPk, b[:]); err != nil { return nil, fmt.Errorf("failed to save signing record: %w", err) } diff --git a/eotsmanager/localmanager_test.go b/eotsmanager/localmanager_test.go index 08d8e6cb..0f3672d2 100644 --- a/eotsmanager/localmanager_test.go +++ b/eotsmanager/localmanager_test.go @@ -6,7 +6,9 @@ import ( "path/filepath" "testing" + "github.com/babylonlabs-io/babylon/crypto/eots" "github.com/babylonlabs-io/babylon/testutil/datagen" + bbntypes "github.com/babylonlabs-io/babylon/types" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -98,7 +100,6 @@ func FuzzSignRecord(f *testing.F) { f.Fuzz(func(t *testing.T, seed int64) { r := rand.New(rand.NewSource(seed)) - fpName := testutil.GenRandomHexStr(r, 4) homeDir := filepath.Join(t.TempDir(), "eots-home") eotsCfg := eotscfg.DefaultConfigWithHomePath(homeDir) dbBackend, err := eotsCfg.DatabaseConfig.GetDBBackend() @@ -111,29 +112,41 @@ func FuzzSignRecord(f *testing.F) { lm, err := eotsmanager.NewLocalEOTSManager(homeDir, eotsCfg.KeyringBackend, dbBackend, zap.NewNop()) require.NoError(t, err) - fpPk, err := lm.CreateKey(fpName, passphrase, hdPath) - require.NoError(t, err) - - chainID := datagen.GenRandomByteArray(r, 10) startHeight := datagen.RandomInt(r, 100) - num := r.Intn(10) + 1 - pubRandList, err := lm.CreateRandomnessPairList(fpPk, chainID, startHeight, uint32(num), passphrase) - require.NoError(t, err) - require.Len(t, pubRandList, num) + numRand := r.Intn(10) + 1 msg := datagen.GenRandomByteArray(r, 32) + numFps := 3 + for i := 0; i < numFps; i++ { + chainID := datagen.GenRandomByteArray(r, 10) + fpName := testutil.GenRandomHexStr(r, 4) + fpPk, err := lm.CreateKey(fpName, passphrase, hdPath) + require.NoError(t, err) + pubRandList, err := lm.CreateRandomnessPairList(fpPk, chainID, startHeight, uint32(numRand), passphrase) + require.NoError(t, err) + require.Len(t, pubRandList, numRand) - sig, err := lm.SignEOTS(fpPk, chainID, msg, startHeight, passphrase) - require.NoError(t, err) - require.NotNil(t, sig) + sig, err := lm.SignEOTS(fpPk, chainID, msg, startHeight, passphrase) + require.NoError(t, err) + require.NotNil(t, sig) - // we expect return from db - sig2, err := lm.SignEOTS(fpPk, chainID, msg, startHeight, passphrase) - require.NoError(t, err) - require.Equal(t, sig, sig2) + eotsPk, err := bbntypes.NewBIP340PubKey(fpPk) + require.NoError(t, err) + + err = eots.Verify(eotsPk.MustToBTCPK(), pubRandList[0], msg, sig) + require.NoError(t, err) + + // we expect return from db + sig2, err := lm.SignEOTS(fpPk, chainID, msg, startHeight, passphrase) + require.NoError(t, err) + require.Equal(t, sig, sig2) - // same height diff msg - _, err = lm.SignEOTS(fpPk, chainID, datagen.GenRandomByteArray(r, 32), startHeight, passphrase) - require.ErrorIs(t, err, types.ErrDoubleSign) + err = eots.Verify(eotsPk.MustToBTCPK(), pubRandList[0], msg, sig2) + require.NoError(t, err) + + // same height diff msg + _, err = lm.SignEOTS(fpPk, chainID, datagen.GenRandomByteArray(r, 32), startHeight, passphrase) + require.ErrorIs(t, err, types.ErrDoubleSign) + } }) } diff --git a/eotsmanager/proto/signstore.pb.go b/eotsmanager/proto/signstore.pb.go index ebf6d8fc..142d3d73 100644 --- a/eotsmanager/proto/signstore.pb.go +++ b/eotsmanager/proto/signstore.pb.go @@ -21,15 +21,18 @@ const ( ) // SigningRecord represents a record of a signing operation. +// it is keyed by (public_key || chain_id || height) type SigningRecord struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - BlockHash []byte `protobuf:"bytes,1,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"` // The hash of the block. - PublicKey []byte `protobuf:"bytes,2,opt,name=public_key,json=publicKey,proto3" json:"public_key,omitempty"` // The public key used for signing. - Signature []byte `protobuf:"bytes,3,opt,name=signature,proto3" json:"signature,omitempty"` // The signature of the block. - Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // The timestamp of the signing operation, in Unix seconds. + // msg is the message that the signature is signed over + Msg []byte `protobuf:"bytes,1,opt,name=msg,proto3" json:"msg,omitempty"` + // eots_sig is the eots signature + EotsSig []byte `protobuf:"bytes,2,opt,name=eots_sig,json=eotsSig,proto3" json:"eots_sig,omitempty"` + // timestamp is the timestamp of the signing operation, in Unix seconds. + Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` } func (x *SigningRecord) Reset() { @@ -64,23 +67,16 @@ func (*SigningRecord) Descriptor() ([]byte, []int) { return file_signstore_proto_rawDescGZIP(), []int{0} } -func (x *SigningRecord) GetBlockHash() []byte { +func (x *SigningRecord) GetMsg() []byte { if x != nil { - return x.BlockHash + return x.Msg } return nil } -func (x *SigningRecord) GetPublicKey() []byte { +func (x *SigningRecord) GetEotsSig() []byte { if x != nil { - return x.PublicKey - } - return nil -} - -func (x *SigningRecord) GetSignature() []byte { - if x != nil { - return x.Signature + return x.EotsSig } return nil } @@ -96,15 +92,12 @@ var File_signstore_proto protoreflect.FileDescriptor var file_signstore_proto_rawDesc = []byte{ 0x0a, 0x0f, 0x73, 0x69, 0x67, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x89, 0x01, 0x0a, 0x0d, 0x53, 0x69, 0x67, - 0x6e, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, - 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, - 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x70, - 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, - 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, - 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5a, 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, + 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x19, 0x0a, 0x08, 0x65, + 0x6f, 0x74, 0x73, 0x5f, 0x73, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x65, + 0x6f, 0x74, 0x73, 0x53, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x61, 0x62, 0x79, 0x6c, 0x6f, 0x6e, 0x6c, 0x61, 0x62, 0x73, 0x2d, 0x69, 0x6f, 0x2f, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, diff --git a/eotsmanager/proto/signstore.proto b/eotsmanager/proto/signstore.proto index 406b281f..a20f1a04 100644 --- a/eotsmanager/proto/signstore.proto +++ b/eotsmanager/proto/signstore.proto @@ -5,9 +5,12 @@ package proto; option go_package = "github.com/babylonlabs-io/finality-provider/eotsmanager/proto"; // SigningRecord represents a record of a signing operation. +// it is keyed by (chain_id || public_key || height) message SigningRecord { - bytes block_hash = 1; // The hash of the block. - bytes public_key = 2; // The public key used for signing. - bytes signature = 3; // The signature of the block. - int64 timestamp = 4; // The timestamp of the signing operation, in Unix seconds. -} \ No newline at end of file + // msg is the message that the signature is signed over + bytes msg = 1; + // eots_sig is the eots signature + bytes eots_sig = 2; + // timestamp is the timestamp of the signing operation, in Unix seconds. + int64 timestamp = 3; +} diff --git a/eotsmanager/store/eotsstore.go b/eotsmanager/store/eotsstore.go index 0af6ee17..c197db67 100644 --- a/eotsmanager/store/eotsstore.go +++ b/eotsmanager/store/eotsstore.go @@ -1,13 +1,14 @@ package store import ( - "encoding/binary" "errors" "fmt" - "github.com/babylonlabs-io/finality-provider/eotsmanager/proto" - pm "google.golang.org/protobuf/proto" "time" + pm "google.golang.org/protobuf/proto" + + "github.com/babylonlabs-io/finality-provider/eotsmanager/proto" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcwallet/walletdb" @@ -107,11 +108,12 @@ func (s *EOTSStore) GetEOTSKeyName(pk []byte) (string, error) { func (s *EOTSStore) SaveSignRecord( height uint64, - blockHash []byte, + chainID []byte, + msg []byte, publicKey []byte, signature []byte, ) error { - key := uint64ToBytes(height) + key := getSignRecordKey(chainID, publicKey, height) return kvdb.Batch(s.db, func(tx kvdb.RwTx) error { bucket := tx.ReadWriteBucket(signRecordBucketName) @@ -124,9 +126,8 @@ func (s *EOTSStore) SaveSignRecord( } signRecord := &proto.SigningRecord{ - BlockHash: blockHash, - PublicKey: publicKey, - Signature: signature, + Msg: msg, + EotsSig: signature, Timestamp: time.Now().UnixMilli(), } @@ -139,8 +140,8 @@ func (s *EOTSStore) SaveSignRecord( }) } -func (s *EOTSStore) GetSignRecord(height uint64) (*SigningRecord, bool, error) { - key := uint64ToBytes(height) +func (s *EOTSStore) GetSignRecord(eotsPk, chainID []byte, height uint64) (*SigningRecord, bool, error) { + key := getSignRecordKey(chainID, eotsPk, height) protoRes := &proto.SigningRecord{} err := s.db.View(func(tx kvdb.RTx) error { @@ -169,10 +170,3 @@ func (s *EOTSStore) GetSignRecord(height uint64) (*SigningRecord, bool, error) { return res, true, nil } - -// Converts an uint64 value to a byte slice. -func uint64ToBytes(v uint64) []byte { - var buf [8]byte - binary.BigEndian.PutUint64(buf[:], v) - return buf[:] -} diff --git a/eotsmanager/store/eotsstore_test.go b/eotsmanager/store/eotsstore_test.go index fdc76df1..bf160a18 100644 --- a/eotsmanager/store/eotsstore_test.go +++ b/eotsmanager/store/eotsstore_test.go @@ -1,14 +1,13 @@ package store_test import ( - "github.com/babylonlabs-io/babylon/testutil/datagen" - "github.com/babylonlabs-io/finality-provider/eotsmanager/proto" - "github.com/btcsuite/btcd/btcec/v2/schnorr" - "github.com/stretchr/testify/require" "math/rand" "os" "testing" - "time" + + "github.com/babylonlabs-io/babylon/testutil/datagen" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/stretchr/testify/require" "github.com/babylonlabs-io/finality-provider/eotsmanager/config" "github.com/babylonlabs-io/finality-provider/eotsmanager/store" @@ -88,41 +87,40 @@ func FuzzSignStore(f *testing.F) { require.NoError(t, err) }() - expectedRecord := proto.SigningRecord{ - BlockHash: testutil.GenRandomByteArray(r, 32), - PublicKey: testutil.GenRandomByteArray(r, 32), - Signature: testutil.GenRandomByteArray(r, 32), - Timestamp: time.Now().UnixMilli(), - } expectedHeight := r.Uint64() + pk := testutil.GenRandomByteArray(r, 32) + msg := testutil.GenRandomByteArray(r, 32) + eotsSig := testutil.GenRandomByteArray(r, 32) + chainID := []byte("test-chain") // save for the first time err = vs.SaveSignRecord( expectedHeight, - expectedRecord.BlockHash, - expectedRecord.PublicKey, - expectedRecord.Signature, + chainID, + msg, + pk, + eotsSig, ) require.NoError(t, err) // try to save the record at the same height err = vs.SaveSignRecord( expectedHeight, - expectedRecord.BlockHash, - expectedRecord.PublicKey, - expectedRecord.Signature, + chainID, + msg, + pk, + eotsSig, ) require.ErrorIs(t, err, store.ErrDuplicateSignRecord) - signRecordFromDB, found, err := vs.GetSignRecord(expectedHeight) + signRecordFromDB, found, err := vs.GetSignRecord(pk, chainID, expectedHeight) require.True(t, found) require.NoError(t, err) - require.Equal(t, expectedRecord.PublicKey, signRecordFromDB.PublicKey) - require.Equal(t, expectedRecord.BlockHash, signRecordFromDB.BlockHash) - require.Equal(t, expectedRecord.Signature, signRecordFromDB.Signature) + require.Equal(t, msg, signRecordFromDB.Msg) + require.Equal(t, eotsSig, signRecordFromDB.Signature) rndHeight := r.Uint64() - _, found, err = vs.GetSignRecord(rndHeight) + _, found, err = vs.GetSignRecord(pk, chainID, rndHeight) require.NoError(t, err) require.False(t, found) }) diff --git a/eotsmanager/store/storedsigningrecord.go b/eotsmanager/store/storedsigningrecord.go index 53bddd70..07c2729d 100644 --- a/eotsmanager/store/storedsigningrecord.go +++ b/eotsmanager/store/storedsigningrecord.go @@ -1,17 +1,33 @@ package store -import "github.com/babylonlabs-io/finality-provider/eotsmanager/proto" +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/babylonlabs-io/finality-provider/eotsmanager/proto" +) type SigningRecord struct { - BlockHash []byte // The hash of the block. - PublicKey []byte // The public key used for signing. - Signature []byte // The signature of the block. - Timestamp int64 // The timestamp of the signing operation, in Unix seconds. + Msg []byte // The message that the signature is signed over. + Signature []byte + Timestamp int64 // The timestamp of the signing operation, in Unix seconds. } func (s *SigningRecord) FromProto(sr *proto.SigningRecord) { - s.PublicKey = sr.PublicKey - s.BlockHash = sr.BlockHash + s.Msg = sr.Msg s.Timestamp = sr.Timestamp - s.Signature = sr.Signature + s.Signature = sr.EotsSig +} + +// the record key is (chainID || pk || height) +func getSignRecordKey(chainID, pk []byte, height uint64) []byte { + // Convert height to bytes + heightBytes := sdk.Uint64ToBigEndian(height) + + // Concatenate all components to create the key + key := make([]byte, 0, len(pk)+len(chainID)+len(heightBytes)) + key = append(key, chainID...) + key = append(key, pk...) + key = append(key, heightBytes...) + + return key } diff --git a/itest/e2e_test.go b/itest/e2e_test.go index e35e1b8a..a7f6b667 100644 --- a/itest/e2e_test.go +++ b/itest/e2e_test.go @@ -1,3 +1,6 @@ +//go:build e2e +// +build e2e + package e2etest import (