diff --git a/.github/scripts/setup-simplex b/.github/scripts/setup-simplex index 57be0614..3f1ff180 100644 --- a/.github/scripts/setup-simplex +++ b/.github/scripts/setup-simplex @@ -41,7 +41,7 @@ main() { SIMPLEX_BIN_DIR="$SIMPLEX_DIR/bin" BIN_PATH="$SIMPLEX_BIN_DIR/simplexup" - # Ensure bin directory exists and download simplexup into it + # Create the bin directory and download simplexup there mkdir -p "$SIMPLEX_BIN_DIR" say "Fetching ${FILE_PATH} from ${RAW_URL}" diff --git a/Cargo.toml b/Cargo.toml index 9a9cbb0f..fe040b8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" edition = "2024" [workspace.dependencies] -smplx-macros = { path = "./crates/macros", version = "=0.2.0" } +smplx-macros = { path = "./crates/macros", version = "=0.2.0", default-features = false } smplx-build = { path = "./crates/build", version = "=0.2.0" } smplx-test = { path = "./crates/test", version = "=0.2.0" } smplx-regtest = { path = "./crates/regtest", version = "=0.2.0" } diff --git a/SECURITY.md b/SECURITY.md index 6975097d..111a2a8a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,8 +8,8 @@ In your report, please include as much information as you can, including: * a description of the vulnerability and how it could be exploited * its potential impact (e.g. privacy leak, denial of service, theft of funds) -* steps or code for reproducing it -* a proposed patch for remedying it +* steps or code for reproducing the vulnerability +* a proposed patch, if you have one Also, provide us with a secure means to contact you with any follow up questions we might have. @@ -17,7 +17,7 @@ Also, provide us with a secure means to contact you with any follow up questions Please take care not to violate the privacy of users in your report. For example, stack traces or exploit scripts sent to us should never contain private keys or personally identifiable information. -Give us at least one week to investigate the vulnerability you found and up to 90 days to fix it. Also, please give us reasonable advanced notice if at any point you intend to disclose the vulnerability to anyone else. +Give us at least one week to investigate the vulnerability you found and up to 90 days to fix the issue. Also, please give us reasonable advanced notice if at any point you intend to disclose the vulnerability to anyone else. In general, please investigate and report bugs in a way that makes a reasonable, good faith effort not to be disruptive or harmful to us, this software's users, or the users of dependent projects. diff --git a/crates/build/src/config/dependency.rs b/crates/build/src/config/dependency.rs index 20b5b275..7bf75e32 100644 --- a/crates/build/src/config/dependency.rs +++ b/crates/build/src/config/dependency.rs @@ -60,7 +60,7 @@ struct RawDependency { rev: Option, /// The specific tag to download (only applicable if `git` is provided). tag: Option, - /// The specific branch to download (only applicable if `branch` is provided). + /// The specific branch to download (only applicable if `git` is provided). branch: Option, /// The specific directory with contracts to download (only applicable if `git` is provided). package: Option, diff --git a/crates/build/src/generator.rs b/crates/build/src/generator.rs index 382a0da4..b67d31b7 100644 --- a/crates/build/src/generator.rs +++ b/crates/build/src/generator.rs @@ -113,7 +113,7 @@ impl ArtifactsGenerator { .map_err(|e| BuildError::DependencyMap(e.to_string())) } - /// Processes a single `.simf` source file and write it to metadata: + /// Processes a single `.simf` source file into its metadata: /// - Writes its content to the mirrored path under `simf_out_dir` /// - Extracts the contract name /// diff --git a/crates/build/src/resolver.rs b/crates/build/src/resolver.rs index c3665425..98c42ac4 100644 --- a/crates/build/src/resolver.rs +++ b/crates/build/src/resolver.rs @@ -184,7 +184,7 @@ impl ArtifactsResolver { let repo_name = clean_url.split('/').next_back()?; // Only fields that are actually set take part in the key, so adding a new - // optional field never changes the directory of dependencies that don't use it. + // optional field never changes the directory of dependencies that don't set the field. let reference = reference.map(|reference| match reference { GitRef::Rev(rev) => format!("rev={rev}"), GitRef::Tag(tag) => format!("tag={tag}"), diff --git a/crates/cli/src/commands/error.rs b/crates/cli/src/commands/error.rs index 3ccf9c20..25981e2a 100644 --- a/crates/cli/src/commands/error.rs +++ b/crates/cli/src/commands/error.rs @@ -29,8 +29,8 @@ pub enum CommandError { #[error("IO error: {0}")] Io(#[from] std::io::Error), - #[error("Verbosity level should be either -v or -vv, got: -v x {0}")] - BadVersbosityMode(u8), + #[error("Verbosity must be -v or -vv, got {0} -v flags")] + BadVerbosityMode(u8), #[error("test command failed with exit status {0}")] TestFailed(i32), diff --git a/crates/cli/src/commands/fmt.rs b/crates/cli/src/commands/fmt.rs index b3c5073f..5f99dad3 100644 --- a/crates/cli/src/commands/fmt.rs +++ b/crates/cli/src/commands/fmt.rs @@ -141,12 +141,10 @@ impl Format { Ok(Self::normalize_status(status)) } - /// Resolves the file path to the `simfmt` binary in the same directory where `simplex` is situated. - /// Given that `simfmt` would be placed in one directory with `simplex`, this is suitable for our usecase. + /// The path to the `simfmt` binary next to the `simplex` binary, where `simplexup` installs both. /// /// # Warning - /// Can fail to search for binary when env isn't set and `simfmt` binary lies in the other directory. - /// Even when `simfmt` is visible through PATH. + /// Doesn't find a `simfmt` in another directory, even one on `PATH`, unless the env variable is set. #[inline] fn simfmt_binary() -> Result { Ok(env::current_exe() diff --git a/crates/cli/src/commands/install.rs b/crates/cli/src/commands/install.rs index d6caa9ec..620c8637 100644 --- a/crates/cli/src/commands/install.rs +++ b/crates/cli/src/commands/install.rs @@ -203,7 +203,7 @@ impl Install { /// at `reference`, keeping only `package` when it is set. /// /// 1. Clone the history without contents and any checks. - /// 2. If `package` is set, restrict the checkout to it. + /// 2. If `package` is set, restrict the checkout to that directory. /// 3. Check out `rev` (or the cloned ref), downloading only the files it needs. fn git_commands(url: &str, reference: Option<&GitRef>, package: Option<&str>, target_dir: &Path) -> Vec { let mut clone = Command::new("git"); diff --git a/crates/cli/src/commands/test.rs b/crates/cli/src/commands/test.rs index 69f2f843..4080630b 100644 --- a/crates/cli/src/commands/test.rs +++ b/crates/cli/src/commands/test.rs @@ -7,7 +7,7 @@ use smplx_test::{TestConfig, smplx_test_marker}; use super::core::{TestArguments, TestFlags}; use super::error::CommandError; -/// Nextest dsl variable to filter and use only simplex tests +/// Nextest filter expression that selects only Simplex tests. const SMPLX_NEXTEST_DSL_TEST_MARKER: &str = concat!("test(/", smplx_test_marker!(), "$/)"); const SMPLX_NEXTEST_FUZZ_DSL_TEST_MARKER: &str = concat!("test(/", smplx_test_marker!(fuzz), "$/)"); const DEFAULT_THREADS_NUMBER: usize = 1; @@ -26,7 +26,7 @@ impl Test { let cache_path = Self::get_test_config_cache_name()?; if flags.verbose > Verbosity::MAX_VERBOSITY_LEVEL { - return Err(CommandError::BadVersbosityMode(flags.verbose)); + return Err(CommandError::BadVerbosityMode(flags.verbose)); } config.verbosity = std::cmp::max(config.verbosity, Verbosity::new(flags.verbose)); diff --git a/crates/cli/src/config/error.rs b/crates/cli/src/config/error.rs index b67ee5a1..7c15cf97 100644 --- a/crates/cli/src/config/error.rs +++ b/crates/cli/src/config/error.rs @@ -29,7 +29,7 @@ pub enum ConfigError { #[error("Unable to get env variable: {0}")] UnableToGetEnv(#[from] std::env::VarError), - #[error("Path doesn't a file: '{0}'")] + #[error("Path isn't a file: '{0}'")] PathIsNotFile(PathBuf), #[error("Path doesn't exist: '{0}'")] diff --git a/crates/macros/Cargo.toml b/crates/macros/Cargo.toml index c9b66e19..e35d7648 100644 --- a/crates/macros/Cargo.toml +++ b/crates/macros/Cargo.toml @@ -13,8 +13,12 @@ proc-macro = true [lints] workspace = true +[features] +default = ["test"] +test = ["dep:smplx-test"] + [dependencies] smplx-build = { workspace = true } -smplx-test = { workspace = true } +smplx-test = { workspace = true, optional = true } syn = { version = "3.0.8", default-features = false, features = ["parsing", "proc-macro"] } diff --git a/crates/macros/src/lib.rs b/crates/macros/src/lib.rs index 6842b40f..fcfc964a 100644 --- a/crates/macros/src/lib.rs +++ b/crates/macros/src/lib.rs @@ -12,6 +12,7 @@ pub fn include_simf(tokenstream: TokenStream) -> TokenStream { } } +#[cfg(feature = "test")] #[proc_macro_attribute] pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::ItemFn); @@ -22,6 +23,7 @@ pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { } } +#[cfg(feature = "test")] #[proc_macro_attribute] pub fn fuzz(args: TokenStream, input: TokenStream) -> TokenStream { let input = syn::parse_macro_input!(input as syn::ItemFn); diff --git a/crates/regtest/src/args.rs b/crates/regtest/src/args.rs index bb950474..2ae05a32 100644 --- a/crates/regtest/src/args.rs +++ b/crates/regtest/src/args.rs @@ -24,7 +24,7 @@ pub(super) fn get_elementsd_bin_args(config: &RegtestConfig) -> Vec { ]; if let Some(port) = config.rpc_port { - // A lib works with autoassigned rpcport, so we are required to do this woodoo + // The node library picks the RPC port itself, so a fixed port needs explicit bind settings args.push("-rpcbind=127.0.0.1".to_string()); args.push(format!("-rpcbind=127.0.0.1:{port}")); args.push("-rpcallowip=127.0.0.1/8".to_string()); diff --git a/crates/sdk/src/constants.rs b/crates/sdk/src/constants.rs index cd5d7044..adb54429 100644 --- a/crates/sdk/src/constants.rs +++ b/crates/sdk/src/constants.rs @@ -1,7 +1,10 @@ -/// Dummy signature, which is used for fee estimation +/// A placeholder for a witness signature that the signer replaces, for example at the end of a +/// [`ProgramSignature::at_path`](crate::transaction::ProgramSignature::at_path) path. pub const DUMMY_SIGNATURE: [u8; 64] = [1; 64]; -/// Minimal acceptable fee for nodes to relay a transaction +/// The smallest policy-asset surplus `Signer::finalize_strict` accepts, and the smallest blinded +/// change output the signer creates. It isn't a minimum fee: the fee follows the fee rate and can +/// be smaller. pub const MIN_FEE: u64 = 10; /// The smallest explicit output Elements relays by default for any standard script. diff --git a/crates/sdk/src/global.rs b/crates/sdk/src/global.rs index 6333af95..5e8825ca 100644 --- a/crates/sdk/src/global.rs +++ b/crates/sdk/src/global.rs @@ -6,18 +6,19 @@ use crate::program::TrackerLogLevel; static GLOBAL_CONFIG: OnceLock = OnceLock::new(); -/// A structure to represent the global configuration settings for the application. +/// The process-wide verbosity of program execution. +/// +/// Verbosity `Debug` or `Trace` also compiles programs with debug symbols, which changes their +/// CMRs and addresses. An application that works with contracts deployed by others should leave +/// verbosity at `None`, or set [`Program::with_debug_symbols`](crate::program::Program::with_debug_symbols) +/// on each program. #[derive(Clone, Copy, Debug, Default)] pub struct GlobalConfig { verbosity: Verbosity, } impl GlobalConfig { - /// Sets the global configuration for the SDK. - /// - /// This function allows setting a global configuration which includes - /// the logging level for `simplicity` contracts execution. - /// It must be called exactly once during the application's lifetime. + /// Sets the process-wide verbosity. It can be set only once per process. /// /// # Errors /// Returns an error containing the newly created `GlobalConfig` if the global configuration has already been initialized. @@ -25,7 +26,8 @@ impl GlobalConfig { GLOBAL_CONFIG.set(GlobalConfig { verbosity }) } - /// Returns the default log level if `GLOBAL_CONFIG` is not initialized + /// The tracker log level for the configured verbosity, or `TrackerLogLevel::None` if it was + /// never set. pub fn get_log_level() -> TrackerLogLevel { GLOBAL_CONFIG .get() diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index c6e8dfcf..5baba68b 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -14,7 +14,6 @@ pub mod program; pub mod provider; /// Traits and mechanisms for signing transactions and satisfying witness requirements. pub mod signer; -/// Constructs and builders for assembling, tracking, and managing Elements transactions. pub mod transaction; /// General utility functions, conversions, and helper tools. pub mod utils; diff --git a/crates/sdk/src/program/core.rs b/crates/sdk/src/program/core.rs index 722a2324..726e453a 100644 --- a/crates/sdk/src/program/core.rs +++ b/crates/sdk/src/program/core.rs @@ -20,9 +20,12 @@ use super::error::ProgramError; use crate::provider::SimplicityNetwork; use crate::utils::{check_budget, hash_script, tap_data_hash, tr_unspendable_key}; -/// Executes `simplicity` programs at runtime. +/// What the [`Signer`](crate::signer::Signer) needs from a program to sign for and finalize a +/// program input. /// -/// This trait defines a core behavior related to testing and execution. +/// [`Program`] implements it, as does `&T` for any `T: ProgramTrait`. +/// [`ProgramInput`](crate::transaction::ProgramInput) holds a `Box`, which is +/// `Clone`. pub trait ProgramTrait: DynClone + Send + Sync { /// Retrieves the types of arguments required by a `simplicity` program. /// @@ -36,10 +39,13 @@ pub trait ProgramTrait: DynClone + Send + Sync { /// Returns a `ProgramError` if parsing or generating ABI metadata fails. fn get_witness_types(&self) -> Result; - /// Constructs the Elements environment for a specified input index, PST, and network for further program execution. + /// The Simplicity transaction environment for input `input_index` of `pst`: what the + /// program's transaction jets, such as `jet::sig_all_hash()`, read. /// /// # Errors - /// Returns a `ProgramError` if the input index is out of bounds or if the script pubkey of the UTXO mismatches the expected program script. + /// Returns `ProgramError::MissingWitnessUtxo` if an input of `pst` doesn't carry the output it + /// spends, `ProgramError::UtxoIndexOutOfBounds` if `input_index` is past the inputs, and + /// `ProgramError::ScriptPubkeyMismatch` if the input doesn't spend this program's address. fn get_env( &self, pst: &PartiallySignedTransaction, @@ -47,14 +53,22 @@ pub trait ProgramTrait: DynClone + Send + Sync { network: &SimplicityNetwork, ) -> Result>, ProgramError>; - /// Executes a Simplicity program for the given input index of a partially signed transaction. + /// Runs the program locally on input `input_index` of `pst` with `witness`: satisfies it, + /// prunes it, and runs the pruned program on the Bit Machine. Returns the pruned program and + /// its output. /// - /// This function evaluates a Simplicity script associated with a specific transaction input - /// in a given network, producing the result of the computation along with the redeem node - /// used during execution. + /// Doesn't check the witness budget; [`ProgramTrait::finalize`] does. With [`Verbosity`](crate::global::Verbosity) + /// `Debug` or `Trace`, the execution output is buffered, not printed: call + /// [`ProgramLogger::flush_logs`] to print the output. /// /// # Errors - /// Returns a `ProgramError` if loading the program, satisfying the witness, retrieving the environment, or executing the `BitMachine` fails. + /// - `ProgramError::WitnessSatisfaction` if the witness doesn't fit the program's witness types. + /// - `ProgramError::Pruning` if the program fails. A failed `assert!` or jet is + /// `ExecutionError::JetFailed`. Reaching a pruned branch, for example with `unwrap(None)`, is + /// `ExecutionError::ReachedPrunedBranch`. [`Program`] reports a failure in the Bit Machine run + /// after pruning with this variant too. + /// - `ProgramError::BitMachineCreation` if the program exceeds the Bit Machine's limits. + /// - The errors of [`ProgramTrait::get_env`] and of compiling the program. fn execute( &self, pst: &PartiallySignedTransaction, @@ -63,14 +77,16 @@ pub trait ProgramTrait: DynClone + Send + Sync { network: &SimplicityNetwork, ) -> Result<(Arc, Value), ProgramError>; - /// Finalizes and returns `pruned_witness` as output after executing the program on certain parameters. + /// Runs the program as [`ProgramTrait::execute`] does and returns the input's witness stack: + /// `[witness, program, cmr, control_block]`. /// /// Implementations must compare the pruned program's static cost with the budget from the /// exact serialized witness stack they return. Use [`check_budget`] for this check, /// as the built-in `Program` does. /// /// # Errors - /// Returns a `ProgramError` if execution, control block construction or budget validation fails. + /// Returns the errors of [`ProgramTrait::execute`], and `ProgramError::InsufficientBudget` if the + /// witness stack doesn't pay for the program's execution cost. fn finalize( &self, pst: &PartiallySignedTransaction, @@ -81,6 +97,9 @@ pub trait ProgramTrait: DynClone + Send + Sync { } /// Static description of a program's inputs and integer literals. Implemented by `include_simf!()`. +/// +/// The fuzz engine uses it to generate arguments and witnesses for a program it knows only by +/// type. pub trait ProgramSchema { /// Generated arguments struct. type Arguments; @@ -94,22 +113,31 @@ pub trait ProgramSchema { /// Witness names and types. fn witness_types() -> Vec<(TemplateProgramWitness, ResolvedType)>; - /// Integer literals in the source. + /// The integer literals in the source, sorted and without duplicates. fn literals() -> Vec; } -/// A trait for creating instances of a program. The `ProgramFactory` trait defines a mechanism -/// for constructing and returning a program instance of a type that implements `AsRef`. -/// Only by generic struct name we have a possibility to create an instance of a program. +/// Creates a program of type `P` from arguments, for code that knows the program only by type, +/// such as the fuzz engine. Implemented by `include_simf!()`. pub trait ProgramFactory + Sized> { /// Instantiates a program with the given arguments. fn instantiate_program(args: impl Into) -> Box

; } -/// Represents a program structure containing its public key, compiled program, and associated storage. -/// The compiled program with its CMR and the taproot spend info act as caches, filled on first use. +/// A `SimplicityHL` program with its arguments, its Taproot internal key, and its storage slots. +/// +/// The program compiles on first use, and the result is cached and shared between clones. The +/// getters that need the compiled program panic if it doesn't compile. Call [`Program::load`] +/// to get the compile error instead. +/// +/// The program's address is a Taproot output. Its script tree holds the program as one leaf and +/// one hidden leaf per storage slot, `tagged_hash("TapData", slot)`. The tree is left-folded: +/// `tapbranch(tapbranch(tapbranch(program, slot0), slot1), slot2)`. So the source, the arguments +/// and the debug-symbol setting determine the CMR, and the address also depends on the internal +/// key and the storage. /// -/// Abstraction giving the power to execute Simplicity contracts without specifying any additional parameters. +/// Code generated by `simplex build` wraps a `Program` in one type per contract. Call `as_ref()` +/// on that type to get the `Program`. #[derive(Clone)] pub struct Program { source: Arc, @@ -287,10 +315,14 @@ impl Program { /// The width of a storage slot. pub const STORAGE_SLOT_BYTES: usize = 32; - /// Each slot deepens the taproot tree by one and the tree is at most 128 layers deep. + /// The most storage slots a program can have. Each slot deepens the Taproot tree by one, and + /// the tree is at most 128 layers deep. pub const MAX_STORAGE_SLOTS: usize = 128; - /// Creates a new instance of the struct with the provided source string and arguments. + /// Creates a program from `SimplicityHL` source and its `param::` arguments. The source + /// compiles later, on first use. + /// + /// The Taproot internal key is [`tr_unspendable_key`], and there aren't any storage slots. #[must_use] pub fn new(source: impl Into>, arguments: impl Into) -> Self { Self { @@ -304,8 +336,7 @@ impl Program { } } - /// Sets the `pub_key` field of the struct to the provided `XOnlyPublicKey` value and returns the updated builder instance. - /// This is used to set the taproot public key for the program. + /// Sets the Taproot internal key, which changes the address. #[must_use] pub fn with_taproot_pubkey(mut self, pub_key: XOnlyPublicKey) -> Self { self.pub_key = pub_key; @@ -314,7 +345,11 @@ impl Program { self } - /// Builds this program in the mode the protocol declares. + /// Compiles the program with or without debug symbols, overriding [`GlobalConfig`]. + /// + /// Debug symbols change the CMR and so the address. A program whose address must match one + /// computed elsewhere, such as a deployed contract, should set this explicitly, because + /// verbosity `Debug` or `Trace` turns debug symbols on by default. #[must_use] pub fn with_debug_symbols(mut self, include: bool) -> Self { self.include_debug_symbols = Some(include); @@ -325,7 +360,7 @@ impl Program { self } - /// Sets storage capacity for further usage. + /// Replaces the storage with `capacity` zeroed slots. /// /// # Panics /// Panics if `capacity` exceeds `MAX_STORAGE_SLOTS`. @@ -383,31 +418,31 @@ impl Program { &self.arguments } - /// Returns the number of storage chunks for a program. + /// The number of storage slots. #[must_use] pub fn get_storage_len(&self) -> usize { self.storage.len() } - /// Returns storage as a whole array of 32-byte chunks. + /// The storage slots, each [`Program::STORAGE_SLOT_BYTES`] long. #[must_use] pub fn get_storage(&self) -> &[Vec] { &self.storage } - /// Returns storage value at a certain index. + /// The storage slot at `index`. /// /// # Panics - /// Panics if the `index` is out of bounds for the initiated storage. + /// Panics if the `index` is out of bounds for the initialized storage. #[must_use] pub fn get_storage_at(&self, index: usize) -> Vec { self.storage[index].clone() } - /// Returns a taproot address for a defined `SimplicityNetwork`. + /// The program's unconfidential Taproot address on `network`. /// /// # Panics - /// Panics if generating the taproot spending information fails. + /// Panics if the program doesn't compile. #[must_use] pub fn get_tr_address(&self, network: &SimplicityNetwork) -> Address { let spend_info = self.taproot_spending_info().unwrap_or_else(|error| panic!("{error}")); @@ -421,19 +456,27 @@ impl Program { ) } - /// Retrieves the `ScriptPubKey` associated with the Simplicity address for the specified network. + /// The script of the program's address on `network`, which locks the outputs the program + /// controls. + /// + /// # Panics + /// Panics if the program doesn't compile. #[must_use] pub fn get_script_pubkey(&self, network: &SimplicityNetwork) -> Script { self.get_tr_address(network).script_pubkey() } - /// Retrieves the 32-byte `ScriptPubKey` hash associated with the Simplicity address for the specified network. + /// The SHA-256 hash of [`Program::get_script_pubkey`], as Esplora's `scripthash` endpoints + /// and the `jet::output_script_hash` family use. + /// + /// # Panics + /// Panics if the program doesn't compile. #[must_use] pub fn get_script_hash(&self, network: &SimplicityNetwork) -> [u8; 32] { hash_script(&self.get_script_pubkey(network)) } - /// Compiles the program and returns its Commitment Merkle Root. + /// The program's Commitment Merkle Root. /// /// # Panics /// Panics if the `SimplicityHL` compilation fails. @@ -442,7 +485,7 @@ impl Program { self.cmr().unwrap_or_else(|error| panic!("{error}")).to_byte_array() } - /// Returns the 32-byte tapleaf hash of the program's Simplicity script. + /// The tapleaf hash of the program's leaf. Storage doesn't affect this hash. /// /// # Panics /// Panics if the `SimplicityHL` compilation fails. @@ -453,7 +496,7 @@ impl Program { taproot::TapLeafHash::from_script(&script, version).to_byte_array() } - /// Retrieves program ABI metadata for argument types. + /// The names and types of the program's `param::` arguments. /// /// # Errors /// Returns a `ProgramError` if compilation fails or generating ABI metadata fails. @@ -466,7 +509,7 @@ impl Program { Ok(abi_meta.param_types) } - /// Retrieves the witness types of the compiled program. + /// The names and types of the program's `witness::` fields. /// /// # Errors /// Returns a `ProgramError` if compilation fails. @@ -474,10 +517,10 @@ impl Program { Ok(self.load()?.witness_types().shallow_clone()) } - /// Compiles program by providing saved `Arguments`. + /// Compiles the program, or returns the cached result of an earlier compilation. /// /// # Errors - /// Retruns an error we have problems in a program compilation. + /// Returns `ProgramError::Compilation` if the source doesn't compile with these arguments. pub fn load(&self) -> Result<&CompiledProgram, ProgramError> { Ok(&self.load_with_cmr()?.0) } @@ -597,7 +640,7 @@ mod tests { } #[test] - fn storage_slot_takes_a_full_width_value() { + fn storage_slot_accepts_32_bytes() { let mut program = stateful(1); program.set_storage_at(0, vec![0x11; Program::STORAGE_SLOT_BYTES]); @@ -607,18 +650,18 @@ mod tests { #[test] #[should_panic(expected = "A storage slot is 32 bytes, and this one is 1")] - fn storage_slot_refuses_a_short_value() { + fn storage_slot_rejects_a_short_value() { stateful(1).set_storage_at(0, vec![0x11]); } #[test] #[should_panic(expected = "A storage slot is 32 bytes, and this one is 33")] - fn storage_slot_refuses_a_long_one() { + fn storage_slot_rejects_a_long_value() { stateful(1).set_storage_at(0, vec![0x11; 33]); } #[test] - fn changing_a_slot_changes_the_address_but_not_the_program() { + fn storage_changes_the_address_but_not_the_cmr() { let one = stateful(1); let network = SimplicityNetwork::LiquidTestnet; let address = one.get_tr_address(&network); @@ -679,7 +722,7 @@ mod tests { } #[test] - fn compiles_once_and_keeps_it() { + fn program_is_compiled_once() { let program = dummy_program(); let first = program.load().expect("the dummy program compiles"); @@ -689,7 +732,7 @@ mod tests { } #[test] - fn changed_build_mode_is_not_served_the_old_compilation() { + fn debug_symbols_change_the_cmr() { let plain = dummy_program(); let plain_cmr = plain.get_cmr(); @@ -700,7 +743,7 @@ mod tests { } #[test] - fn test_get_env_idx() { + fn get_env_checks_the_script_pubkey() { let program = dummy_program(); let network = dummy_network(); @@ -721,7 +764,7 @@ mod tests { ..Default::default() }); - // take a script with a wrong pubkey + // Input 0 spends a script that is not the program's assert!(matches!( program.get_env(&pst, 0, &network).unwrap_err(), ProgramError::ScriptPubkeyMismatch { .. } @@ -731,7 +774,7 @@ mod tests { } #[test] - fn input_without_its_utxo_is_an_error() { + fn input_without_witness_utxo_is_an_error() { let network = SimplicityNetwork::default_regtest(); let program = stateful(0); let mut pst = PartiallySignedTransaction::new_v2(); @@ -752,7 +795,7 @@ mod tests { } #[test] - fn storage_is_at_most_as_deep_as_the_taproot_tree() { + fn max_storage_slots_fit_the_taproot_tree() { let network = SimplicityNetwork::default_regtest(); let _ = stateful(Program::MAX_STORAGE_SLOTS).get_tr_address(&network); @@ -760,12 +803,12 @@ mod tests { #[test] #[should_panic(expected = "A program holds at most 128 storage slots, and this one asks for 129")] - fn storage_beyond_the_taproot_tree_is_refused() { + fn too_many_storage_slots_panic() { let _ = stateful(Program::MAX_STORAGE_SLOTS + 1); } #[test] - fn left_folded_depths() { + fn taproot_leaf_depths_are_left_folded() { assert_eq!(Program::taproot_leaf_depths(1), vec![0]); assert_eq!(Program::taproot_leaf_depths(2), vec![1, 1]); assert_eq!(Program::taproot_leaf_depths(3), vec![2, 2, 1]); @@ -773,10 +816,10 @@ mod tests { assert_eq!(Program::taproot_leaf_depths(5), vec![4, 4, 3, 2, 1]); } - // The measured boundary: a balanced tree agrees with a left fold up to three leaves and - // diverges from four. Four leaves balanced is [2, 2, 2, 2]; left-folded it is not. + // A balanced tree and a left fold agree up to three leaves. With four leaves, a balanced tree + // has depths [2, 2, 2, 2] and a left fold doesn't. #[test] - fn diverges_from_a_balanced_tree_at_four_leaves() { + fn left_fold_differs_from_a_balanced_tree_at_four_leaves() { assert_eq!(Program::taproot_leaf_depths(3), vec![2, 2, 1]); assert_ne!(Program::taproot_leaf_depths(4), vec![2, 2, 2, 2]); } diff --git a/crates/sdk/src/program/error.rs b/crates/sdk/src/program/error.rs index 63252324..4a62e667 100644 --- a/crates/sdk/src/program/error.rs +++ b/crates/sdk/src/program/error.rs @@ -1,7 +1,7 @@ /// Errors that can occur when compiling, preparing, and executing Simplicity programs. #[derive(Debug, thiserror::Error)] pub enum ProgramError { - /// The serialized witness stack does not buy enough budget to cover the program's execution cost. + /// The witness budget, set by the serialized witness stack size, doesn't cover the program's execution cost. /// /// Consensus requires `cost <= 1000 * (serialized witness stack size + 50)` milli weight units. /// The 50 is Elements' `VALIDATION_WEIGHT_OFFSET` @@ -15,7 +15,7 @@ pub enum ProgramError { InsufficientBudget { /// Static execution cost of the pruned program, rounded up to whole weight units. cost_wu: u64, - /// Budget bought by the witness stack: its serialized size plus 50 weight units. + /// Witness budget: the serialized witness stack size plus 50 weight units. budget_wu: u64, /// Serialized size of the witness stack in bytes. stack_bytes: usize, @@ -31,7 +31,11 @@ pub enum ProgramError { #[error("Failed to satisfy witness: {0}")] WitnessSatisfaction(String), - /// Error indicating pruning the node tree during execution failed safely. + /// The program failed. `ExecutionError::JetFailed` is a failed `assert!` or jet, and + /// `ExecutionError::ReachedPrunedBranch` is, for example, `unwrap(None)`. + /// + /// [`Program`](super::Program) reports both a failure during pruning and a failure in the Bit + /// Machine run after pruning with this variant. #[error("Failed to prune program: {0}")] Pruning(#[from] simplicityhl::simplicity::bit_machine::ExecutionError), @@ -39,14 +43,15 @@ pub enum ProgramError { #[error("Failed to construct a Bit Machine with enough space: {0}")] BitMachineCreation(#[from] simplicityhl::simplicity::bit_machine::LimitError), - /// Error thrown during bit machine execution due to underlying logical or environment validation errors. + /// The Bit Machine rejected the pruned program. [`Program`](super::Program) doesn't return + /// this variant; it reports such failures as `Pruning`. #[error("Failed to execute program on the Bit Machine: {0}")] Execution(simplicityhl::simplicity::bit_machine::ExecutionError), /// Error indicating an input that doesn't carry the output it spends. #[error("Input {input_index} has no witness UTXO")] MissingWitnessUtxo { - /// The index of the input without one. + /// The index of the input without a witness UTXO. input_index: usize, }, @@ -59,12 +64,13 @@ pub enum ProgramError { utxo_count: usize, }, - /// Error indicating the script pubkey present on the targeted UTXO differs from the expectation. + /// The input spends an output whose script isn't the program's. #[error("Script pubkey mismatch: expected hash {expected_hash}, got {actual_hash}")] ScriptPubkeyMismatch { - /// The expected CMR (Commitment Merkle Root) hash. + /// The HASH160 of the script of the program's address, not the SHA-256 that + /// [`Program::get_script_hash`](super::Program::get_script_hash) returns. expected_hash: String, - /// The actual CMR hash present in the script pubkey. + /// The HASH160 of the script of the spent output. actual_hash: String, }, diff --git a/crates/sdk/src/program/logger.rs b/crates/sdk/src/program/logger.rs index 0973be97..ffd7191e 100644 --- a/crates/sdk/src/program/logger.rs +++ b/crates/sdk/src/program/logger.rs @@ -32,10 +32,14 @@ pub struct ProgramInfo { pub trace_buffer: Vec, } -/// Logger state for buffering execution output. +/// Buffers program execution output, per input and per thread, until it is printed. /// -/// Buffers are flushed to stderr only on successful transaction finalization, -/// discarding output from intermediate estimation passes. +/// With [`Verbosity`](crate::global::Verbosity) `Debug` or `Trace`, every +/// [`ProgramTrait::execute`](super::ProgramTrait::execute) replaces its input's buffer, so only +/// the last run's output is kept. `Signer::finalize` and `estimate_fee` print the buffers after +/// their last signing pass, and `finalize_strict` does when it succeeds. Code that calls +/// `ProgramTrait::execute` or `Signer::sign_tx` directly must call [`ProgramLogger::flush_logs`] +/// to see the output. `Signer::sign_witnesses` doesn't run any program. pub struct ProgramLogger { /// A mapping of program ids to program logs pub logs: HashMap, diff --git a/crates/sdk/src/program/mod.rs b/crates/sdk/src/program/mod.rs index 6d9e4f5a..8c67340b 100644 --- a/crates/sdk/src/program/mod.rs +++ b/crates/sdk/src/program/mod.rs @@ -2,7 +2,7 @@ pub mod core; /// Error types and definitions for program compilation, manipulation, and execution failures. pub mod error; -/// Program execution's specific logger +/// Buffering of the output of program execution. pub mod logger; pub use core::{Program, ProgramFactory, ProgramSchema, ProgramTrait}; diff --git a/crates/sdk/src/provider/core.rs b/crates/sdk/src/provider/core.rs index e0f5e6f9..64995b65 100644 --- a/crates/sdk/src/provider/core.rs +++ b/crates/sdk/src/provider/core.rs @@ -10,40 +10,53 @@ use crate::transaction::{TxReceipt, UTXO}; use super::error::ProviderError; -/// The fallback default fee rate (in sats/kvb) to use when dynamic estimates fail. +/// The fee rate, in sats/kvb, that [`ProviderTrait::fetch_fee_rate`] returns when the provider +/// doesn't report any estimate. pub const DEFAULT_FEE_RATE: f32 = 100.0; -/// The standard timeout duration (in seconds) applied to Esplora REST API requests. +/// The default timeout, in seconds, of each request an `EsploraProvider` makes. pub const DEFAULT_ESPLORA_TIMEOUT_SECS: u64 = 10; /// How many times `EsploraProvider::wait` polls for a confirmation by default. pub const DEFAULT_CONFIRMATION_ATTEMPTS: u32 = 9; -/// Contains foundational configuration elements required for initializing a generic blockchain provider. +/// The endpoints of a provider: an Esplora URL, and for a regtest node, its RPC URL and +/// credentials. Neither the SDK nor the test framework reads these. #[cfg(feature = "provider")] #[derive(Debug, Clone)] pub struct ProviderInfo { /// URL of the target Esplora REST service. pub esplora_url: String, - /// URL of the target direct `elementsd` or `bitcoind` RPC interface. + /// URL of the `elementsd` JSON-RPC interface. pub elements_url: Option, /// Authentication settings (e.g. cookie or username/password) for the RPC backend. pub auth: Option, } -/// Baseline traits detailing required interaction methods between the SDK client and the underlying blockchain node or API. +/// Access to a Liquid or Elements network: broadcasting, confirmation, chain queries and UTXO +/// lookups. +/// +/// All methods block. `EsploraProvider` and `SimplexProvider` implement this trait with the +/// `provider` feature. The trait itself doesn't need the feature, so a host without the feature +/// can still implement the trait. +/// +/// UTXOs come back as the chain has them: a confidential output has `secrets: None`. A +/// [`Signer`](crate::signer::Signer) unblinds its own outputs. pub trait ProviderTrait: Send + Sync { - /// Retrieves the network configured for this provider. + /// The network this provider talks to. fn get_network(&self) -> &SimplicityNetwork; - /// Attempts to broadcast a fully compiled transaction to the configured backend. + /// Broadcasts `tx` and returns the transaction's receipt. /// /// # Errors - /// Returns a `ProviderError` if network transmission fails, or if the backend explicitly rejects the transaction. + /// Returns `ProviderError::BroadcastRejected` if the provider answers with an error status, + /// as it does when the node refuses the transaction, and `ProviderError::Request` if the + /// request fails. fn broadcast_transaction(&self, tx: &Transaction) -> Result, ProviderError>; - /// Blocks and repeatedly polls the network until the specified transaction receives its first confirmation. + /// Blocks until transaction `txid` has one confirmation. /// /// # Errors - /// Returns a `ProviderError` if the network fails or the designated timeout elapses without confirmation. + /// Returns `ProviderError::Confirmation` if the transaction doesn't confirm before the + /// provider stops waiting, and another `ProviderError` if a request fails. fn wait(&self, txid: &Txid) -> Result<(), ProviderError>; /// Retrieves the current block height of the network tip. @@ -52,55 +65,65 @@ pub trait ProviderTrait: Send + Sync { /// Returns a `ProviderError` if the backend request fails or the returned height cannot be parsed. fn fetch_tip_height(&self) -> Result; - /// Retrieves the current block hash of the network tip. + /// The hash of the tip block, as a hex string. /// /// # Errors /// Returns a `ProviderError` if the backend request fails or the returned block hash cannot be parsed. fn fetch_tip_block_hash(&self) -> Result; - /// Retrieves the block timestamp representing network consensus clock tip. + /// The timestamp of the tip block, in seconds since the Unix epoch. /// /// # Errors /// Returns a `ProviderError` if the hash query, subsequent block query, or block parsing fails. fn fetch_tip_timestamp(&self) -> Result; - /// Retrieves the block hash at a specific block height. + /// The hash of the block at `block_height`, as a hex string. /// /// # Errors /// Returns a `ProviderError` if the passed block height does not exist or the returned hash cannot be parsed. fn fetch_block_hash_at_height(&self, block_height: u32) -> Result; - /// Retrieves the transaction IDs for a specific block hash. + /// The IDs of the transactions in the block with hex hash `block_hash`. /// /// # Errors /// Returns a `ProviderError` if the passed block hash does not exist or the returned data cannot be parsed. fn fetch_block_txids(&self, block_hash: &str) -> Result, ProviderError>; - /// Retrieves the serialized transaction payload given its hex transaction ID. + /// The transaction with ID `txid`. /// /// # Errors /// Returns a `ProviderError` if the node fails to locate the transaction or serialization fails. fn fetch_transaction(&self, txid: &Txid) -> Result; - /// Fetches all active unspent transaction outputs correlated to a particular public `Address`. + /// The unspent outputs paying to `address`'s script, confidential ones not unblinded. /// /// # Errors /// Returns a `ProviderError` if the backend request fails or if UTXO parsing fails. fn fetch_address_utxos(&self, address: &Address) -> Result, ProviderError>; - /// Fetches all active unspent transaction outputs correlated to a given custom `Script` mapping. + /// The unspent outputs locked by `script`, confidential ones not unblinded. + /// + /// This finds the outputs a program controls, given its + /// [`get_script_pubkey`](crate::program::Program::get_script_pubkey), which nobody holds a + /// key for. /// /// # Errors /// Returns a `ProviderError` if the backend request fails or if UTXO parsing fails. fn fetch_scripthash_utxos(&self, script: &Script) -> Result, ProviderError>; - /// Fetches network fee estimation models based on varying target confirmation block delays. + /// The provider's fee estimates: keys are confirmation targets in blocks, as strings, and + /// values are fee rates in sat/vB. /// /// # Errors /// Returns a `ProviderError` if the REST request fails or the resulting mappings fail to parse cleanly. fn fetch_fee_estimates(&self) -> Result, ProviderError>; - /// Attempts to extract the specific fee rate (in sats/kvb) necessary for the transaction to be confirmed within `target_blocks`. + /// The fee rate, in sats/kvb, for confirmation within `target_blocks`. + /// + /// Uses the estimate for `target_blocks` if there is one. Otherwise it looks only at the + /// targets 1 to 25, 144, 504 and 1008: the smallest of them that is at least `target_blocks` + /// and has an estimate, else the smallest that has one. Without any, it returns + /// [`DEFAULT_FEE_RATE`]. /// /// # Errors /// Passes along `ProviderError` if `fetch_fee_estimates` fails. diff --git a/crates/sdk/src/provider/error.rs b/crates/sdk/src/provider/error.rs index 145943ce..7df52b0d 100644 --- a/crates/sdk/src/provider/error.rs +++ b/crates/sdk/src/provider/error.rs @@ -1,23 +1,27 @@ #[cfg(feature = "provider")] use crate::provider::rpc::error::RpcError; -/// Defines standard errors possible when using a blockchain interaction provider. +/// Errors from a [`ProviderTrait`](super::ProviderTrait) implementation. #[derive(Debug, thiserror::Error)] pub enum ProviderError { - /// Wrapper around an RPC-level error representing transport or network-level connectivity failures to the inner node. + /// An [`ElementsRpc`](super::ElementsRpc) call failed. #[cfg(feature = "provider")] #[error(transparent)] Rpc(#[from] RpcError), - /// Error indicating that a standard HTTP request to a provider (such as an Esplora REST instance) encountered a failure. + /// An HTTP request failed or timed out, or a query answered with a status other than 200. #[error("HTTP request failed: {0}")] Request(String), - /// Error indicating the configured timeout to wait for transaction confirmation elapsed without confirmation. + /// `wait` stopped polling before the transaction confirmed. #[error("Couldn't wait for the transaction to be confirmed")] Confirmation, - /// Error indicating a provider returned a non-success response explicitly rejecting a broadcasted transaction payload. + /// The provider answered a broadcast with an HTTP status outside 200 to 299. + /// + /// This is how a node's refusal arrives, for consensus or policy reasons such as + /// `non-BIP68-final` for a relative timelock that hasn't passed, with the node's reason in + /// `message`. A rate limit or a server error arrives the same way, so check `status`. #[error("Broadcast failed with HTTP {status} for {url}: {message}")] BroadcastRejected { /// The HTTP status code indicating the failure. diff --git a/crates/sdk/src/provider/esplora.rs b/crates/sdk/src/provider/esplora.rs index 6d7c5237..05546d8d 100644 --- a/crates/sdk/src/provider/esplora.rs +++ b/crates/sdk/src/provider/esplora.rs @@ -16,16 +16,23 @@ use crate::utils::hash_script; use super::core::{DEFAULT_CONFIRMATION_ATTEMPTS, DEFAULT_ESPLORA_TIMEOUT_SECS, ProviderTrait}; use super::error::ProviderError; -/// A provider implementation that interacts with the Esplora REST API backend. +/// A provider that talks to an Esplora REST API, such as +/// `https://blockstream.info/liquidtestnet/api`. +/// +/// Each request times out after `timeout`. The provider doesn't retry a failed request. #[derive(Debug)] pub struct EsploraProvider { - /// The base URL of the Esplora REST API. + /// The base URL of the Esplora REST API, without a trailing slash. pub esplora_url: String, - /// The currently configured Simplicity network (e.g. Liquid, Testnet, Regtest). + /// The network the API serves. pub network: SimplicityNetwork, - /// Timeout duration used in underlying HTTP requests. + /// The timeout of each request, in whole seconds. Default: [`DEFAULT_ESPLORA_TIMEOUT_SECS`]. pub timeout: Duration, - /// How many times `wait` polls for a confirmation before giving up. + /// How many times `wait` polls for a confirmation before giving up. Default: + /// [`DEFAULT_CONFIRMATION_ATTEMPTS`]. + /// + /// `wait` polls every 100 ms on [`SimplicityNetwork::ElementsRegtest`] and every 10 seconds on + /// other networks, so by default it gives up after about 1 second or about 90 seconds. pub confirmation_attempts: u32, } @@ -48,7 +55,8 @@ struct EsploraUtxo { } impl EsploraProvider { - /// Creates a new `EsploraProvider` connected to the provided endpoint targeting the specific network. + /// Creates a provider for the Esplora API at `url`, which serves `network`, with the default + /// timeout and confirmation attempts. #[must_use] pub fn new(url: String, network: SimplicityNetwork) -> Self { Self { @@ -59,7 +67,7 @@ impl EsploraProvider { } } - /// Sends a GET to `path` under the Esplora URL, whatever the status it answers with. + /// Sends a GET to `path` under the Esplora URL, whatever the response status. fn request(&self, path: &str) -> Result { minreq::get(format!("{}{path}", self.esplora_url)) .with_timeout(self.timeout.as_secs()) @@ -67,7 +75,7 @@ impl EsploraProvider { .map_err(|e| ProviderError::Request(e.to_string())) } - /// Sends a GET to `path`, failing unless it answers 200. + /// Sends a GET to `path`, failing unless the response status is 200. fn get(&self, path: &str) -> Result { let response = self.request(path)?; @@ -187,7 +195,7 @@ impl ProviderTrait for EsploraProvider { for _ in 0..self.confirmation_attempts { let response = self.request(&path)?; - // an unknown transaction answers 404 until Esplora indexes it + // Esplora answers 404 for a transaction it hasn't indexed yet if response.status_code == 200 { let status: TxStatus = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?; diff --git a/crates/sdk/src/provider/network.rs b/crates/sdk/src/provider/network.rs index b2a1ae4a..099be565 100644 --- a/crates/sdk/src/provider/network.rs +++ b/crates/sdk/src/provider/network.rs @@ -46,7 +46,11 @@ pub static LIQUID_REGTEST_GENESIS: LazyLock = LazyLock::new ]) }); -/// Represents the target network configuration for Simplicity interactions. +/// The network a program, signer or provider works on. It determines the policy asset, the +/// genesis block hash that Simplicity's `sighash_all` commits to, and the address format. +/// +/// `FromStr` accepts `"Liquid"`, `"LiquidTestnet"` and `"ElementsRegtest"`, which gives +/// [`SimplicityNetwork::default_regtest`]. Construct `ElementsCustom` directly. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SimplicityNetwork { /// Liquid mainnet. @@ -58,7 +62,8 @@ pub enum SimplicityNetwork { /// Regtest mode `AssetId`, which is used as a default policy asset locally. policy_asset: elements::AssetId, }, - /// Custom Elements environment whose genesis is derived from its chain parameters. + /// An Elements network with its own policy asset and genesis block hash, such as a regtest + /// node started with non-default chain parameters. ElementsCustom { /// Custom network policy asset. policy_asset: elements::AssetId, @@ -68,7 +73,8 @@ pub enum SimplicityNetwork { } impl SimplicityNetwork { - /// Creates a default Elements Regtest configuration. + /// Elements regtest with the default policy asset, + /// [`LIQUID_DEFAULT_REGTEST_ASSET_STR`]. #[must_use] pub fn default_regtest() -> Self { Self::ElementsRegtest { @@ -86,7 +92,7 @@ impl SimplicityNetwork { } } - /// Returns the genesis block hash for the network variant. + /// The network's genesis block hash, which Simplicity's `sighash_all` commits to. #[must_use] pub fn genesis_block_hash(&self) -> elements::BlockHash { match self { @@ -97,13 +103,14 @@ impl SimplicityNetwork { } } - /// Determines if the current network is the mainnet (Liquid). + /// Whether this is Liquid mainnet. Only `Liquid` is. #[must_use] pub fn is_mainnet(&self) -> bool { self == &Self::Liquid } - /// Returns the address parameters associated with the current enum variant. + /// The address parameters: Liquid's, Liquid testnet's, or `AddressParams::ELEMENTS` for + /// regtest and custom networks. #[must_use] pub const fn address_params(&self) -> &'static elements::AddressParams { match self { diff --git a/crates/sdk/src/provider/simplex.rs b/crates/sdk/src/provider/simplex.rs index aacb262f..846056b4 100644 --- a/crates/sdk/src/provider/simplex.rs +++ b/crates/sdk/src/provider/simplex.rs @@ -11,8 +11,12 @@ use super::core::ProviderTrait; use super::error::ProviderError; use super::{ElementsRpc, EsploraProvider}; -/// A local provider used during Regtest or local development. -/// It wraps an `EsploraProvider` for REST API queries and an `ElementsRpc` for direct node interactions. +/// A provider for a regtest node: an [`EsploraProvider`] for queries and an [`ElementsRpc`] that +/// mines blocks. +/// +/// Every method forwards to `esplora`, except that `broadcast_transaction` also mines one block +/// and waits for the transaction to confirm. So each broadcast advances the height by one. Use it +/// only against a regtest node. #[derive(Debug)] pub struct SimplexProvider { /// The Esplora provider for handling REST API queries. @@ -22,10 +26,10 @@ pub struct SimplexProvider { } impl SimplexProvider { - /// Creates a new `SimplexProvider` with the given URLs, authentication, and network. + /// Creates a provider from the Esplora URL, the node's RPC URL and credentials, and the network. /// /// # Panics - /// Panics if the `ElementsRpc` client fails to initialize. + /// Panics if the node doesn't answer the RPC client's first request. #[must_use] pub fn new(esplora_url: String, elements_url: String, auth: Auth, network: SimplicityNetwork) -> Self { let elements = ElementsRpc::new(elements_url, auth) diff --git a/crates/sdk/src/signer/core.rs b/crates/sdk/src/signer/core.rs index f0d51ca0..c6920ef6 100644 --- a/crates/sdk/src/signer/core.rs +++ b/crates/sdk/src/signer/core.rs @@ -48,7 +48,8 @@ use crate::transaction::{NativeSignature, PartialInput, TxReceipt, UTXO}; use super::error::SignerError; -/// A placeholder dummy fee amount used during transaction estimation. +/// The amount the signer gives the change and fee outputs while it measures a transaction's +/// weight. pub const PLACEHOLDER_FEE: u64 = 1; /// Discounted weight of a native input with its signature. @@ -58,12 +59,22 @@ const NATIVE_INPUT_WEIGHT: usize = 274; /// Weight units per virtual byte. const WITNESS_SCALE_FACTOR: usize = 4; -/// Common signing interface spanning over standard explicit inputs and Simplicity programs. +/// The two signatures a [`Signer`] makes: Schnorr for a program input, ECDSA for a native one. +/// +/// [`Signer::sign_witnesses`] and [`Signer::sign_tx`] make the same signatures for every input +/// that needs one. Call these methods directly to sign a PSET built some other way. pub trait SignerTrait { - /// Generates a Schnorr signature to satisfy a target Simplicity program input. + /// Signs, with Schnorr, the message `message` derives from the Simplicity `sighash_all` of + /// input `input_index`, using the key at `derivation_path`. Pass `&SigMessage::Sighash` to + /// sign `sighash_all` itself. + /// + /// This is Simplicity's `sighash_all`, the value a contract reads with `jet::sig_all_hash()`, + /// not a BIP-341 sighash. /// /// # Errors - /// Returns a `SignerError` if the elements environment fails to build or if the message digest fails to construct. + /// Returns `SignerError::Program` if the input doesn't spend `program`'s address or the + /// transaction can't be extracted, and `SignerError::InvalidDerivationPath` for a malformed + /// `derivation_path`. fn sign_program( &self, pst: &PartiallySignedTransaction, @@ -74,10 +85,14 @@ pub trait SignerTrait { message: &SigMessage, ) -> Result; - /// Generates an ECDSA signature to spend a standard transaction input. + /// Signs input `input_index` as a P2WPKH input with a low-R ECDSA signature over its + /// `SIGHASH_ALL` message, using the key at `derivation_path`. Returns the public key and + /// the signature for the input's witness. /// /// # Errors - /// Returns a `SignerError` if the transaction formatting or sighash msg extraction fails. + /// Returns `SignerError::KeyMismatch` if the spent output is P2WPKH but locked to another key, + /// `SignerError::InvalidDerivationPath` for a malformed `derivation_path`, and + /// `SignerError::SighashConstruction` or `SignerError::TxExtraction` if the PSET is incomplete. fn sign_input( &self, pst: &PartiallySignedTransaction, @@ -86,12 +101,52 @@ pub trait SignerTrait { ) -> Result<(PublicKey, ecdsa::Signature), SignerError>; } -/// Core interface responsible for managing keys, interfacing with the blockchain provider, -/// assembling descriptors, estimating fees, and finalizing/signing transactions. +/// A wallet that completes, signs and broadcasts [`FinalTransaction`]s. +/// +/// The keys derive from a BIP-39 mnemonic with an empty passphrase. The account path is +/// `m/84h/1776h/0h` on Liquid mainnet and `m/84h/1h/0h` on other networks. The wallet has one +/// address, the P2WPKH address at `0/0` under the account, with a SLIP-77 blinding key. +/// [`PartialInput::with_derivation_path`](crate::transaction::PartialInput::with_derivation_path) +/// can sign an input with another key, but the signer only finds and spends its own funds at +/// that one address. /// /// Without the `provider` feature the signer has no blockchain access: it can assemble, /// blind, sign and finalize a transaction it is handed, but it cannot discover UTXOs, /// look up a fee rate, or broadcast. +/// +/// # Completing a transaction +/// +/// The methods differ in what they add to the transaction: +/// +/// | Method | Adds wallet inputs | Fetches a fee rate | Adds change and fee outputs | Broadcasts | +/// | --- | --- | --- | --- | --- | +/// | `send` | yes | yes | yes | yes | +/// | `broadcast` | yes | yes | yes | yes | +/// | `finalize` | yes | yes | yes | no | +/// | [`finalize_strict`](Signer::finalize_strict) | no | no | yes | no | +/// | [`estimate_fee`](Signer::estimate_fee) | no | no | only to measure | no | +/// | [`sign_tx`](Signer::sign_tx) | no | no | no | no | +/// | [`sign_witnesses`](Signer::sign_witnesses) | no | no | no | no | +/// +/// Fee rates are in sats/kvb. Every method except `sign_witnesses` runs each program input +/// locally, so a contract that rejects the spend fails with `SignerError::CovenantExecution` +/// before anything is broadcast. +/// +/// # Fees and change +/// +/// `finalize`, `finalize_strict` and `estimate_fee` append a change output and then a fee +/// output, which has an empty script. The change goes to the transaction's +/// [`change`](FinalTransaction::change) target, or to the signer's own address, blinded, if it +/// has none. The signer signs once to measure the weight, sets the fee for that weight, pays the +/// rest of the policy-asset surplus as change, and signs again. Each signing runs every program +/// input, so a program runs at least twice. +/// +/// Change below [`MIN_FEE`] (blinded) or [`DUST_THRESHOLD`] (explicit) is dropped, and the whole +/// surplus becomes the fee. Blinded change is kept, and more funds are needed instead, when it is +/// the only blinded output of a transaction that spends a confidential input, or when the +/// transaction doesn't spend any confidential input and exactly one other output is blinded. +/// +/// Change is paid only in the policy asset. Every other asset must already balance. pub struct Signer { #[cfg(feature = "provider")] provider: Option>, @@ -145,7 +200,8 @@ enum Estimate { // TODO: refactor descriptors to be a standalone object to specify custom derivation paths. impl Signer { - /// Creates a new `Signer` instance seeded from the provided mnemonic and paired with the specified provider. + /// Creates a signer from a mnemonic, with `provider` for network access. The network is the + /// provider's. /// /// # Panics /// Panics if the mnemonic fails to parse, or if deriving the master private key fails. @@ -204,10 +260,13 @@ impl Signer { } } - /// Composes, funds, and broadcasts a standard network transaction sending the specified value of the primary policy asset. + /// Sends `amount` of the network's policy asset to `to`, from the wallet's funds, and + /// broadcasts the transaction. + /// + /// This is [`Signer::broadcast`] on a transaction with that one output. /// /// # Errors - /// Returns a `SignerError` if compiling the inputs fails, there are insufficient funds/fees, or broadcast is rejected. + /// Returns the errors of [`Signer::broadcast`]. // TODO: add an ability to send arbitrary assets #[cfg(feature = "provider")] pub fn send(&self, to: Script, amount: u64) -> Result, SignerError> { @@ -220,10 +279,14 @@ impl Signer { Ok(self.get_provider()?.broadcast_transaction(&tx)?) } - /// Evaluates, funds, and broadcasts an already assembled `FinalTransaction`. + /// Completes `tx` with [`Signer::finalize`] and broadcasts it through the provider. + /// + /// Returns only the receipt. To learn the fee, call [`Signer::finalize`] and broadcast with + /// [`ProviderTrait::broadcast_transaction`]. /// /// # Errors - /// Returns a `SignerError` if finalizing the payload fails or if the network rejects the broadcast. + /// Returns the errors of [`Signer::finalize`], and `SignerError::Provider` if the broadcast + /// fails. A node's rejection arrives as `SignerError::Provider(ProviderError::BroadcastRejected)`. #[cfg(feature = "provider")] pub fn broadcast(&self, tx: &FinalTransaction) -> Result, SignerError> { let (tx, _fee) = self.finalize(tx)?; @@ -231,10 +294,26 @@ impl Signer { Ok(self.get_provider()?.broadcast_transaction(&tx)?) } - /// Evaluates the input components of a `FinalTransaction`, iteratively selecting available wallet UTXOs to cover outputs and estimated fees. + /// Completes `tx` with wallet inputs, a change output and a fee output, and signs it, without + /// broadcasting. Returns the transaction and its fee. + /// + /// Fetches the fee rate for a one-block target. If the inputs already in `tx` don't cover the + /// outputs and the fee, adds the wallet's policy-asset UTXOs as native inputs after `tx`'s own, + /// largest first, until they do. Skips UTXOs that `tx` already spends. If `tx` has an explicit + /// change target and no blinded output, adds only explicit UTXOs. See + /// [Fees and change](Signer#fees-and-change). /// /// # Errors - /// Returns a `SignerError` if the wallet contains insufficient funds to satisfy output values and target fee rates. + /// - `SignerError::NotEnoughFunds` if the wallet runs out before the fee is covered. + /// - `SignerError::UnknownInputAmount` if an input is confidential and has no unblinding secrets. + /// - `SignerError::DuplicateInput` if `tx` spends an outpoint twice. + /// - `SignerError::ConfidentialInputWithoutBlindedOutput` if `tx` spends a confidential input, + /// has an explicit change target, and doesn't have any blinded output. + /// - `SignerError::Unbalanced` if an asset other than the policy asset doesn't balance. + /// - `SignerError::InvalidFeeRate` if the provider's fee rate isn't a finite, non-negative number. + /// - `SignerError::Provider` or `SignerError::ProviderUnavailable` for provider failures. + /// - The errors of [`FinalTransaction::calculate_fee_delta`] and [`Signer::sign_tx`], including + /// `SignerError::CovenantExecution` if a program rejects the spend. #[cfg(feature = "provider")] pub fn finalize(&self, tx: &FinalTransaction) -> Result<(Transaction, u64), SignerError> { Self::check_known_amounts(tx)?; @@ -305,7 +384,8 @@ impl Signer { inputs_since_estimate += 1; } - // Need to try one more time after the loop. No UTXO is left to add, so any chance is taken + // One last estimate for the UTXOs added since the previous one. No UTXO is left to add, + // so skip the margin for their fees and let the estimate decide let policy_amount_delta = fee_tx.calculate_fee_delta(&self.network)?; if i128::from(policy_amount_delta) >= i128::from(curr_fee) { @@ -321,12 +401,25 @@ impl Signer { )) } - /// Verifies and finalizes a transaction against a strict target confirmation window (in blocks). - /// This function also assumes that the transaction already includes the coin selection. + /// Finalizes a transaction using only the inputs it already has, paying `fee_rate` sats/kvb. + /// + /// Unlike `Signer::finalize`, this doesn't add any wallet inputs or fetch a fee rate, so it + /// doesn't need a provider. Like `Signer::finalize`, it appends a fee output and, unless the + /// change would be too small to keep, a change output before it, then signs and runs every + /// program input. Returns the transaction and its fee. /// /// # Errors - /// Returns a `SignerError` if the assembled inputs do not meet dust limits or fail to cover the - /// dynamically estimated required fee. + /// - `SignerError::InvalidFeeRate` if `fee_rate` isn't a finite, non-negative number. + /// - `SignerError::UnknownInputAmount` if an input is confidential and has no unblinding secrets. + /// - `SignerError::DuplicateInput` if the transaction spends an outpoint twice. + /// - `SignerError::DustAmount` if the inputs don't exceed the outputs by at least [`MIN_FEE`] of + /// the policy asset. + /// - `SignerError::NotEnoughFeeAmount` if that surplus can't pay the fee, or the fee and the + /// smallest blinded change when the change can't be dropped. + /// - `SignerError::ConfidentialInputWithoutBlindedOutput` if the transaction spends a + /// confidential input, has an explicit change target, and doesn't have any blinded output. + /// - `SignerError::Unbalanced` if an asset other than the policy asset doesn't balance. + /// - The errors of [`FinalTransaction::calculate_fee_delta`] and [`Signer::sign_tx`]. pub fn finalize_strict(&self, tx: &FinalTransaction, fee_rate: f32) -> Result<(Transaction, u64), SignerError> { Self::check_known_amounts(tx)?; Self::check_unique_inputs(tx)?; @@ -348,10 +441,17 @@ impl Signer { } } - /// Reports what an assembled transaction would cost in fees at the given rate. + /// The fee `tx` would pay at `fee_rate` sats/kvb with a change output and a fee output added, + /// as [`Signer::finalize_strict`] would add them. Doesn't add any wallet inputs. + /// + /// Returns the fee even if `tx`'s inputs can't cover the fee. If the change is blinded and + /// can't be dropped, and the surplus is too small for the change, this returns the fee plus + /// [`MIN_FEE`] instead: the smallest surplus that pays both the fee and the change. /// /// # Errors - /// Returns a `SignerError` if the transaction cannot be signed. + /// Returns `SignerError::InvalidFeeRate`, `UnknownInputAmount`, `DuplicateInput`, + /// `ConfidentialInputWithoutBlindedOutput` or `Unbalanced` as [`Signer::finalize_strict`] does, + /// and the errors of [`Signer::sign_tx`]. pub fn estimate_fee(&self, tx: &FinalTransaction, fee_rate: f32) -> Result { Self::check_known_amounts(tx)?; Self::check_unique_inputs(tx)?; @@ -367,13 +467,17 @@ impl Signer { }) } - /// Signs transaction in raw format for easy processing later in a format of `PartiallySignedTransaction`. + /// Blinds and signs exactly the transaction it is given, and finalizes every program input. /// - /// Every program input is executed and finalized with its signed witness. + /// Doesn't add any input, change output or fee output, and doesn't check that `tx` balances. + /// The node rejects a transaction that doesn't balance, including its fee output. Runs + /// [`Signer::sign_witnesses`], then executes each program input with its signed witness and + /// sets the input's final witness. Call `extract_tx()` on the result for a [`Transaction`]. /// /// # Errors - /// Returns a `SignerError` if we have an error in singing and constructing program witness, - /// or if a program fails to execute. + /// Returns `SignerError::CovenantExecution` if a program rejects the spend, with the input's + /// index and the locktime and sequence it ran with, and the errors of + /// [`Signer::sign_witnesses`]. pub fn sign_tx(&self, tx: &FinalTransaction) -> Result { let (mut pst, signed_witnesses) = self.sign_witnesses(tx)?; @@ -403,6 +507,14 @@ impl Signer { /// Blinds the transaction, signs the `NativeEcdsa` inputs and injects signatures into program witnesses, /// without executing or finalizing the programs. Inputs that require no signature stay unsigned. /// + /// Returns the PSET, with the native inputs finalized, and each program input's witness with + /// its signature inserted, by input index. Blinding is random, so when `tx` has blinded + /// outputs, every call produces different output commitments and so a different + /// `sighash_all`. + /// + /// Running a program input with [`ProgramTrait::execute`] on the result is a dry run: it + /// checks the program with its real signature without finalizing anything. + /// /// # Errors /// Returns a `SignerError` if blinding, signing or injecting a signature into a program witness fails. pub fn sign_witnesses( @@ -481,19 +593,23 @@ impl Signer { self.provider.as_deref().ok_or(SignerError::ProviderUnavailable) } - /// Returns the confidential elements address matching the local wallet logic. + /// The wallet's address with its SLIP-77 blinding key. Funds sent here arrive confidential. #[must_use] pub fn get_confidential_address(&self) -> Address { self.confidential_address.clone() } - /// Returns the standard unblinded address matching the local wallet logic. + /// The wallet's P2WPKH address, at `0/0` under the account, without a blinding key. #[must_use] pub fn get_address(&self) -> Address { self.address.clone() } - /// Retrieves UTXOs with known assets and amounts, skipping outputs that cannot be unblinded. + /// The wallet's UTXOs, with confidential ones unblinded. Skips outputs that can't be + /// unblinded with the wallet's key. + /// + /// Only finds outputs at the wallet's one address. To find outputs locked by a program, use + /// [`ProviderTrait::fetch_scripthash_utxos`] with the program's script. /// /// # Errors /// Returns a `SignerError` if querying the provider fails. @@ -513,7 +629,7 @@ impl Signer { }) } - /// Finds all known UTXOs deriving from a targeted `Txid`. + /// The wallet's UTXOs that transaction `txid` created. /// /// # Errors /// Returns a `SignerError` if querying the network fails. @@ -523,8 +639,8 @@ impl Signer { self.get_utxos_filter(&|utxo| utxo.outpoint.txid == txid, &|utxo| utxo.outpoint.txid == txid) } - /// Maps UTXOs retrieved from the provider through arbitrary functional filters. - /// Separate filtering criteria apply explicitly vs confidentially. + /// The wallet's explicit UTXOs that pass `explicit_filter`, followed by its unblinded + /// confidential UTXOs that pass `confidential_filter`. /// /// Outputs with null fields, unresolved partial commitments, or proofs that /// cannot be rewound with the wallet key are skipped. Already supplied secrets @@ -578,7 +694,8 @@ impl Signer { Ok(all_utxos) } - /// Derives the X-Only public key specifically used for Schnorr and Taproot structures. + /// The x-only public key at the default path, for a contract that checks the signer's + /// Schnorr signatures. #[must_use] pub fn get_schnorr_public_key(&self) -> XOnlyPublicKey { let private_key = self.get_private_key(); @@ -587,13 +704,13 @@ impl Signer { keypair.x_only_public_key().0 } - /// Resolves the standard format ECDSA public key. + /// The public key at the default path, for ECDSA. #[must_use] pub fn get_ecdsa_public_key(&self) -> PublicKey { self.get_private_key().public_key(&self.secp) } - /// Resolves the corresponding blinding public key. + /// The public key of [`Signer::get_blinding_private_key`]. #[must_use] pub fn get_blinding_public_key(&self) -> PublicKey { self.get_blinding_private_key().public_key(&self.secp) @@ -605,10 +722,12 @@ impl Signer { self.default_key } - /// Derives the signing key at a path relative to the account path. `None` defaults to `0/0`. + /// The private key at a path relative to the account path. `None` is the default path, + /// `0/0`. /// /// # Errors - /// Returns `SignerError::InvalidDerivationPath` if the path is not of the form `m/n`. + /// Returns `SignerError::InvalidDerivationPath` if the path doesn't have exactly two unhardened + /// steps, such as `0/5`. pub fn get_private_key_at(&self, relative: Option<&DerivationPath>) -> Result { match relative { Some(path) => { @@ -627,7 +746,8 @@ impl Signer { self.blinding_key } - /// Checks that `path` is in correct form of `m/n`. + /// Checks that `path` has exactly two unhardened steps, such as `0/5`, as the paths of + /// [`Signer::get_private_key_at`] and `PartialInput::derivation_path` must. /// /// # Errors /// Returns `SignerError::InvalidDerivationPath` otherwise. @@ -755,7 +875,7 @@ impl Signer { return Ok(Estimate::Failure(fee.saturating_add(change_minimum))); } - // Not enough funds for the change, so estimate without it. + // Not enough funds for the change, so estimate without a change output. fee_tx.remove_output(change_index); let final_tx = sign_and_extract_tx(self, &fee_tx)?; @@ -1130,7 +1250,7 @@ mod tests { } #[test] - fn blinded_output_lets_the_explicit_change_target_stand() { + fn explicit_change_target_is_allowed_with_a_blinded_output() { let signer = create_signer(); let mut ft = FinalTransaction::new(); @@ -1152,7 +1272,7 @@ mod tests { } #[test] - fn default_change_target_balances_it_by_itself() { + fn default_change_target_balances_a_confidential_input() { let signer = create_signer(); let mut ft = FinalTransaction::new(); @@ -1186,7 +1306,7 @@ mod tests { } #[test] - fn descriptors() { + fn prints_addresses() { let signer = create_signer(); println!("{}", signer.get_address()); @@ -1402,7 +1522,7 @@ mod tests { } #[test] - fn leftover_too_small_for_change_is_the_fee_unless_it_reveals_a_blinded_amount() { + fn small_leftover_goes_to_the_fee_unless_a_blinded_output_needs_change() { let signer = create_signer(); let policy = signer.network.policy_asset(); let script = signer.get_address().script_pubkey(); @@ -1429,7 +1549,7 @@ mod tests { } #[test] - fn only_wallet_paths_owning_the_input_sign_it() { + fn derivation_path_must_own_the_input() { let signer = create_signer(); let path = |path: &str| DerivationPath::from_str(path).unwrap(); diff --git a/crates/sdk/src/signer/error.rs b/crates/sdk/src/signer/error.rs index 51bfbf10..765fdab4 100644 --- a/crates/sdk/src/signer/error.rs +++ b/crates/sdk/src/signer/error.rs @@ -4,14 +4,30 @@ use crate::program::ProgramError; use crate::provider::ProviderError; use crate::transaction::{FinalTransactionError, UtxoError}; -/// Core error types for the Signer component. +/// Errors from a [`Signer`](super::Signer). +/// +/// A contract that rejects a spend fails with `CovenantExecution` before anything is broadcast. +/// A transaction that is broadcast and refused fails with +/// `Provider(ProviderError::BroadcastRejected { .. })`. #[derive(Debug, thiserror::Error)] pub enum SignerError { - /// Errors originating from Simplicity program evaluation and state. + /// A program failed while the signer prepared its signature, before the program ran: in + /// [`SignerTrait::sign_program`](super::SignerTrait::sign_program), or in + /// [`Signer::sign_witnesses`](super::Signer::sign_witnesses) for an input with a + /// [`ProgramSignature`](crate::transaction::ProgramSignature). The program doesn't compile, + /// the input doesn't spend its address, or its witness types can't be read. For an input + /// without a signature, the same failures are `CovenantExecution`. #[error(transparent)] Program(#[from] ProgramError), - /// Error indicating that a Simplicity program failed to satisfy, prune or execute. + /// Running a program input failed, so the transaction wasn't finalized. + /// + /// Raised before broadcast, by [`Signer::sign_tx`](super::Signer::sign_tx) and the methods + /// built on `sign_tx`. `source` says why: `ProgramError::Pruning` when the contract rejects + /// the spend, and other variants, such as `ProgramError::WitnessSatisfaction`, + /// `ProgramError::ScriptPubkeyMismatch` or `ProgramError::InsufficientBudget`, when the + /// program can't run on this input or its witness stack doesn't cover the execution cost. The + /// locktime and sequence identify most timelock failures. #[error( "Covenant input {index} did not execute (transaction locktime {locktime}, input sequence {sequence}): {source}" )] @@ -102,12 +118,13 @@ pub enum SignerError { #[error("A confidential input needs at least one blinded output, but the change target is explicit")] ConfidentialInputWithoutBlindedOutput, - /// Error indicating a derivation path isn't `m/n`. - #[error("The derivation path `{0}` is not of the form m/n")] + /// A derivation path doesn't have exactly two unhardened steps, such as `0/5`. + #[error("The derivation path `{0}` must have exactly two unhardened steps, such as 0/5")] InvalidDerivationPath(DerivationPath), - /// Error indicating the key an input is signed with doesn't own the output it spends. - #[error("Input {0} is locked to another key than its derivation path gives")] + /// A native input spends a P2WPKH output locked to a different key than the one at its + /// derivation path. + #[error("Input {0} is locked to a different key than the one at its derivation path")] KeyMismatch(usize), /// Error indicating an invalid upstream `secp256k1` secret key. @@ -142,32 +159,34 @@ pub enum SignerError { #[error("The input spending {0} is confidential and has no unblinding secrets, so its amount is unknown")] UnknownInputAmount(simplicityhl::elements::OutPoint), - /// Error indicating an expected witness field could not be found. - #[error("Missing such witness field: {0}")] + /// A [`ProgramSignature`](crate::transaction::ProgramSignature) with a path names a field + /// that the program's witness doesn't have. + #[error("The program's witness has no field `{0}`")] WitnessFieldNotFound(String), } /// Errors originating from manipulating witness paths and injecting values. #[derive(Debug, thiserror::Error)] pub enum WitnessWrappingError { - /// Error indicating a failure while parsing the provided witness path string. + /// A path step isn't `Left`, `Right` or a decimal index. #[error("Failed to parse path")] ParsingError, - /// Error pointing to the use of a path type that is currently not supported. + /// A path step doesn't fit the type it steps into: an index into a type that isn't an array or + /// tuple, or `Left` or `Right` into a type that isn't an `Either`. #[error("Unsupported path type: {0}")] UnsupportedPathType(String), - /// Error thrown during path traversal when an index exceeds the inner array lengths. + /// An index is past the end of an array or tuple: `(length, index)`. #[error("Path index out of bounds: len is {0}, got {1}")] IdxOutOfBounds(usize, usize), - /// Error indicating that the runtime type at the path root expected one type but encountered another. + /// The type at the end of the path isn't the signature's type: `(expected, signature type)`. #[error("Root type mismatch: expected {0}, got {1}")] RootTypeMismatch(String, String), - /// Error indicating that a path traversal attempted to reach an undefined or mismatched Either branch. - #[error("Path reached undefined branch of Either")] + /// The supplied witness value takes the other branch of an `Either` than the path does. + #[error("The witness value takes the other branch of an Either than the path")] EitherBranchMismatch, } diff --git a/crates/sdk/src/transaction/final_transaction.rs b/crates/sdk/src/transaction/final_transaction.rs index d1488c55..4e6ed6a2 100644 --- a/crates/sdk/src/transaction/final_transaction.rs +++ b/crates/sdk/src/transaction/final_transaction.rs @@ -14,21 +14,25 @@ use super::partial_input::{IssuanceDetails, IssuanceInput, PartialInput, Program use super::partial_output::PartialOutput; use super::required_signature::{NativeSignature, ProgramSignature, RequiredSignature}; -/// Represents the final input structure put into a `FinalTransaction` for processing. +/// One input of a [`FinalTransaction`]: the output it spends, and optionally the program that +/// locks that output and an asset issuance, together with the signature the signer adds. +/// +/// [`FinalTransaction::inputs`] returns these. All fields are public, so they can be changed +/// through [`FinalTransaction::inputs_mut`]. #[derive(Clone, Debug)] pub struct FinalInput { - /// Holds the base input data required for the operation. + /// The output this input spends. pub partial_input: PartialInput, - /// Holds program inputs, which are used for program witness finalization. + /// The program that locks the spent output and its witness, for a program input. pub program_input: Option, - /// Contains optional issuance-related information. + /// The asset this input issues or reissues, if any. pub issuance_input: Option, - /// Required signature for finalizing the transaction. + /// The signature the signer adds to this input. pub required_sig: RequiredSignature, } impl FinalInput { - /// Creates a new instance of the type with the specified `partial_input` and `required_sig`. + /// Creates an input without a program or an issuance. #[must_use] pub fn new(partial_input: PartialInput, required_sig: RequiredSignature) -> Self { Self { @@ -39,7 +43,7 @@ impl FinalInput { } } - /// Sets the `program_input` field with the given `ProgramInput` and returns the modified `FinalInput`. + /// Sets the program that locks the spent output. #[must_use] pub fn with_program(mut self, program_input: ProgramInput) -> Self { self.program_input = Some(program_input); @@ -47,7 +51,7 @@ impl FinalInput { self } - /// Sets the `issuance_input` field of the current instance and returns the updated `FinalInput`. + /// Sets the asset this input issues or reissues. #[must_use] pub fn with_issuance(mut self, issuance_input: IssuanceInput) -> Self { self.issuance_input = Some(issuance_input); @@ -55,12 +59,8 @@ impl FinalInput { self } - /// Retrieves the issuance details associated with the current instance. - /// - /// # Errors - /// - /// This method does not explicitly return errors but returns `None` if no issuance - /// input is available. + /// The asset IDs and entropy of this input's issuance or reissuance, or `None` if it doesn't + /// issue anything. #[must_use] pub fn get_issuance_details(&self) -> Option { self.issuance_input @@ -77,14 +77,14 @@ impl FinalInput { self.partial_input.asset_and_amount() } - /// Converts the current object into an `Input` representation, including any - /// issuance input and partial input details. + /// Converts this input, with its issuance, into a PSET input. /// - /// # Panics + /// The input's own sequence and locktime are used as they are. [`FinalTransaction::extract_pst`] + /// fills in the transaction-wide ones first and checks the issuance, so prefer `extract_pst`. /// - /// This function will panic if the `issuance_input` is of type `Reissuance` - /// and the `partial_input.secrets` field is `None` or does not contain the necessary - /// confidential information. Specifically, a panic occurs when attempting to unwrap the `asset_bf` value. + /// # Panics + /// Panics if the input reissues an asset and `partial_input.secrets` is `None`, because the + /// reissuance needs the asset blinding factor of the token it spends. #[must_use] pub fn to_input(&self) -> Input { let mut pst_input = self.partial_input.to_input(); @@ -136,7 +136,17 @@ impl FinalInput { } } -/// A struct representing a final (but not yet signed) transaction. +/// A transaction under construction: inputs, outputs, an optional change target, and the +/// transaction-wide sequence and locktime. +/// +/// Build one with [`FinalTransaction::new`] and the `add_*` methods, then hand it to a +/// [`Signer`](crate::signer::Signer). The signer can add wallet inputs to pay the fee, appends a +/// fee output and, unless the change is too small to keep, a change output before it, then +/// blinds, signs, and runs every program input. Inputs and outputs keep the order in which they +/// were added, and anything the signer adds comes after them. +/// +/// The policy-asset amounts are what pay the fee. Every other asset must already balance: add an +/// output for every non-policy asset the inputs bring in. #[derive(Clone, Debug)] pub struct FinalTransaction { inputs: Vec, @@ -147,7 +157,8 @@ pub struct FinalTransaction { } impl FinalTransaction { - /// Creates a new instance of the final transaction with default values. + /// Creates an empty transaction with no change target, no transaction-wide sequence, and a + /// zero locktime. #[must_use] #[allow(clippy::new_without_default)] pub fn new() -> Self { @@ -160,22 +171,27 @@ impl FinalTransaction { } } - /// Sets a specific `Sequence` for the transaction. - /// - /// Injects this value into the inputs that don't declare their own sequence. + /// Sets the sequence of every input that doesn't set its own with + /// [`PartialInput::with_sequence`]. pub fn set_sequence(&mut self, sequence: Sequence) { self.sequence = Some(sequence); } - /// Sets a specific `LockTime` for the transaction. + /// Sets the locktime of every input that doesn't set its own with + /// [`PartialInput::with_locktime`]. /// - /// Injects this value into the inputs that don't declare their own locktime. + /// A non-zero locktime also makes [`FinalTransaction::sequence`] default to + /// `Sequence::ENABLE_LOCKTIME_NO_RBF`, because nodes ignore the locktime of a transaction whose + /// inputs all have the final sequence. pub fn set_locktime(&mut self, locktime: LockTime) { self.locktime = locktime; } - /// The sequence of the inputs that don't declare their own. The one set with `set_sequence`, - /// or `ENABLE_LOCKTIME_NO_RBF` when a locktime is set to be enforced, and `MAX` otherwise. + /// The sequence of the inputs that don't set their own. + /// + /// This is the value given to [`FinalTransaction::set_sequence`]. Without one, it is + /// `Sequence::ENABLE_LOCKTIME_NO_RBF` if the transaction or any input sets a non-zero locktime, + /// so that the locktime is enforced, and `Sequence::MAX` otherwise. #[must_use] pub fn sequence(&self) -> Sequence { self.sequence.unwrap_or(if self.has_locktime() { @@ -185,15 +201,18 @@ impl FinalTransaction { }) } - /// The locktime of the the transaction, e.g. inputs that don't declare their own. + /// The locktime of the inputs that don't set their own, as given to + /// [`FinalTransaction::set_locktime`]. #[must_use] pub fn locktime(&self) -> LockTime { self.locktime } - /// Sets where this transaction's change should go. + /// Sets where the signer sends this transaction's change. /// - /// Left unset, the signer sends change to the single address it derives internally. + /// Without a change target, change goes to the signer's own address, blinded with the + /// signer's blinding key. An explicit target works only if the transaction has another + /// blinded output or doesn't spend any confidential input. pub fn add_change(&mut self, change: ChangeOutput) { self.change = Some(change); } @@ -203,12 +222,22 @@ impl FinalTransaction { self.change = None; } - /// Adds a native input to the transaction. + /// Adds an input that spends an output not locked by a Simplicity program, such as a wallet + /// UTXO. + /// + /// With `Some(NativeSignature::Ecdsa)`, the signer signs it as a P2WPKH input with the key at + /// the input's derivation path. With `None`, the signer leaves it unsigned, for its owner to + /// sign. pub fn add_input(&mut self, partial_input: PartialInput, signature: Option) { self.inputs.push(FinalInput::new(partial_input, signature.into())); } - /// Adds a program input to the transaction, with the signature the signer injects into its witness, if any. + /// Adds an input that spends an output locked by a Simplicity program. + /// + /// With `Some(signature)`, the signer signs the input and puts the signature into the witness + /// field that `signature` names. It replaces the field's value supplied in `program_input`, + /// or only the value at the end of the path given to [`ProgramSignature::at_path`]. With + /// `None`, the witness is used as supplied. pub fn add_program_input( &mut self, partial_input: PartialInput, @@ -219,7 +248,11 @@ impl FinalTransaction { .push(FinalInput::new(partial_input, signature.into()).with_program(program_input)); } - /// Adds a native issuance (or reissuance) input to the transaction. + /// Adds a native input, as [`FinalTransaction::add_input`] does, that also issues or reissues + /// an asset. + /// + /// Returns the IDs of the issued asset and its reissuance token, and the asset entropy that a + /// later reissuance needs. pub fn add_issuance_input( &mut self, partial_input: PartialInput, @@ -234,7 +267,11 @@ impl FinalTransaction { details } - /// Adds a program issuance (or reissuance) input to the transaction. + /// Adds a program input, as [`FinalTransaction::add_program_input`] does, that also issues or + /// reissues an asset. + /// + /// Returns the IDs of the issued asset and its reissuance token, and the asset entropy that a + /// later reissuance needs. pub fn add_program_issuance_input( &mut self, partial_input: PartialInput, @@ -258,7 +295,7 @@ impl FinalTransaction { (index < self.inputs.len()).then(|| self.inputs.remove(index)) } - /// Adds a partial output to the list of outputs. + /// Appends an output. pub fn add_output(&mut self, partial_output: PartialOutput) { self.outputs.push(partial_output); } @@ -268,56 +305,54 @@ impl FinalTransaction { (index < self.outputs.len()).then(|| self.outputs.remove(index)) } - /// Where this transaction's change should go, when the caller said. + /// The change target set with [`FinalTransaction::add_change`], if any. #[must_use] pub fn change(&self) -> Option<&ChangeOutput> { self.change.as_ref() } - /// Where this transaction's change should go, for a caller that needs to amend it. + /// The change target set with [`FinalTransaction::add_change`], if any, for changing the target + /// in place. #[must_use] pub fn change_mut(&mut self) -> Option<&mut ChangeOutput> { self.change.as_mut() } - /// Provides a slice reference to the collection of `FinalInput` elements. + /// The inputs, in the order they were added. #[must_use] pub fn inputs(&self) -> &[FinalInput] { &self.inputs } - /// Provides mutable access to the `inputs` field. - /// - /// This method returns a mutable slice of `FinalInput` elements, - /// allowing the caller to modify the elements in the `inputs` field. + /// The inputs, for changing them in place. pub fn inputs_mut(&mut self) -> &mut [FinalInput] { &mut self.inputs } - /// Returns a reference to the slice of `PartialOutput` elements contained within the struct. + /// The outputs, in the order they were added. #[must_use] pub fn outputs(&self) -> &[PartialOutput] { &self.outputs } - /// Provides mutable access to the `outputs` field of the current struct. + /// The outputs, for changing them in place. pub fn outputs_mut(&mut self) -> &mut [PartialOutput] { &mut self.outputs } - /// Returns the number of inputs associated with the current instance. + /// The number of inputs. #[must_use] pub fn n_inputs(&self) -> usize { self.inputs.len() } - /// Returns the number of outputs associated with the object. + /// The number of outputs. #[must_use] pub fn n_outputs(&self) -> usize { self.outputs.len() } - /// Checks if any of the outputs require blinding, determines if at least one of them has a `blinding_key` specified. + /// Whether any output has a blinding key, so that the signer blinds the transaction. #[must_use] pub fn needs_blinding(&self) -> bool { self.outputs.iter().any(|el| el.blinding_key.is_some()) @@ -336,12 +371,10 @@ impl FinalTransaction { }) } - /// Calculates the fee delta for a transaction based on the inputs and outputs. + /// The amount of `network`'s policy asset that the inputs bring in, minus the amount the + /// outputs pay. This is what remains for the fee and change. /// - /// The fee delta represents the net difference between the available asset amount - /// from the transaction's inputs and the consumed asset amount by its outputs. - /// The function considers the network's policy asset to determine which inputs - /// and outputs contribute to the calculation. + /// Only the outputs the inputs spend count. Amounts an input issues don't. /// /// # Errors /// Returns [`FinalTransactionError::MissingInputSecrets`] if an input's asset or @@ -369,10 +402,14 @@ impl FinalTransaction { i64::try_from(available_amount - consumed_amount).map_err(|_| FinalTransactionError::AmountOverflow) } - /// Checks if the transaction is balanced, meaning all inputs - all outputs = 0 for every asset. + /// Whether, for every asset, the inputs bring in exactly what the outputs pay. /// - /// Issued and reissued amounts are credited to the input that declares them, so a newly created - /// asset is balanced against its declared `issuance_amount`/`inflation_amount`. + /// Issued and reissued amounts count as inputs, so a newly created asset is balanced against + /// its declared `issuance_amount` and `inflation_amount`. Zero-value `OP_RETURN` outputs are + /// skipped. An output paying an asset that no input brings in makes the transaction + /// unbalanced. + /// + /// The fee is an output, so a transaction is balanced only once its fee output is present. /// /// # Errors /// Returns [`FinalTransactionError::MissingInputSecrets`] if an input's asset or @@ -425,7 +462,15 @@ impl FinalTransaction { Ok(transfers.values().all(|&value| value == 0)) } - /// Extracts a partially signed transaction (PST) and a mapping of input secrets from the current state. + /// Converts the transaction into an unsigned PSET, and returns it with each input's secrets, + /// by input index. + /// + /// Each input without its own sequence or locktime gets [`FinalTransaction::sequence`] and + /// [`FinalTransaction::locktime`]. An explicit input gets secrets with zero blinding + /// factors, so that blinding can balance against the explicit amount. Outputs are converted + /// as they are: this doesn't add any change or fee output. + /// + /// This is where code that signs outside the [`Signer`](crate::signer::Signer) takes over. /// /// # Errors /// Returns [`FinalTransactionError::MissingInputSecrets`] if an input's asset or @@ -529,7 +574,7 @@ mod tests { } #[test] - fn explicit_input_is_not_a_confidential_one() { + fn explicit_input_is_not_confidential() { let policy = dummy_asset_id(0xAA); let mut ft = FinalTransaction::new(); @@ -539,7 +584,7 @@ mod tests { } #[test] - fn input_carrying_secrets_is_a_confidential_one() { + fn input_with_secrets_is_confidential() { let policy = dummy_asset_id(0xAA); let mut ft = FinalTransaction::new(); @@ -549,7 +594,7 @@ mod tests { } #[test] - fn confidential_input_paying_an_explicit_output_leaves_nothing_blinded() { + fn explicit_output_needs_no_blinding() { let policy = dummy_asset_id(0xAA); let mut ft = FinalTransaction::new(); @@ -561,7 +606,7 @@ mod tests { } #[test] - fn blinded_output_is_what_balances_the_transaction() { + fn output_with_blinding_key_needs_blinding() { let policy = dummy_asset_id(0xAA); let mut ft = FinalTransaction::new(); @@ -572,7 +617,7 @@ mod tests { assert!(ft.needs_blinding()); } - // Manually construct PST and check extract_pst correctness based on it + // Build the expected PST by hand and compare `extract_pst` with the result #[test] fn extract_pst_single_explicit_input_single_output() { let policy = dummy_asset_id(0xAA); @@ -601,7 +646,7 @@ mod tests { } #[test] - fn declared_height_becomes_the_transactions_locktime() { + fn locktime_height_applies_to_every_input() { let policy = dummy_asset_id(0xAA); let mut ft = FinalTransaction::new(); @@ -637,7 +682,7 @@ mod tests { } #[test] - fn transaction_that_declares_no_height_still_has_none() { + fn no_locktime_keeps_final_sequences() { let policy = dummy_asset_id(0xAA); let mut ft = FinalTransaction::new(); @@ -979,7 +1024,7 @@ mod tests { } #[test] - fn token_only_issuance_has_a_null_amount_and_an_empty_one_is_rejected() { + fn token_only_issuance_is_allowed_and_empty_issuance_is_rejected() { let policy = dummy_asset_id(0xAA); let issuance = |issuance_amount, inflation_amount| { let mut tx = FinalTransaction::new(); diff --git a/crates/sdk/src/transaction/mod.rs b/crates/sdk/src/transaction/mod.rs index 430c502a..b6728ffa 100644 --- a/crates/sdk/src/transaction/mod.rs +++ b/crates/sdk/src/transaction/mod.rs @@ -1,3 +1,27 @@ +//! Building Elements transactions for the [`Signer`](crate::signer::Signer) to complete. +//! +//! A [`FinalTransaction`] holds inputs and outputs. Each input spends a [`UTXO`] through a +//! [`PartialInput`], and an input that spends a Simplicity program's output also carries a +//! [`ProgramInput`] with the program and its witness. The signature the signer adds to each +//! input is a [`NativeSignature`] or a [`ProgramSignature`]. +//! +//! ```ignore +//! let mut tx = FinalTransaction::new(); +//! +//! tx.add_program_input( +//! PartialInput::new(utxo), +//! ProgramInput::new(Box::new(program), witness), +//! Some(ProgramSignature::new("SIGNATURE")), +//! ); +//! tx.add_output(PartialOutput::new(script, 40_000, network.policy_asset())); +//! +//! let receipt = signer.broadcast(&tx)?; +//! ``` +//! +//! The signer adds the fee output, and a change output unless the change is too small to keep. +//! Don't add them yourself unless you sign with `Signer::sign_tx`, which signs exactly the +//! transaction it is given. + /// Represents outputs under construction before transaction finalization. pub mod change_output; /// Errors for transaction accounting and UTXO data access. diff --git a/crates/sdk/src/transaction/partial_input.rs b/crates/sdk/src/transaction/partial_input.rs index 8b2751de..01d9d1e2 100644 --- a/crates/sdk/src/transaction/partial_input.rs +++ b/crates/sdk/src/transaction/partial_input.rs @@ -11,34 +11,42 @@ use crate::utils; use super::UTXO; -/// Represents partially prepared input data for Elements transactions. +/// The output an input spends, with the input's own sequence, locktime and signing key. +/// +/// Create one from a [`UTXO`] with [`PartialInput::new`]. #[derive(Debug, Clone)] pub struct PartialInput { /// The outpoint of the UTXO being spent. pub outpoint: OutPoint, - /// The native transaction output corresponding to the targeted UTXO. + /// The output being spent. pub witness_utxo: TxOut, - /// The sequence number indicating transaction replaceability or relative timelocking. - /// `None` takes the transaction's. + /// This input's sequence. `None` takes the transaction's, + /// [`FinalTransaction::sequence`](super::FinalTransaction::sequence). pub sequence: Option, - /// Absolute timelock criteria enforced against the input. `None` takes the transaction's. + /// This input's locktime. `None` takes the transaction's, + /// [`FinalTransaction::locktime`](super::FinalTransaction::locktime). pub locktime: Option, - /// Optional blinding secrets mapping values and asset states into confidential outputs. - /// Note: if UTXO is confidential, `secrets` are `Some`. + /// The unblinded asset, amount and blinding factors of a confidential output. + /// + /// A confidential input needs these, because the signer must know every input's asset and + /// amount. `Signer::get_utxos` and its siblings return wallet UTXOs with them already set. pub secrets: Option, - /// Derivation path of the key that can spend this input, relative to the account path. + /// The path of the key that signs this input, relative to the signer's account path + /// `m/84h/{coin}h/0h`. `None` means the default path, `0/0`. /// - /// `None` means the signer's default path. A wallet whose UTXOs sit across many - /// derivation indices must set this per input, or the signer will sign with the wrong key. + /// A wallet whose UTXOs sit at different derivation indices must set this on each input, or + /// the signer signs with the wrong key. pub derivation_path: Option, } -/// Represents an input that runs a specific Simplicity program with an associated witness. +/// The Simplicity program that locks an input's spent output, and the witness that satisfies the +/// program. #[derive(Clone)] pub struct ProgramInput { - /// The compiled program interface associated with the input. + /// The program. Its address must be the spent output's script. pub program: Box, - /// The witness values required to satisfy the program. + /// The witness. A field that a [`ProgramSignature`](super::ProgramSignature) names holds a + /// placeholder, which the signer replaces with the signature. pub witness: WitnessValues, } @@ -50,34 +58,41 @@ impl Debug for ProgramInput { } } -/// A structure representing the details of token issuance and related metadata. +/// The assets an issuance creates, or a reissuance mints again. +/// +/// Returned by [`FinalTransaction::add_issuance_input`](super::FinalTransaction::add_issuance_input) +/// and [`FinalTransaction::add_program_issuance_input`](super::FinalTransaction::add_program_issuance_input). #[derive(Debug, Clone)] pub struct IssuanceDetails { - /// The unique `AssetId` generated from the provided entropy, representing the issued tokens struct. + /// The issued asset. pub asset_id: AssetId, - /// The `AssetId` corresponding to the reissuance (inflation) token, used for minting new tokens. + /// The reissuance token, which a later reissuance of the asset must spend. pub inflation_asset_id: AssetId, - /// The entropy value that was used to derive both the `asset_id` and `inflation_asset_id`. + /// The asset entropy both IDs derive from. Pass its bytes to + /// [`IssuanceInput::new_reissuance`] to reissue the asset. pub asset_entropy: AssetEntropy, } -/// Represents an input designated for asset issuance or reissuance. +/// An asset issuance or reissuance carried by an input. +/// +/// Issued amounts are always explicit. #[derive(Clone, Debug)] pub enum IssuanceInput { - /// Represents a completely new asset issuance. + /// Issues a new asset, whose ID derives from the input's outpoint and `asset_entropy`. Issuance { - /// The initial issuance amount for the asset. + /// The amount of the asset to issue. issuance_amount: u64, - /// The initial issuance amount for the inflation key. + /// The amount of reissuance tokens to issue. With 0, the asset can't be reissued. inflation_amount: u64, - /// The contract hash or entropy used to derive the generated `AssetId`. + /// The contract hash. asset_entropy: [u8; 32], }, - /// Represents a reissuance of an existing asset. + /// Mints more of an existing asset. The input must spend a confidential output that holds the + /// asset's reissuance token. Reissuance { - /// The amount of the generated asset to issue. + /// The amount of the asset to mint. issuance_amount: u64, - /// The original asset's entropy used to tie this reissuance back to the parent issuance. + /// The asset entropy, from [`IssuanceDetails::asset_entropy`]. asset_entropy: [u8; 32], }, } @@ -90,7 +105,8 @@ impl Default for PartialInput { } impl PartialInput { - /// Creates a new `PartialInput` from an existing `UTXO`. + /// Creates an input that spends `utxo`, keeping its secrets, with no sequence, locktime or + /// derivation path of its own. #[must_use] pub fn new(utxo: UTXO) -> Self { Self { @@ -103,10 +119,11 @@ impl PartialInput { } } - /// Sets the derivation path, relative to the account path, of the key that spends this input. + /// Sets the path of the key that signs this input, relative to the signer's account path + /// `m/84h/{coin}h/0h`. /// - /// Relative means it is appended to `m/84h/{coin}h/0h`. - /// Should be in the form of `m/n`. + /// The path must have exactly two unhardened steps, such as `0/5`. The signer returns + /// `SignerError::InvalidDerivationPath` for any other path when it signs the input. #[must_use] pub fn with_derivation_path(mut self, derivation_path: DerivationPath) -> Self { self.derivation_path = Some(derivation_path); @@ -114,7 +131,7 @@ impl PartialInput { self } - /// Sets a specific `Sequence` for the input. + /// Sets this input's sequence, overriding the transaction's. #[must_use] pub fn with_sequence(mut self, sequence: Sequence) -> Self { self.sequence = Some(sequence); @@ -122,7 +139,7 @@ impl PartialInput { self } - /// Sets a specific `LockTime` for the input. + /// Sets this input's locktime, overriding the transaction's. #[must_use] pub fn with_locktime(mut self, locktime: LockTime) -> Self { self.locktime = Some(locktime); @@ -140,8 +157,11 @@ impl PartialInput { } } - /// Converts this `PartialInput` into a fully formed PSET `Input`. - /// An unset sequence is final and an unset locktime is zero. + /// Converts this input into a PSET input. + /// + /// An unset sequence becomes `Sequence::MAX` and an unset locktime becomes zero. The + /// transaction-wide values aren't applied here; + /// [`FinalTransaction::extract_pst`](super::FinalTransaction::extract_pst) applies them. #[must_use] pub fn to_input(&self) -> Input { let locktime = self.locktime.unwrap_or(LockTime::ZERO); @@ -180,7 +200,7 @@ impl PartialInput { } impl ProgramInput { - /// Creates a new `ProgramInput` from a `ProgramTrait` and its associated `WitnessTrait`. + /// Pairs a program with its witness. #[must_use] pub fn new(program: Box, witness: impl Into) -> Self { Self { @@ -191,7 +211,10 @@ impl ProgramInput { } impl IssuanceInput { - /// Creates a new `IssuanceInput` for creating a new asset issuance. + /// Issues `issuance_amount` of a new asset and `inflation_amount` of its reissuance token. + /// + /// `asset_entropy` is the contract hash. The asset's entropy derives from it and the + /// outpoint of the input that carries the issuance. #[must_use] pub fn new_issuance(issuance_amount: u64, inflation_amount: u64, asset_entropy: [u8; 32]) -> Self { Self::Issuance { @@ -201,7 +224,10 @@ impl IssuanceInput { } } - /// Creates a new `IssuanceInput` for reissuing an existing asset. + /// Mints `issuance_amount` more of the asset whose entropy is `asset_entropy`. + /// + /// `asset_entropy` is the already derived entropy, such as + /// `details.asset_entropy.to_byte_array()` for the [`IssuanceDetails`] of the first issuance. #[must_use] pub fn new_reissuance(issuance_amount: u64, asset_entropy: [u8; 32]) -> Self { Self::Reissuance { @@ -210,7 +236,7 @@ impl IssuanceInput { } } - /// Converts this `IssuanceInput` into a partial PSET `Input` configured for issuance or reissuance. + /// The issuance fields of a PSET input. The other fields are left at their defaults. #[must_use] pub fn to_input(&self) -> Input { let (issuance_amount, asset_entropy, inflation_amount) = match self { diff --git a/crates/sdk/src/transaction/partial_output.rs b/crates/sdk/src/transaction/partial_output.rs index 29f10ebc..7f45e132 100644 --- a/crates/sdk/src/transaction/partial_output.rs +++ b/crates/sdk/src/transaction/partial_output.rs @@ -3,16 +3,20 @@ use elements_miniscript::bitcoin::PublicKey; use simplicityhl::elements::pset::Output; use simplicityhl::elements::{AssetId, Script}; -/// Represents partially prepared output data for Elements transactions. +/// An output of a [`FinalTransaction`](super::FinalTransaction), with an explicit amount and asset +/// that the signer blinds if it has a blinding key. +/// +/// A fee output is `PartialOutput::new(Script::new(), fee, network.policy_asset())`. The signer's +/// `finalize`, `finalize_strict` and `estimate_fee` add one. #[derive(Debug, Clone)] pub struct PartialOutput { - /// Represents a bound `ScriptPubKey` for arbitrary output. + /// The script the output pays to. pub script_pubkey: Script, - /// Amount of a certain transaction output + /// The amount, in the asset's base units: satoshis for the policy asset. pub amount: u64, - /// Amount of a certain transaction output + /// The asset. pub asset: AssetId, - /// Public key of a blinding key + /// The blinding public key. With one, the signer blinds this output's amount and asset. pub blinding_key: Option, } @@ -24,7 +28,7 @@ impl Default for PartialOutput { } impl PartialOutput { - /// Creates a new `PartialOutput` assigning a base script, amount, and `AssetId`. + /// Creates an explicit output paying `amount` of `asset` to `script`. #[must_use] pub fn new(script: Script, amount: u64, asset: AssetId) -> Self { Self { @@ -35,7 +39,7 @@ impl PartialOutput { } } - /// Creates a new `PartialOutput` representing an `OP_RETURN` data metadata output. + /// Creates a zero-value `OP_RETURN` output carrying `data`. #[must_use] pub fn new_metadata(data: &[u8]) -> Self { Self { @@ -46,7 +50,7 @@ impl PartialOutput { } } - /// Attaches an optional blinding public key to the partial output. + /// Sets the blinding public key, so that the signer blinds this output. #[must_use] pub fn with_blinding_key(mut self, blinding_key: PublicKey) -> Self { self.blinding_key = Some(blinding_key); @@ -54,7 +58,7 @@ impl PartialOutput { self } - /// Converts this `PartialOutput` into a fully formed PSET `Output`. + /// Converts this output into a PSET output. #[must_use] pub fn to_output(&self) -> Output { let mut output = Output::new_explicit(self.script_pubkey.clone(), self.amount, self.asset, self.blinding_key); diff --git a/crates/sdk/src/transaction/required_signature.rs b/crates/sdk/src/transaction/required_signature.rs index 456b4a7c..490391f3 100644 --- a/crates/sdk/src/transaction/required_signature.rs +++ b/crates/sdk/src/transaction/required_signature.rs @@ -3,10 +3,16 @@ use std::sync::Arc; use crate::utils::tagged_hash; -/// Derives the 32-byte message to sign from the input's `sighash_all`. +/// A function that derives the 32-byte message to sign from the input's `sighash_all`, for +/// [`SigMessage::Custom`]. pub type SigMessageFn = Arc [u8; 32] + Send + Sync>; -/// Defines the 32-byte message a witness signature actually covers. +/// The 32-byte message a program signature covers, derived from the input's Simplicity +/// `sighash_all`. +/// +/// Choose the message the contract checks the signature against: `Sighash` for a contract that +/// calls `jet::bip_0340_verify` on `jet::sig_all_hash()`, and `Tagged` or `Custom` for one that +/// hashes it first. #[derive(Clone)] pub enum SigMessage { /// Sign `sighash_all` itself. @@ -17,14 +23,24 @@ pub enum SigMessage { Custom(SigMessageFn), } -/// The signature the signer puts on a native input. +/// The signature the signer puts on a native input, one not locked by a Simplicity program. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NativeSignature { - /// A native ECDSA (WPKH) signature by the signer. + /// An ECDSA signature for a P2WPKH output, with the key at the input's derivation path. Ecdsa, } -/// A signature the signer injects into a program's witness. +/// A Schnorr signature the signer puts into a field of a program input's witness. +/// +/// ```ignore +/// // The signature goes into `witness::SIGNATURE` itself. +/// ProgramSignature::new("SIGNATURE"); +/// // It goes inside `witness::PATH`, which has the type `Either<_, Either>`. +/// ProgramSignature::new("PATH").at_path(["Right", "Left"]); +/// ``` +/// +/// The signer signs the message [`SigMessage`] derives from the input's `sighash_all`, using the +/// key at the input's derivation path. #[derive(Debug, Clone)] pub struct ProgramSignature { name: String, @@ -32,14 +48,18 @@ pub struct ProgramSignature { message: SigMessage, } -/// The signature an input needs, native or into a program's witness. +/// The signature the signer adds to an input. +/// +/// [`FinalTransaction`](super::FinalTransaction)'s `add_*` methods take an +/// `Option` or an `Option` and convert it into this type. #[derive(Debug, Clone)] pub enum RequiredSignature { - /// No signature is required. + /// The signer doesn't sign the input. A native input stays unsigned, for its owner to sign, + /// and a program input's witness is used as supplied. None, - /// A native ECDSA (WPKH) signature is required. + /// The signer signs a native input with ECDSA, as [`NativeSignature::Ecdsa`]. NativeEcdsa, - /// A signature injected into the program's witness. + /// The signer puts a signature into the program's witness, as [`ProgramSignature`] describes. Program(ProgramSignature), } @@ -67,6 +87,9 @@ impl fmt::Debug for SigMessage { impl ProgramSignature { /// A signature over `sighash_all`, put into the witness field `name`. + /// + /// `name` is the field's name in the contract without the `witness::` prefix. The signer + /// replaces the field's whole value with the 64-byte signature. #[must_use] pub fn new(name: &str) -> Self { Self { @@ -76,7 +99,17 @@ impl ProgramSignature { } } - /// Puts the signature inside the witness field at `path`, e.g. `["Right", "Left", "0"]`. + /// Puts the signature at `path` inside the witness field, rather than replacing the field. + /// + /// Each step goes one level into the field's type: `"Left"` or `"Right"` into that branch of + /// an `Either`, and a decimal index such as `"0"` into that element of an array or tuple. The + /// supplied witness value must already take the branches the path names, and the value at + /// the end of the path must have the signature's type, `[u8; 64]`. + /// + /// The signer returns `SignerError::WitnessFieldNotFound` if the program doesn't have the + /// field, and `SignerError::WitnessInjectError` if the path doesn't fit the field's type or + /// value. It panics if the program has the field but the witness supplied in the + /// [`ProgramInput`](super::ProgramInput) doesn't contain the field. #[must_use] pub fn at_path(mut self, path: I) -> Self where @@ -88,7 +121,8 @@ impl ProgramSignature { self } - /// Signs the message `message` derives from `sighash_all` instead of `sighash_all` itself. + /// Signs the message that `message` derives from `sighash_all`, instead of `sighash_all` + /// itself. #[must_use] pub fn with_message(mut self, message: SigMessage) -> Self { self.message = message; @@ -96,7 +130,8 @@ impl ProgramSignature { self } - /// Signs the BIP-340 tagged hash over `tag` and `sighash_all`. + /// Signs the BIP-340 tagged hash `sha256(sha256(tag) || sha256(tag) || sighash_all)`, as + /// `with_message(SigMessage::Tagged(tag))` does. #[must_use] pub fn tagged(self, tag: &str) -> Self { self.with_message(SigMessage::Tagged(tag.to_string())) diff --git a/crates/sdk/src/transaction/tx_receipt.rs b/crates/sdk/src/transaction/tx_receipt.rs index 9b9b270a..775eb0d7 100644 --- a/crates/sdk/src/transaction/tx_receipt.rs +++ b/crates/sdk/src/transaction/tx_receipt.rs @@ -5,7 +5,9 @@ use simplicityhl::elements::Txid; use crate::provider::{ProviderError, ProviderTrait}; -/// A receipt for a broadcast transaction, containing the provider context and the transaction ID. +/// The ID of a broadcast transaction, with the provider it was broadcast through. +/// +/// `Display` and `Debug` print the transaction ID. #[derive(Clone, Copy)] pub struct TxReceipt<'a> { provider: &'a dyn ProviderTrait, @@ -31,21 +33,22 @@ impl AsRef for TxReceipt<'_> { } impl<'a> TxReceipt<'a> { - /// Creates a new `TxReceipt` associated with a specific provider and transaction ID. + /// Creates a receipt for `tx_id`, broadcast through `provider`. pub fn new(provider: &'a dyn ProviderTrait, tx_id: Txid) -> Self { Self { provider, tx_id } } - /// Returns the ID of the broadcasted transaction. + /// Returns the ID of the broadcast transaction. #[must_use] pub fn txid(self) -> Txid { self.tx_id } - /// Blocks and waits for the transaction to be confirmed by the provider. + /// Blocks until the transaction has one confirmation, using [`ProviderTrait::wait`]. /// /// # Errors - /// Returns a `ProviderError` if the provider encounters an error while tracking the transaction state. + /// Returns `ProviderError::Confirmation` if the transaction doesn't confirm before the provider + /// stops waiting, or another `ProviderError` if a request fails. #[inline] pub fn wait(&self) -> Result<(), ProviderError> { self.provider.wait(&self.tx_id) diff --git a/crates/sdk/src/transaction/utxo.rs b/crates/sdk/src/transaction/utxo.rs index f3669225..7a78b85d 100644 --- a/crates/sdk/src/transaction/utxo.rs +++ b/crates/sdk/src/transaction/utxo.rs @@ -2,14 +2,20 @@ use simplicityhl::elements::{AssetId, OutPoint, Script, TxOut, TxOutSecrets}; use super::error::UtxoError; -/// Represents an Unspent Transaction Output (UTXO). +/// An unspent output: its outpoint, the output itself, and its unblinded secrets if it is +/// confidential. +/// +/// `UTXO::default()` is a zero-value explicit output of the all-zero asset, locked by an empty +/// script, at the null outpoint. #[derive(Debug, Clone)] pub struct UTXO { - /// Bounded outpoint for this object + /// Where the output is: the transaction that created it and its index there. pub outpoint: OutPoint, - /// Transaction output characteristics + /// The output. pub txout: TxOut, - /// Already unblinded transaction output secrets + /// The unblinded asset, amount and blinding factors, for a confidential output that has been + /// unblinded. `None` for an explicit output and for a confidential one that hasn't been + /// unblinded. pub secrets: Option, } @@ -39,7 +45,8 @@ impl UTXO { self.txout.asset.is_explicit() && self.txout.value.is_explicit() } - /// Checks if the asset or the amount is blinded. + /// Checks if the asset or the amount is blinded. This is still true once the UTXO has + /// `secrets`. #[must_use] pub fn is_confidential(&self) -> bool { self.txout.asset.is_confidential() || self.txout.value.is_confidential() @@ -63,21 +70,21 @@ impl UTXO { self.txout.value.explicit().expect("The UTXO's amount is not explicit") } - /// Retrieves the unblinded `AssetId` of the current UTXO. + /// Retrieves the unblinded `AssetId` from `secrets`. /// /// # Panics - /// - /// This function will panic if the UTXO is not blinded. The panic occurs when - /// `self.secrets` is `None`, as it expects the UTXO to be in an unblinded state to retrieve the `AssetId`. + /// Panics if `secrets` is `None`, which is the case for an explicit UTXO and for a + /// confidential one that hasn't been unblinded. #[must_use] pub fn unblinded_asset(&self) -> AssetId { self.secrets.expect("The UTXO is not unblinded").asset } - /// Retrieves the unblinded amount from the UTXO. + /// Retrieves the unblinded amount from `secrets`. /// /// # Panics - /// This function will panic if the UTXO is not confidential. + /// Panics if `secrets` is `None`, which is the case for an explicit UTXO and for a + /// confidential one that hasn't been unblinded. #[must_use] pub fn unblinded_amount(&self) -> u64 { self.secrets.expect("The UTXO is not unblinded").value diff --git a/crates/sdk/src/utils.rs b/crates/sdk/src/utils.rs index 6bee5820..3db12852 100644 --- a/crates/sdk/src/utils.rs +++ b/crates/sdk/src/utils.rs @@ -19,7 +19,8 @@ pub fn random_mnemonic() -> String { mnemonic.to_string() } -/// Generates a hardcoded "unspendable" Taproot public key. +/// The fixed Taproot internal key that [`Program`](crate::program::Program) uses by default, +/// which nobody has the private key for. /// /// # Panics /// Panics if the hardcoded byte slice is not exactly 32 bytes or cannot be parsed into a valid `XOnlyPublicKey` (which should never happen statically). @@ -32,7 +33,7 @@ pub fn tr_unspendable_key() -> secp256k1::XOnlyPublicKey { .expect("key should be valid") } -/// Calculates new sha256 midstate and binds generated entropy to the specific outpoint. +/// The entropy of an asset issued by the input spending `outpoint`, with contract hash `entropy`. #[must_use] pub fn asset_entropy(outpoint: &OutPoint, entropy: [u8; 32]) -> AssetEntropy { let contract_hash = ContractHash::from_byte_array(entropy); @@ -52,13 +53,14 @@ pub fn tagged_hash(tag: &str, data: &[u8]) -> sha256::Hash { sha256::Hash::from_engine(eng) } -/// Hashes arbitrary data with and additional `TapData` and tags. +/// `tagged_hash("TapData", data)`, the hash of a storage slot's hidden leaf in a program's +/// Taproot tree. #[must_use] pub fn tap_data_hash(data: &[u8]) -> sha256::Hash { tagged_hash("TapData", data) } -/// Computes the SHA-256 hash of a given script and returns the resulting 32-byte array. +/// The SHA-256 hash of the script's bytes, the `scripthash` that Esplora indexes outputs by. #[must_use] pub fn hash_script(script: &Script) -> [u8; 32] { sha256::Hash::hash(script.as_bytes()).to_byte_array() @@ -70,7 +72,7 @@ pub fn sat2btc(sat: u64) -> f64 { bitcoin::Amount::from_sat(sat).to_btc() } -/// Converts a whole BTC amount into Satoshi. +/// Converts a whole number of coins into satoshis. #[must_use] pub fn btc2sat(btc: u64) -> u64 { bitcoin::Amount::from_int_btc(btc).to_sat() @@ -84,7 +86,7 @@ pub fn btc2sat(btc: u64) -> u64 { /// Pass the pruned program's `bounds().cost` and the exact witness stack the input will carry. /// /// # Errors -/// Returns `ProgramError::InsufficientBudget` if the cost exceeds the budget the stack buys. +/// Returns `ProgramError::InsufficientBudget` if the cost exceeds the witness budget. #[allow(clippy::ptr_arg)] // `Cost::is_budget_valid` takes `&Vec>` to reuse its consensus encoding pub fn check_budget(cost: Cost, stack: &Vec>) -> Result<(), ProgramError> { if cost.is_budget_valid(stack) { diff --git a/crates/simplex/Cargo.toml b/crates/simplex/Cargo.toml index 7048fab5..5189beab 100644 --- a/crates/simplex/Cargo.toml +++ b/crates/simplex/Cargo.toml @@ -17,9 +17,13 @@ name = "simplex" [lints] workspace = true +[features] +default = ["test"] +test = ["dep:smplx-test", "smplx-macros/test"] + [dependencies] smplx-macros = { workspace = true } -smplx-test = { workspace = true } +smplx-test = { workspace = true, optional = true } smplx-sdk = { workspace = true, features = ["provider"] } serde = { workspace = true } diff --git a/crates/simplex/src/lib.rs b/crates/simplex/src/lib.rs index f4bbcf8f..146aa9d0 100644 --- a/crates/simplex/src/lib.rs +++ b/crates/simplex/src/lib.rs @@ -6,9 +6,14 @@ pub use simplicityhl; pub use smplx_sdk::*; +#[cfg(feature = "test")] pub use smplx_test::config::TestConfig; +#[cfg(feature = "test")] pub use smplx_test::context::{ContextMode, FuzzMode, InitMode, RegularMode, TestContext}; +#[cfg(feature = "test")] pub use smplx_test::fuzz; pub use smplx_macros; -pub use smplx_macros::{fuzz, include_simf, test}; +pub use smplx_macros::include_simf; +#[cfg(feature = "test")] +pub use smplx_macros::{fuzz, test}; diff --git a/crates/test/src/context.rs b/crates/test/src/context.rs index fedc97cf..e77076f9 100644 --- a/crates/test/src/context.rs +++ b/crates/test/src/context.rs @@ -262,7 +262,7 @@ impl TestContext { self.create_signer(random_mnemonic().as_str()) } - /// Returns the default signer, initiated from the config. + /// Returns the default signer, created from the config. pub fn get_default_signer(&self) -> &Signer { &self.mode.signer } @@ -277,7 +277,8 @@ impl TestContext { &self.mode.proptest_config } - /// Builds a fuzz engine with a fresh copy of the default signer, so it can be called repeatedly. + /// Builds a fuzz engine for `scenario` with this context's proptest configuration and a fresh + /// copy of the default signer, so it can be called repeatedly. pub fn engine(&self, scenario: Scenario) -> SimplexFuzzEngine { SimplexFuzzEngine::new( self.mode.proptest_config.clone(), diff --git a/crates/test/src/error.rs b/crates/test/src/error.rs index 3e5943f8..bf65afa0 100644 --- a/crates/test/src/error.rs +++ b/crates/test/src/error.rs @@ -21,7 +21,7 @@ pub enum TestError { #[error(transparent)] BadNetworkName(#[from] NetworkConvertError), - #[error("Occurred a network utils execution error: '{0}'")] + #[error("Network utils failed: '{0}'")] NetworkUtilsExecution(#[from] NetworkUtilsError), } @@ -30,6 +30,6 @@ pub enum NetworkUtilsError { #[error(transparent)] Provider(#[from] ProviderError), - #[error("Unsuccessful action completion, err: '{0}'")] + #[error("Network sync failed: '{0}'")] UnsuccessfulSync(String), } diff --git a/crates/test/src/fuzz/case.rs b/crates/test/src/fuzz/case.rs index 8626f830..110a45fe 100644 --- a/crates/test/src/fuzz/case.rs +++ b/crates/test/src/fuzz/case.rs @@ -11,15 +11,25 @@ use crate::fuzz::mutations::{Mutation, UtxoMap}; use crate::fuzz::program::{FuzzProgram, FuzzProgramTrait}; use crate::fuzz::scenario::Scenario; -/// What is expected to happen to the spend. +/// What a spec expects to happen to a case's spend. +/// +/// The spend is accepted when every program input of the transaction accepts. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum Expect { + /// Every program input must accept. A rejection is a liveness finding. Accept, + /// Some program input must reject. Acceptance is an unsoundness finding. Reject, + /// Either outcome is fine. A run where the spec returns this for every case fails. Any, } /// Read-only view of one fuzz case, given to bindings while it is built and to the spec before it executes. +/// +/// The transaction getters return what the program's jets read, so a spec can compute the +/// contract's expected decision the way the contract does. The exception is an output with a +/// blinding key: `output_asset` and `output_amount` return its explicit values, while the jets +/// see commitments. pub struct FuzzCase<'a> { scenario: &'a Scenario, programs: &'a [Program], @@ -91,6 +101,8 @@ impl<'a> FuzzCase<'a> { self.map.new_output_position(old_index) } + /// The SHA-256 hash of the script that input `index` spends. + /// /// # Panics /// Panics when the transaction has no input `index`. pub fn input_script_hash(&self, index: usize) -> [u8; 32] { @@ -129,34 +141,41 @@ impl<'a> FuzzCase<'a> { .unwrap_or(self.transaction.sequence()) } + /// The SHA-256 hash of the script of output `index`. + /// /// # Panics /// Panics when the transaction has no output `index`. pub fn output_script_hash(&self, index: usize) -> [u8; 32] { hash_script(&self.transaction.outputs()[index].script_pubkey) } + /// The asset of output `index`. + /// /// # Panics /// Panics when the transaction has no output `index`. pub fn output_asset(&self, index: usize) -> [u8; 32] { self.transaction.outputs()[index].asset.to_byte_array() } + /// The amount of output `index`. + /// /// # Panics /// Panics when the transaction has no output `index`. pub fn output_amount(&self, index: usize) -> u64 { self.transaction.outputs()[index].amount } - /// The lock height as the jets see it: zero when every input sequence is final or the locktime is a time. + /// The lock height the jets read: zero when every input sequence is final or the locktime is a time. pub fn lock_height(&self) -> u32 { self.read_lock(|locktime| locktime < LOCK_TIME_THRESHOLD) } - /// The lock time as the jets see it: zero when every input sequence is final or the locktime is a height. + /// The lock time the jets read: zero when every input sequence is final or the locktime is a height. pub fn lock_time(&self) -> u32 { self.read_lock(|locktime| locktime >= LOCK_TIME_THRESHOLD) } + /// The network the case is built for. pub fn network(&self) -> &SimplicityNetwork { self.signer.get_network() } diff --git a/crates/test/src/fuzz/core.rs b/crates/test/src/fuzz/core.rs index e199f1a6..884d88f4 100644 --- a/crates/test/src/fuzz/core.rs +++ b/crates/test/src/fuzz/core.rs @@ -31,18 +31,32 @@ enum CaseFailure { Finding(String), } +/// Generates and checks the cases of a [`Scenario`]. Usually created with +/// `TestContext::::engine`. +/// +/// By default, every spend is expected to be accepted, the transaction isn't mutated, and the +/// lock and authorization checks run on every accepted case. pub struct SimplexFuzzEngine { + /// The proptest configuration: the number of cases, the seed, and failure persistence. pub config: ProptestConfig, + /// The signer that signs every case. pub signer: Signer, + /// The transaction and programs being fuzzed. pub scenario: Scenario, + /// What each case is expected to do. See [`SimplexFuzzEngine::spec`]. pub spec: Box, + /// The mutations cases draw from, `None` for unmutated cases. pub mutations: Option, + /// Whether a run passes without any accepted case. pub allow_unspendable: bool, + /// Whether to run the lock check on accepted cases. pub check_lock: bool, + /// Whether to run the authorization check on accepted cases. pub check_auth: bool, } impl SimplexFuzzEngine { + /// Creates an engine for `scenario` with the default settings. pub fn new(config: ProptestConfig, signer: Signer, scenario: Scenario) -> SimplexFuzzEngine { SimplexFuzzEngine { config, @@ -56,6 +70,7 @@ impl SimplexFuzzEngine { } } + /// Replaces the proptest configuration, for example to change the number of cases. #[must_use] pub fn with_custom_config(mut self, config: ProptestConfig) -> Self { self.config = config; @@ -63,6 +78,10 @@ impl SimplexFuzzEngine { self } + /// Replaces the signer. + /// + /// The signer must not use the attacker's mnemonic, which the smart checks use, or `run` + /// panics. #[must_use] pub fn with_custom_signer(mut self, signer: Signer) -> Self { self.signer = signer; @@ -71,6 +90,10 @@ impl SimplexFuzzEngine { } /// Sets what should happen to each spend. Without a spec, every spend is expected to be accepted. + /// + /// The spec sees the case before it runs and returns an [`Expect`]. It isn't called for a + /// case that violates a witness requirement, which must be rejected. A panic in the spec is + /// reported as a broken setup. #[must_use] pub fn spec(mut self, spec: impl Fn(&FuzzCase<'_>) -> Expect + 'static) -> Self { self.spec = Box::new(spec); @@ -78,7 +101,8 @@ impl SimplexFuzzEngine { self } - /// Mutates the transaction in half the cases. The spec judges the mutated transaction. + /// Mutates the transaction, using the kinds `mutations` enables, in half of the cases that + /// don't violate a witness requirement. The spec judges the mutated transaction. #[must_use] pub fn mutations(mut self, mutations: Mutations) -> Self { self.mutations = Some(mutations); @@ -110,6 +134,12 @@ impl SimplexFuzzEngine { self } + /// Runs the cases and prints a summary to standard error. + /// + /// # Panics + /// Panics on the first finding, with the case's programs, values and mutations; if no case + /// was accepted, unless [`SimplexFuzzEngine::allow_unspendable`] was called; if the spec + /// returned [`Expect::Any`] for every case; and if the setup is broken. pub fn run(self) { let strategy = match self .scenario diff --git a/crates/test/src/fuzz/mod.rs b/crates/test/src/fuzz/mod.rs index 038e1efe..2258b0f3 100644 --- a/crates/test/src/fuzz/mod.rs +++ b/crates/test/src/fuzz/mod.rs @@ -1,3 +1,52 @@ +//! Property testing of Simplicity contracts, without a network. +//! +//! A fuzz test describes a transaction and the programs in it as a [`Scenario`], and states what +//! should happen to each spend. [`SimplexFuzzEngine`] then generates cases: for each, it draws +//! arguments, witnesses and storage for every program, optionally mutates the transaction, signs +//! it with the test's signer, and finalizes every program input locally, which checks the witness +//! budget too. It compares the result against the expectation and, for an accepted spend, runs +//! the lock and authorization checks. +//! +//! ```ignore +//! #[simplex::fuzz] +//! fn p2pk_fuzz(context: TestContext) { +//! let public_key = context.get_default_signer().get_schnorr_public_key().serialize(); +//! let program = P2pkProgram::new(P2pkArguments { public_key }).as_ref().clone(); +//! +//! let mut transaction = FinalTransaction::new(); +//! transaction.add_program_input( +//! PartialInput::default(), +//! ProgramInput::new(Box::new(program), P2pkWitness::default()), +//! Some(ProgramSignature::new("SIGNATURE")), +//! ); +//! +//! let mut scenario = Scenario::new(transaction); +//! scenario +//! .add::("p2pk") +//! .spends(0) +//! .bind_arguments(move |arguments, _| arguments.public_key = public_key); +//! +//! context.engine(scenario).run(); +//! } +//! ``` +//! +//! A run fails with one of these findings: +//! +//! - **unsound**: the contract accepted a spend that the spec rejects, or that violates a +//! witness requirement. +//! - **liveness**: the contract rejected a spend that the spec accepts. +//! - **lock bypass**: an accepted spend is still accepted after an output the program locks is +//! redirected to an attacker. +//! - **authorization bypass**: an accepted spend is still accepted with an attacker's +//! signature, or with a signature made for another transaction. +//! +//! It also fails if no case was accepted, if the spec returned [`Expect::Any`] for every case, or +//! if the setup is broken, for example if the programs don't fit the transaction. A broken setup +//! says so in its message, and doesn't mean anything about the contract. +//! +//! Failing cases aren't shrunk. The number of cases, the seed and the network come from the +//! `[test.fuzz]` section of `Simplex.toml`. + mod case; mod common; mod core; diff --git a/crates/test/src/fuzz/mutations/core.rs b/crates/test/src/fuzz/mutations/core.rs index f6195f05..90a12f39 100644 --- a/crates/test/src/fuzz/mutations/core.rs +++ b/crates/test/src/fuzz/mutations/core.rs @@ -1,4 +1,7 @@ -/// Which mutations a run draws. +/// Which kinds of [`Mutation`](crate::fuzz::Mutation) a run draws. +/// +/// `Mutations::default()` enables amounts, assets and timelocks. Layout changes, and mutating +/// UTXOs that no program spends or locks, are off by default. #[derive(Clone, Debug)] pub struct Mutations { pub(crate) amounts: bool, diff --git a/crates/test/src/fuzz/outcome.rs b/crates/test/src/fuzz/outcome.rs index 368c85a3..2a274249 100644 --- a/crates/test/src/fuzz/outcome.rs +++ b/crates/test/src/fuzz/outcome.rs @@ -19,7 +19,7 @@ pub(crate) enum Rejection { Panic, /// A pruned branch was reached, e.g. `unwrap(None)`. Unwrap, - /// A Bit Machine limit exceeded. + /// A Bit Machine limit was exceeded. Resource, /// The program costs more than its witness pays for. Budget, diff --git a/crates/test/src/fuzz/program.rs b/crates/test/src/fuzz/program.rs index 27c2e454..ef98539b 100644 --- a/crates/test/src/fuzz/program.rs +++ b/crates/test/src/fuzz/program.rs @@ -82,10 +82,12 @@ impl<'a, P: FuzzProgramTrait> FuzzProgram<'a, P> { } } + /// The name the program was added with. pub fn name(&self) -> &str { self.program_scenario.name() } + /// The program's arguments, as generated and bound. pub fn arguments(&self) -> &P::Arguments { &self.arguments } @@ -100,10 +102,13 @@ impl<'a, P: FuzzProgramTrait> FuzzProgram<'a, P> { .unwrap_or_else(|| panic!("`{}` spends no input, so it has no witness", self.name())) } + /// The program's arguments, as [`Arguments`]. pub fn raw_arguments(&self) -> &Arguments { self.program.get_arguments() } + /// The witness, as [`WitnessValues`]. + /// /// # Panics /// Panics when the program spends no input. pub fn raw_witness(&self) -> &WitnessValues { @@ -111,6 +116,7 @@ impl<'a, P: FuzzProgramTrait> FuzzProgram<'a, P> { .unwrap_or_else(|| panic!("`{}` spends no input, so it has no witness", self.name())) } + /// The compiled program. pub fn program(&self) -> &Program { self.program } @@ -124,14 +130,17 @@ impl<'a, P: FuzzProgramTrait> FuzzProgram<'a, P> { .collect() } + /// The script of the program's address on the case's network. pub fn script_pubkey(&self) -> Script { self.program.get_script_pubkey(self.network) } + /// The SHA-256 hash of [`FuzzProgram::script_pubkey`]. pub fn script_hash(&self) -> [u8; 32] { self.program.get_script_hash(self.network) } + /// The program's CMR. pub fn cmr(&self) -> [u8; 32] { self.program.get_cmr() } diff --git a/crates/test/src/fuzz/program_scenario.rs b/crates/test/src/fuzz/program_scenario.rs index 5bf48d6b..110019a3 100644 --- a/crates/test/src/fuzz/program_scenario.rs +++ b/crates/test/src/fuzz/program_scenario.rs @@ -20,13 +20,14 @@ use crate::fuzz::witness_path::WitnessPath; type Binding = Box) -> T>; type Transition = Box) -> Vec<[u8; 32]>>; -/// A builder to configure what each FuzzProgram does. +/// Configures one program of a [`Scenario`](crate::fuzz::Scenario), as returned by +/// `Scenario::add`. pub struct FuzzProgramScenarioBuilder<'a, P> { program_scenario: &'a mut FuzzProgramScenario, _program: PhantomData P>, } -/// The FuzzProgram fuzz configuration. +/// The fuzz configuration of one program. pub(crate) struct FuzzProgramScenario { pub(crate) name: String, pub(crate) inputs: Vec, @@ -186,8 +187,11 @@ impl<'a, P: FuzzProgramTrait> FuzzProgramScenarioBuilder<'a, P> { self } - /// Specifies the spending path of the program to fuzz. The path must exist in witness - /// and end with "Left" or "Right". + /// Fuzzes only the spending path at `path` in witness field `name`: every generated witness + /// takes those `Either` branches. + /// + /// The steps are those of [`ProgramSignature::at_path`](smplx_sdk::transaction::ProgramSignature::at_path), + /// and the last step must be `"Left"` or `"Right"`. pub fn spending_path(self, name: &str, path: I) -> Self where I: IntoIterator, @@ -200,7 +204,7 @@ impl<'a, P: FuzzProgramTrait> FuzzProgramScenarioBuilder<'a, P> { self } - /// Locks the transaction output at `index` to this program with the storage `transition` function returns. + /// Locks the transaction output at `index` to this program with the storage that `transition` returns. pub fn recursive(self, index: usize, transition: impl Fn(&FuzzProgram<'_, P>) -> Vec<[u8; 32]> + 'static) -> Self { let name = self.program_scenario.name.clone(); @@ -267,7 +271,9 @@ impl<'a, P: FuzzProgramTrait> FuzzProgramScenarioBuilder<'a, P> { } /// Sets the only witness values the contract accepts, after the bindings. - /// Required witness is fuzzed and the contract should reject the alternatives. + /// + /// In about half the cases, the engine keeps a generated value that differs from a required + /// one instead. The contract must reject those cases, and the spec isn't asked about them. pub fn requires_witness(self, require: impl Fn(&mut P::Witness, &FuzzCase<'_>) + 'static) -> Self { self.program_scenario .witness_requirements diff --git a/crates/test/src/fuzz/program_strategy/core.rs b/crates/test/src/fuzz/program_strategy/core.rs index 3edf38a8..d716582d 100644 --- a/crates/test/src/fuzz/program_strategy/core.rs +++ b/crates/test/src/fuzz/program_strategy/core.rs @@ -7,7 +7,9 @@ use crate::fuzz::program_strategy::FuzzProgramSchema; /// The values a program strategy generates for one program of a case. #[derive(Clone, Debug, PartialEq)] pub struct FuzzProgramValues { + /// The program's arguments. pub arguments: Arguments, + /// The witness of the inputs the program spends. pub witness: WitnessValues, /// One 32-byte value per storage slot. pub storage: Vec<[u8; 32]>, diff --git a/crates/test/src/fuzz/scenario.rs b/crates/test/src/fuzz/scenario.rs index 8280d03b..0ad352e7 100644 --- a/crates/test/src/fuzz/scenario.rs +++ b/crates/test/src/fuzz/scenario.rs @@ -18,6 +18,13 @@ use crate::fuzz::requirement::{RequiredValue, Requirements}; use crate::fuzz::scenario_strategy::{CaseChange, CaseValues, CombinedStrategy}; /// A transaction and the programs being fuzzed. +/// +/// Assemble the transaction as for a real spend, with placeholder UTXOs such as +/// `PartialInput::default()`, then [`add`](Scenario::add) each program and say which inputs it +/// spends and which outputs it locks. Each case replaces those inputs' and outputs' scripts with +/// the program's, and those inputs' witnesses with generated ones. +/// +/// `Scenario::default()` is a transaction with one default input. pub struct Scenario { transaction: FinalTransaction, program_scenarios: Vec, @@ -32,6 +39,10 @@ impl Scenario { } } + /// Adds a program of type `P`, named `name`, and returns a builder for that program. + /// + /// Names must be unique. At least one program must spend an input, and an input or output + /// can belong to only one program. pub fn add(&mut self, name: impl Into) -> FuzzProgramScenarioBuilder<'_, P> { self.program_scenarios.push(FuzzProgramScenario::new::

