Skip to content

Commit 087961c

Browse files
committed
sdk: reduce runtime checks and panics in FinalTransaction builder
* enforce compile time checks by using sealed trait implementations
1 parent d707a6b commit 087961c

15 files changed

Lines changed: 189 additions & 75 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/sdk/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ serde = { workspace = true }
2020
hex = { workspace = true }
2121

2222
serde_json = { version = "1.0" }
23+
sealed = { version = "0.6.0" }
2324
elements-miniscript = { version = "0.4", features = ["base64", "serde"] }
2425
bip39 = { version = "2.0.0", features = ["rand"] }
2526
dyn-clone = { version = "1.0.20" }

crates/sdk/src/signer/core.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ use elements_miniscript::{
2828
slip77::MasterBlindingKey,
2929
};
3030

31+
use super::error::SignerError;
3132
use crate::constants::MIN_FEE;
3233
use crate::program::ProgramTrait;
3334
use crate::program::logger::ProgramLogger;
3435
use crate::provider::ProviderTrait;
3536
use crate::provider::SimplicityNetwork;
3637
use crate::signer::wtns_injector::WtnsInjector;
37-
use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, RequiredSignature, TxReceipt, UTXO};
38-
39-
use super::error::SignerError;
38+
use crate::transaction::partial_input::{NativeEcdsaSig, RequiredSignature};
39+
use crate::transaction::{FinalTransaction, PartialInput, PartialOutput, TxReceipt, UTXO};
4040

4141
/// A placeholder dummy fee amount used during transaction estimation.
4242
pub const PLACEHOLDER_FEE: u64 = 1;
@@ -209,7 +209,7 @@ impl Signer {
209209
}
210210
}
211211

212-
fee_tx.add_input(PartialInput::new(utxo), RequiredSignature::NativeEcdsa);
212+
fee_tx.add_input(PartialInput::new(utxo), NativeEcdsaSig);
213213
}
214214

215215
// need to try one more time after the loop

crates/sdk/src/transaction/final_transaction.rs

Lines changed: 27 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use simplicityhl::elements::{
99
};
1010

1111
use crate::provider::SimplicityNetwork;
12+
use crate::transaction::partial_input::{IssuanceSig, ProgramSig};
13+
use crate::transaction::{ProgramIssuanceSig, RegularSig};
1214
use crate::utils;
1315

1416
use super::partial_input::{IssuanceInput, PartialInput, ProgramInput, RequiredSignature};
@@ -157,79 +159,51 @@ impl FinalTransaction {
157159
}
158160

