- Proof of Authority
- The PoA centralized consensus relies on the designated set of accountable authority nodes that put at stake their identity and their reputation in order to create, sign and propose new blocks on the blockchain. The PoA consensus requires other validator nodes to trust the designated set of authority nodes. The designated authority nodes are incentivized by the risk of reputational damage or the complete loss of the reputation in the case of dishonest or malicious behavior. The designated authority nodes take turns to create, sign, and propose blocks. The proposed blocks must be validated by the majority of other validator nodes in order to become confirmed. The advantages of the PoA centralized consensus are the fast and efficient centralized agreement, the high transaction throughput, the low transaction confirmation time, the low computational overhead, the low energy consumption, a simple implementation. The disadvantages of the PoA centralized consensus are the centralization of control over the blockchain by the designated set of authority nodes, the high security risk if some or all of authority nodes are compromised, the PoA centralized consensus requires the validator nodes to completely trust the designated set of authority nodes. The PoA centralized consensus is the most suitable for high-throughput, efficient, permissioned or private blockchains
- Block proposer type
- The block proposer type implements the single authority
multiple validators PoA centralized consensus. The single authority node is
also the bootstrap node and holds the authority account that signs the genesis
and all proposed blocks. The single authority creates, signs and proposes
through the block relay mechanism new blocks to the list of known peers
including the authority node itself. On reception of a proposed block through
the block relay mechanism every validator node, including the authority node
itself, validates the block against the cloned state, and, if successful,
applies the block to the confirmed state. After the successful block
application, the validated block is further relayed to the list of known
peers, other validator nodes on the blockchain. The application of already
applied block relayed from other validators results in a block application
error and the duplicated block is not relayed any more. Relay of only
successfully applied blocks prevents the propagation of duplicates of already
applied blocks. There is no possibility of the blockchain fork, as there is
only the single authority node that proposes blocks on the blockchain. The
block proposer type is fully integrated into the node graceful shutdown
mechanism through the node shared context hierarchy to signal the graceful
shutdown and the node shared wait group to wait for the concurrent node
processes to gracefully terminate. The block proposer type contains the
authority account to sign proposed blocks, the confirmed state to apply the
confirmed blocks, the pending state with the list of pending transactions to
create new blocks to be proposed, and the block relayer to relay the proposed
blocks. The advantages of using the single authority PoA centralized consensus
algorithm with multiple validators are the simple to understand and the simple
to implement algorithm, the easily traceable behavior helps to understand and
troubleshoot the block proposal and propagation in the peer-to-peer network
ctx context.Context
Node shared context hierarchy wg *sync.WaitGroup
Node shared wait group authority chain.Account
Authority account state *chain.State
Confirmed and pending state blkRelayer BlockRelayer
Block relayer type BlockProposer struct { ctx context.Context wg *sync.WaitGroup authority chain.Account state *chain.State blkRelayer BlockRelayer } func NewBlockProposer( ctx context.Context, wg *sync.WaitGroup, blkRelayer BlockRelayer, ) *BlockProposer { return &BlockProposer{ctx: ctx, wg: wg, blkRelayer: blkRelayer} }
- Block proposer algorithm
- The block proposer algorithm combines the node
graceful shutdown mechanism with the periodic block creation, signing, and
proposal. The block proposer algorithm in this blockchain is only performed on
the authority node that is also the bootstrap node. The block proposal happens
periodically with a random delay to introduce some randomness to the moments
when new blocks are proposed. The random delay in the block proposal is
parameterized by the max period ensuring that the next block proposal happens
in between the time frame of [1/2, 3/2] of the max period. The block proposal
algorithm resets the block proposal timer with the random delay for the next
block proposal. Then the new block with all pending transactions is created
from the cloned state. The non-empty new block is than applied to the cloned
state and, if successful, the block is proposed through the block relay
mechanism on the peer-to-peer network for other validators to validate. On any
error during the block creation or the block application, the failed block is
not proposed to the peer-to-peer network and the current block proposal cycle
finishes without proposing a block. The block proposer algorithm
- Schedule the timer with a random delay parameterized by the max period within the time frame of [1/2, 3/2] of the max period for the next block proposal
- Combine the cancellation channel of the node shared context hierarchy with
the timer channel of the random block proposal
- When the node shared context cancellation happens, stop the block proposal timer and stop the block proposal process
- When the random block proposal timer expires, reset the block proposal timer with the next random block proposal moment, create a new block on the cloned state, apply the block to the cloned state, if successful, relay the proposed block to the peer-to-peer network of validator nodes
func randPeriod(maxPeriod time.Duration) time.Duration { minPeriod := maxPeriod / 2 randSpan, _ := rand.Int(rand.Reader, big.NewInt(int64(maxPeriod))) return minPeriod + time.Duration(randSpan.Int64()) } func (p *BlockProposer) ProposeBlocks(maxPeriod time.Duration) { defer p.wg.Done() randPropose := time.NewTimer(randPeriod(maxPeriod)) for { select { case <- p.ctx.Done(): randPropose.Stop() return case <- randPropose.C: randPropose.Reset(randPeriod(maxPeriod)) clone := p.state.Clone() blk, err := clone.CreateBlock(p.authority) if err != nil { continue } if len(blk.Txs) == 0 { continue } clone = p.state.Clone() err = clone.ApplyBlock(blk) if err != nil { fmt.Println(err) continue } if p.blkRelayer != nil { p.blkRelayer.RelayBlock(blk) } fmt.Printf("==> Block propose\n%v", blk) } } }
- Block relay mechanism
- The block relay mechanism propagates proposed blocks through the peer-to-peer network to all validators including the authority node that creates and proposes blocks using the self-relay function of the message relay mechanism. The block relay mechanism does not relay received blocks the the received blocks do not pass the block application. This happens when an already applied block is relayed again to the validator. This design prevents propagation of duplicated blocks. The block relay mechanism reuses the message relay infrastructure that is also used for the transaction relay. Specifically, the message relay algorithm is reused. The message relay algorithm is parameterized with the signed block type and the block-specific gRPC relay function to adapt to the block relay use case. The block relay mechanism also uses the self-relay function of the message relay infrastructure. The authority node relays proposed blocks not only to the list of known peers, but also to the authority node itself for the block validation and the block confirmation using the self-relay function. This design clearly separates the block proposal function from the block validation and block confirmation functions on the authority node reusing the same block validation and confirmation mechanisms used by other validators
- Transaction relay through gRPC client streaming
- The gRPC client streaming
relays blocks from the outbound block relay channel to the gRPC client stream
of blocks. The gRPC client streaming is message type specific and is
parameterized in the message relay type with the gRPC relay generic function.
The gRPC relay generic function accepts the node shared context hierarchy, the
gRPC client connection, and the outbound block relay channel. The gRPC client
streaming creates the message-specific gRPC clients and establishes the gRPC
client stream. The gRPC client streaming combines the node shared context
cancellation channel for the graceful shutdown with the outbound block relay
channel for streaming blocks to the peer. When a new message is sent to the
outbound block relay channel, the message is encoded and sent over the gRPC
client stream to the peer. The block relay through the gRPC client streaming
- Create the gRPC block client
- Call the gRPC
BlockReceive
method to establish the gRPC client stream - Combine the cancellation channel of the node shared context hierarchy with
the outbound block relay channel
- When the node shared context hierarchy is canceled, close the gRPC client connection and stop the block relay to the peer
- When a new block is sent to the outbound block relay channel, forward the block to the established gRPC client stream
type GRPCMsgRelay[Msg any] func( ctx context.Context, conn *grpc.ClientConn, chRelay chan Msg, ) error var GRPCBlockRelay GRPCMsgRelay[chain.SigBlock] = func( ctx context.Context, conn *grpc.ClientConn, chRelay chan chain.SigBlock, ) error { cln := rpc.NewBlockClient(conn) stream, err := cln.BlockReceive(ctx) if err != nil { return err } defer stream.CloseAndRecv() for { select { case <- ctx.Done(): return nil case blk, open := <- chRelay: if !open { return nil } jblk, err := json.Marshal(blk) if err != nil { fmt.Println(err) continue } req := &rpc.BlockReceiveReq{Block: jblk} err = stream.Send(req) if err != nil { fmt.Println(err) continue } } } }
The gRPC Block
service provides the BlockReceive
method to receive blocks
relayed from the peer-to-peer network of the blockchain. The block relay happens
from the ProposeBlocks
method of the block proposer type and from the gRPC
BlockReceive
method to further relay validated blocks to other peers. The
block relay forwards blocks to other peers through the gRPC client streaming.
The interface of the service
message BlockReceiveReq {
bytes Block = 1;
}
message BlockReceiveRes { }
service Block {
rpc BlockReceive(stream BlockReceiveReq) returns (BlockReceiveRes);
}
The implementation of the BlockReceive
method
- For each block received from the gRPC client stream
- Decode the block
- Apply the decoded block to the cloned state, if successful,
- Apply the cloned state to the confirmed state
- Persist the block to the local block store of the node
- Relay the confirmed block to the list of known peers
- Publish the confirmed blocks with all confirmed transactions to the node event stream
func (s *BlockSrv) BlockReceive(
stream grpc.ClientStreamingServer[BlockReceiveReq, BlockReceiveRes],
) error {
for {
req, err := stream.Recv()
if err == io.EOF {
res := &BlockReceiveRes{}
return stream.SendAndClose(res)
}
if err != nil {
return status.Errorf(codes.Internal, err.Error())
}
var blk chain.SigBlock
err = json.Unmarshal(req.Block, &blk)
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("<== Block receive\n%v", blk)
err = s.blkApplier.ApplyBlockToState(blk)
if err != nil {
fmt.Print(err)
continue
}
err = blk.Write(s.blockStoreDir)
if err != nil {
fmt.Println(err)
continue
}
if s.blkRelayer != nil {
s.blkRelayer.RelayBlock(blk)
}
if s.eventPub != nil {
s.publishBlockAndTxs(blk)
}
}
}
The TestBlockReceive
testing process
- Create and persist the genesis
- Create the state from the genesis
- Get the initial owner account and its balance from the genesis
- Re-create the initial owner account from the genesis
- Re-create the authority account from the genesis to sign blocks
- Create several transactions on the pending state
- Create a new block on the cloned state
- Set up the gRPC server and gRPC client
- Create the gRPC block client
- Call the
BlockReceive
method go get the gRPC client stream to relay validated blocks - Start relaying validated blocks to the gRPC client stream. For the created
block
- Encode the validated block
- Send the encoded block over the gRPC client stream
- Wait for the relayed block to be received and processed
- Verify that the balance of the initial owner account on the confirmed state after receiving the relayed block is correct
go test -v -cover -coverprofile=coverage.cov ./... -run BlockReceive
The TestBlockProposer
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
- Create and start the block relay for the bootstrap node
- Re-create the authority account from the genesis to sign blocks
- Create and start the block proposer on the bootstrap node
- Start the gRPC server on the bootstrap node
- Set up the new node
- Create and start the peer discovery for the new node
- Wait for the peer discovery to discover peers
- Synchronize the state on the new node by fetching the genesis and confirmed blocks from the bootstrap node
- Start the gRPC server on the new node
- Wait for the gRPC server of the new node to start
- Get the initial owner account and its balance from the genesis
- Re-create the initial owner account from the genesis
- Sign and send several signed transactions to the bootstrap node
- Wait for the block proposal to propose a block and the block relay to propagate the proposed block
- Verify that the initial account balance on the confirmed state of the new node and the bootstrap node are equal
go test -v -cover -coverprofile=coverage.cov ./... -run BlockProposer