Skip to main content

iota_types/unit_tests/
utils.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::collections::BTreeMap;
6
7use fastcrypto::traits::KeyPair as KeypairTraits;
8use iota_protocol_config::ProtocolConfig;
9use iota_sdk_crypto::{
10    Signer, ed25519::Ed25519PrivateKey, secp256k1::Secp256k1PrivateKey,
11    secp256r1::Secp256r1PrivateKey, simple::SimpleKeypair,
12};
13use iota_sdk_types::{
14    Address, Argument, Command, ObjectId, ProgrammableTransaction, SenderSignedTransaction,
15    SharedObjectReference, SimpleSignature, Transaction, TransactionKind, TransferObjects,
16    UserSignature, Version,
17    crypto::{Intent, MultisigAggregatedSignature, MultisigCommittee, MultisigMember},
18};
19use rand::{SeedableRng, rngs::StdRng};
20
21use crate::{
22    base_types::{dbg_addr, random_object_ref},
23    committee::Committee,
24    crypto::{
25        AccountPrivateKey, AuthorityKeyPair, AuthorityPublicKeyBytes, get_key_pair,
26        get_key_pair_from_rng,
27    },
28    error::UserInputError,
29    object::Object,
30    programmable_transaction_builder::ProgrammableTransactionBuilder,
31    transaction::{CallArg, TEST_ONLY_GAS_UNIT_FOR_TRANSFER, TransactionAPI, TransactionEnvelope},
32};
33
34pub fn make_committee_key<R>(rand: &mut R) -> (Vec<AuthorityKeyPair>, Committee)
35where
36    R: rand::CryptoRng,
37{
38    make_committee_key_num(4, rand)
39}
40
41pub fn make_committee_key_num<R>(num: usize, rand: &mut R) -> (Vec<AuthorityKeyPair>, Committee)
42where
43    R: rand::CryptoRng,
44{
45    let mut authorities: BTreeMap<AuthorityPublicKeyBytes, u64> = BTreeMap::new();
46    let mut keys = Vec::new();
47
48    for _ in 0..num {
49        let (_, inner_authority_key): (_, AuthorityKeyPair) = get_key_pair_from_rng(rand);
50        authorities.insert(
51            // address
52            AuthorityPublicKeyBytes::from(inner_authority_key.public()),
53            // voting right
54            1,
55        );
56        keys.push(inner_authority_key);
57    }
58
59    let committee = Committee::new_for_testing_with_normalized_voting_power(0, authorities);
60    (keys, committee)
61}
62
63// Creates a fake sender-signed transaction for testing. This transaction will
64// not actually work.
65pub fn create_fake_transaction() -> TransactionEnvelope {
66    let (sender, sender_key): (_, AccountPrivateKey) = get_key_pair();
67    let recipient = dbg_addr(2);
68    let object_id = ObjectId::random();
69    let object = Object::immutable_with_id_for_testing(object_id);
70    let pt = {
71        let mut builder = ProgrammableTransactionBuilder::new();
72        builder.transfer_iota(recipient, None);
73        builder.finish()
74    };
75    let tx = Transaction::new_programmable(
76        sender,
77        vec![object.object_ref()],
78        pt,
79        TEST_ONLY_GAS_UNIT_FOR_TRANSFER, // gas price is 1
80        1,
81    );
82    to_sender_signed_transaction(tx, &sender_key)
83}
84
85pub fn make_transaction_data(sender: Address) -> Transaction {
86    let object =
87        Object::immutable_with_id_for_testing(ObjectId::random_with(StdRng::from_seed([0; 32])));
88    let pt = {
89        let mut builder = ProgrammableTransactionBuilder::new();
90        builder.transfer_iota(dbg_addr(2), None);
91        builder.finish()
92    };
93    Transaction::new_programmable(
94        sender,
95        vec![object.object_ref()],
96        pt,
97        TEST_ONLY_GAS_UNIT_FOR_TRANSFER, // gas price is 1
98        1,
99    )
100}
101
102/// Make sponsored [`Transaction`] with a transfer-IOTA programmable
103/// transaction and a random gas object, for use in tests.
104pub fn make_sponsored_transaction_data(sender: Address, sponsor: Address) -> Transaction {
105    let pt = {
106        let mut builder = ProgrammableTransactionBuilder::new();
107        builder.transfer_iota(dbg_addr(2), None);
108        builder.finish()
109    };
110    Transaction::new_with_gas_coins_allow_sponsor(
111        TransactionKind::new_programmable(pt),
112        sender,
113        vec![random_object_ref()],
114        TEST_ONLY_GAS_UNIT_FOR_TRANSFER, // gas price is 1
115        1,
116        sponsor,
117    )
118}
119
120/// Make a user signed transaction with the given sender and its keypair. This
121/// is not verified or signed by authority.
122pub fn make_transaction(sender: Address, kp: &SimpleKeypair) -> TransactionEnvelope {
123    let data = make_transaction_data(sender);
124    TransactionEnvelope::from_data_and_signer(data, vec![kp])
125}
126
127// This is used to sign transaction with signer using default Intent.
128pub fn to_sender_signed_transaction(
129    tx: Transaction,
130    signer: &impl Signer<SimpleSignature>,
131) -> TransactionEnvelope {
132    to_sender_signed_transaction_with_multi_signers(tx, vec![signer])
133}
134
135pub fn to_sender_signed_transaction_with_optional_sponsor(
136    tx: Transaction,
137    sender_signature: UserSignature,
138    sponsor_signer_opt: Option<&impl Signer<SimpleSignature>>,
139) -> TransactionEnvelope {
140    let mut signatures = vec![sender_signature];
141    if let Some(sponsor) = sponsor_signer_opt {
142        let sponsor_sig = TransactionEnvelope::signature_from_signer(
143            tx.clone(),
144            Intent::iota_transaction(),
145            sponsor,
146        )
147        .into();
148        signatures.push(sponsor_sig);
149    };
150
151    TransactionEnvelope::from_user_sig_data(tx, signatures)
152}
153
154pub fn to_sender_signed_transaction_with_multi_signers(
155    tx: Transaction,
156    signers: Vec<&impl Signer<SimpleSignature>>,
157) -> TransactionEnvelope {
158    TransactionEnvelope::from_data_and_signer(tx, signers)
159}
160
161pub fn make_upgraded_multisig_tx() -> TransactionEnvelope {
162    let kp1 = Ed25519PrivateKey::random();
163    let kp2 = Secp256k1PrivateKey::random();
164    let kp3 = Secp256r1PrivateKey::random();
165    let pk1 = kp1.public_key();
166    let pk2 = kp2.public_key();
167    let pk3 = kp3.public_key();
168
169    // `MultisigCommittee::new` rejects committees mixing Ed25519 with other key
170    // schemes, but signatures over them are still accepted when every member
171    // signs.
172    let multisig_pk = MultisigCommittee::new_unchecked(
173        vec![
174            MultisigMember::new(pk1, 1),
175            MultisigMember::new(pk2, 1),
176            MultisigMember::new(pk3, 1),
177        ],
178        2,
179    );
180    let addr = Address::from(&multisig_pk);
181    let tx = make_transaction(addr, &SimpleKeypair::from(kp1.clone()));
182
183    let msg = tx.transaction().signing_digest();
184    let sig1: SimpleSignature = kp1.sign(&msg);
185    let sig2: SimpleSignature = kp2.sign(&msg);
186    let sig3: SimpleSignature = kp3.sign(&msg);
187
188    let multi_sig1 =
189        MultisigAggregatedSignature::new(vec![sig1.into(), sig2.into(), sig3.into()], multisig_pk)
190            .unwrap();
191    TransactionEnvelope::new(SenderSignedTransaction::new(
192        tx.transaction().clone(),
193        vec![UserSignature::Multisig(multi_sig1)],
194    ))
195}
196
197/// Make a sponsored transaction where both sender and sponsor sign with regular
198/// (Ed25519) signatures, for use in tests.
199///
200/// Returns the transaction together with the sender's and sponsor's addresses
201/// so callers can locate each signature within the transaction.
202pub fn make_sponsored_regular_sig_tx() -> (TransactionEnvelope, Address, Address) {
203    let (sender, sender_key): (_, AccountPrivateKey) = get_key_pair();
204    let (sponsor, sponsor_key): (_, AccountPrivateKey) = get_key_pair();
205    let tx_data = make_sponsored_transaction_data(sender, sponsor);
206    let sender_sig: UserSignature = TransactionEnvelope::signature_from_signer(
207        tx_data.clone(),
208        Intent::iota_transaction(),
209        &sender_key,
210    )
211    .into();
212    let tx =
213        to_sender_signed_transaction_with_optional_sponsor(tx_data, sender_sig, Some(&sponsor_key));
214    (tx, sender, sponsor)
215}
216
217mod move_authenticator {
218    use fastcrypto::hash::HashFunction;
219    use iota_sdk_types::{
220        Address, Digest, Identifier, MoveAuthenticator, MoveAuthenticatorV1, ObjectId,
221        SenderSignedTransaction, SharedObjectReference, Transaction, TransactionExpiration,
222        UserSignature, Version,
223    };
224
225    use crate::{
226        base_types::random_object_ref,
227        committee::EpochId,
228        crypto::DefaultHash,
229        object::OBJECT_START_VERSION,
230        programmable_transaction_builder::ProgrammableTransactionBuilder,
231        transaction::{
232            CallArg, TEST_ONLY_GAS_UNIT_FOR_TRANSFER, TransactionAPI, TransactionEnvelope,
233        },
234        utils::{make_sponsored_transaction_data, make_transaction_data},
235    };
236
237    /// Make a transaction signed with `MoveAuthenticator` for testing.
238    pub fn make_move_authenticator_tx(address: Address) -> TransactionEnvelope {
239        let data = make_transaction_data(address);
240        let (authenticator, _) = make_move_authenticator_sig(address);
241        TransactionEnvelope::new(SenderSignedTransaction::new(data, vec![authenticator]))
242    }
243
244    /// Build a [`UserSignature::MoveAuthenticator`] and the underlying
245    /// [`MoveAuthenticator`] for the given address, for use in tests.
246    ///
247    /// There is no real Move account behind this address.
248    ///
249    /// TODO: if it is necessary, AA accounts need to be supported properly in
250    /// the `AuthorityState` used for testing.
251    pub fn make_move_authenticator_sig(address: Address) -> (UserSignature, MoveAuthenticator) {
252        let authenticator = MoveAuthenticator::from(MoveAuthenticatorV1::new(
253            vec![],
254            vec![],
255            SharedObjectReference::new(address.into(), OBJECT_START_VERSION, false),
256        ));
257        let sig = UserSignature::MoveAuthenticator(authenticator.clone());
258        (sig, authenticator)
259    }
260
261    /// Make a transaction that passes a shared object to a Move call and is
262    /// signed with a [`MoveAuthenticator`] that names the same object with a
263    /// different initial shared version, for use in tests. The transaction
264    /// expires in `epoch`.
265    ///
266    /// Returns the transaction and the shared object as the transaction itself
267    /// names it.
268    pub fn make_move_authenticator_tx_with_inconsistent_shared_input(
269        address: Address,
270        epoch: EpochId,
271    ) -> (TransactionEnvelope, SharedObjectReference) {
272        let shared = SharedObjectReference::new(ObjectId::random(), Version::from(1), false);
273        let mut builder = ProgrammableTransactionBuilder::new();
274        let arg = builder.obj(CallArg::Shared(shared)).unwrap();
275        builder.programmable_move_call(
276            ObjectId::random(),
277            Identifier::from_static("module"),
278            Identifier::from_static("function"),
279            vec![],
280            vec![arg],
281        );
282        let mut data = Transaction::new_programmable(
283            address,
284            vec![random_object_ref()],
285            builder.finish(),
286            TEST_ONLY_GAS_UNIT_FOR_TRANSFER,
287            1,
288        );
289        *data.expiration_mut_for_testing() = TransactionExpiration::Epoch(epoch);
290        let authenticator = MoveAuthenticator::from(MoveAuthenticatorV1::new(
291            vec![CallArg::Shared(SharedObjectReference::new(
292                shared.object_id,
293                Version::from(2),
294                false,
295            ))],
296            vec![],
297            SharedObjectReference::new(address.into(), OBJECT_START_VERSION, false),
298        ));
299        let tx = TransactionEnvelope::new(SenderSignedTransaction::new(
300            data,
301            vec![UserSignature::MoveAuthenticator(authenticator)],
302        ));
303        (tx, shared)
304    }
305
306    /// Make a sponsored transaction where both sender and sponsor sign with
307    /// [`MoveAuthenticator`], for use in tests.
308    ///
309    /// Returns the transaction together with the sender's and sponsor's
310    /// [`MoveAuthenticator`] so callers can independently verify the expected
311    /// auth digests.
312    pub fn make_sponsored_move_authenticator_tx(
313        sender_addr: Address,
314        sponsor_addr: Address,
315    ) -> (TransactionEnvelope, MoveAuthenticator, MoveAuthenticator) {
316        let (sender_sig, sender_auth) = make_move_authenticator_sig(sender_addr);
317        let (sponsor_sig, sponsor_auth) = make_move_authenticator_sig(sponsor_addr);
318        let tx_data = make_sponsored_transaction_data(sender_addr, sponsor_addr);
319        let tx = TransactionEnvelope::new(SenderSignedTransaction::new(
320            tx_data,
321            vec![sender_sig, sponsor_sig],
322        ));
323        (tx, sender_auth, sponsor_auth)
324    }
325
326    /// Compute the Blake2b256 hash of the serialized (flag-prefixed) bytes of a
327    /// [`UserSignature`], matching the digest used for
328    /// non-[`MoveAuthenticator`] signatures by
329    /// [`UserSignature::auth_digest`].
330    pub fn blake2b256_of_sig(sig: &UserSignature) -> Digest {
331        let mut hasher = DefaultHash::default();
332        hasher.update(sig.to_bytes());
333        Digest::new(hasher.finalize().into())
334    }
335}
336
337pub use move_authenticator::*;
338
339mod passkey {
340    use iota_sdk_crypto::secp256r1::Secp256r1PrivateKey;
341    use iota_sdk_types::crypto::PasskeyAuthenticator;
342
343    use super::*;
344
345    /// Build a [`UserSignature::PasskeyAuthenticator`] backed by a
346    /// freshly-generated Secp256r1 key pair, for use in tests.
347    ///
348    /// The challenge field is 32 zero-bytes encoded as base64url without
349    /// padding, satisfying the length requirement without needing a real
350    /// WebAuthn round-trip.
351    pub fn make_passkey_authenticator_sig() -> UserSignature {
352        let r1_kp = Secp256r1PrivateKey::random();
353        let user_sig: SimpleSignature = r1_kp.sign(&[0u8; 32]);
354        let client_data_json = r#"{"type":"webauthn.get","challenge":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","origin":"https://test.iota.org"}"#;
355        let passkey =
356            PasskeyAuthenticator::new(vec![], client_data_json.to_string(), user_sig).unwrap();
357        UserSignature::PasskeyAuthenticator(passkey)
358    }
359}
360
361pub use passkey::*;
362
363/// Asserts that `err` is a `SizeLimitExceeded` error whose limit name matches
364/// `limit_name` exactly, or matches the form `{limit_name} of {value}`.
365#[track_caller]
366pub fn assert_size_limit_err(err: &UserInputError, limit_name: &str) {
367    let UserInputError::SizeLimitExceeded { limit, .. } = err else {
368        panic!("expected a size limit, got {err:?}");
369    };
370    assert!(
371        limit == limit_name || limit.starts_with(&format!("{limit_name} of ")),
372        "expected {limit_name}, got {limit}"
373    );
374}
375
376/// A programmable transaction with `count` pure inputs of `size` zero bytes
377/// each, then the randomness state object when `with_randomness` is set. Its
378/// one command uses the first two inputs, so there must be at least two.
379pub fn ptb_with_pure_inputs(
380    count: usize,
381    size: usize,
382    with_randomness: bool,
383) -> ProgrammableTransaction {
384    let mut inputs = vec![CallArg::Pure(vec![0; size]); count];
385    if with_randomness {
386        inputs.push(CallArg::Shared(SharedObjectReference {
387            object_id: ObjectId::RANDOMNESS_STATE,
388            initial_shared_version: Version::from_u64(1),
389            mutable: false,
390        }));
391    }
392    assert!(inputs.len() >= 2, "the command names the first two inputs");
393    let commands = vec![Command::TransferObjects(TransferObjects {
394        objects: vec![Argument::Input(0)],
395        address: Argument::Input(1),
396    })];
397    ProgrammableTransaction { inputs, commands }
398}
399
400/// A programmable transaction above `max_tx_size_bytes` whose every pure input
401/// is under `max_pure_argument_size`, so only the transaction size cap rejects
402/// it.
403pub fn ptb_above_max_tx_size(config: &ProtocolConfig) -> ProgrammableTransaction {
404    let pure_size = config.max_pure_argument_size() as usize - 1;
405    let count = config.max_tx_size_bytes() as usize / pure_size + 1;
406    ptb_with_pure_inputs(count, pure_size, false)
407}