diff --git a/CHANGELOG.md b/CHANGELOG.md index caa94e6..eaeceb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Preserve LNURL-pay invoice millisatoshi precision by creating invoices with LND `value_msat` instead of truncating callback amounts to sats ### Added +- Add an optional deterministic Trezor emulator and Bridge fixture for isolated regtest wallet projects. - Add an opt-in LNURL-pay regtest fixture with switchable invoice callback errors for payment retry journeys - Add Pubky marketplace test fixture behind the `marketplace` compose profile: Pubky testnet, Paykit Server `722ef268` built from pinned source, and the `pubky-marketplace` driver CLI (`up`, `seed`, `purchase`, `mine`, `verify`, `seller-auth`) for the Bitkit marketplace wallet journey with a headless or Bitkit-approved seller; see `docs/pubky-marketplace.md` - Add opt-in rc56 Payment Request fixture services for linked-peer and deadline-history wallet journeys diff --git a/README.md b/README.md index 790b206..6fe8b86 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,47 @@ with the app's identity or an app build that supports importing fixture state. #### Trezor Hardware PRs +For isolated Linux or Docker-backed simulator projects, use the optional +`trezor-emulator` profile. It builds a named image from the pinned official User +Env, starts Bridge, and initializes the deterministic T2T1 device automatically: + +```bash +docker compose --profile trezor-emulator build trezor-emulator +docker compose --profile trezor-emulator up -d --wait trezor-emulator +curl -fsS -X POST http://127.0.0.1:21325/enumerate +docker compose --profile trezor-emulator exec -T trezor-emulator \ + /trezor-user-env/.venv/bin/python3 /opt/bitkit-trezor/trezor-fixture-check.py +``` + +Point the simulator's Bridge URL at `http://127.0.0.1:21325`. The dashboard is at +`http://127.0.0.1:9005`, the controller at `ws://127.0.0.1:9004`, and noVNC at +`http://127.0.0.1:6080`. For projects that remap ports, use their published Bridge +port (or the simulator seat's forwarded address). The device uses the `all all +...` seed, no PIN or passphrase, and the label `Bitkit Test Trezor`. Each project's +volumes are separate, and each startup wipes and initializes its own emulator. +The image's digest fixes both the bundled firmware and Bridge; startup downloads +nothing. Health requires setup to finish and both Bridge and the emulator to run. + +The diagnostic checks the deterministic public key, a `bcrt1` address, and +Bridge signing of a synthetic transaction with an independently verified +signature. It uses the debug link to approve only that diagnostic's prompts; +run it while no wallet uses the device. It needs no app or live chain and +does not broadcast a transaction. + +Run the existing `bitcoind` and `electrs` services for funding and broadcasting +regtest transactions. The emulator signs `Regtest` transactions without needing +its own chain backend. To control button confirmations through the debug link: + +```bash +docker compose --profile trezor-emulator exec -T trezor-emulator \ + /trezor-user-env/.venv/bin/python3 /opt/bitkit-trezor/trezor-controller.py \ + send-json '{"type":"emulator-press-yes"}' +``` + +See [docs/trezor-emulator.md](docs/trezor-emulator.md) for fixture diagnostics and +the separate manual User Env workflow below. Stop only this profile's service +with `docker compose --profile trezor-emulator stop trezor-emulator`. + Use this section as the entry point when checking Bitkit app PRs or merged features that need the official Trezor emulator. Start by preparing the deterministic Trezor User Env: ```bash diff --git a/docker-compose.yml b/docker-compose.yml index 5ba2678..6c8841f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -315,6 +315,40 @@ services: ports: - "${HOMEGATE_PORT:-6288}:6288" + # Disposable, automatically initialized emulator for isolated test projects. + # Its firmware and Bridge come from the same digest as the manual User Env. + trezor-emulator: + profiles: [trezor-emulator] + image: bitkit-docker/trezor-emulator:fixture-v1 + build: + context: ./scripts + dockerfile: trezor-fixture.Dockerfile + restart: "no" + environment: + MACOS: "1" # use published ports rather than physical USB / host networking + TREZOR_MODEL: T2T1 + TREZOR_FIRMWARE: 2-main # frozen in the pinned base image + TREZOR_BRIDGE_VERSION: node-bridge + TREZOR_MNEMONIC: all all all all all all all all all all all all + TREZOR_LABEL: Bitkit Test Trezor + TREZOR_WIPE: "true" + ports: + - "127.0.0.1:21325:21325" # Bridge legacy API used by Bitkit + - "127.0.0.1:21328:21328" # Bridge current API + - "127.0.0.1:9004:9001" # controller; avoids marketplace port 3001 seat blocks + - "127.0.0.1:9005:9002" # dashboard + - "127.0.0.1:6080:6080" # emulator screen / noVNC + volumes: + - trezor_emulator_state:/var/tmp + - trezor_emulator_logs:/trezor-user-env/logs + healthcheck: + test: ["CMD", "/trezor-user-env/.venv/bin/python3", "/opt/bitkit-trezor/trezor-fixture.py", "--health"] + interval: 5s + timeout: 20s + retries: 6 + start_period: 120s + stop_grace_period: 20s + trezor-user-env-mac: image: *trezor_user_env_image ports: @@ -500,6 +534,8 @@ services: BITCOIN_RPC_URL: http://bitcoind:43782 volumes: + trezor_emulator_state: + trezor_emulator_logs: bitcoin_home: postgres_data: lnurl_auth_data: diff --git a/docs/trezor-emulator.md b/docs/trezor-emulator.md index 3160a61..2063aa2 100644 --- a/docs/trezor-emulator.md +++ b/docs/trezor-emulator.md @@ -1,5 +1,51 @@ # Trezor Emulator Checks +## Isolated automatic fixture + +The `trezor-emulator` Compose profile provides a Linux-compatible service with +published ports, a reusable named image, project-scoped state and logs, and a +healthcheck. It uses the same pinned official User Env image as the manual +workflow, including its bundled SDL3 runtime. Startup launches the +controller and initializes Bridge plus the deterministic T2T1 device, with bounded +startup waits and process cleanup on exit. Firmware `2-main` and `node-bridge` +refer to binaries already frozen in the base image's digest. + +```bash +docker compose --profile trezor-emulator up -d --build --wait trezor-emulator +docker compose --profile trezor-emulator ps trezor-emulator +docker compose --profile trezor-emulator logs --tail 80 trezor-emulator +curl -fsS -X POST http://127.0.0.1:21325/enumerate +docker compose --profile trezor-emulator exec -T trezor-emulator \ + /trezor-user-env/.venv/bin/python3 /opt/bitkit-trezor/trezor-fixture-check.py +``` + +Ports: Bridge legacy `21325`, Bridge current `21328`, controller `9004`, dashboard +`9005`, and noVNC `6080`. The controller and dashboard are published on different +ports from the manual User Env to avoid collisions with the regtest services when +several projects use blocks of 1000 ports. Docker-backed simulator seats remap +these base ports and forward them to their seat's loopback address. Declare this +profile as an on-demand offer; run no global shared device instance. + +Every start wipes only its own emulator and loads the public `all all ...` test +seed, no PIN, no passphrase and the `Bitkit Test Trezor` label. Its two named +volumes belong to the Compose project. Bridge enumeration must contain a device, +and controller status must report both emulator and Bridge running, before the +container is healthy. Confirmations remain controlled by the dashboard or +controller so the runner can observe and approve signing prompts. + +`trezor-fixture-check.py` is a bounded, offline fixture diagnostic. It talks to +the device through Bridge, derives the expected public key independently from +the public test seed, obtains regtest addresses, signs a synthetic SegWit +transaction and verifies its signature independently. The debug link approves +its signing prompts and restores the normal interaction mode afterwards. Run +it before connecting an app; it neither runs an app journey nor broadcasts. + +The fixture does not run its own Bitcoin stack. Use the existing regtest node and +Electrum server to fund the derived address and broadcast signed transactions. +Avoid starting the manual User Env alongside this profile on the same base ports. + +## Manual User Env + Bitkit app PRs that need Trezor hardware behavior can use the official Trezor User Env through this repo. The helper starts the User Env without its extra regtest stack, starts Trezor Bridge, wipes a deterministic T2T1 emulator, and configures it with a stable seed and label. ## Start the Emulator diff --git a/scripts/trezor-fixture-check.py b/scripts/trezor-fixture-check.py new file mode 100644 index 0000000..1f39a89 --- /dev/null +++ b/scripts/trezor-fixture-check.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Check the fixture through Bridge without an app or a live chain. + +Uses the public test seed and a synthetic previous transaction. Debug-link +approval is confined to this check; normal signing still needs a runner's input. +""" + +import hashlib +import hmac +import json +import signal +import struct + +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, utils +from trezorlib import btc, messages +from trezorlib.debuglink import TrezorTestContext +from trezorlib.tools import parse_path +from trezorlib.transport.bridge import BridgeTransport +from trezorlib.transport.udp import UdpTransport + + +def sha256d(data): + return hashlib.sha256(hashlib.sha256(data).digest()).digest() + + +def hash160(data): + return hashlib.new("ripemd160", hashlib.sha256(data).digest()).digest() + + +def compressed_public_key(secret): + key = ec.derive_private_key(secret, ec.SECP256K1()).public_key().public_numbers() + return bytes([2 + key.y % 2]) + key.x.to_bytes(32, "big") + + +def expected_public_key(path): + seed = hashlib.pbkdf2_hmac("sha512", b"all all all all all all all all all all all all", b"mnemonic", 2048) + material = hmac.digest(b"Bitcoin seed", seed, "sha512") + secret, chain = int.from_bytes(material[:32], "big"), material[32:] + order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 + for index in path: + parent = b"\0" + secret.to_bytes(32, "big") if index & 0x80000000 else compressed_public_key(secret) + material = hmac.digest(chain, parent + struct.pack(">I", index), "sha512") + secret = (secret + int.from_bytes(material[:32], "big")) % order + chain = material[32:] + return compressed_public_key(secret) + + +def timeout(signum, frame): + raise TimeoutError("fixture check exceeded 90 seconds") + + +def main(): + signal.signal(signal.SIGALRM, timeout) + signal.alarm(90) + transports = BridgeTransport.enumerate() + if len(transports) != 1: + raise RuntimeError(f"expected one emulator, got {len(transports)}") + path = parse_path("m/84h/1h/0h/0/0") + destination_path = parse_path("m/84h/1h/0h/0/1") + with transports[0], TrezorTestContext(transports[0], debug_transport=UdpTransport("127.0.0.1:21325")) as context: + with context.get_session() as session: + features = session.features + assert features.initialized and features.label == "Bitkit Test Trezor" + assert not features.pin_protection and not features.passphrase_protection + public_key = btc.get_public_node(session, path, coin_name="Regtest").node.public_key + assert public_key == expected_public_key(path), "device seed differs from the deterministic fixture" + address = btc.get_address(session, "Regtest", path, script_type=messages.InputScriptType.SPENDWITNESS) + destination = btc.get_address(session, "Regtest", destination_path, script_type=messages.InputScriptType.SPENDWITNESS) + assert address.startswith("bcrt1") and destination.startswith("bcrt1") + witness_script = b"\0\x14" + hash160(public_key) + previous = messages.TransactionType( + version=1, lock_time=0, + inputs=[messages.TxInputType(prev_hash=b"\0" * 32, prev_index=0xFFFFFFFF, script_sig=b"fixture", sequence=0xFFFFFFFF)], + bin_outputs=[messages.TxOutputBinType(amount=1_000_000, script_pubkey=witness_script)], + ) + raw_previous = ( + struct.pack(" 150 + # Independently verify the returned signature using the BIP143 digest. + outpoint = previous_hash[::-1] + b"\0" * 4 + output = struct.pack("