Skip to content

CHIP-0062: The Forge — Weighted N-Asset AMM - #217

Open
awizardxch wants to merge 21 commits into
Chia-Network:mainfrom
awizardxch:chip-awizard-weighted-n-asset-amm
Open

awizardxch wants to merge 21 commits into
Chia-Network:mainfrom
awizardxch:chip-awizard-weighted-n-asset-amm

Conversation

@awizardxch

@awizardxch awizardxch commented Sep 11, 2026 •

Copy link
Copy Markdown

Proposes a weighted constant-function market maker in which one singleton holds any number of assets, each in its own reserve coin, at weights fixed at pool creation. Built on CHIP-0050's action layer; authorizations are CHIP-0025 messages whose receiver coin id the pool derives rather than accepts.

Informational / Puzzle. Requires CHIP-0025 and CHIP-0050.

Supporting material — the full specification, architecture, a written CLVM pass over every leaf, security notes, and a comparison against publicly documented AMM failures — is referenced from the public forge-puzzles repository, alongside the implementation it describes.


Note

Low Risk
Documentation-only CHIP and diagram assets; no consensus, node, or wallet code changes in this repository.

Overview
Adds CHIP-0062 (The Forge: Weighted N-Asset AMM), an informational puzzle CHIP that specifies a CHIP-0050 action-layer AMM where one singleton holds N assets at fixed weights, with leaves for swap, add, remove, collect, observe, and DAO fee reduction.

The document is the normative summary of the design: weighted invariant pricing, derived CHIP-0025 receiver coin ids (not solution-supplied), settlement binding by parent and amount, a sorted on-chain registry with reserve-launcher proofs, oracle height crediting, and coexistence of puzzle revisions (V16 / protocol 17 as the described revision). It points to the forge-puzzles reference implementation and external spec docs rather than embedding puzzle code here.

Also adds assets/chip-0062/ Mermaid sources plus a rendered lifecycle.svg (and references to other figures) so the CHIP renders offline. Revision 13 is editorial only: Backwards Compatibility and Additional Assets now consistently name V16 alongside live V15/V14.

Reviewed by Cursor Bugbot for commit 5d24016. Bugbot is set up for automated code reviews on this repo. Configure here.

Proposes a weighted constant-function market maker in which one singleton
holds any number of assets, each in its own reserve coin, at weights fixed
at pool creation. Built on CHIP-0050's action layer; authorizations are
CHIP-0025 messages whose receiver coin id the pool derives rather than
accepts.

Informational / Puzzle. Requires CHIP-0025 and CHIP-0050.

Supporting material — the full specification, architecture, a written CLVM
pass over every leaf, security notes, and a comparison against publicly
documented AMM failures — is referenced from the public forge-puzzles
repository, alongside the implementation it describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread CHIPs/chip-0062.md Resolved
@danieljperry danieljperry changed the title CHIP: The Forge — Weighted N-Asset AMM CHIP-0062: The Forge — Weighted N-Asset AMM Sep 11, 2026
@danieljperry

Copy link
Copy Markdown
Member

This CHIP is now a Draft. Please leave your reviews here, and feel free to discuss it in the #chips channel of our Discord.

@danieljperry

Copy link
Copy Markdown
Member

@awizardxch Thanks for creating this CHIP. I have updated the file name and added myself as a reviewer. Otherwise, I haven't made any changes. You may want to update the formatting to remove the mid-paragraph new line characters, as well as the non-standard U+2014 characters.

@greimela greimela left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Daybreak Blue security review

These findings come from a ChatGPT Daybreak Blue review of PR head 29203bd and Forge implementation f8fbcaa.

Severity Finding
P0 Genesis can authorize more LP tokens than pool state records
P1 Successive singleton generations can backfill oracle time
P1 The final LP position cannot redeem
P1 The protocol-fee units disagree
P1 Reserve receiver derivation trusts solution data

The first two issues were reproduced with spend bundles accepted by consensus validation. The terminal-LP and reserve-substitution cases were tested against the compiled puzzles.

Comment thread CHIPs/chip-0062.md
| A substituted coin satisfying an authorization | Derived receiver ids; CHIP-0025 mode binds sender puzzle and receiver coin at consensus |
| Value extracted by chaining actions in one spend | The invariant holds per action; suites value the actor's closing position at pre-sequence prices |
| A negative or reversed trade amount, reversing the fee | Sign assertions in the leaf, and an invariant unsatisfiable for such inputs |
| LP minted or melted without the pool's consent | The TAIL requires a message from the pool and a CAT parent; the melt inner's only behaviour is to destroy its whole amount |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: Bind genesis issuance to a single LP eve

Impact: A pool creator or keyless router can mint LP tokens that the pool state does not record, wait for users to deposit, then redeem the hidden supply for reserve assets.

Attack path

  1. The TAIL genesis branch accepts the singleton launcher coin announcement instead of a pool message.
  2. That announcement does not commit to the LP eve coin making the assertion.
  3. Multiple independently funded eves can reuse the announcement in one bundle.

Reproduction: A complete registered-pool bundle passed consensus validation. Pool state recorded 5,000,000 LP, while two eves minted 10,000,000 LP in total.

Required change: Bind genesis authorization to exactly one derived LP eve coin ID. Add a regression test that rejects duplicate eves.

Relevant Forge code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in revision 2. The registry's register leaf now announces

sha256(launcher_id + tree_hash([pool_puzzle_hash, 1, [total_lp, eve_coin_id]]))

so the announcement names the eve coin. The TAIL's genesis branch asserts that announcement using its own coin id, taken from the CAT truths rather than from its solution, so a second eve would have to assert an announcement that was never made.

contracts/v12/puzzles/forge_registry_register.rue, contracts/v12/puzzles/forge_lp_cat_tail.rue.

Regression coverage is in two places. _test_v12_genesis.py covers the duplicate eve directly. _test_v11_v12_review_findings.py builds your attack against the previous revision, expects it to be ACCEPTED, then builds it against this one and expects it to be REFUSED -- a finding that cannot be demonstrated against the old build fails the suite as loudly as one this build still allows, so the test cannot quietly stop testing anything.

Comment thread CHIPs/chip-0062.md Outdated
| LP minted or melted without the pool's consent | The TAIL requires a message from the pool and a CAT parent; the melt inner's only behaviour is to destroy its whole amount |
| An LP token used against a pool of a different revision | The TAIL asserts the protocol version |
| Two pools claiming the same market | The sorted registry |
| Oracle manipulation within one spend | The accumulator advances on pre-spend prices, with the height pinned across every action in a bundle |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Prevent cross-generation oracle backfilling

Impact: An attacker can make the oracle report elapsed blocks at a manipulated price even though no block elapsed. A downstream contract that releases assets based on this TWAP can be drained.

Attack path

  1. Spend the pool at declared height H - 31 and perform a swap.
  2. In the same bundle, spend the ephemeral successor at declared height H and observe the new price.
  3. The same-height guard only covers leaves within one action-layer spend, so the cross-generation pair passes.

Reproduction: The compiled bundle passed consensus validation and recorded 31 blocks at the manipulated price at one actual height. Spot moved from 1, while the claimed 32-block TWAP became 3.9004.

Required change: Bind elapsed time to actual coin birth heights across singleton generations. If the puzzle cannot enforce that relationship, remove the oracle from this proposal.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in revision 2, by binding to the coin's birth height rather than removing the oracle. The prologue now takes birth, emits ASSERT_MY_BIRTH_HEIGHT, asserts h >= birth, and accumulates over h - birth instead of over the difference between two solver-chosen declared heights. An ephemeral successor spent in the same bundle has birth equal to that block, so it accumulates nothing, which is what closes your two-generation pair.

contracts/v12/puzzles/forge_action_common.rue::prologue.

Because the claim is about what consensus will accept, the test is too: _test_v12_consensus_timelocks.py runs chia.consensus.check_time_locks -- the function the mempool itself calls -- over the bundles, so a false birth height is judged the way a node judges it rather than the way our own code does.

Comment thread CHIPs/chip-0062.md Outdated
|---|---|
| `swap` | One reserve rises, one falls. Requires a positive input and a positive claimed output, distinct in-range asset indices, and an exact invariant match. The protocol and DAO slices are taken from the output and accrued in state. |
| `add` | Deposits to one or more reserves, minting LP. Requires a positive LP delta, non-negative deposits, and at least one positive deposit. |
| `remove` | Burns LP and pays a proportional share of every reserve. Requires the burn to be positive and strictly less than total LP, so the pool outlives every withdrawal. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Provide a redeemable terminal state

Impact: The final LP holder cannot withdraw their share of the reserves. Another holder can redeem all but the victim's final LP unit and leave that share permanently trapped.

Cause: The rule burn < total_lp forbids burning the remaining LP supply, while the registry accepts any positive supply without requiring a minimum relationship to the reserves.

Reproduction: A compiled pool registered successfully with reserves of 10,000,000 and 20,000,000 and total_lp = 1. Its only LP unit could not be burned.

Required change: Add a terminal close path. Alternatively, define a bounded minimum-liquidity amount, lock it deliberately at genesis, and enforce that invariant during registration.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in revision 2 by the second option you named. MIN_LOCKED_LP is 1,000, burned at genesis; remove asserts burn <= total_lp - MIN_LOCKED_LP; and registration refuses a genesis supply below the floor, so the invariant cannot be skipped by minting a pool under it.

