use std::io::Read;
use bsv_rs::transaction::beef_stream::{display_hex, Hash32, Step, StreamError};
use bsv_rs::transaction::{
verify_stream_structure, Beef, BeefDecoder, BeefStream, Element, Headers, MerklePath,
Transaction, Verdict,
};
pub use bsv_rs::transaction::beef_stream::StreamError as DoorError;
pub use bsv_rs::transaction::{Kind, Reason, Refusal};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BeefLimits {
pub max_txs: usize,
pub max_bumps: usize,
pub max_bytes: usize,
}
pub const SUBMIT_BODY_MAX_BYTES: usize = 10_000_000;
pub const ATOMIC_HEADER_BYTES: usize = 36;
const MAX_SUBMISSION_TXS: usize = 512;
const MAX_SUBMISSION_BUMPS: usize = 512;
pub const SUBMIT_BEEF_LIMITS: BeefLimits = BeefLimits {
max_txs: MAX_SUBMISSION_TXS,
max_bumps: MAX_SUBMISSION_BUMPS,
max_bytes: SUBMIT_BODY_MAX_BYTES,
};
pub const ENGINE_BEEF_LIMITS: BeefLimits = BeefLimits {
max_bytes: SUBMIT_BODY_MAX_BYTES + ATOMIC_HEADER_BYTES,
..SUBMIT_BEEF_LIMITS
};
pub const EF_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
pub const STORED_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
pub const PEER_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
pub const DISCOVERY_BEEF_LIMITS: BeefLimits = ENGINE_BEEF_LIMITS;
pub const APP_BEEF_LIMITS: BeefLimits = STORED_BEEF_LIMITS;
pub const QUEUE_BEEF_LIMITS: BeefLimits = BeefLimits {
max_bytes: ENGINE_BEEF_LIMITS.max_bytes,
..SUBMIT_BEEF_LIMITS
};
pub const DEAD_LETTER_BEEF_LIMITS: BeefLimits = QUEUE_BEEF_LIMITS;
pub const CENSUS_BEEF_LIMITS: BeefLimits = BeefLimits {
max_bytes: 2 * 1024 * 1024,
..SUBMIT_BEEF_LIMITS
};
pub const COURIER_PROOF_MAX_BYTES: usize = (8 * 1024 - 2) / 2;
pub const PUSH_PROOF_MAX_BYTES: usize = COURIER_PROOF_MAX_BYTES;
pub const STORED_PROOF_MAX_BYTES: usize = COURIER_PROOF_MAX_BYTES;
pub const PEER_PROOF_MAX_BYTES: usize = COURIER_PROOF_MAX_BYTES;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BeefRead {
pub version: u32,
pub subject: Option<String>,
pub bumps: u64,
pub txs: u64,
pub last_txid: Option<String>,
pub bytes: u64,
}
#[derive(Default)]
struct Tally {
bumps: u64,
txs: u64,
last_txid: Option<[u8; 32]>,
}
impl Tally {
fn see(&mut self, element: &Element) {
match element {
Element::Bump(_) => self.bumps += 1,
Element::Tx { txid, .. } | Element::TxidOnly { txid, .. } => {
self.txs += 1;
self.last_txid = Some(*txid);
}
}
}
fn read(self, version: Option<u32>, subject: Option<[u8; 32]>, bytes: u64) -> BeefRead {
BeefRead {
version: version.unwrap_or_default(),
subject: subject.as_ref().map(display_hex),
bumps: self.bumps,
txs: self.txs,
last_txid: self.last_txid.as_ref().map(display_hex),
bytes,
}
}
}
pub fn fold_beef<R: Read>(
source: R,
mut visit: impl FnMut(Element),
) -> Result<BeefRead, StreamError> {
let mut stream = BeefStream::new(source);
let mut tally = Tally::default();
while let Some(element) = stream.next_element()? {
tally.see(&element);
visit(element);
}
Ok(tally.read(stream.version(), stream.subject(), stream.offset()))
}
pub fn fold_held(bytes: &[u8], mut visit: impl FnMut(Element)) -> Result<BeefRead, Refusal> {
let mut decoder = BeefDecoder::new();
let mut tally = Tally::default();
let mut input = bytes;
loop {
match decoder.next(&mut input)? {
Step::Element(element) => {
tally.see(&element);
visit(element);
}
Step::NeedMore => {
decoder.finish()?;
break;
}
Step::Done => break,
}
}
Ok(tally.read(decoder.version(), decoder.subject(), decoder.offset()))
}
pub fn read_beef(bytes: &[u8]) -> Result<BeefRead, Refusal> {
fold_held(bytes, |_| {})
}
fn refused(refusal: &Refusal) -> bsv_rs::Error {
bsv_rs::Error::BeefError(refusal.to_string())
}
struct EveryRoot;
impl Headers for EveryRoot {
fn carries(&self, _height: u64, _root: &Hash32) -> bool {
true
}
}
pub fn structure_roots(bytes: &[u8]) -> Result<Vec<(u64, String)>, Refusal> {
match verify_stream_structure(bytes, EveryRoot, None) {
Ok(Verdict::Valid { roots, .. }) => Ok(roots
.iter()
.map(|(height, root)| (*height, display_hex(root)))
.collect()),
Ok(Verdict::Invalid { offset, reason, .. }) => Err(Refusal { offset, reason }),
Ok(Verdict::Unverifiable { .. }) => {
unreachable!("the structure-only reader requested mining context")
}
Ok(Verdict::SpendRefused { .. }) | Err(_) => {
unreachable!("the structure check of held bytes ran a script or failed to read")
}
}
}
pub fn parse_beef(bytes: &[u8], _door: &BeefLimits) -> bsv_rs::Result<Beef> {
read_beef(bytes).map_err(|refusal| refused(&refusal))?;
Beef::from_binary(bytes)
}
pub fn transaction_from_beef(
bytes: &[u8],
txid: Option<&str>,
door: &BeefLimits,
) -> bsv_rs::Result<Transaction> {
let parsed = parse_beef(bytes, door)?;
let target = txid
.map(str::to_string)
.or_else(|| parsed.atomic_txid.clone())
.or_else(|| parsed.txs.last().map(bsv_rs::transaction::BeefTx::txid))
.ok_or_else(|| bsv_rs::Error::TransactionError("No transactions in BEEF".into()))?;
parsed.find_atomic_transaction(&target).ok_or_else(|| {
bsv_rs::Error::TransactionError(format!("Transaction {target} not found in BEEF"))
})
}
fn own_bump_index(bytes: &[u8], txid: &str) -> Result<Option<u64>, Refusal> {
let mut found = None;
fold_held(bytes, |element| match element {
Element::Tx {
txid: id,
bump_index,
..
} if display_hex(&id) == txid => found = Some(bump_index),
Element::TxidOnly { txid: id, .. } if display_hex(&id) == txid => found = Some(None),
_ => {}
})?;
Ok(found.flatten())
}
pub fn has_proof(bytes: &[u8], txid: &str) -> bool {
matches!(own_bump_index(bytes, txid), Ok(Some(_)))
}
pub fn own_proof(bytes: &[u8], txid: &str) -> Result<Option<MerklePath>, Refusal> {
let Some(wanted) = own_bump_index(bytes, txid)? else {
return Ok(None);
};
let (mut seen, mut own) = (0u64, None);
fold_held(bytes, |element| {
if let Element::Bump(bump) = element {
if seen == wanted {
own = bump.to_merkle_path().ok();
}
seen += 1;
}
})?;
Ok(own)
}
pub fn own_bump(bytes: &[u8], txid: &str) -> Option<MerklePath> {
own_proof(bytes, txid).ok().flatten()
}
pub fn merkle_path_from_hex(hex: &str, max_bytes: usize) -> bsv_rs::Result<MerklePath> {
check_size(hex.len(), max_bytes.saturating_mul(2), "merkle proof hex")?;
proof_from_hex(hex)
}
pub fn proof_from_hex(hex: &str) -> bsv_rs::Result<MerklePath> {
let bytes = bsv_rs::primitives::from_hex(hex)?;
MerklePath::from_binary(&bytes)
}
pub fn is_limit_breach(_e: &bsv_rs::Error) -> bool {
false
}
pub fn check_size(size: usize, max_bytes: usize, kind: &str) -> bsv_rs::Result<()> {
if size > max_bytes {
return Err(bsv_rs::Error::BeefError(format!(
"{kind} of {size} bytes is over max_bytes {max_bytes}"
)));
}
Ok(())
}
#[cfg(test)]
#[path = "../tests/support/beef_doors.rs"]
mod door_shapes;
#[cfg(test)]
mod door_pin {
use super::door_shapes as shapes;
use super::*;
fn tree_height_65() -> Vec<u8> {
let mut bin = vec![0x02, 0x00, 0xbe, 0xef, 0x01];
bin.extend_from_slice(&[0xfe, 0x00, 0x35, 0x0c, 0x00]);
bin.push(65);
bin
}
#[test]
fn a_refusal_names_the_offset_and_the_kind() {
let refusal = read_beef(&tree_height_65()).unwrap_err();
assert_eq!(
(refusal.offset, refusal.kind()),
(10, Kind::TreeHeightOver64)
);
let refusal = read_beef(&[0xde, 0xad, 0xbe, 0xef]).unwrap_err();
assert_eq!((refusal.offset, refusal.kind()), (0, Kind::BadVersion));
let e = parse_beef(&tree_height_65(), &SUBMIT_BEEF_LIMITS).unwrap_err();
assert_eq!(
e.to_string()
.matches("invalid BEEF at byte 10: TreeHeightOver64")
.count(),
1,
"{e}"
);
}
#[test]
fn a_claimed_count_is_refused_for_the_bytes_never_as_a_count() {
let bin = [0x02, 0x00, 0xbe, 0xef, 0xfd, 0x2c, 0x01];
let refusal = read_beef(&bin).unwrap_err();
assert_eq!((refusal.offset, refusal.kind()), (7, Kind::BadVarint));
let e = parse_beef(&bin, &SUBMIT_BEEF_LIMITS).unwrap_err();
assert!(!e.to_string().contains("max_"), "{e}");
assert!(!is_limit_breach(&e));
}
struct Drip<'a>(&'a [u8]);
impl Read for Drip<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.0.len().min(buf.len()).min(7);
buf[..n].copy_from_slice(&self.0[..n]);
self.0 = &self.0[n..];
Ok(n)
}
}
struct Fails<'a>(&'a [u8]);
impl Read for Fails<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.0.is_empty() {
return Err(std::io::Error::other("the object body broke"));
}
let n = self.0.len().min(buf.len());
buf[..n].copy_from_slice(&self.0[..n]);
self.0 = &self.0[n..];
Ok(n)
}
}
#[test]
fn a_source_is_read_as_the_held_bytes_are_one_element_in_hand() {
for (bytes, _) in [
shapes::body(1),
shapes::transactions(2_048),
shapes::bumps(2_048),
shapes::sized_body(SUBMIT_BODY_MAX_BYTES + 1),
] {
let held = read_beef(&bytes).unwrap();
assert_eq!(held.bytes, bytes.len() as u64);
let (mut elements, mut largest) = (0u64, 0u64);
let streamed = fold_beef(Drip(&bytes), |element| {
elements += 1;
largest = largest.max(element.wire_len());
})
.unwrap();
assert_eq!(streamed, held);
assert_eq!(elements, held.bumps + held.txs);
assert!(largest < held.bytes);
}
let (bytes, _) = shapes::body(1);
let cut = &bytes[..bytes.len() - 3];
assert!(matches!(
fold_beef(Fails(cut), |_| {}),
Err(StreamError::Io(_))
));
assert_eq!(read_beef(cut).unwrap_err().kind(), Kind::Truncated);
}
#[test]
fn the_own_proof_is_the_in_memory_index_s() {
let (stub_only, _) = shapes::transactions(3);
for (bytes, id) in [
shapes::body(1),
shapes::transactions(600),
shapes::bumps(600),
] {
let beef = Beef::from_binary(&bytes).unwrap();
let entry = beef.find_txid(&id).unwrap();
let in_memory = entry.bump_index().and_then(|i| beef.bumps.get(i));
assert!(has_proof(&bytes, &id));
assert_eq!(
own_bump(&bytes, &id).map(|b| b.to_hex()),
in_memory.map(MerklePath::to_hex)
);
let stub = format!("{:064x}", 1);
assert!(!has_proof(&stub_only, &stub));
assert!(matches!(own_proof(&stub_only, &stub), Ok(None)));
assert!(matches!(own_proof(&bytes, &"ee".repeat(32)), Ok(None)));
}
for (bytes, offset, _) in shapes::invalid() {
let id = "11".repeat(32);
assert_eq!(own_proof(&bytes, &id).unwrap_err().offset, offset as u64);
assert!(!has_proof(&bytes, &id) && own_bump(&bytes, &id).is_none());
}
}
#[test]
fn the_own_proof_follows_the_bump_index_not_the_earliest_carrier() {
let (bytes, id) = shapes::body(1);
let mut beef = Beef::from_binary(&bytes).unwrap();
beef.bumps
.push(MerklePath::from_coinbase_txid(&id, 900_000));
beef.find_txid_mut(&id).unwrap().set_bump_index(Some(1));
let moved = beef.to_binary();
assert_eq!(own_bump(&moved, &id).unwrap().block_height, 900_000);
assert_eq!(beef.find_bump(&id).unwrap().block_height, 800_000);
}
#[test]
fn a_proof_over_the_wire_bound_is_read_where_no_wire_bounds_it() {
let (at, over) = shapes::proof_boundaries();
assert_eq!(over.to_binary().len(), COURIER_PROOF_MAX_BYTES + 1);
assert_eq!(
proof_from_hex(&over.to_hex()).unwrap().to_hex(),
over.to_hex()
);
assert_eq!(proof_from_hex(&at.to_hex()).unwrap().to_hex(), at.to_hex());
assert!(proof_from_hex("zz").is_err());
for bound in [COURIER_PROOF_MAX_BYTES, PUSH_PROOF_MAX_BYTES] {
assert!(merkle_path_from_hex(&at.to_hex(), bound).is_ok());
assert!(merkle_path_from_hex(&over.to_hex(), bound).is_err());
}
}
#[test]
fn the_structure_check_answers_the_roots_or_the_refusal() {
let (bytes, id) = shapes::body(1);
let beef = Beef::from_binary(&bytes).unwrap();
let root = beef
.find_bump(&id)
.unwrap()
.compute_root(Some(&id))
.unwrap();
assert_eq!(structure_roots(&bytes).unwrap(), vec![(800_000, root)]);
for (bytes, offset, _) in shapes::no_input() {
let refusal = structure_roots(&bytes).unwrap_err();
assert_eq!(
(refusal.offset, refusal.kind()),
(offset as u64, Kind::NoInputs)
);
}
for (bytes, offset, kind) in shapes::invalid() {
let refusal = structure_roots(&bytes).unwrap_err();
assert_eq!(refusal.offset, offset as u64, "{kind}");
}
let parent = Transaction::from_binary(beef.txs[0].raw_tx().unwrap()).unwrap();
let mut child = Transaction::new();
let mut input = bsv_rs::transaction::TransactionInput::new(id.clone(), 0);
input.unlocking_script = Some(bsv_rs::script::UnlockingScript::new());
child.inputs.push(input);
child.outputs.push(parent.outputs[0].clone());
let mut wire = vec![0x01, 0x00, 0xbe, 0xef, 0x01];
wire.extend_from_slice(&beef.bumps[0].to_binary());
wire.push(0x02);
let child_at = wire.len();
wire.extend_from_slice(&child.to_binary());
wire.push(0x00);
wire.extend_from_slice(&parent.to_binary());
wire.extend_from_slice(&[0x01, 0x00]);
assert!(Beef::from_binary(&wire).unwrap().is_valid(false));
let refusal = structure_roots(&wire).unwrap_err();
assert_eq!(
(refusal.offset, refusal.kind()),
(child_at as u64 + 5, Kind::InputNamesNoElement)
);
}
#[test]
fn an_empty_beef_is_read() {
let read = read_beef(&Beef::new().to_binary()).unwrap();
assert_eq!((read.bumps, read.txs, read.bytes), (0, 0, 6));
assert_eq!(read.subject, None);
}
}