diff --git a/.gitattributes b/.gitattributes index 19dcf34..b0501c7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,4 @@ *.json text eol=lf *.rs text eol=lf *.yml text eol=lf +*.simf text eol=lf diff --git a/CHANGELOG.md b/CHANGELOG.md index bbe8efe..cdfe025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,27 @@ # Changelog -User-facing changes to the `tx-manifest-wallet` CLI and the manifest format. +User-facing changes to the `tx-manifest-wallet` CLI. Changes to the manifest format +itself are in the [format changelog](docs/format-changelog.md). Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/). No changelog was kept before 0.2.0. ## [Unreleased] +### Breaking + +- `run` refuses an *unpinned* manifest: one where a Simplicity program has no content hash, no compiler-version requirement, or doesn't match its hash. Add the hashes with `tx-manifest-wallet pin` and set a compiler version (see the [format changelog](docs/format-changelog.md)), or pass `--allow-unpinned` (or `--debug`) while developing, which turns each of these into a warning. + ### Added - Release binaries for macOS (Apple Silicon). +- Supports manifest format [`0.3.1`](docs/format-changelog.md), which adds program pinning. `0.3.0` manifests still load. +- `pin` command: writes every program's current hash into a manifest without reformatting it; `pin --check` fails if any is missing or stale. +- `validate --strict`: unpinned programs are errors rather than warnings. +- `run --debug`: development mode. For now the same as `--allow-unpinned`; it may enable more development-only behaviour later. + +### Changed + +- `validate` also checks each program file: that it exists, matches its hash, meets its compiler requirement, and declares the witnesses the manifest supplies. ## [0.3.0] - 2026-10-06 diff --git a/Cargo.lock b/Cargo.lock index 39d6967..a84cc98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3361,6 +3361,7 @@ dependencies = [ "lwk_wollet", "rand 0.8.7", "schemars", + "semver", "serde", "serde_json", "simplicityhl", diff --git a/README.md b/README.md index 2acbeaf..f2bed66 100644 --- a/README.md +++ b/README.md @@ -63,24 +63,33 @@ A manifest declares: - **`chain`** and **`requires`** — the ledger it targets and what the wallet must support (e.g. `"simplicity"`). - **`utxo_types`** — covenant output types, each pointing at a `.simf` program. -- **`actions`** — the operations a user performs, each with `params`, `inputs`, - `outputs` and `validations`. +- **`actions`** — the operations a user performs, each with `params`, `inputs` and + `outputs`. The full format is defined by the JSON Schema in [`schema/txmanifest.schema.json`](schema/txmanifest.schema.json); point your editor at it -with `"$schema"` for completion and inline errors. `manifest_version` must be `"0.3.0"`. +with `"$schema"` for completion and inline errors. `manifest_version` is `"0.3.1"`; `"0.3.0"` manifests still load. [`examples/p2pk`](examples/p2pk/txmanifest.json) is the smallest complete manifest. -Check a manifest with `validate` (offline structure checks) and `capabilities` (what a -wallet needs to run it; `--supports` turns it into a CI check for other wallet -implementations). +Each program is **pinned** by a hash of its file and a SimplicityHL version requirement, +so a manifest identifies exactly the programs it runs. `tx-manifest-wallet pin` writes the +hashes after you edit a `.simf`; set the version with `"simplicity_hl": { "version": +"0.7.1" }`. Unpinned manifests, including ones whose programs changed since they were +pinned, are refused by `run` unless you pass `--allow-unpinned` or `--debug`. + +Check a manifest with `validate` (structure and programs; `--strict` before publishing) +and `capabilities` (what a wallet needs to run it; `--supports` turns it into a CI check +for other wallet implementations). + +Terms and design decisions are recorded in [`docs/`](docs). ## Commands | Command | What it does | |---------|--------------| | `run ` | Run an action: resolve inputs, build, sign, broadcast. | -| `validate ` | Check a manifest without touching the network. | +| `validate ` | Check a manifest and its programs without touching the network. | +| `pin ` | Write each program's current hash into the manifest. | | `capabilities ` | Report what a wallet must support to run a manifest. | | `describe ` | Browse a manifest's actions interactively. | | `prepare ` | Split wallet funds so an action has the UTXOs it needs (Liquid). | @@ -124,7 +133,8 @@ The `simplicityhl` dependency is a fork that adds Bitcoin support; see [`contrib/regtest`](contrib/regtest) builds a local Bitcoin node with Simplicity active. CI runs `cargo fmt --check` and `cargo clippy -- -D warnings`. -Changes are listed in [CHANGELOG.md](CHANGELOG.md). +Changes to the CLI are listed in [CHANGELOG.md](CHANGELOG.md), and changes to the manifest +format in [docs/format-changelog.md](docs/format-changelog.md). ## License diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..c9fb280 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Docs + +This repository defines the txmanifest format, and other wallets implement it, so the +format's terms and design decisions are recorded here. + +- [`glossary.md`](glossary.md): the terms this repo uses, one entry each. +- [`format-changelog.md`](format-changelog.md): changes to the manifest format, by + `manifest_version`. +- [`decisions/`](decisions): one record per settled design decision, numbered in order. + +## Decision records + +Each record is short: the context, what was decided, and what follows from it. Once +accepted, a record isn't edited except for typos. A later decision that changes it gets +a new record, and the old one's status becomes "superseded by NNNN". + +To add one, copy the shape of the latest record, take the next number, and link any new +terms from the glossary. diff --git a/docs/decisions/0001-program-pinning.md b/docs/decisions/0001-program-pinning.md new file mode 100644 index 0000000..90dfd2f --- /dev/null +++ b/docs/decisions/0001-program-pinning.md @@ -0,0 +1,73 @@ +# 0001: Pin Simplicity programs by hash and compiler version + +- **Status:** accepted +- **Date:** 2026-10-07 + +## Context + +A manifest references its Simplicity programs by path (`"source": "./lending.simf"`). Its +manifest id is computed from the manifest file, so the programs were not covered by it: +editing, swapping or corrupting a `.simf` file left the id unchanged while changing where +funds are locked. A wallet given a manifest and its sources separately, such as a browser +extension receiving a request from a dapp, had no way to check it was given the right +sources. + +Nothing in a manifest said which compiler a program was written for either, and +different compilers could in principle produce different programs from the same source. + +## Decision + +1. **The manifest id stays derived from the manifest file alone.** Each program reference + carries a hash of the file it points to, so the id covers the programs. +2. **Hashes are self-describing:** `"sha256:"`, over the file's exact bytes. + `sha256` is the only algorithm defined; an unknown one is an error. +3. **Two forms, both valid, and they may be mixed:** + - on the reference: `source` / `source_hash` / `simplicity_hl_version` on a `utxo_types` + script, `simf` / `simf_hash` / `simplicity_hl_version` on a `tapleaf` or `simf_fn` + compute; + - a top-level `programs` table of `{ source, hash, simplicity_hl_version }`, named from a + reference with `"program": ""`. + + Every reference to one file must agree on its hash and version. A reference that + states neither inherits them; one that states a different value is an error. +4. **Compiler versions are semver requirements**, with the same syntax and meaning as + SimplicityHL's `simc "";` directive (`"0.7.1"` means `^0.7.1`). Where a program's + requirement comes from, highest priority first: the reference or `programs` entry, then + `simplicity_hl.version` for the whole manifest, then the file's own `simc` directive. + The manifest can narrow the file's directive but never widen it, because the compiler + still enforces the directive. +5. **Pinned and unpinned.** A program is *pinned* when it has a hash and a compiler-version + requirement. A manifest with any unpinned program is unpinned: + - allowed while developing (`validate` warns; `run --allow-unpinned` runs it). A + program edited since it was pinned counts as unpinned here, so `run --allow-unpinned` + warns about the stale hash instead of refusing; + - refused for publishing (`validate --strict` errors) and by wallets (`run` refuses by + default, and a wallet library has no way to opt out). + + Manifests written before this change load as unpinned. +6. **Introduced in manifest format `0.3.1`.** The change only adds fields, so it's a patch + release of the format: `0.3.0` manifests still load (as unpinned). +7. **The compiler version is not separately part of the manifest id.** It's a declared + requirement, covered by the id like any other field. + +## Consequences + +- Changing one byte of a pinned program makes loading fail, naming the file and both + hashes, unless running with `--allow-unpinned`, where it's a warning. Two manifests that differ only in a program hash have different ids. +- Every program is read once per run, by one checked loader; the covenant code receives + source text, never a path. +- Authors refresh hashes after editing a program with `tx-manifest-wallet pin`, which + edits only the hash values and leaves the rest of the file as written. + `pin --check` fails if any hash is missing or stale, for CI. +- **Assumption:** a newer SimplicityHL compiles the same source to the same program as the + version it was written for. Under that assumption a requirement is a minimum, and wallets + on different compiler versions derive the same addresses. If it stops holding, + requirements must become exact versions. +- **Single-file programs only.** A program is compiled from the one file it names; imports + from other files are not resolved, so one hash per file is enough. Supporting multi-file + programs will need the hash rule extended to their dependencies. +- **A planned compiled object file** for Simplicity programs could replace compiling from + source. The hash rule ("exact bytes of whatever `source` points at") carries over to it + unchanged. +- The protocol-defined program ids some manifests carry (e.g. simplicity-lending's + `LENDING_PROGRAM_ID`) are unrelated and unchanged. diff --git a/docs/format-changelog.md b/docs/format-changelog.md new file mode 100644 index 0000000..ae3139b --- /dev/null +++ b/docs/format-changelog.md @@ -0,0 +1,87 @@ +# Manifest format changelog + +Changes to the txmanifest **format**: the fields a `txmanifest.json` may contain, what they +mean, and the rules a valid manifest follows. This is for manifest authors and for +anyone implementing the format in a wallet. Changes to the `tx-manifest-wallet` CLI are in +the [app changelog](../CHANGELOG.md). + +A manifest declares the format it's written against in `manifest_version`. While the major +version is `0`, a different minor version is a different, incompatible format; the patch +version only adds things, so a reader of `0.3.x` accepts any `0.3` manifest. The format's +history before `0.2.0` was not recorded. + +The JSON Schema for the current version is +[`schema/txmanifest.schema.json`](../schema/txmanifest.schema.json). + +## [0.3.1] - unreleased + +Additive: every `0.3.0` manifest is a valid `0.3.1` manifest. + +### Added + +- **Program pinning** ([decision 0001](decisions/0001-program-pinning.md)). A manifest can + pin each Simplicity program by a content hash and a compiler-version requirement, so its + manifest id covers the programs it runs: + - `source_hash` beside a `utxo_types` script's `source`, and `simf_hash` beside a + `tapleaf` / `simf_fn` compute's `simf`. Hashes are self-describing, + `"sha256:"`, over the file's exact bytes. + - A top-level `programs` table of `{ source, hash, simplicity_hl_version, description }`. + A script or compute names an entry with `"program": ""` instead of giving + `source` / `simf`. + - `simplicity_hl_version` on a reference or a `programs` entry, and + `simplicity_hl.version` for the whole manifest: semver requirements in the syntax of + SimplicityHL's `simc` directive (`"0.7.1"` means `^0.7.1`). Priority: the reference or + entry, then the manifest-wide value, then the file's own `simc` directive. The manifest + can narrow the directive but not widen it. +- **Pinned and unpinned manifests.** A program is pinned when it has a hash and a + compiler-version requirement; a manifest is pinned when all its programs are. Wallets + must refuse unpinned manifests and programs that don't match their hash. Tools may + accept them for development. + +### Rules + +- A `tapleaf` / `simf_fn` compute gives exactly one of `simf` and `program`; a + `utxo_types` script gives at most one of `source` and `program`. +- Every reference to one file must agree on its hash and compiler requirement. A + reference that gives neither inherits them from the others. +- A `program` must name an entry in `programs`. + +## [0.3.0] - 2026-10-06 + +### Breaking + +- `manifest_version` must be `"0.3.0"`. +- A manifest with covenant `utxo_types` must declare `"requires": ["simplicity"]`. +- `chain` accepts only `elements`, `bitcoin`, `liquid` or `btc`; `cross-chain` is refused. + +### Added + +- `"chain": "bitcoin"` (or `"btc"`): manifests for Bitcoin mainnet, testnet, signet and + regtest. Fields that only make sense on Elements (non-policy assets, issuance, blinded + outputs) are invalid on a Bitcoin manifest. +- `requires`: the capabilities a wallet must support to run the manifest, including + namespaced capabilities (`ns::name`) defined by third parties. + +## [0.2.0] - 2026-08-20 + +### Breaking + +- `manifest_version` must be `"0.2.0"`, and readers enforce it. +- `utxo_type.confidential` is removed; set `confidential` on each output instead. + +### Added + +- `blinding` (`asset_bf` / `value_bf`) on outputs and inputs, to pin or declare blinding + factors. +- `confidential: true` on covenant outputs, and confidential covenant inputs. +- `allow_change` on actions: `none`, `lbtc_only` or `any`. +- `params` on `utxo_type`, and `args` at each place it's used. +- `script_hash` param compute: `sha256(scriptPubKey)` of an address. +- `simplicity_hl.unstable_features`, for SimplicityHL `imports` and `enums`. + +### Changed + +- A reissuance uses the spent token's asset blinding factor as its + `issuance_blinding_nonce`. +- Pinning an output's `asset_bf` to the factor an input of the same asset already carries + is invalid. diff --git a/docs/glossary.md b/docs/glossary.md new file mode 100644 index 0000000..cefa127 --- /dev/null +++ b/docs/glossary.md @@ -0,0 +1,53 @@ +# Glossary + +Terms as this repo uses them. Where a term is defined in more detail, the entry links there. + +**Action.** One operation a manifest defines: its parameters, the inputs it spends, the +outputs it creates. Declared under `actions`, or under a contract template. + +**Capability.** Something a wallet must support to run a manifest, listed in the +manifest's `requires` (e.g. `"simplicity"`). Third parties may define their own under a +namespace (`ns::name`). + +**Compiler-version requirement.** The SimplicityHL versions a program is written for, as a +semver requirement in the syntax of SimplicityHL's `simc` directive: `"0.7.1"` means +`^0.7.1`. See [decision 0001](decisions/0001-program-pinning.md). + +**Content hash.** A self-describing hash of a file's exact bytes: `"sha256:"`. See [decision 0001](decisions/0001-program-pinning.md). + +**Contract template.** A reusable contract definition under `contract_templates`: typed +fields plus the actions that operate on one instance of it. + +**Instance.** One deployed contract made from a template: the values its fields were given +when it was created. + +**Manifest.** A `txmanifest.json` file describing a protocol's transactions. The format is +defined by [`schema/txmanifest.schema.json`](../schema/txmanifest.schema.json). + +**Manifest id.** The hash identifying a manifest, computed from its canonical form. It is +derived from the manifest file alone; programs are covered through their content hashes. + +**Pin / pinned / unpinned.** A program is *pinned* when the manifest gives it a content +hash and it has a compiler-version requirement (from the manifest or its own `simc` +directive). A manifest is pinned when all its programs are. Unpinned manifests are for +development only: wallets refuse them. `tx-manifest-wallet pin` writes the hashes. See +[decision 0001](decisions/0001-program-pinning.md). + +**Program.** A Simplicity program, written in SimplicityHL (a `.simf` file), that a +covenant or a computed parameter uses. + +**Program reference.** A place in a manifest that names a program: a `utxo_types` script's +`source` or `program`, or a `tapleaf` / `simf_fn` compute's `simf` or `program`. + +**Programs table.** The optional top-level `programs` object of named programs, each with +its `source`, `hash` and `simplicity_hl_version`, which program references can name. + +**State.** The on-chain UTXOs a contract instance currently holds, as recorded after each +broadcast. + +**SWEL.** The expression language used inside manifests for amounts, references and +conditions (`params.amount_sat`, `vault_in.amount_sat - fee`). Not yet formally specified. + +**UTXO type.** A kind of output a manifest's actions create or spend, under `utxo_types`; +for a covenant, the program that locks it. diff --git a/examples/bitcoin_covenant/txmanifest.json b/examples/bitcoin_covenant/txmanifest.json index d412d91..fa1e8a4 100644 --- a/examples/bitcoin_covenant/txmanifest.json +++ b/examples/bitcoin_covenant/txmanifest.json @@ -1,11 +1,11 @@ { "$schema": "https://raw.githubusercontent.com/stringhandler/txmanifest-wallet/main/schema/txmanifest.schema.json", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "bitcoin-covenant", "description": "A Simplicity covenant on Bitcoin. Locks coins under a program the node itself executes, then spends them by satisfying it — the same p2pk.simf that runs on Liquid, unchanged.", "chain": "bitcoin", "requires": ["simplicity"], - "simplicity_hl": { "debug_symbols": false }, + "simplicity_hl": { "version": "0.7.1", "debug_symbols": false }, "utxo_types": { "p2pk_vault": { "description": "Coins spendable only by a BIP340 signature over the whole transaction, checked on-chain by Simplicity rather than by Bitcoin script.", @@ -15,7 +15,7 @@ "description": "x-only key the covenant checks against. Take it from `info` (Wallet Signing Key), so this wallet can satisfy it." } }, - "script": { "type": "simplicity", "source": "./p2pk.simf" } + "script": { "type": "simplicity", "source": "./p2pk.simf", "source_hash": "sha256:658bf994eac2f8dcbf5486787910513a1e4a79bd56f6e3c6c2a74dac25c0e2bf" } } }, "actions": { diff --git a/examples/bitcoin_pay/txmanifest.json b/examples/bitcoin_pay/txmanifest.json index 3db2cf3..525292c 100644 --- a/examples/bitcoin_pay/txmanifest.json +++ b/examples/bitcoin_pay/txmanifest.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/stringhandler/txmanifest-wallet/main/schema/txmanifest.schema.json", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "bitcoin-pay", "description": "A plain Bitcoin payment. The smallest manifest that runs on Bitcoin, and the one that shows what `requires: []` buys you: no covenant, no Simplicity, nothing a stock node lacks.", "chain": "bitcoin", diff --git a/examples/deadcat_v3/txmanifest.json b/examples/deadcat_v3/txmanifest.json index 4b52b64..cd6d092 100644 --- a/examples/deadcat_v3/txmanifest.json +++ b/examples/deadcat_v3/txmanifest.json @@ -1,12 +1,12 @@ { "$schema": "../../schema/txmanifest.schema.json", "$comment": "A fork of Deadcat's prediction market that this engine can run. The reissuance tokens stay confidential (Elements requires it), but each recreated token advances both blinding factors by exactly one, so the covenant checks the outputs as a translation of the inputs and needs no output-side witnesses. The factors are therefore derivable from the previous spend's on-chain witness and nothing has to be persisted; the first pair is the constant abf = vbf = 1, set by CreateMarket. The fee is checked at num_outputs - 1 on every path, which lets these actions declare an L-BTC change output instead of paying the surplus to miners. Different CMR, different addresses, NOT interoperable with Deadcat. Original header follows. Ported from Deadcat.Live (github.com/Resolvr-io/deadcat, src-tauri/crates/deadcat-sdk). prediction_market.simf is a verbatim copy of that crate's contract/prediction_market.simf — do not edit it, every byte feeds the CMR and therefore all four covenant addresses. The upstream SDK builds these transactions in Rust (src/pset/*.rs); this manifest is the same seven spending paths expressed declaratively. Deadcat's OTHER covenant, maker_order.simf, is deliberately NOT modelled: it tweaks the MAKER's key as the taproot internal key, while this engine hardcodes the NUMS internal key (covenant.rs::NUMS_KEY_BYTES), so no address it computed would be correct.", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "deadcat-prediction-market-v3", "description": "Deadcat v3 — a binary (YES/NO) prediction market on Liquid. Collateral is locked in a covenant that mints matched YES/NO token pairs at 2 x COLLATERAL_PER_TOKEN per pair; an off-chain oracle commits the outcome ON-CHAIN as a state transition, and winners then burn tokens to draw the whole pair's collateral. The market's state (0 dormant, 1 unresolved, 2 resolved-YES, 3 resolved-NO) is not stored in a variable — it is a tapdata leaf in the covenant's tap tree, so each state is a DIFFERENT address and the covenant proves its own state by comparing the address it is being spent from. Modelled as one template: one instance per market.", "chain": "liquid", "requires": ["simplicity"], - "simplicity_hl": { + "simplicity_hl": { "version": "0.7.1", "$comment": "Deadcat compiles with debug symbols OFF (contract.rs: template.instantiate(args, false)). Flipping this changes every fail-node commitment, hence the CMR, hence all four addresses.", "debug_symbols": false }, @@ -16,6 +16,7 @@ "script": { "type": "simplicity", "source": "./prediction_market.simf", + "source_hash": "sha256:6b17b170282f751a3727f4e66969b8310b4934b3e7264d930460937ac6725c88", "compile_params": { "ORACLE_PUBLIC_KEY": "ORACLE_PUBLIC_KEY", "COLLATERAL_ASSET_ID": "COLLATERAL_ASSET_ID", @@ -47,6 +48,7 @@ "script": { "type": "simplicity", "source": "./prediction_market.simf", + "source_hash": "sha256:6b17b170282f751a3727f4e66969b8310b4934b3e7264d930460937ac6725c88", "compile_params": { "ORACLE_PUBLIC_KEY": "ORACLE_PUBLIC_KEY", "COLLATERAL_ASSET_ID": "COLLATERAL_ASSET_ID", @@ -77,6 +79,7 @@ "script": { "type": "simplicity", "source": "./prediction_market.simf", + "source_hash": "sha256:6b17b170282f751a3727f4e66969b8310b4934b3e7264d930460937ac6725c88", "compile_params": { "ORACLE_PUBLIC_KEY": "ORACLE_PUBLIC_KEY", "COLLATERAL_ASSET_ID": "COLLATERAL_ASSET_ID", @@ -107,6 +110,7 @@ "script": { "type": "simplicity", "source": "./prediction_market.simf", + "source_hash": "sha256:6b17b170282f751a3727f4e66969b8310b4934b3e7264d930460937ac6725c88", "compile_params": { "ORACLE_PUBLIC_KEY": "ORACLE_PUBLIC_KEY", "COLLATERAL_ASSET_ID": "COLLATERAL_ASSET_ID", diff --git a/examples/dex/txmanifest.json b/examples/dex/txmanifest.json index 592cce3..aeb2bb1 100644 --- a/examples/dex/txmanifest.json +++ b/examples/dex/txmanifest.json @@ -1,17 +1,19 @@ { "$schema": "../../schema/txmanifest.schema.json", "$comment": "Ported from Mosaik's tessera.simf (github.com/kaleidoswap/mosaik, crates/tessera/contracts/tessera.simf). Upstream substitutes the four offer terms as inline TESSERA_PARAM literals; here they are ordinary compile params. Change tessera.simf and every offer address changes — the terms live in the tapleaf. NOTE: the Refund method is not executable until upnext/12 (absolute nLockTime) lands; see its description.", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "tessera-dex", "description": "Tessera — a keyless atomic swap offer on Liquid, the primitive a Mosaik DEX is built from. One UTXO is one all-or-nothing offer: it holds asset A and is spendable by ANYONE who pays AMOUNT_B of ASSET_B to the maker (Settle), or, after TIMEOUT, by anyone who returns asset A to the maker (Refund). No signature on either path — the covenant is pure transaction introspection, and the maker is identified only by a scriptPubKey hash. Modelled as a class: one instance per offer.", "chain": "liquid", "requires": ["simplicity"], + "simplicity_hl": { "version": "0.7.1" }, "utxo_types": { "tessera_offer": { "description": "The offer UTXO: holds OFFER_AMOUNT of the maker's asset A, spendable via the keyless Settle or Refund paths. Its address commits to the four offer terms (ASSET_B, AMOUNT_B, MAKER_SPK, TIMEOUT) plus MAX_FEE — so the terms cannot change once funded. It does NOT commit to asset A or its amount: the covenant never inspects them on the settle path (see the note in tessera.simf), which is why OFFER_ASSET_ID/OFFER_AMOUNT are instance fields but not compile params.", "script": { "type": "simplicity", "source": "./tessera.simf", + "source_hash": "sha256:751809a5967f9f7b72eeb29c655a596752f17c828a53c0c3128d170773fcb26f", "compile_params": { "ASSET_B": "ASSET_B", "AMOUNT_B": "AMOUNT_B", @@ -27,6 +29,7 @@ "script": { "type": "simplicity", "source": "./maker_payout.simf", + "source_hash": "sha256:dbdd3a1bb2a9b2b145b2a1bffe3b8bc81030b4012fb6043da809432932082e3e", "compile_params": { "PUB_KEY": "MAKER_PUB_KEY" } @@ -182,6 +185,7 @@ "MAKER_SPK": { "type": "tapleaf", "simf": "./maker_payout.simf", + "simf_hash": "sha256:dbdd3a1bb2a9b2b145b2a1bffe3b8bc81030b4012fb6043da809432932082e3e", "params": { "PUB_KEY": { "type": "pubkey", diff --git a/examples/last_will/txmanifest.json b/examples/last_will/txmanifest.json index e049547..461eb6a 100644 --- a/examples/last_will/txmanifest.json +++ b/examples/last_will/txmanifest.json @@ -1,16 +1,18 @@ { "$schema": "../../schema/txmanifest.schema.json", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "last-will", "description": "Last Will — a recursive covenant with three spending paths: inherit (after a 180-day timelock), cold-key break-out, and hot-key refresh. Modelled as a class: one instance per will.", "chain": "liquid", "requires": ["simplicity"], + "simplicity_hl": { "version": "0.7.1" }, "utxo_types": { "last_will": { "description": "Funds locked under the last-will covenant.", "script": { "type": "simplicity", "source": "./last_will.simf", + "source_hash": "sha256:6673a57e6dfe818c21660fa7187c5f48905dd903e214efbf9c7fba5436b2a28a", "compile_params": { "INHERITOR_PUB_KEY": "INHERITOR_PUB_KEY", "HOT_PUB_KEY": "HOT_PUB_KEY", diff --git a/examples/lending_v2/txmanifest.json b/examples/lending_v2/txmanifest.json index f25f11d..ca07149 100644 --- a/examples/lending_v2/txmanifest.json +++ b/examples/lending_v2/txmanifest.json @@ -1,16 +1,17 @@ { "$schema": "../../schema/txmanifest.schema.json", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "simplicity-lending", "requires": ["simplicity"], - "simplicity_hl": { "debug_symbols": true }, + "simplicity_hl": { "version": "0.7.1", "debug_symbols": true }, "description": "P2P collateralised lending on Liquid, wire-compatible with the simplicity-lending reference implementation (github BlockstreamResearch/simplicity-lending, `smplx-sdk` covenants). This 'v2' manifest reproduces that protocol's exact on-chain transaction layout so offers created with tx-manifest-wallet are discoverable and settleable by simplicity-lending's own CLI / indexer / web app, and vice-versa. Key differences from the standalone 'lending' example: (1) the principal payout AND the borrower NFT both go to the borrower's plain wallet address (an explicit v0 P2WPKH), committed into the covenant as sha256(scriptPubKey) — there is no p2pk covenant and no separate claim step; (2) the borrower NFT is held in the borrower's wallet during the active loan and spent with an ordinary signature on repayment; (3) NFT ordering is first-params, second-params, borrower, lender; (4) the creation OP_RETURN carries borrower_pubkey || principal_asset_id (64 bytes, internal asset order) for indexer discovery; (5) a single AMOUNTS_DECIMALS drives the bit-packed parameter NFTs, matching the reference wallet.", "utxo_types": { "pre_lock": { "description": "Collateral held in the PreLockCovenant while the borrower's offer is open. PATH::LEFT (create_lending_path) lets a lender activate the loan; PATH::RIGHT (cancel_pre_lock_path) lets the borrower cancel with a Schnorr signature, burning all Utility NFTs. Address is byte-identical to simplicity-lending's pre_lock covenant for the same parameters.", "script": { "type": "simplicity", - "source": "./pre_lock.simf" + "source": "./pre_lock.simf", + "source_hash": "sha256:27ca19d440bf2def58ca0348814ca2214154e4b9206a2d75a7e866dbb9195ac9" }, "asset": "COLLATERAL_ASSET_ID" }, @@ -18,7 +19,8 @@ "description": "Collateral locked in the active LendingCovenant. Released via repayment (borrower returns principal+interest, reclaims collateral) or liquidation (lender claims collateral after LOAN_EXPIRATION_TIME).", "script": { "type": "simplicity", - "source": "./lending.simf" + "source": "./lending.simf", + "source_hash": "sha256:ea3eae41299583dfea56132f04e8eb3eda024c3d5c92b7970b22c2d12de65e5b" }, "asset": "COLLATERAL_ASSET_ID" }, @@ -27,6 +29,7 @@ "script": { "type": "simplicity", "source": "./script_auth.simf", + "source_hash": "sha256:ffe473ee77ac0b51d90f1b7f1b26d883a64b40e7276212aedf9f4bfadb54e8f7", "compile_params": { "SCRIPT_HASH": "PRE_LOCK_COV_HASH" } @@ -37,6 +40,7 @@ "script": { "type": "simplicity", "source": "./script_auth.simf", + "source_hash": "sha256:ffe473ee77ac0b51d90f1b7f1b26d883a64b40e7276212aedf9f4bfadb54e8f7", "compile_params": { "SCRIPT_HASH": "LENDING_COV_HASH" } @@ -47,6 +51,7 @@ "script": { "type": "simplicity", "source": "./asset_auth.simf", + "source_hash": "sha256:cfd27392a1571e2ee47e0399b5db0c5009ec2cdee11e0f0283a85d0b17e664b6", "compile_params": { "ASSET_ID": "LENDER_NFT_ASSET_ID", "ASSET_AMOUNT": "1", @@ -279,6 +284,7 @@ "LENDER_PRINCIPAL_COV_HASH": { "type": "tapleaf", "simf": "./asset_auth.simf", + "simf_hash": "sha256:cfd27392a1571e2ee47e0399b5db0c5009ec2cdee11e0f0283a85d0b17e664b6", "params": { "ASSET_ID": { "type": "liquid.asset_id", @@ -297,6 +303,7 @@ "LENDING_COV_HASH": { "type": "tapleaf", "simf": "./lending.simf", + "simf_hash": "sha256:ea3eae41299583dfea56132f04e8eb3eda024c3d5c92b7970b22c2d12de65e5b", "params": { "COLLATERAL_AMOUNT": { "type": "u64", @@ -348,6 +355,7 @@ "PARAMETERS_NFT_OUTPUT_SCRIPT_HASH": { "type": "tapleaf", "simf": "./script_auth.simf", + "simf_hash": "sha256:ffe473ee77ac0b51d90f1b7f1b26d883a64b40e7276212aedf9f4bfadb54e8f7", "params": { "SCRIPT_HASH": { "type": "bytes32", @@ -358,6 +366,7 @@ "PRE_LOCK_COV_HASH": { "type": "tapleaf", "simf": "./pre_lock.simf", + "simf_hash": "sha256:27ca19d440bf2def58ca0348814ca2214154e4b9206a2d75a7e866dbb9195ac9", "params": { "COLLATERAL_AMOUNT": { "type": "u64", @@ -424,6 +433,7 @@ "PRELOCK_PARAMETERS_NFT_SCRIPT_HASH": { "type": "tapleaf", "simf": "./script_auth.simf", + "simf_hash": "sha256:ffe473ee77ac0b51d90f1b7f1b26d883a64b40e7276212aedf9f4bfadb54e8f7", "params": { "SCRIPT_HASH": { "type": "bytes32", diff --git a/examples/lending_v3/txmanifest.json b/examples/lending_v3/txmanifest.json index 94b3cde..cdc72ec 100644 --- a/examples/lending_v3/txmanifest.json +++ b/examples/lending_v3/txmanifest.json @@ -1,10 +1,10 @@ { "$schema": "../../schema/txmanifest.schema.json", "$comment": "lending_v3 targets the DEPLOYED simplicity-lending indexer (odev). Interop constants are fixed: factory params (2,0), NUMS key, protocol-fee keeper 38fca2d9…, and the program-id tags below. Change a covenant .simf and you must recompute the matching *_PROGRAM_ID + covenant address.", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "simplicity-lending", "requires": ["simplicity"], - "simplicity_hl": { "debug_symbols": true }, + "simplicity_hl": { "version": "0.7.1", "debug_symbols": true }, "description": "lending_v3 — the redesigned 'issuance factory' lending protocol, wire-compatible with the deployed simplicity-lending indexer/site (odev branch). Phase 3a models factory creation: a persistent issuance_factory covenant plus the wallet-held auth NFT, from which many lending offers are later minted.", "utxo_types": { "issuance_factory": { @@ -12,6 +12,7 @@ "script": { "type": "simplicity", "source": "./issuance_factory.simf", + "source_hash": "sha256:dd1e7f89c6843291b8939d33c4f6ec57a88037726a8ecf40b76eeee024d24438", "compile_params": { "ISSUING_UTXOS_COUNT": "ISSUING_UTXOS_COUNT", "REISSUANCE_FLAGS": "REISSUANCE_FLAGS" @@ -24,6 +25,7 @@ "script": { "type": "simplicity", "source": "./lending.simf", + "source_hash": "sha256:f80c61629d295119a6ca6307f77d5cd5a0cd0c73e8a47a963901a08dba61b95c", "compile_params": { "COLLATERAL_ASSET_ID": "COLLATERAL_ASSET_ID", "PRINCIPAL_ASSET_ID": "PRINCIPAL_ASSET_ID", @@ -67,6 +69,7 @@ "script": { "type": "simplicity", "source": "./lending.simf", + "source_hash": "sha256:f80c61629d295119a6ca6307f77d5cd5a0cd0c73e8a47a963901a08dba61b95c", "compile_params": { "COLLATERAL_ASSET_ID": "COLLATERAL_ASSET_ID", "PRINCIPAL_ASSET_ID": "PRINCIPAL_ASSET_ID", @@ -110,6 +113,7 @@ "script": { "type": "simplicity", "source": "./asset_auth.simf", + "source_hash": "sha256:cfd27392a1571e2ee47e0399b5db0c5009ec2cdee11e0f0283a85d0b17e664b6", "compile_params": { "ASSET_ID": "BORROWER_NFT_ASSET_ID", "ASSET_AMOUNT": "1", @@ -123,6 +127,7 @@ "script": { "type": "simplicity", "source": "./asset_auth_vault.simf", + "source_hash": "sha256:3a1bbaa2e28fa84f2573fb06a90fcf450b401733009102cb878c612194be7d7f", "compile_params": { "VAULT_ASSET_ID": "PRINCIPAL_ASSET_ID", "KEEPER_AUTH_ASSET_ID": "LENDER_NFT_ASSET_ID", @@ -141,6 +146,7 @@ "script": { "type": "simplicity", "source": "./asset_auth_vault.simf", + "source_hash": "sha256:3a1bbaa2e28fa84f2573fb06a90fcf450b401733009102cb878c612194be7d7f", "compile_params": { "VAULT_ASSET_ID": "PRINCIPAL_ASSET_ID", "KEEPER_AUTH_ASSET_ID": "PROTOCOL_FEE_KEEPER_ASSET_ID", @@ -159,6 +165,7 @@ "script": { "type": "simplicity", "source": "./script_auth.simf", + "source_hash": "sha256:ffe473ee77ac0b51d90f1b7f1b26d883a64b40e7276212aedf9f4bfadb54e8f7", "compile_params": { "SCRIPT_HASH": "LENDING_COV_SCRIPT_HASH" } @@ -459,6 +466,7 @@ "FINALIZED_LENDER_VAULT_COV_HASH": { "type": "tapleaf", "simf": "./asset_auth_vault.simf", + "simf_hash": "sha256:3a1bbaa2e28fa84f2573fb06a90fcf450b401733009102cb878c612194be7d7f", "params": { "VAULT_ASSET_ID": { "type": "liquid.asset_id", @@ -497,6 +505,7 @@ "LENDER_VAULT_COV_HASH": { "type": "tapleaf", "simf": "./asset_auth_vault.simf", + "simf_hash": "sha256:3a1bbaa2e28fa84f2573fb06a90fcf450b401733009102cb878c612194be7d7f", "params": { "VAULT_ASSET_ID": { "type": "liquid.asset_id", @@ -535,6 +544,7 @@ "FINALIZED_PROTOCOL_FEE_VAULT_COV_HASH": { "type": "tapleaf", "simf": "./asset_auth_vault.simf", + "simf_hash": "sha256:3a1bbaa2e28fa84f2573fb06a90fcf450b401733009102cb878c612194be7d7f", "params": { "VAULT_ASSET_ID": { "type": "liquid.asset_id", @@ -573,6 +583,7 @@ "PROTOCOL_FEE_VAULT_COV_HASH": { "type": "tapleaf", "simf": "./asset_auth_vault.simf", + "simf_hash": "sha256:3a1bbaa2e28fa84f2573fb06a90fcf450b401733009102cb878c612194be7d7f", "params": { "VAULT_ASSET_ID": { "type": "liquid.asset_id", @@ -611,6 +622,7 @@ "PRINCIPAL_OUTPUT_SCRIPT_HASH": { "type": "tapleaf", "simf": "./asset_auth.simf", + "simf_hash": "sha256:cfd27392a1571e2ee47e0399b5db0c5009ec2cdee11e0f0283a85d0b17e664b6", "params": { "ASSET_ID": { "type": "liquid.asset_id", @@ -629,6 +641,7 @@ "LENDING_COV_SCRIPT_HASH": { "type": "tapleaf", "simf": "./lending.simf", + "simf_hash": "sha256:f80c61629d295119a6ca6307f77d5cd5a0cd0c73e8a47a963901a08dba61b95c", "params": { "COLLATERAL_ASSET_ID": { "type": "liquid.asset_id", diff --git a/examples/p2pk/txmanifest.json b/examples/p2pk/txmanifest.json index d33c9be..49b83ac 100644 --- a/examples/p2pk/txmanifest.json +++ b/examples/p2pk/txmanifest.json @@ -1,16 +1,18 @@ { "$schema": "../../schema/txmanifest.schema.json", - "manifest_version": "0.3.0", + "manifest_version": "0.3.1", "protocol": "p2pk-simplicity", "description": "Hello World — Pay-to-public-key using a Simplicity checksig program on Liquid.", "chain": "liquid", "requires": ["simplicity"], + "simplicity_hl": { "version": "0.7.1" }, "utxo_types": { "p2pk_output": { "description": "A Liquid UTXO locked to PUBKEY via the compiled p2pk.simf program.", "script": { "type": "simplicity", - "source": "./p2pk.simf" + "source": "./p2pk.simf", + "source_hash": "sha256:658bf994eac2f8dcbf5486787910513a1e4a79bd56f6e3c6c2a74dac25c0e2bf" }, "asset": "lbtc" } diff --git a/schema/txmanifest.schema.json b/schema/txmanifest.schema.json index 3ebd972..ddba907 100644 --- a/schema/txmanifest.schema.json +++ b/schema/txmanifest.schema.json @@ -597,8 +597,33 @@ "description": "Explicit param map for the simf. Each entry combines the value (a compile-param reference or string literal) with an optional manifest type hint. Omit entirely to pass ALL current compile params (auto-populate mode).", "type": "object" }, + "program": { + "description": "The name of an entry in [`Manifest::programs`]. Give this or `simf`.", + "type": [ + "string", + "null" + ] + }, "simf": { - "type": "string" + "description": "Path to the `.simf` file, relative to the manifest. Give this or `program`.", + "type": [ + "string", + "null" + ] + }, + "simf_hash": { + "description": "The content hash of `simf`, as in [`ProgramDef::hash`].", + "type": [ + "string", + "null" + ] + }, + "simplicity_hl_version": { + "description": "The SimplicityHL versions `simf` is written for.", + "type": [ + "string", + "null" + ] }, "type": { "enum": [ @@ -608,7 +633,6 @@ } }, "required": [ - "simf", "type" ], "type": "object" @@ -713,8 +737,33 @@ "null" ] }, + "program": { + "description": "The name of an entry in [`Manifest::programs`]. Give this or `simf`.", + "type": [ + "string", + "null" + ] + }, "simf": { - "type": "string" + "description": "Path to the `.simf` file, relative to the manifest. Give this or `program`.", + "type": [ + "string", + "null" + ] + }, + "simf_hash": { + "description": "The content hash of `simf`, as in [`ProgramDef::hash`].", + "type": [ + "string", + "null" + ] + }, + "simplicity_hl_version": { + "description": "The SimplicityHL versions `simf` is written for.", + "type": [ + "string", + "null" + ] }, "type": { "enum": [ @@ -724,7 +773,6 @@ } }, "required": [ - "simf", "type" ], "type": "object" @@ -771,9 +819,47 @@ ], "type": "object" }, + "ProgramDef": { + "additionalProperties": false, + "description": "One entry of [`Manifest::programs`]: a Simplicity program file and what pins it.", + "properties": { + "$comment": { + "description": "Documentation only; ignored by the engine.", + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "hash": { + "description": "The file's content hash, self-describing: `\"sha256:\"`, over the file's exact bytes. Optional while developing; a manifest with an unhashed program is *unpinned*, which wallets refuse.", + "type": [ + "string", + "null" + ] + }, + "simplicity_hl_version": { + "description": "The SimplicityHL versions this program is written for, as a semver requirement. Overrides [`SimplicityHl::version`].", + "type": [ + "string", + "null" + ] + }, + "source": { + "description": "Path to the `.simf` file, relative to the manifest.", + "type": "string" + } + }, + "required": [ + "source" + ], + "type": "object" + }, "SimplicityHl": { "additionalProperties": false, - "description": "SimplicityHL toolchain settings — how the `.simf` programs are compiled, as distinct from what the protocol does.\n\nDeliberately carries **no** compiler-version field. SimplicityHL has its own `simc \"\";` source directive, which the compiler enforces fail-fast before lexing, across the entry file and every reachable dependency — none of which a manifest key can do. Tooling that wants the requirement without compiling can read it via `version::SimcDirective::requirement_of`. Declaring it here as well would only create a second place to disagree.", + "description": "SimplicityHL toolchain settings — how the `.simf` programs are compiled, as distinct from what the protocol does.", "properties": { "$comment": { "description": "Documentation only; ignored by the engine.", @@ -790,6 +876,13 @@ "$ref": "#/definitions/UnstableFeatureName" }, "type": "array" + }, + "version": { + "description": "The SimplicityHL versions every program in this manifest is written for, as a semver requirement (`\"0.7.1\"` means `^0.7.1`, as in Cargo and in SimplicityHL's own `simc \"\";` directive).\n\nA per-program `simplicity_hl_version` overrides this, and this overrides a program's own `simc` directive. It can only *narrow* that directive, never widen it: the compiler still refuses a source whose directive it does not satisfy. What this adds is a requirement a wallet can check from the manifest alone, and one that covers sources which cannot be edited to carry a directive — a byte-exact copy of another protocol's program, say, where any added line would move every debug-symbol position and with it the program's address.", + "type": [ + "string", + "null" + ] } }, "type": "object" @@ -1038,7 +1131,29 @@ "null" ] }, + "program": { + "description": "The name of an entry in [`Manifest::programs`]. Give this or `source`.", + "type": [ + "string", + "null" + ] + }, + "simplicity_hl_version": { + "description": "The SimplicityHL versions `source` is written for, as in [`ProgramDef::simplicity_hl_version`].", + "type": [ + "string", + "null" + ] + }, "source": { + "description": "Path to the `.simf` file, relative to the manifest. Give this or `program`.", + "type": [ + "string", + "null" + ] + }, + "source_hash": { + "description": "The content hash of `source`, as in [`ProgramDef::hash`].", "type": [ "string", "null" @@ -1170,6 +1285,16 @@ "description": "Version of the manifest **format** this file is written against, as specified by ELIP-205 — not the version of any tool that reads it. Checked against [`FORMAT_VERSION`] at parse time; see [`check_format_version`] for the compatibility rule.", "type": "string" }, + "programs": { + "additionalProperties": { + "$ref": "#/definitions/ProgramDef" + }, + "description": "Named Simplicity programs, each pinned by hash and optionally by compiler version.\n\nOne of two ways to reference a program: a `utxo_type` script or a computed param names an entry here with `\"program\"`, instead of giving a `source` / `simf` path directly. Both forms may be mixed; see [`crate::programs`].", + "type": [ + "object", + "null" + ] + }, "protocol": { "type": "string" }, diff --git a/txmanifest_lib/Cargo.toml b/txmanifest_lib/Cargo.toml index 1a015cd..99304b0 100644 --- a/txmanifest_lib/Cargo.toml +++ b/txmanifest_lib/Cargo.toml @@ -33,6 +33,8 @@ simplicityhl = { git = "https://github.com/delta1/SimplicityHL", branch = "bitco "serde", ] } schemars = "0.8" +# Program version requirements; already in the tree via simplicityhl. +semver = "1" [dev-dependencies] jsonschema = { version = "0.17", default-features = false } diff --git a/txmanifest_lib/src/assembly.rs b/txmanifest_lib/src/assembly.rs index ee577da..36cdb7b 100644 --- a/txmanifest_lib/src/assembly.rs +++ b/txmanifest_lib/src/assembly.rs @@ -148,7 +148,7 @@ pub trait AssemblyContext { /// never be hoisted into the shared assembly. fn covenant_script_pubkey( &self, - simf_path: &std::path::Path, + program: &crate::programs::ProgramSource, compile_params: &std::collections::HashMap, type_hints: &std::collections::HashMap, extra_leaf_payloads: &[Vec], @@ -208,14 +208,14 @@ impl AssemblyContext for ElementsContext<'_> { fn covenant_script_pubkey( &self, - simf_path: &std::path::Path, + program: &crate::programs::ProgramSource, compile_params: &std::collections::HashMap, type_hints: &std::collections::HashMap, extra_leaf_payloads: &[Vec], opts: &CompileOpts, ) -> Result