speckl/examples/TigerBeetleLedger.speck
Scott Scoble a2f7b7b2e9
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
fix: add dist-check guard + List<T> type parsing + state machine examples
- check-dist.sh: Prevents DIST DESYNC by verifying all dist/*.js files
  are newer than their src/*.ts sources. Fails build if stale.
- Makefile: Standard build targets (build/check-dist/test/recompile/clean)
- src/index.ts: Integrated dist freshness check before compilation
- src/parser.ts: Added List<T> type parsing alongside existing Set<T>/Map<K,V>
  (e.g., 'log: List<LogEntry>' now parses as { type: 'list', elementType })
- examples/: Restored v0.3 state machine specs (TigerBeetleLedger,
  AccountLedger, ToggleSwitch, TwoPhaseCommit)
- DIST DESYNC is a known recurring anti-pattern — this guard prevents it

Tests: 39/41 pass (2 pre-existing E2E WASM failures due to missing wabt)
2026-05-05 03:11:53 -07:00

155 lines
5.7 KiB
Text

speck TigerBeetleLedger {
// State machine primitives for a naive TigerBeetle
// TigerBeetle is a distributed financial transaction database
// with co-designed consensus protocol + storage engine.
state {
// Accounts: id -> { balance, debits_pending, credits_pending, flags }
accounts: Map(Nat, Account)
// Transfers: id -> { from, to, amount, pending, posted, voided }
transfers: Map(Nat, Transfer)
// Pending two-phase transfers awaiting commit/void
pending: Set(Nat)
// Replica state for consensus
replicaState: Map(Nat, Replica)
// Cluster log
log: List(LogEntry)
// Highest committed log index
commitIndex: Nat
// Current leader replica id
leader: Nat
}
init {
accounts == emptyMap
transfers == emptyMap
pending == emptySet
replicaState == mapKeys(replicas, { role: "follower", logIndex: 0, votedFor: null })
log == emptyList
commitIndex == 0
leader == 0
}
invariant AccountBalanceInvariant {
forall a in accounts.values:
a.balance == a.credits_posted - a.debits_posted
forall a in accounts.values:
a.debits_pending >= 0
a.credits_pending >= 0
}
invariant TransferStateConsistency {
forall t in transfers.values:
t.pending implies t.posted == false && t.voided == false
forall t in transfers.values:
t.posted implies t.voided == false
forall t in transfers.values:
t.voided implies t.posted == false
}
invariant NoOverdraft {
forall a in accounts.values:
a.balance >= 0 or a.flags.allows_overdraft
}
// Two-phase transfer: reserve funds
action CreateTransfer(t: Transfer) {
precondition: t.id notIn transfers.keys
precondition: t.from in accounts.keys
precondition: t.to in accounts.keys
precondition: t.from != t.to
precondition: accounts[t.from].balance >= t.amount
or accounts[t.from].flags.allows_overdraft
postcondition: transfers[t.id] == t
postcondition: pending == pending union {t.id}
postcondition: accounts[t.from].debits_pending == old(accounts[t.from].debits_pending) + t.amount
postcondition: accounts[t.to].credits_pending == old(accounts[t.to].credits_pending) + t.amount
}
// Commit a pending two-phase transfer
action CommitTransfer(id: Nat) {
precondition: id in pending
precondition: id in transfers.keys
precondition: transfers[id].pending == true
postcondition: pending == pending \\ {id}
postcondition: transfers[id].pending == false
postcondition: transfers[id].posted == true
postcondition: accounts[transfers[id].from].debits_posted ==
old(accounts[transfers[id].from].debits_posted) + transfers[id].amount
postcondition: accounts[transfers[id].to].credits_posted ==
old(accounts[transfers[id].to].credits_posted) + transfers[id].amount
postcondition: accounts[transfers[id].from].debits_pending ==
old(accounts[transfers[id].from].debits_pending) - transfers[id].amount
postcondition: accounts[transfers[id].to].credits_pending ==
old(accounts[transfers[id].to].credits_pending) - transfers[id].amount
}
// Void a pending transfer
action VoidTransfer(id: Nat) {
precondition: id in pending
precondition: id in transfers.keys
precondition: transfers[id].pending == true
postcondition: pending == pending \\ {id}
postcondition: transfers[id].pending == false
postcondition: transfers[id].voided == true
postcondition: accounts[transfers[id].from].debits_pending ==
old(accounts[transfers[id].from].debits_pending) - transfers[id].amount
postcondition: accounts[transfers[id].to].credits_pending ==
old(accounts[transfers[id].to].credits_pending) - transfers[id].amount
}
// Single-phase transfer: no pending state
action PostTransfer(t: Transfer) {
precondition: t.id notIn transfers.keys
precondition: t.from in accounts.keys
precondition: t.to in accounts.keys
precondition: t.from != t.to
precondition: accounts[t.from].balance >= t.amount
or accounts[t.from].flags.allows_overdraft
postcondition: transfers[t.id] == { ...t, pending: false, posted: true, voided: false }
postcondition: accounts[t.from].debits_posted == old(accounts[t.from].debits_posted) + t.amount
postcondition: accounts[t.to].credits_posted == old(accounts[t.to].credits_posted) + t.amount
}
// Consensus: replica becomes leader
action BecomeLeader(replica: Nat, term: Nat) {
precondition: replica in replicaState.keys
precondition: replicaState[replica].role == "candidate"
precondition: replicaState[replica].currentTerm < term
postcondition: leader == replica
postcondition: replicaState[replica].role == "leader"
postcondition: replicaState[replica].currentTerm == term
}
// Consensus: append entry to log
action AppendEntry(entry: LogEntry, term: Nat) {
precondition: entry.index == length(log) + 1
precondition: term >= commitIndex
postcondition: log == append(log, entry)
postcondition: commitIndex == entry.index
}
next:
CreateTransfer
| CommitTransfer
| VoidTransfer
| PostTransfer
| BecomeLeader
| AppendEntry
verify: Always(AccountBalanceInvariant)
verify: Always(NoOverdraft)
verify: Eventually(TransferStateConsistency)
}