kaspa-mining 2.1.0

Kaspa mining
Documentation
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
use crate::mempool::{
    Mempool,
    errors::{NonStandardError, NonStandardResult},
};
use kaspa_consensus_core::{
    constants::{MAX_SCRIPT_PUBLIC_KEY_VERSION, MAX_SOMPI},
    tx::MutableTransaction,
};
use kaspa_txscript::{p2sh_sig_scanner, script_class::ScriptClass};

/// MAX_STANDARD_P2SH_SIG_OPS is the maximum number of signature operations
/// that are considered standard in a pay-to-script-hash script.
///
/// The upper-bound execution limit comes from compute mass: some zk opcodes already cost the equivalent
/// of roughly 140-250 signature operations. However, for classic Schnorr/ECDSA signature operations, this
/// standardness limit encourages parallelism across inputs rather than concentrating work in one input.
const MAX_STANDARD_P2SH_SIG_OPS: u16 = 15;

impl Mempool {
    pub(crate) fn check_transaction_standard_in_isolation(&self, transaction: &MutableTransaction) -> NonStandardResult<()> {
        let transaction_id = transaction.id();

        // None of the output public key scripts can be a non-standard script.
        for (i, output) in transaction.tx.outputs.iter().enumerate() {
            if output.script_public_key.version() > MAX_SCRIPT_PUBLIC_KEY_VERSION {
                return Err(NonStandardError::RejectScriptPublicKeyVersion(transaction_id, i));
            }

            if ScriptClass::from_script(&output.script_public_key) == ScriptClass::NonStandard {
                return Err(NonStandardError::RejectOutputScriptClass(transaction_id, i));
            }
        }

        Ok(())
    }

    /// check_transaction_standard_in_context performs a series of checks on a transaction's
    /// inputs to ensure they are "standard". A standard transaction input within the
    /// context of this function is one whose referenced public key script is of a
    /// standard form and, for pay-to-script-hash, does not have more than
    /// MAX_STANDARD_P2SH_SIG_OPS signature operations.
    /// In addition, makes sure that the transaction's fee is above the minimum for acceptance
    /// into the mempool and relay.
    pub(crate) fn check_transaction_standard_in_context(&self, transaction: &MutableTransaction) -> NonStandardResult<()> {
        let transaction_id = transaction.id();

        for i in 0..transaction.tx.inputs.len() {
            // It is safe to elide existence and index checks here since
            // they have already been checked prior to calling this
            // function.
            let entry = transaction.entries[i].as_ref().unwrap();
            match ScriptClass::from_script(&entry.script_public_key) {
                ScriptClass::NonStandard => {
                    return Err(NonStandardError::RejectInputScriptClass(transaction_id, i));
                }
                ScriptClass::PubKey => {}
                ScriptClass::PubKeyECDSA => {}
                ScriptClass::ScriptHash => {
                    let num_sig_ops = p2sh_sig_scanner(&transaction.tx.inputs[i].signature_script, &entry.script_public_key);
                    if num_sig_ops > MAX_STANDARD_P2SH_SIG_OPS as u64 {
                        return Err(NonStandardError::RejectSignatureCount(transaction_id, i, num_sig_ops, MAX_STANDARD_P2SH_SIG_OPS));
                    }
                }
            }
        }

        // Minimum relay fee applies to normalized non-contextual mass so block-space usage has a
        // minimum cost, whether dominated by compute or by transient byte footprint.
        // Storage mass does not require an additional relay-fee floor here since storage growth is
        // sufficiently protected even under worst-case block-limit usage.
        let masses = transaction.calculated_non_contextual_masses.unwrap();
        let cofactors = self.config.mempool_mass_cofactors;
        let normalized_transient_mass = masses.normalized_transient(&cofactors);

        let fee_mass = masses.compute_mass.max(normalized_transient_mass);
        let minimum_fee = self.minimum_required_transaction_relay_fee(fee_mass, self.config.minimum_relay_transaction_fee);

        let fee = transaction.calculated_fee.unwrap();
        if fee < minimum_fee {
            return if masses.compute_mass >= normalized_transient_mass {
                Err(NonStandardError::RejectInsufficientComputeFee(transaction_id, fee, minimum_fee, masses.compute_mass))
            } else {
                Err(NonStandardError::RejectInsufficientTransientFee(transaction_id, fee, minimum_fee, normalized_transient_mass))
            };
        }

        Ok(())
    }

