• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ergoplatform / sigma-rust / 19957094785

05 Dec 2025 08:23AM UTC coverage: 86.918% (+8.5%) from 78.463%
19957094785

Pull #837

github

web-flow
Merge dec08367a into 2f840d387
Pull Request #837: Split TransactionHintsBag hints properly

44 of 53 new or added lines in 13 files covered. (83.02%)

1621 existing lines in 221 files now uncovered.

27453 of 31585 relevant lines covered (86.92%)

253204.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.57
/ergo-lib/src/wallet.rs
1
//! Wallet-related features for Ergo
2

3
pub mod box_selector;
4
pub mod derivation_path;
5
mod deterministic;
6
pub mod ext_pub_key;
7
pub mod ext_secret_key;
8
pub mod miner_fee;
9
pub mod mnemonic;
10
#[cfg(feature = "mnemonic_gen")]
11
pub mod mnemonic_generator;
12
#[cfg(feature = "std")]
13
pub mod multi_sig;
14
pub mod secret_key;
15
pub mod signing;
16
pub mod tx_builder;
17
pub mod tx_context;
18

19
use crate::ergotree_interpreter::sigma_protocol::prover::hint::{Hint, HintsBag};
20
use alloc::boxed::Box;
21
use alloc::vec::Vec;
22
use ergotree_interpreter::sigma_protocol::private_input::PrivateInput;
23
use ergotree_interpreter::sigma_protocol::prover::hint::CommitmentHint;
24
use ergotree_interpreter::sigma_protocol::prover::Prover;
25
use ergotree_interpreter::sigma_protocol::prover::ProverError;
26
use ergotree_interpreter::sigma_protocol::prover::TestProver;
27
use hashbrown::HashMap;
28
use secret_key::SecretKey;
29
use thiserror::Error;
30

31
use crate::chain::ergo_state_context::ErgoStateContext;
32
use crate::chain::transaction::reduced::reduce_tx;
33
use crate::chain::transaction::reduced::ReducedTransaction;
34
use crate::chain::transaction::unsigned::UnsignedTransaction;
35
#[cfg(feature = "std")]
36
use crate::chain::transaction::Input;
37
use crate::chain::transaction::Transaction;
38
#[cfg(feature = "std")]
39
use crate::ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
40
use crate::wallet::mnemonic::Mnemonic;
41
#[cfg(feature = "std")]
42
use crate::wallet::multi_sig::{generate_commitments, generate_commitments_for};
43

44
use self::ext_secret_key::ExtSecretKey;
45
use self::ext_secret_key::ExtSecretKeyError;
46
use self::signing::sign_reduced_transaction;
47
use self::signing::TransactionContext;
48
use self::signing::TxSigningError;
49
#[cfg(feature = "std")]
50
use self::signing::{make_context, sign_message, sign_transaction, sign_tx_input};
51

52
/// Wallet
53
pub struct Wallet {
54
    prover: Box<dyn Prover + Send + Sync>,
55
}
56

57
/// Wallet errors
58
#[allow(missing_docs)]
59
#[derive(Error, Debug)]
60
pub enum WalletError {
61
    #[error("Transaction signing error: {0}")]
62
    TxSigningError(#[from] TxSigningError),
63

64
    #[error("Prover error: {0}")]
65
    ProverError(#[from] ProverError),
66

67
    #[error("ExtSecretKeyError: {0}")]
68
    ExtSecretKeyError(#[from] ExtSecretKeyError),
69

70
    #[error("error parsing SecretKey from ExtSecretKey.bytes")]
71
    SecretKeyParsingError,
72
}
73

74
impl Wallet {
75
    /// Create wallet instance loading secret key from mnemonic
76
    /// Returns None if a DlogSecretKey cannot be parsed from the provided phrase
77
    pub fn from_mnemonic(
×
UNCOV
78
        mnemonic_phrase: &str,
×
UNCOV
79
        mnemonic_pass: &str,
×
UNCOV
80
    ) -> Result<Wallet, WalletError> {
×
81
        let seed = Mnemonic::to_seed(mnemonic_phrase, mnemonic_pass);
×
82
        let ext_sk = ExtSecretKey::derive_master(seed)?;
×
83
        Ok(Wallet::from_secrets(vec![ext_sk.secret_key()]))
×
UNCOV
84
    }
×
85

86
    /// Create Wallet from secrets
87
    pub fn from_secrets(secrets: Vec<SecretKey>) -> Wallet {
673✔
88
        let prover = TestProver {
673✔
89
            secrets: secrets.into_iter().map(PrivateInput::from).collect(),
673✔
90
        };
673✔
91
        Wallet {
673✔
92
            prover: Box::new(prover),
673✔
93
        }
673✔
94
    }
673✔
95

96
    /// Add a new secret to the wallet prover
97
    pub fn add_secret(&mut self, secret: SecretKey) {
×
98
        self.prover.append_secret(secret.into())
×
UNCOV
99
    }
×
100

101
    /// Signs a transaction
102
    #[cfg(feature = "std")]
103
    pub fn sign_transaction(
640✔
104
        &self,
640✔
105
        tx_context: TransactionContext<UnsignedTransaction>,
640✔
106
        state_context: &ErgoStateContext,
640✔
107
        tx_hints: Option<&TransactionHintsBag>,
640✔
108
    ) -> Result<Transaction, WalletError> {
640✔
109
        sign_transaction(self.prover.as_ref(), tx_context, state_context, tx_hints)
640✔
110
            .map_err(WalletError::from)
640✔
111
    }
640✔
112

113
    /// Signs a reduced transaction (generating proofs for inputs)
114
    #[cfg(feature = "std")]
115
    pub fn sign_reduced_transaction(
1✔
116
        &self,
1✔
117
        reduced_tx: ReducedTransaction,
1✔
118
        tx_hints: Option<&TransactionHintsBag>,
1✔
119
    ) -> Result<Transaction, WalletError> {
1✔
120
        sign_reduced_transaction(self.prover.as_ref(), reduced_tx, tx_hints)
1✔
121
            .map_err(WalletError::from)
1✔
122
    }
1✔
123

124
    /// Generate commitments for Transaction by wallet secrets
125
    #[cfg(feature = "std")]
126
    pub fn generate_commitments(
×
UNCOV
127
        &self,
×
UNCOV
128
        tx_context: TransactionContext<UnsignedTransaction>,
×
UNCOV
129
        state_context: &ErgoStateContext,
×
UNCOV
130
    ) -> Result<TransactionHintsBag, TxSigningError> {
×
131
        let public_keys: Vec<SigmaBoolean> = self
×
UNCOV
132
            .prover
×
UNCOV
133
            .secrets()
×
UNCOV
134
            .iter()
×
135
            .map(|secret| secret.public_image())
×
UNCOV
136
            .collect();
×
137
        generate_commitments(tx_context, state_context, public_keys.as_slice())
×
UNCOV
138
    }
×
139

140
    /// Generate Commitments for reduced Transaction
141
    #[cfg(feature = "std")]
142
    pub fn generate_commitments_for_reduced_transaction(
×
UNCOV
143
        &self,
×
UNCOV
144
        reduced_tx: ReducedTransaction,
×
UNCOV
145
    ) -> Result<TransactionHintsBag, TxSigningError> {
×
146
        let mut tx_hints = TransactionHintsBag::empty();
×
147
        let public_keys: Vec<SigmaBoolean> = self
×
UNCOV
148
            .prover
×
UNCOV
149
            .secrets()
×
UNCOV
150
            .iter()
×
151
            .map(|secret| secret.public_image())
×
UNCOV
152
            .collect();
×
153
        for (index, input) in reduced_tx.reduced_inputs().iter().enumerate() {
×
154
            let sigma_prop = input.clone().sigma_prop;
×
155
            let hints = generate_commitments_for(&sigma_prop, &public_keys);
×
156
            tx_hints.add_hints_for_input(index, hints);
×
UNCOV
157
        }
×
158
        Ok(tx_hints)
×
UNCOV
159
    }
×
160

161
    /// Generate commitments for P2PK inputs using deterministic nonces. \
162
    /// See: [`Wallet::sign_transaction_deterministic`]
163
    pub fn generate_deterministic_commitments(
288✔
164
        &self,
288✔
165
        reduced_tx: &ReducedTransaction,
288✔
166
        aux_rand: &[u8],
288✔
167
    ) -> Result<TransactionHintsBag, TxSigningError> {
288✔
168
        let mut tx_hints = TransactionHintsBag::empty();
288✔
169
        let msg = reduced_tx.unsigned_tx.bytes_to_sign()?;
288✔
170
        for (index, input) in reduced_tx.reduced_inputs().iter().enumerate() {
1,512✔
171
            if let Some(bag) = self::deterministic::generate_commitments_for(
1,512✔
172
                &*self.prover,
1,512✔
173
                &input.sigma_prop,
1,512✔
174
                &msg,
1,512✔
175
                aux_rand,
1,512✔
176
            ) {
1,512✔
177
                tx_hints.add_hints_for_input(index, bag)
1,512✔
UNCOV
178
            };
×
179
        }
180
        Ok(tx_hints)
288✔
181
    }
288✔
182

183
    /// Generate signatures for P2PK inputs deterministically
184
    ///
185
    /// Schnorr signatures need an unpredictable nonce added to the signature to avoid private key leakage. Normally this is generated using 32 bytes of entropy, but on platforms where that
186
    /// is not available, `sign_transaction_deterministic` can be used to generate the nonce using a hash of the private key and message. \
187
    /// Additionally `aux_rand` can be optionally supplied with up 32 bytes of entropy.
188
    /// # Limitations
189
    /// Only inputs that reduce to a single public key can be signed. Thus proveDhTuple, n-of-n and t-of-n signatures can not be produced using this method
190
    pub fn sign_transaction_deterministic(
288✔
191
        &self,
288✔
192
        tx_context: TransactionContext<UnsignedTransaction>,
288✔
193
        state_context: &ErgoStateContext,
288✔
194
        aux_rand: &[u8],
288✔
195
    ) -> Result<Transaction, WalletError> {
288✔
196
        let reduced_tx = reduce_tx(tx_context, state_context)?;
288✔
197
        let hints = self.generate_deterministic_commitments(&reduced_tx, aux_rand)?;
288✔
198
        sign_reduced_transaction(&*self.prover, reduced_tx, Some(&hints)).map_err(From::from)
288✔
199
    }
288✔
200

201
    /// Generate signatures for P2PK inputs deterministically
202
    /// See: [`Wallet::sign_transaction_deterministic`]
203
    pub fn sign_reduced_transaction_deterministic(
×
UNCOV
204
        &self,
×
UNCOV
205
        reduced_tx: ReducedTransaction,
×
UNCOV
206
        aux_rand: &[u8],
×
UNCOV
207
    ) -> Result<Transaction, WalletError> {
×
208
        let hints = self.generate_deterministic_commitments(&reduced_tx, aux_rand)?;
×
209
        sign_reduced_transaction(&*self.prover, reduced_tx, Some(&hints)).map_err(From::from)
×
UNCOV
210
    }
×
211

212
    /// Signs a message
213
    #[cfg(feature = "std")]
214
    pub fn sign_message(
×
UNCOV
215
        &self,
×
UNCOV
216
        sigma_tree: SigmaBoolean,
×
UNCOV
217
        msg: &[u8],
×
UNCOV
218
    ) -> Result<Vec<u8>, WalletError> {
×
219
        sign_message(self.prover.as_ref(), sigma_tree, msg).map_err(WalletError::from)
×
UNCOV
220
    }
×
221

222
    /// Signs a transaction input
223
    #[cfg(feature = "std")]
224
    pub fn sign_tx_input(
×
UNCOV
225
        &self,
×
UNCOV
226
        input_idx: usize,
×
UNCOV
227
        tx_context: TransactionContext<UnsignedTransaction>,
×
UNCOV
228
        state_context: &ErgoStateContext,
×
UNCOV
229
        tx_hints: Option<&TransactionHintsBag>,
×
UNCOV
230
    ) -> Result<Input, WalletError> {
×
231
        let tx = tx_context.spending_tx.clone();
×
232
        let message_to_sign = tx.bytes_to_sign().map_err(TxSigningError::from)?;
×
233
        let mut context =
×
UNCOV
234
            make_context(state_context, &tx_context, input_idx).map_err(TxSigningError::from)?;
×
235
        Ok(sign_tx_input(
×
236
            self.prover.as_ref(),
×
UNCOV
237
            &tx_context,
×
UNCOV
238
            state_context,
×
UNCOV
239
            &mut context,
×
UNCOV
240
            tx_hints,
×
UNCOV
241
            input_idx,
×
242
            message_to_sign.as_slice(),
×
UNCOV
243
        )?)
×
UNCOV
244
    }
×
245
}
246

247
#[cfg(feature = "arbitrary")]
248
use proptest::prelude::Strategy;
249
/// TransactionHintsBag
250
#[cfg_attr(feature = "json", derive(serde::Serialize, serde::Deserialize))]
251
#[cfg_attr(
252
    feature = "json",
253
    serde(
254
        try_from = "crate::chain::json::hint::TransactionHintsBagJson",
255
        into = "crate::chain::json::hint::TransactionHintsBagJson"
256
    )
257
)]
258
#[cfg_attr(feature = "arbitrary", derive(proptest_derive::Arbitrary))]
259
#[derive(PartialEq, Debug, Clone)]
260
pub struct TransactionHintsBag {
261
    #[cfg_attr(
262
        feature = "arbitrary",
263
        proptest(
264
            strategy = "proptest::collection::hash_map(proptest::prelude::any::<usize>(), proptest::prelude::any::<HintsBag>(), 0..5).prop_map(HashMap::from_iter)"
265
        )
266
    )]
267
    pub(crate) secret_hints: HashMap<usize, HintsBag>,
268
    #[cfg_attr(
269
        feature = "arbitrary",
270
        proptest(
271
            strategy = "proptest::collection::hash_map(proptest::prelude::any::<usize>(), proptest::prelude::any::<HintsBag>(), 0..5).prop_map(HashMap::from_iter)"
272
        )
273
    )]
274
    pub(crate) public_hints: HashMap<usize, HintsBag>,
275
}
276

277
impl TransactionHintsBag {
278
    /// Empty TransactionHintsBag
279
    pub fn empty() -> Self {
304✔
280
        TransactionHintsBag {
304✔
281
            secret_hints: HashMap::new(),
304✔
282
            public_hints: HashMap::new(),
304✔
283
        }
304✔
284
    }
304✔
285

286
    /// Private helper function to correctly segregate a HintsBag into secret and public parts.
287
    fn segregate_hints(hints_bag: HintsBag) -> (HintsBag, HintsBag) {
1,768✔
288
        let (secret, public): (Vec<Hint>, Vec<Hint>) =
1,768✔
289
            hints_bag.hints.into_iter().partition(|hint| {
3,291✔
290
                matches!(hint, Hint::CommitmentHint(CommitmentHint::OwnCommitment(_)))
3,164✔
291
            });
3,291✔
292

293
        (HintsBag { hints: secret }, HintsBag { hints: public })
1,768✔
294
    }
1,768✔
295

296
    /// Replacing Hints for an input index
297
    pub fn replace_hints_for_input(&mut self, index: usize, hints_bag: HintsBag) {
×
NEW
298
        let (secret_bag, public_bag) = Self::segregate_hints(hints_bag);
×
NEW
299
        self.secret_hints.insert(index, secret_bag);
×
NEW
300
        self.public_hints.insert(index, public_bag);
×
UNCOV
301
    }
×
302

303
    /// Adding hints for a input index
304
    pub fn add_hints_for_input(&mut self, index: usize, hints_bag: HintsBag) {
1,512✔
305
        let (mut new_secret_bag, mut new_public_bag) = Self::segregate_hints(hints_bag);
1,512✔
306

307
        // Get the existing secret hints, or an empty bag if none exist, and add the new ones.
308
        if let Some(existing_secrets) = self.secret_hints.get_mut(&index) {
1,512✔
NEW
309
            existing_secrets.hints.append(&mut new_secret_bag.hints);
×
310
        } else {
1,512✔
311
            self.secret_hints.insert(index, new_secret_bag);
1,512✔
312
        }
1,512✔
313

314
        // Get the existing public hints, or an empty bag if none exist, and add the new ones.
315
        if let Some(existing_public) = self.public_hints.get_mut(&index) {
1,512✔
NEW
316
            existing_public.hints.append(&mut new_public_bag.hints);
×
317
        } else {
1,512✔
318
            self.public_hints.insert(index, new_public_bag);
1,512✔
319
        }
1,512✔
320
    }
1,512✔
321

322
    /// Outputting HintsBag corresponding for an index
323
    /// WARNING: This HintsBag also contains private randomness that should not be shared with other signers
324
    pub fn all_hints_for_input(&self, index: usize) -> HintsBag {
1,609✔
325
        let mut all_hints = Vec::new();
1,609✔
326

327
        if let Some(secret_bag) = self.secret_hints.get(&index) {
1,609✔
328
            all_hints.extend_from_slice(&secret_bag.hints);
1,512✔
329
        }
1,512✔
330

331
        if let Some(public_bag) = self.public_hints.get(&index) {
1,609✔
332
            all_hints.extend_from_slice(&public_bag.hints);
1,512✔
333
        }
1,512✔
334

335
        HintsBag { hints: all_hints }
1,609✔
336
    }
1,609✔
337
}
338

339
#[cfg(test)]
340
#[cfg(feature = "arbitrary")]
341
mod test {
342
    use ergotree_interpreter::sigma_protocol::prover::hint::{CommitmentHint, Hint, HintsBag};
343
    use proptest::prelude::*;
344

345
    use crate::wallet::TransactionHintsBag;
346
    proptest! {
347
        #[test]
348
        fn test_segregate(hints in any::<HintsBag>()) {
349
            let (secret, public) = TransactionHintsBag::segregate_hints(hints);
350
            assert!(secret.hints.iter().all(|hint| matches!(hint, Hint::CommitmentHint(CommitmentHint::OwnCommitment(_)))));
55✔
351
            assert!(public.hints.iter().all(|hint| !matches!(hint, Hint::CommitmentHint(CommitmentHint::OwnCommitment(_)))));
212✔
352
        }
353
    }
354
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc