Documentation
¶
Overview ¶
Package bal is the conservation primitive (@B): an append-only double-entry journal over bounded accounts, where conservation is an arithmetic identity — every transfer writes two signed entries (−a, +a) in one transaction, so the system total always equals the sum of its boundary accounts.
Identity (@B09a): the substrate-preferred two-identity split. The external account_id is a namespaced STRING at every boundary — no numeric width exists to truncate, so the /ts id-boundary bug class is structurally impossible here (@C04d the strong way). The internal account key is a dense uint32, engine-internal, fixed-width codec, never on any wire struct.
Numerics (@B04): amounts are int64 minor units through the account's scale. No float64 touches an amount anywhere, ever — ParseAmount is the only sanctioned string→amount crossing.
Index ¶
- func EncodeAccountKey(k AccountKey) [4]byte
- func FormatAmount(v int64, scale uint8) string
- func LoadSealer(ctx context.Context, db *sql.DB, tenantID tenant.TenantID) (*chronicle.Sealer, error)
- func ParseAmount(s string, scale uint8) (int64, error)
- func ValidateAccountID(id string) error
- type AccountDef
- type AccountKey
- type AccountSummary
- type Adapter
- func (a *Adapter) Execute(ctx context.Context, store dxp.ParticipantStore, c dxp.Claim) (dxp.Result, error)
- func (a *Adapter) PostCommit(ctx context.Context, c dxp.Claim) error
- func (a *Adapter) Release(ctx context.Context, c dxp.Claim) error
- func (a *Adapter) Reserve(ctx context.Context, tenant string, op dxp.OpParams, txn, participantID string, ...) (dxp.Claim, error)
- func (a *Adapter) Validate(ctx context.Context, c dxp.Claim) error
- type AmountScaleError
- type BackdatedError
- type BoundsError
- type ChainBreak
- type CheckpointDivergence
- type DuplicateAccountError
- type Entry
- type NotPostableError
- type RollupPebble
- type SealedPeriodError
- type Store
- func (s *Store) AccountScale(ctx context.Context, accountID string) (uint8, error)
- func (s *Store) Balance(ctx context.Context, accountID string) (value int64, version int64, err error)
- func (s *Store) BalanceAsOf(ctx context.Context, accountID string, t time.Time) (int64, error)
- func (s *Store) BalanceAsOfExact(ctx context.Context, accountID string, t time.Time) (int64, error)
- func (s *Store) ChainOracle() chronicle.RebuildOracle
- func (s *Store) Checkpoint(ctx context.Context, accountID string, at time.Time) error
- func (s *Store) DefineAccount(ctx context.Context, def AccountDef) (AccountKey, error)
- func (s *Store) EmitDeltas(ctx context.Context, srcKey, dstKey, amount int64, at time.Time) error
- func (s *Store) Entries(ctx context.Context, accountID string, afterEntryID int64, limit int) ([]Entry, error)
- func (s *Store) GlobalFoldOracle() chronicle.RebuildOracle
- func (s *Store) Init(ctx context.Context) error
- func (s *Store) InitRollup(ctx context.Context) error
- func (s *Store) InitSeal(ctx context.Context) error
- func (s *Store) ListAccounts(ctx context.Context) ([]AccountSummary, error)
- func (s *Store) OnRollupError(fn func(error))
- func (s *Store) PruneJournal(ctx context.Context, before time.Time) (int, error)
- func (s *Store) RebuildRollup(ctx context.Context, accountID string) error
- func (s *Store) RollupOracle() chronicle.RebuildOracle
- func (s *Store) SealPeriod(ctx context.Context, at time.Time) (int, error)
- func (s *Store) SetClaimsCache(c *dxp.MemCache)
- func (s *Store) SetRollupPebble(rp *RollupPebble)
- func (s *Store) SetSealer(sealer *chronicle.Sealer)
- func (s *Store) TenantID() tenant.TenantID
- func (s *Store) Transfer(ctx context.Context, transferID, from, to string, amount int64, memo string, ...) error
- func (s *Store) VerifyChains(ctx context.Context) ([]ChainBreak, error)
- func (s *Store) VerifyCheckpoints(ctx context.Context) ([]CheckpointDivergence, error)
- type TransferParams
- type UnknownAccountError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func EncodeAccountKey ¶
func EncodeAccountKey(k AccountKey) [4]byte
EncodeAccountKey writes the key with an explicit fixed-width codec (4 bytes, big-endian) — the sanctioned byte crossing.
func FormatAmount ¶
FormatAmount renders minor units back to the canonical decimal string at the account's scale.
func LoadSealer ¶ added in v0.26.0
func LoadSealer(ctx context.Context, db *sql.DB, tenantID tenant.TenantID) (*chronicle.Sealer, error)
LoadSealer constructs a chronicle.Sealer over bal's calendar-month tiling (chronicle.MonthWindows -- "bal's period shape", per its own doc) and restores any persisted frontier, so a restarted process recovers exactly where it left off rather than silently un-sealing the past. Returns a fresh, unsealed Sealer if no frontier was ever persisted (the common case: most tenants never call bal/close).
The returned Sealer is long-lived (mirrors RollupPebble/dxp.MemCache): callers should construct it once per tenant and attach it to each request's freshly-built Store via SetSealer, not reconstruct it per request -- reconstructing per request would still recover the correct frontier from SQL, but would defeat the in-memory monotonicity AdvanceTo exists to provide between requests.
func ParseAmount ¶
ParseAmount converts a decimal string to int64 minor units under the account's scale — the ONLY string→amount crossing. It refuses anything that is not an exact integer of minor units at that scale (XOLU-BAL004 territory), and never passes through float64.
func ValidateAccountID ¶
ValidateAccountID enforces the external-id shape: non-empty, ≤256, no whitespace; '/' and ':' and '.' are the namespace vocabulary.
Types ¶
type AccountDef ¶
type AccountDef struct {
ID string // namespaced external id, e.g. "warehouse:A/widget" or "1.1.9.10"
Unit string // "EUR", "widget", "gram"
Scale uint8 // decimal places of the minor unit
Floor int64 // minimum balance (default 0)
Ceiling *int64 // optional maximum balance
Postable bool // only leaf (imputable) accounts accept entries (@B03a)
Policy string // temporal policy (T-55): "" or "append_only" (default) | "backdated"
}
AccountDef defines an account (@B03, @B03a).
type AccountKey ¶
type AccountKey uint32
AccountKey is the engine-internal dense identity: uint32, the wave-1 per-primitive width. It never appears in JSON.
const MaxAccountKey AccountKey = 0xFFFFFFFF
MaxAccountKey is the codec ceiling; it fits uint32 exactly.
func DecodeAccountKey ¶
func DecodeAccountKey(b [4]byte) AccountKey
DecodeAccountKey reads the fixed-width codec back, losslessly across the full uint32 span.
type AccountSummary ¶ added in v0.30.23
type AccountSummary struct {
AccountID string
Unit string
Scale uint8
Floor int64
Ceiling *int64
Postable bool
Policy string
Value int64
Version int64
}
AccountSummary is one row of ListAccounts -- an account's own definition joined with its current balance, sparing a caller the N+1 round trip of listing accounts and then calling Balance on each one individually. Floor/Ceiling/Value are minor-unit int64 at the account's own Scale, matching every other bal type's own internal representation (@B04) -- the server handler, not this method, is responsible for formatting them as decimal strings on the wire.
type Adapter ¶ added in v0.26.0
type Adapter struct {
// contains filtered or unexported fields
}
Adapter is bal's dxp.Participant. One Adapter per Store; safe for concurrent use.
func NewAdapter ¶ added in v0.26.0
NewAdapter wires store into cache (store.SetClaimsCache) and returns an Adapter ready to register with a dxp coordinator under the primitive key "bal".
func (*Adapter) Execute ¶ added in v0.26.0
func (a *Adapter) Execute(ctx context.Context, store dxp.ParticipantStore, c dxp.Claim) (dxp.Result, error)
Execute applies c's transfer via transferInTx against the coordinator-supplied tx (proposal §11: one SQL transaction for every participant; the coordinator opens and commits tx, never Execute). It does not emit rollup deltas: those must run strictly after commit (see Transfer), and a shared multi-participant transaction has no single commit point this method can observe. A coordinator driving Execute is responsible for its own post-commit rollup pass per participant; dxp.Participant has no post-commit verb today — flagged as an item 21 gap (recorded in TRACKING.md under T-54), not silently dropped.
srcClaimed/dstClaimed come from the pendingTransfer snapshot (captured at Reserve, refreshed at Validate for pessimistic claims) rather than being read fresh here. Execute runs with the coordinator's transaction already open on the single-connection writer pool, so acquiring the tenant cache lock here — however briefly — closes the AB/BA cycle T-138 documents against Reserve/Validate/Transfer, which hold that lock while waiting for this pool. srcClaimed already excludes c itself (snapshot taken before Hold, or explicitly subtracted at Validate); dstClaimed never includes self, since v1 reserves the debit side only. A DIFFERENT instance's post-snapshot claim is still respected transitively: its own Reserve arithmetic saw this claim live in the cache, and the authoritative admission remains transferInTx's guarded UPDATE.
func (*Adapter) PostCommit ¶ added in v0.26.0
PostCommit folds the transfer's two signed legs into bal's own rollup cascade (@B05) after the coordinator has confirmed genuine, durable commit -- the same post-commit rollup pass Transfer's own doc names as owed for any coordinator driving transferInTx directly (T-62/T-83). The mechanism itself (dxp.Participant.PostCommit) was built for cal (T-108), which named bal's rollup plane as the next real consumer; this wires bal onto that same mechanism.
Re-reads fresh rather than trusting anything captured before commit, matching cal's own PostCommit discipline exactly: accountKeyOf looks up each leg's account_key fresh, and journalInstant reads the actual recorded `at` from the journal entry Execute already wrote -- not a freshly-taken "now", which could drift into the wrong rollup bucket if PostCommit runs some time after the transaction it follows.
func (*Adapter) Release ¶ added in v0.26.0
Release drops txn's stashed params, if any. Idempotent and unconditional per the proposal's error taxonomy (§16): releasing an unknown or already-cleared txn is a no-op, never an error. The cache entry itself is removed by the coordinator's ReleaseTxn, not here.
func (*Adapter) Reserve ¶ added in v0.26.0
func (a *Adapter) Reserve(ctx context.Context, tenant string, op dxp.OpParams, txn, participantID string, deadline int64, w dxp.Weight) (dxp.Claim, error)
Reserve evaluates whether tp.Amount is available against tp.From — balance minus floor minus every live PESSIMISTIC claim already held against the account, regardless of THIS reservation's own weight (§7: pessimistic claims bind every guard everywhere; only whether the NEW claim itself counts toward others' admission depends on w). On consent it Holds a claim and stashes tp for Execute. The whole evaluate-then-hold sequence runs under one tenant.Lock/Unlock critical section (proposal §4).
func (*Adapter) Validate ¶ added in v0.26.0
Validate re-checks that the sum of every live pessimistic claim against c's account — c's own included — still fits within balance minus floor. That sum already includes c.Amount, so this is exactly the invariant Reserve established, re-evaluated against whatever the account's balance and floor are now (proposal §6: "guard inputs may change during the window"). The balance read and the claims read run under the SAME tenant.Lock, matching Reserve — a read split across two lock acquisitions would reopen the TOCTOU gap the lock exists to close.
Optimistic claims are invisible to guard arithmetic everywhere (§7) and have nothing of their own for Validate to re-check here; the coordinator (item 21, not yet built) discovers invalidation-by-loss for them via ConfirmTxn's empty-return contract, not via this method — Validate cannot itself distinguish DXP007 (lost to a competitor) from DXP003 (drift) without coordinator-level context on who else held claims, so it returns a bal-native error and leaves that classification to the coordinator, consistent with Reserve's refusals.
type AmountScaleError ¶
type AmountScaleError struct{ Detail string }
func (*AmountScaleError) Error ¶
func (e *AmountScaleError) Error() string
type BackdatedError ¶ added in v0.26.0
BackdatedError refuses a strictly-backdated entry on an append_only account (T-55; code XOLU-BAL006). Same-instant entries are admitted.
func (*BackdatedError) Error ¶ added in v0.26.0
func (e *BackdatedError) Error() string
func (*BackdatedError) Unwrap ¶ added in v0.26.0
func (e *BackdatedError) Unwrap() error
type BoundsError ¶
func (*BoundsError) Error ¶
func (e *BoundsError) Error() string
type ChainBreak ¶
ChainBreak localises a violation of the per-account arithmetic chain.
type CheckpointDivergence ¶ added in v0.26.0
CheckpointDivergence records one checkpoint whose frozen balance disagrees with the journal's authoritative sum at its boundary.
type DuplicateAccountError ¶ added in v0.26.0
type DuplicateAccountError struct{ AccountID string }
DuplicateAccountError: XOLU-BAL007 — account_id already defined. HTTP 409. Found by /loc's own adversarial testing pass (T-118), which surfaced the identical gap here: a UNIQUE constraint violation on account_id previously had no typed error, falling through to a bare 500 for what is an ordinary client mistake (defining an id that already exists), not a server failure.
func (*DuplicateAccountError) Error ¶ added in v0.26.0
func (e *DuplicateAccountError) Error() string
type Entry ¶ added in v0.16.15
type Entry struct {
EntryID int64 `json:"entry_id"`
TransferID string `json:"transfer_id"`
AccountID string `json:"account_id"`
Amount int64 `json:"-"` // rendered as string by the handler (@B04)
PreviousBalance int64 `json:"-"`
CurrentBalance int64 `json:"-"`
Version int64 `json:"version"`
Memo string `json:"memo,omitempty"`
At time.Time `json:"at"`
}
Entry is one journal row on the API surface: external account id, signed amount, the chain triple, memo, instant. Internal keys never appear (@B09a).
type NotPostableError ¶
type NotPostableError struct{ AccountID string }
func (*NotPostableError) Error ¶
func (e *NotPostableError) Error() string
type RollupPebble ¶ added in v0.26.0
type RollupPebble struct {
// contains filtered or unexported fields
}
RollupPebble is the long-lived Pebble handle backing bal's rollup plane for one tenant. Opened once (OpenRollupPebble) and attached to each freshly-constructed *Store via SetRollupPebble — mirroring dxp.MemCache/SetClaimsCache exactly, and for the same reason: bal.Store is built fresh per request in pkg/server, but a *pebble.DB handle holds an exclusive on-disk lock and cannot be re-opened per request. The caller (pkg/server) owns caching one RollupPebble per tenant and attaching it to each request's Store; bal itself stays agnostic of that lifecycle, same as it is for the claims cache.
func OpenRollupPebble ¶ added in v0.26.0
func OpenRollupPebble(dir string) (*RollupPebble, error)
OpenRollupPebble opens (creating if needed) the Pebble database backing bal's rollup plane at dir — typically storelayout.TenantBalRollupDir(base, tenantID), matching cal's OpenIndexStore convention exactly (Pebble database lives in dir/db). The returned handle is attached to a Store via SetRollupPebble; the same handle may be shared across multiple Store instances for the same tenant (they all read the same account_key numbering, since account_key is stable per tenant regardless of which Store issued it).
func (*RollupPebble) Close ¶ added in v0.26.0
func (rp *RollupPebble) Close() error
Close closes the underlying Pebble database. The caller must ensure no Store still holds this handle (via SetRollupPebble) when Close is called — mirroring the same discipline pkg/server already applies to dxp.MemCache and cal.Manager's Pebble-backed IndexStore.
type SealedPeriodError ¶ added in v0.26.0
SealedPeriodError reports a transfer refused because its instant falls within the tenant's sealed (immutable) past -- XOLU-BAL003. Unlike BoundsError (a per-account admission refusal), this is a tenant-wide, policy-independent refusal: no account's temporal_policy setting can override a sealed period.
func (*SealedPeriodError) Error ¶ added in v0.26.0
func (e *SealedPeriodError) Error() string
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is bal's SQL plane (@B05): the append-only journal and the balances table, maintained in the same transaction as each entry. The bounds guard's input commits-or-aborts with the entry it guards (@C04a); no rollup is ever consulted by a guard.
The db handle must carry WAL + busy_timeout (the house defaults). Transfer is deliberately WRITE-FIRST: its opening statement is the guarded UPDATE itself (accounts resolved by subquery), so the transaction is a writer from its first statement and contending transfers queue under busy_timeout even on plain deferred transactions. A read-first shape would hit WAL's snapshot invalidation (SQLITE_BUSY past the busy handler) on read→write upgrade — the G-13 harness caught exactly that in an earlier form.
func NewStore ¶
NewStore binds bal to a database with a tenant table prefix. NewStore binds bal to a database for one tenant. tenantID is the canonical identity (pkg/tenant.IDString is the substrate-wide invariant every primitive's cross-primitive-comparable tenant key must derive from — see IDString's doc); prefix is derived from it, never accepted independently, so the two cannot drift the way an earlier version of this constructor allowed by taking a bare prefix string with no tenantID behind it at all.
func (*Store) AccountScale ¶ added in v0.16.15
AccountScale returns an account's scale (render support).
func (*Store) Balance ¶
func (s *Store) Balance(ctx context.Context, accountID string) (value int64, version int64, err error)
Balance returns the current balance and version for an account.
func (*Store) BalanceAsOf ¶ added in v0.26.0
BalanceAsOf returns the account's balance at an instant, read the fast way (@B05): nearest sealed checkpoint at or before t, plus the fold of buckets between that checkpoint and t. With no checkpoint it folds from the epoch. The exact/audit path is the journal chain, which BalanceAsOfExact reads independently — the two must agree, and the rollup oracle asserts they do.
func (*Store) BalanceAsOfExact ¶ added in v0.26.0
BalanceAsOfExact reads the same quantity from the authoritative journal — the exact/audit path (@B08). Slower (scans entries) but independent of the rollup plane, which is what makes it a valid oracle for it.
func (*Store) ChainOracle ¶
func (s *Store) ChainOracle() chronicle.RebuildOracle
ChainOracle wraps VerifyChains as a rebuild oracle for iolu db check.
func (*Store) Checkpoint ¶ added in v0.26.0
Checkpoint writes a closing balance at a sealed period boundary. The checkpoint is what makes as-of independent of journal length, and what permits prefix-collapse retention later (@B05, item 16).
This is for sealing a NEW boundary that has no checkpoint yet — not for repairing an existing one after a backdated entry. Existing checkpoints are kept correct as of the moment they're written by transferInTx's eager delta-adjustment (T-58); this recompute-from- journal path is never needed to fix one. Calling Checkpoint again at an already-checkpointed boundary is harmless (it recomputes the same correct value from source) but is a no-op in effect, not a repair.
func (*Store) DefineAccount ¶
func (s *Store) DefineAccount(ctx context.Context, def AccountDef) (AccountKey, error)
DefineAccount creates an account and its zero balance row. The internal key is allocated densely (MAX+1) inside the transaction. DefineAccount creates an account plus its paired balances row (value 0, version 0) in one transaction. Write-first: the dense MAX(key)+1 allocation and the insert happen inside a single INSERT...SELECT...RETURNING statement, not a preceding SELECT — an earlier version read the next key as a separate statement first, the same WAL read-then-write-upgrade race T-115 found and fixed in loc.Move, confirmed present here too by /loc's own adversarial concurrency test (30 concurrent defines) applied against this function directly, not assumed safe by analogy alone. A duplicate account_id is now refused with a typed DuplicateAccountError (XOLU-BAL007, HTTP 409) rather than an unwrapped SQLite driver error falling through to a bare 500 — the same finding and fix as loc's own DuplicateLocationError.
func (*Store) EmitDeltas ¶ added in v0.26.0
EmitDeltas folds a transfer's two signed legs into the rollup cascade. Called after the transfer's transaction commits: the rollup is derived, so a crash between commit and emit loses only derived state, which the rebuild oracle detects and RebuildRollup repairs.
func (*Store) Entries ¶ added in v0.16.15
func (s *Store) Entries(ctx context.Context, accountID string, afterEntryID int64, limit int) ([]Entry, error)
Entries returns up to limit journal rows for an account, oldest first, starting after afterEntryID (0 = from the beginning).
func (*Store) GlobalFoldOracle ¶
func (s *Store) GlobalFoldOracle() chronicle.RebuildOracle
GlobalFoldOracle: SELECT SUM per account from the journal, compared row-for-row against balances — derive(journal) == current, exactly.
func (*Store) Init ¶
Init creates the bal tables. Idempotent. The journal's `state` column (default 'committed') leaves room for holds (@B10) without migration.
func (*Store) InitRollup ¶ added in v0.26.0
InitRollup verifies the rollup plane's Pebble store is open (OpenRollupPebble must be called first — matching cal's OpenIndexStore/Manager split). Separate from Init so the SQL plane can exist without the derived plane (the derived plane is always rebuildable). T-62: previously created a SQL bucketsTable here; the rollup plane is now Pebble, opened via OpenRollupPebble, which creates its own directory on first use — there is no DDL left to run, so this is now a precondition check, not a table creation.
The checkpoints table itself is still created by Init (core schema), because Transfer's write path maintains checkpoint staleness in-transaction (T-51/T-58) — the journal plane owns that table's lifecycle even though the rollup plane is its main reader. Checkpoints are a write-locality case, not a guard-locality one (rollup_pebble.go doc), which is why they did NOT move here.
func (*Store) InitSeal ¶ added in v0.26.0
InitSeal creates the seal-frontier persistence table. Idempotent, separate from Init and InitRollup for the same reason both of those are separate: the SQL plane and each derived/administrative plane can exist independently (the seal frontier, once persisted, is the durable record LoadSealer recovers from on restart -- chronicle.Sealer itself is memory-only by design, per its own doc comment: "recovery is the consumer's, because the consumer owns the durable record").
func (*Store) ListAccounts ¶ added in v0.30.23
func (s *Store) ListAccounts(ctx context.Context) ([]AccountSummary, error)
ListAccounts returns every account defined on the tenant, joined with each one's current balance, ordered by account_id for a stable, predictable listing. Requested by xoluman (XM-2, XOT172) -- no way existed to enumerate a tenant's own accounts at all before this; BalDefine/BalBalance/BalEntries all require already knowing an account's own id.
func (*Store) OnRollupError ¶ added in v0.26.0
OnRollupError registers a callback for derived-plane degradation.
func (*Store) PruneJournal ¶ added in v0.26.0
PruneJournal implements the finiteness law (chronicle-substrate.md §4b): "entries older than a sealed checkpoint are derivationally redundant... policy may archive-then-prune the pre-checkpoint journal while conservation survives through the checkpoint chain."
For each postable account, PruneJournal finds that account's latest checkpoint which is BOTH itself sealed (Sealer.Sealed) AND at or before `before`, and deletes every journal entry at or before that checkpoint's instant. `before` is a caller-supplied retention floor, never a ceiling the seal alone doesn't already impose: an account is never pruned past what's actually sealed, regardless of `before` -- "no seam to tamper with" (§4b) requires the boundary to be a real seal, not an arbitrary caller-chosen instant. `before` only lets a caller retain MORE than the seal strictly requires (a longer compliance retention window, for instance), never less. An account with no sealed, in-range checkpoint is left untouched -- pruning nothing is always a correct, safe outcome.
Archival (§4b: "optional everywhere, and never a precondition for correctness") is deliberately not implemented here -- this is prune only. A caller wanting cold-storage archival before calling this owns that step itself (blob storage, per §4b's own guidance on where it would target).
Go-only by design, not yet exposed over HTTP -- see docs/KNOWN_ISSUES.md's "bal design — recorded decisions" for why, and cmd/iolu's `bal prune` command for the operator-facing path.
func (*Store) RebuildRollup ¶ added in v0.26.0
RebuildRollup discards and re-derives an account's buckets from the journal — the repair path for a rollup lost to a crash between commit and emit, and the operation the rebuild oracle's divergence implies.
func (*Store) RollupOracle ¶ added in v0.26.0
func (s *Store) RollupOracle() chronicle.RebuildOracle
RollupOracle proves the derived plane against the authoritative one ACROSS THE CASCADE, not merely at the leaf: for every account, the week-level fold of the whole journal span must equal the journal's own SUM. A cascade that combines correctly at the finest grain but loses a carry upward would pass a leaf-only check and fail this one.
func (*Store) SealPeriod ¶ added in v0.26.0
SealPeriod advances the tenant's seal frontier to at and checkpoints every postable account as of that instant -- the two things item 16 §7 says bal/close does together ("its closing checkpoints are written"). Returns the number of accounts checkpointed.
Persisting AFTER AdvanceTo (not the caller's raw `at`) means a concurrent or repeated close can never regress the persisted value: AdvanceTo's own monotonicity decides what actually got sealed, and that -- not the request's input -- is what gets written durably.
func (*Store) SetClaimsCache ¶ added in v0.26.0
SetClaimsCache wires bal into the dxp reservation cache (T-54, item 19): once set, Transfer folds live pessimistic claims against each leg into its own guarded UPDATE before admitting, so an ordinary HTTP transfer cannot spend balance a dxp reservation is holding (proposal §4). nil (the default) is exact pre-dxp behaviour.
func (*Store) SetRollupPebble ¶ added in v0.26.0
func (s *Store) SetRollupPebble(rp *RollupPebble)
SetRollupPebble attaches an already-open rollup Pebble handle to this Store. Required before InitRollup/EmitDeltas/BalanceAsOf/ RebuildRollup/RollupOracle — mirrors SetClaimsCache exactly.
func (*Store) SetSealer ¶ added in v0.26.0
SetSealer attaches an already-loaded seal frontier to this Store. Mirrors SetRollupPebble/SetClaimsCache exactly, and for the same reason: bal.Store is built fresh per request, but the Sealer must be the SAME long-lived instance across requests for AdvanceTo's monotonicity to mean anything.
func (*Store) TenantID ¶ added in v0.26.0
TenantID returns the tenant this Store is scoped to — the canonical identity prefix is derived from, exposed for callers (the dxp adapter) that need a cross-primitive-comparable tenant key rather than bal's own table-naming-specific prefix form.
func (*Store) Transfer ¶
func (s *Store) Transfer(ctx context.Context, transferID, from, to string, amount int64, memo string, at time.Time) error
Transfer moves amount minor units from `from` to `to` as two signed journal entries (−a, +a) in ONE transaction (@B03). Admission is the house CAS discipline (@B06, T-34): the decision lives inside each UPDATE's predicate, rows-affected is the verdict — never read-decide-write. The guarded UPDATE is the transaction's FIRST statement (write-first; see Store docs), resolving the account and its floor by subquery and returning the chain triple in the same statement. Error discrimination (unknown vs not-postable vs bounds) happens on the failure path only, where the transaction is read-only and about to roll back.
func (*Store) VerifyChains ¶
func (s *Store) VerifyChains(ctx context.Context) ([]ChainBreak, error)
VerifyChains is the local verifier (@B08): per account, every entry satisfies previous+amount=current, entryₙ.previous = entryₙ₋₁.current, and versions are contiguous. A lost, duplicated, or altered entry is not merely detected but LOCALISED to the exact break. VerifyChains proves the per-account arithmetic and linkage chain. The first entry it sees for an account is normally required to carry version 1 (the account's true first-ever entry) -- but once PruneJournal has removed an account's earlier entries, the first entry it SEES is no longer the first entry that ever existed, and asserting version == 1 would be a false positive on every pruned account, every run. Re-scoped (chronicle-substrate.md §4b): the version == 1 assertion is skipped only when a checkpoint precedes the first retained entry -- that's the signal an account was legitimately pruned, not evidence of a real gap. The arithmetic and linkage checks (prev+amount==cur, and consecutive-entry linkage among what IS retained) are unaffected either way; only the single "was this truly entry #1" assertion needed re-scoping.
func (*Store) VerifyCheckpoints ¶ added in v0.26.0
func (s *Store) VerifyCheckpoints(ctx context.Context) ([]CheckpointDivergence, error)
VerifyCheckpoints is the T-51 oracle prong: every NON-STALE checkpoint must equal SUM(journal WHERE at <= checkpoint boundary). This is the check whose absence let a wrong frozen balance ship silently.
The stale exemption is now (T-58) a migration-safety net, not the everyday case: transferInTx keeps every checkpoint correct by eager delta-adjustment as of the moment it's written, so no NEW checkpoint is ever stale. The exemption only matters for a checkpoint that was marked stale by the OLD (pre-T-58) code path and never recomputed — it stays correctly exempted rather than reported as a false divergence, and the exemption becomes naturally vacuous as no process writes stale=1 anymore. A divergent non-stale checkpoint is a defect, unconditionally, under either temporal policy. VerifyCheckpoints proves each checkpoint's stored balance against the journal ENTRIES IT ACTUALLY OWNS: the delta since the account's previous retained checkpoint (zero for the first one seen), not an absolute sum from the epoch. This is the "rebuild oracle re-scopes to the earliest retained checkpoint" requirement (chronicle-substrate.md §4b) — a delta-from-previous check gives an IDENTICAL result to the old from-epoch sum when nothing has ever been pruned (there is no "previous" to differ from zero), and stays correct once PruneJournal removes the entries before an account's earliest retained checkpoint. VerifyCheckpoints proves each checkpoint's stored balance against the journal ENTRIES IT ACTUALLY OWNS: the delta since the account's previous retained checkpoint, not an absolute sum from the epoch. This is the "rebuild oracle re-scopes to the earliest retained checkpoint" requirement (chronicle-substrate.md §4b) — a delta-from-previous check gives an IDENTICAL result to the old from-epoch sum when nothing has ever been pruned, and stays correct once PruneJournal removes the entries before an account's earliest retained checkpoint.
An account's FIRST retained checkpoint is a special case, not just a zero baseline: once PruneJournal has run, the journal entries that would PROVE that checkpoint correct are gone by design -- that's the whole point of retention (§4b: "forgetting is not editing"). This is NOT the same as "zero entries because nothing happened before it" (a legitimately verifiable case). The two are told apart the only way they can be: if ANY journal entry exists at or before the checkpoint, there was never any pruning to hide, and the normal sum-and-compare applies, still catching real corruption. If NONE exist, the checkpoint is trusted as an unverifiable genesis point rather than compared against a wrongly-assumed zero -- PruneJournal only ever deletes an account's FULL pre-checkpoint range at once (never partial), so "zero entries remain" is unambiguous once it's true.
type TransferParams ¶ added in v0.26.0
type TransferParams struct {
From string `json:"from"`
To string `json:"to"`
Amount int64 `json:"-"`
Memo string `json:"memo,omitempty"`
}
TransferParams is bal's dxp.OpParams (T-54's typed-per-primitive decision, dxp.OpParams doc): a debit-hold against one account, captured later as a transfer's debit leg.
v1 scope, deliberate: Reserve/Execute admission-check and hold only the DEBIT (From) side — matching the composed-commitment proposal's own hotel worked example (§5a), which reserves the paying account only. The credit (To) side is still guarded by the ordinary ceiling check inside transferInTx at Execute time; a ceiling refusal there surfaces as XOLU-DXP008 (execute failed, participant error carried) rather than being caught at Reserve. Reserving both legs — a second Claim per Transfer — is future work if a def author needs to fail fast on a bounded receiving account; nothing here forecloses it.
Amount is deliberately json:"-" — excluded from generic json.Unmarshal entirely. Checked directly against bal's own real HTTP handler (handleBalTransfer, pkg/server/v2_bal_handlers.go) before choosing this: amounts cross any JSON boundary as decimal STRINGS only (@B04), never a bare JSON number, refused via UseNumber()-based decoding and a json.Number type check before bal.ParseAmount(s, scale) ever runs. A plain json:"amount" tag on this int64 field would silently accept a raw JSON number and bypass @B04 entirely for any caller reaching this type through the dxp coordinator's own params-decoding step. That step must replicate the same UseNumber/ParseAmount path bal's own HTTP handler already uses (per-primitive, not a generic Unmarshal), setting Amount explicitly rather than through this struct's own json tags.
func (TransferParams) Primitive ¶ added in v0.26.0
func (TransferParams) Primitive() string
Primitive satisfies dxp.OpParams.
type UnknownAccountError ¶
type UnknownAccountError struct{ AccountID string }
func (*UnknownAccountError) Error ¶
func (e *UnknownAccountError) Error() string