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
  1. What to call
  2. A contiguous calendar
  3. Sealing
  4. What a seal commits to
  5. The watermark
  6. Why
  7. Limits

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.

CodeRefused because
121the period ends before it starts
425the id is already defined with other dates
426the period does not start the day after the last one ends
300an entry's date lies in no defined period
301an 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 everywhere

What a seal commits to

The seal is a log record (kind 0x08), provable like any entry.

FieldMeaning
period, start, endthe sealed period
throughthe position of the last record the seal covers
accountsthe registered accounts at through: the size of every balance tree
treesper (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.