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
396 changes: 396 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

13 changes: 6 additions & 7 deletions Cargo.lock

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

25 changes: 25 additions & 0 deletions contrib/regtest/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Bitcoin Core fork with Simplicity activated on regtest.
# Node only: no GUI, no wallet, no tests — `generatetoaddress` and `scantxoutset`
# are node RPCs, so nothing we need requires the wallet.
FROM debian:bookworm-slim AS build
RUN apt-get update && apt-get install -y --no-install-recommends \
git ca-certificates cmake ninja-build g++ pkg-config \
libevent-dev libboost-dev python3 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
RUN git clone --depth 1 --branch simplicity-inquisition \
https://github.com/delta1/bitcoin.git .
RUN cmake -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_GUI=OFF -DENABLE_WALLET=OFF \
-DBUILD_TESTS=OFF -DBUILD_BENCH=OFF -DBUILD_FUZZ_BINARY=OFF
RUN cmake --build build -j"$(nproc)" --target bitcoind bitcoin-cli

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libevent-2.1-7 libevent-extra-2.1-7 libevent-pthreads-2.1-7 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /src/build/bin/bitcoind /usr/local/bin/
COPY --from=build /src/build/bin/bitcoin-cli /usr/local/bin/
EXPOSE 18443 18444
ENTRYPOINT ["bitcoind"]
126 changes: 126 additions & 0 deletions contrib/regtest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Simplicity regtest

A local Bitcoin regtest with Simplicity active, for exercising the Bitcoin path without a
faucet and without waiting on a public network that has not activated the soft fork.

