Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Optimistic fast finality #316

Merged
merged 42 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
95c8922
go.mod: update goja version and fix tests (#300)
minh-bq Jul 5, 2023
6586619
feat: Implement high level function for bls_12381 (#296)
DNK90 Jul 5, 2023
068a74f
feat: implement bls accounts and keymanager (#298)
DNK90 Jul 5, 2023
5f8920d
go.mod: import go-eth2-wallet-encryptor-keystorev4 (#302)
minh-bq Jul 5, 2023
ec15ae4
core/types: add finality vote file (#305)
DNK90 Jul 7, 2023
2fe0f4a
consensus: add fast finality interface (#304)
minh-bq Jul 7, 2023
42e63c6
crypto/bls: fix bls signature batch unit test (#306)
minh-bq Jul 10, 2023
7b2f9cf
core/vote: implement finality vote manager and pool (#301)
DNK90 Jul 11, 2023
039e4dd
core/types/vote: introduce RawVoteEnvelop (#308)
minh-bq Jul 12, 2023
6d0a974
core/vote/vote_pool: protect pool against malicious peer (#309)
minh-bq Jul 13, 2023
cc76bf6
build: add cflags to build portable blst library (#310)
minh-bq Jul 14, 2023
d5a271d
consortium/v2: add finality vote to consensus layer (#299)
minh-bq Jul 20, 2023
2b01e56
blockchain, rpc, backend: add new forkchoice and finalized block quer…
minh-bq Jul 20, 2023
d3e0b17
eth/protocols: implement finality vote communication via ronin protoc…
minh-bq Jul 20, 2023
46a63f5
vote/vote_pool: use TryRLock in FetchVoteByBlockHash (#311)
minh-bq Jul 20, 2023
9aa5709
core/vm: add precompiled contract to verify finality vote violation p…
minh-bq Jul 20, 2023
654a936
monitor/finality_vote: add finality vote monitor (#312)
minh-bq Jul 20, 2023
27e6cac
consortium/v2: fix snapshot save after Shillin (#317)
minh-bq Jul 21, 2023
acb814b
build: add CGO flags to all build targets (#322)
minh-bq Jul 25, 2023
2d51eb5
consortium/v2: add contract interaction to get BLS public key (#320)
minh-bq Jul 25, 2023
7318ee4
eth/backend: add vote pool to consortium engine (#323)
minh-bq Jul 25, 2023
835c313
consortium/v2: fix the finality vote fetch from pool (#319)
minh-bq Jul 25, 2023
cb16894
blockchain: check total difficulty only when having same justified bl…
minh-bq Jul 27, 2023
6f2bf3a
consortium/v2: edit the finality vote threshold (#324)
minh-bq Jul 27, 2023
555a252
consortium/v2: add RPC API for querying finality vote easily (#318)
minh-bq Jul 27, 2023
0f0e06f
accountcmd, entrypoint: add BLS key management commands (#325)
minh-bq Jul 28, 2023
4a11959
eth/handler_ronin: add nil check for ethPeer in PeerInfo (#328)
minh-bq Jul 31, 2023
c46cf70
consortium/v2: add VerifyVote and fix blsPublicKey access (#327)
minh-bq Jul 31, 2023
9255cdb
consortium/v2/api: lowercase the first character in returned json fie…
minh-bq Jul 31, 2023
2e6d7d7
consortium/v2: copy justified block number and block hash in snapshot…
minh-bq Jul 31, 2023
a0faee0
consortium/v2: fix the incorrect snapshot lookup in GetFinalizedBlock…
minh-bq Jul 31, 2023
60b52c1
Dockerfile, entrypoint: fix start script and install additional packa…
minh-bq Jul 31, 2023
96febbb
protocols/ronin, vote/votepool: improve vote channel queue (#336)
minh-bq Aug 3, 2023
857e078
entrypoint: fix BLS key generate flow and add show BLS secret key opt…
minh-bq Aug 3, 2023
ab7efcc
vote/vote_pool: reject older than justified block number vote (#334)
minh-bq Aug 9, 2023
c59e820
consortium/v2: make SealHash return unchanged hash after Shillin (#332)
minh-bq Aug 10, 2023
c0b28d6
consortium/v2: protect the contract interaction under mutex (#339)
minh-bq Aug 14, 2023
e677d26
consortium/v2: reject the vote with wrong target number (#338)
minh-bq Aug 14, 2023
efe0841
consortium_precompiled: fix typo in the validate finality proof funct…
minh-bq Aug 28, 2023
aa6a2cc
go.mod: bump blst to v0.3.11 (#348)
minh-bq Aug 28, 2023
64b67de
consortium/v2: distribute finality reward for the voters in parent bl…
minh-bq Sep 6, 2023
716f8b5
params: add optimistic fast finality testnet configuration (#353)
minh-bq Sep 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Build Geth in a stock Go builder container
FROM golang:1.20.0-alpine3.17 as builder

RUN apk add --no-cache make gcc musl-dev linux-headers git
RUN apk add --no-cache make gcc musl-dev linux-headers git libstdc++-dev

COPY . /opt
RUN cd /opt && make ronin
Expand All @@ -23,6 +23,12 @@ ENV NODEKEY ''
ENV FORCE_INIT 'true'
ENV RONIN_PARAMS ''
ENV INIT_FORCE_OVERRIDE_CHAIN_CONFIG 'false'
ENV ENABLE_FAST_FINALITY 'true'
ENV ENABLE_FAST_FINALITY_SIGN 'true'
ENV BLS_PRIVATE_KEY ''
ENV BLS_PASSWORD ''
ENV BLS_AUTO_GENERATE 'false'
ENV BLS_SHOW_PRIVATE_KEY 'false'

COPY --from=builder /opt/build/bin/ronin /usr/local/bin/ronin
COPY --from=builder /opt/genesis/ ./
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
.PHONY: ronin-darwin ronin-darwin-386 geth-darwin-amd64
.PHONY: ronin-windows ronin-windows-386 geth-windows-amd64

CFLAGS = "-O -D__BLST_PORTABLE__"
GOBIN = ./build/bin
GO ?= latest
GORUN = go run
GORUN = CGO_CFLAGS_ALLOW=$(CFLAGS) CGO_CFLAGS=$(CFLAGS) go run
RONIN_CONTRACTS_PATH = ../ronin-dpos-contracts
RONIN_CONTRACTS_OUTPUT_PATH = ./tmp/contracts
GEN_CONTRACTS_OUTPUT_PATH = ./consensus/consortium/generated_contracts
Expand Down
342 changes: 342 additions & 0 deletions accounts/bls/keymanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,342 @@
package bls

import (
"context"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"sync"

ethCommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto/bls"
"github.com/ethereum/go-ethereum/crypto/bls/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/params"
"github.com/google/uuid"
"github.com/pkg/errors"
keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4"
)

const (
// IncorrectPasswordErrMsg defines a common error string representing an EIP-2335
// keystore password was incorrect.
IncorrectPasswordErrMsg = "invalid checksum"
ImportedKeystoreStatus_IMPORTED = 0
ImportedKeystoreStatus_DUPLICATE = 1
ImportedKeystoreStatus_ERROR = 2
)

type ImportedKeystoreStatus struct {
Status int32 `json:"status,omitempty"`
Message string `json:"message,omitempty"`
}

var (
ErrNoPasswords = errors.New("no passwords provided for keystores")
ErrMismatchedNumPasswords = errors.New("number of passwords does not match number of keystores")
)

type SignRequest struct {
PublicKey []byte `json:"public_key,omitempty"`
SigningRoot []byte `json:"signing_root,omitempty"`
}

type KeyManager struct {
lock sync.RWMutex

pubKeys [][params.BLSPubkeyLength]byte
secKeys map[[params.BLSPubkeyLength]byte]common.SecretKey

wallet *Wallet
accountsStore *AccountStore
}

// NewKeyManager instantiates a new local keymanager .
func NewKeyManager(ctx context.Context, wallet *Wallet) (*KeyManager, error) {
k := &KeyManager{
wallet: wallet,
accountsStore: &AccountStore{},
}

if err := k.initializeAccountKeystore(ctx); err != nil {
return nil, errors.Wrap(err, "failed to initialize account store")
}
return k, nil
}

func (km *KeyManager) initializeAccountKeystore(ctx context.Context) error {
encoded, err := km.wallet.ReadFile(ctx, AccountsKeystoreFileName)
if err != nil && strings.Contains(err.Error(), "no files found") {
// If there are no keys to initialize at all, just exit.
return nil
} else if err != nil {
return errors.Wrapf(err, "could not read keystore file for accounts %s", AccountsKeystoreFileName)
}
keystoreFile := &AccountsKeystoreRepresentation{}
if err := json.Unmarshal(encoded, keystoreFile); err != nil {
return errors.Wrapf(err, "could not decode keystore file for accounts %s", AccountsKeystoreFileName)
}
// We extract the validator signing private key from the keystore
// by utilizing the password and initialize a new BLS secret key from
// its raw bytes.
password := km.wallet.walletPassword
decryptor := keystorev4.New()
enc, err := decryptor.Decrypt(keystoreFile.Crypto, password)
if err != nil && strings.Contains(err.Error(), IncorrectPasswordErrMsg) {
return errors.Wrap(err, "wrong password for wallet entered")
} else if err != nil {
return errors.Wrap(err, "could not decrypt keystore")
}

store := &AccountStore{}
if err := json.Unmarshal(enc, store); err != nil {
return err
}
if len(store.PublicKeys) != len(store.PrivateKeys) {
return errors.New("unequal number of public keys and private keys")
}
if len(store.PublicKeys) == 0 {
return nil
}
km.accountsStore = store
err = km.initializeKeysCachesFromKeystore()
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches")
}
return err
}

// Initialize public and secret key caches that are used to speed up the functions
// FetchValidatingPublicKeys and Sign
func (km *KeyManager) initializeKeysCachesFromKeystore() error {
km.lock.Lock()
defer km.lock.Unlock()
count := len(km.accountsStore.PrivateKeys)
km.pubKeys = make([][params.BLSPubkeyLength]byte, count)
km.secKeys = make(map[[params.BLSPubkeyLength]byte]common.SecretKey, count)
for i, publicKey := range km.accountsStore.PublicKeys {
publicKey48 := ethCommon.ToBytes48(publicKey)
km.pubKeys[i] = publicKey48
secretKey, err := bls.SecretKeyFromBytes(km.accountsStore.PrivateKeys[i])
if err != nil {
return errors.Wrap(err, "failed to initialize keys caches from account keystore")
}
km.secKeys[publicKey48] = secretKey
}
return nil
}

// FetchValidatingPublicKeys fetches the list of active public keys from the local account keystores.
func (km *KeyManager) FetchValidatingPublicKeys(ctx context.Context) ([][params.BLSPubkeyLength]byte, error) {
km.lock.RLock()
defer km.lock.RUnlock()
keys := km.pubKeys
result := make([][params.BLSPubkeyLength]byte, len(keys))
copy(result, keys)
return result, nil
}

// FetchValidatingSecretKeys fetches the list of active secret keys from the local account keystores.
func (km *KeyManager) FetchValidatingSecretKeys(ctx context.Context) ([][params.BLSSecretKeyLength]byte, error) {
km.lock.RLock()
defer km.lock.RUnlock()

var secretKeys [][params.BLSSecretKeyLength]byte
for _, secretKey := range km.secKeys {
secretKeys = append(secretKeys, [params.BLSSecretKeyLength]byte(secretKey.Marshal()))
}

return secretKeys, nil
}

// Sign signs a message using a validator key.
func (km *KeyManager) Sign(ctx context.Context, req *SignRequest) (bls.Signature, error) {
publicKey := req.PublicKey
if publicKey == nil {
return nil, errors.New("nil public key in request")
}
km.lock.RLock()
secretKey, ok := km.secKeys[ethCommon.ToBytes48(publicKey)]
km.lock.RUnlock()
if !ok {
return nil, errors.New("no signing key found in keys cache")
}
return secretKey.Sign(req.SigningRoot), nil
}

// ImportKeystores into the local keymanager from an external source.
func (km *KeyManager) ImportKeystores(
ctx context.Context,
keystores []*Keystore,
passwords []string,
) ([]*ImportedKeystoreStatus, error) {
if len(passwords) == 0 {
return nil, ErrNoPasswords
}
if len(passwords) != len(keystores) {
return nil, ErrMismatchedNumPasswords
}
decryptor := keystorev4.New()
keys := map[string]string{}
statuses := make([]*ImportedKeystoreStatus, len(keystores))
var err error

for i := 0; i < len(keystores); i++ {
var privKeyBytes []byte
var pubKeyBytes []byte
privKeyBytes, pubKeyBytes, _, err = km.attemptDecryptKeystore(decryptor, keystores[i], passwords[i])
if err != nil {
statuses[i] = &ImportedKeystoreStatus{
Status: ImportedKeystoreStatus_ERROR,
Message: err.Error(),
}
continue
}
// if key exists prior to being added then output log that duplicate key was found
if _, ok := keys[string(pubKeyBytes)]; ok {
log.Warn(fmt.Sprintf("Duplicate key in import will be ignored: %#x", pubKeyBytes))
statuses[i] = &ImportedKeystoreStatus{
Status: ImportedKeystoreStatus_DUPLICATE,
}
continue
}
keys[string(pubKeyBytes)] = string(privKeyBytes)
statuses[i] = &ImportedKeystoreStatus{
Status: ImportedKeystoreStatus_IMPORTED,
}
}
privKeys := make([][]byte, 0)
pubKeys := make([][]byte, 0)
for pubKey, privKey := range keys {
pubKeys = append(pubKeys, []byte(pubKey))
privKeys = append(privKeys, []byte(privKey))
}

// Write the accounts to disk into a single keystore.
accountsKeystore, err := km.CreateAccountsKeystore(ctx, privKeys, pubKeys)
if err != nil {
return nil, err
}
encodedAccounts, err := json.MarshalIndent(accountsKeystore, "", "\t")
if err != nil {
return nil, err
}
if err := km.wallet.WriteFile(ctx, AccountsKeystoreFileName, encodedAccounts); err != nil {
return nil, err
}
return statuses, nil
}

// ImportKeypairs directly into the keyManager.
func (km *KeyManager) ImportKeypairs(ctx context.Context, privKeys, pubKeys [][]byte) error {
// Write the accounts to disk into a single keystore.
accountsKeystore, err := km.CreateAccountsKeystore(ctx, privKeys, pubKeys)
if err != nil {
return errors.Wrap(err, "could not import account keypairs")
}
encodedAccounts, err := json.MarshalIndent(accountsKeystore, "", "\t")
if err != nil {
return errors.Wrap(err, "could not marshal accounts keystore into JSON")
}
return km.wallet.WriteFile(ctx, AccountsKeystoreFileName, encodedAccounts)
}

// CreateAccountsKeystore creates a new keystore holding the provided keys.
func (km *KeyManager) CreateAccountsKeystore(
_ context.Context,
privateKeys, publicKeys [][]byte,
) (*AccountsKeystoreRepresentation, error) {
encryptor := keystorev4.New()
id, err := uuid.NewRandom()
if err != nil {
return nil, err
}
if len(privateKeys) != len(publicKeys) {
return nil, fmt.Errorf(
"number of private keys and public keys is not equal: %d != %d", len(privateKeys), len(publicKeys),
)
}
if km.accountsStore == nil {
km.accountsStore = &AccountStore{
PrivateKeys: privateKeys,
PublicKeys: publicKeys,
}
} else {
existingPubKeys := make(map[string]bool)
existingPrivKeys := make(map[string]bool)
for i := 0; i < len(km.accountsStore.PrivateKeys); i++ {
existingPrivKeys[string(km.accountsStore.PrivateKeys[i])] = true
existingPubKeys[string(km.accountsStore.PublicKeys[i])] = true
}
// We append to the accounts store keys only
// if the private/secret key do not already exist, to prevent duplicates.
for i := 0; i < len(privateKeys); i++ {
sk := privateKeys[i]
pk := publicKeys[i]
_, privKeyExists := existingPrivKeys[string(sk)]
_, pubKeyExists := existingPubKeys[string(pk)]
if privKeyExists || pubKeyExists {
continue
}
km.accountsStore.PublicKeys = append(km.accountsStore.PublicKeys, pk)
km.accountsStore.PrivateKeys = append(km.accountsStore.PrivateKeys, sk)
}
}
err = km.initializeKeysCachesFromKeystore()
if err != nil {
return nil, errors.Wrap(err, "failed to initialize keys caches")
}
encodedStore, err := json.MarshalIndent(km.accountsStore, "", "\t")
if err != nil {
return nil, err
}
cryptoFields, err := encryptor.Encrypt(encodedStore, km.wallet.walletPassword)
if err != nil {
return nil, errors.Wrap(err, "could not encrypt accounts")
}
return &AccountsKeystoreRepresentation{
Crypto: cryptoFields,
ID: id.String(),
Version: encryptor.Version(),
Name: encryptor.Name(),
}, nil
}

// Retrieves the private key and public key from an EIP-2335 keystore file
// by decrypting using a specified password. If the password fails,
// it prompts the user for the correct password until it confirms.
func (_ *KeyManager) attemptDecryptKeystore(
enc *keystorev4.Encryptor, keystore *Keystore, password string,
) ([]byte, []byte, string, error) {
// Attempt to decrypt the keystore with the specifies password.
var privKeyBytes []byte
var err error
privKeyBytes, err = enc.Decrypt(keystore.Crypto, password)
doesNotDecrypt := err != nil && strings.Contains(err.Error(), IncorrectPasswordErrMsg)
if doesNotDecrypt {
return nil, nil, "", fmt.Errorf(
"incorrect password for key 0x%s",
keystore.Pubkey,
)
}
if err != nil && !strings.Contains(err.Error(), IncorrectPasswordErrMsg) {
return nil, nil, "", errors.Wrap(err, "could not decrypt keystore")
}
var pubKeyBytes []byte
// Attempt to use the pubkey present in the keystore itself as a field. If unavailable,
// then utilize the public key directly from the private key.
if keystore.Pubkey != "" {
pubKeyBytes, err = hex.DecodeString(keystore.Pubkey)
if err != nil {
return nil, nil, "", errors.Wrap(err, "could not decode pubkey from keystore")
}
} else {
privKey, err := bls.SecretKeyFromBytes(privKeyBytes)
if err != nil {
return nil, nil, "", errors.Wrap(err, "could not initialize private key from bytes")
}
pubKeyBytes = privKey.PublicKey().Marshal()
}
return privKeyBytes, pubKeyBytes, password, nil
}
Loading