chain

package
v1.7.38 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: BSD-3-Clause Imports: 13 Imported by: 0

Documentation

Overview

Package chain holds what every chain in this module is made of: a block's place in the sequence, the store that advances the chain one block at a time, the queue of entries waiting for a block, and the fee floor that admits them.

A chain package declares what it IS — its transactions, its records, its authorization rules. It does not restate how a block becomes fact. That is here, once, because every chain does it identically and the one way to get it wrong is to write it again.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrFull reports a pool at its bound. Admission is open to anyone who can
	// pay, so without a bound the queue is whatever an adversary makes it.
	ErrFull = errors.New("chain: pool is full")

	// ErrHeld reports a second entry claiming what a queued entry already
	// claims. Both would pass every check alone and together spend a block on
	// work only one of them can do.
	ErrHeld = errors.New("chain: claimed by a queued entry")
)
View Source
var ErrNoBlock = errors.New("chain: no such block")

ErrNoBlock reports that no block is known by that id or at that height.

Functions

This section is empty.

Types

type Block

type Block interface {
	ID() ids.ID
	Height() uint64
	Bytes() []byte

	// Write stages every durable change the block makes, through the view it
	// is given and nothing else. Writing anywhere else escapes the rollback.
	Write(database.Database) error

	// Publish makes the block's effects visible in memory. It runs after the
	// commit, under the store's lock. It cannot fail: anything that can fail
	// belongs in Write.
	Publish()
}

Block is a unit of state change a Store can accept: an identity, a place in the sequence, an encoding, and the two halves of applying it.

Splitting Write from Publish is what makes a half-applied block unwritable. Write can fail and is discarded whole; Publish cannot fail and runs only once the writes are durable. A chain that writes and publishes in one pass has no such boundary, and its first failed write leaves the chain believing something that is not on disk.

This is deliberately NOT the engine's block interface. A linear chain's blocks satisfy that as well; a DAG's vertices have several parents and no timestamp and do not. Both change state the same way, and this is that way.

type Factory

type Factory[T any] struct{}

Factory builds one chain's VM. The zero VM is the whole of it: a VM gets its database, its configuration and its caches in Initialize, and nothing else may hand it state — a factory that half-populates a VM leaves two places that decide what a fresh VM holds, and they drift.

func (Factory[T]) New

func (Factory[T]) New(log.Logger) (interface{}, error)

New returns a VM with nothing set. Initialize does the rest.

type Fee

type Fee struct {
	// contains filtered or unexported fields
}

Fee is what a chain charges to admit a user transaction, and the check that refuses one paying less.

It is a DECLARATION at the boundary, orthogonal to settlement: this says what the chain costs to submit to, while the actual per-operation debit and burn happen inside consensus against the payer's on-chain balance (github.com/luxfi/chains/fee). A chain declares one of these at Initialize; the node's boot-time nodefee.Validate reads it.

The zero Fee admits nothing, so a chain that forgets to declare one refuses every caller rather than admitting every caller.

func Closed

func Closed() Fee

Closed is the declaration for a chain that accepts no user transactions at all — one driven by validators through consensus, or one that only reads. Every caller is refused, so an entry that exposes itself as user-callable still refuses explicitly instead of by omission.

func Floor

func Floor(networkID uint32) Fee

Floor is the canonical declaration for a chain that accepts user-submitted work: the network's UTXO asset at the minimum transaction fee. A chain with a user-facing entry MUST charge at least this, or nodefee.Validate flags it at boot as a zero-fee user-facing chain.

func (Fee) Admit

func (f Fee) Admit(paid uint64) error

Admit refuses a payment the declaration does not cover. Every user-facing entry that produces an on-chain effect passes through here before the effect is reachable.

func (Fee) Policy

func (f Fee) Policy() nodefee.Policy

Policy is the declaration itself, for diagnostics and the boot-time gate.

type Pool

type Pool[T any, K comparable] struct {
	// contains filtered or unexported fields
}

Pool is what a chain has waiting for a block: the queue, in the order it arrived, and the set of effects those entries claim.

The claim set is DERIVED from the queue — rebuilt from what is left whenever the queue shrinks — so a claim cannot outlive the entry that made it. A set maintained alongside the queue instead of from it drifts the moment one removal path forgets to touch it, and what it then refuses is work nothing is going to do.

LOCK ORDER: a chain takes its Store's lock first and a Pool's second, never the other way round. Accept holds the store lock and drops from the pool inside it, so the reverse order deadlocks.

func NewPool

func NewPool[T any, K comparable](max int, claim func(T) K) *Pool[T, K]

NewPool returns a pool bounded at max entries, where claim says what an entry takes — which is also how it is found again.

func (*Pool[T, K]) Add

func (p *Pool[T, K]) Add(entry T) error

Add queues an entry and tells consensus there is something to build. A chain builds nothing until it is told.

func (*Pool[T, K]) Drop

func (p *Pool[T, K]) Drop(entries []T)

Drop removes accepted entries and rebuilds the claim set from what remains.

func (*Pool[T, K]) Len