Built from [`delta1/bitcoin@simplicity-inquisition`](https://github.com/delta1/bitcoin/tree/simplicity-inquisition),
which is the implementation proposed in BINANA 2026-0003.

## Build and run

```sh
docker build -t simplicity-regtest contrib/regtest
docker run -d --name simplicity-regtest -p 18443:18443 simplicity-regtest \
-regtest -server -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 \
-rpcuser=tx -rpcpassword=manifest -fallbackfee=0.0001 -txindex=1
```

Confirm the rules are on. `SIMPLICITY` appears in `script_flags`, not in `deployments` —
it is enabled for the chain rather than being deployed through BIP9:

```sh
docker exec simplicity-regtest bitcoin-cli -regtest -rpcuser=tx -rpcpassword=manifest \
getdeploymentinfo | head -20
```

```
"script_flags": [ "ANYPREVOUT", "CHECKSIGFROMSTACK", "CHECKTEMPLATEVERIFY",
"INTERNALKEY", "NULLDUMMY", "OP_CAT", "P2SH", "SIMPLICITY",
"TAPROOT", "TEMPLATEHASH", "WITNESS" ]
```

## Helpers

Two scripts, sharing one definition of the container and the burn address so they cannot
disagree about either:

| | |
|---|---|
| `./contrib/regtest/faucet.sh --wallet <file> [--utxos N]` | fund a wallet with spendable coins |
| `./contrib/regtest/mine.sh [N] [--txid <id>]` | confirm transactions, advance the chain |

Both take `--config` where a wallet's network has to be resolved, and honour
`FAUCET_CONTAINER` / `FAUCET_RPCUSER` / `FAUCET_RPCPASS`.

`mine.sh` mines to an unspendable address by default, so advancing the chain never quietly
adds coins to a wallet under test — `faucet.sh` is the one that gives you money. Passing
`--txid` reports that transaction afterwards, and distinguishes "not yet mined" from "never
reached the mempool", which look identical until something tells you which:

```
$ ./contrib/regtest/mine.sh --txid 6655a5a2…
✓ mined 1 block(s) — height 1344 → 1345
✓ 6655a5a22a174239… confirmed (2 confirmation(s))
out[0] 0.0015 BTC -> bcrt1p5z45vylh6vue39806mze8wl7z360ynn0uhxd8cnr5p4swe6n5gts4pf74z
out[1] 0.38912192 BTC -> bcrt1p5chl5z5t268jja3p6rpknkxsdskxqv7fwxc6d0umhmkgzwyl0qqq0ssl0u
```

## Point the wallet at it

```json
{
"default_network": "bitcoin-regtest",
"bitcoin_backend": "rpc",
"bitcoin_rpc_url": "http://127.0.0.1:18443",
"bitcoin_rpc_auth": "tx:manifest",
"simplicity_activated": true
}
```

The node backend rather than Esplora: an Esplora instance for a four-block chain means an
indexer and an API server alongside the node, and `scantxoutset` finds our coins with no
wallet, no import and no rescan.

## End to end

```sh
export TX_MANIFEST_DATA_DIR=/tmp/txm-regtest # keeps this off your real config
mkdir -p "$TX_MANIFEST_DATA_DIR"
cat > "$TX_MANIFEST_DATA_DIR/config.json" <<'JSON'
{ "default_network": "bitcoin-regtest",
"bitcoin_backend": "rpc",
"bitcoin_rpc_url": "http://127.0.0.1:18443",
"bitcoin_rpc_auth": "tx:manifest",
"simplicity_activated": true }
JSON

tx-manifest-wallet create-wallet --out "$TX_MANIFEST_DATA_DIR/wallet.json"
tx-manifest-wallet info --wallet "$TX_MANIFEST_DATA_DIR/wallet.json" # copy the receive address
```

Fund it:

```sh
./contrib/regtest/faucet.sh --config "$TX_MANIFEST_DATA_DIR/config.json" \
--wallet "$TX_MANIFEST_DATA_DIR/wallet.json" --utxos 3
```

That mines the coins you asked for to the wallet, then mines 100 more to an unspendable
address to mature them — so you get exactly three spendable UTXOs and nothing else. Mining
201 blocks to your own address works too, but leaves a hundred immature outputs the wallet
has to skip and every later scan has to walk.

Then run an action. `--export-pset` writes the signed transaction instead of broadcasting,
which is the easy way to inspect it first:

```sh
tx-manifest-wallet run manifest.json Pay \
--wallet "$TX_MANIFEST_DATA_DIR/wallet.json" \
--data-dir "$TX_MANIFEST_DATA_DIR" \
--params params.json \
--export-pset signed.json

docker exec simplicity-regtest bitcoin-cli -regtest -rpcuser=tx -rpcpassword=manifest \
testmempoolaccept "[\"$(jq -r .tx_hex signed.json)\"]"
```

Drop `--export-pset` to broadcast instead; the wallet prompts before it sends.

The image is built without wallet support, which is fine: `generatetoaddress` and
`scantxoutset` are node RPCs.

## What works against this today

Plain payments. Covenant spending needs a SimplicityHL build with the Bitcoin jet hinter,
which this crate does not yet pin — the node will execute Simplicity, but the wallet cannot
yet compile a program for it. See `covenant::jet_hinter`.
42 changes: 42 additions & 0 deletions contrib/regtest/_common.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Shared plumbing for the regtest helpers. Sourced, not executed.
#
# Extracted rather than copied into each script: these two must agree about which container
# they talk to, how they report a failure, and where they mine throwaway blocks. Two copies
# agree on the day they are written.

CONTAINER="${FAUCET_CONTAINER:-simplicity-regtest}"
RPCUSER="${FAUCET_RPCUSER:-tx}"
RPCPASS="${FAUCET_RPCPASS:-manifest}"

# A P2TR output paying the BIP341 NUMS point — a key nobody holds. Blocks mined here are
# real blocks whose reward is unspendable by anyone, which is what makes it safe to mine
# hundreds of them just to advance the chain.
BURN="bcrt1p2zffkaxp5py4fdutfdsrt6t6tcrc5ks09rkfd428hlhf4n5q8tqq5az5cr"

die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
note() { printf '\033[2m%s\033[0m\n' "$*"; }
ok() { printf '\033[32m✓\033[0m %s\n' "$*"; }

cli() {
docker exec "$CONTAINER" bitcoin-cli -regtest \
-rpcuser="$RPCUSER" -rpcpassword="$RPCPASS" "$@"
}

# Fail before doing anything, with the command that fixes it.
require_node() {
docker inspect "$CONTAINER" >/dev/null 2>&1 \
|| die "container '$CONTAINER' not found. Start it:
docker run -d --name $CONTAINER -p 18443:18443 simplicity-regtest \\
-regtest -server -rpcbind=0.0.0.0 -rpcallowip=0.0.0.0/0 \\
-rpcuser=$RPCUSER -rpcpassword=$RPCPASS -fallbackfee=0.0001 -txindex=1"
[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER")" = "true" ] \
|| die "container '$CONTAINER' exists but is not running: docker start $CONTAINER"
cli getblockcount >/dev/null 2>&1 \
|| die "cannot reach bitcoind in '$CONTAINER' over RPC"
}

# The header comment block, for --help. Reads the comment rather than a line range, so it
# cannot drift out of step with what it documents.
print_help() {
awk 'NR>1 { if ($0 ~ /^#/) { sub(/^# ?/, ""); print } else exit }' "$1"
}
88 changes: 88 additions & 0 deletions contrib/regtest/faucet.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# Fund a wallet on the local Simplicity regtest.
#
# Mining is how a regtest wallet gets money, and a block reward cannot be spent until it is
# 100 blocks deep. Mining 101 blocks to your own address therefore leaves you one spendable
# coin and a hundred you have to wait on — and every later scan has to walk all of them.
#
# So this mines the coins you asked for to your address, then matures them by mining 100
# blocks to a throwaway address instead. You end up with exactly the UTXOs you wanted, all
# spendable, and nothing extra in your wallet.
#
# ./contrib/regtest/faucet.sh --wallet /tmp/txm-regtest/wallet.json
# ./contrib/regtest/faucet.sh --wallet w.json --utxos 5
# ./contrib/regtest/faucet.sh --address bcrt1p...
#
set -euo pipefail
# Resolved before the cd, or `$0` stops naming this file.
SELF="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
. "$(dirname "$SELF")/_common.sh"

MATURITY=100

wallet="" address="" config="" utxos=1


while [ $# -gt 0 ]; do
case "$1" in
--wallet) wallet="${2:-}"; shift 2 ;;
--address) address="${2:-}"; shift 2 ;;
--config) config="${2:-}"; shift 2 ;;
--utxos) utxos="${2:-}"; shift 2 ;;
-h|--help) print_help "$SELF"; exit 0 ;;
*) die "unknown argument: $1 (try --help)" ;;
esac
done


