Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and

### Changed

- `validate` errors when a manifest uses a Simplicity program in a `tapleaf` or `simf_fn` compute, or a `programs` entry, without declaring `"requires": ["simplicity"]`. Previously only covenant `utxo_types` triggered this. Add `"simplicity"` to `requires` if `validate` now reports it.
- `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
Expand Down
5 changes: 3 additions & 2 deletions docs/decisions/0001-program-pinning.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ different compilers could in principle produce different programs from the same
- 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).
- refused for publishing (`validate --strict` errors) and by wallets, which must not
run an unpinned manifest. `run` refuses by default; the library's `Unpinned::Allow`
exists only for development tooling such as `--allow-unpinned`.

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
Expand Down
17 changes: 14 additions & 3 deletions docs/format-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ anyone implementing the format in a wallet. Changes to the `tx-manifest-wallet`
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
version is `0`, a different minor version is a different, incompatible format. A patch
version only adds things, so a newer reader accepts an older patch's manifests (a `0.3.1`
reader reads `0.3.0`). Not the reverse: an older reader rejects fields it doesn't know,
rather than ignoring them. 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.
Additive, with one tightened rule (below): a `0.3.0` manifest is a valid `0.3.1` manifest
unless it uses a Simplicity program without declaring `simplicity`.

### Added

Expand Down Expand Up @@ -46,6 +49,14 @@ Additive: every `0.3.0` manifest is a valid `0.3.1` manifest.
reference that gives neither inherits them from the others.
- A `program` must name an entry in `programs`.

### Changed

- `"requires": ["simplicity"]` is required by any use of a Simplicity program, not only by
covenant `utxo_types`: also a `tapleaf` or `simf_fn` compute, or a `programs` entry.
Each of these needs a compiler, and `requires` is how a wallet without one knows to
refuse the manifest before trying it. A manifest that relied on the narrower rule was
already unrunnable by such a wallet; it now fails validation instead.

## [0.3.0] - 2026-10-06

### Breaking
Expand Down
27 changes: 19 additions & 8 deletions txmanifest_lib/src/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1411,7 +1411,9 @@ pub fn run(
};
let (leaf_payloads, inp_params, inp_hints) =
(site.leaf_payloads, site.compile_params, site.type_hints);
let inp_simf_path = programs.for_script(inp_ut.script.as_ref())?;
let inp_simf_path = programs
.for_script(inp_ut.script.as_ref())
.with_context(|| format!("utxo_type '{type_name}'"))?;
let script_pubkey = match actx.covenant_script_pubkey(
inp_simf_path,
&inp_params,
Expand Down Expand Up @@ -1811,7 +1813,9 @@ pub fn run(
};
let (leaf_payloads, out_params, out_hints) =
(site.leaf_payloads, site.compile_params, site.type_hints);
let out_simf_path = programs.for_script(ut.script.as_ref())?;
let out_simf_path = programs
.for_script(ut.script.as_ref())
.with_context(|| format!("utxo_type '{type_name}'"))?;
let script_pubkey = match actx.covenant_script_pubkey(
out_simf_path,
&out_params,
Expand Down Expand Up @@ -2213,7 +2217,10 @@ pub fn run(
action,
&ctx,
)?;
let program = programs.for_script(ut.script.as_ref())?.clone();
let program = programs
.for_script(ut.script.as_ref())
.with_context(|| format!("utxo_type '{type_name}'"))?
.clone();
Ok(CovenantSpendSpec {
program,
params: site.compile_params,
Expand Down Expand Up @@ -2436,8 +2443,9 @@ pub fn run(
for inp in &covenant_inputs {
let type_name = inp.utxo_type_name().unwrap();
let check_ut = manifest.utxo_type(&type_name).ok();
let check_simf_path =
programs.for_script(check_ut.as_ref().and_then(|ut| ut.script.as_ref()))?;
let check_simf_path = programs
.for_script(check_ut.as_ref().and_then(|ut| ut.script.as_ref()))
.with_context(|| format!("utxo_type '{type_name}'"))?;
let check_site = check_ut.map(|ut| {
resolve_utxo_site(
ut,
Expand Down Expand Up @@ -2558,8 +2566,9 @@ pub fn run(
dry_site.compile_params,
dry_site.type_hints,
);
let dry_simf_path =
programs.for_script(dry_ut.script.as_ref())?;
let dry_simf_path = programs
.for_script(dry_ut.script.as_ref())
.with_context(|| format!("utxo_type '{type_name}'"))?;

use std::io::Write;
print!(
Expand Down Expand Up @@ -2736,7 +2745,9 @@ pub fn run(
fin_site.compile_params,
fin_site.type_hints,
);
let fin_simf_path = programs.for_script(fin_ut.script.as_ref())?;
let fin_simf_path = programs
.for_script(fin_ut.script.as_ref())
.with_context(|| format!("utxo_type '{type_name}'"))?;

print!(
" {} Input '{}' ({}) — finalizing… ",
Expand Down
17 changes: 13 additions & 4 deletions txmanifest_lib/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,10 @@ fn normalize_compute_value<E: serde::de::Error>(
// parse error naming the field rather than a failure much later in a run.
let kind = obj.get("type").and_then(|t| t.as_str()).unwrap_or_default();
if matches!(kind, "tapleaf" | "simf_fn") {
match (obj.contains_key("simf"), obj.contains_key("program")) {
// `null` counts as absent: serde reads it as `None`, so a `"simf": null` that
// passed here would leave the compute naming no program at all.
let given = |key: &str| obj.get(key).is_some_and(|v| !v.is_null());
match (given("simf"), given("program")) {
(true, true) => {
return Err(E::custom(format!(
"a {kind} compute gives both \"simf\" and \"program\"; give one"
Expand Down Expand Up @@ -1666,16 +1669,22 @@ impl Manifest {
/// `requires` claims.
///
/// Only the residue that [`Manifest::chain`] does not settle, which today means: does
/// any `utxo_type` carry a `script`. Namespaced capabilities are never inferred — this
/// crate does not know what they mean, so only the author can say one is needed.
/// anything need a Simplicity compiler. That is any `utxo_type` with a `script`, and
/// any other program reference — a `tapleaf` or `simf_fn` compute, or a `programs`
/// entry — since computing a tapleaf hash compiles the program just as deriving a
/// covenant address does. A wallet without a compiler relies on this to refuse such a
/// manifest up front rather than fail partway through. Namespaced capabilities are
/// never inferred — this crate does not know what they mean, so only the author can
/// say one is needed.
pub fn inferred_capabilities(&self) -> Capabilities {
let mut caps = Capabilities::none();
let uses_covenants = self
.utxo_types
.iter()
.flatten()
.any(|(_, t)| t.script.is_some());
if uses_covenants {
let (program_refs, _) = crate::programs::references(self);
if uses_covenants || !program_refs.is_empty() {
caps.insert(Capability::SIMPLICITY);
}
caps
Expand Down
Loading
Loading