(name.into())); @@ -368,7 +379,7 @@ impl<'a, 's> CaseBuilder<'a, 's> { Ok(()) } - /// Sets the required witness values, then violates one of them when the case picks it. + /// Sets the required witness values, then violates the one the case picks, if any. fn require_witnesses(&mut self) -> Result>, SetupError> { let scenario = self.scenario; let mut requirements = Requirements::new(); diff --git a/crates/test/src/fuzz/utils.rs b/crates/test/src/fuzz/utils.rs index 8a9d1e86..51788c4c 100644 --- a/crates/test/src/fuzz/utils.rs +++ b/crates/test/src/fuzz/utils.rs @@ -9,10 +9,12 @@ use rand::{Rng, RngExt}; use smplx_sdk::program::ProgramSchema; +/// Random arguments for program `P`, with each value drawn by [`generate_random_value_by_ty`]. pub fn random_arguments(rng: &mut R) -> Arguments { Arguments::from_map(random_fields(&P::argument_types(), rng)) } +/// A random witness for program `P`, with each value drawn by [`generate_random_value_by_ty`]. pub fn random_witness(rng: &mut R) -> WitnessValues { WitnessValues::from_map(random_fields(&P::witness_types(), rng)) } @@ -27,6 +29,11 @@ pub(crate) fn random_fields( .collect() } +/// A random value of type `ty`. +/// +/// Integers and booleans are uniform. Each `Either` branch and each `Option` variant is picked +/// with equal probability, so an `Option` is `None` half the time, and a list's length is uniform +/// over the lengths it allows. pub fn generate_random_value_by_ty(ty: &ResolvedType, rng: &mut R) -> Value { match ty.as_inner() { TypeInner::Either(left_ty, right_ty) => match rng.random::() { diff --git a/crates/test/src/network_utils.rs b/crates/test/src/network_utils.rs index da484989..e84b85ac 100644 --- a/crates/test/src/network_utils.rs +++ b/crates/test/src/network_utils.rs @@ -32,7 +32,7 @@ impl NetworkUtils { } return Err(NetworkUtilsError::UnsuccessfulSync(format!( - "Failed to complete mining until height, got: '{h}', desired height: '{current_height}'", + "Esplora reached height {h} instead of {target_height}", ))); } diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index 0fad1f09..c63e298e 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -154,7 +154,7 @@ impl Covenant { /// Compiles the covenant and returns its Commitment Merkle Root as lowercase hex. /// /// # Errors - /// Returns an error the source fails to compile. + /// Returns an error if the source fails to compile. #[wasm_bindgen(js_name = commitmentMerkleRoot)] pub fn commitment_merkle_root(&self) -> Result { self.validate()?; @@ -166,7 +166,7 @@ impl Covenant { /// Compiles the covenant and returns the tapleaf hash of its Simplicity script, as hex. /// /// # Errors - /// Returns an error the source fails to compile. + /// Returns an error if the source fails to compile. #[wasm_bindgen(js_name = tapleafHash)] pub fn tapleaf_hash(&self) -> Result { self.validate()?; @@ -426,7 +426,7 @@ impl TransactionBuilder { } /// Sets the block height this transaction may not be mined before. - /// Unless a sequence is set, the inputs get one that enforces it. + /// Unless a sequence is set, the inputs get one that enforces the locktime. /// /// # Panics /// Panics if `height` is `500_000_000` or greater, which Elements reads as a time. @@ -436,7 +436,7 @@ impl TransactionBuilder { } /// Sets the block time this transaction may not be mined before. - /// Unless a sequence is set, the inputs get one that enforces it. + /// Unless a sequence is set, the inputs get one that enforces the locktime. /// /// # Panics /// Panics if `time` is below `500_000_000`, which Elements reads as a height. @@ -807,10 +807,10 @@ impl TransactionBuilder { self.transaction.n_outputs() } - /// The issuer contract an issuance commits to, which is nothing unless one is named. + /// The issuer contract hash an issuance commits to, all zeroes when none is given. fn issuer_contract(written: Option<&str>) -> Result<[u8; 32], String> { match written.map(str::trim).filter(|hex_id| !hex_id.is_empty()) { - Some(hex_id) if hex_id.len() != 64 => Err("an id is thirty-two bytes".to_string()), + Some(hex_id) if hex_id.len() != 64 => Err("a contract hash is 64 hex characters".to_string()), Some(hex_id) => ContractHash::from_str(hex_id) .map(ContractHash::to_byte_array) .map_err(|e| e.to_string()), @@ -818,8 +818,8 @@ impl TransactionBuilder { } } - /// Which signature a covenant input needs. Can either be a witness name like `SIGNATURE`, - /// or a withess path if the signature is embedded like `SIGNATURE.Left.Right.1` + /// Which signature a covenant input needs: a witness name like `SIGNATURE`, or a witness path + /// like `SIGNATURE.Left.Right.1` when the signature sits inside the field. /// /// `None` means no signature is required. fn program_signature(signature_witness: Option<&str>) -> Option { @@ -1031,19 +1031,19 @@ mod tests { const TRIVIAL: &str = "fn main() { }"; #[test] - fn state_leaf_that_is_not_a_full_slot_is_refused() { + fn short_state_leaf_is_rejected() { let refused = Covenant::state_leaves("[\"00\"]").expect_err("a one byte leaf"); assert!(refused.contains("is 1 bytes, and a leaf is 32"), "{refused}"); } #[test] - fn state_leaf_that_is_not_hex_is_refused() { + fn non_hex_state_leaf_is_rejected() { assert!(Covenant::state_leaves("[\"nonsense\"]").is_err()); } #[test] - fn full_width_leaves_are_read_in_the_order_they_were_given() { + fn state_leaves_keep_their_order() { let read = Covenant::state_leaves(&format!("[\"{}\", \"0x{}\"]", "00".repeat(32), "11".repeat(32))) .expect("two full leaves"); @@ -1051,7 +1051,7 @@ mod tests { } #[test] - fn full_width_state_leaf_is_accepted_and_updates_the_address() { + fn state_leaf_changes_the_address_but_not_the_cmr() { let leaf = "00".repeat(32); let other = format!("{}01", "00".repeat(31)); @@ -1064,7 +1064,7 @@ mod tests { assert_ne!(plain.address(network).unwrap(), stateful.address(network).unwrap()); assert_ne!(stateful.address(network).unwrap(), moved.address(network).unwrap()); - // The program did not change, only where its funds sit. + // The program is the same, only the address changes. assert_eq!( stateful.commitment_merkle_root().unwrap(), moved.commitment_merkle_root().unwrap() @@ -1121,21 +1121,21 @@ mod tests { } #[test] - fn reports_the_assets_liquid_actually_holds() { + fn issuance_report_matches_liquid_asset_ids() { for (txid, vout, contract, asset, _) in ON_CHAIN { assert_eq!(report_for(txid, vout, contract).asset_id, asset); } } #[test] - fn reports_the_reissuance_tokens_liquid_actually_holds() { + fn issuance_report_matches_liquid_token_ids() { for (txid, vout, contract, _, token) in ON_CHAIN { assert_eq!(report_for(txid, vout, contract).reissuance_token_id, token); } } #[test] - fn reports_an_entropy_its_own_asset_can_be_rederived_from() { + fn reported_entropy_derives_the_asset() { for (txid, vout, contract, asset, _) in ON_CHAIN { let reported = report_for(txid, vout, contract).entropy; let read_back = AssetEntropy::from_str(&reported).expect("a reported entropy"); @@ -1145,7 +1145,7 @@ mod tests { } #[test] - fn commits_to_no_issuer_contract_unless_one_is_named() { + fn missing_issuer_contract_is_all_zeroes() { let empty = [0_u8; 32]; assert_eq!(TransactionBuilder::issuer_contract(None), Ok(empty)); @@ -1155,7 +1155,7 @@ mod tests { } #[test] - fn id_leaves_in_the_form_it_arrived_in() { + fn issuer_contract_keeps_its_display_byte_order() { let written = "ce091c998b83c78bb71a632313ba3760f1763d9cfcffae02258ffa9865a37bd2"; let read = TransactionBuilder::issuer_contract(Some(written)).expect("a written id"); @@ -1165,7 +1165,7 @@ mod tests { } #[test] - fn refuses_an_issuer_contract_that_is_not_an_id() { + fn malformed_issuer_contract_is_rejected() { assert!(TransactionBuilder::issuer_contract(Some("not hex at all")).is_err()); assert!(TransactionBuilder::issuer_contract(Some("00ff")).is_err()); } @@ -1178,8 +1178,8 @@ mod tests { }"#; #[test] - fn reads_back_what_the_wallet_unblinded() { - let secrets = TransactionBuilder::blinding_secrets(SECRETS).expect("the wallet's own reading"); + fn blinding_secrets_are_parsed() { + let secrets = TransactionBuilder::blinding_secrets(SECRETS).expect("valid blinding secrets"); assert_eq!(secrets.value, 100_000); assert_eq!( @@ -1189,25 +1189,25 @@ mod tests { } #[test] - fn refuses_secrets_that_leave_a_field_out() { + fn blinding_secrets_without_a_field_are_rejected() { for missing in ["value", "asset", "assetBlindingFactor", "valueBlindingFactor"] { let without = SECRETS.replace(missing, "somethingElse"); assert!( TransactionBuilder::blinding_secrets(&without).is_err(), - "a reading without {missing} is not a reading" + "secrets without {missing} are rejected" ); } } #[test] - fn refuses_secrets_that_are_not_a_reading_at_all() { + fn malformed_blinding_secrets_are_rejected() { assert!(TransactionBuilder::blinding_secrets("not json").is_err()); assert!(TransactionBuilder::blinding_secrets("{}").is_err()); } #[test] - fn reads_a_value_written_as_a_decimal_string_exactly() { + fn decimal_string_value_is_parsed_exactly() { for (written, exact) in [ ("0", 0), ("9007199254740993", 9_007_199_254_740_993), @@ -1222,7 +1222,7 @@ mod tests { } #[test] - fn refuses_a_json_number_above_javascripts_safe_integer_limit() { + fn json_number_above_safe_integer_is_rejected() { let safe = serde_json::json!(9_007_199_254_740_991_u64); let unsafe_number = serde_json::from_str::("288230376151711740") .expect("the decimal digits emitted by JSON.stringify(2 ** 58)"); @@ -1236,7 +1236,7 @@ mod tests { } #[test] - fn refuses_an_amount_that_is_not_plain_decimal_digits() { + fn non_decimal_amount_is_rejected() { for written in [ serde_json::json!(""), serde_json::json!("+1"), @@ -1253,7 +1253,7 @@ mod tests { const TOKEN_BLINDER: &str = "0000000000000000000000000000000000000000000000000000000000000001"; const VALUE_BLINDER: &str = "0000000000000000000000000000000000000000000000000000000000000003"; - /// The entropy of the first chain vector, as `IssuanceReport` writes it. + /// The entropy of the first chain vector, in the format `IssuanceReport` uses. fn first_entropy() -> String { let (txid, vout, contract, _, _) = ON_CHAIN[0]; @@ -1271,8 +1271,8 @@ mod tests { AssetBlindingFactor::from_str(written).expect("a blinder") } - /// An output holding one unit of `asset`, committed with `committed_bf` or left explicit, - /// paired with the opening the wallet claims for it. + /// An input spending one unit of `asset`, blinded with `committed_bf` or left explicit, with + /// `claimed_bf` as its secrets. fn holding( asset: AssetId, committed_bf: Option, @@ -1304,7 +1304,7 @@ mod tests { } #[test] - fn reissuance_mints_the_asset_its_entropy_first_issued() { + fn reissuance_mints_the_original_asset() { let entropy = first_entropy(); let token = token_of(&entropy, false); let spent = holding(token, Some(blinder(TOKEN_BLINDER)), blinder(TOKEN_BLINDER)); @@ -1349,7 +1349,7 @@ mod tests { } #[test] - fn reads_a_path_relative_to_the_account() { + fn relative_path_is_parsed() { assert_eq!( TransactionBuilder::relative_path("0/7").expect("a path").to_string(), "0/7" @@ -1372,7 +1372,7 @@ mod tests { } #[test] - fn covenant_input_is_signed_with_the_key_it_names() { + fn covenant_input_keeps_its_derivation_path() { let mut builder = TransactionBuilder::new(); let added = builder.add_covenant_input( @@ -1393,7 +1393,7 @@ mod tests { } #[test] - fn covenant_input_naming_no_key_keeps_the_signers_default() { + fn covenant_input_without_path_uses_the_default_key() { let mut builder = TransactionBuilder::new(); let added = builder.add_covenant_input( @@ -1414,7 +1414,7 @@ mod tests { } #[test] - fn covenant_issuance_input_is_signed_with_the_key_it_names() { + fn covenant_issuance_input_keeps_its_derivation_path() { let mut builder = TransactionBuilder::new(); let added = builder.add_covenant_issuance_input( @@ -1438,7 +1438,7 @@ mod tests { } #[test] - fn wallet_input_is_signed_with_the_key_it_names() { + fn wallet_input_keeps_its_derivation_path() { let mut builder = TransactionBuilder::new(); let added = builder.add_wallet_input(ON_CHAIN[0].0, 0, &spent_output(), None, Some("0/7".to_string())); diff --git a/fixtures/simf/budget.simf b/fixtures/simf/budget.simf index 551a50b7..fab89feb 100644 --- a/fixtures/simf/budget.simf +++ b/fixtures/simf/budget.simf @@ -6,9 +6,9 @@ fn checksig(sig: Signature) { /// Pruning drops the branch that is not taken, so each branch is costed on its own. fn main() { match witness::SIGNATURE { - // One check fits the budget the witness stack buys. + // One check fits within the witness budget. Left(light: Signature) => checksig(light), - // Eight checks of the same signature exceed it. + // Eight checks of the same signature exceed the witness budget. Right(heavy: Signature) => { checksig(heavy); checksig(heavy); diff --git a/fixtures/simf/sig_message.simf b/fixtures/simf/sig_message.simf index eaf373e5..94281b20 100644 --- a/fixtures/simf/sig_message.simf +++ b/fixtures/simf/sig_message.simf @@ -18,7 +18,7 @@ fn tagged_message() -> u256 { } /// sha256(sig_all_hash). Not a tagged hash and not the sighash itself, -/// so neither Sighash nor Tagged can produce it. +/// so neither Sighash nor Tagged can produce this message. fn custom_message() -> u256 { let ctx: Ctx8 = jet::sha_256_ctx_8_init(); let ctx: Ctx8 = jet::sha_256_ctx_8_add_32(ctx, jet::sig_all_hash()); diff --git a/fixtures/tests/fuzz_mutation.rs b/fixtures/tests/fuzz_mutation.rs index 715003cb..9812fadc 100644 --- a/fixtures/tests/fuzz_mutation.rs +++ b/fixtures/tests/fuzz_mutation.rs @@ -102,7 +102,7 @@ fn unbalanced_transaction_is_a_setup_error(test_context: TestContext) test_context.engine(scenario).mutations(Mutations::default()).run(); } -/// The template sets the locktime to 100, which makes input 0 enforce it. +/// The template sets the locktime to 100, and input 0 enforces that locktime. #[simplex::fuzz] fn timelock_passes_with_a_spec_on_the_lock_height(test_context: TestContext) { let mut transaction = FinalTransaction::new(); @@ -185,7 +185,7 @@ fn auth_input_requirement_follows_its_utxo(test_context: TestContext) .run(); } -/// The transaction enables the locktime, so a final input has to override it. +/// The transaction enables the locktime, so a final input has to override the transaction's sequence. #[simplex::fuzz] fn sequence_mutation_reaches_the_signed_transaction(test_context: TestContext) { let mut transaction = FinalTransaction::new(); diff --git a/fixtures/tests/fuzz_testing.rs b/fixtures/tests/fuzz_testing.rs index 39aa404d..de935ea1 100644 --- a/fixtures/tests/fuzz_testing.rs +++ b/fixtures/tests/fuzz_testing.rs @@ -66,7 +66,7 @@ fn signed_witness_is_accepted(test_context: TestContext) -> anyhow::Re Ok(()) } -/// The hot path signs with the first of its two signatures, the second is never checked. +/// The hot path checks the first of its two signatures and never the second. #[simplex::fuzz] fn nested_sig_spends_through_its_hot_path(test_context: TestContext) -> anyhow::Result<()> { let public_key = test_context.get_default_signer().get_schnorr_public_key().serialize(); diff --git a/fixtures/tests/ignore_default_tests.rs b/fixtures/tests/ignore_default_tests.rs index f6007878..e7deab9e 100644 --- a/fixtures/tests/ignore_default_tests.rs +++ b/fixtures/tests/ignore_default_tests.rs @@ -1,14 +1,14 @@ -// Dummy test to insure that `simplex test` runs only `#[simplex::test]` functions +// Checks that `simplex test` runs only `#[simplex::test]` functions #[cfg(test)] mod test { #[simplex::test] - fn smplx_test_invoked(_: simplex::TestContext) -> anyhow::Result<()> { + fn simplex_test_is_run(_: simplex::TestContext) -> anyhow::Result<()> { Ok(()) } #[test] - fn default_tests_should_not_be_invoked() { + fn plain_test_is_not_run() { panic!() } } diff --git a/fixtures/tests/log_level.rs b/fixtures/tests/log_level.rs index ed5084e2..b9db0884 100644 --- a/fixtures/tests/log_level.rs +++ b/fixtures/tests/log_level.rs @@ -15,7 +15,7 @@ fn setup_dummy(context: &simplex::TestContext) -> (DummyPanicProgram, simplex::s } #[simplex::test] -fn dummy_log_level(context: simplex::TestContext) -> anyhow::Result<()> { +fn failing_program_reports_its_error(context: simplex::TestContext) -> anyhow::Result<()> { let provider = context.get_default_provider(); let signer = context.get_default_signer(); diff --git a/simplexup/simplexup b/simplexup/simplexup index b72bc35c..a9aacdb8 100755 --- a/simplexup/simplexup +++ b/simplexup/simplexup @@ -3,7 +3,7 @@ set -eo pipefail # NOTE: if you make modifications to this script, please increment the version number. # WARNING: the SemVer pattern: major.minor.patch must be followed as we use it to determine if the script is up to date. -SIMPLEXUP_INSTALLER_VERSION="0.0.8" +SIMPLEXUP_INSTALLER_VERSION="0.0.9" BASE_DIR=${XDG_CONFIG_HOME:-$HOME} SIMPLEX_DIR=${SIMPLEX_DIR:-"$BASE_DIR/.simplex"} @@ -41,7 +41,6 @@ GH_VERIFY=false main() { need_cmd git need_cmd curl - need_cmd jq while [[ -n $1 ]]; do case $1 in @@ -285,6 +284,8 @@ fetch_attestation() { say "found attestation for $SIMPLEX_VERSION version, downloading attestation bundle, checking..." + need_cmd jq + ATTESTATION_BUNDLE="$WORK_DIR/attestation.json" ensure download "${bundle_link}/download" "$ATTESTATION_BUNDLE" @@ -520,8 +521,17 @@ attested_hash() { jq -r --arg name "$1" '.subject[]? | select(.name == $name) | .digest.sha256' <<< "$ATTESTATION_STATEMENT" } +# Prints the latest release tag, read from where GitHub redirects the repo's latest release page latest_tag() { - curl -fsSL "https://api.github.com/repos/$SIMPLEX_REPO/releases/latest" | jq -r .tag_name + local url + url=$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/$SIMPLEX_REPO/releases/latest") \ + || return 1 + + case $url in + */releases/tag/*) echo "${url##*/releases/tag/}" ;; + # A repo without releases redirects to its releases list + *) return 1 ;; + esac } add_v_prefix() {