chia-query 0.14.0

Query the Chia blockchain via decentralized peers with coinset.org fallback
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
//! What the peer tier will and will not call an absence.
//!
//! These exercise the tier the suite has never reached. Every absence fixture elsewhere in the
//! ecosystem is built with `max_peers: 0`, so the peer road is never instantiated and the rule
//! under test here could be deleted without turning anything red (dig_ecosystem#2456).
//!
//! The peers are REAL — a `Peer` reads its address off a live socket and cannot be mocked — and
//! each one is admitted under its own [`Peer::socket_addr`], which is what lets a scripted read
//! give different peers different things to say.

use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;

use super::connect::PeerOrigin;
use super::pool::PeerPool;
use super::test_support::loopback_peer;
use super::{OptAnswer, PeerBackend};
use crate::types::{ChainClaim, ChiaQueryError};
use crate::NetworkType;

/// The scripted answers are strings, so a string's own content is the claim it makes.
impl ChainClaim for &'static str {
    fn chain_claim(&self) -> String {
        (*self).to_string()
    }
}

/// What a scripted peer says when asked.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Says {
    /// A successful response carrying nothing — the empty coin-state list the defect trusted.
    Absent,
    /// A successful response carrying the thing.
    Present,
    /// A successful response carrying a DIFFERENT thing — the same read, a different claim about
    /// the chain. This is what a fabricated height looks like from here.
    PresentOther,
    /// The read itself failed.
    Fails,
}

/// A backend over exactly the peers given, in the order given, dialling nothing.
///
/// `max_peers` is set to the number admitted so `try_refill` is a no-op: a test that reaches the
/// network is not a unit test, and a refill would also change the pool mid-read.
async fn backend_over(members: &[(Says, PeerOrigin)]) -> (PeerBackend, HashMap<SocketAddr, Says>) {
    let pool = PeerPool::for_tests(members.len());
    let mut script = HashMap::new();

    for (says, origin) in members {
        let peer = loopback_peer().await;
        let addr = peer.socket_addr();
        assert!(
            pool.admit_for_tests(peer, addr, *origin).await,
            "each scripted peer must be admitted under its own address"
        );
        script.insert(addr, *says);
    }

    let backend = PeerBackend {
        pool,
        network: NetworkType::Mainnet,
        request_timeout: Duration::from_millis(50),
    };
    (backend, script)
}

/// Run the corroborated read against `script`, reporting how many peers were actually asked.
async fn read_scripted(
    backend: &PeerBackend,
    script: &HashMap<SocketAddr, Says>,
) -> (Result<OptAnswer<&'static str>, ChiaQueryError>, usize) {
    let asked = Arc::new(AtomicUsize::new(0));
    let counter = Arc::clone(&asked);

    let result = backend
        .read_opt_corroborated(move |peer| {
            let counter = Arc::clone(&counter);
            let says = *script
                .get(&peer.socket_addr())
                .expect("every peer in the pool is scripted");
            async move {
                counter.fetch_add(1, Ordering::SeqCst);
                match says {
                    Says::Absent => Ok(None),
                    Says::Present => Ok(Some("the thing")),
                    Says::PresentOther => Ok(Some("a different thing")),
                    Says::Fails => Err(ChiaQueryError::PeerConnection("scripted failure".into())),
                }
            }
        })
        .await;

    (result, asked.load(Ordering::SeqCst))
}

/// **The defect, stated as a test.**
///
/// One discovered peer answers successfully and empty, and no second answer arrives because there
/// is no second peer. The caller must NOT be handed an absence. Before this change the identical
/// fixture produced `Ok(None)` — indistinguishable, to every consumer, from a chain that was
/// consulted and provably lacks the thing.
#[tokio::test]
async fn one_peer_saying_absent_is_not_an_absence() {
    let (backend, script) = backend_over(&[(Says::Absent, PeerOrigin::Discovered)]).await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("a lone absent answer is not an ERROR, it is an ungraded fact"),
        OptAnswer::UncorroboratedAbsent,
        "absence on one peer's word must be reported as uncorroborated"
    );
    assert_eq!(asked, 1, "there was only one peer to ask");
}

/// A second peer that is not an INDEPENDENT peer cannot corroborate.
///
/// The fixture varies exactly one thing against the passing case below: the second peer's origin.
/// It is honest and it agrees — so if this returned corroborated absence, the reason would be that
/// a host-local node was counted as a witness to the chain, which is the failure
/// [`PeerPool::independent_peer_count`] exists to prevent. Nothing else in the fixture differs, so
/// nothing else can explain a green.
#[tokio::test]
async fn a_preferred_peer_agreeing_is_not_corroboration() {
    let (backend, script) = backend_over(&[
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Absent, PeerOrigin::Priority),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("no read failed"),
        OptAnswer::UncorroboratedAbsent,
        "a preferred peer is not an independent voice, however honestly it agrees"
    );
    assert_eq!(asked, 1, "the preferred peer must not even be consulted");
}

/// The control: a FLOOR's worth of independent peers, all absent, IS an absence.
///
/// Without this the three tests above would all pass on a backend that simply never reported
/// absence at all.
///
/// Three peers, not two: the answering peer cannot corroborate itself, so `CORROBORATION_FLOOR`
/// (2) agreeing voices need two peers BESIDES it. A two-peer fixture is the at-bound-minus-one
/// case and is covered separately by
/// [`agreement_below_the_floor_is_not_an_absence`].
#[tokio::test]
async fn a_floor_of_independent_peers_agreeing_is_an_absence() {
    let (backend, script) = backend_over(&[
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Absent, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("no read failed"),
        OptAnswer::CorroboratedAbsent
    );
    assert_eq!(
        asked, 3,
        "corroboration means every corroborator was really asked"
    );
}

/// **The floor from BELOW: one agreeing voice is not an absence.**
///
/// The pool is ARMED — two independent peers besides the one that answered — so this cannot pass
/// by accident on a membership check. One of them fails, leaving ONE agreeing voice, and one is
/// exactly the effective floor this crate shipped while declaring two.
///
/// Paired with the at-bound test above, this pins the boundary from both sides: two agreeing
/// voices corroborate, one does not.
#[tokio::test]
async fn agreement_below_the_floor_is_not_an_absence() {
    let (backend, script) = backend_over(&[
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Fails, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("the first peer answered, so the read did not fail"),
        OptAnswer::UncorroboratedAbsent,
        "one agreeing voice is a second opinion, not corroboration"
    );
    assert_eq!(asked, 3, "both corroborators were asked; only one answered");
}

/// A contradiction is surfaced, never resolved.
///
/// One peer says absent and the other produces the thing. Neither answer is preferred — there is
/// nothing in them to prefer on — so the read fails with the disagreement rather than picking a
/// winner in either direction.
#[tokio::test]
async fn a_contradicting_peer_is_refused_not_broken_in_either_direction() {
    let (backend, script) = backend_over(&[
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Present, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, _) = read_scripted(&backend, &script).await;

    assert!(
        matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
        "a contradiction is evidence about the sources, not a tie to break: got {answer:?}"
    );
}

/// A corroborator that cannot answer corroborates nothing.
///
/// The distinction this pins: a FAILED second opinion must not read as a confirmed first one. The
/// nearest wrong implementation treats "I asked and got no contradiction" as agreement, and that
/// implementation passes every other test in this file.
#[tokio::test]
async fn a_corroborator_that_fails_leaves_the_absence_uncorroborated() {
    let (backend, script) = backend_over(&[
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Fails, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("the FIRST peer answered, so the read did not fail"),
        OptAnswer::UncorroboratedAbsent,
        "silence from the second peer is not agreement"
    );
    assert_eq!(asked, 2, "the corroborator was asked and failed");
}

// ---------------------------------------------------------------------------
// Presence — dig_ecosystem#2462
//
// The absence tests above exist because an empty answer carries no proof. These exist because a
// POSITIVE answer carries less proof than it looks like it does: the coin-id binding covers
// `parent_coin_info ‖ puzzle_hash ‖ amount` and NOT `created_height` / `spent_height`, which are
// the only reason the read is made. A peer that returns a genuine coin's fields with a fabricated
// height passes every check the record can perform on itself.
// ---------------------------------------------------------------------------

/// **The defect, stated as a test.**
///
/// One independent peer produces the thing and there is nobody else to ask. The caller must not be
/// handed a corroborated presence. Before this change the identical fixture produced
/// `OptAnswer::Found` — indistinguishable, to every consumer, from heights two sources agreed on.
#[tokio::test]
async fn one_peer_saying_present_is_not_a_corroborated_presence() {
    let (backend, script) = backend_over(&[(Says::Present, PeerOrigin::Discovered)]).await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("a lone positive answer is not an ERROR, it is an ungraded fact"),
        OptAnswer::UncorroboratedFound("the thing"),
        "presence on one peer's word must be reported as uncorroborated"
    );
    assert_eq!(asked, 1, "there was only one peer to ask");
}

/// The control: a FLOOR's worth of independent peers making the SAME claim corroborates it.
///
/// Without this, every presence test here would pass on a backend that had simply stopped
/// reporting corroborated presence at all.
#[tokio::test]
async fn a_floor_of_independent_peers_agreeing_makes_the_presence_corroborated() {
    let (backend, script) = backend_over(&[
        (Says::Present, PeerOrigin::Discovered),
        (Says::Present, PeerOrigin::Discovered),
        (Says::Present, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("no read failed"),
        OptAnswer::Found("the thing")
    );
    assert_eq!(
        asked, 3,
        "corroboration means every corroborator was really asked"
    );
}

/// **The floor from BELOW, on the presence side: one agreeing voice is not corroboration.**
///
/// The pool is ARMED and one corroborator fails, so exactly one voice agrees. This is the read
/// that previously returned `Found` — the declared floor of two satisfied by one — and it is the
/// money-lie this wiring closes: a `Found` is what a consumer records a height from.
#[tokio::test]
async fn agreement_below_the_floor_is_not_a_corroborated_presence() {
    let (backend, script) = backend_over(&[
        (Says::Present, PeerOrigin::Discovered),
        (Says::Present, PeerOrigin::Discovered),
        (Says::Fails, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("the first peer answered, so the read did not fail"),
        OptAnswer::UncorroboratedFound("the thing"),
        "one agreeing voice is a second opinion, not corroboration"
    );
    assert_eq!(asked, 3, "both corroborators were asked; only one answered");
}

/// Agreement is on the CLAIM, not on the fact that something came back.
///
/// Both peers produce a record; they describe different chain state. An implementation that merely
/// counted positive answers would call this corroborated, which is exactly the fabricated-height
/// attack succeeding with two peers instead of one.
#[tokio::test]
async fn corroborators_that_claim_different_chain_state_disagree() {
    let (backend, script) = backend_over(&[
        (Says::Present, PeerOrigin::Discovered),
        (Says::PresentOther, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, _) = read_scripted(&backend, &script).await;

    assert!(
        matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
        "two different claims about the same coin is evidence, not a tie to break: got {answer:?}"
    );
}

/// **The peer that answers first does not decide.**
///
/// The first peer picked is the hostile one, and the two peers behind it agree with each other and
/// contradict it. The read must fail — not resolve to the first answer, and not resolve to the
/// majority either, because nothing in the answers says which set to believe.
///
/// The ask count is the second half of the assertion and it is what pins the round as CONCURRENT
/// rather than first-responder-wins: every corroborator is asked, so a hostile peer cannot win by
/// being fastest, and a sequential implementation that stopped at the first corroborator would
/// leave the third peer unasked.
#[tokio::test]
async fn the_first_peers_answer_does_not_decide_against_the_corroborators() {
    let (backend, script) = backend_over(&[
        (Says::PresentOther, PeerOrigin::Discovered),
        (Says::Present, PeerOrigin::Discovered),
        (Says::Present, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert!(
        matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
        "a contradicted first answer is refused, in either direction: got {answer:?}"
    );
    assert_eq!(asked, 3, "every corroborator is asked, concurrently");
}

/// A corroborator that reports the thing ABSENT contradicts the presence.
///
/// The mirror of `a_contradicting_peer_is_refused_not_broken_in_either_direction`, approached from
/// the presence side: which peer answered first must not change the outcome.
#[tokio::test]
async fn a_corroborator_reporting_absent_contradicts_the_presence() {
    let (backend, script) = backend_over(&[
        (Says::Present, PeerOrigin::Discovered),
        (Says::Absent, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, _) = read_scripted(&backend, &script).await;

    assert!(
        matches!(answer, Err(ChiaQueryError::SourcesDisagree(_))),
        "present-then-absent is the same contradiction as absent-then-present: got {answer:?}"
    );
}

/// A corroborator that cannot answer corroborates nothing.
///
/// The nearest wrong implementation reads "I asked and heard no contradiction" as agreement. It
/// passes every other presence test in this file and fails this one.
#[tokio::test]
async fn a_corroborator_that_fails_leaves_the_presence_uncorroborated() {
    let (backend, script) = backend_over(&[
        (Says::Present, PeerOrigin::Discovered),
        (Says::Fails, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("the FIRST peer answered, so the read did not fail"),
        OptAnswer::UncorroboratedFound("the thing"),
        "silence from the corroborator is not agreement"
    );
    assert_eq!(asked, 2, "the corroborator was asked and failed");
}

/// A host-local peer agreeing is not an independent voice about the chain.
///
/// Varies exactly one thing against the passing control: the second peer's origin.
#[tokio::test]
async fn a_preferred_peer_agreeing_is_not_corroboration_of_presence() {
    let (backend, script) = backend_over(&[
        (Says::Present, PeerOrigin::Discovered),
        (Says::Present, PeerOrigin::Priority),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("no read failed"),
        OptAnswer::UncorroboratedFound("the thing"),
        "a preferred peer is not an independent voice, however honestly it agrees"
    );
    assert_eq!(asked, 1, "the preferred peer must not even be consulted");
}

/// **A read from the operator's own node still corroborates against the discovered set.**
///
/// The preferred peer is admitted FIRST, so it is the one `select_peer` asks. Two genuinely
/// independent peers then agree with it, which is a floor's worth of corroboration by every rule
/// this tier states — the preferred peer is not among them and never claimed to be.
///
/// This fixture returned `UncorroboratedAbsent` before the fix, because readiness subtracted the
/// asker's slot from the independent set without knowing the asker was not in it. That is not a
/// harmless conservatism: an `Uncorroborated*` answer is settled by the router against the
/// centralized coinset tier, so the effect was to route the most common configuration this pool is
/// sized for — a host running a node, or one with `TRUSTED_FULLNODE` — away from peer plurality
/// and on to a single HTTPS source (NC-12).
///
/// Deliberately the SAME peer count and the same answers as
/// [`a_floor_of_independent_peers_agreeing_is_an_absence`]; only the first peer's origin differs,
/// so nothing else in the fixture can explain the verdict.
#[tokio::test]
async fn a_preferred_peer_answering_is_still_corroborated_by_the_discovered_set() {
    let (backend, script) = backend_over(&[
        (Says::Absent, PeerOrigin::Priority),
        (Says::Absent, PeerOrigin::Discovered),
        (Says::Absent, PeerOrigin::Discovered),
    ])
    .await;

    let (answer, asked) = read_scripted(&backend, &script).await;

    assert_eq!(
        answer.expect("no read failed"),
        OptAnswer::CorroboratedAbsent,
        "two independent peers agreeing is corroboration whoever was asked first"
    );
    assert_eq!(
        asked, 3,
        "the preferred peer answered and both independent peers were really asked"
    );
}