Periods and seals
A contiguous calendar of periods, seals that commit every balance through a period's end, and the watermark below which nothing changes.
On this page
What to call
define_period(id, start, end) and define_months(year, months) build the
calendar; seal_period(id) and seal_through(id) seal it, returning a
SealReceipt with the
Seal record;
prove_balance proves one sealed balance (Proofs).
A contiguous calendar
A ledger books only into defined periods, each a record with an id, a first
and a last day. The first may start on any day; every later one starts the
day after the last one ends — no gaps, no overlaps. Periods may have any
length. Defining a period again identically writes nothing;
define_months(2026, 1..=12) defines 2026-01 … 2026-12 and skips months
already defined.
| Code | Refused because |
|---|---|
| 121 | the period ends before it starts |
| 425 | the id is already defined with other dates |
| 426 | the period does not start the day after the last one ends |
| 300 | an entry's date lies in no defined period |
| 301 | an entry's date lies in a sealed period |
use doubleentry::prelude::*;
let mut books = Journal::new(LedgerId::new("example.com/acme")?);
books.define_months(2026, 1..=3)?;
books.define_months(2026, 1..=3)?; // already defined: writes nothing
let gap = books.define_period("2026-Q2", date!("2026-04-02"), date!("2026-06-30"));
assert_eq!(gap.unwrap_err().code(), Some(426));
books.define_period("2026-Q2", date!("2026-04-01"), date!("2026-06-30"))?;Sealing
A period is open until a seal record for it exists, and sealed from then
on, permanently. Seals follow the calendar: seal_period(id) is refused while
an earlier period is open (427); seal_through(id) seals every open period up
to id, earliest first. Each seal is its own write, so a failed
seal_through keeps the seals before it and a second call continues. Sealing a
sealed period returns its receipt and writes nothing.
use doubleentry::prelude::*;
let mut books = Journal::new(LedgerId::new("example.com/acme")?);
books.declare_currency(EUR, 2, date!("2026-01-01"))?;
books.open("Assets:Cash", AccountKind::Asset, date!("2026-01-01"))?;
books.open("Income:Sales", AccountKind::Income, date!("2026-01-01"))?;
books.define_months(2026, 1..=12)?;
let sale = books.record(
Entry::new("sale-1", date!("2026-01-10"))
.debit("Assets:Cash", amount!("1190.00"), EUR)
.credit("Income:Sales", amount!("1190.00"), EUR),
)?;
assert_eq!(books.seal_period("2026-03").unwrap_err().code(), Some(427));
let receipts = books.seal_through("2026-03")?;
let sealed: Vec<String> = receipts.iter().map(|r| r.period.to_string()).collect();
assert_eq!(sealed, ["2026-01", "2026-02", "2026-03"]);
let january = &receipts[0].seal;
assert_eq!((january.parts().through, january.parts().accounts), (sale.position, 2));
let eur = january.tree(EUR, Layer::Settled).ok_or("no EUR tree")?;
assert_eq!(eur.totals.debit(), amount!("1190.00"));
assert!(january.tree(EUR, Layer::Pending).is_none()); // no holds: zero everywhereWhat a seal commits to
The seal is a log record (kind 0x08), provable like any entry.
| Field | Meaning |
|---|---|
| period, start, end | the sealed period |
through | the position of the last record the seal covers |
accounts | the registered accounts at through: the size of every balance tree |
trees | per (currency, layer) with postings: a balance-tree root and its totals |
Leaf i of a balance tree is account handle i's cumulative gross balance through the period's end — debit, credit, posting count — or the zero leaf. Postings in later periods are not counted. Indexing by handle binds every row to its account: no account has two rows, and "no balance" is provable. A (currency, layer) without a tree is zero for every account. Totals are checked sums of the leaves and never decrease from one seal to the next.
From a seal, prove_balance(account, currency, layer, period) returns the leaf,
the seal's inclusion in the log and the inclusion of the account's register
record; a third party verifies it against a tree head alone.
The watermark
The watermark is the last day of the latest sealed period. Entries dated on or before it are refused (301), master data taking effect on or before it is refused (309), and a policy must also take effect after the latest booking date (431). A sealed period's figures are never recomputed under other master data. Corrections go into a later open period (Corrections).
use doubleentry::prelude::*;
let mut books = Journal::new(LedgerId::new("example.com/acme")?);
books.declare_currency(EUR, 2, date!("2026-01-01"))?;
books.open("Liabilities:Wallet", AccountKind::Liability, date!("2026-01-01"))?;
books.define_months(2026, 3..=4)?;
books.seal_period("2026-03")?;
let back = books.set_limit("Liabilities:Wallet", BalanceLimit::NoDebitBalance, date!("2026-03-31"));
assert_eq!(back.unwrap_err().code(), Some(309));
books.set_limit("Liabilities:Wallet", BalanceLimit::NoDebitBalance, date!("2026-04-01"))?;Why
The calendar is a fold of period records, so it cannot have gaps; "sealed" means a seal record exists, so the state cannot exist without the record. Seals in calendar order make the watermark one date, checked in one comparison.
Limits
- A seal is a record, not a signature: third parties trust it through the signed, witnessed tree head that includes it (Transparency).
- There is no unseal.
- Sealing books no year-end close; that is
close_year, recorded as entries before the seal.