    /// minimum_required_transaction_relay_fee returns the minimum transaction fee required
    /// for a transaction with the passed mass to be accepted into the mempool and relayed.
    fn minimum_required_transaction_relay_fee(&self, mass: u64, minimum_relay_transaction_fee: u64) -> u64 {
        // Calculate the minimum fee for a transaction to be allowed into the
        // mempool and relayed by scaling the base fee. MinimumRelayTransactionFee is in
        // sompi/kg so multiply by mass (which is in grams) and divide by 1000 to get
        // minimum sompis.
        let mut minimum_fee = mass.saturating_mul(minimum_relay_transaction_fee) / 1000; // If we overflow it means the mass is too high, and the tx will be rejected later.
        if minimum_fee == 0 {
            minimum_fee = minimum_relay_transaction_fee;
        }

        // Set the minimum fee to the maximum possible value if the calculated
        // fee is not in the valid range for monetary amounts.
        minimum_fee.min(MAX_SOMPI)
    }
}

#[cfg(test)]
#[allow(clippy::arithmetic_side_effects)]
mod tests {
    use super::*;
    use crate::{
        MiningCounters,
        mempool::config::{Config, DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE},
    };
    use kaspa_addresses::{Address, Prefix, Version};
    use kaspa_consensus_core::{
        config::params::Params,
        constants::{MAX_TX_IN_SEQUENCE_NUM, SOMPI_PER_KASPA, TRANSIENT_BYTE_TO_MASS_FACTOR, TX_VERSION},
        mass::NonContextualMasses,
        network::NetworkType,
        subnets::SUBNETWORK_ID_NATIVE,
        tx::{ScriptPublicKey, Transaction, TransactionInput, TransactionOutpoint, TransactionOutput, UtxoEntry},
    };
    use kaspa_txscript::{
        opcodes::codes::{OpReturn, OpTrue},
        script_builder::ScriptBuilder,
    };
    use std::sync::Arc;

    const RELAY_FEE_TEST_MASS: u64 = 500_000;
    const fn default_minimum_relay_fee_for_mass(mass: u64) -> u64 {
        mass * DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE / 1000
    }

    #[test]
    fn test_calc_min_required_tx_relay_fee() {
        struct Test {
            name: &'static str,
            size: u64,
            minimum_relay_transaction_fee: u64,
            want: u64,
        }

        let tests = [
            Test {
                // Ensure combination of size and fee that are less than 1000
                // produce a non-zero fee.
                name: "250 bytes with relay fee of 3",
                size: 250,
                minimum_relay_transaction_fee: 3,
                want: 3,
            },
            Test {
                name: "100 bytes with default minimum relay fee",
                size: 100,
                minimum_relay_transaction_fee: DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
                want: default_minimum_relay_fee_for_mass(100),
            },
            Test {
                name: "large relay fee test mass with default minimum relay fee",
                size: RELAY_FEE_TEST_MASS,
                minimum_relay_transaction_fee: DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
                want: default_minimum_relay_fee_for_mass(RELAY_FEE_TEST_MASS),
            },
            Test { name: "1500 bytes with 5000 relay fee", size: 1500, minimum_relay_transaction_fee: 5000, want: 7500 },
            Test { name: "1500 bytes with 3000 relay fee", size: 1500, minimum_relay_transaction_fee: 3000, want: 4500 },
            Test { name: "782 bytes with 5000 relay fee", size: 782, minimum_relay_transaction_fee: 5000, want: 3910 },
            Test { name: "782 bytes with 3000 relay fee", size: 782, minimum_relay_transaction_fee: 3000, want: 2346 },
            Test { name: "782 bytes with 2550 relay fee", size: 782, minimum_relay_transaction_fee: 2550, want: 1994 },
        ];

        for test in tests.iter() {
            for net in NetworkType::iter() {
                let params: Params = net.into();
                let mut config =
                    Config::build_default(params.target_time_per_block(), false, params.block_mass_limits, params.block_lane_limits);
                config.minimum_relay_transaction_fee = test.minimum_relay_transaction_fee;
                let minimum_relay_transaction_fee = config.minimum_relay_transaction_fee;
                let counters = Arc::new(MiningCounters::default());
                let mempool = Mempool::new(Arc::new(config), counters);

                let got = mempool.minimum_required_transaction_relay_fee(test.size, minimum_relay_transaction_fee);
                if got != test.want {
                    println!("test_calc_min_required_tx_relay_fee test '{}' failed: got {}, want {}", test.name, got, test.want);
                }
                assert_eq!(test.want, got);
            }
        }
    }

