Fantom-foundation/go-lachesis

View on GitHub
poset/poset.go

Summary

Maintainability
A
1 hr
Test Coverage
package poset

import (
    "github.com/pkg/errors"

    "github.com/Fantom-foundation/go-lachesis/eventcheck/epochcheck"
    "github.com/Fantom-foundation/go-lachesis/hash"
    "github.com/Fantom-foundation/go-lachesis/inter"
    "github.com/Fantom-foundation/go-lachesis/inter/idx"
    "github.com/Fantom-foundation/go-lachesis/lachesis"
    "github.com/Fantom-foundation/go-lachesis/logger"
    "github.com/Fantom-foundation/go-lachesis/poset/election"
    "github.com/Fantom-foundation/go-lachesis/utils"
    "github.com/Fantom-foundation/go-lachesis/vector"
)

var (
    ErrWrongEpochHash   = errors.New("mismatched prev epoch hash")
    ErrNonZeroEpochHash = errors.New("prev epoch hash isn't zero for non-first event")
    ErrCheatersObserved = errors.New("Cheaters observed by self-parent aren't allowed as parents")
    ErrWrongFrame       = errors.New("Claimed frame mismatched with calculated")
    ErrWrongIsRoot      = errors.New("Claimed isRoot mismatched with calculated")
    ErrWrongMedianTime  = errors.New("Claimed medianTime mismatched with calculated")
)

// Poset processes events to get consensus.
type Poset struct {
    dag   lachesis.DagConfig
    store *Store
    input EventSource
    *Checkpoint
    EpochState

    election *election.Election
    vecClock *vector.Index

    callback inter.ConsensusCallbacks

    epochMu utils.SpinLock // protects p.Validators and p.EpochN

    logger.Instance
}

// New creates Poset instance.
// It does not start any process.
func New(dag lachesis.DagConfig, store *Store, input EventSource) *Poset {
    p := &Poset{
        dag:   dag,
        store: store,
        input: input,

        Instance: logger.MakeInstance(),
    }

    return p
}

// GetVectorIndex returns vector clock.
func (p *Poset) GetVectorIndex() *vector.Index {
    return p.vecClock
}

// LastBlock returns current block.
func (p *Poset) LastBlock() (idx.Block, hash.Event) {
    return p.LastBlockN, p.LastAtropos
}

// Prepare fills consensus-related fields: Frame, IsRoot, MedianTimestamp, PrevEpochHash
// returns nil if event should be dropped
func (p *Poset) Prepare(e *inter.Event) *inter.Event {
    if err := epochcheck.New(&p.dag, p).Validate(e); err != nil {
        p.Log.Error("Event prepare error", "err", err, "event", e)
        return nil
    }
    id := e.Hash() // remember, because we change event here
    p.vecClock.Add(&e.EventHeaderData)
    defer p.vecClock.DropNotFlushed()

    e.Frame, e.IsRoot = p.calcFrameIdx(e, false)
    e.MedianTime = p.vecClock.MedianTime(id, p.PrevEpoch.Time)
    if e.Seq <= 1 {
        e.PrevEpochHash = p.PrevEpoch.Hash()
    } else {
        e.PrevEpochHash = hash.Zero
    }

    return e
}

// checks consensus-related fields: Frame, IsRoot, MedianTimestamp, PrevEpochHash
func (p *Poset) checkAndSaveEvent(e *inter.Event) error {
    if e.Seq <= 1 && e.PrevEpochHash != p.PrevEpoch.Hash() {
        return ErrWrongEpochHash
    }
    if e.Seq > 1 && e.PrevEpochHash != hash.Zero {
        return ErrNonZeroEpochHash
    }

    // don't link to known cheaters
    if len(p.vecClock.NoCheaters(e.SelfParent(), e.Parents)) != len(e.Parents) {
        return ErrCheatersObserved
    }

    p.vecClock.Add(&e.EventHeaderData)
    defer p.vecClock.DropNotFlushed()

    // check frame & isRoot
    frameIdx, isRoot := p.calcFrameIdx(e, true)
    if e.IsRoot != isRoot {
        return ErrWrongIsRoot
    }
    if e.Frame != frameIdx {
        return ErrWrongFrame
    }
    // check median timestamp
    medianTime := p.vecClock.MedianTime(e.Hash(), p.PrevEpoch.Time)
    if e.MedianTime != medianTime {
        return ErrWrongMedianTime
    }

    // save in DB the {vectorindex, e, heads}
    p.vecClock.Flush()
    if e.IsRoot {
        p.store.AddRoot(e)
    }

    return nil
}

// calculates Atropos election for the root, calls p.onFrameDecided if election was decided
func (p *Poset) handleElection(root *inter.Event) {
    if root != nil { // if root is nil, then just bootstrap election
        if !root.IsRoot {
            return
        }

        decided := p.processRoot(root.Frame, root.Creator, root.Hash())
        if decided == nil {
            return
        }

        // if we’re here, then this root has observed that lowest not decided frame is decided now
        if p.onFrameDecided(decided.Frame, decided.Atropos) {
            return
        }
    }

    // then call processKnownRoots until it returns nil -
    // it’s needed because new elections may already have enough votes, because we process elections from lowest to highest
    for {
        decided := p.processKnownRoots()
        if decided == nil {
            break
        }

        if p.onFrameDecided(decided.Frame, decided.Atropos) {
            return
        }
    }
}

