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

Initialize the DB with the GenesisState if the node is bootstrapping for the first time #1124

Merged
merged 1 commit into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions app/node/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, acc
failBuild(err, "failed to parse leader public key")
}

genHash := d.genesisCfg.ComputeGenesisHash()

ceCfg := &consensus.Config{
PrivateKey: d.privKey,
Leader: leaderPubKey,
Expand All @@ -245,6 +247,7 @@ func buildConsensusEngine(_ context.Context, d *coreDependencies, db *pg.DB, acc
MaxVotesPerTx: d.genesisCfg.MaxVotesPerTx,
},
},
GenesisHash: genHash,
DB: db,
Accounts: accounts,
BlockStore: bs,
Expand Down
24 changes: 24 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package config

import (
"cmp"
"encoding/binary"
"encoding/json"
"os"
"slices"
"time"

"github.com/kwilteam/kwil-db/core/log"
Expand Down Expand Up @@ -80,6 +83,27 @@ func LoadGenesisConfig(filename string) (*GenesisConfig, error) {
return &nc, nil
}

// TODO: Harden this code to prevent from consensus breaking changes
func (gc *GenesisConfig) ComputeGenesisHash() types.Hash {
hasher := ktypes.NewHasher()

hasher.Write([]byte(gc.ChainID))
hasher.Write(gc.Leader)

// sort the validators by public key
slices.SortFunc(gc.Validators, func(a, b ktypes.Validator) int {
return cmp.Compare(a.PubKey.String(), b.PubKey.String())
})

binary.Write(hasher, binary.BigEndian, gc.MaxBlockSize)
binary.Write(hasher, binary.BigEndian, gc.JoinExpiry)
binary.Write(hasher, binary.BigEndian, gc.VoteExpiry)
binary.Write(hasher, binary.BigEndian, gc.DisabledGasCosts)
binary.Write(hasher, binary.BigEndian, gc.MaxVotesPerTx)
Comment on lines +98 to +102
Copy link
Member

@jchappelow jchappelow Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One point about these, but not necessarily needing change: these are almost too convenient in that we can casually change the type of these and this code will not break, but the result will change, which is a consensus breaker. It's a lot more cumbersome to use the functions like binary.BigEndian.Append/PutUint64 etc, so let's go ahead with what you have now since we will certainly be changing gc a lot before release, but at some point I think we should harden this code.


return hasher.Sum(nil)
}

// const (
// defaultUserRPCPort = 8484
// defaultAdminRPCPort = 8584
Expand Down
5 changes: 0 additions & 5 deletions node/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,11 +415,6 @@ func (n *Node) startDiscoveryRequestGossip(ctx context.Context, ps *pubsub.PubSu
return
}

// We're only interested if we are the validator.
if n.ce.Role() != types.RoleValidator {
continue // discard, we are just relaying to leader
}

if peer.ID(discMsg.From) == me {
continue
}
Expand Down
6 changes: 4 additions & 2 deletions node/consensus/blocksync.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ import (
// caught up with the network, before it can start proposing blocks.
// Else it can propose a block at a height that is already finalized
// leading to a fork.

func (ce *ConsensusEngine) doBlockSync(ctx context.Context) error {
if ce.role.Load() == types.RoleLeader {
if len(ce.validators.GetValidators()) == 1 {
// TODO: which validator set we should use here? whatever we have in the state?
// what if the current validators are not the same as the ones in the state?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I understand the difference between "current validators" and "the ones in the state".

// and they don't respond to the status request?
if len(ce.validatorSet) == 1 {
return nil // we are the network
}
// figure out the best height to sync with the network
Expand Down
80 changes: 70 additions & 10 deletions node/consensus/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,9 @@ type ConsensusEngine struct {

proposeTimeout time.Duration

networkHeight atomic.Int64
validatorSet map[string]ktypes.Validator
networkHeight atomic.Int64
validatorSet map[string]ktypes.Validator
genesisAppHash types.Hash

// stores state machine state for the consensus engine
state state
Expand Down Expand Up @@ -196,6 +197,7 @@ type Config struct {
// Leader is the public key of the leader.
Leader crypto.PublicKey

GenesisHash types.Hash
GenesisParams *GenesisParams // *config.GenesisConfig

DB *pg.DB
Expand Down Expand Up @@ -298,13 +300,14 @@ func New(cfg *Config) *ConsensusEngine {
resetChan: make(chan int64, 1),
bestHeightCh: make(chan *discoveryMsg, 1),
// interfaces
mempool: cfg.Mempool,
blockStore: cfg.BlockStore,
txapp: cfg.TxApp,
accounts: cfg.Accounts,
validators: cfg.ValidatorStore,
snapshotter: cfg.Snapshots,
log: logger,
mempool: cfg.Mempool,
blockStore: cfg.BlockStore,
txapp: cfg.TxApp,
accounts: cfg.Accounts,
validators: cfg.ValidatorStore,
snapshotter: cfg.Snapshots,
log: logger,
genesisAppHash: cfg.GenesisHash,
}

ce.role.Store(role)
Expand All @@ -313,6 +316,8 @@ func New(cfg *Config) *ConsensusEngine {
return ce
}

var initialHeight int64 = 0 // TODO: get it from genesis?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point. Yeah we should add this to the genesis config struct. Can do later though.


func (ce *ConsensusEngine) Start(ctx context.Context, proposerBroadcaster ProposalBroadcaster,
blkAnnouncer BlkAnnouncer, ackBroadcaster AckBroadcaster, blkRequester BlkRequester, stateResetter ResetStateBroadcaster,
discoveryReqBroadcaster DiscoveryReqBroadcaster) error {
Expand All @@ -337,6 +342,52 @@ func (ce *ConsensusEngine) Start(ctx context.Context, proposerBroadcaster Propos
return ce.runConsensusEventLoop(ctx)
}

// GenesisInit initializes the node with the genesis state. This included initializing the
// votestore with the genesis validators, accounts with the genesis allocations and the
// chain meta store with the genesis network parameters.
// This is called only once when the node is bootstrapping for the first time.
func (ce *ConsensusEngine) GenesisInit(ctx context.Context) error {
genesisTx, err := ce.db.BeginTx(ctx)
if err != nil {
return err
}
defer genesisTx.Rollback(ctx)

// TODO: genesis allocs

// genesis validators
genVals := make([]*ktypes.Validator, 0, len(ce.validatorSet))
for _, v := range ce.validatorSet {
genVals = append(genVals, &ktypes.Validator{
PubKey: v.PubKey,
Power: v.Power,
})
}

startParams := *ce.chainCtx.NetworkParameters

if err := ce.txapp.GenesisInit(ctx, genesisTx, genVals, nil, initialHeight, ce.chainCtx); err != nil {
return err
}

if err := meta.SetChainState(ctx, genesisTx, initialHeight, ce.genesisAppHash[:], false); err != nil {
return fmt.Errorf("error storing the genesis state: %w", err)
}

if err := meta.StoreDiff(ctx, genesisTx, &startParams, ce.chainCtx.NetworkParameters); err != nil {
return fmt.Errorf("error storing the genesis consensus params: %w", err)
}

// TODO: Genesis hash and what are the mechanics for producing the first block (genesis block)?
ce.txapp.Commit()

ce.state.lc.appHash = ce.genesisAppHash
ce.state.lc.height = initialHeight

ce.log.Info("Initialized chain", "height", initialHeight, "appHash", ce.state.lc.appHash.String())
return genesisTx.Commit(ctx)
}

// runEventLoop starts the event loop for the consensus engine.
// Below are the event triggers that nodes can receive depending on their role:
// Leader:
Expand Down Expand Up @@ -497,13 +548,22 @@ func (ce *ConsensusEngine) catchup(ctx context.Context) error {
ce.setLastCommitInfo(appHeight, blkHash, types.Hash(appHash))
}

if appHeight == -1 {
// This is the first time the node is bootstrapping
// initialize the db with the genesis state
if err := ce.GenesisInit(ctx); err != nil {
return fmt.Errorf("error initializing the genesis state: %w", err)
}
}

// Replay the blocks from the blockstore if the app hasn't played all the blocks yet.
if appHeight < storeHeight {
// Replay the blocks from the blockstore
if err := ce.replayFromBlockStore(appHeight+1, storeHeight); err != nil {
return err
}
}

// Sync with the network using the blocksync
if err := ce.doBlockSync(ctx); err != nil {
return err
}
Expand Down
9 changes: 6 additions & 3 deletions node/consensus/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,7 @@ func TestValidatorStateMachine(t *testing.T) {
return false
}
return true
}, 2*time.Second, 100*time.Millisecond)
}, 6*time.Second, 100*time.Millisecond)
}
})
}
Expand Down Expand Up @@ -658,7 +658,7 @@ func TestCELeaderTwoNodesMajorityAcks(t *testing.T) {
height := n1.lastCommitHeight()
fmt.Printf("Height: %d\n", height)
return height == 1
}, 2*time.Second, 100*time.Millisecond)
}, 6*time.Second, 100*time.Millisecond)
}

