use crate::types::GASPNode;
use serde::{Deserialize, Serialize};
pub const NODE_BATCH_MAX_OUTPOINTS: usize = 100;
pub const NODE_BATCH_MAX_BYTES: usize = 4 * 1024 * 1024;
pub mod refusal {
pub const NOT_FOUND: &str = "not_found";
pub const OVER_BUDGET: &str = "over_budget";
pub const ERROR: &str = "error";
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BatchOutpoint {
pub txid: String,
pub vout: u32,
}
impl BatchOutpoint {
pub fn key(&self) -> String {
format!("{}.{}", self.txid, self.vout)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NodeBatchRequest {
pub outpoints: Vec<BatchOutpoint>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeBatchEntry {
pub txid: String,
pub vout: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node: Option<GASPNode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refused: Option<String>,
}
impl NodeBatchEntry {
pub fn key(&self) -> String {
format!("{}.{}", self.txid, self.vout)
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct NodeBatchAnswer {
pub nodes: Vec<NodeBatchEntry>,
}
#[derive(Debug, Clone, Default)]
pub struct NodeBatch {
pub answer: NodeBatchAnswer,
pub bytes: usize,
}
const ENVELOPE_BYTES: usize = 12;
pub(crate) struct Packer {
budget: usize,
used: usize,
pub(crate) answer: NodeBatchAnswer,
}
impl Packer {
pub(crate) fn new(budget: usize) -> Self {
Self {
budget,
used: ENVELOPE_BYTES,
answer: NodeBatchAnswer::default(),
}
}
fn size(entry: &NodeBatchEntry) -> usize {
serde_json::to_string(entry).map_or(usize::MAX, |s| s.len())
}
pub(crate) fn push(&mut self, entry: NodeBatchEntry) -> bool {
let comma = usize::from(!self.answer.nodes.is_empty());
let mut entry = entry;
let mut size = Self::size(&entry);
if ENVELOPE_BYTES.saturating_add(size) > self.budget {
entry = NodeBatchEntry {
txid: entry.txid,
vout: entry.vout,
node: None,
refused: Some(refusal::OVER_BUDGET.into()),
};
size = Self::size(&entry);
}
if self.used.saturating_add(comma).saturating_add(size) > self.budget {
return false;
}
self.used += comma + size;
self.answer.nodes.push(entry);
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn node(hex_len: usize) -> GASPNode {
GASPNode {
graph_id: "aa.0".into(),
raw_tx: "0".repeat(hex_len),
output_index: 0,
proof: None,
tx_metadata: None,
output_metadata: None,
inputs: None,
}
}
fn entry(vout: u32, hex_len: usize) -> NodeBatchEntry {
NodeBatchEntry {
txid: "aa".into(),
vout,
node: Some(node(hex_len)),
refused: None,
}
}
#[test]
fn batch_021_the_packer_never_crosses_its_budget() {
let budget = 1_000;
let mut packer = Packer::new(budget);
let mut count = 0;
while packer.push(entry(count, 200)) {
count += 1;
}
let body = serde_json::to_string(&packer.answer).unwrap();
assert_eq!(body.len(), packer.used);
assert!(body.len() <= budget, "{}", body.len());
assert_eq!(count as usize, packer.answer.nodes.len());
assert!(count >= 3, "{count}");
}
#[test]
fn batch_021_a_node_over_the_whole_budget_is_refused_by_name() {
let mut packer = Packer::new(1_000);
assert!(packer.push(entry(0, 5_000)));
assert!(packer.push(entry(1, 10)));
let alone = &packer.answer.nodes[0];
assert!(alone.node.is_none());
assert_eq!(alone.refused.as_deref(), Some(refusal::OVER_BUDGET));
assert!(packer.answer.nodes[1].node.is_some());
let body = serde_json::to_string(&packer.answer).unwrap();
assert_eq!(body.len(), packer.used);
}
#[test]
fn batch_021_the_wire_shape() {
let request: NodeBatchRequest =
serde_json::from_str(r#"{"outpoints":[{"txid":"aa","vout":1}]}"#).unwrap();
assert_eq!(request.outpoints[0].key(), "aa.1");
assert!(serde_json::from_str::<NodeBatchRequest>(r#"{"outpoints":[],"x":1}"#).is_err());
let refused = NodeBatchEntry {
txid: "aa".into(),
vout: 1,
node: None,
refused: Some(refusal::NOT_FOUND.into()),
};
assert_eq!(
serde_json::to_string(&refused).unwrap(),
r#"{"txid":"aa","vout":1,"refused":"not_found"}"#
);
let served = serde_json::to_value(entry(0, 2)).unwrap();
assert_eq!(served["node"]["graphID"], "aa.0");
assert_eq!(served["node"]["rawTx"], "00");
}
#[test]
fn batch_021_a_full_request_of_proven_uhrp_nodes_fits_the_budget() {
let mut node = node(483 * 2);
node.proof = Some("0".repeat(crate::beef_limits::PEER_PROOF_MAX_BYTES * 2));
let mut packer = Packer::new(NODE_BATCH_MAX_BYTES);
for vout in 0..NODE_BATCH_MAX_OUTPOINTS as u32 {
assert!(packer.push(NodeBatchEntry {
txid: "ab".repeat(32),
vout,
node: Some(node.clone()),
refused: None,
}));
}
let body = serde_json::to_string(&packer.answer).unwrap().len();
assert!(body < 1_000_000, "{body}");
}
}