func (p *Poset) processRoot(f idx.Frame, from idx.StakerID, id hash.Event) (decided *election.Res) {
    decided, err := p.election.ProcessRoot(election.RootAndSlot{
        ID: id,
        Slot: election.Slot{
            Frame:     f,
            Validator: from,
        },
    })
    if err != nil {
        p.Log.Crit("If we're here, probably more than 1/3W are Byzantine, and the problem cannot be resolved automatically ",
            "err", err)
    }
    return decided
}

// The function is similar to processRoot, but it fully re-processes the current voting.
// This routine should be called after node startup, and after each decided frame.
func (p *Poset) processKnownRoots() *election.Res {
    // iterate all the roots from LastDecidedFrame+1 to highest, call processRoot for each
    var decided *election.Res
    for f := p.LastDecidedFrame + 1; ; f++ {
        frameRoots := p.store.GetFrameRoots(f)
        for _, it := range frameRoots {
            p.Log.Debug("Calculate root votes in new election", "root", it.ID.String())
            decided = p.processRoot(it.Slot.Frame, it.Slot.Validator, it.ID)
            if decided != nil {
                return decided
            }
        }
        if len(frameRoots) == 0 {
            break
        }
    }
    return nil
}

// ProcessEvent takes event into processing.
// Event order matter: parents first.
// ProcessEvent is not safe for concurrent use.
func (p *Poset) ProcessEvent(e *inter.Event) (err error) {
    err = epochcheck.New(&p.dag, p).Validate(e)
    if err != nil {
        return
    }
    p.Log.Debug("Consensus: start event processing", "event", e)

    err = p.checkAndSaveEvent(e)
    if err != nil {
        return
    }

    p.handleElection(e)
    return
}

// forklessCausedByQuorumOn returns true if event is forkless caused by 2/3W roots on specified frame
func (p *Poset) forklessCausedByQuorumOn(e *inter.Event, f idx.Frame) bool {
    observedCounter := p.Validators.NewCounter()
    // check "observing" prev roots only if called by creator, or if creator has marked that event as root
    for _, it := range p.store.GetFrameRoots(f) {
        if p.vecClock.ForklessCause(e.Hash(), it.ID) {
            observedCounter.Count(it.Slot.Validator)
        }
        if observedCounter.HasQuorum() {
            break
        }
    }
    return observedCounter.HasQuorum()
}

// calcFrameIdx checks root-conditions for new event
// and returns event's frame.
// It is not safe for concurrent use.
func (p *Poset) calcFrameIdx(e *inter.Event, checkOnly bool) (frame idx.Frame, isRoot bool) {
    if len(e.Parents) == 0 {
        // special case for very first events in the epoch
        return 1, true
    }

    // calc maxParentsFrame, i.e. max(parent's frame height)
    maxParentsFrame := idx.Frame(0)
    selfParentFrame := idx.Frame(0)

    for _, parent := range e.Parents {
        pFrame := p.GetEventHeader(p.EpochN, parent).Frame
        if maxParentsFrame == 0 || pFrame > maxParentsFrame {
            maxParentsFrame = pFrame
        }

        if e.IsSelfParent(parent) {
            selfParentFrame = pFrame
        }
    }

    if checkOnly {
        // check frame & isRoot
        frame = e.Frame
        if !e.IsRoot {
            // don't check forklessCausedByQuorumOn if not claimed as root
            // if not root, then not allowed to move frame
            return selfParentFrame, false
        }
        // every root must be greater than prev. self-root. Instead, election will be faulty
        // roots aren't allowed to "jump" to higher frame than selfParentFrame+1, even if they are forkless caused
        // by 2/3W+1 there. It's because of liveness with forks, when up to 1/3W of roots on any frame may become "invisible"
        // for forklessCause relation (so if we skip frames, there's may be deadlock when frames cannot advance because there's
        // less than 2/3W visible roots)
        isRoot = frame == selfParentFrame+1 && (e.Frame <= 1 || p.forklessCausedByQuorumOn(e, e.Frame-1))
        return selfParentFrame + 1, isRoot
    }

    // calculate frame & isRoot
    if e.SelfParent() == nil {
        return 1, true
    }
    if p.forklessCausedByQuorumOn(e, selfParentFrame) {
        return selfParentFrame + 1, true
    }
    // Note: if we assign maxParentsFrame, it'll break the liveness for a case with forks, because there may be less
    // than 2/3W possible roots at maxParentsFrame, even if 1 validator is cheater and 1/3W were offline for some time
    // and didn't create roots at maxParentsFrame - they won't be able to create roots at maxParentsFrame because
    // every frame must be greater than previous
    return selfParentFrame, false

}