feat(ledger): double-entry, where a stored balance cannot exist - #21
Merged
Conversation
Phase 7A. 06 calls ledger-first "the single most consequential engineering decision in the system, and the one most likely to be compromised for convenience", so the shortcuts it warns about are made unavailable rather than discouraged. There is no balance column, so a balance cannot be stored. There is no method that writes a single entry, so nothing can post one side of a movement. There is no update and no delete, so history cannot be repaired in place. What remains is post, which writes a balanced transaction or writes nothing at all. Law XXI is the one worth reading the code for. 06: "an implementation that can produce a negative balance can mint currency." So every account a movement touches is checked after the entries are written and inside the same database transaction, with those accounts locked first. Without the lock two concurrent debits can each read a sufficient balance, each pass, and together overdraw, which is exactly the path that mints. There is a test that runs both at once. Amounts are bigint end to end, summed in Postgres and read back as text before being widened. A SUM over bigint that arrived as a JavaScript number would be the floating point error 06 forbids, entering through the one place nobody looks. A value past MAX_SAFE_INTEGER round-trips exactly, and there is a test for that too. Three things the domain enforced that this did not know about until the tests ran: an entry must carry its transaction's idempotency key, an entry must carry its transaction's type, and Kredbits cannot hold a negative value at all. The last one is why the overdraft check reads an unbranded bigint: the type cannot express the thing being detected, and branding first would raise a range error instead of reporting the overdraft. A finding rather than a gap: post cannot create the opening supply, because a transaction must sum to zero and creating money is precisely the movement that does not. Genesis belongs to the Central Bank, which is Phase 8. The tests seed an opening balance directly and say so. Verified against a real Postgres, including by mutation: removing the Law XXI check fails three tests, one of them the concurrent overdraft.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 7A. 06 calls ledger-first "the single most consequential engineering decision in the system, and the one most likely to be compromised for convenience", so the shortcuts it warns about are made unavailable rather than discouraged.
What remains is
post, which writes a balanced transaction or writes nothing.Law XXI is the part to read closely
Every account a movement touches is checked after the entries are written and inside the same database transaction, with those accounts
SELECT ... FOR UPDATElocked first.Without the lock, two concurrent debits can each read a sufficient balance, each pass the check, and together overdraw. There is a test that fires both at once and asserts exactly one survives.
Precision
bigintend to end. Summed in Postgres, cast totext, widened toBigInt. ASUMoverbigintarriving as a JavaScript number would be the floating point error 06 forbids, entering through the one place nobody looks.A value past
MAX_SAFE_INTEGERround-trips exactly, and twenty small movements conserve to the subunit.Three things the domain caught that I had wrong
The tests found all three, which is the domain validator from Phase 0 doing its job:
Kredbitscannot hold a negative value. That is why the overdraft check reads an unbrandedbigint: the type cannot express the thing being detected, and branding first raises a range error instead of reporting the overdraft.A finding, not a gap
postcannot create the opening supply. A transaction must sum to zero, and creating money is precisely the movement that does not. Genesis belongs to the Central Bank, which is Phase 8. The tests seed an opening balance directly and say so in a comment.Verification
Against a real Postgres, stable across three runs. Verified by mutation: removing the Law XXI check fails three tests, including the concurrent overdraft.
374 tests. Format, lint, typecheck and build clean.
Not in this PR
Phase 7B is the Official global ledger and lives in
kreds-network. The roadmap is explicit that it must implement its own accounting domain rather than importing this AGPL package: same principles, separate boundary.