diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f2ebba9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Report a reproducible bug +title: "bug: " +labels: bug +assignees: "" +--- + +## Description + + + +## Steps to reproduce + + + +## Expected behaviour + + + +## Actual behaviour + + + +## Environment + +- **OS**: +- **Rust version**: +- **Commit / tag**: +- **Affected crate(s)**: + +## Logs + + + +```plaintext +paste here +``` + +## Additional context + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..763d64a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/BlockstreamResearch/simplicity-unchained/security/advisories/new + about: Please report security issues privately via GitHub's security advisory feature rather than opening a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..987dd29 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature request +about: Propose a new feature or improvement +title: "feat: " +labels: enhancement +assignees: "" +--- + +## Problem / motivation + + + +## Proposed solution + + + +## Affected crate(s) + + + +## Alternatives considered + + + +## Additional context + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..cdd3453 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,39 @@ +# Summary + + + +## Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor +- [ ] Documentation +- [ ] Dependency update +- [ ] Other (describe below) + +## Affected crates + +- [ ] `core` +- [ ] `jet_plugins` +- [ ] `service` +- [ ] `cli` +- [ ] `plugin_tests` +- [ ] CI / tooling + +## Checklist + +- [ ] `cargo fmt --all` passes +- [ ] `cargo clippy --workspace --all-targets -- -D warnings` passes +- [ ] `cargo test --workspace --all-targets` passes + +## Breaking changes + + + +## Testing + + + +## Related issues + + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..391dafe --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Contributing to Simplicity Unchained + +Contributions of all kinds are welcome — bug reports, documentation improvements, and tests. + +## Project Structure + +The workspace is organised into focused crates. Keep changes scoped to the appropriate crate and discuss cross-crate changes in an issue first. + +- [`core`](core/README.md): Simplicity execution engine, Bitcoin/Elements environments, and the dynamic jet loading API. +- [`jet_plugins`](jet_plugins/README.md): Procedural macro that derives the `Jet` trait and C FFI for custom jet plugins. +- [`service`](service/README.md): Axum HTTP service exposing the sign/PSBT, sign/PSET, and key-tweak endpoints. +- [`cli`](cli/README.md): `clap`-based CLI for interacting with the service and accessing ecosystem utilities. +- `plugin_tests/`: Example and integration-test jet plugins for Bitcoin, Elements, and opcode pubkey variants. + +## Issues + +If this is just a bug report or a feature request, please open an issue first so it can be discussed. PRs for bug fixes are also welcome and can be discussed directly in the PR. However, for larger changes, it is better to open an issue first to discuss the proposed change before implementing it. + +### Pull Requests + +1. Fork the repository and create a branch from `development`. +2. Make your changes, including tests where applicable. +3. Ensure CI checks pass. +4. Open a PR against `development` describing what was changed and why. +5. Keep PRs focused, one logical change per PR. Split large changes into sequential PRs if needed. + +## Coding Conventions + +- Run `cargo fmt` before every commit. +- The project targets zero `clippy` warnings with `-D warnings`. Fix all warnings introduced by your changes. +- Use `thiserror` for library errors. Avoid `unwrap()` outside of tests and examples. +- Keep the public API surface minimal. Only `pub` what downstream crates genuinely need, and add `///` doc comments to all new public items. +- Adding a new dependency requires justification. Prefer `[workspace.dependencies]` entries and avoid floating version requirements. +- Add unit tests under a `#[cfg(test)]` module in the same file. + +## Adding Custom Jets + +Custom jets are the primary extension point. To add a new jet plugin: + +1. Create a new crate that depends on `jet_plugins` and `simplicity_unchained_core`, declare it as a `cdylib`: + + ```toml + # Cargo.toml + [lib] + crate-type = ["cdylib"] + ``` + +2. Define your jet functions matching the expected signature: + + ```rust + fn my_jet(_dst: &mut CFrameItem, _src: CFrameItem, _env: &BitcoinUnchainedEnv) -> bool { + // return true on success + todo!() + } + ``` + +3. Register them with the `register_jets!` macro: + + ```rust + use jet_plugins::register_jets; + use simplicity_unchained_core::jets::environments::BitcoinUnchainedEnv; + use hal_simplicity::simplicity::ffi::CFrameItem; + + register_jets!( + hal_simplicity::simplicity::jet::Core, // base jet set + BitcoinUnchainedEnv, // environment type + "my_jet_name" => my_jet, b"h", b"h", // (name, fn, source_type, target_type) + ); + ``` + +4. Pass the compiled `.so`/`.dylib` path to the core runner via the dynamic loading API. See `core/src/jets/jet_dyn.rs` and `cli/assets/custom_jet_dlls/` for reference. + +## Running the Service Locally + +```bash +cd service +cargo run --quiet -- start +``` + +The service reads `config.toml` from the current directory by default. You can point it at a custom file with `--config`: + +```bash +cargo run --quiet -- start --config path/to/your/config.toml +``` + +To exercise CLI commands against the running service, use the demo scripts: + +```bash +# Bitcoin regtest demo +cd cli && ./scripts/demo_btc.sh + +# Elements / Liquid testnet demo +cd cli && ./scripts/demo_elements.sh +``` + +## Docker + +The multi-stage `Dockerfile` builds the `service` binary. To build and run: + +```bash +docker build -t simplicity-unchained-service . +docker run -p 8080:8080 simplicity-unchained-service +``` + +The container runs as a non-root user (`uid=1000`). Ensure mounted config files are readable by that user. + +## Reporting Bugs + +Open a GitHub issue including: + +1. What happened vs. what you expected. +2. A minimal command sequence or code snippet to reproduce the issue. +3. Your OS, Rust version (`rustc --version`), and commit hash. +4. Relevant error output with sensitive data redacted. + +For security-sensitive issues, please follow responsible disclosure and contact the maintainers privately before opening a public issue. + +## Licence + +By contributing to this project you agree that your contributions are released under the [CC0 1.0 Universal](LICENCE) licence, the same licence that covers the project. diff --git a/README.md b/README.md index b150b3e..de27077 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ The primary target is **Bitcoin** (including Bitcoin testnets), with additional > > This project is still in active development and conceptualization. It is **not safe for use in production environments** at this time. Features and functionality may change, and there may be critical bugs or incomplete implementations. +## How It Works + +See [core/README.md](core/README.md) for a detailed explanation of the unchained model, the taproot tree structure, and the co-signing flow. + ## Project Structure - [`core`](core/README.md): Contains the fundamental logic for executing Simplicity programs, interacting with Bitcoin and Elements environments and interface for loading custom jet implementations from dynamic libraries. (Note: In the future, parts of this logic may be migrated to [hal-simplicity](https://github.com/BlockstreamResearch/hal-simplicity)) @@ -30,7 +34,7 @@ A live testnet demo is hosted at **[testnet.simplicity-unchained.blockstream.com ### Bitcoin -You can explore the Bitcoin capabilities by running the [Bitcoin demo script](cli/scripts/demo_bitcoin.sh). This demo guides you through: +You can explore the Bitcoin capabilities by running the [Bitcoin demo script](cli/scripts/demo_btc.sh). This demo guides you through: 1. Tweaking a public key with a Simplicity program via the Simplicity Unchained service. 2. Creating a 2-of-2 multisig P2TR address on Bitcoin regtest. @@ -40,7 +44,7 @@ You can explore the Bitcoin capabilities by running the [Bitcoin demo script](cl ### Elements / Liquid -The [Elements demo script](cli/scripts/demo.sh) demonstrates the same 2-of-2 multisig co-signing flow on the Liquid Testnet. +The [Elements demo script](cli/scripts/demo_elements.sh) demonstrates the same 2-of-2 multisig co-signing flow on the Liquid Testnet. To run either demo, follow the instructions in the [CLI readme](cli/README.md). diff --git a/cli/README.md b/cli/README.md index c3cd858..a7bc18b 100644 --- a/cli/README.md +++ b/cli/README.md @@ -4,46 +4,84 @@ Command-line interface for communicating with the Simplicity Unchained service a ## Commands -You can easily access command descriptions and information about available arguments by using the `--help` flag with any command. There is no need to duplicate this information here. - -```man -A CLI tool for Simplicity Unchained utilities +Use the `--help` flag with any command or subcommand for the full list of arguments. +```plaintext Usage: simplicity-unchained Commands: - address Address operations - keypair Keypair operations - tx Transaction operations - help Print this message or the help of the given subcommand(s) - -Options: - -h, --help Print help + address Address operations + keypair Keypair operations + tx Transaction operations (Elements / Liquid) + btc-tx Transaction operations (Bitcoin) + help Print this message or the help of the given subcommand(s) ``` -## Demo +### address + +- `address multisig` — Generate a P2TR 2-of-2 multisig address from two public keys and a user leaf hash. + +### keypair + +- `keypair generate` — Generate a new secp256k1 keypair. -You can run the [demo script](scripts/demo.sh) to see example usages of the CLI commands and an example of Simplicity Unchained capabilities. +### tx (Elements / Liquid) -Service supports P2SH, P2WSH and P2TR transaction types. +- `tx create` — Create a PSET from UTXOs. +- `tx sign` — Sign a PSET with one secret key (for local co-signing). +- `tx finalize` — Finalize a PSET into a broadcastable transaction. +- `tx build-csv-leaf` — Build a CSV recovery leaf script from a user pubkey and timelock. +- `tx spend-user-leaf` — Spend the user leaf (Leaf 1) of a P2TR output independently. +- `tx finalize-user-leaf` — Finalize a user leaf PSET into a broadcastable transaction. -Firstly, ensure you have the Simplicity Unchained service running. +### btc-tx (Bitcoin) + +- `btc-tx create` — Create a PSBT from UTXOs. +- `btc-tx sign` — Sign a PSBT with one secret key (for local co-signing). +- `btc-tx finalize` — Finalize a PSBT into a broadcastable transaction. +- `btc-tx build-csv-leaf` — Build a CSV recovery leaf script from a user pubkey and timelock. +- `btc-tx spend-user-leaf` — Spend the user leaf (Leaf 1) of a P2TR output independently. +- `btc-tx finalize-user-leaf` — Finalize a user leaf PSBT into a broadcastable transaction. + +## Demo + +First, ensure the Simplicity Unchained service is running: ```bash cd service cargo run --quiet -- start ``` -Then, in a separate terminal, navigate to the `cli` directory and execute the demo script: +Then, in a separate terminal, navigate to the `cli` directory and run one of the demo scripts: + +### Bitcoin (regtest) + +Requires a running `bitcoind` in regtest mode and `bitcoin-cli` in your `PATH`: + +```bash +bitcoind -regtest -fallbackfee=0.0002 -txindex=1 -daemon +bitcoin-cli -regtest loadwallet +``` + +```bash +cd cli +./scripts/demo_btc.sh # 2-of-2 co-signing via Simplicity program +./scripts/demo_btc_user_leaf.sh # CSV recovery leaf spend +``` + +### Elements / Liquid Testnet + +Requires `curl` and `jq` in your `PATH`. Transactions are broadcast to Liquid Testnet via the Blockstream API. ```bash cd cli -./scripts/demo.sh p2sh/p2wsh/p2tr +./scripts/demo_elements.sh # 2-of-2 co-signing via Simplicity program +./scripts/demo_elements_user_leaf.sh # CSV recovery leaf spend ``` -> ⚠️ **Warning: Too many requests** +> ⚠️ **Warning: Liquid Testnet Faucet rate limit** > -> The demo script interacts with the Liquid Testnet Faucet, which imposes a rate limit of 3 requests per minute per IP address. If you exceed this limit, the script may fail. If this happens, please wait at least one minute before running the script again. +> The Elements demo scripts interact with the Liquid Testnet Faucet, which imposes a rate limit. If the script fails due to rate limiting, wait at least one minute before retrying. ## Licence diff --git a/cli/assets/custom_jet_dlls/libbitcoin.so b/cli/assets/custom_jet_dlls/libbitcoin.so index 3912ef1..dfd68a5 100755 Binary files a/cli/assets/custom_jet_dlls/libbitcoin.so and b/cli/assets/custom_jet_dlls/libbitcoin.so differ diff --git a/cli/assets/custom_jet_dlls/libelements.so b/cli/assets/custom_jet_dlls/libelements.so index ac57a64..93210dc 100755 Binary files a/cli/assets/custom_jet_dlls/libelements.so and b/cli/assets/custom_jet_dlls/libelements.so differ diff --git a/cli/assets/custom_jet_dlls/libopcode_pubkey_bitcoin.so b/cli/assets/custom_jet_dlls/libopcode_pubkey_bitcoin.so index 5ff24a2..649c8c6 100755 Binary files a/cli/assets/custom_jet_dlls/libopcode_pubkey_bitcoin.so and b/cli/assets/custom_jet_dlls/libopcode_pubkey_bitcoin.so differ diff --git a/cli/assets/custom_jet_dlls/libopcode_pubkey_elements.so b/cli/assets/custom_jet_dlls/libopcode_pubkey_elements.so index 5b34fcd..13bb11c 100755 Binary files a/cli/assets/custom_jet_dlls/libopcode_pubkey_elements.so and b/cli/assets/custom_jet_dlls/libopcode_pubkey_elements.so differ diff --git a/core/README.md b/core/README.md index cccca21..e89206c 100644 --- a/core/README.md +++ b/core/README.md @@ -9,6 +9,48 @@ Contains the fundamental logic for executing Simplicity programs and interacting - Custom execution environment supporting the injection of custom jets. - Provides API for loading custom Jet trait implementation derived via jet_plugins crate via dynamic library. +## How It Works + +### The Unchained Model + +The core idea is to bind a Simplicity program to a co-signer's key using taproot key tweaking, without requiring Simplicity to be a consensus rule on-chain. The oracle commits to a specific program at address-generation time; it can only produce a valid signature if that exact program executes successfully. + +1. The user writes a Simplicity program encoding the conditions they want enforced (e.g. a signature check, a hash preimage, an oracle assertion). +2. The service computes the **CMR** (Commitment Merkle Root) of the program — a 32-byte hash that uniquely identifies the program's structure and semantics. +3. The service tweaks its own internal key with the CMR using the standard taproot tweak: `tweaked_key = tap_tweak(service_key, CMR)`. This binds the co-signer's key irrevocably to that program. +4. A P2TR address is constructed using the tweaked co-signer key and the user's key (see below). Funds sent to this address can only move with the oracle's cooperation — and the oracle will only cooperate if the Simplicity program passes. + +### Taproot Tree Structure + +Each P2TR address produced by Simplicity Unchained has the following structure: + +```plaintext +P2TR output +├── Internal key: unspendable key +└── Script tree + ├── Leaf 0 — Co-signing leaf (2-of-2 multisig) + │ OP_CHECKSIG + │ OP_CHECKSIGADD OP_2 OP_EQUAL + └── Leaf 1 — User recovery leaf (CSV + user key, hidden from the service) +``` + +**Internal key**: A provably unspendable key is used as the internal key. + +**Leaf 0 — Co-signing leaf**: A 2-of-2 Schnorr multisig script. Both the co-signer and the user must sign. The co-signer's x-only key embedded here is the CMR-tweaked service key, so this leaf is only spendable with signatures from a service that has already executed the matching Simplicity program successfully. + +**Leaf 1 — User recovery leaf**: A user-controlled script (example is a CSV timelock + user key) that allows the user to recover funds unilaterally after the timelock expires, with no involvement from the service. The service receives only the *hash* of this leaf (`user_leaf_hash`) and never sees the script itself, preserving the user's privacy for the recovery path. + +### Co-signing Flow + +When a user wants to spend from the address: + +1. The user constructs a PSBT/PSET spending transaction and submits it to the service along with the Simplicity program, any required witness data, their public key, and the user leaf hash. +2. The service executes the Simplicity program against the transaction environment. +3. If execution succeeds, the service re-derives the tweaked co-signer key from the CMR, reconstructs the taproot tree (co-signing leaf + hidden user leaf), computes the script-path sighash for Leaf 0, and returns a Schnorr signature. +4. The user adds their own signature for the same leaf and finalizes the transaction. + +If the Simplicity program fails, the service refuses to sign and the funds remain locked. + ## Licence See the [LICENCE](../LICENCE) file for details. diff --git a/jet_plugins/README.md b/jet_plugins/README.md index 213e80f..aebd959 100644 --- a/jet_plugins/README.md +++ b/jet_plugins/README.md @@ -1,7 +1,9 @@ # Jet Plugins + Procedural macros for deriving the `Jet` trait for custom jets. -## Capabilities +## Capabilities + Given a set of base jets and custom jets, this crate derives the `Jet` trait alongside a C FFI for dynamic jet loading. Current Limitations: @@ -11,6 +13,7 @@ Current Limitations: - Does not support deriving traits over arbitrary jet sets (other than Bitcoin and Elements). This is due to API limitations within certain internal components of rust-simplicity. ## Usage example + ```rust use jet_plugins::register_jets; use simplicity_unchained_core::jets::environments::ElementsUnchainedEnv; @@ -29,4 +32,4 @@ register_jets!( "custom_jet1" => custom_jet1, b"h", b"h", // source/target type "custom_jet2" => custom_jet2, b"h", b"h", // source/target type ); -``` \ No newline at end of file +``` diff --git a/service/README.md b/service/README.md index 5158bef..a17640f 100644 --- a/service/README.md +++ b/service/README.md @@ -23,8 +23,8 @@ By default, service configuration looks for a `config.toml` file in the current ```toml [service] # Port on which the service will run -port = 8080 -# secpr256k1 32-byte hex-encoded private key, used for signing +port = 30431 +# secp256k1 32-byte hex-encoded private key, used for signing # this is a demo key, do not use it anywhere else private_key = "804622cda0d8e634317a12651d91751ceff5c081f2b5f63ef7912725c7275e5d" # Network to get metadata from: liquid or liquidtestnet @@ -59,9 +59,11 @@ The service exposes the following endpoints: { "psbt_hex": "70736274ff...", "input_index": 0, - "redeem_script_hex": "5221...", "program": "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA", - "witness": "" + "user_pubkey": "02...", + "user_leaf_hash_hex": "a1b2c3...", + "witness": null, + "redeem_script_hex": null } ``` @@ -69,9 +71,11 @@ The service exposes the following endpoints: - `psbt_hex`: The PSBT (Partially Signed Bitcoin Transaction) encoded as a hexadecimal string - `input_index`: The index of the transaction input to sign (must be between 0 and 65535) -- `redeem_script_hex`: The redeem script in hexadecimal format, used for SegWit v0 signature computation (optional, can be an empty string) - `program`: The Simplicity program to execute for validation before signing -- `witness`: The witness data required by the Simplicity program (optional, can be an empty string) +- `user_pubkey`: The user's compressed public key in hexadecimal format +- `user_leaf_hash_hex`: The user leaf hash in hexadecimal format, used to reconstruct the taproot tree +- `witness`: The witness data required by the Simplicity program (optional) +- `redeem_script_hex`: The redeem script in hexadecimal format, used for SegWit v0 signature computation (optional) **Success Response**: @@ -102,9 +106,11 @@ The service exposes the following endpoints: { "pset_hex": "70736574ff...", "input_index": 0, - "redeem_script_hex": "5221...", "program": "zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA", - "witness": "" + "user_pubkey": "02...", + "user_leaf_hash_hex": "a1b2c3...", + "witness": null, + "redeem_script_hex": null } ``` @@ -112,9 +118,11 @@ The service exposes the following endpoints: - `pset_hex`: The PSET (Partially Signed Elements Transaction) encoded as a hexadecimal string - `input_index`: The index of the transaction input to sign (must be between 0 and 65535) -- `redeem_script_hex`: The redeem script in hexadecimal format, used for SegWit v0 signature computation (can be an empty string) - `program`: The Simplicity program to execute for validation before signing -- `witness`: The witness data required by the Simplicity program (can be an empty string) +- `user_pubkey`: The user's compressed public key in hexadecimal format +- `user_leaf_hash_hex`: The user leaf hash in hexadecimal format, used to reconstruct the taproot tree +- `witness`: The witness data required by the Simplicity program (optional) +- `redeem_script_hex`: The redeem script in hexadecimal format, used for SegWit v0 signature computation (optional) **Success Response**: @@ -140,7 +148,7 @@ The service exposes the following endpoints: `POST /simplicity-unchained/tweak`: Accepts a Simplicity program, computes its CMR (commitment Merkle root), and returns the tweaked public key using taproot key tweaking. -### **Note** +### **Note** In P2TR case, key, tweaked with Simplicity CMR, used as inner key. @@ -156,7 +164,7 @@ In P2TR case, key, tweaked with Simplicity CMR, used as inner key. **Request Fields**: - `program`: The Simplicity program to compute the CMR from (must be a non-empty string) -- `jet_env`: The jet environment to use, either `elements` or `bitcoin` +- `jet_env`: The jet environment to use, either `elements` or `bitcoin` (optional, defaults to `elements`) **Success Response**: