diff --git a/Cargo.lock b/Cargo.lock index 6db90da..0b80348 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,6 +414,7 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" name = "core" version = "0.1.0" dependencies = [ + "dlopen2", "hal-simplicity", "hex 0.4.3", "hex-literal", @@ -482,6 +483,29 @@ dependencies = [ "syn", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "elements" version = "0.25.2" @@ -1086,6 +1110,17 @@ version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +[[package]] +name = "jet_plugins" +version = "0.1.0" +dependencies = [ + "bitcoin_hashes", + "core", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jobserver" version = "0.1.34" @@ -1316,6 +1351,15 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plugin_test" +version = "0.1.0" +dependencies = [ + "core", + "hal-simplicity", + "jet_plugins", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1693,6 +1737,7 @@ dependencies = [ "base64 0.22.1", "clap 4.5.53", "core", + "dlopen2", "hal-simplicity", "hex 0.4.3", "log", diff --git a/Cargo.toml b/Cargo.toml index f4f8944..de5a73b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["cli", "core", "service"] +members = ["cli", "core", "jet_plugins", "plugin_test", "service"] [workspace.dependencies] hal-simplicity = "0.2.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 6b7a6bc..4651449 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -13,3 +13,4 @@ hex-literal = "1.1.0" hal-simplicity = { workspace = true } thiserror = { workspace = true } hex = { workspace = true } +dlopen2 = "0.8.2" diff --git a/core/src/jets/custom_jet.rs b/core/src/jets/custom_jet.rs new file mode 100644 index 0000000..7f477d6 --- /dev/null +++ b/core/src/jets/custom_jet.rs @@ -0,0 +1,305 @@ +use crate::jets::{environments::ElementsUnchainedEnv, jet_dyn::CustomJetApi}; +use dlopen2::wrapper::Container; +use hal_simplicity::simplicity::jet::Jet; +use std::sync::LazyLock; + +pub(crate) static JET_DLL: LazyLock>> = LazyLock::new(|| { + std::env::var("JET_DLL_PATH") + .ok() + .and_then(|path| unsafe { Container::::load(&path) }.ok()) +}); +/// `simplicity_unchained_core::jets::jet_dyn::decode` copies input bits into a separate buffer to transfer them via FFI. +/// This constant controls how many bits will be read. +const MAX_JET_BIT_LEN: u32 = 30; + +#[repr(C)] +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct CustomJet { + pub index: usize, +} + +impl CustomJet { + pub fn all() -> &'static [Self] { + JET_DLL.as_ref().expect("DLL is not loaded").all_jets() + } + + pub unsafe fn to_base_jet(&self) -> Option { + unsafe { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .to_base_jet(*self) + } + } + + pub unsafe fn from_base_jet(jet: &T) -> Self { + unsafe { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .from_base_jet(jet) + } + } +} + +impl std::str::FromStr for CustomJet { + type Err = hal_simplicity::simplicity::Error; + + fn from_str(s: &str) -> Result { + match JET_DLL.as_ref().expect("DLL is not loaded").from_str(s) { + Ok(_jet) => Ok(_jet), + Err(err) => Err(err), + } + } +} + +impl PartialOrd for CustomJet { + fn partial_cmp(&self, other: &Self) -> Option { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .partial_cmp(*self, *other) + } +} + +impl Ord for CustomJet { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .cmp(*self, *other) + } +} + +impl core::hash::Hash for CustomJet { + fn hash(&self, state: &mut H) { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .hash(*self, state); + } +} + +impl std::fmt::Debug for CustomJet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .debug_fmt(*self, f) + } +} + +impl std::fmt::Display for CustomJet { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .display_fmt(*self, f) + } +} + +impl Jet for CustomJet { + type Environment = ElementsUnchainedEnv; + type CJetEnvironment = ElementsUnchainedEnv; + + fn c_jet_env(env: &Self::Environment) -> &Self::CJetEnvironment { + env + } + + fn cmr(&self) -> hal_simplicity::simplicity::Cmr { + JET_DLL.as_ref().expect("DLL is not loaded").cmr(*self) + } + + fn source_ty(&self) -> hal_simplicity::simplicity::jet::type_name::TypeName { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .source_ty(*self) + } + + fn target_ty(&self) -> hal_simplicity::simplicity::jet::type_name::TypeName { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .target_ty(*self) + } + + fn encode( + &self, + w: &mut hal_simplicity::simplicity::BitWriter, + ) -> std::io::Result { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .encode(*self, w) + } + + /// # Safety + /// + /// Due to the lack of a `Clone` bound on `I`, the underlying implementation uses + /// `ptr::read` to create bitwise copies of the iterator. This is unsafe and may cause + /// undefined behavior if `I` contains types that manage unique resources. + /// This works correctly for common slice-based iterators like `Copied>`. + /// + /// See for details. + fn decode>( + bits: &mut hal_simplicity::simplicity::BitIter, + ) -> Result { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .decode(bits, MAX_JET_BIT_LEN) + } + + fn c_jet_ptr( + &self, + ) -> &dyn Fn( + &mut hal_simplicity::simplicity::ffi::CFrameItem, + hal_simplicity::simplicity::ffi::CFrameItem, + &Self::CJetEnvironment, + ) -> bool { + JET_DLL + .as_ref() + .expect("DLL is not loaded") + .c_jet_ptr(*self) + } + + fn cost(&self) -> hal_simplicity::simplicity::Cost { + JET_DLL.as_ref().expect("DLL is not loaded").cost(*self) + } +} + +#[cfg(test)] +mod test { + use core::panic; + use std::{ + hash::{Hash, Hasher}, + str::FromStr, + }; + + use hal_simplicity::simplicity::{BitIter, BitWriter}; + + use super::*; + + #[test] + fn test_dll_from_str() { + panic!("Rebuild `plugin_test` using code from `lib1.rs`"); + unsafe { + std::env::set_var("JET_DLL_PATH", "../target/debug/libplugin_test.so"); + } + let jet = CustomJet::from_str("custom_jet_1"); + + assert!(jet.is_ok()); + assert_eq!(jet.unwrap().to_string(), "custom_jet_1") + } + + #[test] + fn test_dll_hash() { + panic!("Rebuild `plugin_test` using code from `lib1.rs`"); + unsafe { + std::env::set_var("JET_DLL_PATH", "../target/debug/libplugin_test.so"); + } + let jet = CustomJet::from_str("custom_jet_1").expect("Failed to load jet"); + + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + jet.hash(&mut hasher); + + let _ = hasher.finish(); + } + + #[test] + fn test_dll_encode() { + panic!("Rebuild `plugin_test` using code from `lib1.rs`"); + unsafe { + std::env::set_var("JET_DLL_PATH", "../target/debug/libplugin_test.so"); + } + let custom_jet = CustomJet::from_str("custom_jet_1").expect("Failed to load jet"); + let elements_jet = CustomJet::from_str("input_asset").expect("Failed to load jet"); + + let mut buffer = Vec::new(); + let mut w = BitWriter::new(&mut buffer); + + let res1 = custom_jet.encode(&mut w); + let res2 = elements_jet.encode(&mut w); + + assert!(res1.is_ok()); + assert!(res2.is_ok()); + + assert_eq!(res1.unwrap(), 20); + assert_eq!(res2.unwrap(), 19); + + w.flush_all().unwrap(); + + let mut bit_iter = BitIter::from(buffer); + + let mut read_code = |read_bits: u8| -> Option { + let mut res = 0; + let mut cursor = 1 << (read_bits - 1); + + for _ in 0..read_bits { + match bit_iter.next() { + None => return None, + Some(true) => res |= cursor, + Some(false) => {} + } + cursor >>= 1; + } + Some(res) + }; + + let custom_jet_code = read_code(20); + let elements_jet_code = read_code(19); + + assert!(custom_jet_code.is_some()); + assert!(elements_jet_code.is_some()); + + assert_eq!(custom_jet_code.unwrap(), 1047454); + assert_eq!(elements_jet_code.unwrap(), 462369); + } + + #[test] + fn test_dll_decode() { + panic!("Rebuild `plugin_test` using code from `lib1.rs`"); + unsafe { + std::env::set_var("JET_DLL_PATH", "../target/debug/libplugin_test.so"); + } + let custom_jet = CustomJet::from_str("custom_jet_1").expect("Failed to load jet"); + let elements_jet = CustomJet::from_str("input_asset").expect("Failed to load jet"); + + let mut buffer = Vec::new(); + let mut w = BitWriter::new(&mut buffer); + + let res1 = custom_jet.encode(&mut w); + let res2 = elements_jet.encode(&mut w); + + assert!(res1.is_ok()); + assert!(res2.is_ok()); + + w.flush_all().unwrap(); + + let mut bit_iter = BitIter::from(buffer.as_ref()); + + let custom_decoded = CustomJet::decode(&mut bit_iter); + assert!(custom_decoded.is_ok()); + + assert_eq!(custom_decoded.unwrap().to_string(), "custom_jet_1"); + + let elements_decoded = CustomJet::decode(&mut bit_iter); + assert!(elements_decoded.is_ok()); + + assert_eq!(elements_decoded.unwrap().to_string(), "input_asset"); + } + + #[test] + fn test_dll_c_jet_ptr() { + panic!("Rebuild `plugin_test` using code from `lib1.rs`"); + unsafe { + std::env::set_var("JET_DLL_PATH", "../target/debug/libplugin_test.so"); + } + let custom_jet = CustomJet::from_str("custom_jet_1").expect("Failed to load jet"); + let elements_jet = CustomJet::from_str("input_asset").expect("Failed to load jet"); + + let _ = custom_jet.c_jet_ptr(); + let _ = elements_jet.c_jet_ptr(); + } +} diff --git a/core/src/jets/environments.rs b/core/src/jets/environments.rs index b34dcca..ef47ca8 100644 --- a/core/src/jets/environments.rs +++ b/core/src/jets/environments.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use hal_simplicity::simplicity::ffi::CElementsTxEnv; use hal_simplicity::simplicity::jet::elements::ElementsEnv; use hal_simplicity::simplicity::elements::{Transaction, script::Script}; @@ -17,3 +18,15 @@ impl UnchainedEnv { Self { redeem_script, env } } } + +impl<'a> Into<&'a CElementsTxEnv> for &'a ElementsUnchainedEnv { + fn into(self) -> &'a CElementsTxEnv { + self.env.c_tx_env() + } +} + +impl<'a> Into<&'a ()> for &'a BitcoinUnchainedEnv { + fn into(self) -> &'a () { + &self.env + } +} diff --git a/core/src/jets/jet_backend.rs b/core/src/jets/jet_backend.rs new file mode 100644 index 0000000..a5ced8d --- /dev/null +++ b/core/src/jets/jet_backend.rs @@ -0,0 +1,127 @@ +use std::{collections::HashMap, sync::LazyLock}; + +use crate::jets::custom_jet::{CustomJet, JET_DLL}; +use crate::jets::environments::ElementsUnchainedEnv; +use hal_simplicity::simplicity::{ + BitIter, BitWriter, Cmr, + ffi::CFrameItem, + jet::{Elements, Jet, type_name::TypeName}, +}; + +static ELEMENTS_JET_PTRS: LazyLock< + HashMap< + Elements, + &'static (dyn Fn(&mut CFrameItem, CFrameItem, &ElementsUnchainedEnv) -> bool + Send + Sync), + >, +> = LazyLock::new(|| { + Elements::ALL + .iter() + .map(|&jet| { + let closure = + move |dst: &mut CFrameItem, src: CFrameItem, env: &ElementsUnchainedEnv| { + jet.c_jet_ptr()(dst, src, env.env.c_tx_env()) + }; + let leaked: &'static ( + dyn Fn(&mut CFrameItem, CFrameItem, &ElementsUnchainedEnv) -> bool + + Send + + Sync + ) = Box::leak(Box::new(closure)); + (jet, leaked) + }) + .collect() +}); + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub enum JetBackend { + Elements(Elements), + Custom(CustomJet), +} + +impl Jet for JetBackend { + type CJetEnvironment = ElementsUnchainedEnv; + type Environment = ElementsUnchainedEnv; + + fn cmr(&self) -> Cmr { + match self { + Self::Elements(jet) => jet.cmr(), + Self::Custom(jet) => jet.cmr(), + } + } + fn source_ty(&self) -> TypeName { + match self { + Self::Elements(jet) => jet.source_ty(), + Self::Custom(jet) => jet.source_ty(), + } + } + + fn target_ty(&self) -> TypeName { + match self { + Self::Elements(jet) => jet.target_ty(), + Self::Custom(jet) => jet.target_ty(), + } + } + + fn encode(&self, w: &mut BitWriter) -> std::io::Result { + match self { + Self::Elements(jet) => jet.encode(w), + Self::Custom(jet) => jet.encode(w), + } + } + /// # Safety + /// + /// Due to the lack of a `Clone` bound on `I`, the underlying implementation uses + /// `ptr::read` to create bitwise copies of the iterator. This is unsafe and may cause + /// undefined behavior if `I` contains types that manage unique resources. + /// This works correctly for common slice-based iterators like `Copied>`. + /// + /// See for details. + fn decode>( + bits: &mut BitIter, + ) -> Result { + if JET_DLL.is_none() { + return Elements::decode(bits).map(|jet| Self::Elements(jet)); + } + CustomJet::decode(bits).map(|jet| Self::Custom(jet)) + } + + fn c_jet_env(env: &Self::Environment) -> &Self::CJetEnvironment { + env + } + + fn c_jet_ptr(&self) -> &dyn Fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool { + match self { + Self::Elements(jet) => ELEMENTS_JET_PTRS + .get(jet) + .expect("All Elements jets must be initialized"), + Self::Custom(jet) => jet.c_jet_ptr(), + } + } + + fn cost(&self) -> hal_simplicity::simplicity::Cost { + match self { + Self::Elements(jet) => jet.cost(), + Self::Custom(jet) => jet.cost(), + } + } +} + +impl std::str::FromStr for JetBackend { + type Err = hal_simplicity::simplicity::Error; + + fn from_str(s: &str) -> Result { + if JET_DLL.is_none() { + return Elements::from_str(s).map(|jet| Self::Elements(jet)); + } + + CustomJet::from_str(s).map(|jet| Self::Custom(jet)) + } +} + +impl std::fmt::Display for JetBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Elements(jet) => jet.fmt(f), + Self::Custom(jet) => jet.fmt(f), + } + } +} diff --git a/core/src/jets/jet_dyn.rs b/core/src/jets/jet_dyn.rs new file mode 100644 index 0000000..bf6d4a4 --- /dev/null +++ b/core/src/jets/jet_dyn.rs @@ -0,0 +1,280 @@ +use dlopen2::wrapper::WrapperApi; +use hal_simplicity::simplicity::{ + BitIter, BitWriter, Cmr, Cost, ffi::CFrameItem, jet::type_name::TypeName, +}; +use std::{fmt::Formatter, hash::Hasher, io::Write}; + +use crate::jets::custom_jet::CustomJet; +#[repr(C)] +pub struct JetSelfHandle(()); + +#[repr(C)] +pub struct CmrHandle(()); + +#[repr(C)] +pub struct TypeNameHandle(()); + +#[repr(C)] +pub struct CostHandle(()); + +#[repr(C)] +pub struct HasherHandle<'a> { + pub _hasher: &'a mut dyn Hasher, +} + +#[repr(C)] +pub struct FmtHandle<'a, 'b> { + pub _formatter: &'a mut core::fmt::Formatter<'b>, +} + +#[repr(C)] +pub struct StrHandle<'a> { + pub _str: &'a dyn AsRef, +} + +#[repr(i8)] +pub enum COrdering { + Less = -1, + Equal = 0, + Greater = 1, +} + +impl From for COrdering { + fn from(o: std::cmp::Ordering) -> Self { + match o { + std::cmp::Ordering::Less => COrdering::Less, + std::cmp::Ordering::Equal => COrdering::Equal, + std::cmp::Ordering::Greater => COrdering::Greater, + } + } +} + +impl From for std::cmp::Ordering { + fn from(o: COrdering) -> Self { + match o { + COrdering::Less => std::cmp::Ordering::Less, + COrdering::Equal => std::cmp::Ordering::Equal, + COrdering::Greater => std::cmp::Ordering::Greater, + } + } +} + +#[repr(i8)] +pub enum COptionOrdering { + None = i8::MIN, + Less = -1, + Equal = 0, + Greater = 1, +} + +impl From> for COptionOrdering { + fn from(o: Option) -> Self { + match o { + None => COptionOrdering::None, + Some(std::cmp::Ordering::Less) => COptionOrdering::Less, + Some(std::cmp::Ordering::Equal) => COptionOrdering::Equal, + Some(std::cmp::Ordering::Greater) => COptionOrdering::Greater, + } + } +} + +impl From for Option { + fn from(o: COptionOrdering) -> Self { + match o { + COptionOrdering::None => None, + COptionOrdering::Less => Some(std::cmp::Ordering::Less), + COptionOrdering::Equal => Some(std::cmp::Ordering::Equal), + COptionOrdering::Greater => Some(std::cmp::Ordering::Greater), + } + } +} + +#[repr(C)] +pub struct BitIterHandle<'a> { + pub data: &'a [u8], +} + +#[repr(C)] +pub struct DecodeResHandle { + pub jet: *const (), // *const Result + pub bits_read: usize, +} + +#[repr(C)] +pub struct BitWriterHandle<'a> { + pub _writer: BitWriter<&'a mut Vec>, +} + +#[repr(C)] +pub struct AllJetsHandle { + pub jets: *const CustomJet, + pub len: usize, +} + +#[derive(WrapperApi)] +pub struct CustomJetApi { + _cmp: extern "C" fn(_self: CustomJet, other: CustomJet) -> COrdering, + _partial_cmp: extern "C" fn(_self: CustomJet, other: CustomJet) -> COptionOrdering, + _hash: extern "C" fn(_self: CustomJet, hasher_handle: *mut HasherHandle), + _debug_fmt: extern "C" fn(_self: CustomJet, fmt_handle: *mut FmtHandle) -> *const (), // *const std::fmt::Result + _display_fmt: extern "C" fn(_self: CustomJet, fmt_handle: *mut FmtHandle) -> *const (), // *const std::fmt::Result + _from_str: extern "C" fn(name: *const StrHandle) -> *const (), // *const Result + _all_jets: extern "C" fn() -> AllJetsHandle, + _cmr: extern "C" fn(_self: CustomJet) -> *const CmrHandle, + _source_ty: extern "C" fn(_self: CustomJet) -> *const TypeNameHandle, + _target_ty: extern "C" fn(_self: CustomJet) -> *const TypeNameHandle, + _encode: extern "C" fn(_self: CustomJet, w: *mut BitWriterHandle) -> *const (), // *std::io::Result + _decode: extern "C" fn(w: *mut BitIterHandle) -> DecodeResHandle, + _c_jet_ptr: extern "C" fn(_self: CustomJet) -> *const (), // * &'static dyn Fn(&mut CFrameItem, CFrameItem, &T) -> bool + _cost: extern "C" fn(_self: CustomJet) -> *const CostHandle, + _to_base_jet: extern "C" fn(_self: CustomJet) -> *const (), // * Option + _from_base_jet: extern "C" fn(jet: *const ()) -> CustomJet, // * BaseJetType +} + +impl CustomJetApi { + pub fn from_str(&self, name: &str) -> Result { + let str_handle = StrHandle { _str: &name }; + unsafe { + *Box::from_raw((self._from_str)(&str_handle) + as *mut Result) + } + } + + pub fn all_jets(&self) -> &'static [CustomJet] { + unsafe { + let AllJetsHandle { jets, len } = (self._all_jets)(); + std::slice::from_raw_parts(jets, len) + } + } + + pub fn cmp(&self, lhs: CustomJet, rhs: CustomJet) -> std::cmp::Ordering { + (self._cmp)(lhs, rhs).into() + } + + pub fn partial_cmp(&self, lhs: CustomJet, rhs: CustomJet) -> Option { + (self._partial_cmp)(lhs, rhs).into() + } + + pub fn hash(&self, jet: CustomJet, hasher: &mut H) { + let mut hasher_handle = HasherHandle { _hasher: hasher }; + (self._hash)(jet, &mut hasher_handle) + } + + pub fn debug_fmt(&self, jet: CustomJet, formatter: &mut Formatter) -> std::fmt::Result { + let mut fmt_handle = FmtHandle { + _formatter: formatter, + }; + unsafe { *Box::from_raw((self._debug_fmt(jet, &mut fmt_handle)) as *mut std::fmt::Result) } + } + + pub fn display_fmt(&self, jet: CustomJet, formatter: &mut Formatter) -> std::fmt::Result { + let mut fmt_handle = FmtHandle { + _formatter: formatter, + }; + unsafe { + *Box::from_raw((self._display_fmt(jet, &mut fmt_handle)) as *mut std::fmt::Result) + } + } + + pub fn cmr(&self, jet: CustomJet) -> Cmr { + *unsafe { Box::from_raw((self._cmr)(jet) as *mut Cmr) } + } + + pub fn source_ty(&self, jet: CustomJet) -> TypeName { + *unsafe { Box::from_raw((self._source_ty)(jet) as *mut TypeName) } + } + + pub fn target_ty(&self, jet: CustomJet) -> TypeName { + *unsafe { Box::from_raw((self._target_ty)(jet) as *mut TypeName) } + } + + pub fn encode(&self, jet: CustomJet, w: &mut BitWriter) -> std::io::Result { + let mut buffer = Vec::new(); + let mut _writer = BitWriter::new(&mut buffer); + + let mut handle = BitWriterHandle { _writer }; + + let res = unsafe { + *Box::from_raw((self._encode)(jet, &mut handle) as *mut std::io::Result) + }; + + match res { + Ok(bits_written) => { + handle._writer.flush_all()?; + + let mut bit_iter = BitIter::from(buffer); + for _ in 0..bits_written { + if let Some(bit) = bit_iter.next() { + w.write_bit(bit)?; + } + } + Ok(bits_written) + } + Err(err) => Err(err), + } + } + + pub fn decode>( + &self, + bits: &mut BitIter, + max_jet_len: u32, + ) -> Result { + let mut bits_copy = unsafe { std::ptr::read(bits) }; + let mut buffer = Vec::with_capacity(max_jet_len.div_ceil(8) as usize); + let mut writer = BitWriter::from(&mut buffer); + + let mut i = 0; + while let Some(bit) = bits_copy.next() + && i < max_jet_len + { + print!("{}", if bit { 1 } else { 0 }); + writer + .write_bit(bit) + .expect("Writing to vec should not fail"); + i += 1; + } + println!(); + writer.flush_all().expect("Writing to vec should not fail"); + + let mut handle = BitIterHandle { data: &buffer }; + + let DecodeResHandle { jet, bits_read } = (self._decode)(&mut handle); + + let decoded = unsafe { + *Box::from_raw(jet as *mut Result) + }; + + if decoded.is_ok() { + for _ in 0..bits_read { + bits.next(); + } + } + + decoded + } + + #[allow(clippy::type_complexity)] + pub fn c_jet_ptr( + &self, + jet: CustomJet, + ) -> &'static (dyn Fn(&mut CFrameItem, CFrameItem, &T) -> bool + Send + Sync) { + unsafe { + *Box::from_raw((self._c_jet_ptr)(jet) + as *mut &'static ( + dyn Fn(&mut CFrameItem, CFrameItem, &T) -> bool + Send + Sync + )) + } + } + + pub fn cost(&self, jet: CustomJet) -> Cost { + unsafe { *Box::from_raw(self._cost(jet) as *mut Cost) } + } + + pub unsafe fn to_base_jet(&self, jet: CustomJet) -> Option { + *unsafe { Box::from_raw(self._to_base_jet(jet) as *mut Option) } + } + + pub unsafe fn from_base_jet(&self, jet: *const T) -> CustomJet { + self._from_base_jet(jet as *const ()) + } +} diff --git a/core/src/jets/mod.rs b/core/src/jets/mod.rs index fa0d9a4..9d0ff3d 100644 --- a/core/src/jets/mod.rs +++ b/core/src/jets/mod.rs @@ -1,7 +1,9 @@ pub mod bitcoin; +pub mod custom_jet; pub mod elements; pub mod environments; pub mod exec; - +pub mod jet_backend; +pub mod jet_dyn; #[cfg(test)] mod tests; diff --git a/core/src/lib.rs b/core/src/lib.rs index c470803..6cb4cff 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -61,3 +61,8 @@ impl FromStr for BitcoinNetwork { } } } + +#[doc(hidden)] +pub mod __simplicity { + pub use hal_simplicity::simplicity; +} diff --git a/core/src/runner.rs b/core/src/runner.rs index 3e36b35..0b08409 100644 --- a/core/src/runner.rs +++ b/core/src/runner.rs @@ -16,7 +16,7 @@ use hal_simplicity::simplicity::{ use hex_literal::hex; use crate::jets::bitcoin::CoreExtension; -use crate::jets::elements::ElementsExtension; +use crate::jets::custom_jet::CustomJet; use crate::jets::environments::UnchainedEnv; use crate::{BitcoinNetwork, ElementsNetwork}; @@ -61,8 +61,8 @@ impl SimplicityRunner { redeem_script: Script, network: ElementsNetwork, ) -> Result { - let program = Program::::from_str(program, witness) - .map_err(RunnerError::ProgramParse)?; + let program = + Program::::from_str(program, witness).map_err(RunnerError::ProgramParse)?; let elements_env = elements_execution_environment( &pset, @@ -176,11 +176,16 @@ mod tests { #[test] fn it_works() { + unsafe { + std::env::set_var("JET_DLL_PATH", "../target/debug/libplugin_test.so"); + } let script = Script::from_hex( "5221033523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf21025eb4655feae15a304653e27441ca8e8ced2bef89c22ab6b20424b4c07b3d14cc52ae" ).unwrap(); - let program = "4gTaj1eRafl6Ylk5SOxn0YFNK1owqziBW1okfwoUbyI60plzg9+aftH+iEF0HPfdhmi6u3/nCaFpYYsjIjrE6oXS8IEIFCbTPw2hAxCY+ss0X9VBirWgK0d+njZWFwRZorrYK2HgFBAIQKE2ACCP8CEChBhocKkGHHCxbGAgG0DgoHCw"; + // Dependent on program, `decode` may provoke doube free inside `let redeem_prog = wit_hex.map()` + // in Program::from_str() func because of owning iterators. Patch it locally + let program = "4AmwAAR/oHAIsFIGQVZtoIwCBNiDgUA="; let pset_bytes = hex!( "70736574ff0102040200000001030400000000010401010105010201fb04020000000001014e01499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c140100000000000186a000220020649be4e4f326f85b2187adb0698d9cab59c4b1c747cc6d884211e90e60484cf001070001080100010e201b42ba45a12d33a9efd32b738e603f5c606dcd59353fc5826f7486d4d5459858010f0400000000011004ffffffff00010308b88201000000000007fc04707365740220499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c140104220020649be4e4f326f85b2187adb0698d9cab59c4b1c747cc6d884211e90e60484cf000010308e80300000000000007fc04707365740220499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c1401040000" diff --git a/jet_plugins/Cargo.toml b/jet_plugins/Cargo.toml new file mode 100644 index 0000000..154ad3b --- /dev/null +++ b/jet_plugins/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "jet_plugins" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" +bitcoin_hashes = "0.14.1" +core = { path = "../core" } diff --git a/jet_plugins/src/c_wrappers.rs b/jet_plugins/src/c_wrappers.rs new file mode 100644 index 0000000..e1afbcb --- /dev/null +++ b/jet_plugins/src/c_wrappers.rs @@ -0,0 +1,352 @@ +use crate::static_tokens::StaticTokenInfo; +use quote::quote; + +pub(crate) fn impl_c_ffi( + base_type: &syn::Path, + unchained_env: &syn::Path, +) -> proc_macro2::TokenStream { + let cmp = cmp_c_wrapper(); + let partial_cmp = partial_cmp_c_wrapper(); + let hash = hash_c_wrapper(); + let debug_fmt = debug_fmt_c_wrapper(); + let display_fmt = display_fmt_c_wrapper(); + let all_jets = all_jets(); + + let cmr_c_wrapper = cmr_c_wrapper(); + let src_ty_c_wrapper = src_trg_ty_c_wrapper("source"); + let trg_ty_c_wrapper = src_trg_ty_c_wrapper("target"); + let encode_c_wrapper = encode_c_wrapper(); + let decode_c_wrapper = decode_c_wrapper(); + let cost_c_wrapper = cost_c_wrapper(); + let c_jet_ptr_wrapper = c_jet_ptr_wrapper(unchained_env); + let from_str = from_str_c_wrapper(); + let to_base_jet = to_base_jet(); + let from_base_jet = from_base_jet(base_type); + + quote! { + #cmp + #partial_cmp + #hash + #debug_fmt + #display_fmt + #all_jets + #cmr_c_wrapper + #src_ty_c_wrapper + #trg_ty_c_wrapper + #encode_c_wrapper + #decode_c_wrapper + #cost_c_wrapper + #c_jet_ptr_wrapper + #from_str + #to_base_jet + #from_base_jet + } +} + +fn cost_c_wrapper() -> proc_macro2::TokenStream { + let jet_self = StaticTokenInfo::jet_self_handle(); + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_trait = StaticTokenInfo::jet_trait_path(); + let cost_handle = StaticTokenInfo::cost_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _cost(_self: #jet_self) -> *const #cost_handle { + let jet = #enum_ident::variant_from_index(_self.index); + let boxed = Box::new( + <#enum_ident as #jet_trait>::cost(&jet) + ); + Box::into_raw(boxed) as *const #cost_handle + } + } +} + +fn c_jet_ptr_wrapper(unchained_env: &syn::Path) -> proc_macro2::TokenStream { + let jet_self = StaticTokenInfo::jet_self_handle(); + let enum_ident = StaticTokenInfo::enum_ident(); + let c_frame_item = StaticTokenInfo::cframe_item_path(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _c_jet_ptr(_self: #jet_self) -> *const () { + let jet = #enum_ident::variant_from_index(_self.index); + let boxed: Box< + &'static ( + dyn Fn( + &mut #c_frame_item, + #c_frame_item, + &#unchained_env, + ) -> bool + + Send + + Sync + )> + = Box::new(*C_JET_PTRS + .get(&jet) + .expect("All enum's variants should be initialized") + ); + // * &'static dyn Fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool + Box::into_raw(boxed) as *const () + } + } +} + +fn decode_c_wrapper() -> proc_macro2::TokenStream { + let bititer_handle = StaticTokenInfo::bititer_handle(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_trait = StaticTokenInfo::jet_trait_path(); + let bit_iter = StaticTokenInfo::bit_iter_path(); + let decode_res_handle = StaticTokenInfo::decode_res_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _decode(w: *mut #bititer_handle) -> #decode_res_handle + { + + let data: &[u8] = unsafe { (*w).data }; + let mut bit_iter = #bit_iter::from(data); + + let decode_res = <#enum_ident as #jet_trait>::decode(&mut bit_iter); + + let res = Box::new(match decode_res { + Ok(jet) => { + let index = #enum_ident::variant_to_index(&jet); + Ok(#jet_self { + index + }) + } + Err(err) => Err(err) + }); + let bits_read = bit_iter.n_total_read(); + + #decode_res_handle { + // *Result + jet: Box::into_raw(res) as *const (), + bits_read + } + + } + } +} + +fn encode_c_wrapper() -> proc_macro2::TokenStream { + let bitwriter_handle = StaticTokenInfo::bitwriter_handle(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_trait = StaticTokenInfo::jet_trait_path(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _encode(_self: #jet_self, w: *mut #bitwriter_handle) -> *const () { + let jet = #enum_ident::variant_from_index(_self.index); + let bit_writer = unsafe { + &mut (*w)._writer + }; + let boxed = Box::new(<#enum_ident as #jet_trait>::encode(&jet, bit_writer)); + // *std::io::Result + Box::into_raw(boxed) as *const () + } + } +} + +fn src_trg_ty_c_wrapper(mode: &str) -> proc_macro2::TokenStream { + let src_trg_ty = quote::format_ident!("{}", { + match mode { + "source" | "target" => format!("{}_ty", mode), + _ => unreachable!(), + } + }); + + let fn_name = quote::format_ident!("{}", { + match mode { + "source" | "target" => format!("_{}_ty", mode), + _ => unreachable!(), + } + }); + + let typename_handle = StaticTokenInfo::typename_handle(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_trait = StaticTokenInfo::jet_trait_path(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn #fn_name(_self: #jet_self) -> *const #typename_handle { + let jet = #enum_ident::variant_from_index(_self.index); + let boxed = Box::new(<#enum_ident as #jet_trait>::#src_trg_ty(&jet)); + Box::into_raw(boxed) as *const #typename_handle + } + } +} + +fn cmr_c_wrapper() -> proc_macro2::TokenStream { + let cmr_handle = StaticTokenInfo::cmr_handle(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_trait = StaticTokenInfo::jet_trait_path(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _cmr(_self: #jet_self) -> *const #cmr_handle { + let jet = #enum_ident::variant_from_index(_self.index); + let boxed = Box::new(<#enum_ident as #jet_trait>::cmr(&jet)); + Box::into_raw(boxed) as *const #cmr_handle + } + } +} + +fn all_jets() -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let all_jets_handle = StaticTokenInfo::all_jets_handle(); + let jet_self_handle = StaticTokenInfo::jet_self_handle(); + + quote! { + + static ALL_JETS_HANDLE: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + #enum_ident::ALL.iter().map(|jet| jet.into()).collect::>().into_boxed_slice() + }); + + #[unsafe(no_mangle)] + pub extern "C" fn _all_jets() -> #all_jets_handle { + #all_jets_handle { + jets: ALL_JETS_HANDLE.as_ptr(), + len: ALL_JETS_HANDLE.len() + } + } + } +} + +fn from_base_jet(base_type: &syn::Path) -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self_handle = StaticTokenInfo::jet_self_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _from_base_jet(jet_ptr: *const ()) -> #jet_self_handle { + let jet = unsafe { &*(jet_ptr as *const #base_type) }; + (&#enum_ident::from_base_jet(*jet)).into() + } + } +} + +fn to_base_jet() -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self_handle = StaticTokenInfo::jet_self_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _to_base_jet(_self: #jet_self_handle) -> *const () { + let inner_jet = #enum_ident::variant_from_index(_self.index); + let try_to_base = Box::new(inner_jet.to_base_jet()); + + // * Option + Box::into_raw(try_to_base) as *const () + } + } +} + +fn from_str_c_wrapper() -> proc_macro2::TokenStream { + let str_handle = StaticTokenInfo::str_handle(); + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self = StaticTokenInfo::jet_self_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _from_str(name: *const #str_handle) -> *const () { + if name.is_null() { + panic!("null ptr at name"); + } + let _str = unsafe { (*name)._str }; + let parsing_res = <#enum_ident as std::str::FromStr>::from_str(_str.as_ref()); + let boxed = Box::new( + match parsing_res { + Ok(jet) => { + let index = #enum_ident::variant_to_index(&jet); + Ok(#jet_self { + index + }) + }, + Err(err) => Err(err) + } + ); + Box::into_raw(boxed) as *const () + } + } +} + +fn display_fmt_c_wrapper() -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let fmt_handle = StaticTokenInfo::fmt_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _display_fmt(_self: #jet_self, fmt_handle: *mut #fmt_handle) -> *const () { + let jet = #enum_ident::variant_from_index(_self.index); + let formatter = unsafe { &mut (*fmt_handle)._formatter }; + let boxed = Box::new(<#enum_ident as std::fmt::Display>::fmt(&jet, formatter)); + Box::into_raw(boxed) as *const () + } + } +} + +fn debug_fmt_c_wrapper() -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let fmt_handle = StaticTokenInfo::fmt_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _debug_fmt(_self: #jet_self, fmt_handle: *mut #fmt_handle) -> *const () { + let jet = #enum_ident::variant_from_index(_self.index); + let formatter = unsafe { &mut (*fmt_handle)._formatter }; + let boxed = Box::new(<#enum_ident as core::fmt::Debug>::fmt(&jet, formatter)); + Box::into_raw(boxed) as *const () + } + } +} + +fn hash_c_wrapper() -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let hasher_handle = StaticTokenInfo::hasher_handle(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _hash(_self: #jet_self, hasher_handle: *mut #hasher_handle) { + let jet = #enum_ident::variant_from_index(_self.index); + let hasher = unsafe { &mut (*hasher_handle)._hasher }; + <#enum_ident as std::hash::Hash>::hash(&jet, hasher) + } + } +} + +fn partial_cmp_c_wrapper() -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let c_option_ordering = StaticTokenInfo::c_option_ordering(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _partial_cmp(_self: #jet_self, other: #jet_self) -> #c_option_ordering { + let lhs = #enum_ident::variant_from_index(_self.index); + let rhs = #enum_ident::variant_from_index(other.index); + #c_option_ordering::from(lhs.partial_cmp(&rhs)) + } + } +} + +fn cmp_c_wrapper() -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let jet_self = StaticTokenInfo::jet_self_handle(); + let c_ordering = StaticTokenInfo::c_ordering(); + + quote! { + #[unsafe(no_mangle)] + pub extern "C" fn _cmp(_self: #jet_self, other: #jet_self) -> #c_ordering { + let lhs = #enum_ident::variant_from_index(_self.index); + let rhs = #enum_ident::variant_from_index(other.index); + #c_ordering::from(lhs.cmp(&rhs)) + } + } +} diff --git a/jet_plugins/src/helpers.rs b/jet_plugins/src/helpers.rs new file mode 100644 index 0000000..206aadf --- /dev/null +++ b/jet_plugins/src/helpers.rs @@ -0,0 +1,310 @@ +use bitcoin_hashes::{Hash, HashEngine, sha256}; +use quote::quote; + +const SIMPLICITY_TAG_PREFIX: &[u8] = b"Simplicity\x1fCommitment\x1f"; +const JETIV: sha256::Midstate = sha256::Midstate([ + 0x95, 0x32, 0xee, 0x28, 0xcd, 0xca, 0x69, 0xde, 0xc8, 0xa0, 0xa2, 0x18, 0xb7, 0x9b, 0xe3, 0x62, + 0xf7, 0x40, 0xce, 0xaf, 0x64, 0x7f, 0x15, 0xb3, 0x8a, 0xed, 0x91, 0x68, 0x16, 0x3f, 0x92, 0x1b, +]); +pub const ENCODE_PREFIX: &[u8] = &[1, 1, 1, 1]; +pub type JetCodeBits = u32; +pub const JET_ENC_BITLEN: usize = 16 + ENCODE_PREFIX.len(); +// `Jet::encode` uses `write_bits_be` which is bounded to u64 +const _: () = { + let _ = 0 as JetCodeBits; + if std::mem::size_of::() > std::mem::size_of::() { + panic!("JetCodeBits type should not exceed u64") + } +}; + +// Warning: The CMRs generated here does not follow the proper Simplicity specification. +// +// TODO(ivanlele): Build valid Simplicity in Haskell from which we can extract the true CMRs. +// Taken from core::utils +#[allow(unused)] +pub fn cmr(name: &str) -> [u8; 32] { + let name = SIMPLICITY_TAG_PREFIX + .iter() + .chain(name.as_bytes().iter()) + .copied() + .collect::>(); + + let right_state = sha256::Hash::hash(&name).as_byte_array().to_owned(); + + let mut engine = sha256::HashEngine::from_midstate(JETIV, 0); + engine.input(&right_state); + + right_state +} + +/// Helper structure for converting jet bit encoding to `decode_bits!` macro input format +/// By construction ensures that token and left/right can not be Some() simultaneously +pub struct JetDecodeTree { + left: Option>, + right: Option>, + token: Option, +} + +// Stores bit pattern starting from most significant bit to be able to input patterns in BE order i.e. +// 0b111 -> +// 0 => {} +// 1 => { +// 0 => {} +// 1 => { +// 0 => {} +// 1 => { +// Ident +// } +// } +// } +#[derive(Clone)] +pub struct JetBranchCode { + pub bits: JetCodeBits, + pub len: usize, + pub token: proc_macro2::Ident, +} + +impl JetBranchCode { + /// Hashes identifier string and takes `JET_ENC_BITLEN - ENCODE_PREFIX.len()` bits of that hash + /// as jet's encoding alongside with `ENCODE_PREFIX` + pub fn from_ident_fixed(token: proc_macro2::Ident) -> Self { + let mut bits = 0 as JetCodeBits; + let mut cursor = 1 << (JET_ENC_BITLEN - 1); + + for &bit in ENCODE_PREFIX { + if bit == 1 { + bits |= cursor; + } + cursor >>= 1; + } + + let mut token_branch = [0 as u8; std::mem::size_of::()]; + token_branch.copy_from_slice( + &sha256::Hash::hash(token.to_string().as_bytes()).to_byte_array() + [0..std::mem::size_of::()], + ); + let mut token_bits = JetCodeBits::from_le_bytes(token_branch); + + for _ in 0..(JET_ENC_BITLEN - ENCODE_PREFIX.len()) { + if token_bits & 1 == 1 { + bits |= cursor; + } + cursor >>= 1; + token_bits >>= 1; + } + + Self { + bits, + len: JET_ENC_BITLEN, + token, + } + } +} + +impl JetDecodeTree { + fn new() -> Self { + Self { + left: None, + right: None, + token: None, + } + } + /// Constructs `JetDecodeTree` from branches. + /// ## Panics + /// Panics if some branches bit patterns collide or some bit pattern is prefix of another + pub fn from_branches(branches: Vec) -> Self { + // check for pattern collision + for i in 0..branches.len() { + for j in (i + 1)..branches.len() { + assert!( + branches[i].bits != branches[j].bits, + "Idents {}, {} collide", + branches[i].token.to_string(), + branches[j].token.to_string() + ) + } + } + + let mut res = Self::new(); + + for JetBranchCode { bits, len, token } in branches { + let mut curr = &mut res; + let mut cursor = 1 << (len - 1); + + for _ in 0..len { + if curr.token.is_some() { + panic!( + "Existing branch is a prefix of the new branch {:b} being added", + bits + ); + } + let bit = (bits & cursor) != 0; + + match bit { + false => { + if curr.left.is_none() { + curr.left = Some(Box::new(JetDecodeTree::new())); + } + curr = curr.left.as_mut().unwrap(); + } + true => { + if curr.right.is_none() { + curr.right = Some(Box::new(JetDecodeTree::new())); + } + curr = curr.right.as_mut().unwrap(); + } + } + cursor >>= 1; + } + + if curr.left.is_some() || curr.right.is_some() { + panic!("{:b} branch is prefix of some other branch", bits); + } + curr.token = Some(token) + } + + res + } +} + +impl Into for JetDecodeTree { + /// Panics if branch somehow contains branch as child and `Ident` value simultaneously + fn into(self) -> proc_macro2::TokenStream { + let (left_branch, right_branch, token) = (self.left, self.right, self.token); + + let (left, right, val) = match (left_branch, right_branch, token) { + (Some(left_branch), Some(right_branch), _) => { + (Self::into(*left_branch), Self::into(*right_branch), None) + } + (Some(left_branch), None, _) => (Self::into(*left_branch), quote! {}, None), + (None, Some(right_branch), _) => (quote! {}, Self::into(*right_branch), None), + (None, None, Some(ident)) => (quote! {}, quote! {}, Some(ident)), + _ => unreachable!("Non null ident implifies null left/right branches by construction"), + }; + + if let Some(ident) = val { + return quote! {Self::#ident}; + } + quote! { + 0 => { + #left + }, + 1 => { + #right + } + } + } +} + +pub fn snake_to_pascal_case(s: &str) -> String { + s.split('_') + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect() +} +pub fn pascal_to_snake_case(s: &str) -> String { + let mut snake = String::new(); + let chars = s.chars().collect::>(); + + chars.windows(2).for_each(|pair| { + if let [curr, next] = pair { + snake.extend(pair[0].to_lowercase()); + if curr.is_lowercase() && !next.is_lowercase() { + snake.push('_'); + } + } + }); + + if let Some(c) = chars.last() { + snake.extend(c.to_lowercase()); + } + + snake +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_to_pascal_case() { + let snake = "valid_str"; + let single = "single"; + let empty = ""; + + assert_eq!(snake_to_pascal_case(&snake), "ValidStr"); + assert_eq!(snake_to_pascal_case(&single), "Single"); + assert_eq!(snake_to_pascal_case(&empty), ""); + } + + #[test] + fn test_to_snake_case() { + let pascal = "PascalCase"; + let single = "Single"; + let acronym = "PAScalCase"; + let empty = ""; + + assert_eq!(pascal_to_snake_case(pascal), "pascal_case"); + assert_eq!(pascal_to_snake_case(single), "single"); + assert_eq!(pascal_to_snake_case(acronym), "pascal_case"); + assert_eq!(pascal_to_snake_case(empty), ""); + } + + fn format_arms(ts: proc_macro2::TokenStream) -> String { + let mut lines: Vec = Vec::new(); + let mut indent = 0usize; + let mut line: Vec<&str> = Vec::new(); + + let s = ts.to_string(); + + let flush = |line: &mut Vec<&str>, indent: usize, lines: &mut Vec| { + if !line.is_empty() { + lines.push(format!("{}{}", " ".repeat(indent), line.join(" "))); + line.clear(); + } + }; + + for token in s.split_whitespace() { + match token { + "{" => { + line.push("{"); + flush(&mut line, indent, &mut lines); + indent += 1; + } + "}" => { + flush(&mut line, indent, &mut lines); + indent = indent.saturating_sub(1); + lines.push(format!("{}}}", " ".repeat(indent))); + } + "," => { + flush(&mut line, indent, &mut lines); + } + t => line.push(t), + } + } + + flush(&mut line, indent, &mut lines); + lines.join("\n") + } + + #[test] + fn test_decode_tree() { + let branches = vec![ + JetBranchCode::from_ident_fixed(quote::format_ident!("CustomJet1")), + JetBranchCode::from_ident_fixed(quote::format_ident!("CustomJet2")), + ]; + println!("jet1 bits {:b} {}", branches[0].bits, branches[0].bits); + + let tree = JetDecodeTree::from_branches(branches); + let tree_tokens: proc_macro2::TokenStream = tree.into(); + + let formatted = format_arms(tree_tokens); + + println!("{}", formatted); + } +} diff --git a/jet_plugins/src/jet_trait.rs b/jet_plugins/src/jet_trait.rs new file mode 100644 index 0000000..dc37d6f --- /dev/null +++ b/jet_plugins/src/jet_trait.rs @@ -0,0 +1,398 @@ +use crate::{ + CUSTOM_JET_COST, JetsInput, StaticTokenInfo, + helpers::{JET_ENC_BITLEN, JetBranchCode, JetDecodeTree, cmr}, + pascal_to_snake_case, +}; +use quote::quote; + +pub(crate) fn jet_trait_full( + base_type: &syn::Path, + unchained_env: &syn::Path, + names: &[proc_macro2::Ident], + jets: &JetsInput, +) -> proc_macro2::TokenStream { + let associated_types = associated_types_impl(unchained_env); + let c_jet_env = c_jet_env_impl(); + let cmr = cmr_impl(names); + + let src_ty = src_trg_ty_impl(names, &build_source_tys(jets), "source"); + let trgt_ty = src_trg_ty_impl(names, &&build_target_tys(jets), "target"); + + let jet_codes = build_jet_codes(names); + + let jet_encode = encode_impl(names, &jet_codes); + let jet_decode_unsafe = decode_unsafe_impl(base_type, jet_codes); + let jet_decode = decode_impl(); + + let c_jet_ptr = c_jet_ptr_impl(); + let c_jet_ptr_builder = c_jet_ptr_table_impl(base_type, unchained_env, names, jets); + let cost = cost_impl(); + + let jet_trait_path = StaticTokenInfo::jet_trait_path(); + let enum_ident = StaticTokenInfo::enum_ident(); + + let invalid_jet_err = StaticTokenInfo::invalid_jet_err(); + let end_of_stream_err = StaticTokenInfo::end_of_stream_err(); + + let fmt_impl = fmt_trait_full(&names); + let from_str_impl = from_str_trait_full(base_type, &names); + + quote! { + // maybe consider moving it somewhere + macro_rules! decode_bits { + ($bits:ident, {}) => { + Err(#invalid_jet_err.into()) + }; + ($bits:ident, {$jet:expr}) => { + Ok($jet) + }; + ($bits:ident, { 0 => $false_branch:tt, 1 => $true_branch:tt }) => { + match $bits.next() { + None => Err(#end_of_stream_err.into()), + Some(false) => decode_bits!($bits, $false_branch), + Some(true) => decode_bits!($bits, $true_branch), + } + }; + } + #c_jet_ptr_builder + impl #enum_ident { + #jet_decode_unsafe + } + impl #jet_trait_path for #enum_ident { + #associated_types + #c_jet_env + #cmr + #src_ty + #trgt_ty + #jet_encode + #jet_decode + #c_jet_ptr + #cost + } + + #fmt_impl + #from_str_impl + } +} + +// TODO: settle on namings +fn from_str_trait_full( + base_type: &syn::Path, + names: &[proc_macro2::Ident], +) -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let snake_case_names = names + .iter() + .map(|name| pascal_to_snake_case(&name.to_string())); + let simpl_err_ty = StaticTokenInfo::simplicity_error_ty(); + quote! { + impl std::str::FromStr for #enum_ident { + type Err = #simpl_err_ty; + + fn from_str(s: &str) -> Result { + match s { + #(#snake_case_names => Ok(Self::#names), )* + _ => { + let inner_jet = s.parse::<#base_type>()?; + Ok(Self::BaseJets(inner_jet)) + } + } + } + } + } +} + +// TODO: settle on namings +fn fmt_trait_full(names: &[proc_macro2::Ident]) -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let snake_case_names = names + .iter() + .map(|name| pascal_to_snake_case(&name.to_string())); + quote! { + impl std::fmt::Display for #enum_ident { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::BaseJets(inner_jet) => f.write_str(&inner_jet.to_string()), + #(Self::#names => f.write_str(#snake_case_names), )* + } + } + } + } +} + +fn cost_impl() -> proc_macro2::TokenStream { + let cost_path = StaticTokenInfo::cost_path(); + quote! { + fn cost(&self) -> #cost_path { + match self { + Self::BaseJets(inner_jet) => inner_jet.cost(), + _ => #cost_path::from_milliweight(#CUSTOM_JET_COST) + } + } + } +} + +fn c_jet_ptr_impl() -> proc_macro2::TokenStream { + let c_frame_path = StaticTokenInfo::cframe_item_path(); + + quote! { + fn c_jet_ptr(&self) -> &dyn Fn(&mut #c_frame_path, #c_frame_path, &Self::CJetEnvironment) -> bool { + C_JET_PTRS.get(self).expect("All enum variants should be initialized") + } + } +} + +/// May have issues with env casting +fn c_jet_ptr_table_impl( + base_type: &syn::Path, + unchained_env: &syn::Path, + names: &[proc_macro2::Ident], + inputs: &JetsInput, +) -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let c_frame_item = StaticTokenInfo::cframe_item_path(); + let jet_trait = StaticTokenInfo::jet_trait_path(); + let funcs = inputs.jets.iter().map(|jet| jet.func.clone()); + + quote! { + static C_JET_PTRS: std::sync::LazyLock< + std::collections::HashMap< + #enum_ident, + &'static ( + dyn Fn( + &mut #c_frame_item, + #c_frame_item, + &#unchained_env, + ) -> bool + + Send + + Sync + ), + >, + > = std::sync::LazyLock::new(|| build_c_jet_ptrs()); + + fn build_c_jet_ptrs() + -> std::collections::HashMap< + #enum_ident, + &'static ( + dyn Fn( + &mut #c_frame_item, + #c_frame_item, + &#unchained_env, + ) -> bool + + Send + + Sync + ), + > { + #enum_ident::ALL + .iter() + .map(|jet| { + let boxed: Box< + dyn Fn(&mut #c_frame_item, #c_frame_item, &#unchained_env) -> bool + Send + Sync, + > = match jet { + #enum_ident::BaseJets(inner_jet) => Box::new( + move |dst: &mut #c_frame_item, src: #c_frame_item, env: &#unchained_env| -> bool { + <#base_type as #jet_trait>::c_jet_ptr(inner_jet)(dst, src, env.into()) + }, + ), + // custom jets + #(#enum_ident::#names => Box::new( + move |dst: &mut #c_frame_item, src: #c_frame_item, env: &#unchained_env| -> bool { + #funcs(dst, src, env) + }, + ), )* + }; + let leaked: &'static ( + dyn Fn(&mut #c_frame_item, #c_frame_item, &#unchained_env) -> bool + + Send + + Sync + ) = Box::leak(boxed); + (*jet, leaked) + }) + .collect() + } + } +} + +fn decode_impl() -> proc_macro2::TokenStream { + let bit_iter = StaticTokenInfo::bit_iter_path(); + let decode_err = StaticTokenInfo::decode_err(); + let enum_ident = StaticTokenInfo::enum_ident(); + + quote! { + /// # Safety + /// + /// Due to the lack of a `Clone` bound on `I`, the underlying implementation uses + /// `ptr::read` to create bitwise copies of the iterator. This is unsafe and may cause + /// undefined behavior if `I` contains types that manage unique resources. + /// This works correctly for common slice-based iterators like `Copied>`. + /// + /// See for details. + fn decode>(bits: &mut #bit_iter) -> Result { + + #enum_ident::decode_unsafe(bits) + } + } +} + +fn decode_unsafe_impl( + base_type: &syn::Path, + codes: Vec, +) -> proc_macro2::TokenStream { + let custom_decode_tree: proc_macro2::TokenStream = JetDecodeTree::from_branches(codes).into(); + let decode_err = StaticTokenInfo::decode_err(); + let bit_iter = StaticTokenInfo::bit_iter_path(); + let jet_trait = StaticTokenInfo::jet_trait_path(); + + quote! { + pub fn decode_unsafe>(bits: &mut #bit_iter) -> Result { + let (mut elements_iter, mut custom_iter) = + unsafe { (std::ptr::read(bits), std::ptr::read(bits)) }; + + let bits_read = bits.n_total_read(); + + let try_elements = <#base_type as #jet_trait>::decode(&mut elements_iter); + + if let Ok(jet) = try_elements { + for _ in 0..(elements_iter.n_total_read() - bits_read) { + bits.next(); + } + + std::mem::forget(elements_iter); + std::mem::forget(custom_iter); + + return Ok(Self::BaseJets(jet)); + } + + let custom_iter_ref = &mut custom_iter; + let try_custom = decode_bits!(custom_iter_ref, { + #custom_decode_tree + }); + + if try_custom.is_ok() { + for _ in 0..(custom_iter.n_total_read() - bits_read) { + bits.next(); + } + } + + std::mem::forget(elements_iter); + std::mem::forget(custom_iter); + + try_custom + } + } +} + +fn encode_impl( + variants: &[proc_macro2::Ident], + codes: &[JetBranchCode], +) -> proc_macro2::TokenStream { + let code_len = JET_ENC_BITLEN; + let codes_bits = codes.iter().map(|code| code.bits); + let bit_writer = StaticTokenInfo::bit_writer_path(); + + quote! { + fn encode(&self, w: &mut #bit_writer) -> std::io::Result { + if let Self::BaseJets(inner_jet) = self { + return inner_jet.encode(w); + } + + let (n, len) = match self { + #(Self::#variants => (#codes_bits, #code_len), )* + _ => unreachable!(), + }; + + w.write_bits_be(n as u64, len) + } + } +} + +fn src_trg_ty_impl( + variants: &[proc_macro2::Ident], + types: &[Vec], + mode: &str, +) -> proc_macro2::TokenStream { + let type_name_path = StaticTokenInfo::type_name_path(); + let source_or_target_ty = match mode { + "source" => quote::format_ident!("source_ty"), + "target" => quote::format_ident!("target_ty"), + _ => unreachable!(), + }; + quote! { + fn #source_or_target_ty(&self) -> #type_name_path { + if let Self::BaseJets(inner_jet) = self { + return inner_jet.#source_or_target_ty(); + } + + let name = match self { + #(Self::#variants => &[#(#types,)*],)* + _ => unreachable!(), + }; + + #type_name_path(name) + } + } +} + +fn cmr_impl(variants: &[proc_macro2::Ident]) -> proc_macro2::TokenStream { + let cmr_by_path = StaticTokenInfo::cmr_path(); + let cmrs = variants + .iter() + .map(|ident| { + let ident_str = ident.to_string(); + cmr(&pascal_to_snake_case(&ident_str)) + }) + .collect::>(); + + quote! { + fn cmr(&self) -> #cmr_by_path { + if let Self::BaseJets(inner_jet) = self { + return inner_jet.cmr(); + } + + let bytes = match self { + #(Self::#variants => [#(#cmrs,)*],)* + _ => unreachable!(), + }; + + #cmr_by_path::from_byte_array(bytes) + } + } +} + +fn c_jet_env_impl() -> proc_macro2::TokenStream { + quote! { + fn c_jet_env(env: &Self::Environment) -> &Self::CJetEnvironment { + // For the time being, we are goint to use the initial environment for unchained jets, + // as we are going to implement them in rust. + env + } + } +} + +fn associated_types_impl(unchained_env: &syn::Path) -> proc_macro2::TokenStream { + quote! { + type Environment = #unchained_env; + type CJetEnvironment = #unchained_env; + } +} + +fn build_jet_codes(variants: &[proc_macro2::Ident]) -> Vec { + variants + .iter() + .map(|ident| JetBranchCode::from_ident_fixed(ident.clone())) + .collect() +} + +fn build_target_tys(jets: &JetsInput) -> Vec> { + jets.jets + .iter() + .map(|jet| jet.target_type.value()) + .collect() +} + +fn build_source_tys(jets: &JetsInput) -> Vec> { + jets.jets + .iter() + .map(|jet| jet.source_type.value()) + .collect() +} diff --git a/jet_plugins/src/lib.rs b/jet_plugins/src/lib.rs new file mode 100644 index 0000000..bce1b8a --- /dev/null +++ b/jet_plugins/src/lib.rs @@ -0,0 +1,246 @@ +use quote::quote; +use syn::{ + Ident, LitByteStr, LitStr, Token, + parse::{Parse, ParseStream}, + parse2, + punctuated::Punctuated, +}; + +mod c_wrappers; +mod helpers; +mod jet_trait; +mod static_tokens; +use helpers::snake_to_pascal_case; + +use crate::{ + c_wrappers::impl_c_ffi, helpers::pascal_to_snake_case, jet_trait::jet_trait_full, + static_tokens::StaticTokenInfo, +}; + +const CUSTOM_JET_COST: u32 = 1000; + +/// Implements `Jet` trait and C FFI compatible with `simplicity_unchained_core::jets::jet_dyn` interface. +/// +/// ## Arguments +/// - `base_type` - a collection of jets the extension will be built on; +/// - `env` - environment type; +/// - `name: literal` - name of jet in snake case; +/// - `function: Fn(CFrameItem, CFrameItem, &ElementsUnchainedEnv)`; +/// +/// see `simplicity::jet::type_name` +/// +/// - `source_type: &[u8]` +/// - `target_type: &[u8]` +/// +/// ## Prerequisites +/// Make sure that input env type implements `Into`, where T is type, which base jets use as env for `c_jet_ptr()`. +/// +/// ## Usage +/// ```rust +/// use jet_plugins::register_jets; +/// use simplicity_unchained_core::jets::environments::ElementsUnchainedEnv; +/// use simplicity_unchained_core::__simplicity::simplicity::ffi::CFrameItem; +/// +/// fn custom_jet1(_dst: &mut CFrameItem, src: CFrameItem, env: &ElementsUnchainedEnv) -> bool { +/// false +/// } +/// +/// fn custom_jet2(_dst: &mut CFrameItem, src: CFrameItem, env: &ElementsUnchainedEnv) -> bool { +/// false +/// } +/// register_jets!( +/// simplicity_unchained_core::__simplicity::simplicity::jet::Elements, +/// simplicity_unchained_core::jets::environments::ElementsUnchainedEnv, +/// "custom_jet1" => custom_jet1, b"h", b"h", // source/target type +/// "custom_jet2" => custom_jet2, b"h", b"h", // source/target type +/// ); +/// ``` +#[proc_macro] +pub fn register_jets(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input: JetsInput = parse2(input.into()).expect("Failed to parse JetsInput"); + + let base_type = &input.base_type; + let unchained_env = &input.env_type; + let names = build_custom_fields(&input); + + let self_impl = self_impl_full(base_type, &names); + let jet_trait_impl = jet_trait_full(base_type, unchained_env, &names, &input); + let c_ffi = impl_c_ffi(base_type, unchained_env); + + quote! { + #self_impl + #jet_trait_impl + #c_ffi + } + .into() +} + +pub(crate) struct JetsInput { + base_type: syn::Path, + _comma1: Token![,], + env_type: syn::Path, + _comma2: Token![,], + jets: Punctuated, +} + +impl Parse for JetsInput { + fn parse(input: ParseStream) -> syn::Result { + Ok(JetsInput { + base_type: input.parse()?, + _comma1: input.parse()?, + env_type: input.parse()?, + _comma2: input.parse()?, + jets: input.parse_terminated(JetDef::parse, Token![,])?, + }) + } +} + +struct JetDef { + name: LitStr, + _arrow: Token![=>], + pub func: Ident, + _comma1: Token![,], + source_type: LitByteStr, + _comma2: Token![,], + target_type: LitByteStr, +} + +impl Parse for JetDef { + fn parse(input: ParseStream) -> syn::Result { + Ok(JetDef { + // TODO: guarantee that its non-empty and starts from letter + name: input.parse()?, + _arrow: input.parse()?, + func: input.parse()?, + _comma1: input.parse()?, + source_type: input.parse()?, + _comma2: input.parse()?, + target_type: input.parse()?, + }) + } +} + +fn self_impl_full(base_type: &syn::Path, names: &[proc_macro2::Ident]) -> proc_macro2::TokenStream { + let definition = enum_definition_impl(base_type, &names); + let all_impl = all_constant_impl(base_type, &names); + + quote! { + #definition + #all_impl + } +} + +fn enum_definition_impl( + base_type: &syn::Path, + variants: &[proc_macro2::Ident], +) -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + quote! { + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] + pub enum #enum_ident { + BaseJets(#base_type), + #( #variants, )* + } + } +} + +fn build_custom_fields(jets: &JetsInput) -> Vec { + jets.jets + .iter() + .map(|jet| quote::format_ident!("{}", snake_to_pascal_case(&jet.name.value()))) + .collect::>() +} + +fn all_constant_impl( + base_type: &syn::Path, + variants: &[proc_macro2::Ident], +) -> proc_macro2::TokenStream { + let enum_ident = StaticTokenInfo::enum_ident(); + let custom_jets_num = variants.len(); + let jet_self_handle = StaticTokenInfo::jet_self_handle(); + + quote! { + impl #enum_ident { + + // TODO: BaseJets local elements instead of dep + pub const ALL_JETS_NUM: usize = #base_type::ALL.len() + #custom_jets_num; + pub const ALL: [Self; Self::ALL_JETS_NUM] = Self::build_all_variants(); + + const fn build_all_variants() -> [Self; Self::ALL_JETS_NUM] { + + // consider moving it outside of macro + struct AllVariantsBuilder { + data: [std::mem::MaybeUninit; LEN], + len: usize, + } + + impl AllVariantsBuilder { + const fn new() -> Self { + Self { + data: [std::mem::MaybeUninit::uninit(); LEN], + len: 0, + } + } + + const fn push(&mut self, item: Enum) { + assert!(self.len < self.data.len()); + + self.data[self.len].write(item); + self.len += 1; + } + + const fn finalize(self) -> [Enum; LEN] { + assert!(self.len == LEN); + + let ptr = &self.data as *const [std::mem::MaybeUninit; LEN] as *const [Enum; LEN]; + let res = unsafe { std::ptr::read(ptr) }; + + std::mem::forget(self.data); + + res + } + } + + let mut builder = AllVariantsBuilder::new(); + + let mut i = 0; + + while i < #base_type::ALL.len() { + builder.push(#enum_ident::BaseJets(#base_type::ALL[i])); + i += 1; + } + + #(builder.push(#enum_ident::#variants);)* + builder.finalize() + } + + /// Returns index of `self` inside `Self::ALL` array + pub fn variant_to_index(&self) -> usize { + Self::ALL.iter().position(|x| x == self).expect("ALL must contain all enum's variants") + } + + pub fn variant_from_index(idx: usize) -> Self { + Self::ALL[idx] + } + + pub fn to_base_jet(&self) -> Option<#base_type> { + match self { + Self::BaseJets(jet) => Some(*jet), + _ => None + } + } + + pub fn from_base_jet(jet: #base_type) -> Self { + Self::BaseJets(jet) + } + } + + impl Into<#jet_self_handle> for &#enum_ident { + fn into(self) -> #jet_self_handle { + #jet_self_handle { + index: self.variant_to_index() + } + } + } + } +} diff --git a/jet_plugins/src/static_tokens.rs b/jet_plugins/src/static_tokens.rs new file mode 100644 index 0000000..b2a0f13 --- /dev/null +++ b/jet_plugins/src/static_tokens.rs @@ -0,0 +1,139 @@ +const STRUCT_EXTENSION_NAME: &str = "JetExtension"; + +const JET_TRAIT_PATH: &str = "simplicity_unchained_core::__simplicity::simplicity::jet::Jet"; +const BIT_WRITER_PATH: &str = "simplicity_unchained_core::__simplicity::simplicity::BitWriter"; +const BIT_ITER_PATH: &str = "simplicity_unchained_core::__simplicity::simplicity::BitIter"; +const CMR_PATH: &str = "simplicity_unchained_core::__simplicity::simplicity::Cmr"; +const COST_PATH: &str = "simplicity_unchained_core::__simplicity::simplicity::Cost"; +const INVALID_JET_ERR: &str = + "simplicity_unchained_core::__simplicity::simplicity::decode::Error::InvalidJet"; +const END_OF_STREAM_ERR: &str = + "simplicity_unchained_core::__simplicity::simplicity::decode::Error::EndOfStream"; +const DECODE_ERR_TY: &str = "simplicity_unchained_core::__simplicity::simplicity::decode::Error"; +const TYPE_NAME_PATH: &str = + "simplicity_unchained_core::__simplicity::simplicity::jet::type_name::TypeName"; +const CFRAME_ITEM_PATH: &str = + "simplicity_unchained_core::__simplicity::simplicity::ffi::CFrameItem"; +const SIMPLICITY_ERROR_TY: &str = "simplicity_unchained_core::__simplicity::simplicity::Error"; + +// C FFI stuff +const JET_SELF_HANDLE: &str = "simplicity_unchained_core::jets::custom_jet::CustomJet"; +const CMR_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::CmrHandle"; +const TYPENAME_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::TypeNameHandle"; +const BIT_WRITER_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::BitWriterHandle"; +const BIT_ITER_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::BitIterHandle"; +const C_ORDERING: &str = "simplicity_unchained_core::jets::jet_dyn::COrdering"; +const C_OPTION_ORDERING: &str = "simplicity_unchained_core::jets::jet_dyn::COptionOrdering"; +const HASHER_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::HasherHandle"; +const FMT_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::FmtHandle"; +const STR_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::StrHandle"; +const COST_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::CostHandle"; +const ALL_JETS_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::AllJetsHandle"; +const DECODE_RES_HANDLE: &str = "simplicity_unchained_core::jets::jet_dyn::DecodeResHandle"; + +pub(crate) struct StaticTokenInfo {} + +impl StaticTokenInfo { + pub fn enum_ident() -> proc_macro2::Ident { + quote::format_ident!("{}", STRUCT_EXTENSION_NAME) + } + + pub fn jet_trait_path() -> syn::Path { + syn::parse_str(JET_TRAIT_PATH).expect("Failed to find Jet trait by given path") + } + + pub fn bit_iter_path() -> syn::Path { + syn::parse_str(BIT_ITER_PATH).expect("Failed to find BitIter by given path") + } + + pub fn bit_writer_path() -> syn::Path { + syn::parse_str(BIT_WRITER_PATH).expect("Failed to find BitWriter by given path") + } + + pub fn invalid_jet_err() -> syn::Path { + syn::parse_str(INVALID_JET_ERR) + .expect("Failed to find simplicity::decode::Error::InvalidJet by given path") + } + + pub fn end_of_stream_err() -> syn::Path { + syn::parse_str(END_OF_STREAM_ERR) + .expect("Failed to find simplicity::decode::Error::EndOfStream by given path") + } + + pub fn decode_err() -> syn::Path { + syn::parse_str(DECODE_ERR_TY) + .expect("Failed to find simplicity::decode::Error by given path") + } + + pub fn cmr_path() -> syn::Path { + syn::parse_str(CMR_PATH).expect("Failed to find Cmr by given path") + } + + pub fn cost_path() -> syn::Path { + syn::parse_str(COST_PATH).expect("Failed to find Cost by given path") + } + + pub fn type_name_path() -> syn::Path { + syn::parse_str(TYPE_NAME_PATH).expect("Failed to find TypeName by given path") + } + + pub fn cframe_item_path() -> syn::Path { + syn::parse_str(CFRAME_ITEM_PATH).expect("Failed to find CFrame by given path") + } + + pub fn simplicity_error_ty() -> syn::Path { + syn::parse_str(SIMPLICITY_ERROR_TY).expect("Failed to find simplicity::Error by given path") + } + + pub fn jet_self_handle() -> syn::Path { + syn::parse_str(JET_SELF_HANDLE).expect("Failed to find JetSelfHandle by given path") + } + + pub fn cmr_handle() -> syn::Path { + syn::parse_str(CMR_HANDLE).expect("Failed to find CmrHandle by given path") + } + + pub fn typename_handle() -> syn::Path { + syn::parse_str(TYPENAME_HANDLE).expect("Failed to find TypeNameHandle by given path") + } + + pub fn bitwriter_handle() -> syn::Path { + syn::parse_str(BIT_WRITER_HANDLE).expect("Failed to find BitWriterHandle by given path") + } + + pub fn bititer_handle() -> syn::Path { + syn::parse_str(BIT_ITER_HANDLE).expect("Failed to find BitIterHandle by given path") + } + + pub fn c_ordering() -> syn::Path { + syn::parse_str(C_ORDERING).expect("Failed to find COrdering by given path") + } + + pub fn c_option_ordering() -> syn::Path { + syn::parse_str(C_OPTION_ORDERING).expect("Failed to find COptionOrdering by given path") + } + + pub fn hasher_handle() -> syn::Path { + syn::parse_str(HASHER_HANDLE).expect("Failed to find HasherHandle by given path") + } + + pub fn fmt_handle() -> syn::Path { + syn::parse_str(FMT_HANDLE).expect("Failed to find FmtHandle by given path") + } + + pub fn str_handle() -> syn::Path { + syn::parse_str(STR_HANDLE).expect("Failed to find StrHandle by given path") + } + + pub fn cost_handle() -> syn::Path { + syn::parse_str(COST_HANDLE).expect("Failed to find CostHandle by given path") + } + + pub fn all_jets_handle() -> syn::Path { + syn::parse_str(ALL_JETS_HANDLE).expect("Failed to find AllJetsHandle by given path") + } + + pub fn decode_res_handle() -> syn::Path { + syn::parse_str(DECODE_RES_HANDLE).expect("Failed to find DecodeResHandle by given path") + } +} diff --git a/plugin_test/Cargo.toml b/plugin_test/Cargo.toml new file mode 100644 index 0000000..15dc30d --- /dev/null +++ b/plugin_test/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "plugin_test" +version = "0.1.0" +edition = "2024" + +[dependencies] +hal-simplicity = { workspace = true } +core = { path = "../core" } +jet_plugins = { path = "../jet_plugins" } + +[lib] +crate-type = ["cdylib"] diff --git a/plugin_test/src/lib.rs b/plugin_test/src/lib.rs new file mode 100644 index 0000000..c2ee896 --- /dev/null +++ b/plugin_test/src/lib.rs @@ -0,0 +1,85 @@ +use hal_simplicity::simplicity::{elements::opcodes::all::OP_PUSHBYTES_33, ffi::CFrameItem}; +use jet_plugins::register_jets; +use simplicity_unchained_core::jets::environments::ElementsUnchainedEnv; + +#[allow(unused)] +unsafe extern "C" { + fn rustsimplicity_0_6_write8(dst: *mut CFrameItem, n: u64); + fn rustsimplicity_0_6_write16(dst: *mut CFrameItem, n: u16); + fn rustsimplicity_0_6_write32(dst: *mut CFrameItem, n: u32); + fn rustsimplicity_0_6_write64(dst: *mut CFrameItem, n: u64); + + fn rustsimplicity_0_6_read8(src: *const CFrameItem) -> u64; + fn rustsimplicity_0_6_read16(src: *const CFrameItem) -> u16; + fn rustsimplicity_0_6_read32(src: *const CFrameItem) -> u32; + fn rustsimplicity_0_6_read64(src: *const CFrameItem) -> u64; +} + +/// 2^8 |- 2^8 +/// +/// Returns the opcode at the given index in the redeem script, each opcode is one byte. +pub fn get_opcode_from_script( + dst: &mut CFrameItem, + src: CFrameItem, + env: &ElementsUnchainedEnv, +) -> bool { + let index = unsafe { rustsimplicity_0_6_read8(&src as *const CFrameItem) } as usize; + if index >= env.redeem_script.len() { + return false; + } + + let opcode = env.redeem_script.as_bytes()[index]; + + unsafe { + rustsimplicity_0_6_write8(dst as *mut CFrameItem, opcode as u64); + } + + true +} +/// 2^8 |- 2^256 +/// +/// Each pubkey is encoded as: [OP_PUSHBYTES_33][0x02 or 0x03][32 bytes X coordinate] +pub fn get_pubkey_from_script( + dst: &mut CFrameItem, + src: CFrameItem, + env: &ElementsUnchainedEnv, +) -> bool { + let start_index = unsafe { rustsimplicity_0_6_read8(&src as *const CFrameItem) } as usize; + if start_index + 34 > env.redeem_script.len() { + return false; + } + + let push_bytes_opcode = env.redeem_script.as_bytes()[start_index]; + if push_bytes_opcode != OP_PUSHBYTES_33.into_u8() { + return false; + } + + let prefix = env.redeem_script.as_bytes()[start_index + 1]; + if prefix != 0x02 && prefix != 0x03 { + return false; + } + + let x_only_pubkey_start = start_index + 2; + + let pubkey_bytes = &env.redeem_script.as_bytes()[x_only_pubkey_start..x_only_pubkey_start + 32]; + + let words: Vec = pubkey_bytes + .chunks(4) + .map(|chunk| u32::from_be_bytes(chunk.try_into().expect("Chunk with incorrect length"))) + .collect::>(); + + for word in words.iter() { + unsafe { + rustsimplicity_0_6_write32(dst as *mut CFrameItem, *word); + } + } + + true +} + +register_jets!( + hal_simplicity::simplicity::jet::Elements, + simplicity_unchained_core::jets::environments::ElementsUnchainedEnv, + "get_opcode_from_script" => get_opcode_from_script, b"c", b"c", + "get_pubkey_from_script" => get_pubkey_from_script, b"c", b"h", +); diff --git a/plugin_test/src/lib1.rs b/plugin_test/src/lib1.rs new file mode 100644 index 0000000..b2fc724 --- /dev/null +++ b/plugin_test/src/lib1.rs @@ -0,0 +1,42 @@ +use hal_simplicity::simplicity::ffi::CFrameItem; +use jet_plugins::register_jets; + +use simplicity_unchained_core::jets::environments::{BitcoinUnchainedEnv, ElementsUnchainedEnv}; + +pub fn custom_jet1_elements( + _dst: &mut CFrameItem, + _: CFrameItem, + _: &ElementsUnchainedEnv, +) -> bool { + false +} + +pub fn custom_jet2_elements( + _dst: &mut CFrameItem, + _: CFrameItem, + _: &ElementsUnchainedEnv, +) -> bool { + false +} + +pub fn custom_jet1_bitcoin(_dst: &mut CFrameItem, _: CFrameItem, _: &BitcoinUnchainedEnv) -> bool { + false +} + +pub fn custom_jet2_bitcoin(_dst: &mut CFrameItem, _: CFrameItem, _: &BitcoinUnchainedEnv) -> bool { + false +} + +//register_jets!( +// hal_simplicity::simplicity::jet::Bitcoin, +// simplicity_unchained_core::jets::environments::BitcoinUnchainedEnv, +// "custom_jet1" => custom_jet1_bitcoin, b"h", b"h", +// "custom_jet2" => custom_jet2_bitcoin, b"h", b"h", +//); + +register_jets!( + hal_simplicity::simplicity::jet::Elements, + simplicity_unchained_core::jets::environments::ElementsUnchainedEnv, + "custom_jet1" => custom_jet1_elements, b"h", b"h", + "custom_jet2" => custom_jet2_elements, b"h", b"h", +); diff --git a/plugin_test/src/main.rs b/plugin_test/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/plugin_test/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/service/Cargo.toml b/service/Cargo.toml index 1cc1f0f..145bbed 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -20,3 +20,4 @@ validator = { version = "0.20.0", features = ["derive"] } hal-simplicity = { workspace = true } core = { path = "../core" } +dlopen2 = "0.8.2"