package dbft import ( "crypto/rand" "encoding/binary" "time" ) // HeightView is a block height/consensus view pair. type HeightView struct { Height uint32 View byte } // Context is a main dBFT structure which // contains all information needed for performing transitions. type Context[H Hash] struct { // Config is dBFT's Config instance. Config *Config[H] // Priv is node's private key. Priv PrivateKey // Pub is node's public key. Pub PublicKey preBlock PreBlock[H] preHeader PreBlock[H] block Block[H] header Block[H] // blockProcessed denotes whether Config.ProcessBlock callback was called for the current // height. If so, then no second call must happen. After new block is received by the user, // dBFT stops any new transaction or messages processing as far as timeouts handling till // the next call to Reset. blockProcessed bool // preBlockProcessed is true when Config.ProcessPreBlock callback was // invoked for the current height. This happens once and dbft continues // to march towards proper commit after that. preBlockProcessed bool // BlockIndex is current block index. BlockIndex uint32 // ViewNumber is current view number. ViewNumber byte // Validators is a current validator list. Validators []PublicKey // MyIndex is an index of the current node in the Validators array. // It is equal to -1 if node is not a validator or is WatchOnly. MyIndex int // PrimaryIndex is an index of the primary node in the current epoch. PrimaryIndex uint // PrevHash is a hash of the previous block. PrevHash H // Timestamp is a nanosecond-precision timestamp Timestamp uint64 Nonce uint64 // TransactionHashes is a slice of hashes of proposed transactions in the current block. TransactionHashes []H // MissingTransactions is a slice of hashes containing missing transactions for the current block. MissingTransactions []H // Transactions is a map containing actual transactions for the current block. Transactions map[H]Transaction[H] // PreparationPayloads stores consensus Prepare* payloads for the current epoch. PreparationPayloads []ConsensusPayload[H] // PreCommitPayloads stores consensus PreCommit payloads sent through all epochs // as a part of anti-MEV dBFT extension. It is assumed that valid PreCommit // payloads can only be sent once by a single node per the whole set of consensus // epochs for particular block. Invalid PreCommit payloads are kicked off this // list immediately (if PrepareRequest was received for the current round, so // it's possible to verify PreCommit against PreBlock built on PrepareRequest) // or stored till the corresponding PrepareRequest receiving. PreCommitPayloads []ConsensusPayload[H] // CommitPayloads stores consensus Commit payloads sent throughout all epochs. It // is assumed that valid Commit payload can only be sent once by a single node per // the whole set of consensus epochs for particular block. Invalid commit payloads // are kicked off this list immediately (if PrepareRequest was received for the // current round, so it's possible to verify Commit against it) or stored till // the corresponding PrepareRequest receiving. CommitPayloads []ConsensusPayload[H] // ChangeViewPayloads stores consensus ChangeView payloads for the current epoch. ChangeViewPayloads []ConsensusPayload[H] // LastChangeViewPayloads stores consensus ChangeView payloads for the last epoch. LastChangeViewPayloads []ConsensusPayload[H] // LastSeenMessage array stores the height and view of the last seen message, for each validator. // If this node never heard a thing from validator i, LastSeenMessage[i] will be nil. LastSeenMessage []*HeightView lastBlockTimestamp uint64 // ns-precision timestamp from the last header (used for the next block timestamp calculations). lastBlockTime time.Time // Wall clock time of when we started (as in PrepareRequest) creating the last block (used for timer adjustments). lastBlockIndex uint32 lastBlockView byte timePerBlock time.Duration // minimum amount of time that need to pass before the pending block will be accepted if there are some transactions in the proposal. maxTimePerBlock time.Duration // maximum amount of time that allowed to pass before the pending block will be accepted even if there's no transactions in the proposal. txSubscriptionOn bool prepareSentTime time.Time rttEstimates rtt } // N returns total number of validators. func (c *Context[H]) N() int { return len(c.Validators) } // F returns number of validators which can be faulty. func (c *Context[H]) F() int { return (len(c.Validators) - 1) / 3 } // M returns number of validators which must function correctly. func (c *Context[H]) M() int { return len(c.Validators) - c.F() } // GetPrimaryIndex returns index of a primary node for the specified view. func (c *Context[H]) GetPrimaryIndex(viewNumber byte) uint { p := (int(c.BlockIndex) - int(viewNumber)) % len(c.Validators) if p >= 0 { return uint(p) } return uint(p + len(c.Validators)) } // IsPrimary returns true iff node is primary for current height and view. func (c *Context[H]) IsPrimary() bool { return c.MyIndex == int(c.PrimaryIndex) } // IsBackup returns true iff node is backup for current height and view. func (c *Context[H]) IsBackup() bool { return c.MyIndex >= 0 && !c.IsPrimary() } // WatchOnly returns true iff node takes no active part in consensus. func (c *Context[H]) WatchOnly() bool { return c.MyIndex < 0 || c.Config.WatchOnly() } // CountCommitted returns number of received Commit (or PreCommit for anti-MEV // extension) messages not only for the current epoch but also for any other epoch. func (c *Context[H]) CountCommitted() (count int) { for i := range c.CommitPayloads { // Consider both Commit and PreCommit payloads since both Commit and PreCommit // phases are one-directional (do not impose view change). if c.CommitPayloads[i] != nil || c.PreCommitPayloads[i] != nil { count++ } } return } // CountFailed returns number of nodes with which no communication was performed // for this view and that hasn't sent the Commit message at the previous views. func (c *Context[H]) CountFailed() (count int) { for i, hv := range c.LastSeenMessage { if (c.CommitPayloads[i] == nil && c.PreCommitPayloads[i] == nil) && (hv == nil || hv.Height < c.BlockIndex || hv.View < c.ViewNumber) { count++ } } return } // RequestSentOrReceived returns true iff PrepareRequest // was sent or received for the current epoch. func (c *Context[H]) RequestSentOrReceived() bool { return c.PreparationPayloads[c.PrimaryIndex] != nil } // ResponseSent returns true iff Prepare* message was sent for the current epoch. func (c *Context[H]) ResponseSent() bool { return !c.WatchOnly() && c.PreparationPayloads[c.MyIndex] != nil } // PreCommitSent returns true iff PreCommit message was sent for the current epoch // assuming that the node can't go further than current epoch after PreCommit was sent. func (c *Context[H]) PreCommitSent() bool { return !c.WatchOnly() && c.PreCommitPayloads[c.MyIndex] != nil } // CommitSent returns true iff Commit message was sent for the current epoch // assuming that the node can't go further than current epoch after commit was sent. func (c *Context[H]) CommitSent() bool { return !c.WatchOnly() && c.CommitPayloads[c.MyIndex] != nil } // BlockSent returns true iff block was formed AND sent for the current height. // Once block is sent, the consensus stops new transactions and messages processing // as far as timeouts handling. // // Implementation note: the implementation of BlockSent differs from the C#'s one. // In C# algorithm they use ConsensusContext's Block.Transactions null check to define // whether block was formed, and the only place where the block can be formed is // in the ConsensusContext's CreateBlock function right after enough Commits receiving. // On the contrary, in our implementation we don't have access to the block's // Transactions field as far as we can't use block null check, because there are // several places where the call to CreateBlock happens (one of them is right after // PrepareRequest receiving). Thus, we have a separate Context.blockProcessed field // for the described purpose. func (c *Context[H]) BlockSent() bool { return c.blockProcessed } // ViewChanging returns true iff node is in a process of changing view. func (c *Context[H]) ViewChanging() bool { if c.WatchOnly() { return false } cv := c.ChangeViewPayloads[c.MyIndex] return cv != nil && cv.GetChangeView().NewViewNumber() > c.ViewNumber } // NotAcceptingPayloadsDueToViewChanging returns true if node should not accept new payloads. func (c *Context[H]) NotAcceptingPayloadsDueToViewChanging() bool { return c.ViewChanging() && !c.MoreThanFNodesCommittedOrLost() } // MoreThanFNodesCommittedOrLost returns true iff a number of nodes which either committed // or are faulty is more than maximum amount of allowed faulty nodes. // A possible attack can happen if the last node to commit is malicious and either sends change view after his // commit to stall nodes in a higher view, or if he refuses to send recovery messages. In addition, if a node // asking change views loses network or crashes and comes back when nodes are committed in more than one higher // numbered view, it is possible for the node accepting recovery to commit in any of the higher views, thus // potentially splitting nodes among views and stalling the network. func (c *Context[H]) MoreThanFNodesCommittedOrLost() bool { return c.CountCommitted()+c.CountFailed() > c.F() } // Header returns current header from context. May be nil in case if no // header is constructed yet. Do not change the resulting header. func (c *Context[H]) Header() Block[H] { return c.header } // PreHeader returns current preHeader from context. May be nil in case if no // preHeader is constructed yet. Do not change the resulting preHeader. func (c *Context[H]) PreHeader() PreBlock[H] { return c.preHeader } // PreBlock returns current PreBlock from context. May be nil in case if no // PreBlock is constructed yet (even if PreHeader is already constructed). // External changes in the PreBlock will be seen by dBFT. func (c *Context[H]) PreBlock() PreBlock[H] { return c.preBlock } func (c *Context[H]) reset(view byte, ts uint64) { c.MyIndex = -1 c.prepareSentTime = time.Time{} c.lastBlockTimestamp = ts c.unsubscribeFromTransactions() if view == 0 { c.PrevHash = c.Config.CurrentBlockHash() c.BlockIndex = c.Config.CurrentHeight() + 1 c.Validators = c.Config.GetValidators() c.timePerBlock = c.Config.TimePerBlock() if c.Config.MaxTimePerBlock != nil { c.maxTimePerBlock = c.Config.MaxTimePerBlock() } n := len(c.Validators) c.LastChangeViewPayloads = emptyReusableSlice(c.LastChangeViewPayloads, n) c.LastSeenMessage = emptyReusableSlice(c.LastSeenMessage, n) c.blockProcessed = false c.preBlockProcessed = false } else { for i := range c.Validators { m := c.ChangeViewPayloads[i] if m != nil && m.GetChangeView().NewViewNumber() >= view { c.LastChangeViewPayloads[i] = m } else { c.LastChangeViewPayloads[i] = nil } } } c.MyIndex, c.Priv, c.Pub = c.Config.GetKeyPair(c.Validators) c.block = nil c.preBlock = nil c.header = nil c.preHeader = nil n := len(c.Validators) c.ChangeViewPayloads = emptyReusableSlice(c.ChangeViewPayloads, n) if view == 0 { c.PreCommitPayloads = emptyReusableSlice(c.PreCommitPayloads, n) c.CommitPayloads = emptyReusableSlice(c.CommitPayloads, n) } c.PreparationPayloads = emptyReusableSlice(c.PreparationPayloads, n) if c.Transactions == nil { // Init. c.Transactions = make(map[H]Transaction[H]) } else { // Regular use. clear(c.Transactions) } c.TransactionHashes = nil if c.MissingTransactions != nil { c.MissingTransactions = c.MissingTransactions[:0] } c.PrimaryIndex = c.GetPrimaryIndex(view) c.ViewNumber = view if c.MyIndex >= 0 { c.LastSeenMessage[c.MyIndex] = &HeightView{c.BlockIndex, c.ViewNumber} } } func emptyReusableSlice[E any](s []E, n int) []E { if len(s) == n { clear(s) return s } return make([]E, n) } // Fill initializes consensus when node is a speaker. It doesn't perform any // context modifications if MaxTimePerBlock extension is enabled and there are // no transactions in the memory pool and force is not set. func (c *Context[H]) Fill(force bool) bool { txx := c.Config.GetVerified() if c.Config.MaxTimePerBlock != nil && !force && len(txx) == 0 { return false } b := make([]byte, 8) _, _ = rand.Read(b) c.Nonce = binary.LittleEndian.Uint64(b) c.TransactionHashes = make([]H, len(txx)) for i := range txx { h := txx[i].Hash() c.TransactionHashes[i] = h c.Transactions[h] = txx[i] } c.Timestamp = c.lastBlockTimestamp + c.Config.TimestampIncrement if now := c.getTimestamp(); now > c.Timestamp { c.Timestamp = now } return true } // getTimestamp returns nanoseconds-precision timestamp using // current context config. func (c *Context[H]) getTimestamp() uint64 { return uint64(c.Config.Timer.Now().UnixNano()) / c.Config.TimestampIncrement * c.Config.TimestampIncrement } // CreateBlock returns resulting block for the current epoch. func (c *Context[H]) CreateBlock() Block[H] { if c.block == nil { if c.block = c.MakeHeader(); c.block == nil { return nil } txx := make([]Transaction[H], len(c.TransactionHashes)) for i, h := range c.TransactionHashes { txx[i] = c.Transactions[h] } // Anti-MEV extension properly sets PreBlock transactions once during PreBlock // construction and then never updates these transactions in the dBFT context. // Thus, user must not reuse txx if anti-MEV extension is enabled. However, // we don't skip a call to Block.SetTransactions since it may be used as a // signal to the user's code to finalize the block. c.block.SetTransactions(txx) } return c.block } // CreatePreBlock returns PreBlock for the current epoch. func (c *Context[H]) CreatePreBlock() PreBlock[H] { if c.preBlock == nil { if c.preBlock = c.MakePreHeader(); c.preBlock == nil { return nil } txx := make([]Transaction[H], len(c.TransactionHashes)) for i, h := range c.TransactionHashes { txx[i] = c.Transactions[h] } c.preBlock.SetTransactions(txx) } return c.preBlock } // isAntiMEVExtensionEnabled returns whether Anti-MEV dBFT extension is enabled // at the currently processing block height. func (c *Context[H]) isAntiMEVExtensionEnabled() bool { return c.Config.AntiMEVExtensionEnablingHeight >= 0 && uint32(c.Config.AntiMEVExtensionEnablingHeight) <= c.BlockIndex } // MakeHeader returns half-filled block for the current epoch. // All hashable fields will be filled. func (c *Context[H]) MakeHeader() Block[H] { if c.header == nil { if !c.RequestSentOrReceived() { return nil } // For anti-MEV dBFT extension it's important to have PreBlock processed and // all envelopes decrypted, because a single PrepareRequest is not enough to // construct proper Block. if c.isAntiMEVExtensionEnabled() { if !c.preBlockProcessed { return nil } } c.header = c.Config.NewBlockFromContext(c) } return c.header } // MakePreHeader returns half-filled block for the current epoch. // All hashable fields will be filled. func (c *Context[H]) MakePreHeader() PreBlock[H] { if c.preHeader == nil { if !c.RequestSentOrReceived() { return nil } c.preHeader = c.Config.NewPreBlockFromContext(c) } return c.preHeader } // hasAllTransactions returns true iff all transactions were received // for the proposed block. func (c *Context[H]) hasAllTransactions() bool { return len(c.TransactionHashes) == len(c.Transactions) } func (c *Context[H]) subscribeForTransactions() { c.txSubscriptionOn = true c.Config.SubscribeForTxs() } func (c *Context[H]) unsubscribeFromTransactions() { c.txSubscriptionOn = false }