UTXO model · PHP 8.4+

Every balance is a set of unspent outputs.

A zero-dependency PHP library for value tracking, where double-spend prevention and a complete audit trail are baked into the data model — not bolted on afterwards.

MIT · zero dependencies · hexagonal architecture
composer
$ composer require chemaclass/unspent
The modelfig. 01

A transaction consumes existing unspent outputs and produces new ones. Spent outputs can never be spent again — that invariant is the ledger.

transaction — alice pays bob 30

  spends                             creates
  ┌─────────────────┐                ┌─────────────────┐
  │ alice     · 100 │ ──── spend ───▶│ bob       ·  30 │
  └─────────────────┘                ├─────────────────┤
    ✕ consumed                       │ alice     ·  70change
                                     └─────────────────┘
unspent output consumed input
Lineagefig. 02

Bitcoin has no account balances — only unspent outputs. Your balance is the set you can unlock; spending consumes whole outputs and creates new ones. This library ports that model to general bookkeeping, keeping its one-spend invariant.

bitcoin
unspent
unspent transaction output
Output
tx spends inputs → creates outputs
Tx
coinbase (minting)
CoinbaseTx · credit()
fee = inputs − outputs
computed on apply()
scriptPubKey conditions
OutputLock: key · multisig · timelock · hashlock
double-spend forbidden by consensus
output removed from the unspent set
Capabilitiesentries 01–06
01

Double-spend safe by construction

An output is spendable exactly once. Invalid transactions are rejected before they ever mutate the ledger — validate first with canApply().

02

Immutable audit trail

Domain objects are readonly. Every output records the transaction that created it and the one that spent it.

03

Pluggable persistence

In-memory for tests, SQLite for production, or implement the repository port for any backend. History and unspent set are stored independently for scale.

04

Expressive locks

Owner, Ed25519 public key, M-of-N multisig, time locks, hash locks — composed and extensible through the OutputLock port.

05

Events & logging as decorators

PSR-14 event dispatching and PSR-3 logging wrap any ledger without touching the core, following the same hexagonal boundaries.

06

Coin selection & analytics

FIFO, largest- and smallest-first selection strategies, plus UTXO analytics for dust, consolidation and per-owner summaries.

In practiceexample.php
example.php
use Chemaclass\Unspent\Ledger;

$ledger = Ledger::inMemory();

$ledger->credit('alice', 100)          // mint 100 to alice
       ->transfer('alice', 'bob', 30); // alice → bob

$ledger->totalUnspentByOwner('bob');   // 30
$ledger->totalUnspentByOwner('alice'); // 70  (change output)
Performanceo( · )

Correctness comes free from the model; the engineering keeps the everyday operations linear and cache-friendly — proven by a phpbench suite, not asserted.

O(1) amortized apply

History mutates in place — N transactions cost O(N), not O(N²).

Owner index

Transfers and per-owner balances are O(outputs-owned), independent of total ledger size.

Copy-on-fork set

Mutations happen in place; reads return a copy-on-write snapshot that never poisons the next write.

Store-backed mode

The SQLite adapter batches writes behind covering indexes and keeps memory bounded to the unspent set — scaling past RAM.

Held to a barci · main
665
tests
90%+
mutation MSI
L8
phpstan · 0 err
0
runtime deps