diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87b1b0f6..10b621c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,13 +39,19 @@ jobs: - name: Build with MSRV run: | - # Ensure we're using the right version rustc --version - # Build the project - cargo build --all-features --locked + cargo build --locked + + - name: Build with MSRV (bitcoin feature) + run: | + rustc --version + cargo build --features bitcoin --locked - name: Run tests with MSRV - run: cargo test --all-features --locked + run: cargo test --locked + + - name: Run tests with MSRV (bitcoin feature) + run: cargo test --features bitcoin --locked check-lock-files: name: Check lock files @@ -177,6 +183,9 @@ jobs: - name: Run tests run: cargo test -vv + - name: Run tests (bitcoin feature) + run: cargo test -vv --features bitcoin + linux-aarch64: name: Build and Test on Linux ARM64 runs-on: ubuntu-24.04-arm @@ -198,6 +207,9 @@ jobs: - name: Build and test run: cargo test -vv + - name: Run tests (bitcoin feature) + run: cargo test -vv --features bitcoin + macos-build: name: Build and Test on macOS runs-on: macos-latest @@ -218,6 +230,9 @@ jobs: - name: Build and test run: cargo test -vv + - name: Run tests (bitcoin feature) + run: cargo test -vv --features bitcoin + fuzz-corpus: name: Verify Fuzz Corpus runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index c71b2b36..c5373314 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Added optional `bitcoin` feature flag that implements `Deref` for `ScriptPubkey` and `ScriptPubkeyRef`, giving kernel script types the full `bitcoin::Script` API transparently. + ### Changed - The `verify` function's `flags` parameter now uses `ScriptVerificationFlags` instead of `u32`, making the type explicit in the public API. diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index 3ef88bea..63a01107 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -68,6 +68,22 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals 0.3.0", + "bitcoin_hashes 0.14.0", +] + [[package]] name = "bech32" version = "0.9.0" @@ -80,6 +96,12 @@ version = "0.10.0-beta" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" +[[package]] +name = "bech32" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" + [[package]] name = "bimap" version = "0.6.0" @@ -93,11 +115,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5973a027b341b462105675962214dfe3c938ad9afd395d84b28602608bdcec7b" dependencies = [ "bech32 0.10.0-beta", - "bitcoin-internals", + "bitcoin-internals 0.2.0", "bitcoin_hashes 0.13.0", - "hex-conservative", + "hex-conservative 0.1.1", "hex_lit", - "secp256k1", + "secp256k1 0.28.1", +] + +[[package]] +name = "bitcoin" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3" +dependencies = [ + "base58ck", + "bech32 0.11.0", + "bitcoin-internals 0.3.0", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes 0.14.0", + "hex-conservative 0.2.2", + "hex_lit", + "secp256k1 0.29.0", ] [[package]] @@ -106,12 +145,33 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e5b76b88667412087beea1882980ad843b660490bbf6cce0a6cfc999c5b989" + [[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d437fd727271c866d6fd5e71eb2c886437d4c97f80d89246be3189b1da4e58b" +dependencies = [ + "bitcoin-internals 0.3.0", +] + [[package]] name = "bitcoin_hashes" version = "0.12.0" @@ -127,14 +187,25 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" dependencies = [ - "bitcoin-internals", - "hex-conservative", + "bitcoin-internals 0.2.0", + "hex-conservative 0.1.1", +] + +[[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 0.2.2", ] [[package]] name = "bitcoinkernel" version = "0.2.1" dependencies = [ + "bitcoin 0.32.9", "env_logger", "hex", "libbitcoinkernel-sys", @@ -206,11 +277,11 @@ dependencies = [ name = "examples" version = "0.0.1" dependencies = [ - "bitcoin", + "bitcoin 0.31.0", "bitcoinkernel", "env_logger", "log", - "secp256k1", + "secp256k1 0.28.1", "silentpayments", ] @@ -226,6 +297,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex_lit" version = "0.1.1" @@ -362,7 +442,17 @@ checksum = "3f622567e3b4b38154fb8190bcf6b160d7a4301d70595a49195b48c116007a27" dependencies = [ "bitcoin_hashes 0.12.0", "rand", - "secp256k1-sys", + "secp256k1-sys 0.9.2", +] + +[[package]] +name = "secp256k1" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" +dependencies = [ + "bitcoin_hashes 0.12.0", + "secp256k1-sys 0.10.0", ] [[package]] @@ -374,6 +464,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1433bd67156263443f14d603720b082dd3121779323fce20cba2aa07b874bc1b" +dependencies = [ + "cc", +] + [[package]] name = "serde" version = "1.0.0" @@ -432,7 +531,7 @@ dependencies = [ "bimap", "bitcoin_hashes 0.13.0", "hex", - "secp256k1", + "secp256k1 0.28.1", "serde", "serde_json", ] diff --git a/Cargo-recent.lock b/Cargo-recent.lock index af29e4ce..cbec49d6 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -70,6 +70,22 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals 0.3.0", + "bitcoin_hashes 0.14.1", +] + [[package]] name = "bech32" version = "0.9.1" @@ -82,6 +98,12 @@ version = "0.10.0-beta" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + [[package]] name = "bimap" version = "0.6.3" @@ -95,11 +117,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69197dee21fe23b45f5239bf88086efaa0cb8679f3e704906eb818e8ea169c14" dependencies = [ "bech32 0.10.0-beta", - "bitcoin-internals", - "bitcoin_hashes", - "hex-conservative", + "bitcoin-internals 0.2.1", + "bitcoin_hashes 0.13.1", + "hex-conservative 0.1.2", "hex_lit", - "secp256k1", + "secp256k1 0.28.2", +] + +[[package]] +name = "bitcoin" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf93e61f2dbc3e3c41234ca26a65e2c0b0975c52e0f069ab9893ebbede584d3" +dependencies = [ + "base58ck", + "bech32 0.11.1", + "bitcoin-internals 0.3.0", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes 0.14.1", + "hex-conservative 0.2.2", + "hex_lit", + "secp256k1 0.29.1", ] [[package]] @@ -108,19 +147,51 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "994dc6fcc13751c85370b7de118e672b193b9b65167bf09e258f124c97fb9685" +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin-units" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118" +dependencies = [ + "bitcoin-internals 0.3.0", +] + [[package]] name = "bitcoin_hashes" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446819536d8121575eeb7e89efdbadb3f055e87e4bb66c6679a6d5cc2f4b64fd" dependencies = [ - "hex-conservative", + "hex-conservative 0.1.2", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative 0.2.2", ] [[package]] name = "bitcoinkernel" version = "0.2.1" dependencies = [ + "bitcoin 0.32.9", "env_logger", "hex", "libbitcoinkernel-sys", @@ -189,11 +260,11 @@ dependencies = [ name = "examples" version = "0.0.1" dependencies = [ - "bitcoin", + "bitcoin 0.31.3", "bitcoinkernel", "env_logger", "log", - "secp256k1", + "secp256k1 0.28.2", "silentpayments", ] @@ -227,6 +298,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex_lit" version = "0.1.1" @@ -418,9 +498,19 @@ version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.13.1", "rand", - "secp256k1-sys", + "secp256k1-sys 0.9.2", +] + +[[package]] +name = "secp256k1" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +dependencies = [ + "bitcoin_hashes 0.14.1", + "secp256k1-sys 0.10.1", ] [[package]] @@ -432,6 +522,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + [[package]] name = "serde" version = "1.0.228" @@ -489,9 +588,9 @@ checksum = "9f3a0169e01dc753fe00f82d0b08a68d49e69f912f592e95ff5ade8b623728a5" dependencies = [ "bech32 0.9.1", "bimap", - "bitcoin_hashes", + "bitcoin_hashes 0.13.1", "hex", - "secp256k1", + "secp256k1 0.28.2", "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 683a5ff5..7576261d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,12 @@ keywords = ["bitcoin", "ffi", "libbitcoinkernel"] categories = ["cryptography::cryptocurrencies"] readme = "README.md" +[features] +bitcoin = ["dep:bitcoin"] + [dependencies] libbitcoinkernel-sys = { path = "libbitcoinkernel-sys", version = "0.3.0" } +bitcoin = { version = "0.32.9", optional = true } [dev-dependencies] hex = "0.4" diff --git a/README.md b/README.md index a62c2315..442a1035 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,15 @@ resolution: cargo build --locked ``` +## Feature flags + +- `bitcoin` - enables zero-copy [`Deref`] bridge to [`bitcoin::Script`](https://docs.rs/bitcoin/latest/bitcoin/struct.Script.html), giving kernel script types the full `bitcoin` crate API transparently. + +Enable with: +```bash +cargo build --features bitcoin +``` + ## Lock files `Cargo-minimal.lock` and `Cargo-recent.lock` pin dependencies to their minimum diff --git a/src/core/bitcoin_interop.rs b/src/core/bitcoin_interop.rs new file mode 100644 index 00000000..715b2452 --- /dev/null +++ b/src/core/bitcoin_interop.rs @@ -0,0 +1,155 @@ +//! Zero-copy [`Deref`] bridge from [`ScriptPubkey`] and [`ScriptPubkeyRef`] +//! to [`bitcoin::Script`], enabled by the `bitcoin` feature flag. +//! +//! This allows kernel script types to transparently use the full +//! `bitcoin::Script` API without any allocation or copying. +//! +//! ```no_run +//! use bitcoinkernel::ScriptPubkey; +//! +//! let bytes = hex::decode("5120deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(); +//! let script = ScriptPubkey::new(&bytes).unwrap(); +//! +//! assert!(script.is_p2tr()); +//! assert!(script.witness_version().is_some()); +//! assert!(!script.is_op_return()); +//! println!("{}", script.to_asm_string()); +//! ``` + +use bitcoin::Script; +use libbitcoinkernel_sys::btck_script_pubkey_to_bytes; +use std::{ffi::c_void, ops::Deref, panic}; + +use crate::core::script::{ScriptPubkey, ScriptPubkeyRef}; +use crate::{c_helpers, ScriptPubkeyExt}; + +struct ScriptBytesOut { + ptr: *const u8, + len: usize, +} + +// SAFETY: This function relies on `btck_script_pubkey_to_bytes` invoking +// the writer callback synchronously, before returning. The raw pointer +// captured in `ScriptBytesOut` is only valid for the duration of that +// callback. If the kernel implementation ever changed to call the writer +// asynchronously or defer it, this would be unsound. +unsafe fn script_as_bytes(val: &T) -> &[u8] { + unsafe extern "C" fn writer(data: *const c_void, len: usize, user_data: *mut c_void) -> i32 { + panic::catch_unwind(|| { + let out = &mut *(user_data as *mut ScriptBytesOut); + out.ptr = data as *const u8; + out.len = len; + c_helpers::to_c_result(true) + }) + .unwrap_or_else(|_| c_helpers::to_c_result(false)) + } + + let mut out = ScriptBytesOut { + ptr: std::ptr::null(), + len: 0, + }; + let ret = btck_script_pubkey_to_bytes( + val.as_ptr(), + Some(writer), + &mut out as *mut ScriptBytesOut as *mut c_void, + ); + assert!( + c_helpers::success(ret), + "btck_script_pubkey_to_bytes should never fail for a valid ScriptPubkey" + ); + + if out.ptr.is_null() { + return &[]; + } + + std::slice::from_raw_parts(out.ptr, out.len) +} + +impl<'a> Deref for ScriptPubkeyRef<'a> { + type Target = Script; + + fn deref(&self) -> &Script { + let bytes = unsafe { script_as_bytes(self) }; + Script::from_bytes(bytes) + } +} + +impl Deref for ScriptPubkey { + type Target = Script; + + fn deref(&self) -> &Script { + let bytes = unsafe { script_as_bytes(self) }; + Script::from_bytes(bytes) + } +} + +#[cfg(test)] +mod tests { + use crate::ScriptPubkey; + + #[test] + fn test_empty_script_via_deref() { + let script = ScriptPubkey::new(&[]).unwrap(); + assert_eq!(script.as_bytes(), &[]); + } + + #[test] + fn test_is_p2tr_via_deref() { + let p2tr = + hex::decode("5120deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + .unwrap(); + let script = ScriptPubkey::new(&p2tr).unwrap(); + assert!(script.is_p2tr()); + assert!(!script.is_p2wpkh()); + assert!(!script.is_p2pkh()); + } + + #[test] + fn test_is_p2pkh_via_deref() { + let p2pkh = hex::decode("76a914deadbeefdeadbeefdeadbeefdeadbeefdeadbeef88ac").unwrap(); + let script = ScriptPubkey::new(&p2pkh).unwrap(); + assert!(script.is_p2pkh()); + assert!(!script.is_p2tr()); + } + + #[test] + fn test_is_p2wpkh_via_deref() { + let p2wpkh = hex::decode("0014deadbeefdeadbeefdeadbeefdeadbeefdeadbeef").unwrap(); + let script = ScriptPubkey::new(&p2wpkh).unwrap(); + assert!(script.is_p2wpkh()); + assert!(!script.is_p2tr()); + } + + #[test] + fn test_script_ref_deref() { + let p2tr = + hex::decode("5120deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + .unwrap(); + let script = ScriptPubkey::new(&p2tr).unwrap(); + let script_ref = script.as_ref(); + assert!(script_ref.is_p2tr()); + assert_eq!(script.is_p2tr(), script_ref.is_p2tr()); + } + + #[test] + fn test_full_script_api_via_deref() { + let p2tr = + hex::decode("5120deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + .unwrap(); + let script = ScriptPubkey::new(&p2tr).unwrap(); + assert!(script.is_p2tr()); + assert!(script.witness_version().is_some()); + assert!(!script.is_op_return()); + assert!(!script.is_p2sh()); + assert!(!script.is_p2wsh()); + } + + #[test] + fn test_bytes_roundtrip() { + let bytes = + hex::decode("5120deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef") + .unwrap(); + let script = ScriptPubkey::new(&bytes).unwrap(); + assert_eq!(script.as_bytes(), bytes.as_slice()); + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs index 13104020..6f8c34df 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "bitcoin")] +mod bitcoin_interop; pub mod block; pub mod block_tree_entry; pub mod script; diff --git a/tests/test.rs b/tests/test.rs index 68b93301..0e99a20e 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -858,6 +858,15 @@ mod tests { } } + #[cfg(feature = "bitcoin")] + #[test] + fn test_bitcoin_feature_exposed() { + let p2pkh = hex::decode("76a9144bfbaf6afb76cc5771bc6404810d1cc041a6933988ac").unwrap(); + let script = ScriptPubkey::try_from(p2pkh.as_slice()).unwrap(); + + assert!(script.is_p2pkh()); + } + fn verify_test( spent: &str, spending: &str,