# --- checks, before anything is mined ---------------------------------------
require_node

case "$utxos" in (*[!0-9]*|"") die "--utxos must be a whole number, got '$utxos'" ;; esac
[ "$utxos" -ge 1 ] || die "--utxos must be at least 1"

# --- resolve the address ----------------------------------------------------
if [ -n "$address" ] && [ -n "$wallet" ]; then
die "pass --wallet or --address, not both"
elif [ -z "$address" ]; then
[ -n "$wallet" ] || die "need --wallet <file> or --address <addr> (try --help)"
[ -f "$wallet" ] || die "wallet file not found: $wallet"
# Derived through the wallet itself rather than read from the file, so the address is
# the one the wallet will actually scan for — including the network it is configured on.
note "deriving receive address from $wallet…"
cfg_args=()
[ -n "$config" ] && cfg_args=(--config "$config")
address=$(cargo run -q -p tx-manifest-wallet -- "${cfg_args[@]}" \
info --wallet "$wallet" 2>/dev/null \
| awk '/Receive Address/{getline; gsub(/ /,""); print; exit}')
[ -n "$address" ] || die "could not derive an address. Is the config pointing at a Bitcoin network?
Try: cargo run -p tx-manifest-wallet -- ${config:+--config $config }info --wallet $wallet"
fi

case "$address" in
bcrt1*) ;;
*) die "'$address' is not a regtest address (expected bcrt1…). Check the config's default_network." ;;
esac

# --- mine -------------------------------------------------------------------
before=$(cli getblockcount)
echo
echo "Funding $address"
note " $utxos coin(s), then $MATURITY blocks to mature them"

cli generatetoaddress "$utxos" "$address" >/dev/null
cli generatetoaddress "$MATURITY" "$BURN" >/dev/null
after=$(cli getblockcount)

ok "mined $((after - before)) blocks (height $before → $after)"

# --- report -----------------------------------------------------------------
spk=$(cli validateaddress "$address" | sed -n 's/.*"scriptPubKey": "\([0-9a-f]*\)".*/\1/p')
total=$(cli scantxoutset start "[{\"desc\":\"raw($spk)\"}]" \
| sed -n 's/.*"total_amount": \([0-9.]*\).*/\1/p')
ok "address now holds ${total:-0} BTC, all spendable"
echo
note "check it with:"
note " cargo run -p tx-manifest-wallet -- ${config:+--config $config }sync --wallet ${wallet:-<wallet>}"
echo
Loading
Loading