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:
The blind index in
clients/appwas not one. Fixed 2026-09-01. It was a bareSHA-256of the email, which can be tested against any word list, so a dump yielded exactly the membership the hash was meant to hide. There is no server here to hold a pepper, so the key is now the user's own password through PBKDF2, salted by the email: a dump without it yields nothing testable. The cost is stated rather than hidden — an account can no longer be found by email alone, so a wrong password and an unknown email are now the same outcome.The larger hole was beside it and is the part worth remembering: the plain email was stored under a fixed key to remember who signed in last, so one dump answered "who uses this" without touching the index at all. The index was decorative however it was computed. That record is gone; a masked hint was considered and rejected, because on a national payments system the domain is most of the answer.
crypto.subtleis undefined outside a secure context, so on the plain-HTTP tailnet host account creation throws and reaches the user as "That password is not right."
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:
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.
What happens to an open netting window when netting is switched off. Setting
cycle_blocks = 0returns 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 inparams.protoand 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.
Whether a bonded validator should still be able to freeze anything.
AssertScopegatesx/enforcement'sOpenCase, 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 onlyEmergencyFreezeand leave ordinary validators chain-wide. Worth confirming deliberately rather than discovering.Still open, and less pressing than it was.
OpenCasenow also accepts a holder ofROLE_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.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, andx/aliashas noEndBlocker, so it would be checked inGrantRoleandInitGenesisthe 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:
- Absent means no requirement, so grants made before the field existed are unchanged in effect and their holders can still shrink to a single key. The alternative — treating absence as "must be at least something" — would disable every existing authority on the upgrade block. Closing it is one foundation proposal per grant, and the runbook says how.
- A missing
x/groupkeeper refuses, unlikeassertGroupAccount, which skips. The asymmetry is which way the bypass runs: there a missing keeper can only produce a grant the perimeter will refuse to act on, here it would produce an action by an office whose shape nobody read.
- Whether a netting reserve is seizable.
x/enforcementseizesSpendableCoinsfrom 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.
No shareholder in x/tokenisation could ever be paid anything.Closed 2026-08-27, live at 119,900.SendRestrictionFnsettles both sides of a share transfer, which the income index requires — a holder earns the movement in the index across the period they held, so a position has to be settled the moment a balance changes. It was written, commented, and registered nowhere:app.goappended only the enforcement restriction, and a repository-wide search found the function referenced by nothing at all, not even a test. So no transfer settled, no position was created by one, and every entitlement read zero. Found against a live vehicle holding 72 YML against 1,000,000 shares which the chain's own query said was owed to nobody. Second dead load-bearing function in this module afterFinaliseSale, and both were found the same way: by driving the thing rather than reading it.mpc.Signpanics when handed shares from two different sharings. Found 2026-08-31 while verifying the reshare claim by running the tool. An account was generated, resharded fromcustodian + recoverywithout the device share — the password-reset path — and the resulting address was identical, which is the property working as designed. Signing with the old device share and the new custodian share then crashes the process:panic: BuildLocalSaveDataSubset: unable to find a signer party in the local save data … tss-lib/ecdsa/keygen/save_data.go:92 … mpc.Sign at mpc/mpc.go:296The refusal is correct — shares from two sharings must not combine — but a panic is the wrong way to deliver it, and the input is not exotic: a stale device share is exactly what a customer's phone holds after a reset it did not finish.
Signvalidates the share count and the digest length and then hands the set to tss-lib without checking that every share agrees on the sameKs, which is the check that would turn this into an error somebody can read.Scope, stated because it was measured and not reasoned: this is
Sign, the all-in-one-process path used bytools/mpcand the tests.NewSigningParty— the production path, and the onetools/custodianuses — builds its committee from its own share'sKs, so it does not reach this construction with a mixed set and cannot panic here. What it does instead when its peer is on a different sharing was not tested, and should be: the plausible answers are a protocol that hangs and a signature that verifies against nothing, and neither is a good thing for a service to do on input a stranger controls.x/tokenisation credits a sale's proceeds that never arrive.
FinaliseSaleputs the whole reported price through the income index while moving no coins, and the only message that does move coins —FundVault— accrues them a second time on the way in. So the proceeds of a sale have no funding path that does not double-count, every holder is credited money the vault does not hold, and redemption fails withinsufficient fundsfor everybody after the first. Found 2026-08-27 while writing the pipeline's first test. Not fixed, because the fix is a decision rather than a mechanism: either finalising pulls the price from the reporter — which makes a reported price binding, and closes the report-low-and-keep-the-difference attack a second way — or funding stops accruing once a sale is reported and the proceeds are simply the lastFundVault. The first is stronger and assumes the sponsor holds the money on chain; the second is weaker and assumes nothing.TestAVehicleCanBeExitedasserts the broken behaviour deliberately, so whoever decides this will see it fail and have to look.A sender is not obliged to seal a payload to the readers the chain names.
ROLE_SUPERVISORnow confers an entitlement — a holder covering a country is a viewing-key recipient for every payload settling there, published byQuery/PayloadReaders— but the envelope is built off chain and the chain holds only a hash of the plaintext. A sender that ignores the published set produces a payload the supervisor can never open, and nothing on chain detects it. That is a limit of where the ciphertext lives rather than a gap in the registry, and closing it would mean putting the recipient key ids on the payment record and checking them, which is a design decision nobody has taken.A regulator appointed before the supervisor rule existed keeps the appointment.
MsgAppointRegulatorrefuses an appointee that holds noROLE_SUPERVISORcovering the country, checked on the write and not re-read afterwards. So a country appointed under the old rule, or one whose regulator's grant is later revoked, has a sitting regulator holding no role — visible inrole-holdersandregulatordisagreeing, and fixed by appointing again. Re-reading it on every payment was rejected because an appointment that could evaporate would leave a country's payments sealed to an authority the chain no longer lists, with nothing saying when it stopped.Role grants made before
required_shapeare unpinned, so their holders can still reduce themselves to a single key. Absent means no requirement, by design: the alternative disabled every authority on the upgrade block. On a chain still in development the fix is to re-make the grants; on one holding value it would need a migration somebody decided on deliberately.x/land's own admin path has the same weaknessrequired_shapecloses. It asks whether a registry office is a group, once, and never again.Every foundation administrator carried across by the upgrade is unpinned and may not be a group.
Migrate2to3writes each address from the retired parameter as a chain-wide grant with norequired_shapeand without asking whether the holder is anx/groupaccount, because neither was true of a parameter entry and a migration that applied today's rule would be deleting the one authority that can correct a country rather than carrying it. Both are closed by re-granting deliberately, per grant, and the guide says how. Until then this is the same class of defect as the unpinned grants above.No payment has been pushed through the Pay screen end to end.Closed 2026-08-26: 12.50 XOF at block 84,121, tx336F01BA…, code 0, carryingym1;e2e=YML-20260826-PM1SJCMM;purp=GDDS;rmt=Invoice 4471on the ledger, and the reference decoded back onto the history row.No account created in the app can be paid, and for a while nothing said so.
MsgRegisterAliaslands in a block and fails there —this account has no recorded jurisdiction, codespacealias, code 9 — because the chain has neither an approved participant nor a foundation administrator. The app now says this on the account screen instead of returning null and carrying on. The fix is chain state, not client code.clients/appsendsMsgSend, notMsgSendPayment.x/paymsgrequires both named participants to be governance-approved and the debtor to be a registered customer of the one it names, and this chain has zero approved participants. So the app sends a real transfer with the reference in the memo and says which rails carried it. The ISO message stays unreachable from any interface until a country is enrolled.Re-measured 2026-08-31:
ListApprovedParticipantandListPaymentRecordboth return an empty page, andRoleHoldersis empty for every country asked. So the payment at height 84,121 is not an ISO 20022 payment and must never be described as one. It is a/cosmos.bank.v1beta1.MsgSendof 12,500,000uxofcarryingym1;e2e=YML-20260826-PM1SJCMM;purp=GDDS;rmt=Invoice 4471in the memo — read back from the transaction, not from the release note. The ISO fields travel in a memo string;x/paymsgwas not involved and holds nothing.The OpenAPI merge collapses every module's
Paramsinto one definition, currently holding onlyx/validatorgov's fields.TestGenesisRoundTripsinx/aliasstill has a vacuousOwnerscheck — the same shape as three others that were fixed.x/tokenisationis served under/yamale/and the nginx allowlist names it under neither prefix, so it falls through to deny-by-default. Correct today, a trap when somebody opens it up.Closed 2026-08-27, live at 119,900. It was a keeper method nothing invoked — no message, no EndBlocker, not one test — so no asset reachedFinaliseSalehas no caller.STATUS_REALISEDandRedeem, which requires it, could never succeed for anybody: every fractionalised vehicle was a one-way door.MsgFinaliseSaleis the caller, permissionless because a crank only the sponsor could turn is one the sponsor can decline to turn.Closed 2026-08-27, live at 119,900.AttestSalenever checked who was attesting.ErrNotAttestorwas registered as code 17 and returned from nowhere, andCollectioncarried no register to check against, so a sponsor met any threshold with fresh addresses at the cost of the gas — leaving the guide's own "the sale price is the attack" defended by nothing. Collections now carry an attestor register that governance appoints, not the seller.A holder who never transferred was paid nothing.Closed 2026-08-27, live at 119,900.Fractionalisecreated the vault and no position for the owner, soSettletreated them as a first-time holder on the way out and started them at an index that had already moved. Hidden because any transfer settles both sides, so the ordinary issue-then-distribute path created the position by accident.Closed 2026-08-27, live at 119,900;DisputeSalereturnedErrStillInWindowfor a window that had closed.ErrWindowClosedis code 34.
Operational loose ends
The oracle had never agreed a price, and now has. Found 2026-08-31, closed 2026-09-01. Both validators had missed every window since genesis — 11,771 of 11,771 for
pi. Nothing was broken: nobody had ever nominated a feeder, and nominating one needs the validator's operator key, which on both hosts lives in a password-protectedkeyring-filethat only the operator can open.With one delegation per validator the oracle agreed 48 rates at 141,264, and both validators now report —
voting_power_bps10,000 — from two different sources, because the aggregate is a stake-weighted median and two feeders on one endpoint is one source counted twice.One number is worth keeping:
pialone is 5,717 bps against a 5,000 bps threshold, so a single feeder does produce a price, with no median at all behind it. That is a property of the current split rather than a safeguard.What this closes is narrower than it looks, and the distinction is the one this document exists for: a rate now exists, so anything consuming one can be exercised. Fee conversion, the appointed-valuer path and a stablecoin peg check still have not been.
A validator above two thirds silently excludes every other validator. Found and fixed 2026-08-21.
pi-2signed 5 of 40 blocks, was jailed for downtime and slashed 1%, on a 21ms direct link with clocks four seconds apart and a load average of 0.5 — nothing wrong with it. A validator holding more than two thirds finishes consensus with its own precommit before gossip reaches anybody, so the other node gets the commit before it has the block and cannot vote for what it has not seen. Rebalancing to 64.56% / 35.44% took it from 5 of 40 to 25 of 25 with nothing else changed. The cost is that neither node can now commit alone, so either one's outage stops the chain — which two validators could never avoid. Four is the number that gets both properties. The split has drifted since and reads 57.18% / 42.82% on 2026-08-31 — 100,000 and 74,900 of 174,900 bonded. Neither holds two thirds, which is the property that matters, and the chain was not catching up when read.The public host is the Pi, and nothing checked that. Found 2026-09-01. Consoles were deployed to the VM for a day while the funnel served the Pi's month-old copy; every check passed, because every check asked the VM. Worse, the Pi's REST allow-list had drifted a revision behind and was missing
cosmos/auth/.../accounts/, which every client reads before it can sign — so signing was broken for every public visitor, and it failed as a401with aWWW-Authenticatechallenge, which a browser renders as a login box on an app that has no login.deploy/deploy.shnow verifies against the funnel hostname and exits non-zero when the public site is not what the repo builds.Ten of the fourteen consoles were linked from nowhere. Found 2026-09-01, when the land register was asked about. It was deployed, working and answering 200 — and reachable only by typing the URL, along with the guided tour, markets, vehicles, governance, oversight, foundation, validator and threshold keys. This is the client-side twin of the oracle row above: served is not reachable, the same way merged is not running.
A validator operator passphrase is in
~/.bash_historyon the VM, in the clear, and appears to be a reused personal password. Mode0600, so it is onesudo, one backup or one host compromise away. It unlocks the key for the majority validator. Nothing needs it again — both feeder delegations are made — so shredding the history and rotating the value costs nothing.The ops signing service is still running with two htpasswd files. It was always a devnet crutch; the plan is client-side signing through
@yamale/connect, then delete/api/ops/, both credential files and both copies ofopsd.py, and move the consoles onto the tailnet.The hosted ceremony process is gone, killed by the VM's reboot on 2026-08-21. It had been holding a coordinator token that could still issue invites. Do not restart it between ceremonies.
There were two faucet units and only one could ever work. A faucet needs a funded key, and duplicating it duplicates custody of that key for no benefit. The Pi's copy was inactive and still configured for
yamale-devnet-1, so the public funnel answered 502 while the real faucet answered fine on the other hostname — which reads as "the faucet is broken" rather than as "you are asking the wrong host". The funnel now proxies to the one real faucet over the tailnet and the stale unit is disabled.A deploy used to be invisible. nginx sent no
Cache-Controlat all, so browsers applied heuristic freshness and an old page after a deploy looked identical to a fix that did not work. That is the diagnosis that burns hours, not the wait. Nowno-cachewith ETag revalidation for HTML, CSS and JS, andimmutablefor the content-hashed bundles under/assets/.The Pi's operator key lives on the Pi. Correct while rehearsing; the validator guide says to take it off the node once genesis is done, because a node needs only its consensus key to produce blocks.
The staging chain's custodians are one person. Its 3-of-5 is a 1-of-1 with four extra steps, which is fine for exercising the process and must never be carried into a deployment holding value.
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:
- Three signatures executed. A restitution paid out with the
MsgExecsent by a custodian who had never voted — the property worth having, since the third signature and the person who presses the button need not be the same. And a custodian swap: one out, one in, count still five, atomically. - Two signatures did not.
EventExec NOT_RUN, status still SUBMITTED, balance unchanged. - The constitution refused what it should — a bare removal rejected at
submission, and
MsgLeaveGrouplikewise. - A country was enrolled end to end: the foundation granted an office
PAYMENTS_AUTHORITYin SN on three signatures, the office admitted a bank inside SN and was refused outside it, a non-foundation group granting was refused, and the foundation granting itself*was refused with chain-wide grants still empty. - An office that shrank lost its authority. Granted as 3-of-5, it voted itself to 1-of-5 and the same action was refused; it restored itself on one signature and worked again with no re-grant.
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:
- Broadcast
code: 0means accepted, not executed. Five separate bugs. The fifth was subtler than the rest: an office acts through its own group, so anx/groupproposal that fails in execution still produces a transaction with code 0, and the refusal is insideEventExec's logs. - proto3 cannot distinguish 0 from unset. Four separate bugs.
OfficeShapeis a nullable message rather than two integers for exactly this reason. - 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.
- 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.