    #[test]
    fn test_check_transaction_standard_in_isolation() {
        // Create some dummy, but otherwise standard, data for transactions.
        let dummy_prev_out = TransactionOutpoint::new(kaspa_hashes::Hash::from_u64_word(1), 1);
        let dummy_sig_script = vec![0u8; 65];
        let dummy_tx_input = TransactionInput::new(dummy_prev_out, dummy_sig_script, MAX_TX_IN_SEQUENCE_NUM, 1);
        let addr_hash = vec![1u8; 32];

        let addr = Address::new(Prefix::Testnet, Version::PubKey, &addr_hash);
        let dummy_script_public_key = kaspa_txscript::pay_to_address_script(&addr);
        let dummy_tx_out = TransactionOutput::new(SOMPI_PER_KASPA, dummy_script_public_key);

        struct Test {
            name: &'static str,
            mtx: MutableTransaction,
            is_standard: bool,
        }

        fn new_mtx(tx: Transaction, mass: u64) -> MutableTransaction {
            let mut mtx = MutableTransaction::from_tx(tx);
            mtx.calculated_non_contextual_masses = Some(NonContextualMasses::new(mass, mass));
            mtx
        }

        let tests = vec![
            Test {
                name: "Typical pay-to-pubkey transaction",
                mtx: new_mtx(
                    Transaction::new(
                        TX_VERSION,
                        vec![dummy_tx_input.clone()],
                        vec![dummy_tx_out.clone()],
                        0,
                        SUBNETWORK_ID_NATIVE,
                        0,
                        vec![],
                    ),
                    1000,
                ),
                is_standard: true,
            },
            Test {
                name: "Transaction version above allowed",
                mtx: new_mtx(
                    Transaction::new(
                        TX_VERSION + 2,
                        vec![dummy_tx_input.clone()],
                        vec![dummy_tx_out.clone()],
                        0,
                        SUBNETWORK_ID_NATIVE,
                        0,
                        vec![],
                    ),
                    1000,
                ),
                is_standard: true, // check_transaction_standard_in_isolation does not check version
            },
            Test {
                name: "Valid but non standard public key script",
                mtx: new_mtx(
                    Transaction::new(
                        TX_VERSION,
                        vec![dummy_tx_input.clone()],
                        vec![TransactionOutput::new(
                            SOMPI_PER_KASPA,
                            ScriptPublicKey::new(
                                MAX_SCRIPT_PUBLIC_KEY_VERSION,
                                ScriptBuilder::new().add_op(OpTrue).unwrap().script().into(),
                            ),
                        )],
                        0,
                        SUBNETWORK_ID_NATIVE,
                        0,
                        vec![],
                    ),
                    1000,
                ),
                is_standard: false,
            },
            Test {
                name: "Null-data transaction",
                mtx: new_mtx(
                    Transaction::new(
                        TX_VERSION,
                        vec![dummy_tx_input],
                        vec![TransactionOutput::new(
                            SOMPI_PER_KASPA,
                            ScriptPublicKey::new(
                                MAX_SCRIPT_PUBLIC_KEY_VERSION,
                                ScriptBuilder::new().add_op(OpReturn).unwrap().script().into(),
                            ),
                        )],
                        0,
                        SUBNETWORK_ID_NATIVE,
                        0,
                        vec![],
                    ),
                    1000,
                ),
                is_standard: false,
            },
        ];

        for test in tests {
            for net in NetworkType::iter() {
                let params: Params = net.into();
                let config =
                    Config::build_default(params.target_time_per_block(), false, params.block_mass_limits, params.block_lane_limits);
                let counters = Arc::new(MiningCounters::default());
                let mempool = Mempool::new(Arc::new(config), counters);

                // Ensure standard-ness is as expected.
                println!("test_check_transaction_standard_in_isolation test '{}' ", test.name);
                let res = mempool.check_transaction_standard_in_isolation(&test.mtx);
                if res.is_ok() && test.is_standard {
                    // Test passes since function returned standard for a
                    // transaction which is intended to be standard.
                    continue;
                }
                if res.is_ok() && !test.is_standard {
                    println!("test_check_transaction_standard_in_isolation ({}): standard when it should not be", test.name);
                }
                if res.is_err() && test.is_standard {
                    println!(
                        "test_check_transaction_standard_in_isolation ({}): nonstandard when it should not be: {:?}",
                        test.name, res
                    );
                }
                assert_eq!(res.is_ok(), test.is_standard, "failed for test '{}': {:?}", test.name, res);
            }
        }
    }