contracts/v12/puzzles/forge_action_common.rue, forge_action_remove.rue, forge_registry_common.rue.

The tests pin both edges: burning everything above the floor is accepted, one unit more is refused, and a pool registering below the floor is refused. The last holder can now take out their whole position, because the locked thousandth was never theirs -- it was burned before anyone deposited.

Comment thread CHIPs/chip-0062.md Outdated

**Configuration** is curried and therefore immutable for the life of a pool:
asset ids in canonical order, the weight of each, the trade fee in basis points,
the protocol fee in parts per million, the LP TAIL hash, and the DAO recipient's

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Use one protocol-fee unit

Impact: A wallet following the CHIP can authorize a fee 100 times larger than intended, diverting the difference to the protocol recipient.

Source Stated unit Denominator
This CHIP Parts per million 1,000,000
Referenced Forge specification and code Basis points 10,000

For example, a value of 5 means 5 ppm under this text but 5 bps in the puzzle.

Required change: Choose one unit, denominator, and maximum. State them normatively and use the same names in the implementation.

Relevant Forge code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed. Basis points throughout. The CHIP now states it normatively in Specification: the protocol fee is protocol_fee_bps, denominator 10,000, at most 100, and the reference implementation uses the same name and the same denominator. Parts per million appears nowhere in the document.

The unit was only ever wrong in the prose; thank you for catching that it would have led a wallet to authorize a hundredfold fee.

Comment thread CHIPs/chip-0062.md Outdated
### Authorization

Every reserve release and every LP mint or melt is a CHIP-0025 `SendMessage` with
mode `SENDER_PUZZLE | RECEIVER_COIN`. The receiver's coin id MUST be derived by

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Commit the canonical reserve parent

Impact: An attacker can replace the expected reserve successor and strand the canonical reserve, breaking the proposal's anti-substitution guarantee. This is not a free drain by itself because the attacker must provide replacement value.

Cause: The reference finalizer accepts reserve_parent_ids from its solution and uses those values in coinid(...). The pool does not independently commit to or authenticate them.

Reproduction: An attacker-created reserve with the same puzzle and amount satisfied the message and became the successor while the canonical reserve remained unspent.

Required change: Commit the reserve parent or full coin ID in pool state, or prove its lineage before deriving the receiver.

Relevant Forge code

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in revision 2 by the first option you named. Reserve parent ids are an eighth field of pool state, written by the finalizer from the coins it has just created, and the finalizer's solution no longer has a field to name them -- so there is nothing left to substitute rather than a check that a substitution must fail.

contracts/v12/puzzles/forge_multi_reserve_finalizer.rue.

_test_v11_v12_review_findings.py accepts your decoy against the previous revision and refuses it against this one with MESSAGE_NOT_SENT_OR_RECEIVED, and separately asserts that the solution has no reserve-parent field at all.

awizardxch and others added 3 commits September 11, 2026 13:16
…mpty actions

Editor's asks: paragraphs are single lines, the 25 em-dashes are spaced
hyphens as in CHIP-0050. Review P1 (greimela): the protocol fee is in basis
points, denominator 10,000, matching the reference implementation -- the
text had said parts per million. Review (cursor-bot): the action layer
refuses an empty action list; the CHIP now says so. Design changes for the
remaining findings follow once the revised puzzles are live on testnet11.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
P0, genesis can authorize more LP than state records: the launcher's
announcement names the eve coin id and the TAIL asserts it with its own.
P1, cross-generation oracle backfill: elapsed time is h minus the pool
coin's consensus-asserted birth height. P1, the final LP position cannot
redeem: MIN_LOCKED_LP at genesis, enforced at registration and in remove.
P1, reserve receiver derivation trusts solution data: reserve parent ids
live in state, written by the finalizer. Security table and a revision
history updated accordingly. The reference implementation's revised
puzzles are described in the linked repository once deployed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…counts

Housekeeping against the Draft review; no design change.

The Additional Assets list named three documents the reference repository no
longer carries. When the shipping revision replaced the retired one, that
revision's specification, architecture and CLVM pass were withdrawn from the
public repository -- deliberately, since publishing working exploit mechanics for
retired code helps an attacker practising against the live one and helps a
reviewer not at all. The list now names the one specification that exists, and
every link in the document has been checked to resolve.

Comments-URI pointed at pull request 192 rather than 217.

The deployment is thirty-three pools rather than twenty, and every action a user
can take -- swap, deposit, withdrawal, multi-hop route and split route -- has now
settled on chain through a wallet-signed Offer handed to the keyless router,
which was not yet true when revision 2 was written.

Also finishes the Editor's two formatting asks, which revision 1 had applied to
the prose but not to everything since added: the remaining mid-paragraph line
breaks are joined, and the two U+2212 MINUS SIGN characters are plain hyphens.
The only non-ASCII character left is the product symbol inside the fenced
invariant formula.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread CHIPs/chip-0062.md Resolved
Comment thread CHIPs/chip-0062.md Resolved
@awizardxch

Copy link
Copy Markdown
Author

@greimela -- thank you for the review, and particularly for reproducing the first two with accepted bundles. That is what made them impossible to argue with.

All five findings are addressed in the reference implementation, which has moved from V11.1 to V12 (protocol 13). Each reply above names the puzzle and the tests. The pattern throughout was to remove the thing that could be substituted rather than to add a check that substitution must fail: the genesis announcement names the eve, elapsed time comes from a consensus-asserted birth height, the reserve parents moved into state and the solution field is gone.

Two notes on process:

  • The previous revision was never on mainnet. Its testnet liquidity was withdrawn and the pools re-created under the revised puzzles, so nothing is running on the code your review exploited. Its sources have been withdrawn from the public repository, which is why the links in your findings now resolve to a retired revision -- the shipping one is under contracts/v12/.
  • Revision 3 is housekeeping only: the Additional Assets list named documents that went with the retired revision, Comments-URI pointed at the wrong PR, and the deployment is now thirty-three pools rather than twenty.

@danieljperry -- both formatting asks are done: mid-paragraph line breaks are gone and there are no U+2014 characters. The only non-ASCII character left is the product symbol inside the fenced invariant formula, which I have kept as notation; happy to spell it out if you would rather it were ASCII.

…nd who holds the locked minimum

Two corrections from an automated review of revision 3. Both are text; neither
changes the design or the reference implementation.

The oracle paragraph described the interval as `h - birth` without stating the
`h >= birth` assertion that keeps it non-negative, and said a successor created
and spent in the same block accumulates nothing. The second part was wrong in a
way worth correcting precisely: such a successor cannot be spent in that block at
all. ASSERT_HEIGHT_ABSOLUTE is checked against the previous transaction block, so
the largest legal `h` is that height, while a coin created in the block being
made is dated one higher, and `h >= birth` then has no solution. Same outcome for
the cross-generation attack, different mechanism, and the difference matters to
anyone implementing from this text: without the assertion the subtraction really
would go negative and take the accumulator with it.

The `remove` row claimed the locked minimum is the creator's and that every later
holder can redeem every unit they hold. The first half was wrong about the
reference implementation and the second only follows from the first being
handled. The floor is on total supply, and LP is a transferable CAT, so if the
minimum were retained it would be a holding that can never be burned and its
eventual holder would be stranded -- the case the rule exists to prevent. The
pool cannot enforce this, because the puzzle sees a total and a burn and never
who holds which unit. So the genesis mint placing the minimum beyond recovery is
now normative, with the reasoning, and the reference implementation's burn
address is named as how it does that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@awizardxch
awizardxch force-pushed the chip-awizard-weighted-n-asset-amm branch from 30f100b to 59f3b25 Compare September 14, 2026 06:23
@trgarrett

Copy link
Copy Markdown

Updated audit report from Opus and Fable.


Forge / CHIP-0062 — V12 Re-Audit & Cross-Validation vs. #217

Target: awizardxch/forge-puzzles, contracts/v12/puzzles/*.rue, commit 26338d57411793c9a1ca9c78fe74ab1ecb03ca17 (2026-09-13 22:20:18 -0700)
Compared against: our prior V11 audit (reports/AUDIT_FORGE_CHIP0062_2026.md, 2026-09-11, 6-agent) and the CNI reviewer's independent PR review, github.com/Chia-Network/chips/pull/217 (greimela + Cursor bot, 2026-09-11 and 2026-09-14).
Method: 6 independent adversarial re-auditors (3 Claude Opus / 3 Claude Fable, paired 1:1 across 3 focus areas) re-verifying every V11 finding against the current code with real chia_rs-executed spend bundles; 1 Sonnet agent extracting and analyzing the actual PR-217 thread; 1 Fable agent cross-validating both bodies of work and closing one gap of its own. Date: 2026-09-14.


Executive summary

V12 should not be characterized as "the CHIP-0062 review findings are closed." Three of PR-217's five human findings are genuinely and solidly fixed, cross-confirmed by two independent review processes using different methodologies. But: the fix claimed for a fourth (terminal-LP / MIN_LOCKED_LP) rests on a burn the puzzle cannot enforce and the reference creation driver does not actually perform; the fix for oracle backfilling closed one manipulation and introduced an equally usable one in its place; and PR-217 never tested the attack surface — cross-leaf configuration and the registry's own admission rules — that produces the single worst result in this whole exercise: a demonstrated, chia_rs-accepted ~99.98% drain of an unregistered pool's reserves.

Of our original V11 findings: C-1 (CRITICAL) and L-1 (LOW) are cleanly closed. M-2 (MEDIUM-HIGH) is closed against its original attack shape but the fix introduces a new HIGH-severity regression (below). M-3, M-4, L-2 (root cause), L-3, L-5, L-6, L-7, I-1 are all still open, mostly untouched because V12 only targeted the five specific shapes PR-217 tested. M-4 is escalated from MEDIUM to HIGH with an executed full-reserve drain. Several new findings were surfaced.


1. Findings that are genuinely closed — cross-confirmed

C-1 → PR Finding 1 (genesis double-mint) — CLOSED

forge_lp_cat_tail.rue:114-125: the genesis-mint announcement now embeds the eve's own CAT2-truths coin id (cat_my_coin_id), not just the amount; forge_registry_register.rue:94-96 asserts the same enriched list. A second, independently-funded eve can no longer replay the launcher's single announcement — the exact V11/V12 double-genesis-mint bundle now rejects with ASSERT_ANNOUNCE_CONSUMED_FAILED.

Confirmed by two unrelated mechanisms, independently:

  • Both genesis-team agents traced cat_my_coin_id to real CAT2 source (cat_v2.clsp, cat_truths.clib) and proved it reads the consensus-verified full coin id — unforgeable, un-substitutable.
  • Both collect-team agents separately proved the new hash construction is safe by construction (not mere circumstance): tree_hash's sha256tree structure hashes every atom independently under a domain-separated prefix, so expected_delta and cat_my_coin_id never share a raw-byte preimage to grind against each other — verified byte-for-byte against Program.get_tree_hash. This is a completely different attack class (Bytes32 grinding) landing on the same conclusion.

M-2 → PR Finding 2 (same-bundle oracle backfill) — CLOSED against its original shape

forge_action_common.rue:274-315 now requires AssertMyBirthHeight{birth} + assert h >= birth, with elapsed measured as h - birth. The chained swap→observe×N→swap-back-in-one-bundle attack now fails consensus with EPHEMERAL_RELATIVE_CONDITION (not merely a mempool policy — confirmed at block-generator level too, so a colluding farmer can't include it either) or IMPOSSIBLE_HEIGHT_ABSOLUTE_CONSTRAINTS. Verified via three independent paths including feeding synthetic coin records into the real chia.consensus.check_time_locks, going beyond what's visible in the PR thread. See §3 — this fix introduces a new, still-open HIGH regression.

L-1 → PR Finding 5 (reserve receiver from solution) — CLOSED

reserve_parents now lives in curried/committed pool state, written by the finalizer itself from the coins it just messaged (forge_multi_reserve_finalizer.rue:110-128, 206-216); the finalizer's solution no longer carries a field for external parent ids. The reviewer's decoy-coin substitution now fails MESSAGE_NOT_SENT_OR_RECEIVED, exactly as the project claimed. Confirmed to remain sound even mid-drain in the M-4 PoC below (the finalizer's unconditional state overwrite is independent of which leaf ran).

PR Finding 4 (fee-unit ppm/bps mismatch) — verified in-puzzle, not just text

forge_action_common.rue:84-86: MAX_PROTOCOL_FEE_BPS = 100, MAX_DAO_FEE_BPS = 100, MAX_FEE_BPS = 200, enforced by valid_config on every spend's first action and again at registration. The author's "at most 100 bps" claim to CNI is real, not just documentation.

PR Finding 6 (empty action list) — verified

upstream/action.rue:119: assert !(selectors_and_proofs is nil); — present in the vendored, hash-pinned CHIP-0050 layer. Confirmed a documentation-only gap, as the author claimed.


2. PR Finding 3/8 (terminal LP / MIN_LOCKED_LP) — the author's rebuttal to CNI is not accurate

This is a genuine gap our own re-audit initially missed and only caught during cross-validation.

PR-217's Cursor bot (round 2, 2026-09-14) flagged that the CHIP text wrongly implied MIN_LOCKED_LP was "the creator's" holding, when LP is a transferable CAT — meaning a later buyer of those units, not just the creator, could be the one left permanently stuck. The author's reply: "The genesis mint pays MIN_LOCKED_LP to a puzzle hash with no preimage in the same transaction that mints it" — i.e., the implementation always burned it; only the CHIP text was wrong.

On inspection of the actual reference implementation, this claim does not hold:

  • The puzzle layer cannot and does not enforce any such burn — eve_coin_id is an opaque solution atom used only inside the genesis announcement hash; nothing constrains the eve's inner puzzle or where the minted LP goes.
  • The keyless V12 creation driver, contracts/forge_v12_create.py:196-217, pays the entire genesis total_lp — all 1,000+ units, no split — to the creator's own recipient_ph.
  • The only burn logic in the repo (forge_create_pool.py:351-354, 444-450) is an opt-in flag requiring the recipient to be ZERO32 for the whole genesis mint — an all-or-nothing choice, not a 1,000-unit floor.
  • docs/FORGE_PUZZLE_V12.md:157 and the CHIP's own leaf-table (pre-revision-4 wording) still say "the creator locks 1,000 LP units," i.e. describe a transferable, spendable holding.

Consequence: the creator holds 1,000 transferable-but-effectively-unredeemable LP units by default — exactly the scenario the bot's Finding 8 warned about, live in the reference implementation, not merely a text error as the author told CNI.


3. Finding 7 vs. our own oracle finding — the most important reconciliation in this report

PR-217's Cursor bot (round 2) claimed h - birth could go negative for a same-block successor. The author rebutted, correctly: assert h >= birth makes the subtraction non-negative by construction, and a same-block successor is provably unspendable (an ephemeral coin is dated prev_transaction_block_height + 1, while AssertHeightAbsolute is checked against prev_transaction_block_height itself — the two can never both hold). Finding 7 was genuinely documentation-only, and a reader of the PR thread alone would reasonably conclude the oracle mechanism is now fully sound.

That conclusion is wrong, for a mechanism nobody in the PR thread raised. Both oracle-team agents (independently, with the Opus agent pinning the exact root cause) found:

forge_action_common.rue:301-305 writes state.oracle.last_height = h — the claimed height — while only crediting h - birth — the coin's actual birth — to the accumulator. Any gap between when the previous spend was mined and the height it claimed is silently and permanently discarded, because last_height has already moved past it. This is not an illegal negative interval (which the birth-height assert correctly forecloses) — it's a legal zero interval, always available by claiming h = birth.

Verified by direct execution:

  • A pool spent honestly in every transaction block freezes its own oracle — 8 consecutive genuinely honest spends produced elapsed = 0 every time, no attacker involved, losing 20% of real blocks to nothing.
  • Weaponized: 10 generations of individually-honest-looking, signature-free observe spends (permissionless, cost = tx fees only) drove the reported TWAP to exactly 0 across 310 real blocks while the true spot price never moved.
  • Price-injection (the original M-2 shape) is now genuinely bounded — real exposure ≥ claimed weight — but price-deletion is unbounded and free in the other direction, an equally serious failure for any TWAP consumer.
  • A related gap: oracle_window has no upper bound in valid_config, so a registrar can set it arbitrarily large and remove even the residual 31-block cadence requirement.

For a TWAP consumer, a reading that can be driven to 0 or ∞ is the same category of failure as a reading that can be inflated — this is a regression of the same underlying finding, not a separate new issue, and it survived precisely because PR-217's testing (and the CHIP's own revision-4 text, "a claimed h can understate that interval by at most the window") never considered the zero case.

Severity: MEDIUM–HIGH for any oracle consumer (matches the original M-2 ladder); no direct Forge fund-loss path, same caveat as M-2 always carried, since no Forge leaf prices off its own oracle.


4. Findings PR-217 never tested — ranked by real-world severity

PR-217's five human findings all exercise a single, internally-consistent pool configuration. None varies config across leaves, none touches registry admission/squatting, none touches collect. That's a structurally different attack surface than what CNI's reviewer tested, and it's where the worst result in this exercise lives.

M-4 (unregistered pool, cross-leaf config mismatch) — STILL OPEN, escalated MEDIUM → HIGH

Nothing on the pool coin itself, and nothing in the action layer (upstream/action.rue:47-76), requires all six leaves in a pool's merkle tree to share one configuration — each leaf just proves its own presence and validates its own curried config in isolation. Registered pools are safe (registration recomputes and pins a single six-leaf root from one config_hash), but nothing distinguishes a registered pool from an unregistered one at the coin level.

Two independent full drains, both accepted by real chia_rs, reproduced by both genesis-team agents:

  • A pool where five leaves share weights [1,1] but the swap leaf alone is curried [8,1] — extracted 7.7× the honest constant-product output on identical input.
  • A pool with a remove leaf curried against a different lp_tail_hash than the other five — a worthless, self-minted fake-asset LP coin answers the pool's release message, draining ~99.98% of both reserves (9,998,000/19,996,000 mojos out of a 10M/20M pool in one reproduction).

This specifically defeats the project's own _test_v12_lp_receive_forgery.py "case E," which only ever tests a forged TAIL against one consistent curried config — never a mismatch introduced via a different leaf in the same tree. Exposure: any third party who funds a pool coin (shared link, naive frontend, a router that trusts a pool by puzzle hash) without independently verifying it against the registry.

MIN_LOCKED_LP burn is unimplemented — MEDIUM (§2 above; new finding from cross-validation)

Registry pool_key squatting family (M-3, L-3, and a new price_scale variant) — MEDIUM

pool_key = tree_hash([asset_ids, weights, fee_bps, protocol_fee_bps, dao_puzzle_hash]) — excludes protocol_puzzle_hash (the actual fee recipient), price_scale, oracle_window, and lp_tail_hash. valid_pool requires only all_positive(reserves) (so [1,1] passes) and total_lp >= MIN_LOCKED_LP; there is no deregister/expiry leaf and no proof the reserves are actually funded.

  • Both genesis-team agents: a squatter registers reserves=[1,1], never funds them, for the cost of one creation fee — permanently blocking the genuine pool at that market's key.
  • One collect-team agent: a pool registered with an absurd price_scale bricks itself permanently on its first real spend, but the identical pool_key — since price_scale isn't part of it — is now occupied forever, blocking a sane pool with the same economics from ever registering.
  • Two pools differing only in protocol_puzzle_hash (i.e., different real fee recipients) collide to the same key.

L-4/N-1 (oracle interval discard) — MEDIUM–HIGH for consumers — see §3

M-1 (collect DUPLICATE_OUTPUT brick) — MEDIUM, narrow

Byte-identical to V11: forge_action_collect.rue:60-61 emits two identical CreateCoins when protocol_puzzle_hash == dao_puzzle_hash and the accrued amounts coincide (exactly, or transiently via integer-floor on unequal rates), rejected as DUPLICATE_OUTPUT by chia_rs — bricking the only exit for accrued fees on that reserve. New nuance found: the DAO can self-rescue by irreversibly lowering its own rate plus one further swap; permanent only if the DAO recipient can't act.

L-2 (Bytes32 grinding, root cause) — STILL OPEN, LOW/hardening

Re-confirmed against the now-pinned rue-compiler 0.8.4: still zero runtime strlen calls anywhere in the compiled output. Both of V12's new hash constructions were checked and found not to revive the exploit (the genesis one is safe by construction, per §1; the registry's is safe by the same "adjacent fixed-width curried constant" circumstance as V11). The warning that a future refactor could revive this remains valid.

MIN_LOCKED_LP boundary bug — LOW

A pool registered at exactly total_lp == 1000 (the >= in valid_pool should be >) has zero redeemable units — reproducing, at the boundary, precisely the bug MIN_LOCKED_LP was introduced to fix. A separate, related gap: an unregistered pool created with total_lp < 1000 has no floor enforced anywhere in the prologue, so a depositor's later LP can be partly trapped.

L-6, L-7, I-1 — unchanged from V11, all reconfirmed live

  • L-6: observation slots remain permanently unspendable (no leaf ever emits the required mode-18 message).
  • L-7: duplicate-output collisions beyond collect (two observes in one spend; swap+remove with coincident net payouts) still reject whole bundles with no puzzle-level hint.
  • I-1: add's assert deposit >= 0 is confirmed still load-bearing via an actual compiled mutant — deleting it accepts a disguised withdrawal-as-deposit that also mints LP.

Minor: register proves the genesis mint was authorized, never that it happened — INFO

A bundle with the genesis eve entirely omitted still registers successfully; eve_coin_id carries no width check. Phantom total_lp behaves economically like a permanently-locked floor, so no loss path was found, but it overstates what "registered" actually guarantees.


5. Priority fix list

Must fix before any real-money deployment:

  1. M-4 — bind all six leaves to one configuration at the pool-coin level (not only at registration), e.g. curry a single config_hash once into the finalizer/action layer that every leaf must assert against. Add a cross-leaf-mismatch regression test (closes the _test_v12_lp_receive_forgery.py case-E blind spot).
  2. Oracle interval discard (§3) — credit the full interval since the prior spend rather than discarding [last_height, birth); re-verify against real check_time_locks. Correct the CHIP's "understate by at most the window" claim, which is wrong in the zero-interval case.
  3. MIN_LOCKED_LP burn — implement the 1,000-unit split to a genuinely unspendable puzzle hash in forge_v12_create.py and every other creation path, or make the floor non-transferable by construction. Correct the CHIP text and the author's standing reply to CNI, which currently misdescribes the reference implementation.
  4. Registry pool_key family — include protocol_puzzle_hash, price_scale, and oracle_window in the key (or drop them from config); require proof the reserves are funded (or the genesis mint occurred) before registration; add a deregister/expiry path; bound price_scale.

Should fix, lower urgency: M-1 (merge or nonce coincident collect payouts), L-7, L-6 (implement or remove the dead slot-read path), I-1 (pin with a regression test), L-2 (explicit length asserts on solution-sourced Bytes32 as hardening), the total_lp >= MIN_LOCKED_LP boundary (> not >=), and a docs correction that registry membership proves authorization of the genesis supply, not its existence or funding.


Process notes

  • The reference implementation moved from contracts/v11 (our original audit target, now withdrawn/retired) to contracts/v12 on 2026-09-13, specifically in response to PR-217 — this re-audit targeted the current contracts/v12 throughout.
  • PR-217 is not a finished review: it is open, CHANGES_REQUESTED still stands from the human reviewer as of 2026-09-14, and a second, same-day automated pass (Cursor bot) caught two new issues in the author's own fixes hours before this re-audit began — the review process is demonstrably still live.
  • One verification gap in our own initial re-audit (whether the MIN_LOCKED_LP burn destination was actually unspendable, vs. just the arithmetic floor) was only caught during cross-validation against the PR thread — worth noting as a reason to keep running this kind of cross-check rather than trusting either process alone.
  • Three separate agent pairs, using different chia_rs/chia-blockchain environment versions and independently-written PoC harnesses, converged on the same conclusions for every finding where both models were assigned the same area — a meaningful cross-model consistency check on top of the cross-process one.

…erify the burn

A second independent review of the reference implementation tested configuration
and registry admission rather than the single well-formed pool the first review
exercised. Three of its findings change what this document requires.

An implementation must bind its whole leaf set to one configuration at the coin
level. The action layer proves a leaf's membership of the merkle root and not the
leaves' agreement with each other, so a pool assembled from leaves of differing
configurations releases its reserves against an asset of the attacker's choosing.
The binding also makes the leaf set legible: the hashes sit in the finalizer's
curry, so a verifier reads them off the coin.

The oracle must credit the interval between a spend's claimed height and its
inclusion, at the price in force during it. Revision 4's claim that a claimed
height 'can understate that interval by at most the window' was wrong in the case
where it understates it to nothing, which is free and permanent.

Registration must verify the genesis burn rather than only require it in prose,
which it can do because the genesis supply passes through a settlement whose
payments are announced.

Alongside: the genesis supply must exceed the locked minimum rather than merely
reach it; the protocol fee recipient and the oracle parameters are pinned by the
registry rather than chosen by the registrant; and both oracle parameters are
bounded.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@awizardxch

Copy link
Copy Markdown
Author

@trgarrett - thank you, and particularly for testing the thing the first review did not: a pool's configuration, and the registry's admission rules. Three of these are real, one is the worst finding this project has had, and all three are fixed in a new revision rather than patched into the old one. Below is the disposition of every item, including the two I could not reproduce.

Revision 5 is now on this PR, and the reference implementation has moved from V12 (protocol 13) to V13 (protocol 14). V12's testnet liquidity has been withdrawn and its sources removed from the public repository, as V11's were. _test_v12_v13_review_findings.py builds each attack below against the V12 build and expects it to be ACCEPTED, then against V13 and expects it to be REFUSED, so a finding that stops being demonstrable fails the suite as loudly as one that still works.

The three that forced a major increment

M-4, cross-leaf configuration mismatch. Correct, and correctly ranked. The action layer proves each leaf is a member of the merkle root; nothing in it says the six leaves agree, and I had been reading "the registry recomputes the root from one config" as a property of the coin when it is a property of registration. Reproduced here at 99.80% of both reserves.

V13 binds the leaf set at the coin level: the finalizer is curried with the configuration hash, the six leaf module hashes and the observe leaf's slot hash, recomputes the six-leaf root from them, and asserts it equals the root the action layer carries, on every spend. Worth being precise about what that buys, because my first write-up overstated it. It cannot stop an unrelated puzzle existing and is not meant to. What it does is make the leaf set legible: the hashes sit in the curry in the clear, so a verifier reads them off the coin and compares them with the published set. Five honest leaves and one poisoned no longer have a valid root, and a pool that rewrites the finalizer no longer matches the published hashes, so the mismatch is visible before anyone deposits. The registry performs exactly that comparison against its own pinned hashes.

The discarded oracle interval. Correct, and the more interesting of the two oracle findings. Your framing is the one that makes it obvious: it is not an illegal negative interval, it is a legal zero one, and it is free.

V13 keeps the spot price it last credited in state and credits two intervals: the previous state over (last_height, birth] at the spot that state recorded, and the current state over [birth, h] at the spot on the pre-spend reserves. The prologue also asserts birth > last_height, which consensus guarantees for every real chain of spends. Understating h now defers credit to the next spend instead of destroying it. Measured on chain after the redeployment: a pool created at 4,686,415, added at a claimed 4,686,597 which was included at 4,686,602, then swapped at 4,686,863, accumulates 182 blocks at the genesis spot, the 5-block tail at that same spot, and 261 blocks at the post-add spot - matching the formula exactly, where V12 credited 443 of those 448 blocks and could never recover the rest.

The unburned floor. You are right, and the part that matters is that my reply to CNI described the deploy script as though it were the implementation. scripts/deploy-v12-testnet.py did burn the floor; contracts/forge_v12_create.py, the lane the website actually uses, paid the whole genesis supply to the creator. I checked one and answered for both.

V13 does not rely on either driver. The genesis mint lands at CAT(lp_tail, OFFER_MOD), whose payments are announced, and register asserts the announcement of a payment of MIN_LOCKED_LP to the zero puzzle hash under the launcher id. Since only the TAIL's genesis branch can create that asset, the same assertion closes your INFO finding: it proves the mint happened, not merely that it was authorized. All 32 pools of the new deployment show the floor burned and unspendable.

The registry findings

M-3 / L-3. I have not added the three values to the key, because they are not market parameters and the key describes a market. The registry pins them instead: protocol_puzzle_hash, price_scale and oracle_window must equal the registry's own curried constants or valid_pool refuses. That closes the fee-recipient squat and the price_scale brick, and two pools differing only in fee recipient can no longer both exist. lp_tail_hash needs no pinning: it is already required to be the TAIL curried with this launcher, so it is determined rather than chosen.

Deregistration and expiry: deliberately not added, and I would rather argue the position than quietly skip it. A registered pool cannot die, because the floor keeps it alive, and a thinly funded pool is a live market anyone can deepen, so "the key is taken" means "this market exists". That claim is worth nothing unless the curve actually accepts the deposit, so it is now a test rather than an argument: a pool registered at the smallest thing the registry admits, reserves [1, 1] and 1,001 LP, takes a real deposit of 10 XCH and 5,000 CAT, mints 7,073,534,532 LP, and the squatter's single remaining unit is diluted to nothing. They paid a creation fee to inconvenience nobody. A minimum reserve would price small markets out without changing that.

The boundary. Correct. Registration now requires total_lp > MIN_LOCKED_LP, and the prologue additionally asserts total_lp >= MIN_LOCKED_LP, so a pool minted below the floor outside the registry cannot be spent at all and therefore cannot trap a later depositor.

M-1. Correct. When the protocol and DAO recipients are equal the leaf now pays one coin of both fees. Pinned by a case that reproduces the V12 DUPLICATE_OUTPUT and shows V13 paying the merged coin.

The two I could not reproduce

I-1, add's assert deposit >= 0. This does not reproduce against the shipping build, and I would rather say so than quietly agree. Rebuilt with that line deleted, the leaf still refuses a negative deposit, and a search over deposit vectors with a negative slot found no lp_delta the bracket accepts at all. exact_invariant_lp_mint is two-sided: a negative slot drives min_deposit_ratio negative, which puts the target below total_lp^K * old_product, so no positive mint satisfies the lower bound, and assert lp_delta > 0 covers the rest. Checked against a compiled mutant rather than argued from the source. Our mutation run reports SURVIVED for that line, and here SURVIVED is genuine redundancy rather than a missing test. If your harness has it accepting something, I would genuinely like the bundle.

L-2, Bytes32 grinding. Agreed on the facts and the ranking. rue 0.8.4 emits no runtime strlen, and every solution-sourced Bytes32 in this design feeds a sha256 preimage whose result is compared against a consensus-committed coin id or announcement, so a wrong width produces an identifier that exists nowhere rather than a collision. Left as recorded hardening rather than paying cost on every spend for a property the comparison already gives. Worth revisiting if a future revision ever compares a derived hash against something that is not a committed id.

L-6 and L-7. Both accurate. The observation slot is written for a consumer to spend with its own message and is never spent by a leaf, which is by design rather than oversight; the live path is the announcement, and the specification now says so instead of leaving it to be discovered. L-7's collisions fail the composer's own bundle rather than anyone else's, so they are a composition rule; I checked the router's lanes and none of them compose a pair that can collide.

What revision 5 changes

The configuration binding, the exact oracle and the verified genesis burn are normative. Alongside them: the genesis supply must exceed the floor rather than reach it, the protocol fee recipient and the oracle parameters are pinned by the registry, and both oracle parameters are bounded. The "understate by at most the window" sentence is gone; it was wrong in exactly the case you found.

Redeployed and exercised

testnet11, 32 pools: 32 deposits, 29 swaps, 25 collects, 6 withdrawals, 4 observations, a multi-hop route, and swap, deposit and withdrawal settled through the real offer lane - the path a browser uses, with the router holding no key. 364 discoverability checks against the chain, including that every pool's floor is burned and unspendable. Every offline suite passes.

Two notes on process

Your cross-validation step is what caught the unburned floor, and it caught it because it compared the implementation against what I had told CNI rather than against the specification. That is a check neither review process would have made alone.

And the finding I could not reproduce was as useful as the three I could, because it sent me to build the mutant rather than argue from the source. If the other two rebuttals above are wrong, that is the way I would like to find out.

Still no external audit, and I would not want this advanced on the strength of the implementation alone.

…chitecture and CLVM pass

Housekeeping, no design change.

The Additional Assets list named the previous revision's specification. That
document was withdrawn from the reference repository when this revision replaced
it, so the link resolved to nothing -- a dead link in a document under review, and
the same failure revision 3 was written to correct when the V11 documents were
withdrawn and the list pointing at them was not updated. It now names the current
specification.

The architecture description and the written CLVM pass have been brought to this
revision and are listed alongside it, restoring the three-document set the list
carried before those two were withdrawn with the revision they described. Both are
published in the reference repository, and each link was checked against it before
this commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@trgarrett

Copy link
Copy Markdown

Re-tested V13 against every item in your reply, adversarially, against the actual shipping source rather than the prose — chia_rs-executed where a bundle would settle it. Seven of nine hold up, two of them more solidly than you argued (the config-root binding is a hard rejection, not merely legible; the oracle fix is exact, not approximate — I rebuilt your own on-chain numbers independently and they match to the block). Two don't hold, and I want to be as precise about those as you were about the others.

The two that don't hold

Registry squatting — the dilution defense has a cheaper counter. Your [1,1]/1,001-LP scenario is real; a genuine deposit does dilute it to nothing, modulo a units slip in your own arithmetic (7,073,534,532 LP matches at 0.01 XCH deposited, not the 10 XCH stated — doesn't change your point). But register never checks that reserve_parents names real coins; nothing in the registration bundle spends or asserts a reserve. So instead of funding [1,1], an attacker registers with reserve_parents pointing at coins that don't exist. Registration succeeds — valid_pool's all_positive only looks at the claimed amounts. From then on, the finalizer derives every reserve coin id from those phantom parents, so the first real interaction anyone attempts — including the dilution deposit your defense relies on — fails MESSAGE_NOT_SENT_OR_RECEIVED. The pool can never be spent by anyone, ever, including the squatter. "The key is taken" no longer means "this market exists"; it means the market's key and name are gone permanently, for the price of one creation fee. This is the same gap M-3 originally named — register proving authorization of a config rather than existence of the thing it configures — surviving under a cheaper construction than the one you closed.

I don't have a preferred fix to push on you, but the shape is: register needs to observe something that only exists if the reserves are real — an announcement from each reserve coin, the way genesis now proves the LP burn actually happened rather than merely being authorized.

I-1, add's assert deposit >= 0 — it does reproduce. I rebuilt the shipping V13 tree with rue, confirmed byte-identical to your compiled hex, then built the mutant with that one line deleted (confirmed the mutant's hex differs, nothing else does). Against the actions-suite pool (reserves [10,000,000 XCH; 20,000,000 CAT], total_lp=5,000,000, fee_bps=30), a solution of deposits=[-100000, +500000] is refused on the shipped leaf and accepted on the mutant, at lp_delta=36611 — reserves become [9,900,000; 20,500,000], total_lp becomes 5,036,611. That's a net withdrawal of 100,000 XCH from the pool, wearing an add.

I think I see the gap in "no positive mint satisfies the lower bound": a negative slot does push min_deposit_ratio negative, but that also shrinks the balanced-deposit subtracted from the positive slot's excess, which inflates effective_product rather than only depressing it. For [-100000,+500000] at ratio=-0.01: slot 0 effective is 9,900,000, slot 1 effective is 20,498,950 (after fee), giving effective_product = 2.0294e14, which is above old_product = 2.0e14 — so the lower bound is satisfied and a positive mint exists. The bracket only stays closed while the positive slot is small enough that it can't overcome the negative slot's own drag; [-100000,+200000] does refuse, on the mutant too. I'd guess your search stayed in that region. Happy to send the actual bundle (six coin spends, serialized) if that's faster than rebuilding from these parameters — say the word and I'll get it to you.

Everything else

Config-root binding: closes both shapes I sent, and closes them as a hard clvm raise for anyone, not merely as something a verifier could notice — I want to be clear I was wrong to imply otherwise was the open question. It doesn't and isn't meant to stop a pool built from genesis with six custom-but-mutually-consistent leaves; that one's still a real exposure but it's the one you named as out of scope, and I agree the registry is the right place to close it, not the coin.

The oracle fix is exact — I reconstructed your on-chain example as a real three-spend bundle sequence independently and got 182 / 5 / 261, same as you. I went looking for a residual (whether deferred credit could still get mis-weighted within an interval, whether a same-block manipulate-then-observe could sneak a reading in) and didn't find one. oracle_window/price_scale bounds check out and are curried in, so they're not swappable after the fact.

Genesis burn, the three pinned registry fields, the >/>= boundary, and the merged collect payout all check out as described — refused every bypass I tried on each.

Two things unrelated to any specific finding, in case they're useful: _test_v13_integrity.py is red at HEAD in my checkout — 10 stale source_sha256 entries in compiled/manifest.json against the current .rue sources (manifest and sources look like they were touched in the same commit without a final regeneration pass). I rebuilt from source independently and the bytecode itself is fine, so this is a provenance-metadata gap, not a shipped-vs-source divergence — but worth a regenerate before it's trusted for anything. And _test_v12_v13_review_findings.py isn't in the repository's history — I checked every commit, not just the tip — so that citation doesn't currently point at anything; may just not have made it into the push.

Still no external audit, on my side either — this was six adversarial passes plus this one, not a security firm.
README.md
reproduce.py
spend_bundle.hex.txt

@danieljperry

Copy link
Copy Markdown
Member

We will hold a public Zoom discussion of CHIP-62 on September 24 at:

  • 4 PM PDT
  • 11 PM UTC (midnight UK time)
  • 7 AM Sep 25 (China)

See the #chips channel of our Discord for more details.

… load-bearing

Two corrections, no design change. The Feasibility section said the puzzle uses
CHIP-0050's upstream action layer and finalizer with no changes to either; the
action layer is upstream and unchanged, the pool's finalizer is this proposal's
own multi-reserve finalizer, and the text now says so. And the author's reply to
the fourth review said the add leaf's non-negative-deposit assertion was
redundant; it is load-bearing, and the reference suite now pins it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@awizardxch

Copy link
Copy Markdown
Author

Two corrections to things we published, both wrong, posted together. Neither changes the reference implementation; both change what we claimed about it. Revision 7 of the CHIP is on this PR with the text.

1. I-1 reproduces. We said it did not.

@trgarrett - you were right, and our reply was wrong. Our earlier response, and FORGE_PUZZLE_V13.md and FORGE_V13_CLVM_PASS.md as published, said add's assert deposit >= 0 was redundant - that the mint bracket refuses every negative-slot vector on its own. It does not.

We rebuilt your vector independently from the parameters you gave, without running your script: on [10,000,000; 20,000,000] at total_lp = 5,000,000, deposits = [-100000, +500000] satisfies the bracket at lp_delta = 36611 - a withdrawal of 100,000 wearing an add, which also mints LP. The shipped leaf refuses it, so V13 was never exploitable; the assert is present and it is load-bearing. What was wrong was our claim and the coverage behind it.

Your arithmetic is exactly right, and the reason we missed it is the useful part. Our search ran through a Python mirror whose wrapper refuses negative deposits before the bracket is reached. We tested the guard we were trying to test, through a mirror carrying the same guard, and read its refusal as the bracket's. We had also stayed in the region where the positive slot is too small to overcome the negative slot's drag - [-100000, +200000] refuses on the mutant, as you said. A mutation verdict of "survived" means no probe reaches the line; it never means the line is redundant. That rule is now in our method, and the vector is pinned in the actions suite and in _test_v13_second_review.py, which turns the verdict on that line to killed.

We did not need the serialized bundle you offered - the reproduction matched to the unit - but thank you for offering it. Errata are on the two documents that made the claim.

2. The CHIP understated the custom code a reviewer must read.

The Feasibility section said the puzzle is "built from CHIP-0050's upstream action layer and finalizer with no changes to either." Half true. The action layer is upstream and unchanged. The pool's finalizer is not: it is Forge's own multi-reserve finalizer - curried with the reserve hashes, the configuration hash and the six leaf module hashes, sending one CHIP-0025 message per reserve on every spend, and asserting that the action layer's merkle root is the six-leaf root of exactly that configuration. It is where M-4 was closed. The upstream finalizer is vendored unchanged and serves only the registry singleton.

Vendoring the upstream finalizer does not make the pool's finalizer upstream code, and a reviewer reading the CHIP would have looked in the wrong place for the custom logic. Revision 7 says so, and names the finalizer as the code to read first.


Flagged by the same review and carried into the next revision rather than corrected here: the integrity suite recompiles only the upstream pins and curve exports, not Forge's leaves and finalizer, and passes when rue is absent - it will recompile everything and fail when it cannot; the O-1 closure argument was wrong (a future-dated h does not force h == birth, since birth is the coin's creation height) and is replaced by a measured one; the architecture's "<=" on the mint and payouts becomes "exact", which is what the leaves enforce; and review provenance gets an attribution table - commissioned audit, none; community review, CNI and @trgarrett here; automated, cursor; the rest ours. Errata on the affected documents in the meantime.

In response to the fourth independent review of the reference implementation
(trgarrett, 2026-09-15). One change is normative, and it is the most serious
finding this proposal has had.

Registration proved the launcher's authorization of a configuration without
proving that the coins the configuration describes were ever made. A registry
that rebuilds the pool's puzzle hash from reserve parents named in the solution,
and reads the reserve amounts from that same solution, admits a pool whose
reserves were never funded. Nothing is stolen, which is what makes it easy to
miss: the pool simply cannot be spent by anyone, because the finalizer derives
every reserve coin id from those parents and the first real interaction fails
message pairing. The market's key is gone permanently for one creation fee, and
the dilution defence the previous text relied on cannot be executed either.

Specification now requires a conforming registry to observe something that exists
only if the reserve does, with the reference implementation's reserve launcher
given as the construction: register takes each reserve's GRANDparent, derives
P_i = coinid(grandparent_i, launcher_hash_i, reserves[i]), and asserts a coin
announcement from P_i. The ASCII-prefix requirement is stated, because the CAT
layer refuses an inner coin announcement beginning 0xcb and a bare tree hash
bricks one creation in 256. The three cheaper alternatives are recorded as
refused so they are not re-attempted.

Two further requirements are stated that the text had left to the implementation.
A leaf that takes value binds its settlement by PARENT AND AMOUNT and asserts the
derived coin, rather than asserting an announcement that says nothing about how
much the coin holds. And the section says plainly what that does NOT guarantee:
an assertion is not consumed, conservation is a property of the whole bundle, and
a network fee is slack -- observed on chain, not reasoned about.

The locked minimum is one unit. It exists so a pool is never spendable-but-empty,
and a larger floor only strands liquidity: a thousand units stranded a median
2.21% of each pool against about 0.002% at one.

Test Cases gains the rule that a mutation verdict of "survived" means UNREACHED,
never redundant -- the method error behind revision 7's correction, written down
as guidance rather than as an apology.

The reference implementation has moved to V14 (protocol 15). V13's testnet
liquidity was withdrawn and its sources removed from the public repository, as
V12's and V11's were, and Additional Assets names the V14 documents.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread CHIPs/chip-0062.md Resolved
Comment thread CHIPs/chip-0062.md Resolved
The proposal describes three mechanisms that are hard to hold in prose, and the
last two are what revision 8 made normative. Each section now carries a figure
beside the description rather than instead of it.

  Coin layout    one pool spend, and the path that does NOT exist: a leaf emits a
                 tagged condition, the finalizer routes it and derives the reserve's
                 receiver id from committed state. A leaf never addresses a reserve.

  Authorization  the settlement binding against the shape it replaces, side by side.
                 Naming the coin id binds the asset and the id and says nothing about
                 the amount; deriving the id from a parent and an amount binds both.

  Registry       the reserve proof: one lineage per reserve inside the registration
                 bundle, the derivation register performs, and the announcement only
                 a coin running the launcher can make.

Hand-authored SVG under assets/chip-0062, following CHIP-0021's convention rather
than a rendering service, so the figures diff like text and need no network fetch.
Each carries its own opaque ground and explicit colours, so it reads on a light or
a dark page, and an aria-label stating the claim for anyone who cannot see it.

The Additional Assets note is updated: it offered to copy assets into this
repository once a number was assigned, and the figures now are here.
Two corrections from an automated review of revision 8, both to the text rather
than the design. The reference implementation is unchanged by either: each
construction below is what it already does, and what the CHIP failed to say.

1. The settlement rule omitted the puzzle hash.

A coin id is coinid(parent, puzzle_hash, amount). Revision 8 said a leaf is given
the settlement's parent and amount and "derives the coin id from them", which is
not implementable -- it names two of the three inputs and leaves an implementer to
guess the third. It is not a guess and it is not a solution field: the settlement
puzzle hash is determined by the asset the leaf is crediting, the offer settlement
puzzle for a native-XCH input and that puzzle inside the asset's CAT wrapper for a
CAT, and the leaf already knows which asset it credits because the index is its
own. That it is derived rather than supplied is the whole point of the rule, so
the rule now says it.

2. The reserve rule left the announcement's message unspecified, and this is the
   better catch of the two.

Deriving P_i binds WHICH coin announced. It does not bind WHAT that coin did.
A launcher built to revision 8's text could create a coin at some other puzzle
hash, announce it truthfully, and satisfy an assertion that only checks that A
coin was created -- leaving a pool whose reserves the finalizer will never
recognise, which occupies the market key exactly as an unfunded reserve did. That
is the finding R-1 closed, reopened one level down.

A conforming launcher's announcement MUST commit to the puzzle hash it created,
the amount, and the pool's launcher id; and the registry MUST build the expected
message from the reserve puzzle hash it derives from the pool's own configuration,
never from a value in the solution. Both halves are now normative.

assets/chip-0062/reserve-proof.svg had the same gap: it drew the announcement as
the ASCII tag with the payload elided. It now shows the tree hash over
(created_puzzle_hash, amount, launcher_id), and the register box says the registry
builds that message from the reserve hash it derives itself.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7305762. Configure here.

Comment thread CHIPs/chip-0062.md Resolved
@awizardxch

Copy link
Copy Markdown
Author

@trgarrett - both of the two that didn't hold are closed, and you were right about both. Revisions 8 and 9 are on this PR and the reference implementation has moved from V13 (protocol 14) to V14 (protocol 15). V13's testnet liquidity has been withdrawn and its sources removed from the public repository, as V12's and V11's were.

Registry squatting - closed, and closed the shape you named

You wrote: "register needs to observe something that only exists if the reserves are real - an announcement from each reserve coin, the way genesis now proves the LP burn actually happened rather than merely being authorized." That is exactly what V14 does, and it is worth saying that the fix came from your sentence rather than from us finding a better one.

V14 adds a seventh puzzle. forge_reserve_launcher.rue creates one coin at the puzzle hash it is given, for its whole amount, hinted with the pool's launcher id, and announces that it did. register is handed each reserve's grandparent and derives

P_i = coinid(grandparent_i, launcher_hash_i, reserves[i])

      launcher_hash_i = RESERVE_LAUNCHER_HASH                            (XCH)
                      = cat_puzzle_hash(asset_i, RESERVE_LAUNCHER_HASH)  (CAT)

then asserts a coin announcement from P_i. A coin id commits to its puzzle hash, so the only coin that can make that announcement runs the launcher, and the launcher does nothing but create the reserve. reserve_parents left the solution entirely - derived, never claimed, which is the rule V12 applied to the pool's own solution now applied to the registry's.

Two details you will want:

The ASCII prefix on the launcher's message is load-bearing. The CAT layer refuses an inner coin announcement whose first byte is its own ring marker 0xcb, so a bare tree hash bricks one creation in 256. The message carries forge-reserve-v14 instead.

Three cheaper shapes were tried against consensus first and all three failed, which is why the launcher exists rather than something simpler. A puzzle announcement binds the puzzle and not the announcing coin, and accepted a funded reserve under a wrong parent. A coin announcement from an arbitrary coin binds the coin and not the deed. And spending the genesis eve to prove the reserve cannot be built at all: EPHEMERAL_RELATIVE_CONDITION, because a coin created and spent in one block may not carry ASSERT_MY_BIRTH_HEIGHT. That last one we could not have reasoned to - our own CLVM pass already recorded that rule for successors and nobody noticed it applies to the eve for the same reason. The rejected routes ship as _sim_v14_r1_routes.py and _sim_v14_r1_candidates.py, because a rejected route is evidence.

Refused by the live node, not by our validator. The V13 construction - a registration that pays the fee and mints the genesis LP exactly as an honest one does, but creates no reserve launcher - pushed at the live V14 registry on testnet11 comes back ASSERT_ANNOUNCE_CONSUMED_FAILED (scripts/v14-squat-probe.py).

Your units correction on the [1,1]/1,001-LP arithmetic is right and is fixed; it didn't change the point, and the point is moot now anyway, since the squat it defended against is refused before it registers.

I-1 - answered in revision 7, and the method is now in the CHIP

Nothing to add to the reply above beyond this: the rule that produced the error is now written into the CHIP itself rather than living in our notes. Test Cases now says a mutation verdict of "survived" means unreached, never redundant, and that a conforming test method SHOULD require either a probe that reaches the line or a written argument for why none can. Our own runs report UNREACHED and fail unless every unreached line is argued in contracts/v14/mutation-arguments.json. Three more lines turned out to be masked the same way in V14's first run, each by a mirror's own guard or an earlier assert, and each now has a hand-sized probe.

The two loose items

_test_v13_integrity.py red in your checkout. Reproduced exactly, and it was still open in V14. 11 of 41 source_sha256 entries matched our working copy and not the committed blob - so the manifest was verifiable by us and by nobody else. You read it right: provenance metadata, not shipped-vs-source.

The cause is line endings. Ten .rue sources are stored LF and sit CRLF in a Windows working copy, so a hash over raw bytes records a property of the machine that built it. All three places that hash a source now normalise CRLF to LF first - the build, the integrity suite, the provenance suite - and the manifest's values are recomputed the same way. Nothing else moved: tree hashes and pins are untouched and the integrity suite still recompiles every puzzle and compares byte for byte. A .gitattributes keeps a checkout from rewriting them. Verified the way you would have: hashing the committed blob now matches every entry.

_test_v12_v13_review_findings.py isn't in the repository's history. Correct, and it is deliberate rather than a push that went missing - it exploits the withdrawn V12, and retired revisions come out of the public repository with their exploit mechanics, as V11's did. That was a bad citation on our part regardless, because a reference an outside reader cannot follow is not a reference. What replaces it publicly is _test_v14_before_after.py, which runs every V14 change against the V13 and V14 builds and carries no exploit against a retired revision. A link checker now fails our build when a published document cites a path we do not publish.

Two things we got wrong ourselves since your review

Neither was found by a reviewer, and both are corrections to what we claimed rather than to the puzzles.

"Two swaps naming one settlement are refused by conservation" was too strong. True only of a bundle with nothing spare in it. An assertion is not consumed, so both leaves' ASSERT_CONCURRENT_SPEND pass, and what decides the pair is conservation across the whole bundle - where a network fee is slack. Pushed at a live pool, the pair confirmed, the second swap funded out of a 5 XCH fee. Nothing was stolen; the pool took full value for both and the trader overpaid. With a zero fee the same pair is refused MINTING_COIN. Revision 8 states the guarantee as which coin, and how much it holds - never "this coin paid for this action" - and says explicitly that an implementation composing bundles for several traders at once must account for that itself.

The locked minimum is one unit in V14, not a thousand. The floor exists so a pool is never spendable-but-empty, and one unit does that. A thousand stranded a median 2.21% of each pool's liquidity, and 4.91% at worst, for no property the puzzle gains.

Also in revision 8

A leaf that takes value now binds its settlement by parent and amount and asserts the derived coin, rather than asserting an announcement that says nothing about how much the coin holds. On V13 a swap naming a one-mojo settlement as its 250,000 input is accepted when another coin supplies the value; the live node refuses it on V14 with ASSERT_CONCURRENT_SPEND_FAILED, and the honest control with the same coins confirms.

An automated review of revision 8, since this was drafted

Cursor's bot raised two, and both are worth having: each is valid against the text and wrong about the implementation, which is the same shape as the fifth review. Revision 9 fixes the text; no puzzle changes.

The settlement rule omitted the puzzle hash. A coin id is coinid(parent, puzzle_hash, amount), and the rule said a leaf derives the id from the parent and the amount. As written it was not implementable. The third input comes from the asset the leaf is crediting - the offer settlement puzzle, CAT-wrapped for a CAT - and is precisely not a solution field, which is the point of the rule.

The reserve rule left the announcement's message unspecified, and this one is the better catch. Deriving P_i binds which coin announced. It does not bind what that coin did. A launcher built to the text as written could create a coin at some other puzzle hash, announce it truthfully, and satisfy an assertion that only checks that a coin was created - taking the market key exactly as an unfunded reserve did. The reference implementation never had this gap: its message is a tree hash over (created_puzzle_hash, amount, launcher_id) and register builds the expected message from the reserve hash it derives from the pool's own configuration. But a conforming implementation could not have known that from the CHIP. It is normative now, and the Registry figure shows the message's payload rather than eliding it.

We built that construction rather than asserting it does not work

Both probes run the same creation lane an honest pool takes, with one value in a launcher's solution changed. A probe that builds its own bundle proves something about the probe.

The one value changed In-process node testnet11
Launcher creates a decoy puzzle hash (the review's own construction) ASSERT_ANNOUNCE_CONSUMED_FAILED ASSERT_ANNOUNCE_CONSUMED_FAILED
Right hash, wrong amount ASSERT_MY_AMOUNT_FAILED ASSERT_MY_AMOUNT_FAILED
Announces another pool's launcher id ASSERT_ANNOUNCE_CONSUMED_FAILED ASSERT_ANNOUNCE_CONSUMED_FAILED
Nothing changed, the honest control accepted and registered -

The live registration is fully funded - the fee, the eve, the LP backing and both launchers are all paid for - so the node is judging the construction and not a shortfall. Nothing was spent, because nothing confirmed. scripts/sim-v14-review-derivations.py is the node run, 7/7, and it is in the public repository so you can run it. The live twin is not published: it drives our testnet wallet through a deploy script we keep private, and you could not sign from our wallet in any case. Both build the same construction; only one of them is yours to re-run, and it is the one that reproduces the finding without needing us.

The MEDIUM finding gets the arithmetic it asks for in the same run: one parent and one amount produce two different coin ids for two different assets, and the id derived for the CAT is the real coin's id to the byte. So the id is not derived from two values. It is derived from three, and the third is fixed by the asset rather than supplied alongside it.

@danieljperry - noted on the September 24 call, and I will be there.

Still no external audit on our side either. Four rounds of review have now found real defects in this code, one a demonstrated drain and one a market key lost forever, which is an argument for more review rather than for confidence.

The hand-authored SVGs did not look like anything else this project publishes. These
are Mermaid, rendered locally and committed as SVG, which is how the Forge workflow
atlas draws the same material -- so the CHIP's figures and the project's documents
now read as one set rather than two.

Two of the five are the atlas's own diagrams rather than new drawings: the pool
lifecycle and a trade end to end, taken verbatim and changed in exactly one way.
Each gains a DASHED BOUNDARY marking where this proposal's normative surface stops.
A CHIP reader needs that line: the lifecycle's deployment index and resync are the
reference implementation's bookkeeping, and in the trade the wallet and the router
are one implementation's choice, while the span inside the boundary is the part a
competing implementation must match.

The other three are specific to this proposal and have no atlas equivalent: one pool
spend including the path that does NOT exist, the settlement binding against the
shape it replaces, and the reserve proof at registration.

Each figure ships as the Mermaid source beside the rendered SVG. A figure that diffs
like text can be regenerated rather than redrawn, and a reviewer can read what it
claims without trusting the picture. The SVGs are self-contained: no font, no script,
no external reference of any kind, and each carries its own background so it reads
whatever the page around it does.

Rendered with mermaid 11 against the same theme variables the Forge pages use.
Two of the five were unreadable where it counts. GitHub renders a document at about
500px wide, and at that width settlement-binding came out 500x69 and pool-spend
500x89 -- a strip of unreadable text rather than a diagram. Measured, not guessed:
the rendered width of the image element on the blob page, against its natural size.

settlement-binding was laying its six nodes out in ONE row, so it was eight times
wider than tall. Each subgraph needs an explicit `direction TB`; I had stripped
those while trying the chart the other way round, and without them the steps do not
stack. Restored, and the long single-line labels are broken across lines so a node
is not a thousand pixels wide on its own. 1944x268 -> 754x652.

pool-spend ran left to right across five nodes. It reads the same top to bottom and
fits a column instead of a billboard. 1653x294 -> 658x772.

All five now land between 231 and 586 pixels tall in a 500px column, which is a
diagram rather than a stripe. Nothing about what they claim has changed.
Mermaid renders labels with htmlLabels off, so it does not decode entities: the two
subgraph titles in settlement-binding read "The id is DERIVED &mdash; what V14
requires" on the rendered figure. Plain hyphens instead, which is what the rest of
this document uses anyway.

Caught by looking at the figure at the width it is read at rather than at the size it
was drawn.
The reserve rule spoke of "the puzzle hash it created" and of the hash the
registry derives as though they were one value. For a CAT reserve they differ
by exactly one wrap: the launcher runs as the CAT's inner puzzle, so the hash
it is given and announces is the reserve's inner hash, the coin that appears is
that hash inside the wrapper, and launcher_hash_i is the launcher's hash inside
that same wrapper.

Built from the text as written, an implementer could hand the launcher the
wrapped hash. The CAT layer would wrap it again, the announcement would still
match, registration would succeed, and the reserve would be one the finalizer
never recognises -- the permanently occupied market key revision 8 exists to
prevent, reached another way.

Registry now tabulates all three hashes for both reserve kinds and states that
failure, and the Registry figure names each one. The reference implementation
already keeps them apart: reserve_launcher_full_hash wraps the launcher,
reserve_inner_hashes does not wrap the reserve, and the driver hands the
launcher r.inner_hash. No puzzle changes; protocol 15 is unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@awizardxch
awizardxch requested a review from greimela September 18, 2026 00:14
@danieljperry

Copy link
Copy Markdown
Member

The video from our discussion of this CHIP is now available on Youtube:
https://youtu.be/hpxxPYg9PMo

@danieljperry

Copy link
Copy Markdown
Member

This CHIP is now in Review status. Please leave your reviews here.
You can test the implementation here: https://forge.awizard.dev/
And there is also a Sage app: https://forge.awizard.dev/sage/

@cursor cursor Bot mentioned this pull request Sep 25, 2026
@awizardxch

Copy link
Copy Markdown
Author

A question for Chia Network, raised at the September 24 discussion: should The Forge build revocable-CAT (CHIP-0038) support into CHIP-0062 now, or leave it for a later CHIP?

Where it stands. The reference implementation (V14) accepts no revocable coin. A reserve and every settlement must hash to exactly CAT(asset, <pool puzzle>), which the pool derives itself, so a coin inside a revocation layer is refused whatever its hidden puzzle hash. That covers the case raised in the discussion: a plain CAT that anyone can wrap in the layer with an H of their choosing, alone or mixed with a genuine coin in a swap or add. We tested it on a simulator and on testnet11: 244/244 refusal probes across all 32 live pools, and a real, signed imprinted coin refused on a swap. The record: FORGE_AUDIT_RUN_V14_2026-09-24.md.

What support would take. Accepting the layer is one curried hash away, but supporting it safely needs three things:

  1. The issuer's hidden puzzle hash fixed in the pool configuration at creation and checked against the issuance, never read from a coin.
  2. An issuer lane, so splits, merges and revokes go through the pool. A 2:1 split the pool does not take part in costs LPs about 29% to arbitrage.
  3. A way for a revoked reserve to freeze only itself. Every pool spend touches every reserve, so one hidden-path spend would freeze all of them, even if the issuer put an identical coin straight back.

The options as we see them.

  • Now: fold it into this CHIP. The specification gains an asset class, an action and a finalizer change, and the implementation goes through another audit round during Review.
  • Later: keep CHIP-0062 as it is, with revocable CATs out of scope and refused by construction, and propose support in a follow-up CHIP once CHIP-0038 use for equity-style tokens is clearer.

A related question either way: is there, or will there be, a canonical way to learn an asset's intended hidden puzzle hash? Wallets read it from the coin today, which is exactly what an imprint takes advantage of.

@danieljperry

Copy link
Copy Markdown
Member

A question for Chia Network, raised at the September 24 discussion: should The Forge build revocable-CAT (CHIP-0038) support into CHIP-0062 now, or leave it for a later CHIP?
The options as we see them.

* **Now:** fold it into this CHIP. The specification gains an asset class, an action and a finalizer change, and the implementation goes through another audit round during Review.

* **Later:** keep CHIP-0062 as it is, with revocable CATs out of scope and refused by construction, and propose support in a follow-up CHIP once CHIP-0038 use for equity-style tokens is clearer.

A related question either way: is there, or will there be, a canonical way to learn an asset's intended hidden puzzle hash? Wallets read it from the coin today, which is exactly what an imprint takes advantage of.

In my opinion, you should go with the Later option. Add support for rCATs if/when there is demand. For now, keep the scope as-is. You can always create a new CHIP with this extra support later. That said, this is a decision you'll need to make for your own CHIP.

As for your related question, every rCAT coin can have a different hidden puzzle, even if the rCAT itself is identical. If the minter can prove that they minted every coin from the same singleton with the same hidden puzzle, then every coin must have the same hidden puzzle. However, this is a difficult thing to prove, so the only safe assumption is that you can't prove what a coin's hidden puzzle is until that coin is spent.

… id by coinid, the 0xcb note corrected

Tracks V15 (protocol 16) of the reference implementation, cut after a sixth
review on 2026-10-07. The registry's creation fee becomes state moved only
by a set_fee leaf on a message from a coin at the treasury's puzzle hash;
the last two concatenation-derived coin ids use coinid and hashed atoms are
stored canonically; the reserve-announcement note claimed the CAT layer
refuses any inner announcement beginning 0xcb, which is wrong (33 bytes
only); two revisions can be live at once, routed across off chain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@awizardxch

Copy link
Copy Markdown
Author

Revision 11 (2026-10-07) is pushed. It lands after the move to Review, because what it records was found by reviews run after the Review request; we would rather the text under Review match the reference implementation than hold a correction back. Why it exists, briefly:

A sixth review of the reference implementation ran on 2026-10-07, this time over the published repositories rather than the puzzles alone. Everything it rated above informational was in the off-chain composer and services, and is fixed there. Two findings were against the puzzles, both low and both fail-closed as they stood: two coin ids still derived by hashing a concatenation instead of coinid, and hashed state that could carry a non-canonical atom. Those change puzzle source every leaf includes, so they could only ship as a revision, and we chose to cut it before the first mainnet pair rather than after.

Since a revision was being cut, it also carries one design change the operator needed: the registry's creation fee moves from a curried constant into registry state, with a set_fee leaf authorized by a CHIP-0025 message from a coin at the treasury's own puzzle hash. Changing the fee used to mean launching a successor registry. The dev-fee recipient and the puzzle set remain constants.

The revision also corrects the text itself. Revision 10's implementer note said the CAT layer refuses any inner coin announcement whose first byte is 0xcb. Measured against the CAT2 puzzle, it refuses only a 33-byte one; a bare 32-byte hash is never refused. The ASCII prefix is therefore a namespace rather than a guard, and the one-in-256 creation failure we had seen was in the off-chain creation lane, not on chain.

Finally, Backwards Compatibility now records that two revisions can be live at once, each pool spent by its own revision's leaves, since V14 is kept serving beside V15 in the reference implementation and a route across the two is composed off chain.

The full disposition and the testnet record are in docs/FORGE_PUZZLE_V15.md in the reference repository.

awizardxch and others added 3 commits October 7, 2026 10:11
…he September 24 discussion

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ss 0x000...dead

Tracks V16 (protocol 17) of the reference implementation. Registry names the
burn address; the rule (a puzzle hash with no preimage) is unchanged. Additional
Assets gains FORGE_PUZZLE_V16.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Backwards Compatibility still described V14 serving beside V15, and Additional
Assets named contracts/v15 as the revision this text describes, where revision 12
tracks V16. Both now name V16 (protocol 17), with V15 and V14 kept live beside it.
No design change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants