blvm-node 0.1.74

Bitcoin Commons BLVM: Minimal Bitcoin node implementation using blvm-protocol and blvm-consensus
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
//! Comprehensive tests for testmempoolaccept RPC method
//!
//! Tests Core compatibility including:
//! - SegWit transaction support with witness data
//! - wtxid calculation
//! - effective-includes from mempool
//! - Package validation
//! - Package-error handling

use blvm_node::node::mempool::MempoolManager;
use blvm_node::rpc::rawtx::RawTxRpc;
use blvm_node::storage::Storage;
use serde_json::json;
use std::sync::Arc;
use tempfile::TempDir;

fn create_test_rawtx_rpc() -> RawTxRpc {
    let temp_dir = TempDir::new().unwrap();
    let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
    let mempool = Arc::new(MempoolManager::new());

    RawTxRpc::with_dependencies(storage, mempool, None, None)
}

/// Test that testmempoolaccept returns all required fields matching Core format
#[tokio::test]
async fn test_testmempoolaccept_output_format() {
    let rawtx = create_test_rawtx_rpc();

    // Create a simple valid transaction (non-SegWit)
    // Version (4 bytes) + Input count (1 byte) + Output count (1 byte) + Locktime (4 bytes)
    let simple_tx_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    let params = json!([simple_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();

    // Should return array with one result
    assert!(result.is_array());
    let results = result.as_array().unwrap();
    assert_eq!(results.len(), 1);

    let tx_result = &results[0];

    // Verify all required fields are present
    assert!(tx_result.get("txid").is_some(), "Missing txid field");
    assert!(tx_result.get("wtxid").is_some(), "Missing wtxid field");
    assert!(tx_result.get("allowed").is_some(), "Missing allowed field");
    assert!(tx_result.get("vsize").is_some(), "Missing vsize field");
    assert!(tx_result.get("fees").is_some(), "Missing fees field");

    // Verify fees structure
    let fees = tx_result.get("fees").unwrap();
    assert!(fees.get("base").is_some(), "Missing fees.base field");

    // If allowed, should have effective-feerate and effective-includes
    if let Some(allowed) = tx_result.get("allowed").and_then(|v| v.as_bool()) {
        if allowed {
            assert!(
                fees.get("effective-feerate").is_some(),
                "Missing fees.effective-feerate when allowed"
            );
            assert!(
                fees.get("effective-includes").is_some(),
                "Missing fees.effective-includes when allowed"
            );
            let includes = fees.get("effective-includes").unwrap();
            assert!(includes.is_array(), "effective-includes should be array");
        } else {
            // If not allowed, should have reject-reason
            assert!(
                tx_result.get("reject-reason").is_some(),
                "Missing reject-reason when not allowed"
            );
        }
    }
}

/// Test wtxid calculation for non-SegWit transactions (wtxid == txid)
#[tokio::test]
async fn test_wtxid_non_segwit() {
    let rawtx = create_test_rawtx_rpc();

    // Simple non-SegWit transaction
    let simple_tx_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    let params = json!([simple_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();
    let tx_result = &result.as_array().unwrap()[0];

    let txid = tx_result.get("txid").unwrap().as_str().unwrap();
    let wtxid = tx_result.get("wtxid").unwrap().as_str().unwrap();

    // For non-SegWit, wtxid should equal txid
    assert_eq!(
        txid, wtxid,
        "wtxid should equal txid for non-SegWit transactions"
    );
}

/// Test SegWit transaction parsing and wtxid calculation
#[tokio::test]
async fn test_segwit_wtxid_calculation() {
    use blvm_protocol::serialization::serialize_transaction_with_witness;
    use blvm_protocol::serialization::transaction::serialize_transaction;
    use blvm_protocol::{OutPoint, Transaction, TransactionInput, TransactionOutput};

    let rawtx = create_test_rawtx_rpc();

    let mut spk = vec![0x00, 0x14];
    spk.extend_from_slice(&[0u8; 20]);
    let tx = Transaction {
        version: 2,
        inputs: blvm_protocol::tx_inputs![TransactionInput {
            prevout: OutPoint {
                hash: [0xab; 32],
                index: 0,
            },
            script_sig: vec![],
            sequence: 0xffffffff,
        }],
        outputs: blvm_protocol::tx_outputs![TransactionOutput {
            value: 1000,
            script_pubkey: spk,
        }],
        lock_time: 0,
    };
    let witnesses = vec![vec![vec![0u8; 72]]];
    let base_size = serialize_transaction(&tx).len();
    let segwit_tx_hex =
        hex::encode(serialize_transaction_with_witness(&tx, &witnesses).expect("witness count"));

    let params = json!([segwit_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();
    let tx_result = &result.as_array().unwrap()[0];

    let txid = tx_result.get("txid").unwrap().as_str().unwrap();
    let wtxid = tx_result.get("wtxid").unwrap().as_str().unwrap();

    assert!(!wtxid.is_empty(), "wtxid should not be empty");
    assert_eq!(
        wtxid.len(),
        64,
        "wtxid should be 64 hex characters (32 bytes)"
    );
    let total_size = serialize_transaction_with_witness(&tx, &witnesses)
        .expect("witness count")
        .len();
    assert_ne!(
        txid, wtxid,
        "wtxid should differ from txid when witness is present"
    );

    let vsize = tx_result.get("vsize").unwrap().as_u64().unwrap();
    assert!(
        vsize < total_size as u64,
        "SegWit vsize ({vsize}) should be below total wire size ({total_size})"
    );
    assert!(
        vsize >= base_size as u64,
        "vsize ({vsize}) should be at least base size ({base_size})"
    );
}

/// Test package validation - duplicate transactions
#[tokio::test]
async fn test_package_validation_duplicates() {
    let rawtx = create_test_rawtx_rpc();

    // Same transaction twice (should fail package validation)
    let tx_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    let params = json!([[tx_hex, tx_hex]]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();

    let results = result.as_array().unwrap();
    assert_eq!(results.len(), 2);

    // Both should have package-error
    for tx_result in results {
        assert!(
            tx_result.get("package-error").is_some(),
            "Should have package-error for duplicate transactions"
        );
        assert_eq!(
            tx_result.get("allowed").unwrap().as_bool(),
            Some(false),
            "Should not be allowed when package validation fails"
        );
    }
}

/// Test package validation - conflicting transactions
#[tokio::test]
async fn test_package_validation_conflicts() {
    let rawtx = create_test_rawtx_rpc();

    // Create two transactions spending the same output (conflict)
    // Both transactions spend output 0 of the same previous transaction
    let tx1_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";
    let tx2_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    let params = json!([[tx1_hex, tx2_hex]]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();

    let results = result.as_array().unwrap();

    // Check if package validation detected conflict (may or may not depending on tx structure)
    // At minimum, both should have results
    assert_eq!(results.len(), 2);
}

/// Test effective-includes field presence
#[tokio::test]
async fn test_effective_includes_present() {
    let rawtx = create_test_rawtx_rpc();

    let simple_tx_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    let params = json!([simple_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();
    let tx_result = &result.as_array().unwrap()[0];

    let fees = tx_result.get("fees").unwrap();
    let allowed = tx_result["allowed"].as_bool().unwrap();
    if allowed {
        let includes = fees.get("effective-includes").unwrap();
        assert!(includes.is_array());
    } else {
        assert!(tx_result.get("reject-reason").is_some());
    }
}

/// Test vsize calculation uses proper BIP141 formula
#[tokio::test]
async fn test_vsize_calculation() {
    let rawtx = create_test_rawtx_rpc();

    let simple_tx_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    let params = json!([simple_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();
    let tx_result = &result.as_array().unwrap()[0];

    let vsize = tx_result.get("vsize").unwrap().as_u64().unwrap();

    // vsize should be positive
    assert!(vsize > 0, "vsize should be positive");

    // vsize should be reasonable (not extremely large)
    assert!(vsize < 1000000, "vsize should be reasonable");
}

/// Test effective-feerate calculation
#[tokio::test]
async fn test_effective_feerate_calculation() {
    let rawtx = create_test_rawtx_rpc();

    let simple_tx_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    let params = json!([simple_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();
    let tx_result = &result.as_array().unwrap()[0];

    // If transaction is allowed, effective-feerate should be present
    if let Some(allowed) = tx_result.get("allowed").and_then(|v| v.as_bool()) {
        if allowed {
            let fees = tx_result.get("fees").unwrap();
            let feerate = fees.get("effective-feerate").unwrap().as_f64().unwrap();

            // Effective feerate should be non-negative
            assert!(feerate >= 0.0, "effective-feerate should be non-negative");
        }
    }
}

/// Test reject-reason format matches Core
#[tokio::test]
async fn test_reject_reason_format() {
    let rawtx = create_test_rawtx_rpc();

    // Invalid transaction (will be rejected)
    let invalid_tx_hex = "0000"; // Too short to be valid

    let params = json!([invalid_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await;

    // Should either return error or transaction with reject-reason
    if let Ok(result) = result {
        let results = result.as_array().unwrap();
        if !results.is_empty() {
            let tx_result = &results[0];
            if let Some(allowed) = tx_result.get("allowed").and_then(|v| v.as_bool()) {
                if !allowed {
                    assert!(
                        tx_result.get("reject-reason").is_some(),
                        "Should have reject-reason when not allowed"
                    );
                    let reason = tx_result.get("reject-reason").unwrap();
                    assert!(
                        reason.is_string() || reason.is_null(),
                        "reject-reason should be string or null"
                    );
                }
            }
        }
    }
}

/// Test that single transaction (not package) works correctly
#[tokio::test]
async fn test_single_transaction_format() {
    let rawtx = create_test_rawtx_rpc();

    let simple_tx_hex = "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff08044c86041b020602ffffffff0100f2052a010000004341041b0e8c2567c12536aa13357b79a073dc4443acf83e08e2c1252d0efcb9a4ba20b4e93f883d634390d26ed65f763194ea3273f11a6718b3615b4d94e82801b0eaac00000000";

    // Single transaction (string, not array)
    let params = json!([simple_tx_hex]);
    let result = rawtx.testmempoolaccept(&params).await.unwrap();

    // Should return array with one result
    assert!(result.is_array());
    let results = result.as_array().unwrap();
    assert_eq!(results.len(), 1);

    assert!(results[0].get("package-error").is_none());
}

#[tokio::test]
async fn test_missing_prevout_is_rejected_and_a_funded_spend_reports_its_fee() {
    use blvm_protocol::serialization::transaction::serialize_transaction;
    use blvm_protocol::{OutPoint, Transaction, TransactionInput, TransactionOutput};

    let temp_dir = TempDir::new().unwrap();
    let storage = Arc::new(Storage::new(temp_dir.path()).unwrap());
    let mempool = Arc::new(MempoolManager::new());
    let rawtx = RawTxRpc::with_dependencies(storage.clone(), mempool, None, None);

    let missing = Transaction {
        version: 1,
        inputs: blvm_protocol::tx_inputs![TransactionInput {
            prevout: OutPoint {
                hash: [0u8; 32],
                index: 0,
            },
            script_sig: vec![],
            sequence: 0xffffffff,
        }],
        outputs: blvm_protocol::tx_outputs![TransactionOutput {
            value: 1,
            script_pubkey: vec![0x51].into(),
        }],
        lock_time: 0,
    };
    let missing_hex = hex::encode(serialize_transaction(&missing));
    let rejected = rawtx
        .testmempoolaccept(&json!([[missing_hex]]))
        .await
        .unwrap();
    let row = &rejected.as_array().unwrap()[0];
    assert_eq!(row["allowed"], json!(false));
    assert_eq!(row["fees"]["base"], json!(0.0));

    let prev = [0x44u8; 32];
    storage
        .utxos()
        .add_utxo(
            &OutPoint {
                hash: prev,
                index: 0,
            },
            &blvm_node::UTXO {
                value: 50_000,
                script_pubkey: vec![0x51].into(),
                height: 1,
                is_coinbase: false,
            },
        )
        .unwrap();
    let spend = Transaction {
        version: 1,
        inputs: blvm_protocol::tx_inputs![TransactionInput {
            prevout: OutPoint {
                hash: prev,
                index: 0,
            },
            script_sig: vec![0x51],
            sequence: 0xffffffff,
        }],
        outputs: blvm_protocol::tx_outputs![TransactionOutput {
            value: 40_000,
            script_pubkey: vec![0x51].into(),
        }],
        lock_time: 0,
    };
    let spend_hex = hex::encode(serialize_transaction(&spend));
    let accepted = rawtx
        .testmempoolaccept(&json!([[spend_hex.clone()]]))
        .await
        .unwrap();
    let row = &accepted.as_array().unwrap()[0];
    assert_eq!(row["allowed"], json!(true));
    let fee = row["fees"]["base"].as_f64().unwrap();
    assert!((fee - 0.0001).abs() < 1e-12);

    let bare = RawTxRpc::new();
    assert!(bare.testmempoolaccept(&json!([[spend_hex]])).await.is_err());
}