Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 39 additions & 20 deletions instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,35 +56,47 @@ type epochChange struct {
epoch uint64
validators common.Nodes
}

// TODO: replace noop-index with a function that checks whether we need to send approvals or auxiliary information
func noopOnIndex(*ParsedBlock) error { return nil }

type timeAdvancer interface {
AdvanceTime(t time.Time)
}

type Instance struct {
Config Config

lock sync.Mutex
started bool
cs *CachedStorage
wal *wal.GarbageCollectedWAL
msm *metadata.StateMachine
e *simplex.Epoch
nv *nonvalidator.NonValidator
epochOrNV timeAdvancer
epochChanges chan epochChange
stopCh chan struct{}
lock sync.Mutex
started bool
cs *CachedStorage
transitionListener *epochTransitionListener
wal *wal.GarbageCollectedWAL
msm *metadata.StateMachine
e *simplex.Epoch
nv *nonvalidator.NonValidator
epochOrNV timeAdvancer
epochChanges chan epochChange
stopCh chan struct{}
}

func NewInstance(config Config) *Instance {
cs := NewCachedStorage(config.Storage)
// Non-validators have no block builder, so they pass a nil approval handler:
// they broadcast approvals but do not need to record their own locally.
transitionListener := newEpochTransitionListener(
config.Logger,
config.Broadcaster,
avalanchego.NodeID(config.ID),
config.PlatformChain.GetValidatorSet,
cs.RetrieveBlock,
config.CryptoOps,
&NoopAuxiliaryInfoApp{}, // TODO: set this in the config
nil,
)

return &Instance{
Config: config,
stopCh: make(chan struct{}),
epochChanges: make(chan epochChange, 1),
cs: NewCachedStorage(config.Storage),
Config: config,
stopCh: make(chan struct{}),
epochChanges: make(chan epochChange, 1),
cs: cs,
transitionListener: transitionListener,
}
}

Expand Down Expand Up @@ -170,7 +182,7 @@ func (i *Instance) createNonValidatorConfig() (nonvalidator.Config, error) {
Config: &metadata.Config{},
}
i.cs.msm = i.msm
instanceStorage := NewInstanceStorage(i.cs, i.msm, noopOnIndex)
instanceStorage := NewInstanceStorage(i.cs, i.msm, i.transitionListener.onIndex)

config := nonvalidator.Config{
ID: i.Config.ID,
Expand Down Expand Up @@ -297,6 +309,13 @@ func (i *Instance) HandleMessage(msg *common.Message, from common.NodeID) error
}

if i.e != nil {
switch {
case msg.AuxiliaryInfo != nil:
i.msm.HandleAuxiliaryInfo(*msg.AuxiliaryInfo, avalanchego.NodeID(from))
case msg.EpochTransitionApproval != nil:
// TODO: pass in time.Now() rather than uint64
i.msm.HandleApproval(msg.EpochTransitionApproval, uint64(time.Now().Unix()))
}
return i.e.HandleMessage(msg, from)
}

Expand Down Expand Up @@ -454,7 +473,7 @@ func (i *Instance) createEpochConfig(epoch uint64, validators common.Nodes) (*ep

comm := newCommunication(i.Config.Sender, i.Config.Broadcaster, validators)

instanceStorage := NewInstanceStorage(i.cs, msm, noopOnIndex)
instanceStorage := NewInstanceStorage(i.cs, msm, i.transitionListener.onIndex)

onEpochChange := func(epoch uint64, validators common.Nodes) {
blockBuilder.stop()
Expand Down
151 changes: 151 additions & 0 deletions transition_listener.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package simplex

import (
"time"

"github.com/ava-labs/simplex/avalanchego"
"github.com/ava-labs/simplex/common"

metadata "github.com/ava-labs/simplex/msm"
)

// epochTransitionListener reacts to blocks committed to storage. When a
// transition block is indexed, it performs any tasks required of this node to
// complete the epoch transition, such as sending out approval messages.
// Non-validators should also use this listener, since they may become
// validators after the transition.
type epochTransitionListener struct {
// broadcaster is used for broadcasting potential approvals and auxiliary information.
// It should be broadcast to the validators of the current epoch.
broadcaster Broadcaster

myNodeID avalanchego.NodeID

// getValidatorSet returns the validator set at a given P-chain height.
getValidatorSet metadata.ValidatorSetRetriever
// getBlock retrieves a previously finalized block, used to traverse the auxiliary info history.
getBlock metadata.BlockRetriever
// signer signs epoch transition approvals.
signer common.Signer
// auxInfoApp decides whether the auxiliary info history is sufficient and generates new entries.
auxInfoApp metadata.AuxiliaryInfoGenVerifier
// handleApproval records our own broadcast approval in the local approval store.
// It is set for validators (whose MSM builds the next blocks and must include the
// approval) and nil for non-validators, which have no block builder to feed.
handleApproval func(approval *common.ValidatorSetApproval, timestamp uint64) error

logger common.Logger
}

func newEpochTransitionListener(
logger common.Logger,
broadcaster Broadcaster,
myNodeID avalanchego.NodeID,
getValidatorSet metadata.ValidatorSetRetriever,
getBlock metadata.BlockRetriever,
signer common.Signer,
auxInfoApp metadata.AuxiliaryInfoGenVerifier,
handleApproval func(approval *common.ValidatorSetApproval, timestamp uint64) error,
) *epochTransitionListener {
return &epochTransitionListener{
broadcaster: broadcaster,
myNodeID: myNodeID,
getValidatorSet: getValidatorSet,
getBlock: getBlock,
signer: signer,
auxInfoApp: auxInfoApp,
handleApproval: handleApproval,
logger: logger,
}
}

func (a *epochTransitionListener) onIndex(block *ParsedBlock) error {
switch block.Type() {
case metadata.BlockTypeTransitioning:
return a.handleTransitionBlock(block)
}

return nil
}

func (a *epochTransitionListener) handleTransitionBlock(block *ParsedBlock) error {
nextEpochPChainReference := block.Metadata.SimplexEpochInfo.NextPChainReferenceHeight

// if our node is not in the next validator set, no need to send anything.
nextEpochValidatorSet, err := a.getValidatorSet(nextEpochPChainReference)
if err != nil {
return err
}

indexes := nextEpochValidatorSet.IndexByNodeID()
if _, ok := indexes[a.myNodeID]; !ok {
return nil // we are not in the next validator set
}

auxInfoHistory, err := metadata.GetAuxiliaryHistory(&block.StateMachineBlock, block.BlockHeader().Seq, a.getBlock, a.auxInfoApp.DefaultVersionID())
if err != nil {
return err
}

isSufficient, err := a.auxInfoApp.IsSufficient(auxInfoHistory.OldestVersionID, nextEpochValidatorSet, auxInfoHistory.Data)
if err != nil {
return err
}

if isSufficient {
// no more auxiliary info to send, maybe send our approval
lastAuxInfoDigest := auxInfoHistory.LastHistoryDigest()
return a.maybeSendApprovals(block, lastAuxInfoDigest)
}

// we need more auxiliary information, attempt to generate
generatedAuxInfo, err := a.auxInfoApp.Generate(auxInfoHistory.OldestVersionID, nextEpochValidatorSet, auxInfoHistory.Data)
if err != nil {
return err
}

if generatedAuxInfo == nil {
return nil
}

auxInfoMessage := &common.Message{
AuxiliaryInfo: &common.AuxiliaryInfo{
Version: auxInfoHistory.OldestVersionID,
Data: generatedAuxInfo,
},
}

a.broadcaster.Broadcast(auxInfoMessage)
return nil
}

// TODO: use common.Digest
func (a *epochTransitionListener) maybeSendApprovals(block *ParsedBlock, auxInfoDigest [32]byte) error {
nextEpochPChainReference := block.Metadata.SimplexEpochInfo.NextPChainReferenceHeight

sig, err := metadata.SignApproval(a.signer, nextEpochPChainReference, auxInfoDigest)
if err != nil {
return err
}

approval := common.ValidatorSetApproval{
NodeID: a.myNodeID,
PChainHeight: nextEpochPChainReference,
AuxInfoDigest: auxInfoDigest,
Signature: sig,
}

approvalMessage := common.Message{
EpochTransitionApproval: &approval,
}

a.broadcaster.Broadcast(&approvalMessage)

// Validators also record their own approval locally so the next block they build
// includes it. Non-validators have no block builder, so handleApproval is nil.
if a.handleApproval == nil {
return nil
}
timestamp := uint64(time.Now().UnixMilli())
return a.handleApproval(&approval, timestamp)
}
Loading
Loading