From a21287579c9d92cb924549a5f26013813ff5232e Mon Sep 17 00:00:00 2001 From: Santiago Carmuega Date: Mon, 2 Jun 2025 14:28:02 -0300 Subject: [PATCH] feat: introduce Bitcoin PoC --- Cargo.lock | 149 ++++++++++++++++++++++++++---- Cargo.toml | 2 +- crates/tx3-bitcoin/Cargo.toml | 22 +++++ crates/tx3-bitcoin/src/compile.rs | 69 ++++++++++++++ crates/tx3-bitcoin/src/lib.rs | 11 +++ crates/tx3-bitcoin/src/resolve.rs | 67 ++++++++++++++ examples/bitcoin_miniscript.tx3 | 34 +++++++ examples/cardano_nativescript.tx3 | 34 +++++++ 8 files changed, 371 insertions(+), 17 deletions(-) create mode 100644 crates/tx3-bitcoin/Cargo.toml create mode 100644 crates/tx3-bitcoin/src/compile.rs create mode 100644 crates/tx3-bitcoin/src/lib.rs create mode 100644 crates/tx3-bitcoin/src/resolve.rs create mode 100644 examples/bitcoin_miniscript.tx3 create mode 100644 examples/cardano_nativescript.tx3 diff --git a/Cargo.lock b/Cargo.lock index e49b0bd1..020e5c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,12 @@ version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -179,6 +185,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + [[package]] name = "base64" version = "0.21.7" @@ -197,6 +213,12 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bincode" version = "1.3.3" @@ -206,6 +228,54 @@ dependencies = [ "serde", ] +[[package]] +name = "bitcoin" +version = "0.32.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8929a18b8e33ea6b3c09297b687baaa71fb1b97353243a3f1029fad5c59c5b" +dependencies = [ + "base58ck", + "bech32 0.11.0", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" + +[[package]] +name = "bitcoin-io" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf" + +[[package]] +name = "bitcoin-units" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +dependencies = [ + "bitcoin-internals", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -638,6 +708,21 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "http" version = "0.2.12" @@ -1258,7 +1343,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f0bf697964d0562ee36727834f3fb698bdcf71dc2fef8c2f15e4e8920c6b55" dependencies = [ "base58", - "bech32", + "bech32 0.9.1", "crc", "cryptoxide", "hex", @@ -1467,7 +1552,7 @@ dependencies = [ "miette", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "ucd-trie", ] @@ -1814,6 +1899,25 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "3.2.0" @@ -1839,18 +1943,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.218" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -2082,11 +2186,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.12", ] [[package]] @@ -2102,9 +2206,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" dependencies = [ "proc-macro2", "quote", @@ -2154,9 +2258,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -2372,6 +2476,19 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tx3-bitcoin" +version = "0.5.0" +dependencies = [ + "bitcoin", + "hex", + "serde", + "thiserror 2.0.12", + "tokio", + "trait-variant", + "tx3-lang", +] + [[package]] name = "tx3-cardano" version = "0.5.0" @@ -2379,7 +2496,7 @@ dependencies = [ "hex", "pallas", "serde", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "trait-variant", "tx3-lang", @@ -2399,7 +2516,7 @@ dependencies = [ "pest_derive", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", ] [[package]] @@ -2410,7 +2527,7 @@ dependencies = [ "hex", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.12", "tokio", "tokio-util", "tracing", diff --git a/Cargo.toml b/Cargo.toml index f52e2e95..2184c76a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/tx3-cardano", "crates/tx3-lang", "crates/tx3-test"] +members = [ "crates/tx3-bitcoin","crates/tx3-cardano", "crates/tx3-lang", "crates/tx3-test"] [workspace.package] publish = true diff --git a/crates/tx3-bitcoin/Cargo.toml b/crates/tx3-bitcoin/Cargo.toml new file mode 100644 index 00000000..9000fdeb --- /dev/null +++ b/crates/tx3-bitcoin/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "tx3-bitcoin" +description = "Bitcoin compiler for tx3-lang protocols" +publish.workspace = true +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +keywords.workspace = true +documentation.workspace = true +homepage.workspace = true +readme.workspace = true + +[dependencies] +bitcoin = "0.32.6" +hex = "0.4.3" +serde = "1.0.219" +thiserror = "2.0.12" +tokio = "1.45.1" +trait-variant = "0.1.2" +tx3-lang = { version = "0.5.0", path = "../tx3-lang" } diff --git a/crates/tx3-bitcoin/src/compile.rs b/crates/tx3-bitcoin/src/compile.rs new file mode 100644 index 00000000..df5fb00b --- /dev/null +++ b/crates/tx3-bitcoin/src/compile.rs @@ -0,0 +1,69 @@ +use bitcoin::{ + absolute::LockTime, hashes::Hash, transaction::Version, OutPoint, ScriptBuf, Sequence, + Transaction, TxIn, TxOut, +}; + +use tx3_lang::ir; + +use crate::Error; + +pub fn expr_into_amount(expr: &ir::Expression) -> Result { + match expr { + ir::Expression::Number(x) => Ok(bitcoin::Amount::from_sat(*x as u64)), + ir::Expression::Assets(x) if x.len() == 1 => expr_into_amount(&x[0].amount), + _ => Err(Error::CoerceError( + format!("{:?}", expr), + "Number".to_string(), + )), + } +} + +fn compile_single_output(output: &ir::Output) -> Result { + let value = output + .amount + .as_ref() + .map_or(Err(Error::MissingAmount), expr_into_amount)?; + + let script_pubkey = output + .address + .as_ref() + .map_or(Err(Error::MissingAddress), expr_into_script_pubkey)?; + + Ok(TxOut { + value, + script_pubkey, + }) +} + +fn compile_outputs(tx: &ir::Tx) -> Result, Error> { + tx.outputs.iter().map(compile_single_output).collect() +} + +fn compile_inputs(tx: &ir::Tx) -> Result, Error> { + tx.inputs + .iter() + .flat_map(|input| input.refs.iter()) + .map(|ref_| { + let txid = Hash::hash(ref_.txid.as_slice()); + let vout = ref_.index as u32; + + TxIn { + previous_output: OutPoint::new(txid, vout), + script_sig: ScriptBuf::new(), + sequence: Sequence::ZERO, + witness: Default::default(), + } + }) + .collect() +} + +pub fn compile_tx(tx: &ir::Tx, pparams: &PParams) -> Result { + let tx = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: compile_inputs(tx)?, + output: compile_outputs(tx)?, + }; + + Ok(tx) +} diff --git a/crates/tx3-bitcoin/src/lib.rs b/crates/tx3-bitcoin/src/lib.rs new file mode 100644 index 00000000..afac8824 --- /dev/null +++ b/crates/tx3-bitcoin/src/lib.rs @@ -0,0 +1,11 @@ +mod compile; +mod resolve; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("error coercing {0} into {1}")] + CoerceError(String, String), + + #[error("missing amount")] + MissingAmount, +} diff --git a/crates/tx3-bitcoin/src/resolve.rs b/crates/tx3-bitcoin/src/resolve.rs new file mode 100644 index 00000000..bf101e2c --- /dev/null +++ b/crates/tx3-bitcoin/src/resolve.rs @@ -0,0 +1,67 @@ +#[derive(Debug, Default)] +pub struct TxEval { + pub payload: Vec, + pub fee: u64, + pub ex_units: u64, +} + +#[trait_variant::make(Send)] +pub trait Ledger { + async fn get_pparams(&self) -> Result; + async fn resolve_input(&self, query: &InputQuery) -> Result; +} + +pub fn resolve_tx( + tx: tx3_lang::ProtoTx, + ledger: impl Ledger, + max_optimize_rounds: usize, +) -> Result { +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use bitcoin::Address; + use tx3_lang::{ArgValue, Protocol}; + + fn load_protocol(example_name: &str) -> Protocol { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let code = format!("{manifest_dir}/../../examples/{example_name}.tx3"); + Protocol::from_file(&code).load().unwrap() + } + + fn address_to_bytes(address: &str) -> ArgValue { + let address = Address::from_str(address) + .unwrap() + .require_network(bitcoin::Network::Testnet) + .unwrap(); + + ArgValue::Bytes(address.script_pubkey().as_bytes().to_vec()) + } + + #[tokio::test] + async fn smoke_test_minscript() { + let protocol = load_protocol("bitcoin_miniscript"); + + let tx = protocol + .new_tx("transfer") + .unwrap() + .with_arg( + "Alice", + address_to_bytes("tb1pl0e4ywt8u483dg400scfpg9alh2v6xvju0zqw07reg6z00y9jnkq3cjanz"), + ) + .with_arg( + "Bob", + address_to_bytes("tb1pthj7rvazre5k8sgvs0vxujmd5jewv5numuv05dfnu8sjt9w33peqv6dzz7"), + ) + .with_arg("quantity", ArgValue::Int(100_000_000)) + .apply() + .unwrap(); + + let tx = resolve_tx(tx, MockLedger, 3).await.unwrap(); + + println!("{}", hex::encode(tx.payload)); + println!("{}", tx.fee); + } +} diff --git a/examples/bitcoin_miniscript.tx3 b/examples/bitcoin_miniscript.tx3 new file mode 100644 index 00000000..c0caedc9 --- /dev/null +++ b/examples/bitcoin_miniscript.tx3 @@ -0,0 +1,34 @@ + +party Alice; +party Bob; + +policy SharedVault = bitcoin::miniscript! { + or( + pk(@Alice), + and( + pk(@Bob), + older(100) + ) + ) +} + +party Receiver; + +tx payment_from_vault( + quantity: Int +) { + input source { + from: SharedVault, + min_amount: sats(quantity), + } + + output { + to: Receiver, + amount: sats(quantity), + } + + output { + to: SharedVault, + amount: source - sats(quantity) - fees, + } +} diff --git a/examples/cardano_nativescript.tx3 b/examples/cardano_nativescript.tx3 new file mode 100644 index 00000000..7fc3ed73 --- /dev/null +++ b/examples/cardano_nativescript.tx3 @@ -0,0 +1,34 @@ + +party Alice; +party Bob; + +policy SharedVault = cardano::nativescript! { + any { + sig(@Alice), + all { + sig(@Bob), + after(@now + 2000) + } + } +} + +party Receiver; + +tx payment_from_vault( + quantity: Int +) { + input source { + from: SharedVault, + min_amount: lovelace(quantity), + } + + output { + to: Receiver, + amount: lovelace(quantity), + } + + output { + to: SharedVault, + amount: source - lovelace(quantity) - fees, + } +}