- Synchronize state
- The initial state sync occurs when either the bootstrap node first starts and initializes a new blockchain, or when a node syncs the confirmed state and the local block store with the bootstrap node starting from the genesis or from a specific block number. The state sync ensures the immutability and the integrity of the blockchain by verifying signatures of the genesis and every confirmed block in order, re-applying every confirmed block to the confirmed state and the local block store of every node. The state sync process is performed during the initialization of a new blockchain. In this case only the genesis is created and persisted to the local block store of the bootstrap node, and the confirmed state of the bootstrap node is initialized with the genesis balances and the authority account address. The state sync process brings a new empty node or an out-of-sync node to have the verified and validated confirmed state as per the latest confirmed block on the blockchain
- Initialize bootstrap state
- The bootstrap state initialization is only performed once on the bootstrap node during the initialization of a new blockchain. The bootstrap state initialization creates, signs, and persists the genesis with the initial blockchain configuration parameters including the blockchain name, the creation time of the genesis, the authority account for signing the genesis and every proposed block, the initial owner account with the initial balance. The bootstrap state initialization process also initializes the confirmed state of the bootstrap node with balances from the genesis
- Synchronize node state
- The node state sync occurs every time when a new node joins the blockchain network and needs to verify and validate all confirmed blocks on the blockchain starting from the genesis. In this case the genesis is fetched from the bootstrap node, the genesis signature is verified, and the genesis is persisted to the local block store of the new node. All the confirmed blocks are fetched either from the bootstrap node or any other know peer that has newer confirmed blocks that the bootstrap node. The node state sync is also performed when an out-of-sync node is catching up with the latest confirmed blocks on the blockchain. In this case the genesis and the initial confirmed blocks are already verified, validated, and persisted. The initial confirmed blocks are read from the local block store and applied to the confirmed state of the node. Only the newer confirmed blocks are fetched from the bootstrap node and all known peers. Regardless of a new node or an out-of-sync node, every fetched block including the genesis goes through the verification of the signature and the full block validation process in the form of the block application. Every verified, validated, and confirmed block is applied to the confirmed state of the node and appended to the local block store. Once the node state sync process is finished the node is ready to receive, validate, and relay new transactions; to receive, validated, and relay new proposed blocks. The node state sync process is only executed when the node starts either from the empty state or from an out-of-sync state
- State sync type
- The
StateSync
type implements the state sync algorithm to initialize the bootstrap node or synchronize an out-of-sync node. The state sync type contains the node configuration including the node address, the seed address, the key store directory, the block store directory, the genesis configuration; the node shared context hierarchy for the graceful shutdown, the reference to the confirmed state for the application of confirmed blocks read from the local block store or fetched from all known peers, and the reference to the peer reader of the peer discovery for fetching of the genesis and newer confirmed blocks. The state sync typecfg NodeCfg
Node configuration ctx context.Context
Node shared context hierarchy state *chain.State
Confirmed and pending state peerReader PeerReader
Peer reader type StateSync struct { cfg NodeCfg ctx context.Context state *chain.State peerReader PeerReader } func NewStateSync( ctx context.Context, cfg NodeCfg, peerReader PeerReader, ) *StateSync { return &StateSync{ctx: ctx, cfg: cfg, peerReader: peerReader} }
- State sync algorithm
- The state sync algorithm covers both the
initialization of the bootstrap node and the synchronization of an out-of-sync
node. The algorithm reads the genesis from the local block store. If the
genesis is not present in the local block store, the genesis is created for
the bootstrap node, or the genesis is fetched from the bootstrap node. In
either case the genesis signature is verified and the new blockchain state is
initialized with the genesis. Next the local block store is initialized if
necessary. Then the confirmed blocks from the local block store are read and
applied to the confirmed state. Finally, the new confirmed blocks are fetched
from all known peers, verified, validated, and applied to the confirmed state.
The state sync algorithm
- Read the genesis from the local block store. If the genesis is not present
- For the bootstrap node create, sign, and persist the genesis to the local block store
- For a new node fetch the genesis from the bootstrap node, verify, validate, and persist the genesis to the local block store
- Initialize the confirmed state with the genesis
- Initialize the local block store if necessary
- Verify, validate, and apply to the confirmed state the confirmed blocks read from the local block store
- Verify, validate, and apply to the confirmed sate the confirmed blocks fetched from all known peers
func (s *StateSync) SyncState() (*chain.State, error) { gen, err := chain.ReadGenesis(s.cfg.BlockStoreDir) if err != nil { if s.cfg.Bootstrap { gen, err = s.createGenesis() if err != nil { return nil, err } } else { gen, err = s.syncGenesis() if err != nil { return nil, err } } } valid, err := chain.VerifyGen(gen) if err != nil { return nil, err } if !valid { return nil, fmt.Errorf("invalid genesis signature") } s.state = chain.NewState(gen) err = chain.InitBlockStore(s.cfg.BlockStoreDir) if err != nil { return nil, err } err = s.readBlocks() if err != nil { return nil, err } err = s.syncBlocks() if err != nil { return nil, err } fmt.Printf("=== Sync state\n%v", s.state) return s.state, nil }
- Read the genesis from the local block store. If the genesis is not present
- Create genesis
- The genesis creation process is performed once on the
bootstrap node during the initialization of a new blockchain. The authority
account is created and protected with the authority account password. The
authority account is used to sign the genesis and sign the proposed blocks on
the blockchain. The initial owner account is created and protected with the
owner password. The initial owner account has the initial balance on the
blockchain. The new genesis is created by providing the blockchain name, the
authority account address, the initial owner account address and the initial
balance. The new genesis is signed by the authority account and is persisted
to the local block store of the bootstrap node. The genesis creation process
- Create and persist the authority account for signing the genesis and proposed blocks
- Create and persist the initial owner account with the initial blockchain balance
- Create and sign the genesis with the authority account
- Persist the genesis to the local block store of the bootstrap node
func (s *StateSync) createGenesis() (chain.SigGenesis, error) { authPass := []byte(s.cfg.AuthPass) if len(authPass) < 5 { return chain.SigGenesis{}, fmt.Errorf("authpass length is less than 5") } auth, err := chain.NewAccount() if err != nil { return chain.SigGenesis{}, err } err = auth.Write(s.cfg.KeyStoreDir, authPass) if err != nil { return chain.SigGenesis{}, err } ownerPass := []byte(s.cfg.OwnerPass) if len(ownerPass) < 5 { return chain.SigGenesis{}, fmt.Errorf("ownerpass length is less than 5") } if s.cfg.Balance == 0 { return chain.SigGenesis{}, fmt.Errorf("balance must be positive") } acc, err := chain.NewAccount() if err != nil { return chain.SigGenesis{}, err } err = acc.Write(s.cfg.KeyStoreDir, ownerPass) s.cfg.OwnerPass = "erase" if err != nil { return chain.SigGenesis{}, err } gen := chain.NewGenesis( s.cfg.Chain, auth.Address(), acc.Address(), s.cfg.Balance, ) sgen, err := auth.SignGen(gen) if err != nil { return chain.SigGenesis{}, err } err = sgen.Write(s.cfg.BlockStoreDir) if err != nil { return chain.SigGenesis{}, err } return sgen, nil }
- Sync genesis
- The genesis sync process is performed once for every new node
when the new node joins the already initialized blockchain with the running
bootstrap node. The genesis sync process contributes to the immutability and
the integrity of the blockchain by ensuring that exactly the same copy of the
genesis is stored in the local block store of every node on the blockchain.
The genesis sync process fetches the encoded and signed genesis from the
bootstrap node. Then the encoded genesis is decoded. Next the genesis
signature is verified. Finally, the verified genesis is persisted to the local
block store of the new node. The genesis sync process
- Fetch the encoded and signed genesis from the bootstrap node
- Decode the fetched genesis
- Verify that the genesis signature is valid
- Persist the verified genesis to the local block store
func (s *StateSync) syncGenesis() (chain.SigGenesis, error) { jgen, err := s.grpcGenesisSync() if err != nil { return chain.SigGenesis{}, err } var gen chain.SigGenesis err = json.Unmarshal(jgen, &gen) if err != nil { return chain.SigGenesis{}, err } valid, err := chain.VerifyGen(gen) if err != nil { return chain.SigGenesis{}, err } if !valid { return chain.SigGenesis{}, fmt.Errorf("invalid genesis signature") } err = gen.Write(s.cfg.BlockStoreDir) if err != nil { return chain.SigGenesis{}, err } return gen, nil }
- Read blocks
- The block reading process is executed every time the node is
restarted. First the genesis is read and the state is initialized with the
genesis. Then all confirmed blocks from the local block store are read in
order and applied to the confirmed state. The block reading process brings the
node to the state when the node left off the last time. The block reading
process creates the iterator over the confirmed blocks in the local block
store. Each confirmed block returned in order by the iterator is applied to
the cloned state, and, if successful, the cloned state is applied to the
confirmed state. The block application process ensures the integrity and the
immutability of the blockchain state on every node. The block reading process
- Create the iterator for reading the confirmed blocks in order from the local block store
- Defer closing the block iterator
- Iterate over the confirmed blocks in order. For each confirmed block
- Apply the confirmed block to the cloned state, if successful
- Apply the cloned state to the confirmed state
func (s *StateSync) readBlocks() error { blocks, closeBlocks, err := chain.ReadBlocks(s.cfg.BlockStoreDir) if err != nil { return err } defer closeBlocks() for err, blk := range blocks { if err != nil { return err } clone := s.state.Clone() err = clone.ApplyBlock(blk) if err != nil { return err } s.state.Apply(clone) } return nil }
- Sync blocks
- The block sync process propagates the recent confirmed blocks
through the blockchain network during the initialization of a new node or the
synchronization of an out-of-sync node on the blockchain. The block sync
process contributes to the immutability and the integrity of the blockchain by
ensuring that exactly the same blocks in the same order are stored on every
node of the blockchain. For every known peer the block sync process fetches
the new confirmed blocks starting from the block number next to the last
confirmed block number on the requesting node. Each fetched block is decoded.
Then the block is applied to the cloned state, and, if successful, the cloned
state is applied to the confirmed state. Finally, after successful
application, the new confirmed block is persisted to the local block store.
The block sync process
- For each known peer fetch new confirmed blocks starting from the block
number next to the last confirmed block number on the requesting node
- For each fetched block
- Decode the fetched block
- Apply the decoded block to the local cloned state, if successful
- Apply the cloned state to the confirmed state
- Persist the confirmed block to the local block store
- For each fetched block
func (s *StateSync) syncBlocks() error { for _, peer := range s.peerReader.Peers() { blocks, closeBlocks, err := s.grpcBlockSync(peer) if err != nil { return err } defer closeBlocks() for err, jblk := range blocks { if err != nil { return err } var blk chain.SigBlock err = json.Unmarshal(jblk, &blk) if err != nil { return err } clone := s.state.Clone() err = clone.ApplyBlock(blk) if err != nil { return err } s.state.Apply(clone) err = blk.Write(s.cfg.BlockStoreDir) if err != nil { return err } } } return nil }
- For each known peer fetch new confirmed blocks starting from the block
number next to the last confirmed block number on the requesting node
The gRPC Block
service provides the GenesisSync
method to fetch the encoded
and signed genesis from the bootstrap node. The interface of the service
message GenesisSyncReq { }
message GenesisSyncRes {
bytes Genesis = 1;
}
service Block {
rpc GenesisSync(GenesisSyncReq) returns (GenesisSyncRes);
}
The implementation of the GenesisSync
method
- Read and return the encoded and signed genesis
func (s *BlockSrv) GenesisSync(
_ context.Context, req *GenesisSyncReq,
) (*GenesisSyncRes, error) {
jgen, err := chain.ReadGenesisBytes(s.blockStoreDir)
if err != nil {
return nil, status.Errorf(codes.NotFound, err.Error())
}
res := &GenesisSyncRes{Genesis: jgen}
return res, nil
}
The gRPC Block
service provides the BlockSync
method to fetch the newer
confirmed blocks the all known peers starting from a specified block number. All
newer blocks starting from the specified block number are returned to the client
through the gRPC server stream. The interface of the service
message BlockSyncReq {
uint64 Number = 1;
}
message BlockSyncRes {
bytes Block = 1;
}
service Block {
rpc BlockSync(BlockSyncReq) returns (stream BlockSyncRes);
}
The implementation of the BlockSync
method
- Create the iterator for confirmed blocks from the local block store
- Defer closing the block iterator
- Send each block staring from the requested block number over the gRPC server stream to the client
func (s *BlockSrv) BlockSync(
req *BlockSyncReq, stream grpc.ServerStreamingServer[BlockSyncRes],
) error {
blocks, closeBlocks, err := chain.ReadBlocksBytes(s.blockStoreDir)
if err != nil {
return status.Errorf(codes.NotFound, err.Error())
}
defer closeBlocks()
num, i := int(req.Number), 1
for err, jblk := range blocks {
if err != nil {
return status.Errorf(codes.Internal, err.Error())
}
if i >= num {
res := &BlockSyncRes{Block: jblk}
err = stream.Send(res)
if err != nil {
return status.Errorf(codes.Internal, err.Error())
}
}
i++
}
return nil
}
The TestGenesisSync
testing process
- Create and persist the genesis
- Create the state from the genesis
- Set up the gRPC server and client
- Create the gRPC block client
- Call the
GenesysSync
method to fetch the genesis - Decode the received genesis
- Verify that the signature of the received genesis is valid
go test -v -cover -coverprofile=coverage.cov ./... -run GenesisSync
The TestBlockSync
testing process
- Create and persist the genesis
- Create the state from the genesis
- Create several confirmed blocks on the state and on the local block store
- Set up the gRPC server and client
- Create the gRPC block client
- Call the
BlockSync
method to get the gRPC server stream of confirmed blocks - Start receiving confirmed blocks from the gRPC server stream. For each block
received from the gRPC server stream
- Decode the received block
- Verify that the signature of the received block is valid
- Verify that the received block number and its parent hash equal to the block number and the parent hash of the last confirmed block
go test -v -cover -coverprofile=coverage.cov ./... -run BlockSync
The TestStateSync
testing process
- Set up the bootstrap node
- Create the peer discovery without starting for the bootstrap node
- Initialize the state on the bootstrap node by creating the genesis
- Get the initial owner account and its balance from the genesis
- Verify that the initial owner balance from the confirmed state on the bootstrap node is equal to the initial owner balance from the genesis
- Create several confirmed blocks on the bootstrap node
- Start the gRPC server on the bootstrap node
- Wait for the gRPC server of the bootstrap node to start
- Set up the new node
- Create the peer discovery without starting for the new node
- Synchronize the state on the new node by fetching the genesis and all confirmed blocks from the bootstrap node
- Verify that the last block number and the last block parent on the confirmed sate of the the new node and the bootstrap node are equal
go test -v -cover -coverprofile=coverage.cov ./... -run StateSync
The node start command is the main entry point for initialization, starting, and restarting of a blockchain node. There are two types of nodes on this blockchain
- Bootstrap and authority node
- The bootstrap and the authority node in this blockchain is the single authority node that initializes the blockchain by creating and signing the genesis on the first start; creates, signs, proposes, and relays new blocks; and serves as the bootstrap node for the initial peer discovery. The bootstrap and the authority node also accepts, validates and relays new transactions to other nodes; streams the blockchain events e.g. confirmed blocks, confirmed transactions to the subscribed clients; maintains the confirmed and the pending in-memory state, and the on-disk local block store with confirmed blocks
- Regular node
- The regular node participates in the peer discovery; accepts, validates, and relays new transactions from clients to other nodes; receives, validates, and relays new transactions and proposed blocks to other nodes. The regular node streams the blockchain events e.g. confirmed blocks, confirmed transactions to the subscribed clients; maintains the confirmed and the pending in-memory state and the on-disk local block store with confirmed blocks
The node start
parameters
--node
specifies the node address--bootstrap
makes the node the bootstrap node for the initial peer discovery and also makes the node the authority node for signing the genesis, proposing and signing new blocks--seed
specifies the seed bootstrap address for a regular node--keystore
defines the key store directory on the local file system to store password-protected key pairs of blockchain accounts created on the node--blockstore
defines the block store directory on the local file system to store confirmed blocks on the blockchain--chain
specifies the name of the blockchain to be included in the genesis--authpass
provides a password for the authority account to sign the genesis and all proposed blocks on the blockchain--ownerpass
provides a password for the initial owner account on the blockchain--balance
specifies the balance for the initial owner account on the blockchain
Options for starting the blockchain node
- Initialize the bootstrap and the authority node
set boot localhost:1122
set authpass password
set ownerpass password
./bcn node start --node $boot --bootstrap --authpass $authpass \
--ownerpass $ownerpass --balance 1000
# === Sync state
# * Balances and nonces
# acc ce68e8c: 1000 0
# * Last block
# blk 0: 79b4f47 -> 0000000 mrk 0000000
# * Pending balances and nonces
# acc ce68e8c: 1000 0
# <=> gRPC localhost:1122
# <=> Blk relay: localhost:1122
- Start the already initialized bootstrap node and the authority node
./bcn node start --node $boot --bootstrap --authpass $authpass
# === Sync state
# * Balances and nonces
# acc ce68e8c: 1000 0
# * Last block
# blk 0: 79b4f47 -> 0000000 mrk 0000000
# * Pending balances and nonces
# acc ce68e8c: 1000 0
# <=> gRPC localhost:1122
# <=> Blk relay: localhost:1122
- Start a regular node with the seed bootstrap address
set node localhost:1123
./bcn node start --node $node --seed $node
# <=> Peer localhost:1122
# === Sync state
# * Balances and nonces
# acc ce68e8c: 1000 0
# * Last block
# blk 0: 79b4f47 -> 0000000 mrk 0000000
# * Pending balances and nonces
# acc ce68e8c: 1000 0
# <=> gRPC localhost:1123
# <=> Blk relay: localhost:1122
# <=> Blk relay: localhost:1123
# <=> Tx relay: localhost:1122