sn_client 0.110.4

Safe Network Client
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
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
// Copyright 2024 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. Please review the Licences for the specific language governing
// permissions and limitations relating to use of the SAFE Network Software.

mod setup;

use std::collections::BTreeSet;

use setup::MockNetwork;

use eyre::Result;
use sn_transfers::SpendAddress;

use crate::{SpendDag, SpendFault};

#[test]
fn test_spend_dag_verify_valid_simple() -> Result<()> {
    let mut net = MockNetwork::genesis()?;
    let genesis = net.genesis_spend;

    let owner1 = net.new_pk_with_balance(100)?;
    let owner2 = net.new_pk_with_balance(0)?;
    let owner3 = net.new_pk_with_balance(0)?;
    let owner4 = net.new_pk_with_balance(0)?;
    let owner5 = net.new_pk_with_balance(0)?;
    let owner6 = net.new_pk_with_balance(0)?;

    net.send(&owner1, &owner2, 100)?;
    net.send(&owner2, &owner3, 100)?;
    net.send(&owner3, &owner4, 100)?;
    net.send(&owner4, &owner5, 100)?;
    net.send(&owner5, &owner6, 100)?;

    let mut dag = SpendDag::new(genesis);
    for spend in net.spends {
        dag.insert(spend.address(), spend.clone());
    }
    assert_eq!(dag.record_faults(&genesis), Ok(()));
    // dag.dump_to_file("/tmp/test_spend_dag_verify_valid_simple")?;

    assert_eq!(dag.verify(&genesis), Ok(BTreeSet::new()));
    Ok(())
}

#[test]
fn test_spend_dag_double_spend_poisonning() -> Result<()> {
    let mut net = MockNetwork::genesis()?;
    let genesis = net.genesis_spend;

    let owner1 = net.new_pk_with_balance(100)?;
    let owner2 = net.new_pk_with_balance(0)?;
    let owner3 = net.new_pk_with_balance(0)?;
    let owner4 = net.new_pk_with_balance(0)?;
    let owner5 = net.new_pk_with_balance(0)?;
    let owner6 = net.new_pk_with_balance(0)?;
    let owner_cheat = net.new_pk_with_balance(0)?;

    // spend normaly and save a cashnote to reuse later
    net.send(&owner1, &owner2, 100)?;
    let cn_to_reuse_later = net
        .wallets
        .get(&owner2)
        .expect("owner2 wallet to exist")
        .cn
        .clone();
    let spend1 = net.send(&owner2, &owner3, 100)?;
    let spend_ko3 = net.send(&owner3, &owner4, 100)?;
    let spend_ok4 = net.send(&owner4, &owner5, 100)?;
    let spend_ok5 = net.send(&owner5, &owner6, 100)?;

    // reuse that cashnote to perform a double spend far back in history
    net.wallets
        .get_mut(&owner2)
        .expect("owner2 wallet to still exist")
        .cn = cn_to_reuse_later;
    let spend2 = net.send(&owner2, &owner_cheat, 100)?;

    // create dag
    let mut dag = SpendDag::new(genesis);
    for spend in net.spends {
        dag.insert(spend.address(), spend.clone());
    }
    assert_eq!(dag.record_faults(&genesis), Ok(()));
    // dag.dump_to_file("/tmp/test_spend_dag_double_spend_poisonning")?;

    // make sure double spend is detected
    assert_eq!(spend1, spend2, "both spends should be at the same address");
    let double_spent = spend1.first().expect("spend1 to have an element");
    let got = dag.get_spend_faults(double_spent);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpend(*double_spent)]);
    assert_eq!(got, expected, "DAG should have detected double spend");

    // make sure the double spend's direct descendants are unspendable
    let upk = net
        .wallets
        .get(&owner_cheat)
        .expect("owner_cheat wallet to exist")
        .cn
        .first()
        .expect("owner_cheat wallet to have 1 cashnote")
        .unique_pubkey();
    let utxo = SpendAddress::from_unique_pubkey(&upk);
    let got = dag.get_spend_faults(&utxo);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpentAncestor {
        addr: utxo,
        ancestor: *double_spent,
    }]);
    assert_eq!(got, expected, "UTXO of double spend should be unspendable");
    let s3 = spend_ko3.first().expect("spend_ko3 to have an element");
    let got = dag.get_spend_faults(s3);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpentAncestor {
        addr: *s3,
        ancestor: *double_spent,
    }]);
    assert_eq!(got, expected, "spend_ko3 should be unspendable");

    // make sure this didn't poison the rest of the DAG
    let s4 = spend_ok4.first().expect("spend_ok4 to be unique");
    let s5 = spend_ok5.first().expect("spend_ok5 to be unique");
    let unaffected = BTreeSet::new();

    assert_eq!(dag.get_spend_faults(s4), unaffected);
    assert_eq!(dag.get_spend_faults(s5), unaffected);
    Ok(())
}

#[test]
fn test_spend_dag_double_spend_branches() -> Result<()> {
    let mut net = MockNetwork::genesis()?;
    let genesis = net.genesis_spend;

    let owner1 = net.new_pk_with_balance(100)?;
    let owner2 = net.new_pk_with_balance(0)?;
    let owner3 = net.new_pk_with_balance(0)?;
    let owner4 = net.new_pk_with_balance(0)?;
    let owner5 = net.new_pk_with_balance(0)?;
    let owner6 = net.new_pk_with_balance(0)?;
    let owner3a = net.new_pk_with_balance(0)?;
    let owner4a = net.new_pk_with_balance(0)?;
    let owner5a = net.new_pk_with_balance(0)?;

    // spend normaly and save a cashnote to reuse later
    net.send(&owner1, &owner2, 100)?;
    let cn_to_reuse_later = net
        .wallets
        .get(&owner2)
        .expect("owner2 wallet to exist")
        .cn
        .clone();
    let spend2 = net.send(&owner2, &owner3, 100)?;
    let spend3 = net.send(&owner3, &owner4, 100)?;
    let spend4 = net.send(&owner4, &owner5, 100)?;
    let spend5 = net.send(&owner5, &owner6, 100)?;

    // reuse that cashnote to perform a double spend and create a branch
    net.wallets
        .get_mut(&owner2)
        .expect("owner2 wallet to still exist")
        .cn = cn_to_reuse_later;
    let spend2a = net.send(&owner2, &owner3a, 100)?;
    let spend3a = net.send(&owner3a, &owner4a, 100)?;
    let spend4a = net.send(&owner4a, &owner5a, 100)?;

    // create dag
    let mut dag = SpendDag::new(genesis);
    for spend in net.spends {
        println!("Adding into dag with spend {spend:?}");
        dag.insert(spend.address(), spend.clone());
    }

    assert_eq!(dag.record_faults(&genesis), Ok(()));
    // dag.dump_to_file("/tmp/test_spend_dag_double_spend_branches")?;

    // make sure double spend is detected
    assert_eq!(spend2, spend2a, "both spends should be at the same address");
    let double_spent = spend2.first().expect("spend1 to have an element");
    let got = dag.get_spend_faults(double_spent);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpend(*double_spent)]);
    assert_eq!(got, expected, "DAG should have detected double spend");

    // make sure the double spend's direct descendants are marked as double spent
    let s3 = spend3.first().expect("spend3 to have an element");
    let got = dag.get_spend_faults(s3);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpentAncestor {
        addr: *s3,
        ancestor: *double_spent,
    }]);
    assert_eq!(got, expected, "spend3 should be unspendable");
    let s3a = spend3a.first().expect("spend3a to have an element");
    let got = dag.get_spend_faults(s3a);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpentAncestor {
        addr: *s3a,
        ancestor: *double_spent,
    }]);
    assert_eq!(got, expected, "spend3a should be unspendable");

    // make sure all the descendants further down the branch are poisoned due to a double spent ancestor
    let utxo_of_5a = SpendAddress::from_unique_pubkey(
        &net.wallets
            .get(&owner5a)
            .expect("owner5a wallet to exist")
            .cn
            .first()
            .expect("owner5a wallet to have 1 cashnote")
            .unique_pubkey(),
    );
    let utxo_of_6 = SpendAddress::from_unique_pubkey(
        &net.wallets
            .get(&owner6)
            .expect("owner6 wallet to exist")
            .cn
            .first()
            .expect("owner6 wallet to have 1 cashnote")
            .unique_pubkey(),
    );
    let all_descendants = [spend4, spend5, vec![utxo_of_6], spend4a, vec![utxo_of_5a]];
    for d in all_descendants.iter() {
        let got = dag.get_spend_faults(d.first().expect("descendant spend to have an element"));
        let expected = BTreeSet::from_iter([SpendFault::PoisonedAncestry(
            *d.first().expect("d to have an element"),
            format!(
                "spend is on one of multiple branches of a double spent ancestor: {double_spent:?}"
            ),
        )]);
        assert_eq!(got, expected, "all descendants should be marked as bad");
    }
    Ok(())
}

#[test]
fn test_spend_dag_double_spend_detection() -> Result<()> {
    let mut net = MockNetwork::genesis()?;
    let genesis = net.genesis_spend;

    let owner1 = net.new_pk_with_balance(100)?;
    let owner2a = net.new_pk_with_balance(0)?;
    let owner2b = net.new_pk_with_balance(0)?;

    // perform double spend
    let cn_to_reuse = net
        .wallets
        .get(&owner1)
        .expect("owner1 wallet to exist")
        .cn
        .clone();
    let spend1_addr = net.send(&owner1, &owner2a, 100)?;
    net.wallets
        .get_mut(&owner1)
        .expect("owner1 wallet to still exist")
        .cn = cn_to_reuse;
    let spend2_addr = net.send(&owner1, &owner2b, 100)?;

    // get the UTXOs of the two spends
    let upk_of_2a = net
        .wallets
        .get(&owner2a)
        .expect("owner2a wallet to exist")
        .cn
        .first()
        .expect("owner2a wallet to have 1 cashnote")
        .unique_pubkey();
    let utxo_of_2a = SpendAddress::from_unique_pubkey(&upk_of_2a);
    let upk_of_2b = net
        .wallets
        .get(&owner2b)
        .expect("owner2b wallet to exist")
        .cn
        .first()
        .expect("owner2b wallet to have 1 cashnote")
        .unique_pubkey();
    let utxo_of_2b = SpendAddress::from_unique_pubkey(&upk_of_2b);

    // make DAG
    let mut dag = SpendDag::new(genesis);
    for spend in net.spends {
        dag.insert(spend.address(), spend.clone());
    }
    dag.record_faults(&genesis)?;
    // dag.dump_to_file("/tmp/test_spend_dag_double_spend_detection")?;

    // make sure the double spend is detected
    assert_eq!(
        spend1_addr, spend2_addr,
        "both spends should be at the same address"
    );
    assert_eq!(spend1_addr.len(), 1, "there should only be one spend");
    let double_spent = spend1_addr.first().expect("spend1_addr to have an element");
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpend(*double_spent)]);
    assert_eq!(
        dag.get_spend_faults(double_spent),
        expected,
        "DAG should have detected double spend"
    );

    // make sure the UTXOs of the double spend are unspendable
    let got = dag.get_spend_faults(&utxo_of_2a);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpentAncestor {
        addr: utxo_of_2a,
        ancestor: *double_spent,
    }]);
    assert_eq!(
        got, expected,
        "UTXO a of double spend should be unspendable"
    );

    let got = dag.get_spend_faults(&utxo_of_2b);
    let expected = BTreeSet::from_iter([SpendFault::DoubleSpentAncestor {
        addr: utxo_of_2b,
        ancestor: *double_spent,
    }]);
    assert_eq!(
        got, expected,
        "UTXO b of double spend should be unspendable"
    );
    Ok(())
}

#[test]
fn test_spend_dag_missing_ancestry() -> Result<()> {
    let mut net = MockNetwork::genesis()?;
    let genesis = net.genesis_spend;

    let owner1 = net.new_pk_with_balance(100)?;
    let owner2 = net.new_pk_with_balance(0)?;
    let owner3 = net.new_pk_with_balance(0)?;
    let owner4 = net.new_pk_with_balance(0)?;
    let owner5 = net.new_pk_with_balance(0)?;
    let owner6 = net.new_pk_with_balance(0)?;

    net.send(&owner1, &owner2, 100)?;
    net.send(&owner2, &owner3, 100)?;
    let spend_missing = net
        .send(&owner3, &owner4, 100)?
        .first()
        .expect("spend_missing should have 1 element")
        .to_owned();
    let spent_after1 = net
        .send(&owner4, &owner5, 100)?
        .first()
        .expect("spent_after1 should have 1 element")
        .to_owned();
    let spent_after2 = net
        .send(&owner5, &owner6, 100)?
        .first()
        .expect("spent_after2 should have 1 element")
        .to_owned();
    let utxo_after3 = net
        .wallets
        .get(&owner6)
        .expect("owner6 wallet to exist")
        .cn
        .first()
        .expect("owner6 wallet to have 1 cashnote")
        .unique_pubkey();
    let utxo_addr = SpendAddress::from_unique_pubkey(&utxo_after3);

    // create dag with one missing spend
    let net_spends = net
        .spends
        .into_iter()
        .filter(|s| spend_missing != s.address());
    let mut dag = SpendDag::new(genesis);
    for spend in net_spends {
        dag.insert(spend.address(), spend.clone());
    }
    dag.record_faults(&genesis)?;
    // dag.dump_to_file("/tmp/test_spend_dag_missing_ancestry")?;

    // make sure the missing spend makes its descendants invalid
    let got = dag.get_spend_faults(&spent_after1);
    let expected = BTreeSet::from_iter([SpendFault::MissingAncestry {
        addr: spent_after1,
        ancestor: spend_missing,
    }]);
    assert_eq!(got, expected, "DAG should have detected missing ancestry");

    let got = dag.get_spend_faults(&spent_after2);
    let expected = BTreeSet::from_iter([SpendFault::PoisonedAncestry(
        spent_after2,
        format!("missing ancestor at: {spend_missing:?}"),
    )]);
    assert_eq!(
        got, expected,
        "DAG should have propagated the error to descendants"
    );

    let got = dag.get_spend_faults(&utxo_addr);
    let expected = BTreeSet::from_iter([SpendFault::PoisonedAncestry(
        utxo_addr,
        format!("missing ancestor at: {spend_missing:?}"),
    )]);
    assert_eq!(
        got, expected,
        "DAG should have propagated the error all the way to descendant utxos"
    );
    Ok(())
}

#[test]
fn test_spend_dag_orphans() -> Result<()> {
    let mut net = MockNetwork::genesis()?;
    let genesis = net.genesis_spend;

    let owner1 = net.new_pk_with_balance(100)?;
    let owner2 = net.new_pk_with_balance(0)?;
    let owner3 = net.new_pk_with_balance(0)?;
    let owner4 = net.new_pk_with_balance(0)?;
    let owner5 = net.new_pk_with_balance(0)?;
    let owner6 = net.new_pk_with_balance(0)?;

    net.send(&owner1, &owner2, 100)?;
    net.send(&owner2, &owner3, 100)?;
    let spend_missing1 = net
        .send(&owner3, &owner4, 100)?
        .first()
        .expect("spend_missing should have 1 element")
        .to_owned();
    let spend_missing2 = net
        .send(&owner4, &owner5, 100)?
        .first()
        .expect("spend_missing2 should have 1 element")
        .to_owned();
    let spent_after1 = net
        .send(&owner5, &owner6, 100)?
        .first()
        .expect("spent_after1 should have 1 element")
        .to_owned();
    let utxo_after2 = net
        .wallets
        .get(&owner6)
        .expect("owner6 wallet to exist")
        .cn
        .first()
        .expect("owner6 wallet to have 1 cashnote")
        .unique_pubkey();
    let utxo_addr = SpendAddress::from_unique_pubkey(&utxo_after2);

    // create dag with two missing spends in the chain
    let net_spends = net
        .spends
        .into_iter()
        .filter(|s| spend_missing1 != s.address() && spend_missing2 != s.address());
    let mut dag = SpendDag::new(genesis);
    for spend in net_spends {
        dag.insert(spend.address(), spend.clone());
    }
    dag.record_faults(&genesis)?;
    // dag.dump_to_file("/tmp/test_spend_dag_orphans")?;

    // make sure the spends after the two missing spends are orphans
    let got = dag.get_spend_faults(&spent_after1);
    let expected = BTreeSet::from_iter([
        SpendFault::OrphanSpend {
            addr: spent_after1,
            src: dag.source(),
        },
        SpendFault::MissingAncestry {
            addr: spent_after1,
            ancestor: spend_missing2,
        },
    ]);
    assert_eq!(got, expected, "DAG should have detected orphan spend");

    let got = dag.get_spend_faults(&utxo_addr);
    let expected = SpendFault::OrphanSpend {
        addr: utxo_addr,
        src: dag.source(),
    };
    assert!(
        got.contains(&expected),
        "Utxo of orphan spend should also be an orphan"
    );
    Ok(())
}