aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/byzantine-lab/dexon-consensus/core/blockchain.go
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/byzantine-lab/dexon-consensus/core/blockchain.go')
-rw-r--r--vendor/github.com/byzantine-lab/dexon-consensus/core/blockchain.go681
1 files changed, 681 insertions, 0 deletions
diff --git a/vendor/github.com/byzantine-lab/dexon-consensus/core/blockchain.go b/vendor/github.com/byzantine-lab/dexon-consensus/core/blockchain.go
new file mode 100644
index 000000000..579ccd44c
--- /dev/null
+++ b/vendor/github.com/byzantine-lab/dexon-consensus/core/blockchain.go
@@ -0,0 +1,681 @@
+// Copyright 2019 The dexon-consensus Authors
+// This file is part of the dexon-consensus library.
+//
+// The dexon-consensus library is free software: you can redistribute it
+// and/or modify it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// The dexon-consensus library is distributed in the hope that it will be
+// useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+// General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the dexon-consensus library. If not, see
+// <http://www.gnu.org/licenses/>.
+
+package core
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "math"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/byzantine-lab/dexon-consensus/common"
+ "github.com/byzantine-lab/dexon-consensus/core/crypto"
+ "github.com/byzantine-lab/dexon-consensus/core/types"
+ "github.com/byzantine-lab/dexon-consensus/core/utils"
+)
+
+// Errors for sanity check error.
+var (
+ ErrBlockFromOlderPosition = errors.New("block from older position")
+ ErrNotGenesisBlock = errors.New("not a genesis block")
+ ErrIsGenesisBlock = errors.New("is a genesis block")
+ ErrIncorrectParentHash = errors.New("incorrect parent hash")
+ ErrInvalidBlockHeight = errors.New("invalid block height")
+ ErrInvalidRoundID = errors.New("invalid round id")
+ ErrInvalidTimestamp = errors.New("invalid timestamp")
+ ErrNotFollowTipPosition = errors.New("not follow tip position")
+ ErrDuplicatedPendingBlock = errors.New("duplicated pending block")
+ ErrRetrySanityCheckLater = errors.New("retry sanity check later")
+ ErrRoundNotSwitch = errors.New("round not switch")
+ ErrIncorrectAgreementResult = errors.New(
+ "incorrect block randomness result")
+ ErrMissingRandomness = errors.New("missing block randomness")
+)
+
+const notReadyHeight uint64 = math.MaxUint64
+
+type pendingBlockRecord struct {
+ position types.Position
+ block *types.Block
+}
+
+type pendingBlockRecords []pendingBlockRecord
+
+func (pb *pendingBlockRecords) insert(p pendingBlockRecord) error {
+ idx := sort.Search(len(*pb), func(i int) bool {
+ return !(*pb)[i].position.Older(p.position)
+ })
+ switch idx {
+ case len(*pb):
+ *pb = append(*pb, p)
+ default:
+ if (*pb)[idx].position.Equal(p.position) {
+ // Allow to overwrite pending block record for empty blocks, we may
+ // need to pull that block from others when its parent is not found
+ // locally.
+ if (*pb)[idx].block == nil && p.block != nil {
+ (*pb)[idx].block = p.block
+ return nil
+ }
+ return ErrDuplicatedPendingBlock
+ }
+ // Insert the value to that index.
+ *pb = append((*pb), pendingBlockRecord{})
+ copy((*pb)[idx+1:], (*pb)[idx:])
+ (*pb)[idx] = p
+ }
+ return nil
+}
+
+func (pb pendingBlockRecords) searchByHeight(h uint64) (
+ pendingBlockRecord, bool) {
+ idx := sort.Search(len(pb), func(i int) bool {
+ return pb[i].position.Height >= h
+ })
+ if idx == len(pb) || pb[idx].position.Height != h {
+ return pendingBlockRecord{}, false
+ }
+ return pb[idx], true
+}
+
+func (pb pendingBlockRecords) searchByPosition(p types.Position) (
+ pendingBlockRecord, bool) {
+ idx := sort.Search(len(pb), func(i int) bool {
+ return !pb[i].block.Position.Older(p)
+ })
+ if idx == len(pb) || !pb[idx].position.Equal(p) {
+ return pendingBlockRecord{}, false
+ }
+ return pb[idx], true
+}
+
+type blockChainConfig struct {
+ utils.RoundBasedConfig
+
+ minBlockInterval time.Duration
+}
+
+func (c *blockChainConfig) fromConfig(round uint64, config *types.Config) {
+ c.minBlockInterval = config.MinBlockInterval
+ c.SetupRoundBasedFields(round, config)
+}
+
+func newBlockChainConfig(prev blockChainConfig, config *types.Config) (
+ c blockChainConfig) {
+ c = blockChainConfig{}
+ c.fromConfig(prev.RoundID()+1, config)
+ c.AppendTo(prev.RoundBasedConfig)
+ return
+}
+
+type tsigVerifierGetter interface {
+ UpdateAndGet(uint64) (TSigVerifier, bool, error)
+ Purge(uint64)
+}
+
+type blockChain struct {
+ lock sync.RWMutex
+ ID types.NodeID
+ lastConfirmed *types.Block
+ lastDelivered *types.Block
+ signer *utils.Signer
+ vGetter tsigVerifierGetter
+ app Application
+ logger common.Logger
+ pendingRandomnesses map[types.Position][]byte
+ configs []blockChainConfig
+ pendingBlocks pendingBlockRecords
+ confirmedBlocks types.BlocksByPosition
+ dMoment time.Time
+
+ // Do not access this variable besides processAgreementResult.
+ lastPosition types.Position
+}
+
+func newBlockChain(nID types.NodeID, dMoment time.Time, initBlock *types.Block,
+ app Application, vGetter tsigVerifierGetter, signer *utils.Signer,
+ logger common.Logger) *blockChain {
+ return &blockChain{
+ ID: nID,
+ lastConfirmed: initBlock,
+ lastDelivered: initBlock,
+ signer: signer,
+ vGetter: vGetter,
+ app: app,
+ logger: logger,
+ dMoment: dMoment,
+ pendingRandomnesses: make(
+ map[types.Position][]byte),
+ }
+}
+
+func (bc *blockChain) notifyRoundEvents(evts []utils.RoundEventParam) error {
+ bc.lock.Lock()
+ defer bc.lock.Unlock()
+ apply := func(e utils.RoundEventParam) error {
+ if len(bc.configs) > 0 {
+ lastCfg := bc.configs[len(bc.configs)-1]
+ if e.BeginHeight != lastCfg.RoundEndHeight() {
+ return ErrInvalidBlockHeight
+ }
+ if lastCfg.RoundID() == e.Round {
+ bc.configs[len(bc.configs)-1].ExtendLength()
+ } else if lastCfg.RoundID()+1 == e.Round {
+ bc.configs = append(bc.configs, newBlockChainConfig(
+ lastCfg, e.Config))
+ } else {
+ return ErrInvalidRoundID
+ }
+ } else {
+ c := blockChainConfig{}
+ c.fromConfig(e.Round, e.Config)
+ c.SetRoundBeginHeight(e.BeginHeight)
+ if bc.lastConfirmed == nil {
+ if c.RoundID() != 0 {
+ panic(fmt.Errorf(
+ "genesis config should from round 0, but %d",
+ c.RoundID()))
+ }
+ } else {
+ if c.RoundID() != bc.lastConfirmed.Position.Round {
+ panic(fmt.Errorf("incompatible config/block round %s %d",
+ bc.lastConfirmed, c.RoundID()))
+ }
+ if !c.Contains(bc.lastConfirmed.Position.Height) {
+ panic(fmt.Errorf(
+ "unmatched round-event with block %s %d %d %d",
+ bc.lastConfirmed, e.Round, e.Reset, e.BeginHeight))
+ }
+ }
+ bc.configs = append(bc.configs, c)
+ }
+ return nil
+ }
+ for _, e := range evts {
+ if err := apply(e); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (bc *blockChain) proposeBlock(position types.Position,
+ proposeTime time.Time, isEmpty bool) (b *types.Block, err error) {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ return bc.prepareBlock(position, proposeTime, isEmpty)
+}
+
+func (bc *blockChain) extractBlocks() (ret []*types.Block) {
+ bc.lock.Lock()
+ defer bc.lock.Unlock()
+ for len(bc.confirmedBlocks) > 0 {
+ c := bc.confirmedBlocks[0]
+ if c.Position.Round >= DKGDelayRound &&
+ len(c.Randomness) == 0 &&
+ !bc.setRandomnessFromPending(c) {
+ break
+ }
+ c, bc.confirmedBlocks = bc.confirmedBlocks[0], bc.confirmedBlocks[1:]
+ ret = append(ret, c)
+ bc.lastDelivered = c
+ }
+ return
+}
+
+func (bc *blockChain) sanityCheck(b *types.Block) error {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ if bc.lastConfirmed == nil {
+ // It should be a genesis block.
+ if !b.IsGenesis() {
+ return ErrNotGenesisBlock
+ }
+ if b.Timestamp.Before(bc.dMoment.Add(bc.configs[0].minBlockInterval)) {
+ return ErrInvalidTimestamp
+ }
+ return nil
+ }
+ if b.IsGenesis() {
+ return ErrIsGenesisBlock
+ }
+ if b.Position.Height != bc.lastConfirmed.Position.Height+1 {
+ if b.Position.Height > bc.lastConfirmed.Position.Height {
+ return ErrRetrySanityCheckLater
+ }
+ return ErrInvalidBlockHeight
+ }
+ tipConfig := bc.tipConfig()
+ if tipConfig.IsLastBlock(bc.lastConfirmed) {
+ if b.Position.Round != bc.lastConfirmed.Position.Round+1 {
+ return ErrRoundNotSwitch
+ }
+ } else {
+ if b.Position.Round != bc.lastConfirmed.Position.Round {
+ return ErrInvalidRoundID
+ }
+ }
+ if !b.ParentHash.Equal(bc.lastConfirmed.Hash) {
+ return ErrIncorrectParentHash
+ }
+ if b.Timestamp.Before(bc.lastConfirmed.Timestamp.Add(
+ tipConfig.minBlockInterval)) {
+ return ErrInvalidTimestamp
+ }
+ if err := utils.VerifyBlockSignature(b); err != nil {
+ return err
+ }
+ return nil
+}
+
+// addEmptyBlock is called when an empty block is confirmed by BA.
+func (bc *blockChain) addEmptyBlock(position types.Position) (
+ *types.Block, error) {
+ bc.lock.Lock()
+ defer bc.lock.Unlock()
+ add := func() *types.Block {
+ emptyB, err := bc.prepareBlock(position, time.Time{}, true)
+ if err != nil || emptyB == nil {
+ // This helper is expected to be called when an empty block is ready
+ // to be confirmed.
+ panic(err)
+ }
+ bc.confirmBlock(emptyB)
+ bc.checkIfBlocksConfirmed()
+ return emptyB
+ }
+ if bc.lastConfirmed != nil {
+ if !position.Newer(bc.lastConfirmed.Position) {
+ bc.logger.Warn("Dropping empty block: older than tip",
+ "position", &position,
+ "last-confirmed", bc.lastConfirmed)
+ return nil, ErrBlockFromOlderPosition
+ }
+ if bc.lastConfirmed.Position.Height+1 == position.Height {
+ return add(), nil
+ }
+ } else if position.Height == types.GenesisHeight && position.Round == 0 {
+ return add(), nil
+ } else {
+ return nil, ErrInvalidBlockHeight
+ }
+ return nil, bc.addPendingBlockRecord(pendingBlockRecord{position, nil})
+}
+
+// addBlock should be called when the block is confirmed by BA, we won't perform
+// sanity check against this block, it's ok to add block with skipping height.
+func (bc *blockChain) addBlock(b *types.Block) error {
+ if b.Position.Round >= DKGDelayRound &&
+ len(b.Randomness) == 0 &&
+ !bc.setRandomnessFromPending(b) {
+ return ErrMissingRandomness
+ }
+ bc.lock.Lock()
+ defer bc.lock.Unlock()
+ confirmed := false
+ if bc.lastConfirmed != nil {
+ if !b.Position.Newer(bc.lastConfirmed.Position) {
+ bc.logger.Warn("Dropping block: older than tip",
+ "block", b, "last-confirmed", bc.lastConfirmed)
+ return nil
+ }
+ if bc.lastConfirmed.Position.Height+1 == b.Position.Height {
+ confirmed = true
+ }
+ } else if b.IsGenesis() {
+ confirmed = true
+ }
+ delete(bc.pendingRandomnesses, b.Position)
+ if !confirmed {
+ return bc.addPendingBlockRecord(pendingBlockRecord{b.Position, b})
+ }
+ bc.confirmBlock(b)
+ bc.checkIfBlocksConfirmed()
+ return nil
+}
+
+func (bc *blockChain) tipRound() uint64 {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ if bc.lastConfirmed == nil {
+ return 0
+ }
+ offset, tipConfig := uint64(0), bc.tipConfig()
+ if tipConfig.IsLastBlock(bc.lastConfirmed) {
+ offset++
+ }
+ return bc.lastConfirmed.Position.Round + offset
+}
+
+func (bc *blockChain) confirmed(h uint64) bool {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ if bc.lastConfirmed != nil && bc.lastConfirmed.Position.Height >= h {
+ return true
+ }
+ r, found := bc.pendingBlocks.searchByHeight(h)
+ if !found {
+ return false
+ }
+ return r.block != nil
+}
+
+func (bc *blockChain) nextBlock() (uint64, time.Time) {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ // It's ok to access tip config directly without checking the existence of
+ // lastConfirmed block in the scenario of "nextBlock" method.
+ tip, config := bc.lastConfirmed, bc.configs[0]
+ if tip == nil {
+ return types.GenesisHeight, bc.dMoment
+ }
+ if tip != bc.lastDelivered {
+ // If tip is not delivered, we should not proceed to next block.
+ return notReadyHeight, time.Time{}
+ }
+ return tip.Position.Height + 1, tip.Timestamp.Add(config.minBlockInterval)
+}
+
+func (bc *blockChain) pendingBlocksWithoutRandomness() []*types.Block {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ blocks := make([]*types.Block, 0)
+ for _, b := range bc.confirmedBlocks {
+ if b.Position.Round < DKGDelayRound ||
+ len(b.Randomness) > 0 ||
+ bc.setRandomnessFromPending(b) {
+ continue
+ }
+ blocks = append(blocks, b)
+ }
+ for _, r := range bc.pendingBlocks {
+ if r.position.Round < DKGDelayRound {
+ continue
+ }
+ if r.block != nil &&
+ len(r.block.Randomness) == 0 &&
+ !bc.setRandomnessFromPending(r.block) {
+ blocks = append(blocks, r.block)
+ }
+ }
+ return blocks
+}
+
+func (bc *blockChain) lastDeliveredBlock() *types.Block {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ return bc.lastDelivered
+}
+
+func (bc *blockChain) lastPendingBlock() *types.Block {
+ bc.lock.RLock()
+ defer bc.lock.RUnlock()
+ if len(bc.confirmedBlocks) == 0 {
+ return nil
+ }
+ return bc.confirmedBlocks[0]
+}
+
+/////////////////////////////////////////////
+//
+// internal helpers
+//
+/////////////////////////////////////////////
+
+// findPendingBlock is a helper to find a block in either pending or confirmed
+// state by position.
+func (bc *blockChain) findPendingBlock(p types.Position) *types.Block {
+ if idx := sort.Search(len(bc.confirmedBlocks), func(i int) bool {
+ return !bc.confirmedBlocks[i].Position.Older(p)
+ }); idx != len(bc.confirmedBlocks) &&
+ bc.confirmedBlocks[idx].Position.Equal(p) {
+ return bc.confirmedBlocks[idx]
+ }
+ pendingRec, _ := bc.pendingBlocks.searchByPosition(p)
+ return pendingRec.block
+}
+
+func (bc *blockChain) addPendingBlockRecord(p pendingBlockRecord) error {
+ if err := bc.pendingBlocks.insert(p); err != nil {
+ if err == ErrDuplicatedPendingBlock {
+ // We need to ignore this error because BA might confirm duplicated
+ // blocks in position.
+ err = nil
+ }
+ return err
+ }
+ return nil
+}
+
+func (bc *blockChain) checkIfBlocksConfirmed() {
+ var err error
+ for len(bc.pendingBlocks) > 0 {
+ if bc.pendingBlocks[0].position.Height <
+ bc.lastConfirmed.Position.Height+1 {
+ panic(fmt.Errorf("unexpected case %s %s", bc.lastConfirmed,
+ bc.pendingBlocks[0].position))
+ }
+ if bc.pendingBlocks[0].position.Height >
+ bc.lastConfirmed.Position.Height+1 {
+ break
+ }
+ var pending pendingBlockRecord
+ pending, bc.pendingBlocks = bc.pendingBlocks[0], bc.pendingBlocks[1:]
+ nextTip := pending.block
+ if nextTip == nil {
+ if nextTip, err = bc.prepareBlock(
+ pending.position, time.Time{}, true); err != nil {
+ // It should not be error when prepare empty block for correct
+ // position.
+ panic(err)
+ }
+ }
+ bc.confirmBlock(nextTip)
+ }
+}
+
+func (bc *blockChain) purgeConfig() {
+ for bc.configs[0].RoundID() < bc.lastConfirmed.Position.Round {
+ bc.configs = bc.configs[1:]
+ }
+ if bc.configs[0].RoundID() != bc.lastConfirmed.Position.Round {
+ panic(fmt.Errorf("mismatched tip config: %d %d",
+ bc.configs[0].RoundID(), bc.lastConfirmed.Position.Round))
+ }
+}
+
+func (bc *blockChain) verifyRandomness(
+ blockHash common.Hash, round uint64, randomness []byte) (bool, error) {
+ if round < DKGDelayRound {
+ return bytes.Compare(randomness, NoRand) == 0, nil
+ }
+ v, ok, err := bc.vGetter.UpdateAndGet(round)
+ if err != nil {
+ return false, err
+ }
+ if !ok {
+ return false, ErrTSigNotReady
+ }
+ return v.VerifySignature(blockHash, crypto.Signature{
+ Type: "bls",
+ Signature: randomness}), nil
+}
+
+func (bc *blockChain) prepareBlock(position types.Position,
+ proposeTime time.Time, empty bool) (b *types.Block, err error) {
+ b = &types.Block{Position: position, Timestamp: proposeTime}
+ tip := bc.lastConfirmed
+ // Make sure we can propose a block at expected position for callers.
+ if tip == nil {
+ if bc.configs[0].RoundID() != uint64(0) {
+ panic(fmt.Errorf(
+ "Genesis config should be ready when preparing genesis: %d",
+ bc.configs[0].RoundID()))
+ }
+ // It should be the case for genesis block.
+ if !position.Equal(types.Position{Height: types.GenesisHeight}) {
+ b, err = nil, ErrNotGenesisBlock
+ return
+ }
+ minExpectedTime := bc.dMoment.Add(bc.configs[0].minBlockInterval)
+ if empty {
+ b.Timestamp = minExpectedTime
+ } else {
+ bc.logger.Debug("Calling genesis Application.PreparePayload")
+ if b.Payload, err = bc.app.PreparePayload(b.Position); err != nil {
+ b = nil
+ return
+ }
+ bc.logger.Debug("Calling genesis Application.PrepareWitness")
+ if b.Witness, err = bc.app.PrepareWitness(0); err != nil {
+ b = nil
+ return
+ }
+ if proposeTime.Before(minExpectedTime) {
+ b.Timestamp = minExpectedTime
+ }
+ }
+ } else {
+ tipConfig := bc.tipConfig()
+ if tip.Position.Height+1 != position.Height {
+ b, err = nil, ErrNotFollowTipPosition
+ return
+ }
+ if tipConfig.IsLastBlock(tip) {
+ if tip.Position.Round+1 != position.Round {
+ b, err = nil, ErrRoundNotSwitch
+ return
+ }
+ } else {
+ if tip.Position.Round != position.Round {
+ b, err = nil, ErrInvalidRoundID
+ return
+ }
+ }
+ minExpectedTime := tip.Timestamp.Add(bc.configs[0].minBlockInterval)
+ b.ParentHash = tip.Hash
+ if !empty {
+ bc.logger.Debug("Calling Application.PreparePayload",
+ "position", b.Position)
+ if b.Payload, err = bc.app.PreparePayload(b.Position); err != nil {
+ b = nil
+ return
+ }
+ bc.logger.Debug("Calling Application.PrepareWitness",
+ "height", tip.Witness.Height)
+ if b.Witness, err = bc.app.PrepareWitness(
+ tip.Witness.Height); err != nil {
+ b = nil
+ return
+ }
+ if b.Timestamp.Before(minExpectedTime) {
+ b.Timestamp = minExpectedTime
+ }
+ } else {
+ b.Witness.Height = tip.Witness.Height
+ b.Witness.Data = make([]byte, len(tip.Witness.Data))
+ copy(b.Witness.Data, tip.Witness.Data)
+ b.Timestamp = minExpectedTime
+ }
+ }
+ if empty {
+ if b.Hash, err = utils.HashBlock(b); err != nil {
+ b = nil
+ return
+ }
+ } else {
+ if err = bc.signer.SignBlock(b); err != nil {
+ b = nil
+ return
+ }
+ }
+ return
+}
+
+func (bc *blockChain) tipConfig() blockChainConfig {
+ if bc.lastConfirmed == nil {
+ panic(fmt.Errorf("attempting to access config without tip"))
+ }
+ if bc.lastConfirmed.Position.Round != bc.configs[0].RoundID() {
+ panic(fmt.Errorf("inconsist config and tip: %d %d",
+ bc.lastConfirmed.Position.Round, bc.configs[0].RoundID()))
+ }
+ return bc.configs[0]
+}
+
+func (bc *blockChain) confirmBlock(b *types.Block) {
+ if bc.lastConfirmed != nil &&
+ bc.lastConfirmed.Position.Height+1 != b.Position.Height {
+ panic(fmt.Errorf("confirmed blocks not continuous in height: %s %s",
+ bc.lastConfirmed, b))
+ }
+ bc.logger.Debug("Calling Application.BlockConfirmed", "block", b)
+ bc.app.BlockConfirmed(*b)
+ bc.lastConfirmed = b
+ bc.confirmedBlocks = append(bc.confirmedBlocks, b)
+ bc.purgeConfig()
+}
+
+func (bc *blockChain) setRandomnessFromPending(b *types.Block) bool {
+ if r, exist := bc.pendingRandomnesses[b.Position]; exist {
+ b.Randomness = r
+ delete(bc.pendingRandomnesses, b.Position)
+ return true
+ }
+ return false
+}
+
+func (bc *blockChain) processAgreementResult(result *types.AgreementResult) error {
+ if result.Position.Round < DKGDelayRound {
+ return nil
+ }
+ if !result.Position.Newer(bc.lastPosition) {
+ return ErrSkipButNoError
+ }
+ ok, err := bc.verifyRandomness(
+ result.BlockHash, result.Position.Round, result.Randomness)
+ if err != nil {
+ return err
+ }
+ if !ok {
+ return ErrIncorrectAgreementResult
+ }
+ bc.lock.Lock()
+ defer bc.lock.Unlock()
+ if !result.Position.Newer(bc.lastDelivered.Position) {
+ return nil
+ }
+ bc.pendingRandomnesses[result.Position] = result.Randomness
+ bc.lastPosition = bc.lastDelivered.Position
+ return nil
+}
+
+func (bc *blockChain) addBlockRandomness(pos types.Position, rand []byte) {
+ if pos.Round < DKGDelayRound {
+ return
+ }
+ bc.lock.Lock()
+ defer bc.lock.Unlock()
+ if !pos.Newer(bc.lastDelivered.Position) {
+ return
+ }
+ bc.pendingRandomnesses[pos] = rand
+}