Skip to content
Draft
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
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,42 @@ released together.

No changelog was kept before 0.2.0; for 0.1.x see the git history.

## [Unreleased]

### Added

- **Messages inside rangeproofs.** A confidential output may carry
`rangeproof_embed`: plain text (with `params.X` / `instance.X` references
resolved), raw bytes in the OP_RETURN `data` dialect, or a nostr event signed
at build time. Elements uses only the first 64 bytes of a rangeproof's message
field, so ≈3125 bytes per output are free — the proof is the same size either
way, so neither the transaction's weight nor its fee changes, and the message
is readable only by the holder of the output's blinding key.
- **Signed nostr events.** `rangeproof_embed.nostr` takes the unsigned event
fields (`kind`, `content`, `tags`, `created_at`); the engine fills in `pubkey`,
`id` and `sig` so a relay will accept what lands on chain. `sign_with` names
which wallet key signs — `"wallet"`, `"oracle"`, or a BIP32 path — so the
manifest never carries a secret and the nostr identity is the key's x-only
pubkey. The build report and the clear-signing preview both show the event's
content and its signing key before broadcast.
- **`read-messages` command.** Reads embedded messages back out of the wallet's
own confidential outputs from persisted state, rendering a payload as a nostr
event when it is one. Only wallet-owned outputs are readable: rewinding a
rangeproof needs the output's blinding key, which is the gate that makes the
message private.
- The frame is byte-compatible with `liquidrangeproof` / `liquid-nostr-bridge`,
so their reader and the bridge relay can read outputs written here.

### Changed

- The engine's own blinding pass now also runs when an output carries a
rangeproof message, not only when it pins blinding factors: Elements'
`blind_last` writes the 64-byte message itself and offers no way to extend it.
Rather than rewind and replace a finished proof, the builder signs the longer
message on the first pass — which means an embed can ride on an output paying
someone *else's* confidential address, since the receiver's blinding key is
never needed.

## [0.2.0] - 2026-08-20

**Breaking:** a manifest that sets `utxo_type.confidential` no longer parses.
Expand Down
76 changes: 76 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ manifest-wallet/
│ ├── eval.rs # expression evaluator (amounts, formulas, references)
│ ├── prepare.rs # UTXO pre-funding / splitting
│ ├── pset_builder.rs # PSET construction
│ ├── rangeproof.rs # messages embedded in a confidential output's rangeproof
│ ├── nostr_embed.rs # signing a nostr event for such a message
│ ├── validate.rs # static manifest schema checks
│ ├── describe.rs # interactive manifest explorer
│ ├── wallet.rs # key management & signing
Expand All @@ -43,7 +45,8 @@ manifest-wallet/
├── deadcat/ # binary prediction market with on-chain oracle resolution
├── deadcat_v2/ # …unblinded tokens — a documented dead end
├── deadcat_v3/ # …derivable blinding factors; the runnable fork
└── last_will/ # time-locked inheritance
├── last_will/ # time-locked inheritance
└── rangeproof_message/ # messages (and signed nostr events) inside rangeproofs
```

## How a manifest works
Expand All @@ -63,6 +66,50 @@ See [`examples/p2pk/txmanifest.json`](examples/p2pk/txmanifest.json) for a minim
example, or [`examples/lending/txmanifest.json`](examples/lending/txmanifest.json)
for a full multi-action covenant protocol.

### Messages inside rangeproofs

Elements signs every confidential output's value rangeproof over an author-supplied
message, uses only the first 64 bytes of it (asset id ‖ asset blinding factor), and
discards the rest. Those spare bytes are recovered verbatim by a rewind, are readable
only by the holder of the output's blinding key, and — because `min_bits = 52` fixes
the ring count — cost nothing: **≈3125 bytes per output, at no change in transaction
size or fee.**

Any confidential output may carry one, via `rangeproof_embed`:

```jsonc
// plain text; params.X / instance.X references are resolved first
"rangeproof_embed": "hello from inside a rangeproof"
"rangeproof_embed": { "message": "params.note" }

// raw bytes, in the same dialect as an OP_RETURN `data` field
"rangeproof_embed": { "data": { "parts": [ { "type": "u64", "value": "params.seq" } ] } }

// a nostr event, signed at build time with a key this wallet derives
"rangeproof_embed": { "nostr": {
"kind": 1,
"content": "params.content",
"tags": [["t", "liquid"]],
"sign_with": "wallet" // or "oracle", or a BIP32 path
} }
```

For the nostr form the engine fills in `pubkey`, `id` and `sig`, so what lands on chain
is a complete NIP-01 event a relay will accept. The manifest never carries a secret:
`sign_with` names a wallet key, and the resulting nostr identity is that key's x-only
pubkey — the one `info` prints. Signing at the authoring end is what makes a bridge that
republishes these events a transport rather than an authority.

Read them back with `read-messages` (wallet-owned outputs only — the blinding key is the
gate). The frame is byte-compatible with
[`liquidrangeproof`](https://github.com/stringhandler/liquidrangeproof) and the
`liquid-nostr-bridge` relay, so outputs written here can be read by those tools.

A message only exists on a *confidential* output. `validate` rejects one on a change,
OP_RETURN, burn, fee or `script_hash` destination, or on an output that sets
`"confidential": false`, rather than silently dropping it. See
[`examples/rangeproof_message/txmanifest.json`](examples/rangeproof_message/txmanifest.json).

## Building

```sh
Expand Down Expand Up @@ -128,6 +175,7 @@ cargo run -- run examples/p2pk/txmanifest.json Pay --wallet wallet.json
| `info` | Show wallet fingerprint, xpub, oracle key, and a receive address. |
| `sync` | Sync wallet state against an Esplora server and show balance. |
| `get-balance` | Show last-synced balance (no network call). |
| `read-messages` | Read messages embedded in the rangeproofs of the wallet's confidential outputs. |
| `split` | Split a wallet asset into N equal UTXOs. |
| `config` | Show or update configuration (`default_network`, `default_esplora`). |

Expand Down
3 changes: 3 additions & 0 deletions examples/rangeproof_message/data/params.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recipient": "tlq1qqdea9qzfk70hqw3ypnlvefmz2dwrhmu9f08dw0uu4qm58n8y0ptun9tsmmrr9ns5kve4crrd3ulnl78tkw256j9hseufz6aux"
}
Loading