Documentation
¶
Overview ¶
Package devnet provides a test harness for running integration tests against a private Cardano DevNet consisting of Dingo and cardano-node instances connected via Docker Compose.
The chain-observation types in this file carry no build tag so the state machine driving the DevNet scenarios is unit-tested on every ordinary `go test ./...` run, without Docker. The pieces that dial real nodes are gated behind the `devnet` tag.
Index ¶
- Constants
- func MaxBlockNumber(snaps []ChainSnapshot) uint64
- func MaxServerTipSlot(snaps []ChainSnapshot) uint64
- func MaxTipSlot(snaps []ChainSnapshot) uint64
- func MinTipSlot(snaps []ChainSnapshot) uint64
- func SlotsDuration(slots uint64, slotDuration time.Duration) time.Duration
- type AgreementResult
- type ChainGroup
- type ChainPoint
- type ChainSnapshot
- type ChainTip
- type DevNetConfig
- func (c *DevNetConfig) BlockFetchStabilityWindowSlots() uint64
- func (c *DevNetConfig) EpochDuration() time.Duration
- func (c *DevNetConfig) ExpectedBlockTime() time.Duration
- func (c *DevNetConfig) ExpectedBlocksPerSlot() float64
- func (c *DevNetConfig) NextEpochBoundary(slot uint64) uint64
- func (c *DevNetConfig) NonceStabilityWindowSlots() uint64
- func (c *DevNetConfig) SlotDuration() time.Duration
- func (c *DevNetConfig) Validate() error
- type ObservedChain
- func (c *ObservedChain) Await(ctx context.Context, desc string, cond func(ChainSnapshot) bool) error
- func (c *ObservedChain) Connected()
- func (c *ObservedChain) Disconnected(err error)
- func (c *ObservedChain) RollBackward(point ChainPoint, serverTip ChainTip)
- func (c *ObservedChain) RollForward(h ObservedHeader, serverTip ChainTip)
- func (c *ObservedChain) Snapshot() ChainSnapshot
- type ObservedHeader
- type ScenarioPlan
- type TimelinePhase
Constants ¶
const ( PhaseReadiness = "readiness" PhasePropagation = "propagation" PhaseAgreement = "agreement" PhaseEpochTransition = "epoch-transition" PhasePeerInterruption = "peer-interruption" PhaseRelayRestart = "relay-restart" )
Phase names on the shared scenario timeline.
const ReferenceRunnerBudget = 5 * time.Minute
ReferenceRunnerBudget is the wall-clock ceiling for one accelerated scenario, excluding image build, on the reference runner documented in internal/test/devnet/README.md. It is a hard timeout, not a target: the scenario is event-driven and normally finishes well inside it.
Variables ¶
This section is empty.
Functions ¶
func MaxBlockNumber ¶
func MaxBlockNumber(snaps []ChainSnapshot) uint64
MaxBlockNumber returns the highest observed block height across snapshots. The scenario uses it to require forward progress between disruption phases, so that each outage starts from a network that is demonstrably still producing rather than merely converged.
func MaxServerTipSlot ¶
func MaxServerTipSlot(snaps []ChainSnapshot) uint64
MaxServerTipSlot returns the highest slot any node reported as its own tip.
This is not the same as MaxTipSlot: an observer intersects at origin and replays history before it reaches the tip, so its observed tip lags during catch-up. Assertions about what the chain is doing *now* — a baseline for "forged after this point", or the next epoch boundary to cross — must use the peer's reported tip, or replayed history could satisfy them without the network having done anything.
func MaxTipSlot ¶
func MaxTipSlot(snaps []ChainSnapshot) uint64
MaxTipSlot returns the highest tip slot across snapshots.
func MinTipSlot ¶
func MinTipSlot(snaps []ChainSnapshot) uint64
MinTipSlot returns the lowest tip slot across snapshots, i.e. how far the slowest node has got.
func SlotsDuration ¶
SlotsDuration returns the wall-clock time n slots take. DevNet slot counts come from a checked-in spec and are small, but the conversion is clamped rather than trusted so a nonsensical spec cannot wrap a duration into the past.
Types ¶
type AgreementResult ¶
AgreementResult reports whether a set of nodes agree on the chain at the deepest slot they have all observed.
func AgreementAtDeepestCommonSlot ¶
func AgreementAtDeepestCommonSlot( snaps []ChainSnapshot, ) (AgreementResult, bool)
AgreementAtDeepestCommonSlot picks the highest slot every snapshot observed a header for and compares the hashes there. Using a slot all nodes actually reached makes the check a deterministic expected point rather than a tolerance window: nodes at different tips still get compared on the chain they share, and a fork shows up as a hash mismatch instead of being masked by a slot-distance allowance.
The second return is false when the snapshots share no observed slot, which means the comparison could not be made at all.
func (AgreementResult) String ¶
func (r AgreementResult) String() string
String renders the per-node hashes for failure messages.
type ChainGroup ¶
type ChainGroup struct {
// contains filtered or unexported fields
}
ChainGroup observes several nodes at once and lets a scenario wait on conditions that span them. Every member shares one change broadcaster, so a group wait costs a single channel receive regardless of how many nodes the topology has.
func NewChainGroup ¶
func NewChainGroup(nodes ...string) *ChainGroup
NewChainGroup returns a group observing the named nodes.
func (*ChainGroup) Await ¶
func (g *ChainGroup) Await( ctx context.Context, desc string, cond func([]ChainSnapshot) bool, ) error
Await blocks until cond holds across every node's observed chain or ctx expires. On timeout the error reports each node's state.
func (*ChainGroup) Chain ¶
func (g *ChainGroup) Chain(node string) *ObservedChain
Chain returns the observer for a node, or nil if it is not in the group.
func (*ChainGroup) Chains ¶
func (g *ChainGroup) Chains() []*ObservedChain
Chains returns every observer in the order the group was built.
func (*ChainGroup) Snapshots ¶
func (g *ChainGroup) Snapshots() []ChainSnapshot
Snapshots returns one snapshot per node, in group order.
type ChainPoint ¶
ChainPoint is a slot/hash pair identifying a position on a chain. The zero value is the origin.
func (ChainPoint) IsOrigin ¶
func (p ChainPoint) IsOrigin() bool
IsOrigin reports whether the point refers to the chain origin, which ChainSync encodes as an empty point rather than a slot/hash pair.
type ChainSnapshot ¶
type ChainSnapshot struct {
Node string `json:"node"`
Tip ChainTip `json:"tip"`
ServerTip ChainTip `json:"serverTip"`
Headers []ObservedHeader `json:"headers"`
RollForwards int `json:"rollForwards"`
RollBackwards int `json:"rollBackwards"`
MaxRollbackDepth uint64 `json:"maxRollbackDepth"`
Connects int `json:"connects"`
Disconnects int `json:"disconnects"`
Connected bool `json:"connected"`
LastError string `json:"lastError,omitempty"`
}
ChainSnapshot is a consistent point-in-time copy of one node's observed chain, safe to read without holding the observer's lock. The json tags matter: a snapshot is what NodeControl.CaptureFailureArtifacts writes to observed-chains.json, and that file is often the only surviving record of what a node's chain did once the DevNet has been torn down.
func (ChainSnapshot) HashAt ¶
func (s ChainSnapshot) HashAt(slot uint64) ([]byte, bool)
HashAt returns the observed header hash at slot, if the slot is still inside the retained window.
func (ChainSnapshot) String ¶
func (s ChainSnapshot) String() string
String renders the snapshot as a single diagnostic line. Await failures embed it so a timeout says what the node was actually doing.
type ChainTip ¶
type ChainTip struct {
SlotNumber uint64 `json:"slot"`
BlockNumber uint64 `json:"block"`
Hash []byte `json:"hash"`
}
ChainTip holds the chain tip information retrieved from a node.
type DevNetConfig ¶
type DevNetConfig struct {
PoolCount int
NetworkMagic uint32
EpochLength uint64
SlotLength float64
ActiveSlotsCoeff float64
SecurityParam uint64
}
DevNetConfig holds the parsed configuration values from testnet.yaml.
func LoadDevNetConfig ¶
func LoadDevNetConfig() (*DevNetConfig, error)
LoadDevNetConfig reads the active network spec and returns the parsed DevNetConfig. The path is taken from the DEVNET_TESTNET_YAML environment variable; if unset, it defaults to defaultTestnetYAMLPath (relative to the test package).
func LoadDevNetConfigFrom ¶
func LoadDevNetConfigFrom(path string) (*DevNetConfig, error)
LoadDevNetConfigFrom parses the network spec at an explicit path. It is the form used by tests that check the checked-in specs directly, without depending on which mode is currently exported to the environment.
func (*DevNetConfig) BlockFetchStabilityWindowSlots ¶
func (c *DevNetConfig) BlockFetchStabilityWindowSlots() uint64
BlockFetchStabilityWindowSlots returns 3k/f, the shorter window used for blockfetch stability.
func (*DevNetConfig) EpochDuration ¶
func (c *DevNetConfig) EpochDuration() time.Duration
EpochDuration returns the wall-clock length of one epoch.
func (*DevNetConfig) ExpectedBlockTime ¶
func (c *DevNetConfig) ExpectedBlockTime() time.Duration
ExpectedBlockTime returns the average wall-clock time between blocks. This is slotDuration / activeSlotsCoeff (e.g. 2.5s with 1s slots and f=0.4). Panics if ActiveSlotsCoeff is zero or negative (invalid configuration).
func (*DevNetConfig) ExpectedBlocksPerSlot ¶
func (c *DevNetConfig) ExpectedBlocksPerSlot() float64
ExpectedBlocksPerSlot returns the approximate probability that any given slot produces a block (i.e. activeSlotsCoeff, ignoring per-pool stake fraction).
func (*DevNetConfig) NextEpochBoundary ¶
func (c *DevNetConfig) NextEpochBoundary(slot uint64) uint64
NextEpochBoundary returns the first epoch-start slot strictly after slot. Deriving the target from the observed tip rather than assuming slot == epochLength lets a scenario cross a real transition even when it attaches to a network that has been running for a while.
func (*DevNetConfig) NonceStabilityWindowSlots ¶
func (c *DevNetConfig) NonceStabilityWindowSlots() uint64
NonceStabilityWindowSlots returns 4k/f, the window cardano-node uses for the candidate-nonce freeze. The candidate nonce for the next epoch stops evolving this many slots before the epoch ends, so an epoch shorter than the window can never freeze it.
func (*DevNetConfig) SlotDuration ¶
func (c *DevNetConfig) SlotDuration() time.Duration
SlotDuration returns the wall-clock duration of a single slot.
func (*DevNetConfig) Validate ¶
func (c *DevNetConfig) Validate() error
Validate checks that the spec describes an internally consistent network. Every violation is reported, not just the first, so shrinking a network's timing tells you everything that has to move with it.
type ObservedChain ¶
type ObservedChain struct {
// contains filtered or unexported fields
}
ObservedChain accumulates the ChainSync messages one node sends and exposes them as timeout-bound conditions. It replaces polling a node's tip over repeated short-lived connections: the chain view is pushed by the node, so a condition becomes true as soon as the protocol event that satisfies it arrives.
All methods are safe for concurrent use; the observer goroutine writes while scenario assertions read.
func NewObservedChain ¶
func NewObservedChain(node string) *ObservedChain
NewObservedChain returns an observer for a single named node.
func (*ObservedChain) Await ¶
func (c *ObservedChain) Await( ctx context.Context, desc string, cond func(ChainSnapshot) bool, ) error
Await blocks until cond holds for this node's observed chain or ctx expires. cond is evaluated once before waiting, then again after every observed protocol event, so no polling interval is involved.
func (*ObservedChain) Connected ¶
func (c *ObservedChain) Connected()
Connected records that the observer established (or re-established) a ChainSync session with the node.
func (*ObservedChain) Disconnected ¶
func (c *ObservedChain) Disconnected(err error)
Disconnected records that the ChainSync session dropped. The observed chain is deliberately preserved so a reconnect resumes from it.
func (*ObservedChain) RollBackward ¶
func (c *ObservedChain) RollBackward(point ChainPoint, serverTip ChainTip)
RollBackward records a rollback to point, dropping every observed header above it. A rollback to the origin clears the observed chain.
The recorded depth counts dropped headers still inside the retained window, so a rollback deeper than that window under-reports rather than over-reports.
func (*ObservedChain) RollForward ¶
func (c *ObservedChain) RollForward(h ObservedHeader, serverTip ChainTip)
RollForward records a header the node sent, extending the observed chain. serverTip is the node's own reported tip from the same message.
func (*ObservedChain) Snapshot ¶
func (c *ObservedChain) Snapshot() ChainSnapshot
Snapshot returns a deep-enough copy for assertions to read safely.
type ObservedHeader ¶
type ObservedHeader struct {
Slot uint64 `json:"slot"`
BlockNumber uint64 `json:"block"`
Hash []byte `json:"hash"`
// BodySize is the header's declared block body size. It is what
// lets a scenario tell a block carrying transactions from an empty
// one without a node-to-client connection, which the mixed
// cardano-node topology does not expose.
BodySize uint64 `json:"bodySize"`
}
ObservedHeader is one block header seen in a ChainSync RollForward.
func AgreedHeaderAbove ¶
func AgreedHeaderAbove( snaps []ChainSnapshot, minSlot uint64, ) (ObservedHeader, bool)
AgreedHeaderAbove returns the lowest header above minSlot that every snapshot observed with an identical hash, and whether one exists.
This is the propagation check: a header only becomes "agreed" once it has reached every node in the topology, including the non-forging relay, so a block that never diffused is never mistaken for one that did. Taking the lowest such header rather than the deepest makes the answer the first block produced after the caller's baseline, which is what a propagation assertion is actually about.
type ScenarioPlan ¶
type ScenarioPlan struct {
Config *DevNetConfig
Phases []TimelinePhase
// HardTimeout bounds the whole scenario.
HardTimeout time.Duration
// InterruptionHold is how long a node stays down during the
// interruption and restart phases.
InterruptionHold time.Duration
// RecoveryBudget is the allowance for a restarted node to rejoin and
// catch back up to the network's chain.
RecoveryBudget time.Duration
// OutageBlocks is how far the rest of the network must advance while
// a node is stopped. Holding the outage to observed chain progress
// rather than wall clock keeps the disruption meaningful on a fast
// or slow runner alike, and keeps it inside what k can reconcile.
OutageBlocks uint64
// EpochMargin is how far past an epoch boundary the scenario runs
// before asserting agreement, so the assertion covers headers built
// on the new epoch nonce rather than the boundary block alone.
EpochMargin uint64
}
ScenarioPlan is the deterministic schedule an accelerated DevNet run follows. Every budget is derived from the network's own timing parameters, so changing the spec moves the schedule with it instead of silently invalidating hardcoded waits.
func NewScenarioPlan ¶
func NewScenarioPlan(cfg *DevNetConfig) (*ScenarioPlan, error)
NewScenarioPlan derives the schedule for a network spec. It rejects a spec that is not internally consistent, since a plan built on one would describe a network that cannot run.
func (*ScenarioPlan) FitsReferenceBudget ¶
func (p *ScenarioPlan) FitsReferenceBudget() bool
FitsReferenceBudget reports whether the plan fits the documented reference-runner budget.
func (*ScenarioPlan) Phase ¶
func (p *ScenarioPlan) Phase(name string) (TimelinePhase, bool)
Phase returns the named phase.
func (*ScenarioPlan) String ¶
func (p *ScenarioPlan) String() string
String renders the schedule for the scenario log, so a run records the timeline it was held to.
func (*ScenarioPlan) Total ¶
func (p *ScenarioPlan) Total() time.Duration
Total returns the scenario length: the last phase's deadline on the shared clock.
type TimelinePhase ¶
TimelinePhase is one stage of the shared scenario timeline.
Deadline is measured from the scenario's start, not from the end of the previous phase. That is what makes this one timeline rather than a series of independent waits: a phase that completes early hands its slack to everything after it, and no assertion adds its own relative slot window on top of the phases before it.