neptune-consensus 0.14.0

Consensus logic and proof abstractions for Neptune Cash.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
use std::sync::Arc;

use neptune_primitives::mast_hash::MastHash;
use neptune_primitives::network::Network;
use neptune_primitives::timestamp::Timestamp;

use crate::block::mutator_set_update::MutatorSetUpdate;
use crate::proof_abstractions::tasm::program::TritonVmProofJobOptions;
use crate::proof_abstractions::triton_vm_job_queue::TritonVmJobQueue;

pub mod announcement;
pub mod lock_script;
pub mod primitive_witness;
#[cfg(any(test, feature = "test-helpers"))]
pub mod test_helpers;
pub mod transaction_kernel;
pub mod transaction_proof;
pub mod transparent_input;
pub mod transparent_transaction_info;
pub mod utxo;
pub mod utxo_triple;
pub mod validity;

use anyhow::ensure;
use anyhow::Result;
use get_size2::GetSize;
use itertools::Itertools;
use neptune_mutator_set::mutator_set_accumulator::MutatorSetAccumulator;
use num_bigint::BigInt;
use num_rational::BigRational;
use serde::Deserialize;
use serde::Serialize;
use tasm_lib::triton_vm::proof::Claim;
use tasm_lib::twenty_first::math::bfield_codec::BFieldCodec;
use tasm_lib::twenty_first::util_types::mmr::mmr_successor_proof::MmrSuccessorProof;
use tracing::info;
pub use transaction_proof::TransactionProof;
use validity::proof_collection::ProofCollection;
use validity::single_proof::SingleProofWitness;
use validity::tasm::single_proof::merge_branch::MergeWitness;
use validity::tasm::single_proof::update_branch::UpdateWitness;

use self::primitive_witness::PrimitiveWitness;
use self::transaction_kernel::TransactionKernel;
use self::transaction_kernel::TransactionKernelModifier;
use self::transaction_kernel::TransactionKernelProxy;
use super::consensus_rule_set::ConsensusRuleSet;
use crate::transaction::validity::neptune_proof::Proof;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, GetSize)]
pub struct Transaction {
    pub kernel: TransactionKernel,

    pub proof: TransactionProof,
}

impl From<PrimitiveWitness> for Transaction {
    fn from(value: PrimitiveWitness) -> Self {
        Transaction {
            kernel: value.kernel.clone(),
            proof: TransactionProof::Witness(value),
        }
    }
}

// for simpler Arc compatibility with existing tests.
#[cfg(any(test, feature = "test-helpers"))]
impl From<Arc<Transaction>> for Transaction {
    fn from(t: Arc<Transaction>) -> Self {
        (*t).clone()
    }
}

impl Transaction {
    /// Create a new `Transaction` by updating the given one with the mutator
    /// set update. If `new_timestamp` is `None`, the timestamp from the old
    /// transaction kernel will be used.
    ///
    /// No primitive witness is present, instead a proof is given. So:
    ///  1. Verify the proof
    ///  2. Update the records
    ///  3. Prove correctness of 1 and 2
    ///  4. Use resulting proof as new witness.
    #[expect(clippy::too_many_arguments)]
    pub async fn new_with_updated_mutator_set_records_given_proof(
        old_transaction_kernel: TransactionKernel,
        previous_mutator_set_accumulator: &MutatorSetAccumulator,
        mutator_set_update: &MutatorSetUpdate,
        old_single_proof: Proof,
        triton_vm_job_queue: Arc<TritonVmJobQueue>,
        proof_job_options: TritonVmProofJobOptions,
        new_timestamp: Option<Timestamp>,
        _consensus_rule_set: ConsensusRuleSet,
    ) -> anyhow::Result<Transaction> {
        ensure!(
            old_transaction_kernel.mutator_set_hash == previous_mutator_set_accumulator.hash(),
            "Old transaction kernel's mutator set hash does not agree \
                with supplied mutator set accumulator."
        );

        // apply mutator set update to get new mutator set accumulator
        let addition_records = mutator_set_update.additions.clone();
        let mut calculated_new_mutator_set = previous_mutator_set_accumulator.clone();
        let mut new_inputs = old_transaction_kernel.inputs.clone();
        mutator_set_update
            .apply_to_accumulator_and_records(
                &mut calculated_new_mutator_set,
                &mut new_inputs.iter_mut().collect_vec(),
                &mut [],
            )
            .unwrap_or_else(|_| panic!("Could not apply mutator set update."));

        let aocl_successor_proof = MmrSuccessorProof::new_from_batch_append(
            &previous_mutator_set_accumulator.aocl,
            &addition_records
                .iter()
                .map(|addition_record| addition_record.canonical_commitment)
                .collect_vec(),
        );

        // compute new kernel
        let mut modifier = TransactionKernelModifier::default()
            .inputs(new_inputs)
            .mutator_set_hash(calculated_new_mutator_set.hash());
        if let Some(new_timestamp) = new_timestamp {
            modifier = modifier.timestamp(new_timestamp);
        }
        let new_kernel = modifier.clone_modify(&old_transaction_kernel);

        // compute updated proof through recursion
        let update_witness = UpdateWitness::from_old_transaction(
            old_transaction_kernel,
            old_single_proof,
            previous_mutator_set_accumulator.clone(),
            new_kernel.clone(),
            calculated_new_mutator_set,
            aocl_successor_proof,
        );

        info!("starting single proof via update ...");
        let single_proof = SingleProofWitness::from_update(update_witness)
            .produce(triton_vm_job_queue, proof_job_options)
            .await?;
        let proof = TransactionProof::SingleProof(single_proof);
        info!("done.");

        Ok(Transaction {
            kernel: new_kernel,
            proof,
        })
    }