func TestCELeaderTwoNodesMajorityNacks(t *testing.T) {
Expand Down Expand Up @@ -689,7 +689,7 @@ func TestCELeaderTwoNodesMajorityNacks(t *testing.T) {
require.Eventually(t, func() bool {
blockRes := n1.blockResult()
return blockRes != nil && !blockRes.appHash.IsZero()
}, 2*time.Second, 100*time.Millisecond)
}, 6*time.Second, 100*time.Millisecond)

_, _, b := n1.info()
assert.NotNil(t, b)
Expand Down Expand Up @@ -777,6 +777,9 @@ func (d *dummyTxApp) Price(ctx context.Context, dbTx sql.DB, tx *ktypes.Transact
func (d *dummyTxApp) Commit() error {
return nil
}
func (d *dummyTxApp) GenesisInit(ctx context.Context, db sql.DB, validators []*ktypes.Validator, genesisAccounts []*ktypes.Account, initialHeight int64, chain *common.ChainContext) error {
return nil
}

type validatorStore struct {
valSet []*ktypes.Validator
Expand Down
1 change: 1 addition & 0 deletions node/consensus/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type TxApp interface {
Execute(ctx *common.TxContext, db sql.DB, tx *ktypes.Transaction) *txapp.TxResponse
Finalize(ctx context.Context, db sql.DB, block *common.BlockContext) (finalValidators []*ktypes.Validator, err error)
Commit() error
GenesisInit(ctx context.Context, db sql.DB, validators []*ktypes.Validator, genesisAccounts []*ktypes.Account, initialHeight int64, chain *common.ChainContext) error

Price(ctx context.Context, dbTx sql.DB, tx *ktypes.Transaction, chainContext *common.ChainContext) (*big.Int, error)
}
Expand Down
4 changes: 4 additions & 0 deletions node/node_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@ func (d *dummyTxApp) Commit() error {
return nil
}

func (d *dummyTxApp) GenesisInit(ctx context.Context, db sql.DB, validators []*ktypes.Validator, genesisAccounts []*ktypes.Account, initialHeight int64, chain *common.ChainContext) error {
return nil
}

type validatorStore struct {
valSet []*ktypes.Validator
}
Expand Down
Loading