    #[test]
    fn test_check_transaction_standard_in_context() {
        let addr = Address::new(Prefix::Testnet, Version::PubKey, &[1u8; 32]);
        let standard_script_public_key = kaspa_txscript::pay_to_address_script(&addr);
        let non_standard_script_public_key =
            ScriptPublicKey::new(MAX_SCRIPT_PUBLIC_KEY_VERSION, ScriptBuilder::new().add_op(OpTrue).unwrap().script().into());

        enum Expected {
            Standard,
            RejectInputScriptClass,
            RejectInsufficientComputeFee { fee: u64, minimum_fee: u64, compute_mass: u64 },
            RejectInsufficientTransientFee { fee: u64, minimum_fee: u64, normalized_transient_mass: u64 },
        }

        struct Test {
            name: &'static str,
            mtx: MutableTransaction,
            expected: Expected,
        }

        fn new_mtx(script_public_key: ScriptPublicKey, masses: NonContextualMasses, fee: u64) -> MutableTransaction {
            let prev_out = TransactionOutpoint::new(kaspa_hashes::Hash::from_u64_word(1), 1);
            let input = TransactionInput::new(prev_out, vec![], MAX_TX_IN_SEQUENCE_NUM, 1);
            let tx = Transaction::new(
                TX_VERSION,
                vec![input],
                vec![TransactionOutput::new(SOMPI_PER_KASPA, script_public_key.clone())],
                0,
                SUBNETWORK_ID_NATIVE,
                0,
                vec![],
            );
            let mut mtx = MutableTransaction::with_entries(
                tx.into(),
                vec![UtxoEntry::new(2 * SOMPI_PER_KASPA, script_public_key, 0, false, None)],
            );
            mtx.calculated_non_contextual_masses = Some(masses);
            mtx.calculated_fee = Some(fee);
            mtx
        }

        // Verifies that relay-fee pricing uses the mass-normalization cofactors correctly.
        let params: Params = NetworkType::Simnet.into();
        let cofactors = params.block_mass_cofactors();
        let transient = |bytes| bytes * TRANSIENT_BYTE_TO_MASS_FACTOR;
        let normalized_transient = |bytes| NonContextualMasses::new(0, transient(bytes)).normalized_transient(&cofactors);

        let bytes = 5_000;
        let compute = normalized_transient(bytes);
        let boundary_fee = default_minimum_relay_fee_for_mass(compute);
        let insufficient_fee = boundary_fee - 1;

        let tests = vec![
            Test {
                name: "standard input with exactly sufficient relay fee",
                mtx: new_mtx(standard_script_public_key.clone(), NonContextualMasses::new(compute, transient(bytes)), boundary_fee),
                expected: Expected::Standard,
            },
            Test {
                name: "non-standard input script class",
                mtx: new_mtx(
                    non_standard_script_public_key,
                    NonContextualMasses::new(1_000, 1_000),
                    DEFAULT_MINIMUM_RELAY_TRANSACTION_FEE,
                ),
                expected: Expected::RejectInputScriptClass,
            },
            Test {
                name: "compute mass triggers insufficient relay fee",
                mtx: new_mtx(
                    standard_script_public_key.clone(),
                    NonContextualMasses::new(compute, transient(bytes - 1)),
                    insufficient_fee,
                ),
                expected: Expected::RejectInsufficientComputeFee {
                    fee: insufficient_fee,
                    minimum_fee: boundary_fee,
                    compute_mass: compute,
                },
            },
            Test {
                name: "transient mass triggers insufficient relay fee",
                mtx: new_mtx(standard_script_public_key, NonContextualMasses::new(compute - 1, transient(bytes)), insufficient_fee),
                expected: Expected::RejectInsufficientTransientFee {
                    fee: insufficient_fee,
                    minimum_fee: boundary_fee,
                    normalized_transient_mass: compute,
                },
            },
        ];

        let config = Config::build_default(params.target_time_per_block(), false, params.block_mass_limits, params.block_lane_limits);
        let counters = Arc::new(MiningCounters::default());
        let mempool = Mempool::new(Arc::new(config), counters);

        for test in tests {
            let res = mempool.check_transaction_standard_in_context(&test.mtx);
            match test.expected {
                Expected::Standard => assert_eq!(res, Ok(()), "failed for test '{}'", test.name),
                Expected::RejectInputScriptClass => {
                    assert_eq!(res, Err(NonStandardError::RejectInputScriptClass(test.mtx.id(), 0)), "failed for test '{}'", test.name)
                }
                Expected::RejectInsufficientComputeFee { fee, minimum_fee, compute_mass } => assert_eq!(
                    res,
                    Err(NonStandardError::RejectInsufficientComputeFee(test.mtx.id(), fee, minimum_fee, compute_mass)),
                    "failed for test '{}'",
                    test.name
                ),
                Expected::RejectInsufficientTransientFee { fee, minimum_fee, normalized_transient_mass } => assert_eq!(
                    res,
                    Err(NonStandardError::RejectInsufficientTransientFee(test.mtx.id(), fee, minimum_fee, normalized_transient_mass)),
                    "failed for test '{}'",
                    test.name
                ),
            }
        }
    }
}