use core::fmt;
use super::Error;
use super::base64;
use super::checkpoint::{SignedCheckpoint, parse_decimal};
use super::error::Syntax;
use super::note::KeyName;
use super::witness::Lines;
use crate::log::{
BalanceProof, Hash, InclusionProof, MAX_PROOF_HASHES, ProvenBalance, SealedBalanceProof,
TreeHead, balance_leaf_bytes,
};
use crate::model::{
AccountHandle, BALANCE_LEAF_KIND, Balance, Currency, Encode, EntryId, Layer, LedgerId,
MAX_LEAF_BYTES, Reader, Record, RecordKind, Writer,
};
const HEADER: &str = "c2sp.org/tlog-proof@v1";
pub const BALANCE_PROOF_TAG: &str = "doubleentry/balance-proof/1";
const BALANCE_LEAF_BYTES: usize = 41;
pub const MAX_EXTRA_BYTES: usize = 2 * MAX_LEAF_BYTES + 2 * MAX_PROOF_HASHES * 32 + 512;
const MAX_EXTRA_BASE64: usize = MAX_EXTRA_BYTES.div_ceil(3) * 4;
pub const MAX_BUNDLE_BYTES: usize = super::note::MAX_NOTE_BYTES + MAX_EXTRA_BASE64 + 4_096;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProofBundle<const P: u8 = 2> {
extra: Option<Vec<u8>>,
index: u64,
path: Vec<Hash>,
checkpoint: SignedCheckpoint,
}
impl ProofBundle<2> {
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
Self::parse_with_precision(bytes)
}
}
impl<const P: u8> ProofBundle<P> {
pub(crate) fn new(
extra: Vec<u8>,
proof: InclusionProof,
checkpoint: SignedCheckpoint,
) -> Result<Self, Error> {
if proof.path.len() > MAX_PROOF_HASHES {
return Err(Error::TooManyProofLines);
}
Ok(Self {
extra: Some(extra),
index: proof.index,
path: proof.path,
checkpoint,
})
}
pub fn parse_with_precision(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() > MAX_BUNDLE_BYTES {
return Err(Error::TooLarge {
limit: MAX_BUNDLE_BYTES,
});
}
let mut lines = Lines::new(bytes);
let header = |line| Error::Malformed {
what: Syntax::Header,
line,
};
if lines.next_line()? != HEADER {
return Err(header(1));
}
let mut line = lines.next_line()?;
let extra = match line.strip_prefix("extra ") {
Some(encoded) => {
if encoded.len() > MAX_EXTRA_BASE64 {
return Err(Error::TooLarge {
limit: MAX_EXTRA_BYTES,
});
}
let extra = base64::decode(encoded).ok_or(Error::Malformed {
what: Syntax::Base64,
line: lines.line(),
})?;
if extra.len() > MAX_EXTRA_BYTES {
return Err(Error::TooLarge {
limit: MAX_EXTRA_BYTES,
});
}
line = lines.next_line()?;
Some(extra)
}
None => None,
};
let index = line
.strip_prefix("index ")
.and_then(parse_decimal)
.ok_or(header(lines.line()))?;
let path = lines.hashes()?;
let checkpoint = SignedCheckpoint::parse(lines.rest())?;
Ok(Self {
extra,
index,
path,
checkpoint,
})
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = format!("{HEADER}\n").into_bytes();
if let Some(extra) = &self.extra {
bytes.extend_from_slice(format!("extra {}\n", base64::encode(extra)).as_bytes());
}
bytes.extend_from_slice(format!("index {}\n", self.index).as_bytes());
for hash in &self.path {
bytes.extend_from_slice(base64::encode(hash.as_bytes()).as_bytes());
bytes.push(b'\n');
}
bytes.push(b'\n');
bytes.extend_from_slice(&self.checkpoint.to_bytes());
bytes
}
#[must_use]
pub const fn index(&self) -> u64 {
self.index
}
#[must_use]
pub fn path(&self) -> &[Hash] {
&self.path
}
#[must_use]
pub const fn checkpoint(&self) -> &SignedCheckpoint {
&self.checkpoint
}
#[must_use]
pub fn extra(&self) -> Option<&[u8]> {
self.extra.as_deref()
}
fn head(&self) -> TreeHead {
self.checkpoint.checkpoint().head()
}
pub fn verify_inclusion(&self) -> Result<Proven<P>, Error> {
let head = self.head();
if self.index >= head.size {
return Err(Error::ProofShape);
}
let extra = self.extra.as_deref().ok_or_else(layout)?;
let origin = self.checkpoint.checkpoint().origin().clone();
let ledger = LedgerId::new(origin.as_str()).map_err(|_| Error::ForeignCheckpoint {
origin: origin.clone(),
})?;
match extra.first() {
Some(kind) if RecordKind::from_u8(*kind).is_some() => {
let proof = self.inclusion(head.size);
let record = proof.verify_record::<P>(&head, extra)?;
let entry_id = match &record {
Record::Entry(entry) => Some(entry.id(&ledger)),
_ => None,
};
Ok(Proven::Record(ProvenRecord {
origin,
head,
position: self.index,
record,
entry_id,
}))
}
_ => {
let proof = decode_balance_extra::<P>(extra, self.inclusion(head.size))?;
Ok(Proven::Balance(proof.verify(&head)?))
}
}
}
fn inclusion(&self, size: u64) -> InclusionProof {
InclusionProof {
index: self.index,
size,
path: self.path.clone(),
}
}
}
impl<const P: u8> fmt::Display for ProofBundle<P> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&String::from_utf8_lossy(&self.to_bytes()))
}
}
const fn layout() -> Error {
Error::Malformed {
what: Syntax::ExtraLayout,
line: 2,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Proven<const P: u8 = 2> {
Record(ProvenRecord<P>),
Balance(ProvenBalance<P>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ProvenRecord<const P: u8 = 2> {
pub origin: KeyName,
pub head: TreeHead,
pub position: u64,
pub record: Record<P>,
pub entry_id: Option<EntryId>,
}
pub(crate) fn balance_bundle<const P: u8>(
proof: SealedBalanceProof<P>,
checkpoint: SignedCheckpoint,
) -> Result<ProofBundle<P>, Error> {
let extra = encode_balance_extra(&proof);
ProofBundle::new(extra, proof.seal_proof, checkpoint)
}
fn encode_balance_extra<const P: u8>(proof: &SealedBalanceProof<P>) -> Vec<u8> {
let mut w = Writer::new(Vec::new());
w.str(BALANCE_PROOF_TAG);
bytes(&mut w, &proof.seal_leaf);
w.value(&proof.currency);
w.value(&proof.layer);
w.value(&proof.handle);
match &proof.balance {
None => w.u8(0),
Some(balance) => {
w.u8(1);
bytes(&mut w, &balance_leaf_bytes(&balance.balance));
hashes(&mut w, &balance.path.path);
}
}
w.u64(proof.account_proof.index);
bytes(&mut w, &proof.account_leaf);
hashes(&mut w, &proof.account_proof.path);
w.into_inner()
}
fn bytes(w: &mut Writer<Vec<u8>>, bytes: &[u8]) {
w.len(bytes.len());
w.fixed(bytes);
}
fn hashes(w: &mut Writer<Vec<u8>>, hashes: &[Hash]) {
w.len(hashes.len());
for hash in hashes {
hash.encode(w);
}
}
fn decode_balance_extra<const P: u8>(
extra: &[u8],
seal_proof: InclusionProof,
) -> Result<SealedBalanceProof<P>, Error> {
let mut r = Reader::new(extra);
if r.str("balance-proof tag").ok() != Some(BALANCE_PROOF_TAG) {
return Err(layout());
}
let seal_leaf = read_bytes(&mut r)?;
let currency: Currency = r.value("currency")?;
let layer: Layer = r.value("layer")?;
let handle: AccountHandle = r.value("handle")?;
let balance = match r.u8()? {
0 => None,
1 => {
let leaf = read_bytes(&mut r)?;
let path = read_hashes(&mut r)?;
Some((leaf, path))
}
_ => return Err(layout()),
};
let account_position = r.u64()?;
let account_leaf = read_bytes(&mut r)?;
let account_path = read_hashes(&mut r)?;
r.finish()?;
let balance = match balance {
None => None,
Some((leaf, path)) => {
let accounts = match Record::<P>::decode(&seal_leaf)? {
Record::Seal(seal) => seal.parts().accounts,
other => {
return Err(Error::WrongRecordKind {
expected: RecordKind::Seal,
found: other.kind(),
});
}
};
Some(BalanceProof {
currency,
layer,
handle,
balance: decode_balance_leaf(&leaf)?,
path: InclusionProof {
index: u64::from(handle.get()),
size: u64::from(accounts),
path,
},
})
}
};
let size = seal_proof.size;
Ok(SealedBalanceProof {
seal_leaf,
seal_proof,
account_leaf,
account_proof: InclusionProof {
index: account_position,
size,
path: account_path,
},
currency,
layer,
handle,
balance,
})
}
fn read_bytes(r: &mut Reader<'_>) -> Result<Vec<u8>, Error> {
let len = r.count("leaf", MAX_LEAF_BYTES)?;
Ok(r.take(len)?.to_vec())
}
fn read_hashes(r: &mut Reader<'_>) -> Result<Vec<Hash>, Error> {
let count = r.count("path", MAX_PROOF_HASHES)?;
(0..count)
.map(|_| r.value::<Hash>("path hash").map_err(Error::from))
.collect()
}
pub(crate) fn decode_balance_leaf<const P: u8>(leaf: &[u8]) -> Result<Balance<P>, Error> {
let (&kind, body) = leaf.split_first().ok_or_else(layout)?;
if kind != BALANCE_LEAF_KIND || leaf.len() != BALANCE_LEAF_BYTES {
return Err(layout());
}
let mut r = Reader::new(body);
let balance: Balance<P> = r.value("balance")?;
r.finish()?;
if balance_leaf_bytes(&balance).as_slice() == leaf {
Ok(balance)
} else {
Err(layout())
}
}
#[cfg(feature = "witness")]
mod with_keys {
use super::{ProofBundle, Proven};
use crate::transparency::Error;
use crate::transparency::policy::Policy;
impl<const P: u8> ProofBundle<P> {
pub fn verify(&self, policy: &Policy) -> Result<Proven<P>, Error> {
policy.verify(&self.checkpoint)?;
self.verify_inclusion()
}
}
}