Skip to content

Commit

Permalink
finality: evidence storage and handler (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastianElvis authored Jul 11, 2023
1 parent 17855d8 commit c46b30f
Show file tree
Hide file tree
Showing 12 changed files with 1,065 additions and 52 deletions.
20 changes: 20 additions & 0 deletions proto/babylon/finality/v1/events.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
syntax = "proto3";
package babylon.finality.v1;

import "gogoproto/gogo.proto";
import "babylon/finality/v1/finality.proto";

option go_package = "github.com/babylonchain/babylon/x/finality/types";

// EventSlashedBTCValidator is the event emitted when a BTC validator is slashed
// due to signing two conflicting blocks
message EventSlashedBTCValidator {
// val_btc_pk is the BTC validator's BTC PK
bytes val_btc_pk = 1 [ (gogoproto.customtype) = "github.com/babylonchain/babylon/types.BIP340PubKey" ];
// indexed_block is the canonical block at this height
IndexedBlock indexed_block = 2;
// evidence is the evidence that the BTC validator double signs
Evidence evidence = 3;
// extracted_btc_sk is the extracted BTC SK of this BTC validator
bytes extracted_btc_sk = 4;
}
21 changes: 21 additions & 0 deletions proto/babylon/finality/v1/finality.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package babylon.finality.v1;

option go_package = "github.com/babylonchain/babylon/x/finality/types";

import "gogoproto/gogo.proto";

// IndexedBlock is the block with some indexed info
message IndexedBlock {
// height is the height of the block
Expand All @@ -13,3 +15,22 @@ message IndexedBlock {
// BTC validators or not
bool finalized = 3;
}

// Evidence is the evidence that a BTC validator has signed a finality
// signature with correct public randomness on a fork header
// It includes all fields of MsgAddFinalitySig, such that anyone seeing
// the evidence and a signature on the canonical fork can extract the
// BTC validator's BTC secret key.
message Evidence {
// val_btc_pk is the BTC Pk of the validator that casts this vote
bytes val_btc_pk = 1 [ (gogoproto.customtype) = "github.com/babylonchain/babylon/types.BIP340PubKey" ];
// block_height is the height of the voted block
uint64 block_height = 2;
// block_last_commit_hash is the last_commit_hash of the voted block
bytes block_last_commit_hash = 3;
// finality_sig is the finality signature to this block
// where finality signature is an EOTS signature, i.e.,
// the `s` in a Schnorr signature `(r, s)`
// `r` is the public randomness that is already committed by the validator
bytes finality_sig = 4 [ (gogoproto.customtype) = "github.com/babylonchain/babylon/types.SchnorrEOTSSig" ];
}
42 changes: 42 additions & 0 deletions x/finality/keeper/evidence.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package keeper

import (
bbn "github.com/babylonchain/babylon/types"
"github.com/babylonchain/babylon/x/finality/types"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
)

func (k Keeper) SetEvidence(ctx sdk.Context, evidence *types.Evidence) {
store := k.evidenceStore(ctx, evidence.BlockHeight)
store.Set(evidence.ValBtcPk.MustMarshal(), k.cdc.MustMarshal(evidence))
}

func (k Keeper) HasEvidence(ctx sdk.Context, height uint64, valBtcPK *bbn.BIP340PubKey) bool {
store := k.evidenceStore(ctx, height)
return store.Has(valBtcPK.MustMarshal())
}

func (k Keeper) GetEvidence(ctx sdk.Context, height uint64, valBtcPK *bbn.BIP340PubKey) (*types.Evidence, error) {
if uint64(ctx.BlockHeight()) < height {
return nil, types.ErrHeightTooHigh
}
store := k.evidenceStore(ctx, height)
evidenceBytes := store.Get(valBtcPK.MustMarshal())
if len(evidenceBytes) == 0 {
return nil, types.ErrEvidenceNotFound
}
var evidence types.Evidence
k.cdc.MustUnmarshal(evidenceBytes, &evidence)
return &evidence, nil
}

// evidenceStore returns the KVStore of the evidences
// prefix: EvidenceKey
// key: (block height || BTC validator PK)
// value: Evidence
func (k Keeper) evidenceStore(ctx sdk.Context, height uint64) prefix.Store {
store := ctx.KVStore(k.storeKey)
prefixedStore := prefix.NewStore(store, types.EvidenceKey)
return prefix.NewStore(prefixedStore, sdk.Uint64ToBigEndian(height))
}
59 changes: 48 additions & 11 deletions x/finality/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,61 @@ func (ms msgServer) AddFinalitySig(goCtx context.Context, req *types.MsgAddFinal
}
if !bytes.Equal(indexedBlock.LastCommitHash, req.BlockLastCommitHash) {
// the BTC validator votes for a fork!
sig2, err := ms.GetSig(ctx, req.BlockHeight, valPK)

// construct and save evidence
evidence := &types.Evidence{
ValBtcPk: req.ValBtcPk,
BlockHeight: req.BlockHeight,
BlockLastCommitHash: req.BlockLastCommitHash,
FinalitySig: req.FinalitySig,
}
ms.SetEvidence(ctx, evidence)

// if this BTC validator has also signed canonical block, extract its secret key and emit an event
canonicalSig, err := ms.GetSig(ctx, req.BlockHeight, valPK)
if err == nil {
btcSK, err := evidence.ExtractBTCSK(indexedBlock, pubRand, canonicalSig)
if err != nil {
panic(fmt.Errorf("failed to extract secret key from two EOTS signatures with the same public randomness: %v", err))
}

eventSlashing := types.NewEventSlashedBTCValidator(req.ValBtcPk, indexedBlock, evidence, btcSK)
if err := ctx.EventManager().EmitTypedEvent(eventSlashing); err != nil {
return nil, fmt.Errorf("failed to emit EventSlashedBTCValidator event: %w", err)
}
}

// NOTE: we should NOT return error here, otherwise the state change triggered in this tx
// (including the evidence) will be rolled back
return &types.MsgAddFinalitySigResponse{}, nil
}

// this signature is good, add vote to DB
ms.SetSig(ctx, req.BlockHeight, valPK, req.FinalitySig)

// if this BTC validator has signed the canonical block before,
// slash it via extracting its secret key, and emit an event
if ms.HasEvidence(ctx, req.BlockHeight, req.ValBtcPk) {
// the BTC validator has voted for a fork before!

// get evidence
evidence, err := ms.GetEvidence(ctx, req.BlockHeight, req.ValBtcPk)
if err != nil {
return nil, fmt.Errorf("the BTC validator %v votes for a fork, but does not vote for the canonical block", valPK.MustMarshal())
panic(fmt.Errorf("failed to get evidence despite HasEvidence returns true"))
}
// the BTC validator votes for a fork AND the canonical block
// slash it via extracting its secret key
btcSK, err := eots.Extract(valBTCPK, pubRand.ToFieldVal(), req.MsgToSign(), req.FinalitySig.ToModNScalar(), indexedBlock.MsgToSign(), sig2.ToModNScalar())

// extract its SK
btcSK, err := evidence.ExtractBTCSK(indexedBlock, pubRand, req.FinalitySig)
if err != nil {
panic(fmt.Errorf("failed to extract secret key from two EOTS signatures with the same public randomness: %v", err))
}
return nil, fmt.Errorf("the BTC validator %v votes two conflicting blocks! extracted secret key: %v", valPK.MustMarshal(), btcSK.Serialize())
// TODO: what to do with the extracted secret key? e.g., have a KVStore that stores extracted SKs/forked blocks

eventSlashing := types.NewEventSlashedBTCValidator(req.ValBtcPk, indexedBlock, evidence, btcSK)
if err := ctx.EventManager().EmitTypedEvent(eventSlashing); err != nil {
return nil, fmt.Errorf("failed to emit EventSlashedBTCValidator event: %w", err)
}
}
// TODO: it's also possible that the validator votes for a fork first, then vote for canonical
// block. We need to save the signatures on the fork, and add a detection here

// all good, add vote to DB
ms.SetSig(ctx, req.BlockHeight, valPK, req.FinalitySig)
return &types.MsgAddFinalitySigResponse{}, nil
}

Expand Down
27 changes: 20 additions & 7 deletions x/finality/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"math/rand"
"testing"

"github.com/babylonchain/babylon/crypto/eots"
"github.com/babylonchain/babylon/testutil/datagen"
keepertest "github.com/babylonchain/babylon/testutil/keeper"
bbn "github.com/babylonchain/babylon/types"
Expand Down Expand Up @@ -166,16 +165,30 @@ func FuzzAddFinalitySig(f *testing.F) {
_, err = ms.AddFinalitySig(ctx, msg)
require.Error(t, err)

// Case 5: fail if the BTC validator has voted for a fork
// Case 5: the BTC validator is slashed if the BTC validator votes for a fork
blockHash2 := datagen.GenRandomByteArray(r, 32)
msg2, err := types.NewMsgAddFinalitySig(signer, btcSK, sr, blockHeight, blockHash2)
require.NoError(t, err)
// NOTE: even though this BTC validator is slashed, the msg should be successful
// Otherwise the saved evidence will be rolled back
_, err = ms.AddFinalitySig(ctx, msg2)
require.Error(t, err)
// Also, this BTC validator can be slashed
// extract the SK and assert the extracted SK is correct
btcSK2, err := eots.Extract(btcPK, pr.ToFieldVal(), msg.MsgToSign(), sig.ToModNScalar(), msg2.MsgToSign(), msg2.FinalitySig.ToModNScalar())
require.NoError(t, err)
require.Equal(t, btcSK.Serialize(), btcSK2.Serialize())
// ensure the evidence has been stored
evidence, err := fKeeper.GetEvidence(ctx, blockHeight, valBTCPK)
require.NoError(t, err)
require.Equal(t, msg2.BlockHeight, evidence.BlockHeight)
require.Equal(t, msg2.ValBtcPk.MustMarshal(), evidence.ValBtcPk.MustMarshal())
require.Equal(t, msg2.BlockLastCommitHash, evidence.BlockLastCommitHash)
require.Equal(t, msg2.FinalitySig.MustMarshal(), evidence.FinalitySig.MustMarshal())
// extract the SK and assert the extracted SK is correct
indexedBlock := &types.IndexedBlock{Height: blockHeight, LastCommitHash: blockHash}
btcSK2, err := evidence.ExtractBTCSK(indexedBlock, &pr, msg.FinalitySig)
require.NoError(t, err)
// ensure btcSK and btcSK2 correspond to the same PK
// NOTE: it's possible that different SKs derive to the same PK
// In this scenario, signature of any of these SKs can be verified with this PK
// exclude the first byte here since it denotes the y axis of PubKey, which does
// not affect verification
require.Equal(t, btcSK.PubKey().SerializeCompressed()[1:], btcSK2.PubKey().SerializeCompressed()[1:])
})
}
13 changes: 7 additions & 6 deletions x/finality/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (

// x/finality module sentinel errors
var (
ErrBlockNotFound = errorsmod.Register(ModuleName, 1100, "Block is not found")
ErrDuplicatedBlock = errorsmod.Register(ModuleName, 1101, "Block is already in KVStore")
ErrVoteNotFound = errorsmod.Register(ModuleName, 1102, "vote is not found")
ErrHeightTooHigh = errorsmod.Register(ModuleName, 1103, "the chain has not reached the given height yet")
ErrPubRandNotFound = errorsmod.Register(ModuleName, 1104, "public randomness is not found")
ErrNoPubRandYet = errorsmod.Register(ModuleName, 1105, "the BTC validator has not committed any public randomness yet")
ErrBlockNotFound = errorsmod.Register(ModuleName, 1100, "Block is not found")
ErrDuplicatedBlock = errorsmod.Register(ModuleName, 1101, "Block is already in KVStore")
ErrVoteNotFound = errorsmod.Register(ModuleName, 1102, "vote is not found")
ErrHeightTooHigh = errorsmod.Register(ModuleName, 1103, "the chain has not reached the given height yet")
ErrPubRandNotFound = errorsmod.Register(ModuleName, 1104, "public randomness is not found")
ErrNoPubRandYet = errorsmod.Register(ModuleName, 1105, "the BTC validator has not committed any public randomness yet")
ErrEvidenceNotFound = errorsmod.Register(ModuleName, 1107, "evidence is not found")
)
15 changes: 15 additions & 0 deletions x/finality/types/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package types

import (
bbn "github.com/babylonchain/babylon/types"
"github.com/btcsuite/btcd/btcec/v2"
)

func NewEventSlashedBTCValidator(valBTCPK *bbn.BIP340PubKey, indexedBlock *IndexedBlock, evidence *Evidence, btcSK *btcec.PrivateKey) *EventSlashedBTCValidator {
return &EventSlashedBTCValidator{
ValBtcPk: valBTCPK,
IndexedBlock: indexedBlock,
Evidence: evidence,
ExtractedBtcSk: btcSK.Serialize(),
}
}
Loading

0 comments on commit c46b30f

Please sign in to comment.