Finality providers are responsible for voting at a finality round on top of the consumer system. A finality provider can receive voting power delegations from BTC stakers, and can earn commission from the staking rewards denominated in the consumer’s native token.
A finality provider connects to a Extractable One-Time Signature (EOTS) manager, a daemon responsible for securely maintaining the finality provider’s private key and producing EOTS signatures from it. BTC staking litepaper contains more details about EOTS.
EOTS key manager runs as a stand-alone daemon. It generates and manages the finality providers' BTC Schnorr secret keys.
The EOTS manager needs to expose the following interfaces that can be called by a finality provider:
type EOTSManager interface {
// CreateKey generates a key pair at the given name and persists it in storage.
// The key pair is formatted by BIP-340 (Schnorr Signatures)
// It fails if there is an existing key Info with the same name or public key.
CreateKey(name, passphrase, hdPath string) ([]byte, error)
// CreateRandomnessPairList generates a list of Schnorr randomness pairs from
// startHeight to startHeight+(num-1) where num means the number of public randomness
// It fails if the finality provider does not exist or a randomness pair has been created before
// or passPhrase is incorrect
// NOTE: the randomness is deterministically generated based on the EOTS key, chainID and
// block height
CreateRandomnessPairList(uid []byte, chainID []byte, startHeight uint64, num uint32, passphrase string) ([]*btcec.FieldVal, error)
// KeyRecord returns the finality provider record
// It fails if the finality provider does not exist or passPhrase is incorrect
KeyRecord(uid []byte, passphrase string) (*types.KeyRecord, error)
// SignEOTS signs an EOTS using the private key of the finality provider and the corresponding
// secret randomness of the give chain at the given height
// It fails if the finality provider does not exist or there's no randomness committed to the given height
// or passPhrase is incorrect
SignEOTS(uid []byte, chainID []byte, msg []byte, height uint64, passphrase string) (*btcec.ModNScalar, error)
// SignSchnorrSig signs a Schnorr signature using the private key of the finality provider
// It fails if the finality provider does not exist or the message size is not 32 bytes
// or passPhrase is incorrect
SignSchnorrSig(uid []byte, msg []byte, passphrase string) (*schnorr.Signature, error)
Close() error
}
The implementations of the above interfaces depend on the actual consumer. For example, if the consumer is a Cosmos SDK based blockchain, then we need specific packages to provide the above RPC endpoints.
Babylon team provides an EOTS manager implementation at https://github.com/babylonchain/finality-provider/tree/dev/eotsmanager. It is fully compliant to the interfaces defined above.
The finality provider daemon is responsible for monitoring for new blocks in the consumer system, committing public randomness for the blocks, and submitting finality signatures (i.e., EOTS signatures). In particular, a finality provider daemon has the following functionalities:
The finality provider daemon needs to maintain a KV store of its managed finality providers. The key is the finality provider’s Bitcoin public key, and the value is the finality provider object defined as follows:
message StoreFinalityProvider {
// consumer_pk is the consumer PK of this finality provider in consumer
bytes consumer_pk = 1;
// btc_pk is the BTC secp256k1 PK of the finality provider encoded in BIP-340 spec
bytes btc_pk = 2;
// description defines the description terms for the finality provider
bytes description = 3;
// commission defines the commission rate for the finality provider
string commission = 4 [
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
// pop is the proof of possession of consumer_pk and btc_pk
ProofOfPossession pop = 5;
// key_name is the identifier of the keyring
string key_name = 6;
// chain_id is the identifier of the consumer chain that the finality provider connected to
string chain_id = 7;
// last_voted_height defines the height of the last voted consumer block
uint64 last_voted_height = 8;
// status defines the current finality provider status
FinalityProviderStatus status = 9;
}
// Description defines description fields for a finality provider
message Description {
string moniker = 1;
string identity = 2;
string website = 3;
string security_contact = 4;
string details = 5;
}
// ProofOfPossession is the proof of possession that a consumer
// secret key and a Bitcoin secp256k1 secret key are held by the same
// person
message ProofOfPossession {
// consumer_sig is the signature generated via sign(sk_consumer, pk_btc)
bytes consumer_sig = 1;
// btc_sig is the signature generated via sign(sk_btc, consumer_sig)
// the signature follows encoding in BIP-340 spec
bytes btc_sig = 2;
}
// FinalityProviderStatus is the status of a finality provider
// a FinalityProvider object has 4 states:
// - Created - created and managed by finality provider client, not registered to
// consumer yet
// - Registered - created and registered to consumer, but not voting yet (No
// delegated stake)
// - Active - created and registered to consumer with stake to vote
// - Inactive - created and registered to consumer with no stake to vote.
// Finality Provider was already active.
// Valid State Transactions:
// - Created -> Registered
// - Registered -> Active
// - Active -> Inactive
// - Inactive -> Active
enum FinalityProviderStatus {
option (gogoproto.goproto_enum_prefix) = false;
// CREATED defines a finality provider that is awaiting registration
CREATED = 0 [(gogoproto.enumvalue_customname) = "CREATED"];
// REGISTERED defines a finality provider that has been registered
// to Babylon but has no delegated stake
REGISTERED = 1 [(gogoproto.enumvalue_customname) = "REGISTERED"];
// ACTIVE defines a finality provider that is delegated to vote
ACTIVE = 2 [(gogoproto.enumvalue_customname) = "ACTIVE"];
// INACTIVE defines a finality provider whose delegations are reduced to zero but not slashed
INACTIVE = 3 [(gogoproto.enumvalue_customname) = "INACTIVE"];
// SLASHED defines a finality provider that has been slashed
SLASHED = 4 [(gogoproto.enumvalue_customname) = "SLASHED"];
}