diff --git a/Cargo.toml b/Cargo.toml index b07e83f1..ff670c8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace] resolver = "3" members = ["crates/*"] -exclude = ["examples/basic", "fixtures"] +exclude = ["examples/basic", "examples/last_will", "fixtures"] [workspace.package] license = "MIT" diff --git a/README.md b/README.md index f95a4418..0a335f1c 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ To view the available options, run the help command: simplex -h ``` -### Example +### Examples Check out the complete project examples in the `examples` directory to learn more. diff --git a/examples/last_will/.gitignore b/examples/last_will/.gitignore new file mode 100644 index 00000000..327555cd --- /dev/null +++ b/examples/last_will/.gitignore @@ -0,0 +1 @@ +src/artifacts diff --git a/examples/last_will/Cargo.lock b/examples/last_will/Cargo.lock new file mode 100644 index 00000000..d6f453f7 --- /dev/null +++ b/examples/last_will/Cargo.lock @@ -0,0 +1,2037 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ar_archive_writer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cd58deff2140a0a8eae87e417bd01db68a33e148aa93d1e8cd837e55e312b6" +dependencies = [ + "object", +] + +[[package]] +name = "ariadne" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31beedec3ce83ae6da3a79592b3d8d7afd146a5b15bb9bb940279aced60faa89" +dependencies = [ + "unicode-width", + "yansi", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048006790404103c488e6c5616b3e7e702a71292aadda08e8f7698a9e0d2eb53" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bfbe32ac36b3c428696ce315464181bc4029d689fd58e3f2a4e58c252242f52" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "base58ck" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" +dependencies = [ + "bitcoin_hashes 0.14.101", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bip39" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4559bb9225f8401dcea601cf8dc9ed81ea0dadd9cf7755fdefdd93ef7961e19e" +dependencies = [ + "bitcoin_hashes 0.14.101", + "rand 0.8.8", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bit-set" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d87354e4229f54a44f7bf2435906a4656dba36026ab6eaca629a2c436a691c" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5727b15fa97d4f4fee0a3b7c3d550ed0269f54329207b86388de918604e31269" +dependencies = [ + "borsh", + "serde", +] + +[[package]] +name = "bitcoin" +version = "0.32.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" +dependencies = [ + "base58ck", + "base64 0.21.7", + "bech32", + "bitcoin-consensus-encoding", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes 0.14.101", + "hex-conservative 0.2.3", + "hex_lit", + "secp256k1", + "serde", +] + +[[package]] +name = "bitcoin-consensus-encoding" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f" +dependencies = [ + "bitcoin-internals 0.7.0", + "hex-conservative 1.3.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-internals" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8bea3a9f0cfece4564e37cb49a38cc245ca5184719e50d7d0dda3268722c4e2" + +[[package]] +name = "bitcoin-io" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] + +[[package]] +name = "bitcoin-private" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" + +[[package]] +name = "bitcoin-units" +version = "0.1.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" +dependencies = [ + "bitcoin-consensus-encoding", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.3", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals 0.6.0", + "hex-conservative 1.3.0", + "serde", +] + +[[package]] +name = "bitcoincore-rpc" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" +dependencies = [ + "bitcoincore-rpc-json", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoincore-rpc-json" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoincore-rpc", + "log", + "tempfile", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "borsh" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375989decec1a0fa424442bed520fa12ede43a48475ce8c7417d2c69b32b5f6b" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core 0.10.1", +] + +[[package]] +name = "chumsky" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc17a6284abccac6e50db35c1cee87f605474a72939b959a3a67d9371800efd" +dependencies = [ + "hashbrown 0.15.5", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261af4dc7a06ea8a527a8717d8cbba3417530d6ea14e1edb3de6b67806be474f" +dependencies = [ + "cmov", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "ctutils", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" +dependencies = [ + "serde", +] + +[[package]] +name = "electrsd" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91435161fb2ad5098e7ac7a4b793bf9c34723b0208a3fcf6f33707489e771396" +dependencies = [ + "bitcoind", + "electrum-client", + "log", + "nix", + "which", +] + +[[package]] +name = "electrum-client" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0bd443023f9f5c4b7153053721939accc7113cbdf810a024434eed454b3db1" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "elements" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34e00f7ddcd826d2f4c9bd72d79a6b6991a0ac4286e7e2b2ff8eecd130549073" +dependencies = [ + "bech32", + "bitcoin", + "bitcoin-consensus-encoding", + "bitcoin-internals 0.6.0", + "bitcoin_hashes 1.2.0", + "hex-conservative 1.3.0", + "secp256k1-zkp", + "serde", + "serde_json", +] + +[[package]] +name = "elements-miniscript" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c18e931c81e6e7e58d63b6fea1c57f5534717f0e6721741c4fce72a6c547e19" +dependencies = [ + "bitcoin", + "elements", + "miniscript", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", +] + +[[package]] +name = "ghost-cell" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" + +[[package]] +name = "globset" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98b0d85522dc0a824cfebbc9594ff4a9b289c568bea0370319dfbdf20ccdd2d" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.18", + "regex-syntax 0.8.11", +] + +[[package]] +name = "globwalk" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" +dependencies = [ + "bitflags 2.13.2", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3fef046dca3ca91ee1408a8c1b80ab777e80a4d308d1bf4e7adb3fcb047e08" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex-conservative" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271e0d19bcb473b6675739a2b536076b24a082316cb5199ad918edce10c599e8" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "ignore" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484be5657551d7868d67587df967fb4fcbe9d266a761cc855aaa148fda0b3cc7" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.18", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" +dependencies = [ + "base64 0.13.1", + "minreq 2.14.1", + "serde", + "serde_json", +] + +[[package]] +name = "last_will_example" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitcoin_hashes 0.14.101", + "smplx-std", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniscript" +version = "12.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a2f93613888f7d069f00cb94f83dab82f01f78a50c9935814a2547fa30ceba" +dependencies = [ + "bech32", + "bitcoin", +] + +[[package]] +name = "minreq" +version = "2.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "minreq" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "659579df697b372ef9e36f02fcbb41f6d6f157dcec7db9c9618fa0f23cf0fc20" +dependencies = [ + "rustls", + "rustls-webpki", + "serde", + "serde_json", + "webpki-roots", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5a6c098c7a3b6547378093f5cc30bc54fd361ce711e05293a5cc589562739b" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bee6c73da26345c729282832b60b0363cf3dd9f4bfd81d8551b7a1c889a113" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.9", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8530004ccb15eae51c7e40009fbe317f341f804db54dc033eec1c50be28cfa0" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.13.2", + "chacha20", + "core_detect", + "num-traits", + "rand 0.10.3", + "rand_xorshift", + "regex-syntax 0.8.11", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "psm" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd034599e63b970727f70d79e02d62390a4a84f7c6b827c27c46d5ac3fa622" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_xorshift" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60aa6af80be32871323012e02e6e65f8a7cc7890931ae421d217ad8fe0df2ccf" +dependencies = [ + "rand_core 0.10.1", +] + +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.11", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes 0.14.101", + "rand 0.8.8", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "secp256k1-zkp" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f6ab6ef5189ded7d11cb384a96a9f871001e50b391a8fcbb310c9ebc2099b" +dependencies = [ + "bitcoin-private", + "rand 0.8.8", + "secp256k1", + "secp256k1-zkp-sys", + "serde", +] + +[[package]] +name = "secp256k1-zkp-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f08b2d0b143a22e07f798ae4f0ab20d5590d7c68e0d090f2088a48a21d1654" +dependencies = [ + "cc", + "secp256k1-sys", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7523beb55eece201a2356bee0bbca0d1ab466c14c07703b2e0ee6d42cb0c2c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simplicity-lang" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff7b10251528d46d9cf60238267911984fedbb0948cccea98dd74fc6bf9f729" +dependencies = [ + "bitcoin", + "bitcoin_hashes 1.2.0", + "byteorder", + "elements", + "getrandom 0.2.17", + "ghost-cell", + "hex-conservative 1.3.0", + "miniscript", + "simplicity-sys", +] + +[[package]] +name = "simplicity-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955935733b2735665b4735d9b1361eade6be4c7aea6bca2640345e4c81756f58" +dependencies = [ + "bitcoin_hashes 1.2.0", + "cc", +] + +[[package]] +name = "simplicityhl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850c93fc9906e3324a299f50912d6176cdb381d31959362a7dd116fd2d0679a3" +dependencies = [ + "ariadne", + "base64 0.21.7", + "chumsky", + "clap", + "either", + "getrandom 0.2.17", + "itertools", + "miniscript", + "semver", + "serde", + "serde_json", + "simplicity-lang", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smplx-build" +version = "0.2.0" +dependencies = [ + "globwalk", + "pathdiff", + "prettyplease", + "proc-macro2", + "quote", + "serde", + "serde_json", + "simplicityhl", + "syn 3.0.9", + "thiserror", + "toml", + "toml_edit", +] + +[[package]] +name = "smplx-macros" +version = "0.2.0" +dependencies = [ + "smplx-build", + "smplx-test", + "syn 3.0.9", +] + +[[package]] +name = "smplx-regtest" +version = "0.2.0" +dependencies = [ + "electrsd", + "hex", + "hmac", + "serde", + "sha2", + "smplx-sdk", + "thiserror", + "toml", +] + +[[package]] +name = "smplx-sdk" +version = "0.2.0" +dependencies = [ + "bip39", + "bitcoincore-rpc", + "dyn-clone", + "elements-miniscript", + "hex", + "minreq 3.0.0", + "serde", + "serde_json", + "simplicityhl", + "thiserror", +] + +[[package]] +name = "smplx-std" +version = "0.2.0" +dependencies = [ + "either", + "rand 0.10.3", + "rand_core 0.10.1", + "serde", + "simplicityhl", + "smplx-macros", + "smplx-sdk", + "smplx-test", +] + +[[package]] +name = "smplx-test" +version = "0.2.0" +dependencies = [ + "electrsd", + "proc-macro2", + "proptest", + "quote", + "rand 0.10.3", + "serde", + "simplicityhl", + "smplx-regtest", + "smplx-sdk", + "syn 3.0.9", + "thiserror", + "toml", +] + +[[package]] +name = "stacker" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707f49d46706bacf8a2b00d51dace3f9de527c13eec3778f570c411f89e69967" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.6", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "toml" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5ffcdf8f50436dbbfd675c1fe1b8e0854f42b7defea8584bdebe1aee900e561" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.18+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec6e78bd70db9420d8d212fa9dc97f116448a1ceff5d00eabb991d407e21396" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-normalization" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "webpki-roots" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef" +dependencies = [ + "memchr", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/examples/last_will/Cargo.toml b/examples/last_will/Cargo.toml new file mode 100644 index 00000000..57371240 --- /dev/null +++ b/examples/last_will/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "last_will_example" +edition = "2024" +rust-version = "1.91.0" +version = "0.1.0" + +[dependencies] +smplx-std = { path = "../../crates/simplex" } + +anyhow = { version = "1.0.101" } +bitcoin_hashes = { version = "0.14" } diff --git a/examples/last_will/README.md b/examples/last_will/README.md new file mode 100644 index 00000000..7da8c7c6 --- /dev/null +++ b/examples/last_will/README.md @@ -0,0 +1,130 @@ +# Last Will Simplex Example + +This example builds on [`examples/basic`](../basic). Where `p2pk.simf` +there is a single spending condition, `last_will.simf` is a recursive +covenant: it re-creates itself on-chain across a series of transactions, +and offers three spending actions, each requiring a signature from a +specific key. + +## Overview + +`last_will.simf` implements a simple inheritance scheme with three keys +and an inheritance distance, all supplied to the contract as `param::` +arguments: + +* **Inherit**: after the covenant sits untouched for + `param::INHERITANCE_DISTANCE` blocks (relative timelock), the inheritor's + key can withdraw the funds. The upstream contract hardcodes 25920 blocks, + about 18 days at Liquid's 1-minute block time; this example passes 1440 + instead, so the tests can mine it. +* **ColdSpend**: the owner's cold key can break out of the covenant at any + time, with no wait required. +* **HotSpend**: the owner's hot key can spend at any time, which resets + the inheritance clock. `refresh_spend` checks a signature and then calls + `recursive_covenant()`, which requires the transaction to pay back into + the same covenant script (output 0) alongside an Elements fee output + (output 1). + +For Simplex compatibility, these actions are represented here as `Either` +types (instantiated with `Left`/`Right`) rather than named `enum` values. + +### Keys + +Each key comes from its own Simplex wallet. `tests/last_will_test.rs` +builds one per party with `TestContext::create_signer`, each from its own +BIP-39 mnemonic, the same way `Simplex.toml`'s `[test] mnemonic` produces +the default test wallet: + +```rust +let inheritor = context.create_signer(INHERITOR_MNEMONIC); +``` + +Their public keys become the contract's `param::INHERITOR_PK`, +`param::COLD_PK` and `param::HOT_PK`, alongside +`param::INHERITANCE_DISTANCE`. The covenant's script, and therefore its +address, is derived from all four; +`hot_spend_wrong_continuation_script_fails` relies on that, refreshing into +a covenant with the same three keys but a doubled inheritance distance, +which the contract rejects. + +The witness holds a placeholder signature at the branch being taken, +`ProgramSignature::at_path` names that position, and the signer fills it +in. + +The mnemonics in the test file are standard BIP-39 test vectors, so +their keys are public. Generate your own secrets for any deployment +holding real funds. + +### Outputs and fees + +The tests give Simplex the input and a change target. Simplex appends the +change output, then an Elements fee output, and works out both amounts: + +```rust +ft.add_change(ChangeOutput::new(change_to)); +``` + +For a **HotSpend** the change target is the covenant's own script, which +produces the two outputs `recursive_covenant()` requires: output 0 paying +back into the same covenant, output 1 the fee. For **ColdSpend** and +**Inherit** the contract doesn't constrain outputs, so the change target is +the spending party's wallet. + +Three tests check that the covenant rejects a malformed output set: one +with a single output, one with a non-fee output at index 1, and one with +the fee and continuation outputs swapped. A change target can't produce +those shapes, because Simplex appends any change output before the fee +output. Those tests list their outputs directly and sign with +`Signer::sign_tx`, which doesn't append any outputs. + +The repository structure is the same shape as `examples/basic`: + +```md +root +├── simf — Simplicity smart contracts source directory +├── src — Declares the generated artifacts module +│ └── artifacts — Simplicity artifacts generated by Simplex (gitignored) +├── tests — Simplex tests directory +├── Cargo.toml — The project's Rust configuration +└── Simplex.toml — The project's Simplex configuration +``` + +### Build + +```bash +simplex build +``` + +As with `examples/basic`, this generates `src/artifacts`, already declared +as a module in `src/lib.rs`. + +### Test + +```bash +simplex test -v +``` + +The `-v` option shows the test suite's narration: each test prints what +it's attempting and whether local execution or the node accepted it. +Several tests expect a rejection (wrong key, broken +`recursive_covenant()` check, or inheritance claimed too early). + +One test, `inherit_after_wait_succeeds`, mines the required blocks on the +local regtest so that the inheritance wait has elapsed. + +### Low-level tracing + +To see a local trace of the SimplicityHL contract executing in each test, +run the tests with an extra `-v`: + +```bash +simplex test -vv +``` + +This shows the individual +[jet](https://docs.simplicity-lang.org/documentation/jets) calls during +the course of the contract's execution, with their input and output +values. You can use this information to follow the contract's own logic +during transaction validation. For example, in the test cases that +exercise invalid spend conditions, you can see where the failures and +rejections happened. diff --git a/examples/last_will/Simplex.toml b/examples/last_will/Simplex.toml new file mode 100644 index 00000000..5f23542e --- /dev/null +++ b/examples/last_will/Simplex.toml @@ -0,0 +1,4 @@ +[build] +src_dir = "./simf" +simf_files = ["*.simf"] +out_dir = "./src/artifacts" diff --git a/examples/last_will/simf/last_will.simf b/examples/last_will/simf/last_will.simf new file mode 100644 index 00000000..75f4ba1d --- /dev/null +++ b/examples/last_will/simf/last_will.simf @@ -0,0 +1,61 @@ +/* + * LAST WILL + * + * The inheritor can spend the coins if the owner doesn't move them for + * `param::INHERITANCE_DISTANCE` blocks. The owner has to repeat the + * covenant when moving the coins with the hot key. The owner can break + * out of the covenant with the cold key. + * + * Adapted from `last_will.simf` in BlockstreamResearch/SimplicityHL. + * Changes: + * + * - The three public keys and the inheritance distance are `param::` + * arguments rather than constants written into the source, so each + * deployment is bound to its own keys and picks its own wait, and the + * test suite can sign with ordinary Simplex wallets. + * - The older Left/Right mechanism for selecting the spending action, + * instead of an `enum`, is restored because Simplex currently doesn't + * support contracts using `enum`. + */ +use crate::timelocks::enforce_relative_distance; + +fn checksig(pk: Pubkey, sig: Signature) { + let msg: u256 = jet::sig_all_hash(); + jet::bip_0340_verify((pk, msg), sig); +} + +// Enforce the covenant to repeat in the first output. +// +// Elements has explicit fee outputs, so enforce a fee output in the second output. +// Disallow further outputs. +fn recursive_covenant() { + assert!(jet::eq_32(jet::num_outputs(), 2)); + let this_script_hash: u256 = jet::current_script_hash(); + let output_script_hash: u256 = unwrap(jet::output_script_hash(0)); + assert!(jet::eq_256(this_script_hash, output_script_hash)); + assert!(unwrap(jet::output_is_fee(1))); +} + +fn inherit_spend(inheritor_sig: Signature) { + enforce_relative_distance(param::INHERITANCE_DISTANCE); + checksig(param::INHERITOR_PK, inheritor_sig); +} + +fn cold_spend(cold_sig: Signature) { + checksig(param::COLD_PK, cold_sig); +} + +fn refresh_spend(hot_sig: Signature) { + checksig(param::HOT_PK, hot_sig); + recursive_covenant(); +} + +fn main() { + match witness::INHERIT_OR_NOT { + Left(inheritor_sig: Signature) => inherit_spend(inheritor_sig), + Right(cold_or_hot: Either) => match cold_or_hot { + Left(cold_sig: Signature) => cold_spend(cold_sig), + Right(hot_sig: Signature) => refresh_spend(hot_sig), + }, + } +} diff --git a/examples/last_will/simf/timelocks.simf b/examples/last_will/simf/timelocks.simf new file mode 100644 index 00000000..7ee55973 --- /dev/null +++ b/examples/last_will/simf/timelocks.simf @@ -0,0 +1,17 @@ +pub fn enforce_relative_distance(min_distance: Distance) { + // Assert that the current input is spent in a transaction that can + // only appear a distance of at least min_distance blocks after the + // block containing the input UTXO. + // Panic otherwise. + + // This is a replacement for jet::broken_do_not_use_check_lock_distance. + + // Transaction version must be at least 2. + assert!(jet::le_32(2, jet::version())); + + // Fetch and parse sequence + let actual_data: Either = unwrap(jet::parse_sequence(jet::current_sequence())); + let actual_distance: Distance = unwrap_left::(actual_data); + + assert!(jet::le_16(min_distance, actual_distance)); +} diff --git a/examples/last_will/src/lib.rs b/examples/last_will/src/lib.rs new file mode 100644 index 00000000..91946c71 --- /dev/null +++ b/examples/last_will/src/lib.rs @@ -0,0 +1 @@ +pub mod artifacts; diff --git a/examples/last_will/tests/last_will_test.rs b/examples/last_will/tests/last_will_test.rs new file mode 100644 index 00000000..4d9ad0da --- /dev/null +++ b/examples/last_will/tests/last_will_test.rs @@ -0,0 +1,788 @@ +// Narrated Simplex demo for `last_will.simf`, a recursive covenant. +// +// These tests exercise various failure conditions in addition to the +// happy paths. They show how the contract logic and network consensus +// enforce spending rules. + +use last_will_example::artifacts::last_will::LastWillProgram; +use last_will_example::artifacts::last_will::derived_last_will::{LastWillArguments, LastWillWitness}; + +use simplex::constants::DUMMY_SIGNATURE; +use simplex::either::Either; +use simplex::program::ProgramError; +use simplex::provider::{ProviderError, SimplicityNetwork}; +use simplex::signer::{Signer, SignerError}; +use simplex::simplicityhl::elements::{Script, Sequence, Txid}; +use simplex::transaction::{ + ChangeOutput, FinalTransaction, PartialInput, PartialOutput, ProgramInput, ProgramSignature, UTXO, +}; + +// The three roles each get their own wallet, built from its own BIP-39 +// mnemonic. The contract takes their public keys as `param::` arguments, so +// the covenant is bound to these three wallets rather than to keys written +// into the contract source. +// +// These are standard BIP-39 test vectors. They are public knowledge and must +// never hold real funds. +const INHERITOR_MNEMONIC: &str = + "legal winner thank year wave sausage worth useful legal winner thank yellow"; +const COLD_MNEMONIC: &str = + "letter advice cage absurd amount doctor acoustic avoid letter advice cage above"; +const HOT_MNEMONIC: &str = "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong"; + +// Passed to the contract as `param::INHERITANCE_DISTANCE`, so this is the only +// place the value is defined. +// +// The original contract uses 25920 blocks, about 18 days at Liquid's +// 1-minute block time. This demo uses 1440 (1 day) to keep the mining cheap; +// mining tens of thousands of blocks on regtest risks a `nextest` timeout. +const INHERITANCE_DISTANCE: u16 = 1440; + +// Every test funds a fresh covenant instance with the same amount, and the +// fee is worked out by Simplex rather than fixed here. +const FUNDING_AMOUNT: u64 = 100_000; + +/// The three parties and the covenant they're bound to. +struct Parties { + inheritor: Signer, + cold: Signer, + hot: Signer, + arguments: LastWillArguments, + program: LastWillProgram, +} + +impl Parties { + fn new(context: &simplex::TestContext) -> Self { + let inheritor = context.create_signer(INHERITOR_MNEMONIC); + let cold = context.create_signer(COLD_MNEMONIC); + let hot = context.create_signer(HOT_MNEMONIC); + + // Parameters are compiled into the program, so these four values + // determine the covenant's script, and therefore its address. + let arguments = LastWillArguments { + inheritance_distance: INHERITANCE_DISTANCE, + inheritor_pk: inheritor.get_schnorr_public_key().serialize(), + cold_pk: cold.get_schnorr_public_key().serialize(), + hot_pk: hot.get_schnorr_public_key().serialize(), + }; + + Self { + inheritor, + cold, + hot, + program: LastWillProgram::new(&arguments), + arguments, + } + } + + /// The covenant these three wallets are bound to. `Program` caches its + /// compilation, so holding one instance here compiles the contract once per + /// test rather than once per spend. + fn program(&self) -> &LastWillProgram { + &self.program + } + + fn script(&self, context: &simplex::TestContext) -> Script { + self.program.get_script_pubkey(context.get_network()) + } + + /// The same contract and the same three parties, but with a doubled + /// inheritance distance, so it's a valid last will covenant at a + /// different address. + fn other_covenant_script(&self, context: &simplex::TestContext) -> Script { + let mut arguments = self.arguments.clone(); + arguments.inheritance_distance = INHERITANCE_DISTANCE * 2; + + LastWillProgram::new(&arguments).get_script_pubkey(context.get_network()) + } +} + +/// Which of the contract's three branches a spend takes. +/// +/// `witness::INHERIT_OR_NOT` is an `Either>`, so each spending path is a position in that nested type. +/// `witness()` puts a placeholder signature at that position, and +/// `signature()` names the same position for the signer, which replaces it. +/// An `enum` would read better in the contract, but Simplex doesn't support +/// contracts that use one. +#[derive(Clone, Copy)] +enum SpendPath { + Inherit, + ColdSpend, + HotSpend, +} + +impl SpendPath { + fn witness(self) -> LastWillWitness { + self.witness_with(DUMMY_SIGNATURE) + } + + fn witness_with(self, signature: [u8; 64]) -> LastWillWitness { + LastWillWitness { + inherit_or_not: match self { + SpendPath::Inherit => Either::Left(signature), + SpendPath::ColdSpend => Either::Right(Either::Left(signature)), + SpendPath::HotSpend => Either::Right(Either::Right(signature)), + }, + } + } + + fn signature(self) -> ProgramSignature { + let path: &[&str] = match self { + SpendPath::Inherit => &["Left"], + SpendPath::ColdSpend => &["Right", "Left"], + SpendPath::HotSpend => &["Right", "Right"], + }; + ProgramSignature::new("INHERIT_OR_NOT").at_path(path) + } +} + +/// Where a spend sends the value it takes out of the covenant. +/// +/// Only `SpendPath::HotSpend` constrains this: `recursive_covenant()` requires +/// output 0 to pay back into the covenant. The other two paths don't constrain +/// outputs, so the value can go wherever the spender wants. +enum ChangeTo { + /// The spending party's own wallet. + SpenderWallet, + /// Back into the covenant itself, which is what a `HotSpend` requires. + Covenant, + /// Some other script, which a `HotSpend` must reject. + Other(Script), +} + +/// An Elements fee output. Unlike Bitcoin, Elements/Liquid represents the +/// transaction fee as an explicit output with an empty scriptPubKey, which +/// `recursive_covenant()` checks for at output index 1 with +/// `jet::output_is_fee(1)`. +fn fee_output(amount: u64, network: &SimplicityNetwork) -> PartialOutput { + PartialOutput::new(Script::new(), amount, network.policy_asset()) +} + +/// Funds a fresh covenant instance, then spends it via `path`, signed by +/// `signer`. +/// +/// Returns the local-execution or broadcast error (if any) unexamined, so +/// callers can assert either success or a specific failure result. +fn spend_action( + context: &simplex::TestContext, + parties: &Parties, + description: &str, + path: SpendPath, + sequence: Sequence, + signer: &Signer, + change_to: ChangeTo, +) -> anyhow::Result { + let prog = parties.program(); + let script = prog.get_script_pubkey(context.get_network()); + + let utxo = fund_covenant(context, &script)?; + + let change_to = match change_to { + ChangeTo::SpenderWallet => signer.get_address().script_pubkey(), + ChangeTo::Covenant => script, + ChangeTo::Other(other) => other, + }; + let ft = covenant_spend(prog, utxo, path, sequence, change_to); + + println!("Submitting {description}..."); + + // One call: coin selection, the change and fee outputs, local execution, + // signing and broadcast. A test that needs to control those steps uses + // `spend_with_exact_outputs` instead. + let receipt = signer.broadcast(&ft)?; + println!(" -> accepted: {receipt}"); + Ok(receipt.txid()) +} + +/// Builds a spend of `utxo` through `path`, sending the value to `change_to`. +/// +/// The transaction carries one input and no explicit outputs: `Signer` appends +/// the change output and then the Elements fee output, and works out both +/// amounts. For a `SpendPath::HotSpend` with `change_to` set to the covenant's +/// own script, those two appended outputs are what `recursive_covenant()` +/// requires: output 0 back into the covenant, output 1 the fee. +fn covenant_spend( + prog: &LastWillProgram, + utxo: UTXO, + path: SpendPath, + sequence: Sequence, + change_to: Script, +) -> FinalTransaction { + let mut ft = FinalTransaction::new(); + + ft.add_program_input( + PartialInput::new(utxo).with_sequence(sequence), + ProgramInput::new(Box::new(prog.as_ref().clone()), path.witness()), + Some(path.signature()), + ); + ft.add_change(ChangeOutput::new(change_to)); + + ft +} + +/// Funds a fresh instance of the covenant at `script` and returns its UTXO, +/// once the funding transaction has confirmed. +/// +/// The built-in regtest mines a block on every broadcast, so there the +/// `TxReceipt::wait` returns at once. With an external Esplora provider +/// (`[test.esplora]`), it makes each test wait for a block instead of reading +/// the UTXO from the mempool. +fn fund_covenant(context: &simplex::TestContext, script: &Script) -> anyhow::Result { + let receipt = context.get_default_signer().send(script.clone(), FUNDING_AMOUNT)?; + let txid = receipt.txid(); + + receipt.wait()?; + + covenant_utxo(context, script, txid) +} + +/// The UTXO that transaction `txid` created at `script`. +/// +/// Selecting by txid avoids picking up whichever other UTXO the covenant's +/// script happens to hold. +fn covenant_utxo(context: &simplex::TestContext, script: &Script, txid: Txid) -> anyhow::Result { + context + .get_default_provider() + .fetch_scripthash_utxos(script)? + .into_iter() + .find(|utxo| utxo.outpoint.txid == txid) + .ok_or_else(|| anyhow::anyhow!("transaction {txid} produced no UTXO at the covenant script")) +} + +/// Spends a covenant UTXO with a transaction whose outputs are given exactly +/// as `outputs`, with no change output and no automatic fee output. +/// +/// Every caller is a test that expects the spend to be rejected. Spends that +/// are meant to succeed go through `spend_action`. +/// +/// The output-shape tests below violate `recursive_covenant()` on purpose: one +/// output instead of two, a non-fee output at index 1, and the fee and +/// continuation outputs swapped. `spend_action` can't express those, because +/// `Signer::broadcast` would append its own change and fee outputs on top and +/// fail the `num_outputs() == 2` check instead of the one under test. +/// `Signer::sign_tx` doesn't append any outputs, so these transactions are +/// balanced by hand. +fn spend_with_exact_outputs( + context: &simplex::TestContext, + parties: &Parties, + description: &str, + path: SpendPath, + sequence: Sequence, + signer: &Signer, + outputs: Vec, +) -> anyhow::Result { + let prog = parties.program(); + let script = prog.get_script_pubkey(context.get_network()); + + let utxo = fund_covenant(context, &script)?; + + let mut ft = FinalTransaction::new(); + ft.add_program_input( + PartialInput::new(utxo).with_sequence(sequence), + ProgramInput::new(Box::new(prog.as_ref().clone()), path.witness()), + Some(path.signature()), + ); + for output in outputs { + ft.add_output(output); + } + + println!("Submitting {description}..."); + + let tx = signer.sign_tx(&ft)?.extract_tx()?; + let receipt = context.get_default_provider().broadcast_transaction(&tx)?; + println!(" -> accepted: {receipt}"); + Ok(receipt.txid()) +} + +/// Asserts that a spend helper's result failed during local Simplicity +/// execution rather than succeeding or failing only once it reached a +/// node. Every test that expects a spend to be rejected outright (wrong +/// key, or a `recursive_covenant()` violation) fails this way: the +/// contract's own logic catches it before broadcast is ever attempted. +/// +/// Both spend helpers run the program through the `Signer`, so a failed +/// `assert!` always arrives as `ProgramError::ProgramExecution`. Matching the +/// type rather than the message text keeps the test independent of a +/// dependency's wording. +fn expect_assert_failure(result: anyhow::Result, what_was_wrong: &str) -> anyhow::Result<()> { + match result { + Ok(txid) => { + anyhow::bail!("expected local execution to reject this ({what_was_wrong}), but it broadcast: {txid}") + } + Err(err) => { + anyhow::ensure!( + matches!( + err.downcast_ref::(), + Some(SignerError::Program(ProgramError::ProgramExecution { .. })) + ), + "expected an assertion failure ({what_was_wrong}), got a different error: {err}" + ); + println!(" -> rejected locally, as expected: {what_was_wrong}\n"); + Ok(()) + } + } +} + +#[simplex::test] +fn cold_spend_breaks_out_of_the_covenant(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== ColdSpend: the owner cancels the covenant entirely ===\n\n\ + `cold_spend` is the simplest of the three paths, just a signature check\n\ + against `param::COLD_PK`. Whoever holds the cold private key\n\ + can withdraw the funds from this covenant at any time, ending it.\n" + ); + + spend_action( + &context, + &parties, + "ColdSpend transaction, signed with the cold key", + SpendPath::ColdSpend, + Sequence::default(), + &parties.cold, + ChangeTo::SpenderWallet, + )?; + Ok(()) +} + +#[simplex::test] +fn hot_spend_refreshes_the_covenant(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== HotSpend: the owner checks in, and the covenant is refreshed ===\n\n\ + `refresh_spend` checks a signature and then calls `recursive_covenant()`,\n\ + which requires the transaction to have two outputs. Output 0 must be a new\n\ + UTXO paying back to the same covenant script, verified by comparing\n\ + script hashes rather than by trusting the witness. Output 1 must be a\n\ + fee output. These two checks carry the covenant across transactions.\n\n\ + The test doesn't assemble those two outputs itself: it hands Simplex the\n\ + covenant's own script as the change target, and Simplex emits the change\n\ + output followed by the fee output, working out both amounts.\n" + ); + + spend_action( + &context, + &parties, + "HotSpend transaction, signed with the hot key", + SpendPath::HotSpend, + Sequence::default(), + &parties.hot, + ChangeTo::Covenant, + )?; + + println!( + "The covenant's {INHERITANCE_DISTANCE}-block inheritance clock has been\n\ + reset. The new UTXO this transaction creates is a fresh instance of the\n\ + same covenant, carrying the same rules as the original.\n" + ); + Ok(()) +} + +#[simplex::test] +fn inherit_too_early_is_rejected(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== Inherit, attempted immediately: rejected by the network ===\n\n\ + The inheritor declares a sequence number claiming the full {INHERITANCE_DISTANCE}-block\n\ + wait has passed. `enforce_relative_distance` only checks that the declared\n\ + value is large enough. It cannot see the chain state, so local execution\n\ + succeeds. No blocks are mined first, so the distance actually elapsed since\n\ + funding falls short of the declared value, and the node rejects the\n\ + transaction as too early.\n" + ); + + let result = spend_action( + &context, + &parties, + &format!( + "Inherit transaction, declaring the full {INHERITANCE_DISTANCE}-block wait, with no blocks actually mined" + ), + SpendPath::Inherit, + Sequence::from_height(INHERITANCE_DISTANCE), + &parties.inheritor, + ChangeTo::SpenderWallet, + ); + + match result { + Ok(txid) => { + anyhow::bail!("expected the node to reject this broadcast, but it succeeded: {txid}") + } + Err(err) => { + // `ProviderError::BroadcastRejected` carries the node's own response + // body. Matching that variant, rather than grepping the formatted + // error chain, keeps this test specific to a network rejection: a + // local `assert!` failure arrives as + // `ProgramError::ProgramExecution` and must not pass here. + let Some(SignerError::Provider(ProviderError::BroadcastRejected { message, .. })) = + err.downcast_ref::() + else { + anyhow::bail!("expected the node to reject this broadcast, got a different error: {err}") + }; + + anyhow::ensure!( + message.contains("non-BIP68-final"), + "expected a `non-BIP68-final` rejection specifically, got: {message}" + ); + println!(" -> rejected by the node: {message}\n"); + println!( + "`non-BIP68-final` means the input's relative timelock (a BIP 68\n\ + consensus rule) hasn't expired yet, so the node refuses the transaction.\n" + ); + } + } + Ok(()) +} + +#[simplex::test] +fn inherit_after_wait_succeeds(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== Inherit successfully: fast-forwarding {INHERITANCE_DISTANCE} blocks ===\n\n\ + At Liquid's 1-minute block time, {INHERITANCE_DISTANCE} blocks is about a day.\n\ + In a real deployment, the inheritor has to wait the correct amount of time\n\ + before inheriting. For demonstration purposes, this test asks the regtest\n\ + node to mine the required number of blocks immediately.\n" + ); + + let prog = parties.program(); + let script = prog.get_script_pubkey(context.get_network()); + + // The relative timelock runs from the height the funding output confirmed + // at, so the target below has to be measured from a tip that contains it. + let utxo = fund_covenant(&context, &script)?; + + let target = context.get_default_provider().fetch_tip_height()? as u64 + u64::from(INHERITANCE_DISTANCE); + context.get_network_utils().mine_until_height(target)?; + println!("Mined up to height {target}.\n"); + + let to = parties.inheritor.get_address().script_pubkey(); + let ft = covenant_spend(prog, utxo, SpendPath::Inherit, Sequence::from_height(INHERITANCE_DISTANCE), to); + + println!("Submitting the same Inherit transaction as before, now that the wait is real..."); + + let receipt = parties.inheritor.broadcast(&ft)?; + println!(" -> accepted: {receipt}"); + + println!( + "\nThe same spend that was rejected before now succeeds. The contract and\n\ + witness are the same, but the blockchain state has changed.\n" + ); + Ok(()) +} + +// ===================================================================== +// Wrong-key tests: each of the three paths checks a signature against a +// specific one of the contract's three `param::` public keys. These tests +// deliberately sign with the wrong party's wallet and so fail during local +// execution. +// ===================================================================== + +#[simplex::test] +fn hot_spend_with_cold_key_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== HotSpend, signed with the cold key instead of the hot key ===\n\n\ + Correctly structured HotSpend, but signed by the cold wallet instead of\n\ + the hot wallet, so the signature does not verify against `param::HOT_PK`.\n" + ); + let result = spend_action( + &context, + &parties, + "HotSpend transaction, signed with the cold key instead of the hot key", + SpendPath::HotSpend, + Sequence::default(), + &parties.cold, + ChangeTo::Covenant, + ); + expect_assert_failure(result, "cold key signed a HotSpend") +} + +#[simplex::test] +fn hot_spend_with_inheritor_key_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== HotSpend, signed with the inheritor's key instead of the hot key ===\n\n\ + Same idea as the cold-key case, but with the inheritor's wallet: the\n\ + inheritor cannot act as the owner and bypass the timelock.\n" + ); + let result = spend_action( + &context, + &parties, + "HotSpend transaction, signed with the inheritor's key instead of the hot key", + SpendPath::HotSpend, + Sequence::default(), + &parties.inheritor, + ChangeTo::Covenant, + ); + expect_assert_failure(result, "inheritor key signed a HotSpend") +} + +#[simplex::test] +fn cold_spend_with_hot_key_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== ColdSpend, signed with the hot key instead of the cold key ===\n\n\ + Even though the two owner-held keys (hot and cold) belong to the same person\n\ + in practice, they aren't interchangeable and have different abilities.\n\ + This test signs a ColdSpend with the hot key.\n" + ); + let result = spend_action( + &context, + &parties, + "ColdSpend transaction, signed with the hot key instead of the cold key", + SpendPath::ColdSpend, + Sequence::default(), + &parties.hot, + ChangeTo::SpenderWallet, + ); + expect_assert_failure(result, "hot key signed a ColdSpend") +} + +#[simplex::test] +fn cold_spend_with_inheritor_key_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== ColdSpend, signed with the inheritor's key ===\n\n\ + The same principle applies to the third of the three wallets.\n" + ); + let result = spend_action( + &context, + &parties, + "ColdSpend transaction, signed with the inheritor's key", + SpendPath::ColdSpend, + Sequence::default(), + &parties.inheritor, + ChangeTo::SpenderWallet, + ); + expect_assert_failure(result, "inheritor key signed a ColdSpend") +} + +#[simplex::test] +fn inherit_with_hot_key_after_wait_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== Inherit, signed with the hot key instead of the inheritor's key ===\n\n\ + Once again, the keys are not interchangeable and the key used must match the\n\ + specific spending path it is supposed to authorize.\n" + ); + let result = spend_action( + &context, + &parties, + "Inherit transaction (declared wait satisfied), signed with the hot key", + SpendPath::Inherit, + Sequence::from_height(INHERITANCE_DISTANCE), + &parties.hot, + ChangeTo::SpenderWallet, + ); + expect_assert_failure(result, "hot key signed an Inherit") +} + +#[simplex::test] +fn inherit_with_cold_key_after_wait_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!("\n=== Inherit, signed with the cold key ===\n\nThe same applies to the other owner-held key.\n"); + let result = spend_action( + &context, + &parties, + "Inherit transaction (declared wait satisfied), signed with the cold key", + SpendPath::Inherit, + Sequence::from_height(INHERITANCE_DISTANCE), + &parties.cold, + ChangeTo::SpenderWallet, + ); + expect_assert_failure(result, "cold key signed an Inherit") +} + +// ===================================================================== +// `recursive_covenant()` violations. Each of these tests breaks one of the +// covenant's structural conditions on purpose. +// ===================================================================== + +#[simplex::test] +fn hot_spend_wrong_output_count_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== HotSpend with only one output ===\n\n\ + `recursive_covenant()`'s first check is that `num_outputs()` equals 2.\n\ + Here, the entire 100,000 sats is provided as a single fee output.\n\ + Since this contract does not constrain the value of the fee output, a\n\ + stolen hot key could still leak most of the balance to miners this way.\n\ + This test only shows that skipping the continuation output entirely\n\ + is caught.\n" + ); + let result = spend_with_exact_outputs( + &context, + &parties, + "HotSpend transaction with a single (fee-only) output", + SpendPath::HotSpend, + Sequence::default(), + &parties.hot, + vec![fee_output(FUNDING_AMOUNT, context.get_network())], + ); + expect_assert_failure(result, "HotSpend with 1 output instead of 2") +} + +#[simplex::test] +fn hot_spend_wrong_continuation_script_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== HotSpend recreating the wrong script ===\n\n\ + Output 0 here pays to a real last will covenant over the same three keys,\n\ + differing only in its inheritance distance. Parameters are compiled into\n\ + the program, so that alone changes the script. `recursive_covenant()`\n\ + compares the output's script hash against this program's own (not trusting\n\ + the witness), so a HotSpend can't move the funds into a covenant on\n\ + different terms while disguised as a routine refresh.\n" + ); + + // Check the test's premise: a different parameter means a different address. + let other = parties.other_covenant_script(&context); + anyhow::ensure!( + other != parties.script(&context), + "changing `inheritance_distance` did not change the covenant's script" + ); + + let result = spend_action( + &context, + &parties, + "HotSpend transaction recreating a covenant on different terms, not this one", + SpendPath::HotSpend, + Sequence::default(), + &parties.hot, + ChangeTo::Other(other), + ); + expect_assert_failure(result, "HotSpend output 0 recreates the wrong script") +} + +#[simplex::test] +fn hot_spend_non_fee_second_output_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== HotSpend with a real script where the fee output should be ===\n\n\ + Output 0 correctly recreates the covenant. Output 1, though, has a real\n\ + destination script instead of the empty scriptPubKey that marks an actual fee\n\ + output, so `jet::output_is_fee(1)` is `false` and the assertion fails.\n" + ); + let script = parties.script(&context); + let not_a_fee = parties.hot.get_address().script_pubkey(); + let result = spend_with_exact_outputs( + &context, + &parties, + "HotSpend transaction with a non-fee second output", + SpendPath::HotSpend, + Sequence::default(), + &parties.hot, + vec![ + PartialOutput::new(script, 99_000, context.get_network().policy_asset()), + PartialOutput::new(not_a_fee, 1_000, context.get_network().policy_asset()), + ], + ); + expect_assert_failure(result, "HotSpend output 1 isn't a real fee output") +} + +#[simplex::test] +fn hot_spend_swapped_outputs_fails(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== HotSpend with the fee and continuation outputs swapped ===\n\n\ + A real continuation output and a real fee output are both present, but at the\n\ + wrong indices. Both of `recursive_covenant()`'s checks are position-specific\n\ + (`output_script_hash(0)`, `output_is_fee(1)`), so swapping the outputs\n\ + fails.\n" + ); + let script = parties.script(&context); + let result = spend_with_exact_outputs( + &context, + &parties, + "HotSpend transaction with fee/continuation outputs swapped", + SpendPath::HotSpend, + Sequence::default(), + &parties.hot, + vec![ + fee_output(1_000, context.get_network()), + PartialOutput::new(script, 99_000, context.get_network().policy_asset()), + ], + ); + expect_assert_failure(result, "HotSpend outputs in the wrong order") +} + +// ===================================================================== +// Spending chain: the owner refreshes the covenant twice, then exits with +// ColdSpend. +// ===================================================================== + +#[simplex::test] +fn covenant_survives_two_refreshes_then_breaks_out(context: simplex::TestContext) -> anyhow::Result<()> { + let parties = Parties::new(&context); + + println!( + "\n=== A full lifecycle: refresh, refresh again, then break out ===\n\n\ + Three chained transactions. The owner checks in twice with the hot key,\n\ + each spend consuming the UTXO the previous transaction created, so the\n\ + covenant persists across several hops. The owner then exits via the cold\n\ + key. The covenant script is the same at every step.\n" + ); + + let prog = parties.program(); + let script = prog.get_script_pubkey(context.get_network()); + + let funded = fund_covenant(&context, &script)?; + + // Each hop takes the covenant UTXO it is spending and returns the fresh one + // it created, so the chain is threaded by outpoint rather than by refetching + // the script. The new balance comes from `UTXO::amount` on that output, so + // the hop never needs to know the fee. + let hot_spend_hop = |utxo: UTXO, hop_name: &str| -> anyhow::Result { + println!("Submitting {hop_name} (HotSpend, {} sats in)...", utxo.amount()?); + + // Paying the change back into the covenant's own script gives + // `recursive_covenant()` the outputs it requires. + let ft = covenant_spend(prog, utxo, SpendPath::HotSpend, Sequence::default(), script.clone()); + + let receipt = parties.hot.broadcast(&ft)?; + let txid = receipt.txid(); + + // Confirm each hop before the next one spends its output, as + // `fund_covenant` does. + receipt.wait()?; + println!(" -> accepted and confirmed: {receipt}\n"); + + covenant_utxo(&context, &script, txid) + }; + + let after_first = hot_spend_hop(funded, "first refresh")?; + println!("Covenant still alive at {} sats.\n", after_first.amount()?); + let after_second = hot_spend_hop(after_first, "second refresh")?; + println!("Covenant still alive at {} sats.\n", after_second.amount()?); + + // Final hop: break out via ColdSpend, spending the UTXO the second refresh + // created. + let to = parties.cold.get_address().script_pubkey(); + let ft = covenant_spend(prog, after_second, SpendPath::ColdSpend, Sequence::default(), to); + + println!("Submitting final hop (ColdSpend, breaking out of the covenant for good)..."); + + let receipt = parties.cold.broadcast(&ft)?; + println!(" -> accepted: {receipt}\n"); + + println!( + "The covenant was recreated through two refresh cycles and then ended\n\ + on purpose.\n" + ); + Ok(()) +}