159161
/// Adds a new input to the transaction.
160-
///
161-
/// # Panics
162-
/// Panics if the requested signature is not `NativeEcdsa` or `None`.
163-
/// (i.e. if `required_sig` is `RequiredSignature::Witness` or `RequiredSignature::WitnessWithPath`)
164-
pub fn add_input(&mut self, partial_input: PartialInput, required_sig: RequiredSignature) {
165-
match required_sig {
166-
RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => {
167-
panic!("Requested signature is not NativeEcdsa or None")
168-
}
169-
_ => {}
170-
}
171-
172-
self.push_new_input(FinalInput::new(partial_input, required_sig));
162+
pub fn add_input<S: RegularSig>(&mut self, partial_input: PartialInput, required_sig: S) {
163+
self.push_new_input(FinalInput::new(partial_input, required_sig.into_required_sig()));
173164
}
174165

175166
/// Adds a new program input to the transaction.
176-
///
177-
/// # Panics
178-
/// The function will panic if the `required_sig` parameter is of type `RequiredSignature::NativeEcdsa`,
179-
/// as this type of signature is not applicable for program inputs.
180-
pub fn add_program_input(
167+
pub fn add_program_input<S: ProgramSig>(
181168
&mut self,
182169
partial_input: PartialInput,
183170
program_input: ProgramInput,
184-
required_sig: RequiredSignature,
171+
required_sig: S,
185172
) {
186-
if let RequiredSignature::NativeEcdsa = required_sig {
187-
panic!("Requested signature is not Witness or None");
188-
}
189-
190-
self.push_new_input(FinalInput::new(partial_input, required_sig).with_program(program_input));
173+
self.push_new_input(
174+
FinalInput::new(partial_input, required_sig.into_required_sig()).with_program(program_input),
175+
);
191176
}
192177

193178
/// Adds an issuance (or reissuance) input to the transaction.
194179
///
195180
/// # Panics
196-
/// This function panics if the `required_sig` is of type `Witness` or
197-
/// `WitnessWithPath`, as these signature types are not allowed in the current context.
198-
pub fn add_issuance_input(
181+
/// Panics if the populated input fails to return valid issuance details.
182+
pub fn add_issuance_input<S: IssuanceSig>(
199183
&mut self,
200184
partial_input: PartialInput,
201185
issuance_input: IssuanceInput,
202-
required_sig: RequiredSignature,
186+
required_sig: S,
203187
) -> IssuanceDetails {
204-
match required_sig {
205-
RequiredSignature::Witness(_) | RequiredSignature::WitnessWithPath(_, _) => {
206-
panic!("Requested signature is not NativeEcdsa or None")
207-
}
208-
_ => {}
209-
}
210-
211-
self.push_new_input(FinalInput::new(partial_input, required_sig).with_issuance(issuance_input))
212-
.unwrap()
188+
self.push_new_input(
189+
FinalInput::new(partial_input, required_sig.into_required_sig()).with_issuance(issuance_input),
190+
)
191+
.unwrap()
213192
}
214193

215194
/// Adds an issuance program input to the transaction with the specified parameters.
216195
///
217196
/// # Panics
218-
/// Panics if the `required_sig` parameter is of type `RequiredSignature::NativeEcdsa`.
219-
/// Also panics if the populated input fails to return valid issuance details.
220-
pub fn add_program_issuance_input(
197+
/// Panics if the populated input fails to return valid issuance details.
198+
pub fn add_program_issuance_input<S: ProgramIssuanceSig>(
221199
&mut self,
222200
partial_input: PartialInput,
223201
program_input: ProgramInput,
224202
issuance_input: IssuanceInput,
225-
required_sig: RequiredSignature,
203+
required_sig: S,
226204
) -> IssuanceDetails {
227-
if let RequiredSignature::NativeEcdsa = required_sig {
228-
panic!("Requested signature is not Witness or None");
229-
}
230-
231205
self.push_new_input(
232-
FinalInput::new(partial_input, required_sig)
206+
FinalInput::new(partial_input, required_sig.into_required_sig())
233207
.with_program(program_input)
234208
.with_issuance(issuance_input),
235209
)
@@ -417,7 +391,7 @@ mod tests {
417391

418392
use simplicityhl::elements::{OutPoint, Script, TxOut, Txid};
419393

420-
use crate::transaction::UTXO;
394+
use crate::transaction::{NoneSig, UTXO};
421395

422396
use super::*;
423397

@@ -460,7 +434,7 @@ mod tests {
460434
let partial_output = PartialOutput::new(Script::new(), 4000, policy);
461435

462436
let mut ft = FinalTransaction::new();
463-
ft.add_input(partial_input.clone(), RequiredSignature::None);
437+
ft.add_input(partial_input.clone(), NoneSig);
464438
ft.add_output(partial_output.clone());
465439

466440
let mut expected_pst = PartiallySignedTransaction::new_v2();
@@ -487,7 +461,7 @@ mod tests {
487461
let partial_output = PartialOutput::new(Script::new(), 2000, policy);
488462

489463
let mut ft = FinalTransaction::new();
490-
ft.add_input(partial_input.clone(), RequiredSignature::None);
464+
ft.add_input(partial_input.clone(), NoneSig);
491465
ft.add_output(partial_output.clone());
492466

493467
let mut expected_pst = PartiallySignedTransaction::new_v2();
@@ -520,8 +494,8 @@ mod tests {
520494
let output_b = PartialOutput::new(Script::new(), 800, other);
521495

522496
let mut ft = FinalTransaction::new();
523-
ft.add_input(explicit_partial.clone(), RequiredSignature::None);
524-
ft.add_input(conf_partial.clone(), RequiredSignature::None);
497+
ft.add_input(explicit_partial.clone(), NoneSig);
498+
ft.add_input(conf_partial.clone(), NoneSig);
525499
ft.add_output(output_a.clone());
526500
ft.add_output(output_b.clone());
527501

@@ -560,7 +534,7 @@ mod tests {
560534
let partial_output = PartialOutput::new(Script::new(), 4000, policy);
561535

562536
let mut ft = FinalTransaction::new();
563-
ft.add_issuance_input(partial_input.clone(), issuance.clone(), RequiredSignature::None);
537+
ft.add_issuance_input(partial_input.clone(), issuance.clone(), NoneSig);
564538
ft.add_output(partial_output.clone());
565539

566540
// build expected pst, merge partial_input and issuance manually
@@ -598,7 +572,7 @@ mod tests {
598572
let partial_output = PartialOutput::new(Script::new(), 1000, policy);
599573

600574
let mut ft = FinalTransaction::new();
601-
ft.add_issuance_input(partial_input.clone(), reissuance_input.clone(), RequiredSignature::None);
575+
ft.add_issuance_input(partial_input.clone(), reissuance_input.clone(), NoneSig);
602576
ft.add_output(partial_output.clone());
603577

604578
// build expected pst, merge partial_input and issuance manually

crates/sdk/src/transaction/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ pub mod tx_receipt;
1010
pub mod utxo;
1111

1212
pub use final_transaction::{FinalInput, FinalTransaction, IssuanceDetails};
13-
pub use partial_input::{PartialInput, ProgramInput, RequiredSignature};
13+
pub use partial_input::{
14+
PartialInput, ProgramInput, RequiredSignature,
15+
{
16+
IssuanceSig, NativeEcdsaSig, NoneSig, ProgramIssuanceSig, ProgramSig, RegularSig, Signature, WitnessSig,
17+
WitnessWithPathSig,
18+
},
19+
};
1420
pub use partial_output::PartialOutput;
1521
pub use tx_receipt::TxReceipt;
1622
pub use utxo::UTXO;

crates/sdk/src/transaction/partial_input.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,102 @@ impl IssuanceInput {
221221
}
222222
}
223223
}
224+
225+
pub use sealed::*;
226+
mod sealed {
227+
use crate::transaction::RequiredSignature;
228+
229+
/// A trait for signature types, sealed to prevent external implementations.
230+
#[sealed::sealed]
231+
pub trait Signature {
232+
/// Converts the signature requirement into the standard `RequiredSignature` enum.
233+
fn into_required_sig(self) -> RequiredSignature;
234+
}
235+
236+
/// A marker trait representing signatures allowed for non-program inputs (ECDSA or None).
237+
pub trait RegularSig: Signature {}
238+
239+
/// A marker trait representing signatures allowed for program inputs (Witness or None).
240+
pub trait ProgramSig: Signature {}
241+
242+
/// A marker trait representing signatures allowed for non-program inputs (ECDSA or None).
243+
pub trait IssuanceSig: Signature {}
244+
245+
/// A marker trait representing signatures allowed for program inputs (Witness or None).
246+
pub trait ProgramIssuanceSig: Signature {}
247+
248+
/// Represents no signature requirement.
249+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250+
pub struct NoneSig;
251+
252+
#[sealed::sealed]
253+
impl Signature for NoneSig {
254+
fn into_required_sig(self) -> RequiredSignature {
255+
RequiredSignature::None
256+
}
257+
}
258+
impl RegularSig for NoneSig {}
259+
impl ProgramIssuanceSig for NoneSig {}
260+
impl IssuanceSig for NoneSig {}
261+
impl ProgramSig for NoneSig {}
262+
263+
/// Represents a standard Native ECDSA (WPKH) signature requirement.
264+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265+
pub struct NativeEcdsaSig;
266+
267+
#[sealed::sealed]
268+
impl Signature for NativeEcdsaSig {
269+
fn into_required_sig(self) -> RequiredSignature {
270+
RequiredSignature::NativeEcdsa
271+
}
272+
}
273+
impl RegularSig for NativeEcdsaSig {}
274+
impl IssuanceSig for NativeEcdsaSig {}
275+
276+
/// Represents a generic witness payload associated with an external name.
277+
#[derive(Debug, Clone, PartialEq, Eq)]
278+
pub struct WitnessSig(pub String);
279+
280+
impl WitnessSig {
281+
/// Creates a `WitnessSig` requirement using an iterator of path segments.
282+
#[must_use]
283+
pub fn new(name: &str) -> Self {
284+
WitnessSig(name.to_string())
285+
}
286+
}
287+
#[sealed::sealed]
288+
impl Signature for WitnessSig {
289+
fn into_required_sig(self) -> RequiredSignature {
290+
RequiredSignature::Witness(self.0)
291+
}
292+
}
293+
impl ProgramIssuanceSig for WitnessSig {}
294+
impl ProgramSig for WitnessSig {}
295+
296+
/// Represents a witness payload requiring traversal through a specified path hierarchy.
297+
#[derive(Debug, Clone, PartialEq, Eq)]
298+
pub struct WitnessWithPathSig(pub String, pub Vec<String>);
299+
300+
impl WitnessWithPathSig {
301+
/// Creates a `WitnessWithPathSig` requirement using an iterator of path segments.
302+
pub fn new<I>(name: &str, path: I) -> Self
303+
where
304+
I: IntoIterator,
305+
I::Item: AsRef<str>,
306+
{
307+
Self(
308+
name.to_string(),
309+
path.into_iter().map(|s| s.as_ref().to_string()).collect(),
310+
)
311+
}
312+
}
313+
314+
#[sealed::sealed]
315+
impl Signature for WitnessWithPathSig {
316+
fn into_required_sig(self) -> RequiredSignature {
317+
RequiredSignature::WitnessWithPath(self.0, self.1)
318+
}
319+
}
320+
impl ProgramIssuanceSig for WitnessWithPathSig {}
321+
impl ProgramSig for WitnessWithPathSig {}
322+
}

examples/basic/Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/basic/tests/basic_test.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use simplex::simplicityhl::elements::Script;
22

3-
use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, RequiredSignature, TxReceipt};
3+
use simplex::transaction::{FinalTransaction, PartialInput, ProgramInput, TxReceipt, WitnessSig};
44

55
use simplex_example::artifacts::p2pk::P2pkProgram;
66
use simplex_example::artifacts::p2pk::derived_p2pk::{P2pkArguments, P2pkWitness};
@@ -44,7 +44,7 @@ fn spend_p2pk(context: &simplex::TestContext) -> anyhow::Result<TxReceipt<'_>> {
4444
ft.add_program_input(
4545
PartialInput::new(p2pk_utxos[0].clone()),
4646
ProgramInput::new(Box::new(p2pk.as_ref().clone()), Box::new(witness.clone())),
47-
RequiredSignature::Witness("SIGNATURE".to_string()),
47+
WitnessSig("SIGNATURE".to_string()),
4848
);
4949

5050
let tx_receipt = signer.broadcast(&ft)?;

0 commit comments

Comments
 (0)