func (p *Pool[T, K]) Len() int

Len is how many entries are waiting.

func (*Pool[T, K]) Take

func (p *Pool[T, K]) Take(n int) []T

Take returns up to n entries in arrival order, oldest first, and leaves them queued. A block SELECTS from the pool rather than draining it, so an engine that discards a proposal — which it may do without ever rejecting it — cannot take the queue with it. n of zero or less takes everything.

func (*Pool[T, K]) Wait

func (p *Pool[T, K]) Wait(ctx context.Context) (vmcore.Message, error)

Wait blocks until there is something to build a block from, or the caller gives up. Waiting on the context alone would mean a chain never leaves genesis however much it is offered.

type Store

type Store[B Block] struct {
	sync.RWMutex
	// contains filtered or unexported fields
}

Store is a chain's durable state, the blocks in flight above it, and the tip it has reached.

ONE lock covers all three, and whatever caches the chain keeps beside them — take it with Lock or RLock. A second mutex over any part of this gives one map two owners, which in Go is a fatal throw rather than a bug you get to debug.

func New

func New[B Block](db database.Database, reload func() error) *Store[B]

New opens a store over db.

reload rebuilds whatever the chain caches in memory from committed state. It is called after a failed apply, so the caches say what the database says rather than what the abandoned block said. A chain that mutates nothing in memory before the commit has nothing to rebuild and passes nil.

func (*Store[B]) Accept

func (s *Store[B]) Accept(b B) error

Accept applies b and commits it.

Every write b makes goes through the view and lands in ONE commit, together with the block itself, its height entry and the tip pointer. So the chain has the whole block or none of it. Any failure rolls the view back, rebuilds the caches from committed state, and leaves the tip where it was: nothing the block claimed survives, and the chain does not believe it happened.

The block's own effects become visible last, after the commit, so there is no window in which the chain has advanced past state that is not on disk.

func (*Store[B]) Accepted

func (s *Store[B]) Accepted(id ids.ID) bool

Accepted reports whether id is the accepted tip or a block committed beneath it. A block in flight is neither.

func (*Store[B]) Base

func (s *Store[B]) Base() database.Database

Base is committed state, for the writes a chain makes outside any block. A write here is durable at once and belongs to no block, so no rollback takes it back — which is right for a record no block claims and wrong for one a block does.

func (*Store[B]) Block

func (s *Store[B]) Block(id ids.ID, parse func([]byte) (B, error)) (B, error)

Block returns a block by id: one in flight, or one read back from committed state and decoded by parse.

func (*Store[B]) Close

func (s *Store[B]) Close() error

Close releases the store. The view is closed rather than committed: anything still staged belongs to a block that was never accepted.

func (*Store[B]) Drop

func (s *Store[B]) Drop(id ids.ID)

Drop forgets a block in flight. A rejected block is one the chain will not build on, and it never wrote anything, so there is nothing else to undo.

func (*Store[B]) IDAtHeight

func (s *Store[B]) IDAtHeight(height uint64) (ids.ID, error)

IDAtHeight names the block accepted at height, from the index written in the same commit as the block itself — so the index can never name a block the chain did not accept.

func (*Store[B]) Open

func (s *Store[B]) Open(genesis B, parse func([]byte) (B, error)) (B, bool, error)

Open sets the chain's starting point: the tip recorded in committed state, or genesis if nothing is recorded. It reports which, so a chain that seeds state on its first run can tell its first run from every later one.

func (*Store[B]) Prefer

func (s *Store[B]) Prefer(id ids.ID)

Prefer records the block the engine wants the next one built on. It is the tip until the engine says otherwise, and a preference the store no longer holds — pruned, or never tracked — falls back to the tip rather than naming a parent nothing can resolve.

func (*Store[B]) Propose

func (s *Store[B]) Propose(build func(parent B) (B, error)) (B, error)

Propose hands the caller the block to build on and tracks whatever it builds, in one step. Reading the parent and registering the child as two steps leaves a window in which a block is accepted between them, and the proposal is then built on a parent that has moved.

A build with nothing to propose says so with an error, and nothing is tracked.

func (*Store[B]) Seed

func (s *Store[B]) Seed(write func(database.Database) error) error

Seed applies the one mutation a chain makes outside consensus: what its genesis allocates, written through the view and committed at once, before any block exists. Everything after this happens in a block.

It commits for the same reason Accept does. Left staged, a genesis allocation would ride on whichever block committed first and vanish with a chain that never accepted one.

func (*Store[B]) Tip

func (s *Store[B]) Tip() (ids.ID, uint64)

Tip is the accepted block's id and height.

func (*Store[B]) Track

func (s *Store[B]) Track(b B)

Track makes a block findable by id while it is in flight, so a child can resolve it as a parent — whether this node built the block or parsed it from a peer. Tracking only what a node builds leaves a follower able to verify the first block of a run and unable to verify the second.

func (*Store[B]) View

func (s *Store[B]) View() database.Database

View is what a block writes through, and what every read sees: committed state plus whatever the block in progress has staged.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL