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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.git
target
**/target
**/config.toml
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/target
/core/target
*.dev.*
/config.toml
/service/config.toml
12 changes: 10 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ If this is just a bug report or a feature request, please open an issue first so

## Running the Service Locally

First follow the service's [key-provisioning instructions](service/README.md#starting-the-service).
The repository no longer supplies an active configuration or a usable signing key.

```bash
cd service
cargo run --quiet -- start
Expand Down Expand Up @@ -60,10 +63,15 @@ 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
docker run --rm -p 30431:30431 \
--mount type=bind,src=/absolute/path/to/service-config.toml,dst=/app/config.toml,readonly \
simplicity-unchained-service
```

The container runs as a non-root user (`uid=1000`). Ensure mounted config files are readable by that user.
Provision the host configuration outside the Docker build context with a unique
signing key; see the service [Docker instructions](service/README.md#docker).
The container runs as a non-root user (`uid=1000`). Ensure the private mounted
configuration is readable by that user. An unconfigured container exits unsuccessfully.

## Reporting Bugs

Expand Down
3 changes: 3 additions & 0 deletions Cargo.lock

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

18 changes: 9 additions & 9 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,18 @@ RUN apt-get update && apt-get install -y \
WORKDIR /build

# Copy workspace manifests
COPY Cargo.toml rust-toolchain.toml ./
COPY Cargo.toml Cargo.lock rust-toolchain.toml ./

# Copy all workspace members (required by Cargo workspace)
COPY cli ./cli
COPY core ./core
COPY service ./service
COPY service/Cargo.toml ./service/Cargo.toml
COPY service/src ./service/src
COPY jet_plugin_abi ./jet_plugin_abi
COPY plugin_tests ./plugin_tests

# Build the service in release mode
RUN cargo build --release --package service
RUN cargo build --locked --release --package service

# Stage 2: Runtime
FROM debian:bookworm-slim
Expand All @@ -41,20 +44,17 @@ WORKDIR /app
# Copy the compiled binary from builder
COPY --from=builder /build/target/release/service /app/service

# Copy the configuration file
COPY service/config.toml /app/config.toml

# Change ownership to the service user
RUN chown -R service:service /app

# Switch to non-root user
USER service

# Expose the service port (default: 8080)
EXPOSE 8080
# Expose the example configuration's service port
EXPOSE 30431

# Set the entrypoint
ENTRYPOINT ["/app/service"]

# Default command arguments
# Requires an operator-provided read-only configuration mount
CMD ["start", "--config", "/app/config.toml"]
48 changes: 47 additions & 1 deletion cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Commands:

- `address multisig` — Generate a P2TR 2-of-2 multisig address from two public keys and a user leaf hash.

Pass the updated service `/tweak` response's `tweaked_public_key_hex` as
`--pubkey1`, not the untweaked service key or an old CMR-only tweaked key.
When using script-reading jets, include the intended `redeem_script_hex` in the
`/tweak` request before funding and supply the identical bytes to `/sign/psbt`
or `/sign/pset`. Use that same tweaked key for `--cosigner-pubkey` when signing
or spending the recovery leaf. The demo scripts omit the script in both
requests, consistently committing to an empty script.

### keypair

- `keypair generate` — Generate a new secp256k1 keypair.
Expand All @@ -37,15 +45,53 @@ Commands:
### btc-tx (Bitcoin)

- `btc-tx create` — Create a PSBT from UTXOs.
- `btc-tx approval-hash` — Compute the digest an approver must sign for a Bitcoin `tx_approval_hash` policy.
- `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.

### Bitcoin Transaction Approvals

Use a program that explicitly verifies the approver's BIP-340 signature against
`jet::tx_approval_hash()`. Build it with the updated core `CoreExtension` jet table.
Choose the approver public key and script data before generating and funding the
address.

For each spend, populate `witness_utxo` for **every** PSBT input, independently
verify the prevouts, review the transaction, and calculate its approval digest:

```bash
cargo run --quiet -- btc-tx approval-hash \
--psbt "$PSBT_HEX" \
--input-index 0 \
--program "$PROGRAM" \
--redeem-script-hex "$REDEEM_SCRIPT_HEX" \
--network testnet4
```

Run this from the `cli` directory. Omit `--redeem-script-hex` for an empty script.
`--network` is required and must match the service's configured `bitcoin_network`
(`bitcoin`, `testnet`, or `testnet4`), even for a local test deployment.

The JSON response contains `approval_hash_hex` and `cmr_hex`, both 64-character
hex strings. Have the approver sign the **32 decoded approval-hash bytes**, not
the ASCII hex text, using BIP-340. Encode the signature into the program's witness
and submit the same PSBT, input, program and script to `/sign/psbt`. Recompute and
obtain a fresh approval after changing the transaction or prevouts.

The approval digest is not the co-signer's Taproot sighash. See the
[versioned digest definition](../core/README.md#bitcoin-per-spend-approvals).
Static hashlocks and fixed-message signature policies retain their intentionally
replayable behavior; existing funded static policies are not upgraded by this
command.

## Demo

First, ensure the Simplicity Unchained service is running:
First, provision a private configuration and unique signing key following the
service's [startup instructions](../service/README.md#starting-the-service).
Then ensure the Simplicity Unchained service is running:

```bash
cd service
Expand Down
1 change: 1 addition & 0 deletions cli/assets/check_opcode.simf
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fn main() {
// Commit to the intended script via /tweak before funding, and use the same script when signing.
// Test that the opcode extraction works correctly
//
// You can read any opcode from a script and build an additional branching logic on it.
Expand Down
1 change: 1 addition & 0 deletions cli/assets/sig_verify_with_pubkey_from_script.simf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//
// You can utilize pubkeys embedded in scripts to verify signatures against them.
// Or do other cryptographic operations that require pubkeys.
// Commit to the intended script via /tweak before funding, and use the same script when signing.
fn main() {
let signature: [u8; 64] = 0x3d5e45a7e5e98964e523b19f460534ad68c2ace2056d6891fc2851bc88eb4a65ce0f7e69fb47fa2105d073df7619a2eaedff9c2685a5862c8c88eb13aa174bc2;

Expand Down
2 changes: 1 addition & 1 deletion cli/scripts/demo_elements.sh
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ echo "User signature added"
echo

echo "==== Step 8: Finalize ===="
FINALIZE_DATA=$(cargo run --quiet -- tx finalize --pset "$PSET_SIGNED2")
FINALIZE_DATA=$(cargo run --quiet -- tx finalize --pset "$PSET_SIGNED2" --network "$NETWORK")
FINAL_TX_HEX=$(echo "$FINALIZE_DATA" | jq -r '.transaction_hex')
echo "TXID: $(echo "$FINALIZE_DATA" | jq -r '.txid')"
echo
Expand Down
33 changes: 33 additions & 0 deletions cli/src/commands/btc_tx/approval_hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use std::str::FromStr;

use anyhow::{Context, Result};
use serde_json::json;
use simplicity::{bitcoin::psbt::Psbt, elements::Script};
use simplicity_unchained_core::{
BitcoinNetwork, jets::bitcoin::CoreExtension, program::Program,
runner::bitcoin_execution_environment,
};

pub fn execute(
psbt_hex: &str,
input_index: usize,
program: &str,
redeem_script_hex: Option<&str>,
network: &str,
) -> Result<()> {
let psbt = Psbt::deserialize(&hex::decode(psbt_hex).context("Failed to decode PSBT hex")?)
.context("Failed to deserialize PSBT")?;
let program = Program::<CoreExtension>::from_str(program, None).context("Invalid program")?;
let script = Script::from(
hex::decode(redeem_script_hex.unwrap_or("")).context("Invalid redeem script hex")?,
);
let network = BitcoinNetwork::from_str(network).map_err(anyhow::Error::msg)?;
let env = bitcoin_execution_environment(&psbt, input_index, program.cmr(), &script, network)
.context("Failed to construct approval context")?;
let output = json!({
"approval_hash_hex": hex::encode(env.approval_hash),
"cmr_hex": hex::encode(program.cmr().to_byte_array()),
});
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
Loading
Loading