    /// Determine whether the transaction is valid but not necessarily
    /// confirmable.
    ///
    /// This method tests the transaction's internal consistency in isolation,
    /// without the context of the canonical chain.
    pub async fn is_valid(&self, network: Network, consensus_rule_set: ConsensusRuleSet) -> bool {
        let kernel_hash = self.kernel.mast_hash();
        self.proof
            .verify(kernel_hash, network, consensus_rule_set)
            .await
    }

    /// Merge two transactions. Both input transactions must have a valid
    /// Proof witness for this operation to work. The `self` argument can be
    /// a transaction with a negative fee.
    ///
    /// # Panics
    ///
    /// Panics if the two transactions cannot be merged, if e.g. the mutator
    /// set hashes are not the same, if both transactions have coinbase a
    /// coinbase UTXO, if either of the transactions are *not* a single
    /// proof, or if the RHS (`other`) has a negative fee.
    pub async fn merge_with(
        self,
        other: Transaction,
        shuffle_seed: [u8; 32],
        triton_vm_job_queue: Arc<TritonVmJobQueue>,
        proof_job_options: TritonVmProofJobOptions,
        _consensus_rule_set: ConsensusRuleSet,
    ) -> Result<Transaction> {
        assert_eq!(
            self.kernel.mutator_set_hash, other.kernel.mutator_set_hash,
            "Mutator sets must be equal for transaction merger."
        );

        assert!(
            self.kernel.coinbase.is_none() && other.kernel.coinbase.is_none(),
            "Don't use me for coinbase transactions, por favor"
        );

        let merge_witness = MergeWitness::from_transactions(self, other, shuffle_seed);
        MergeWitness::merge(merge_witness, triton_vm_job_queue, proof_job_options).await
    }

    /// Calculates a fraction representing the fee-density, defined as:
    /// `transaction_fee/transaction_size`.
    pub fn fee_density(&self) -> BigRational {
        let transaction_as_bytes = bincode::serialize(&self).unwrap();
        let transaction_size = BigInt::from(transaction_as_bytes.get_size());
        let transaction_fee = self.kernel.fee.to_nau();
        BigRational::new_raw(transaction_fee.into(), transaction_size)
    }

    /// Determine if the transaction can be validly confirmed if the block has
    /// the given mutator set accumulator. Specifically, test whether the
    /// removal records determine indices absent in the mutator set sliding
    /// window Bloom filter, and whether the MMR membership proofs are valid.
    ///
    /// Why not testing AOCL MMR membership proofs? These are being verified in
    /// PrimitiveWitness::validate and ProofCollection/RemovalRecordsIntegrity.
    /// AOCL membership is a feature of *validity*, which is a pre-requisite to
    /// confirmability.
    pub fn is_confirmable_relative_to(
        &self,
        mutator_set_accumulator: &MutatorSetAccumulator,
    ) -> bool {
        self.kernel
            .is_confirmable_relative_to(mutator_set_accumulator)
            .is_ok()
    }
}

