Skip to main content

bsv_overlay_engine/
beef_limits.rs

1//! The engine's BEEF doors: a stranger's BEEF, and bytes read back from storage.
2//!
3//! The posture (the charter "a BEEF of any size", 2026-10-09): a valid BEEF is
4//! never refused for its size or its counts. A door reads the bytes through
5//! bsv-rs 0.4.2's [`BeefStream`], one element in hand, and refuses invalid
6//! bytes only; a refusal names the offset of the byte and the kind
7//! ([`Refusal`], [`Kind`]). No function in this module compares a BEEF's
8//! length or its counts against anything.
9//!
10//! The module keeps its name and its constants as NAMES. Each `*_BEEF_LIMITS`
11//! says which door a reader stands at and nothing more: the three numbers are
12//! what P0-5f refused by, kept so a caller that still reads one compiles
13//! (LOW's #585 removes its uses), and no reader here consults them.
14//!
15//! What streams and what does not. [`fold_beef`] reads any byte source
16//! ([`std::io::Read`]: a slice today, an object body at rest when the Worker
17//! has one). [`read_beef`], [`has_proof`], [`own_proof`] and [`own_bump`]
18//! fold over held bytes and keep one element at a time. [`parse_beef`] and
19//! [`transaction_from_beef`] run the same streaming door and then build the
20//! in-memory [`Beef`], because their callers merge, link, sort or
21//! re-serialize one; the caller already holds the bytes whole, so the object
22//! is a second copy of a body the layer above read whole. That is the bound
23//! the platform sets (one isolate), not a refusal of this module.
24//!
25//! The door is the frame and the elements alone (bsv-rs 0.4.2's
26//! `BeefDecoder`): the version, the varints, each BUMP's tree height (at
27//! most 64, the format's rule) and the agreement of its nodes, each
28//! transaction's fields and at least one input, no byte after the frame.
29//! Whether a root is the chain's and whether an input names an element are
30//! the SPV walk's questions (`engine.rs`), asked after the door; the walk's
31//! structure is [`structure_roots`], the same reader's.
32//!
33//! The submit shape is a subject, its unconfirmed ancestors and proven funding
34//! inputs (ts-stack@fb1b2da packages/overlays/overlay-express/src/OverlayExpress.ts
35//! 2655-2693; packages/overlays/overlay/src/Engine.ts 1711-1724).
36
37use std::io::Read;
38
39use bsv_rs::transaction::beef_stream::{display_hex, Hash32, Step, StreamError};
40use bsv_rs::transaction::{
41    verify_stream_structure, Beef, BeefDecoder, BeefStream, Element, Headers, MerklePath,
42    Transaction, Verdict,
43};
44
45pub use bsv_rs::transaction::beef_stream::StreamError as DoorError;
46pub use bsv_rs::transaction::{Kind, Reason, Refusal};
47
48/// A door's name. Until NL-6 these three numbers were refusals; they are not
49/// read by any function of this module now. bsv-rs 0.4.0's own `BeefLimits`
50/// lost `max_bytes` and refuses nothing, so the struct lives here for the
51/// callers that still name a field.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct BeefLimits {
54    pub max_txs: usize,
55    pub max_bumps: usize,
56    pub max_bytes: usize,
57}
58
59/// The Worker's former whole-request cap (decimal 10 MB). A name only: the
60/// submit route no longer answers 413 by it.
61pub const SUBMIT_BODY_MAX_BYTES: usize = 10_000_000;
62/// BRC-95 adds a four-byte marker and a 32-byte subject to a plain BEEF.
63pub const ATOMIC_HEADER_BYTES: usize = 36;
64/// The former count policy (P0-5f). A name only.
65const MAX_SUBMISSION_TXS: usize = 512;
66/// The former count policy (P0-5f). A name only.
67const MAX_SUBMISSION_BUMPS: usize = 512;
68
69/// HTTP submit.
70pub const SUBMIT_BEEF_LIMITS: BeefLimits = BeefLimits {
71    max_txs: MAX_SUBMISSION_TXS,
72    max_bumps: MAX_SUBMISSION_BUMPS,
73    max_bytes: SUBMIT_BODY_MAX_BYTES,
74};
75/// Core library submits and their carried/atomic bodies.
76pub const ENGINE_BEEF_LIMITS: BeefLimits = BeefLimits {
77    max_bytes: SUBMIT_BODY_MAX_BYTES + ATOMIC_HEADER_BYTES,
78    ..SUBMIT_BEEF_LIMITS
79};
80/// EF readers: courier-completed and subject-named submit bodies.
81pub const EF_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
82/// Bytes read back from D1 or another store.
83pub const STORED_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
84/// A peer's lookup output.
85pub const PEER_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
86/// Whole-transaction discovery hooks.
87pub const DISCOVERY_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
88/// App-layer reads and parent merges (LOW's; #585 re-wires them).
89pub const APP_BEEF_LIMITS: BeefLimits = STORED_BEEF_LIMITS;
90/// The queue's byte bound is the engine's: a body past the platform's 128 KB
91/// message rides by key in R2 (bsv-low #585 door 3).
92pub const QUEUE_BEEF_LIMITS: BeefLimits = BeefLimits {
93    max_bytes: ENGINE_BEEF_LIMITS.max_bytes,
94    ..SUBMIT_BEEF_LIMITS
95};
96/// A dead letter holds the queue's payload.
97pub const DEAD_LETTER_BEEF_LIMITS: BeefLimits = QUEUE_BEEF_LIMITS;
98/// The census. The 2 MiB was its evaluation budget (LOW's door 2); it is not
99/// a refusal here.
100pub const CENSUS_BEEF_LIMITS: BeefLimits = BeefLimits {
101    max_bytes: 2 * 1024 * 1024,
102    ..SUBMIT_BEEF_LIMITS
103};
104
105/// A COURIER's wire bound, not a BEEF refusal, and it stays: one merkle proof
106/// arrives as an 8 KiB canonical JSON hex field (two bytes for the quotes, two
107/// hex characters per decoded byte). A 64-level branch needs less than 3 KiB;
108/// this also admits a shared 120-leaf proof within the field.
109pub const COURIER_PROOF_MAX_BYTES: usize = (8 * 1024 - 2) / 2;
110/// A pushed proof arrives in the same field as one fetched from a courier:
111/// the same wire bound, and it stays.
112pub const PUSH_PROOF_MAX_BYTES: usize = COURIER_PROOF_MAX_BYTES;
113/// A name only since NL-6: a proof read back from a store is read whatever
114/// its length ([`proof_from_hex`]).
115pub const STORED_PROOF_MAX_BYTES: usize = COURIER_PROOF_MAX_BYTES;
116/// A name only since NL-6: a GASP peer's proof field is read whatever its
117/// length ([`proof_from_hex`]).
118pub const PEER_PROOF_MAX_BYTES: usize = COURIER_PROOF_MAX_BYTES;
119
120/// What one pass of the door learned of a BEEF it accepted.
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct BeefRead {
123    /// The version word (`BEEF_V1` or `BEEF_V2`).
124    pub version: u32,
125    /// The BRC-95 subject, display hex, when the atomic prefix led.
126    pub subject: Option<String>,
127    /// The BUMPs read.
128    pub bumps: u64,
129    /// The transaction entries read, txid-only entries among them.
130    pub txs: u64,
131    /// The last transaction entry's txid on the wire, display hex.
132    pub last_txid: Option<String>,
133    /// The bytes the frame occupied.
134    pub bytes: u64,
135}
136
137/// What a pass counts as the elements go by.
138#[derive(Default)]
139struct Tally {
140    bumps: u64,
141    txs: u64,
142    last_txid: Option<[u8; 32]>,
143}
144
145impl Tally {
146    fn see(&mut self, element: &Element) {
147        match element {
148            Element::Bump(_) => self.bumps += 1,
149            Element::Tx { txid, .. } | Element::TxidOnly { txid, .. } => {
150                self.txs += 1;
151                self.last_txid = Some(*txid);
152            }
153        }
154    }
155
156    fn read(self, version: Option<u32>, subject: Option<[u8; 32]>, bytes: u64) -> BeefRead {
157        BeefRead {
158            version: version.unwrap_or_default(),
159            subject: subject.as_ref().map(display_hex),
160            bumps: self.bumps,
161            txs: self.txs,
162            last_txid: self.last_txid.as_ref().map(display_hex),
163            bytes,
164        }
165    }
166}
167
168/// The door over any byte source: every element is read once, handed to
169/// `visit` and dropped, so the reader holds one 16 KiB chunk of the source
170/// and one element whatever the BEEF's length. `Err(Refused)` names the
171/// offset and the kind of the invalid bytes; `Err(Io)` is the source's fault
172/// and says nothing about the bytes.
173pub fn fold_beef<R: Read>(
174    source: R,
175    mut visit: impl FnMut(Element),
176) -> Result<BeefRead, StreamError> {
177    let mut stream = BeefStream::new(source);
178    let mut tally = Tally::default();
179    while let Some(element) = stream.next_element()? {
180        tally.see(&element);
181        visit(element);
182    }
183    Ok(tally.read(stream.version(), stream.subject(), stream.offset()))
184}
185
186/// [`fold_beef`] over held bytes: the same decoder fed the one slice, so no
187/// chunk is copied and no source can fail. The only error is a refusal.
188pub fn fold_held(bytes: &[u8], mut visit: impl FnMut(Element)) -> Result<BeefRead, Refusal> {
189    let mut decoder = BeefDecoder::new();
190    let mut tally = Tally::default();
191    let mut input = bytes;
192    loop {
193        match decoder.next(&mut input)? {
194            Step::Element(element) => {
195                tally.see(&element);
196                visit(element);
197            }
198            Step::NeedMore => {
199                decoder.finish()?;
200                break;
201            }
202            Step::Done => break,
203        }
204    }
205    Ok(tally.read(decoder.version(), decoder.subject(), decoder.offset()))
206}
207
208/// The door over held bytes: refuses invalid bytes only, with the offset and
209/// the kind.
210pub fn read_beef(bytes: &[u8]) -> Result<BeefRead, Refusal> {
211    fold_held(bytes, |_| {})
212}
213
214fn refused(refusal: &Refusal) -> bsv_rs::Error {
215    bsv_rs::Error::BeefError(refusal.to_string())
216}
217
218/// Carries every root: [`structure_roots`] hands the roots back and its
219/// caller asks the chain, whose tracker answers asynchronously.
220struct EveryRoot;
221
222impl Headers for EveryRoot {
223    fn carries(&self, _height: u64, _root: &Hash32) -> bool {
224        true
225    }
226}
227
228/// The BEEF's structure on the streaming reader's rules (bsv-rs 0.4.2
229/// `verify_stream_structure`, the Lean's validity but the chain): one pass,
230/// one element in hand, each BUMP's root walked once, linear in its leaves;
231/// each input of an unproven transaction resolved against the elements
232/// before it (the wire's order); each txid-only entry proven by a BUMP of
233/// this BEEF; an atomic BEEF held to its subject. The answer is each BUMP's
234/// block height and root (display hex), in BUMP order, for the caller to ask
235/// of the chain, or the refusal with the offset and the kind.
236pub fn structure_roots(bytes: &[u8]) -> Result<Vec<(u64, String)>, Refusal> {
237    match verify_stream_structure(bytes, EveryRoot, None) {
238        Ok(Verdict::Valid { roots, .. }) => Ok(roots
239            .iter()
240            .map(|(height, root)| (*height, display_hex(root)))
241            .collect()),
242        Ok(Verdict::Invalid { offset, reason, .. }) => Err(Refusal { offset, reason }),
243        // SDK 0.5.0 contextual checks run only with spend checks enabled;
244        // verify_stream_structure disables them (BeefIndex::structure_only).
245        Ok(Verdict::Unverifiable { .. }) => {
246            unreachable!("the structure-only reader requested mining context")
247        }
248        // The structure-only reader runs no script, and a slice never fails.
249        Ok(Verdict::SpendRefused { .. }) | Err(_) => {
250            unreachable!("the structure check of held bytes ran a script or failed to read")
251        }
252    }
253}
254
255/// The streaming door, then the in-memory [`Beef`] for a caller that builds
256/// with one. The refusal is for invalid bytes and reads
257/// `invalid BEEF at byte <offset>: <kind and its data>`. `_door` names where
258/// the reader stands; nothing of it is consulted.
259pub fn parse_beef(bytes: &[u8], _door: &BeefLimits) -> bsv_rs::Result<Beef> {
260    read_beef(bytes).map_err(|refusal| refused(&refusal))?;
261    Beef::from_binary(bytes)
262}
263
264/// Link from the door's parse, keeping the SDK's exact target selection:
265/// explicit txid, then atomic txid, then wire-last.
266pub fn transaction_from_beef(
267    bytes: &[u8],
268    txid: Option<&str>,
269    door: &BeefLimits,
270) -> bsv_rs::Result<Transaction> {
271    let parsed = parse_beef(bytes, door)?;
272    let target = txid
273        .map(str::to_string)
274        .or_else(|| parsed.atomic_txid.clone())
275        .or_else(|| parsed.txs.last().map(bsv_rs::transaction::BeefTx::txid))
276        .ok_or_else(|| bsv_rs::Error::TransactionError("No transactions in BEEF".into()))?;
277    parsed.find_atomic_transaction(&target).ok_or_else(|| {
278        bsv_rs::Error::TransactionError(format!("Transaction {target} not found in BEEF"))
279    })
280}
281
282/// The BUMP index the BEEF gives `txid`'s own entry (the last entry of that
283/// txid, as the in-memory index resolves a repeat), in one streaming pass.
284/// `Ok(None)`: no such entry, or an entry with no proof.
285fn own_bump_index(bytes: &[u8], txid: &str) -> Result<Option<u64>, Refusal> {
286    let mut found = None;
287    fold_held(bytes, |element| match element {
288        Element::Tx {
289            txid: id,
290            bump_index,
291            ..
292        } if display_hex(&id) == txid => found = Some(bump_index),
293        Element::TxidOnly { txid: id, .. } if display_hex(&id) == txid => found = Some(None),
294        _ => {}
295    })?;
296    Ok(found.flatten())
297}
298
299/// Whether the BEEF carries a proof for `txid`'s OWN entry (its bump index,
300/// never an ancestor's). Streaming: one element in hand. Invalid bytes, an
301/// absent entry and a proofless entry are all `false` (fail closed).
302pub fn has_proof(bytes: &[u8], txid: &str) -> bool {
303    matches!(own_bump_index(bytes, txid), Ok(Some(_)))
304}
305
306/// The BUMP the BEEF names for `txid`'s OWN entry, never the one
307/// `Beef::find_bump` would pick (the earliest BUMP that carries the txid).
308/// Two streaming passes over the held bytes, because the BUMPs precede the
309/// entry that names one; each pass has one element in hand and the one BUMP
310/// wanted is the only thing kept. `Err`: invalid bytes, named. `Ok(None)`: no
311/// entry, a proofless entry, or an index that names no BUMP.
312pub fn own_proof(bytes: &[u8], txid: &str) -> Result<Option<MerklePath>, Refusal> {
313    let Some(wanted) = own_bump_index(bytes, txid)? else {
314        return Ok(None);
315    };
316    let (mut seen, mut own) = (0u64, None);
317    fold_held(bytes, |element| {
318        if let Element::Bump(bump) = element {
319            if seen == wanted {
320                own = bump.to_merkle_path().ok();
321            }
322            seen += 1;
323        }
324    })?;
325    Ok(own)
326}
327
328/// [`own_proof`], invalid bytes read as no proof (fail closed).
329pub fn own_bump(bytes: &[u8], txid: &str) -> Option<MerklePath> {
330    own_proof(bytes, txid).ok().flatten()
331}
332
333/// A courier's or a push's proof field: the wire bound is checked on the hex
334/// before the decode, then the proof is parsed. The bound is the field's
335/// (see [`COURIER_PROOF_MAX_BYTES`]), not a verdict on a BEEF.
336pub fn merkle_path_from_hex(hex: &str, max_bytes: usize) -> bsv_rs::Result<MerklePath> {
337    check_size(hex.len(), max_bytes.saturating_mul(2), "merkle proof hex")?;
338    proof_from_hex(hex)
339}
340
341/// A proof read back from a store or handed by a GASP peer: read whatever its
342/// length, refused only if the bytes do not parse.
343pub fn proof_from_hex(hex: &str) -> bsv_rs::Result<MerklePath> {
344    let bytes = bsv_rs::primitives::from_hex(hex)?;
345    MerklePath::from_binary(&bytes)
346}
347
348/// Always `false` since NL-6: no door of this module refuses by a limit, so
349/// no parse error is a breach. Kept exported for the callers that still ask.
350pub fn is_limit_breach(_e: &bsv_rs::Error) -> bool {
351    false
352}
353
354/// A length check for a WIRE field or a non-BEEF body (a courier's proof hex,
355/// the ARC callback's JSON). No reader here applies it to a BEEF.
356pub fn check_size(size: usize, max_bytes: usize, kind: &str) -> bsv_rs::Result<()> {
357    if size > max_bytes {
358        return Err(bsv_rs::Error::BeefError(format!(
359            "{kind} of {size} bytes is over max_bytes {max_bytes}"
360        )));
361    }
362    Ok(())
363}
364
365#[cfg(test)]
366#[path = "../tests/support/beef_doors.rs"]
367mod door_shapes;
368
369#[cfg(test)]
370mod door_pin {
371    use super::door_shapes as shapes;
372    use super::*;
373
374    /// `0200BEEF`, one BUMP at height 800,000 whose tree-height byte is 65.
375    fn tree_height_65() -> Vec<u8> {
376        let mut bin = vec![0x02, 0x00, 0xbe, 0xef, 0x01];
377        bin.extend_from_slice(&[0xfe, 0x00, 0x35, 0x0c, 0x00]);
378        bin.push(65);
379        bin
380    }
381
382    #[test]
383    fn a_refusal_names_the_offset_and_the_kind() {
384        let refusal = read_beef(&tree_height_65()).unwrap_err();
385        assert_eq!(
386            (refusal.offset, refusal.kind()),
387            (10, Kind::TreeHeightOver64)
388        );
389        let refusal = read_beef(&[0xde, 0xad, 0xbe, 0xef]).unwrap_err();
390        assert_eq!((refusal.offset, refusal.kind()), (0, Kind::BadVersion));
391        let e = parse_beef(&tree_height_65(), &SUBMIT_BEEF_LIMITS).unwrap_err();
392        assert_eq!(
393            e.to_string()
394                .matches("invalid BEEF at byte 10: TreeHeightOver64")
395                .count(),
396            1,
397            "{e}"
398        );
399    }
400
401    #[test]
402    fn a_claimed_count_is_refused_for_the_bytes_never_as_a_count() {
403        // The prefix claims 300 BUMPs and carries none.
404        let bin = [0x02, 0x00, 0xbe, 0xef, 0xfd, 0x2c, 0x01];
405        let refusal = read_beef(&bin).unwrap_err();
406        // The stream ends where the first BUMP's height varint would start.
407        assert_eq!((refusal.offset, refusal.kind()), (7, Kind::BadVarint));
408        let e = parse_beef(&bin, &SUBMIT_BEEF_LIMITS).unwrap_err();
409        assert!(!e.to_string().contains("max_"), "{e}");
410        assert!(!is_limit_breach(&e));
411    }
412
413    /// A source that hands out at most seven bytes a read.
414    struct Drip<'a>(&'a [u8]);
415    impl Read for Drip<'_> {
416        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
417            let n = self.0.len().min(buf.len()).min(7);
418            buf[..n].copy_from_slice(&self.0[..n]);
419            self.0 = &self.0[n..];
420            Ok(n)
421        }
422    }
423
424    /// A source that fails after its bytes.
425    struct Fails<'a>(&'a [u8]);
426    impl Read for Fails<'_> {
427        fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
428            if self.0.is_empty() {
429                return Err(std::io::Error::other("the object body broke"));
430            }
431            let n = self.0.len().min(buf.len());
432            buf[..n].copy_from_slice(&self.0[..n]);
433            self.0 = &self.0[n..];
434            Ok(n)
435        }
436    }
437
438    #[test]
439    fn a_source_is_read_as_the_held_bytes_are_one_element_in_hand() {
440        for (bytes, _) in [
441            shapes::body(1),
442            shapes::transactions(2_048),
443            shapes::bumps(2_048),
444            shapes::sized_body(SUBMIT_BODY_MAX_BYTES + 1),
445        ] {
446            let held = read_beef(&bytes).unwrap();
447            assert_eq!(held.bytes, bytes.len() as u64);
448            let (mut elements, mut largest) = (0u64, 0u64);
449            let streamed = fold_beef(Drip(&bytes), |element| {
450                elements += 1;
451                largest = largest.max(element.wire_len());
452            })
453            .unwrap();
454            assert_eq!(streamed, held);
455            assert_eq!(elements, held.bumps + held.txs);
456            // What the reader held at once: one element, never the frame.
457            assert!(largest < held.bytes);
458        }
459        // A source's failure is the source's, never a refusal of the bytes.
460        let (bytes, _) = shapes::body(1);
461        let cut = &bytes[..bytes.len() - 3];
462        assert!(matches!(
463            fold_beef(Fails(cut), |_| {}),
464            Err(StreamError::Io(_))
465        ));
466        assert_eq!(read_beef(cut).unwrap_err().kind(), Kind::Truncated);
467    }
468
469    /// The streaming readers answer as the in-memory index does.
470    #[test]
471    fn the_own_proof_is_the_in_memory_index_s() {
472        let (stub_only, _) = shapes::transactions(3);
473        for (bytes, id) in [
474            shapes::body(1),
475            shapes::transactions(600),
476            shapes::bumps(600),
477        ] {
478            let beef = Beef::from_binary(&bytes).unwrap();
479            let entry = beef.find_txid(&id).unwrap();
480            let in_memory = entry.bump_index().and_then(|i| beef.bumps.get(i));
481            assert!(has_proof(&bytes, &id));
482            assert_eq!(
483                own_bump(&bytes, &id).map(|b| b.to_hex()),
484                in_memory.map(MerklePath::to_hex)
485            );
486            // A txid-only entry and an absent txid carry no proof.
487            let stub = format!("{:064x}", 1);
488            assert!(!has_proof(&stub_only, &stub));
489            assert!(matches!(own_proof(&stub_only, &stub), Ok(None)));
490            assert!(matches!(own_proof(&bytes, &"ee".repeat(32)), Ok(None)));
491        }
492        // Invalid bytes: named by `own_proof`, no proof for the other two.
493        for (bytes, offset, _) in shapes::invalid() {
494            let id = "11".repeat(32);
495            assert_eq!(own_proof(&bytes, &id).unwrap_err().offset, offset as u64);
496            assert!(!has_proof(&bytes, &id) && own_bump(&bytes, &id).is_none());
497        }
498    }
499
500    /// The own entry's BUMP, not the earliest BUMP that carries the txid.
501    #[test]
502    fn the_own_proof_follows_the_bump_index_not_the_earliest_carrier() {
503        let (bytes, id) = shapes::body(1);
504        let mut beef = Beef::from_binary(&bytes).unwrap();
505        beef.bumps
506            .push(MerklePath::from_coinbase_txid(&id, 900_000));
507        beef.find_txid_mut(&id).unwrap().set_bump_index(Some(1));
508        let moved = beef.to_binary();
509        assert_eq!(own_bump(&moved, &id).unwrap().block_height, 900_000);
510        assert_eq!(beef.find_bump(&id).unwrap().block_height, 800_000);
511    }
512
513    /// A stored or a peer's proof is read whatever its length; the courier's
514    /// and the push's FIELD keeps its wire bound.
515    #[test]
516    fn a_proof_over_the_wire_bound_is_read_where_no_wire_bounds_it() {
517        let (at, over) = shapes::proof_boundaries();
518        assert_eq!(over.to_binary().len(), COURIER_PROOF_MAX_BYTES + 1);
519        assert_eq!(
520            proof_from_hex(&over.to_hex()).unwrap().to_hex(),
521            over.to_hex()
522        );
523        assert_eq!(proof_from_hex(&at.to_hex()).unwrap().to_hex(), at.to_hex());
524        assert!(proof_from_hex("zz").is_err());
525        for bound in [COURIER_PROOF_MAX_BYTES, PUSH_PROOF_MAX_BYTES] {
526            assert!(merkle_path_from_hex(&at.to_hex(), bound).is_ok());
527            assert!(merkle_path_from_hex(&over.to_hex(), bound).is_err());
528        }
529    }
530
531    /// The structure check (NL-6e): each BUMP's height and root for the
532    /// chain, or the reader's refusal with its offset and kind; the wire's
533    /// order is read as written.
534    #[test]
535    fn the_structure_check_answers_the_roots_or_the_refusal() {
536        let (bytes, id) = shapes::body(1);
537        let beef = Beef::from_binary(&bytes).unwrap();
538        let root = beef
539            .find_bump(&id)
540            .unwrap()
541            .compute_root(Some(&id))
542            .unwrap();
543        assert_eq!(structure_roots(&bytes).unwrap(), vec![(800_000, root)]);
544        for (bytes, offset, _) in shapes::no_input() {
545            let refusal = structure_roots(&bytes).unwrap_err();
546            assert_eq!(
547                (refusal.offset, refusal.kind()),
548                (offset as u64, Kind::NoInputs)
549            );
550        }
551        for (bytes, offset, kind) in shapes::invalid() {
552            let refusal = structure_roots(&bytes).unwrap_err();
553            assert_eq!(refusal.offset, offset as u64, "{kind}");
554        }
555        // A child written before its parent: the in-memory check sorts and
556        // accepts it; the reader refuses the child's input at its first byte
557        // (after the version and the input count).
558        let parent = Transaction::from_binary(beef.txs[0].raw_tx().unwrap()).unwrap();
559        let mut child = Transaction::new();
560        let mut input = bsv_rs::transaction::TransactionInput::new(id.clone(), 0);
561        input.unlocking_script = Some(bsv_rs::script::UnlockingScript::new());
562        child.inputs.push(input);
563        child.outputs.push(parent.outputs[0].clone());
564        let mut wire = vec![0x01, 0x00, 0xbe, 0xef, 0x01];
565        wire.extend_from_slice(&beef.bumps[0].to_binary());
566        wire.push(0x02);
567        let child_at = wire.len();
568        wire.extend_from_slice(&child.to_binary());
569        wire.push(0x00);
570        wire.extend_from_slice(&parent.to_binary());
571        wire.extend_from_slice(&[0x01, 0x00]);
572        assert!(Beef::from_binary(&wire).unwrap().is_valid(false));
573        let refusal = structure_roots(&wire).unwrap_err();
574        assert_eq!(
575            (refusal.offset, refusal.kind()),
576            (child_at as u64 + 5, Kind::InputNamesNoElement)
577        );
578    }
579
580    #[test]
581    fn an_empty_beef_is_read() {
582        let read = read_beef(&Beef::new().to_binary()).unwrap();
583        assert_eq!((read.bumps, read.txs, read.bytes), (0, 0, 6));
584        assert_eq!(read.subject, None);
585    }
586}