Yamale docs ← back to the site

What is left

Checked against the tree and the running chain rather than against a task list — a task here was once marked complete while its artefact did not exist, so the list is not evidence.

Chain state last verified 2026-09-01, against yamale-devnet-2 at block 141,500 — queried over https://yamale.tail4355e8.ts.net/api/rpc/, and every figure below that reads as chain state was read from that node on that day.

Repository state last verified 2026-09-03, which is a weaker claim and is labelled separately on purpose: it means the code exists and its tests pass, not that anything has run on the network. The account-service rows added that day are all of this kind.

The hostname in that sentence is itself a correction. Every previous verification queried pay.yamalelegal.com, which is the VM — and the VM is not the host the public reaches. The funnel terminates on the Pi. So a document written to stop claims being made about the tree instead of the chain was, for the client half of its rows, checking the wrong machine. See deploy/README.md.

Three states, not one. This document once headed a single table "built, merged, and live on the chain", and that phrasing hid a real gap for two days: rows that were merged sat beside rows that were running, and a row reading "land registry - module, CLI, client" concealed that x/tokenisation had no CLI at all and the land client could send two of twelve messages. So the states are now separated, and a row may only move down the page when somebody has checked the running chain rather than the tree.


Live on the chain, and verified there

Constitutional layer 13 invariants fixed at genesis; MsgUpdateParams refuses them
Concentration caps beneficial-ownership registry, epoch enforcement, demotion by jailing
Enforcement oversight legal instrument, delays scaling with amount, ombudsman veto, rolling window cap
Foundation 3-of-5 x/group, in genesis, as recovery_destination
Key ceremony air-gapped CLI, loopback page, and hosted multi-device with in-browser keygen
Payment confidentiality — metadata salted hash on-chain, payload off-chain
Payment confidentiality — schema fields 10–13 reserved; commitment refused until verification ships
Encrypted payload store X25519 + ChaCha20-Poly1305, three recipients, erasure demonstrated
Jurisdictional registry country on the account, country prefix in the x/alias identifier
Build profiles settlement compiles out the token and five modules; IBC opt-in
Fees in issued currency ante gate on denom, swept to a treasury operating account
Validator key rotation planned rotation, recovery quorum, veto-by-signing
Land registry - the module 12 messages, four-party transfer, 31 tests; x/tokenisation refuses unauthorised fractionalisation
Tiered netting x/netting: collateral posted first, hold-and-retry, no recompute path
Foundation console /foundation/ — the 3-of-5 has an interface, with the limits below
Roles and the perimeter x/alias role grants, and AssertScope consulted by four modules
Coordinated upgrade proposed, voted, halted at height, binaries swapped, applied — on the live chain
Signing-request decoding the wallet reads a TxBody and says what it does, instead of naming a type URL
Visual system clients/shared/yamale.css — real typefaces, a scale, elevation, semantic colour
All five roles confer something roles-that-do-something applied at 95,400; two chain-wide ROLE_FOUNDATION_ADMINISTRATOR grants exist, made at 95,758, both pinned (3-of-5 and 3-of-4)
Threshold consumer accounts mpc — 2-of-3 ECDSA. Two payments from one such account at 118,885 and 118,968, the second after a reshare, under a byte-identical public key
The custodian service tools/custodian — holds one share, authenticates, co-signs, refuses. Not deployed against anybody's money
x/tokenisation pays its shareholders income-that-arrives applied at 119,900; a vehicle minted after it owes 18 YML to its majority holder where the one minted before owes nobody anything against a 72 YML vault
Four more client surfaces /keys/, /markets/, /oversight/, /demo/ — all answering 200 on 2026-08-31
The oracle agrees a price first ever at 141,264 on 2026-09-01: 48 denominations, both validators reporting, voting_power_bps 10,000. Two feeders under systemd on two different sources
Structuring and stuck slices structuring-and-stuck-slices applied at 132,700, identical app hashes on both hosts. Both new parameters default to zero, so nothing changes until governance sets them

Merged, but not yet running

The distinction this document previously lost. Everything here is in main and has never been exercised against the network the validators are running, so a query against the live chain returns nothing for any of it.

Country enrolment ceremony country - offices, grants, jurisdictions, approved by the foundation tool complete, never run: RoleHolders is empty for every country asked
Enrolment for a threshold account tools/custodian --import takes a share file an operator produced with tools/mpc keygen there is no path by which a member of the public gets an account
Distributed key generation mpc.KeygenParty — one participant per process, share computed locally and never transmitted closed 2026-09-03. A test captures every byte the three parties exchange and asserts no party's secret scalar appears in it
Enrolment over HTTP tools/custodian /v1/enrol/*, driven by the device against two deployments (--role custodian and --role recovery) built and tested; never run against a real browser or a deployed pair
The recovery process of Part 5 two approvers from different teams, 72-hour delay, notice that aborts on failure, 24-hour outbound freeze enforced on every signature, recorded proof, aggregate publication built and tested; never exercised by a real operator

Three rows left this table on 2026-08-31, and one of them had been wrong for four days. roles-that-do-something was applied at height 95,400 — all five roles confer something, the retired parameter lists are grants, and required_shape is checked on every authority action. Administrator appointment was listed as "tool complete, never run" and had in fact been run: ChainWideGrants returns two ROLE_FOUNDATION_ADMINISTRATOR grants made at height 95,758, both to x/group accounts and both pinned, at 3-of-5 and 3-of-4. x/tokenisation's CLI rides the same binary and is therefore live too.

That is the same failure this document was restructured to prevent, in the opposite direction: a row stayed pessimistic because nobody re-read the chain after the upgrade landed. Reading the chain has to happen in both directions.

How this was found, and it is the reason for the split. On 2026-08-27 the running binary on both hosts was four days old. query alias chain-wide-grants returned {}, so no account held ROLE_FOUNDATION_ADMINISTRATOR - not because a grant was missing but because the state machine had no such role. The symptom surfaced two modules away: an account created in the payments app could send money and could never be addressed, because MsgRegisterAlias needs a recorded jurisdiction and only an approved participant or a foundation administrator can record one. A binary's build date is now part of verifying this document.

The account model - the key is built, the service around it is not

Separated out because it keeps being asked about. This section previously read "decided, specified, and the largest unbuilt thing", and the first half of that stopped being true on 2026-08-31.

Decided 2026-08-20: threshold key custody, built in house rather than bought. The design is complete in accounts.md: the key is split, the server holds one share and the device the other, and neither can sign alone - so "the operator cannot move your money" is a statement about mathematics rather than about policy. Operator custody was chosen first and reversed, because on a state-operated system "the authority can spend any citizen's balance" is a very different political object from "the authority runs the payment rails". Recovery is specified to the same depth: two approvers from different teams, a 72-hour delay with notice to email and every enrolled device, outbound payments frozen for 24 hours afterwards, proof that is not public knowledge, and recoveries published in aggregate so an unusual rate is visible without exposing who.

The share protocol is built, and it has signed on this chain. mpc is 2-of-3 threshold ECDSA over secp256k1 on tss-lib: three shares named device, custodian and recovery, any two of which sign, none of which signs alone. It is documented in mpc.md. tools/custodian holds the second share and decides whether to co-sign — custodian.md. mpc/wasm is the device's half compiled for the browser, and clients/keys runs the protocol in front of an audience at /keys/.

What was verified on the chain, and it is the claim the design was chosen for:

Height Transaction Memo
Before the reset 118,885 A8F18CAB…B15C3C threshold signed: device + custodian
After it 118,968 6C784D06…4AFCFE after a password reset: new shares, same address

Both code 0, both MsgSend from yml1ael7jxwlvacc3daawzc2kpd6lst6w8nmml6a97, sequences 0 and 1 — and both carrying the byte-identical compressed public key 0320ddc3…3c34, which derives to that address. The second was signed by shares that did not exist when the first was signed. A Cosmos multisig could not have done that: rotating a member changes the address, which retires the account's x/alias identifier and silently breaks every saved payee.

Two honest limits on that. The chain sees an ordinary single-signature account and cannot tell you the signature was produced jointly — that indistinguishability is deliberate, and it is also why the transaction alone is not proof of the arrangement. And the shares in that rehearsal were three files on one machine driven by tools/mpc, which is precisely the arrangement the design exists to avoid; the CLI's own header says so.

Most of the service now exists, built 2026-09-03. Enrolment over HTTP, distributed key generation with no process ever holding two shares, a pre-parameter pool, and the whole of Part 5 — two approvers from different teams, the 72-hour delay, notice that aborts the recovery if it fails, the 24-hour outbound freeze enforced on every signature, a recorded standard of proof, and publication in aggregate. Thirty-three tests in tools/custodian, five more in mpc.

Enrolment runs mpc.KeygenParty in three places at once: the device in the browser, and two independent deployments of the same binary as custodian and recovery, with separate sealing keys and separate directories. The store is constructed for exactly one role and refuses to write or hand back any other, so "no single service can sign" is enforced rather than configured.

What is still missing is smaller but not small. No identity check at enrolment — the service can refuse a duplicate email and verify that everybody generated the same key, and it cannot verify who anybody is; that belongs to enrolment policy and is the first thing a deployment must answer. No second factor. No notice to enrolled devices, only to one email, and device enrolment does not exist. No rate limit or lockout on the password check. And the reshare after a recovery is deliberately left to the customer's device, which means a recovery is not finished when the endpoint says completed.

None of it has run outside a test. No browser has driven an enrolment, no operator has approved a recovery, and the two deployments have never been stood up as two deployments. That is the next thing, and it is the same distinction this document exists for: built is not running.

And the consumer app still ships the model the design rejected. clients/app holds a CosmJS password-wrapped key in localStorage. clients/app/src/account.ts says so in its own header and calls itself not acceptable in production, which is the right way to carry a proof of concept — but it means a forgotten password is a lost account today, and the threshold work above is not wired into any interface a person could use. That wiring is now the gap: the service it would talk to exists, and nothing talks to it.

Two divergences worth recording rather than leaving only in the code:

Designed, documented, not built

Browser signing for the foundation. The console at /foundation/ reads the chain and composes commands a custodian runs; it still cannot sign.

Its stated prerequisite has since shipped. The wallet no longer stops at a message's type URL — clients/sdk/src/signrequest.ts decodes a TxBody through the generated encoders and describes what it does, following nested Any payloads so a proposal describes the act rather than the envelope, and refusing to describe a type it does not recognise. So the argument against in-browser signing is now weaker than the argument for it, and the three actions a custodian signs personally — MsgVote, MsgExec, MsgSubmitProposal — are the ones to revisit first.

What has not changed is why it matters: on the account that receives every seizure, a page that said "pay 5,000 to Amara" while the wallet said MsgSubmitProposal would be worse than a command line.

Confidential amounts. Deliberately deferred, with the measurements recorded in confidentiality.md: no audited Go library, ~6 transactions per second with the only credible Bulletproofs implementation (which is itself broken on three of five curves), ~130 with gnark. The reserved field numbers are the part that mattered and they are done.

The seat token for a no-native-token profile. -tags settlement removes the token's issuance, not its denomination: sdk.DefaultBondDenom is still uyml, and staking, gov deposits, slashing and the oracle's accepted denoms all follow it. Equal seats needs a non-inflating, non-transferable seat token with equal genesis balances, delegation blocked and slash fractions at zero.

Untouched

Scope §6, workstream three. Batch payment messages with per-item failure isolation, mempool lanes prioritising settlement, a published state-growth model with pruning and a tested restore, an institution registry carrying LEI or BIC, oracle hardening with deviation circuit breakers.

Scope §6, workstream four — assurance, which gates everything commercial. Two independent audits with non-overlapping scopes, property-based and fuzz testing on every value-moving path, written invariant specifications produced before audit so reviewers have something to check against, a key ceremony with HSM custody, a documented upgrade rollback.

The account service has its own section above and is mostly no longer untouched: enrolment, distributed key generation and the recovery workflow were built on 2026-09-03. What remains untouched around it is second-factor enrolment, rate limiting or lockout on the custodian's password check, an identity check at enrolment, and any client that speaks to it.

Plus USSD and feature-phone access, without which most African transaction volume is unreachable, and agent-network and mobile-money integration. Those are still entirely absent and are the larger commercial gap now.

A legal entity able to sign an indemnity. A LICENSE now exists — proprietary, with reading, compiling, running on a test network and publishing security findings explicitly permitted, and production or distribution requiring a written agreement. That removes the "no licence at all" blocker that stopped a counterparty's legal team from opening the repository, and it is not the same thing as being able to contract. The file itself flags what is unsettled: the registered entity, named "Sermium" on the basis of the repository's own naming, and the governing law, which it deliberately does not assert. It was drafted for clarity rather than by a lawyer and has not been reviewed by one.

Open decisions

Answered 2026-08-20. Threshold key custody is built in house. The product is a vendor with support obligations, not a reference implementation — which makes the §6 assurance workstream, the LTS branches, the backport policy and a legal entity able to sign a warranty into owned scope rather than somebody else's problem. Cross-chain collateral is not a requirement, so IBC stays compiled out and outside audit scope.

Still open, and the one with the shortest fuse:

  1. Which beachhead — UEMOA, or an Afreximbank partnership. The two imply different first roadmaps. The recommendation on file is Afreximbank first: a vendor sells to a buyer, and one counterparty answers faster than eight governments whether anybody buys this at all.

  2. What happens to an open netting window when netting is switched off. Setting cycle_blocks = 0 returns before closing anything, so the open window never settles, held slices stop being retried, and every participant in it has an exposure with no settlement date until a second proposal passes. The choice is between refusing the proposal and closing the open window immediately; both are defensible and it is a settlement-policy call, not a coding one. Documented in params.proto and settlement.md, deliberately not decided.

Answered 2026-08-23. Every role is granted by the foundation, and chain-wide * scope remains governance's alone. A country authority may not grant roles inside its own country. That is what the code already enforces, so nothing changed — but it was a live question and the reasoning is worth keeping: delegating national appointment to a national office means one compromised office yields every power in that country, because the payments authority could appoint the enforcement authority that freezes accounts. The cost accepted in exchange is that three custodians sign every national appointment, which is a bottleneck and a rubber-stamping risk. Revisit by giving offices the power to propose rather than to grant, if the bottleneck turns out to be drafting rather than approval.

Answered 2026-08-25. The retired emergency authority is carried across chain-wide, and that widens it. emergency_authority was one address with no territorial limit, so the roles-that-do-something upgrade grants the address it held ROLE_ENFORCEMENT_AUTHORITY at *. Granting it a country instead would have been the upgrade choosing to narrow an authority nobody voted to narrow, and choosing which country on top of it; granting nothing would have removed the emergency path from a running chain with no one noticing until it was needed.

What the chain-wide grant adds, stated because collapsing two mechanisms into one always adds something: that account can now open an ordinary case as well, including a seizure accusation, which emergency_authority could not do. It still cannot decide one — a seizure needs two thirds of bonded voting power, and this account has no vote unless it is also a validator. The first thing a deployment should do after the upgrade is revoke the chain-wide grant and issue country ones, which is one foundation proposal per country.

  1. Whether a bonded validator should still be able to freeze anything. AssertScope gates x/enforcement's OpenCase, which changes the module's central property from any bonded validator can freeze to any bonded validator governance has placed in that country's perimeter. That is what roles-and-perimeter.md asks for and it is the point of having a perimeter — but it is a real narrowing of who can act in an emergency, and the narrower alternative was to scope only EmergencyFreeze and leave ordinary validators chain-wide. Worth confirming deliberately rather than discovering.

    Still open, and less pressing than it was. OpenCase now also accepts a holder of ROLE_ENFORCEMENT_AUTHORITY, so a country that has enrolled an enforcement office is no longer relying on a validator being both awake and granted. The question the decision turns on is unchanged: who can stop money in a country that has NOT enrolled one.

  2. Whether the count of chain-wide * grants belongs in the constitution. A validator set wanting to act outside its perimeter would grant itself *, and today an ordinary governance vote is all that stands in the way — which is exactly the test for constitutional-ness. The argument against is that it is a count rather than a threshold, and x/alias has no EndBlocker, so it would be checked in GrantRole and InitGenesis the way the foundation-administrator cap already is. Not added, because the invariant set is customer-visible.

Answered 2026-08-23. An office's shape is pinned on its grant and checked on every action, not stamped once at grant time. assertGroupAccount refused a role holder that was not an x/group policy and asked nothing about the arrangement inside, so a one-of-one satisfied it and a proper three-of-five could vote itself down to one afterwards with nothing notified. RoleGrant now carries an optional required_shape and both perimeter functions refuse an office that has fallen below it. Two decisions inside that are worth recording because they could reasonably have gone the other way:

  1. Whether a netting reserve is seizable. x/enforcement seizes SpendableCoins from a bank account; the uncommitted part of a posted reserve is plainly the participant's own money and sits in the netting module account, out of reach. A freeze blocks posting and withdrawing, so value cannot escape — but a seizure cannot reach it either.

Known defects

Closed in the tree is not closed on the chain. The five x/tokenisation defects struck through below were fixed on 2026-08-27 and were still being executed in their broken form by the validators for four more days. They became live with the income-that-arrives upgrade at height 119,900, verified against AppliedPlan. A row here now says both dates when they differ.

Operational loose ends

No fault tolerance, and the arithmetic that fixes it

A chain survives losing a validator only if the ones left hold more than two thirds — so every validator must hold less than a third, and no set of two can manage it. With 100,000 against 10,000 the majority node is a single point of failure; with 55,000 each, either node's outage halts the chain. Two validators can be arranged to tolerate the joining node's outages, which is what launch.md recommends and is right for a rehearsal, but they cannot be arranged to tolerate both.

There is a second cost, and it is the one that bites first. A validator above two thirds commits without waiting for anybody, so the minority node is structurally unable to get its votes counted and is jailed for downtime while being entirely healthy. A lopsided pair does not buy one strong validator and one weak one; it buys one validator and one node being punished.

Four equal validators is the minimum that tolerates one loss: each holds 25%, any three hold 75%. That is also exactly what the equal-seats decision produces, so this is an argument for seating four rather than for weighting three.

The test that had never run, and has now

Three of five custodian keys moving something, and two failing to.

Exercised repeatedly, on local chains built by the real ceremony group tool:

What remains is doing it on yamale-devnet-2, with its seven-day voting period, through the console at /foundation/. That is a test of the process rather than of the mechanism: five people in five countries, a deadline a week out, and an interface instead of a command line.

The measurement that keeps being worth repeating

Every claim in this document that reads as a fact was measured. The ones that cost the most to learn, in the order they bit:

  1. Broadcast code: 0 means accepted, not executed. Five separate bugs. The fifth was subtler than the rest: an office acts through its own group, so an x/group proposal that fails in execution still produces a transaction with code 0, and the refusal is inside EventExec's logs.
  2. proto3 cannot distinguish 0 from unset. Four separate bugs. OfficeShape is a nullable message rather than two integers for exactly this reason.
  3. A policy address derives from the group's sequence number alone. A live ceremony predicted an office address and got the foundation's own, because both were policy sequence 1. Never predict one; read it back.
  4. x/group counts weight, not heads. A threshold of 3 over member weights 3,1,1,1,1 is a 1-of-5. Any interface displaying an office's shape must compute the fewest members who can reach the threshold, or it will show 3-of-5 for what is actually a single key.