#[cfg(any(test, feature = "test-helpers"))]
impl Transaction {
    /// Create a new transaction with primitive witness for a new mutator set.
    ///
    /// Takes unpacked removal records as input.
    pub fn new_with_primitive_witness_ms_data(
        old_primitive_witness: PrimitiveWitness,
        new_addition_records: Vec<neptune_mutator_set::addition_record::AdditionRecord>,
        new_removal_records: Vec<neptune_mutator_set::removal_record::RemovalRecord>,
    ) -> Transaction {
        let mutator_set_update = MutatorSetUpdate::new(new_removal_records, new_addition_records);
        let primitive_witness =
            PrimitiveWitness::update_with_new_ms_data(old_primitive_witness, mutator_set_update);
        let kernel = primitive_witness.kernel.clone();
        let witness = TransactionProof::Witness(primitive_witness);

        Transaction {
            kernel,
            proof: witness,
        }
    }
}

#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
pub(crate) mod tests {
    use lock_script::LockScript;
    use macro_rules_attr::apply;
    use neptune_primitives::network::Network;
    use neptune_primitives::timestamp::Timestamp;
    use proptest::prelude::Strategy;
    use proptest::test_runner::TestRunner;
    use rand::random;
    use tasm_lib::prelude::Digest;
    use tests::primitive_witness::SaltedUtxos;
    use tests::utxo::Utxo;
    use tracing_test::traced_test;

    use super::*;
    use crate::proof_abstractions::test_runtime::shared_tokio_runtime;
    use crate::transaction::test_helpers::make_mock_transaction;
    use crate::transaction::utxo_triple::UtxoTriple;
    use crate::transaction::validity::single_proof::produce_single_proof;
    use crate::type_scripts::native_currency_amount::NativeCurrencyAmount;

    impl Transaction {
        /// Create a new [`Transaction`], backed by a [`SingleProof`].
        pub(crate) fn new_single_proof(kernel: TransactionKernel, proof: Proof) -> Self {
            Self {
                kernel,
                proof: TransactionProof::SingleProof(proof),
            }
        }
    }

    #[test]
    fn decode_encode_test_empty() {
        let empty_kernel = TransactionKernelProxy {
            inputs: vec![],
            outputs: vec![],
            announcements: vec![],
            fee: NativeCurrencyAmount::coins(0),
            coinbase: None,
            timestamp: Default::default(),
            mutator_set_hash: Digest::default(),
            merge_bit: false,
        }
        .into_kernel();
        let primitive_witness = PrimitiveWitness {
            input_utxos: SaltedUtxos::empty(),
            type_scripts_and_witnesses: vec![],
            lock_scripts_and_witnesses: vec![],
            input_membership_proofs: vec![],
            output_utxos: SaltedUtxos::empty(),
            output_sender_randomnesses: vec![],
            output_receiver_digests: vec![],
            mutator_set_accumulator: MutatorSetAccumulator::default(),
            kernel: empty_kernel,
        };

        let encoded = primitive_witness.encode();
        let decoded = *PrimitiveWitness::decode(&encoded).unwrap();
        assert_eq!(primitive_witness, decoded);
    }

    #[traced_test]
    #[test]
    fn tx_get_timestamp_test() {
        let output_1 = Utxo::new_native_currency(
            LockScript::anyone_can_spend().hash(),
            NativeCurrencyAmount::coins(42),
        );
        let ar = UtxoTriple {
            utxo: output_1.clone(),
            sender_randomness: random(),
            receiver_digest: random(),
        }
        .addition_record();

        // Verify that a sane timestamp is returned. `make_mock_transaction` must follow
        // the correct time convention for this test to work.
        let coinbase_transaction = make_mock_transaction(vec![], vec![ar]);
        assert!(Timestamp::now() - coinbase_transaction.kernel.timestamp < Timestamp::seconds(10));
    }

