Skip to content

Commit

Permalink
fix: EOTS signing for multiple finality providers (#199)
Browse files Browse the repository at this point in the history
To support EOTS signing for multiple finality providers, the signing
record should be saved with key (public key || chainID || height)
  • Loading branch information
gitferry committed Dec 6, 2024
1 parent 40cad01 commit 950410f
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 109 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 25 additions & 14 deletions eotsmanager/localmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -193,50 +194,60 @@ 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)),
)

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)
}

Expand Down
51 changes: 32 additions & 19 deletions eotsmanager/localmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
})
}
41 changes: 17 additions & 24 deletions eotsmanager/proto/signstore.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 8 additions & 5 deletions eotsmanager/proto/signstore.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
// 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;
}
28 changes: 11 additions & 17 deletions eotsmanager/store/eotsstore.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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(),
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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[:]
}
Loading

0 comments on commit 950410f

Please sign in to comment.