    // `traced_test` macro inserts return type that clippy doesn't like.
    // Macro is at fault.
    #[traced_test]
    #[apply(shared_tokio_runtime)]
    async fn update_single_proof_works() {
        async fn prop(
            to_be_updated: PrimitiveWitness,
            mined: PrimitiveWitness,
            consensus_rule_set: ConsensusRuleSet,
        ) {
            let network = Network::Main;
            let as_single_proof = produce_single_proof(
                &to_be_updated,
                TritonVmJobQueue::get_instance(),
                TritonVmProofJobOptions::default_with_network(network),
                consensus_rule_set,
            )
            .await
            .unwrap();
            let original_tx = Transaction {
                kernel: to_be_updated.kernel,
                proof: TransactionProof::SingleProof(as_single_proof),
            };
            assert!(original_tx.is_valid(network, consensus_rule_set).await);

            let mutator_set_update =
                MutatorSetUpdate::new(mined.kernel.inputs.clone(), mined.kernel.outputs.clone());
            let updated_tx = Transaction::new_with_updated_mutator_set_records_given_proof(
                original_tx.kernel,
                &to_be_updated.mutator_set_accumulator,
                &mutator_set_update,
                original_tx.proof.into_single_proof(),
                TritonVmJobQueue::get_instance(),
                TritonVmProofJobOptions::default_with_network(network),
                None,
                consensus_rule_set,
            )
            .await
            .unwrap();

            assert!(updated_tx.is_valid(network, consensus_rule_set).await)
        }

        let consensus_rule_set = ConsensusRuleSet::HardforkGamma;
        for (to_be_updated_params, mined_params) in [
            ((4, 4, 4), (3, 3, 3)),
            ((1, 0, 1), (1, 1, 0)),
            ((1, 1, 0), (1, 0, 0)),
            ((6, 2, 1), (1, 1, 1)),
            ((2, 2, 2), (2, 2, 2)),
        ] {
            println!("consensus_rule_set: {consensus_rule_set}");
            println!("to_be_updated_params: {to_be_updated_params:?}");
            println!("mined_params: {mined_params:?}");
            let mut test_runner = TestRunner::deterministic();
            let [to_be_updated, mined] =
                PrimitiveWitness::arbitrary_tuple_with_matching_mutator_sets([
                    to_be_updated_params,
                    mined_params,
                ])
                .new_tree(&mut test_runner)
                .unwrap()
                .current();
            assert!(to_be_updated.validate().await.is_ok());
            assert!(mined.validate().await.is_ok());

            prop(to_be_updated.clone(), mined.clone(), consensus_rule_set).await;
        }
    }

    #[traced_test]
    #[apply(shared_tokio_runtime)]
    async fn primitive_witness_update_properties() {
        async fn assert_valid_as_pw(transaction: &Transaction) {
            let TransactionProof::Witness(pw) = &transaction.proof else {
                panic!("Expected primitive witness variant");
            };
            assert!(pw.validate().await.is_ok())
        }

        let mut test_runner = TestRunner::deterministic();
        let [to_be_updated, mined] =
            PrimitiveWitness::arbitrary_tuple_with_matching_mutator_sets([(4, 4, 4), (3, 3, 3)])
                .new_tree(&mut test_runner)
                .unwrap()
                .current();
        assert!(to_be_updated.validate().await.is_ok());
        assert!(mined.validate().await.is_ok());

        let updated_with_block = Transaction::new_with_primitive_witness_ms_data(
            to_be_updated.clone(),
            mined.kernel.outputs.clone(),
            mined.kernel.inputs.clone(),
        );
        assert_valid_as_pw(&updated_with_block).await;

        // Asssert some properties of the updated transaction.
        assert_eq!(
            to_be_updated.kernel.coinbase,
            updated_with_block.kernel.coinbase
        );
        assert_eq!(to_be_updated.kernel.fee, updated_with_block.kernel.fee);
        assert_eq!(
            to_be_updated.kernel.outputs,
            updated_with_block.kernel.outputs
        );
        assert_eq!(
            to_be_updated.kernel.announcements,
            updated_with_block.kernel.announcements
        );
        assert_eq!(
            to_be_updated.kernel.inputs.len(),
            updated_with_block.kernel.inputs.len(),
        );
        assert_eq!(
            to_be_updated
                .kernel
                .inputs
                .iter()
                .map(|x| x.absolute_indices)
                .collect_vec(),
            updated_with_block
                .kernel
                .inputs
                .iter()
                .map(|x| x.absolute_indices)
                .collect_vec()
        );
        assert_ne!(
            to_be_updated.kernel.mutator_set_hash,
            updated_with_block.kernel.mutator_set_hash
        );
    }
}