use core::fmt;
use super::Error;
use super::base64;
use super::checkpoint::{SignedCheckpoint, parse_decimal};
use super::error::Syntax;
use super::note::MAX_NOTE_BYTES;
use crate::log::{ConsistencyProof, Hash, MAX_PROOF_HASHES};
pub const MAX_REQUEST_BYTES: usize = MAX_NOTE_BYTES + 4_096;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AddCheckpoint {
old: u64,
proof: Vec<Hash>,
checkpoint: SignedCheckpoint,
}
impl AddCheckpoint {
pub fn new(
checkpoint: &SignedCheckpoint,
old: u64,
proof: Option<&ConsistencyProof>,
) -> Result<Self, Error> {
let size = checkpoint.checkpoint().head().size;
let path = match (needs_proof(old, size)?, proof) {
(false, None) => Vec::new(),
(false, Some(proof)) if proof.path.is_empty() => Vec::new(),
(true, Some(proof)) if (proof.old_size, proof.new_size) == (old, size) => {
proof.path.clone()
}
_ => return Err(Error::ProofShape),
};
if path.len() > MAX_PROOF_HASHES {
return Err(Error::TooManyProofLines);
}
Ok(Self {
old,
proof: path,
checkpoint: checkpoint.clone(),
})
}
pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() > MAX_REQUEST_BYTES {
return Err(Error::TooLarge {
limit: MAX_REQUEST_BYTES,
});
}
let mut lines = Lines::new(bytes);
let old = lines
.next_line()?
.strip_prefix("old ")
.and_then(parse_decimal)
.ok_or(Error::Malformed {
what: Syntax::Header,
line: 1,
})?;
let proof = lines.hashes()?;
let checkpoint = SignedCheckpoint::parse(lines.rest())?;
Ok(Self {
old,
proof,
checkpoint,
})
}
#[must_use]
pub const fn old(&self) -> u64 {
self.old
}
#[must_use]
pub fn proof(&self) -> &[Hash] {
&self.proof
}
#[must_use]
pub const fn checkpoint(&self) -> &SignedCheckpoint {
&self.checkpoint
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
let mut body = format!("old {}\n", self.old).into_bytes();
for hash in &self.proof {
body.extend_from_slice(base64::encode(hash.as_bytes()).as_bytes());
body.push(b'\n');
}
body.push(b'\n');
body.extend_from_slice(&self.checkpoint.to_bytes());
body
}
}
fn needs_proof(old: u64, size: u64) -> Result<bool, Error> {
if old > size {
Err(Error::ProofShape)
} else {
Ok(old != 0 && old != size)
}
}
pub(crate) struct Lines<'a> {
bytes: &'a [u8],
line: u32,
}
impl<'a> Lines<'a> {
pub(crate) const fn new(bytes: &'a [u8]) -> Self {
Self { bytes, line: 0 }
}
pub(crate) const fn line(&self) -> u32 {
self.line
}
pub(crate) fn next_line(&mut self) -> Result<&'a str, Error> {
self.line = self.line.saturating_add(1);
let malformed = |what| Error::Malformed {
what,
line: self.line,
};
let end = self
.bytes
.iter()
.position(|byte| *byte == b'\n')
.ok_or(malformed(Syntax::MissingNewline))?;
let (line, rest) = self.bytes.split_at(end);
self.bytes = rest.get(1..).unwrap_or_default();
let line = core::str::from_utf8(line).map_err(|_| malformed(Syntax::Utf8))?;
if line.bytes().any(|byte| byte < 0x20) {
return Err(malformed(Syntax::ControlChar));
}
Ok(line)
}
pub(crate) fn hashes(&mut self) -> Result<Vec<Hash>, Error> {
let mut hashes = Vec::new();
loop {
let line = self.next_line()?;
if line.is_empty() {
return Ok(hashes);
}
if hashes.len() == MAX_PROOF_HASHES {
return Err(Error::TooManyProofLines);
}
let hash = base64::decode_hash(line).ok_or(Error::Malformed {
what: Syntax::Base64,
line: self.line,
})?;
hashes.push(Hash::from_bytes(hash));
}
}
pub(crate) const fn rest(&self) -> &'a [u8] {
self.bytes
}
}
impl fmt::Display for AddCheckpoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&String::from_utf8_lossy(&self.to_bytes()))
}
}
#[cfg(feature = "witness")]
pub use with_keys::{Next, WitnessReply, WitnessSession};
#[cfg(feature = "witness")]
mod with_keys {
use super::{AddCheckpoint, Error};
use crate::log::ConsistencyProof;
use crate::transparency::checkpoint::{SignedCheckpoint, parse_decimal};
use crate::transparency::error::WitnessError;
use crate::transparency::note::{MAX_NOTE_BYTES, MAX_SIGNATURES, NoteSignature};
use crate::transparency::signing::{Cosignature, KeyType, Verified, Vkey, verify_line};
const SIZE_MEDIA_TYPE: &str = "text/x.tlog.size";
const MAX_SIZE_BYTES: usize = 21;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct WitnessReply;
impl WitnessReply {
pub fn read(
status: u16,
content_type: Option<&str>,
body: &[u8],
checkpoint: &SignedCheckpoint,
witness: &Vkey,
) -> Result<Cosignature, Error> {
cosigner(witness)?;
let outcome = match status {
200 => return cosignature(body, checkpoint, witness),
409 => conflict(content_type, body),
400 => WitnessError::BadRequest,
403 => WitnessError::Forbidden,
404 => WitnessError::UnknownLog,
422 => WitnessError::Unprocessable,
429 => WitnessError::Pushback,
status => WitnessError::Unexpected { status },
};
Err(outcome.into())
}
}
fn cosigner(witness: &Vkey) -> Result<(), Error> {
match witness.key_type() {
KeyType::CosignatureEd25519 => Ok(()),
found => Err(Error::WrongKeyType {
expected: KeyType::CosignatureEd25519,
found,
}),
}
}
fn conflict(content_type: Option<&str>, body: &[u8]) -> WitnessError {
let media_type = content_type
.and_then(|value| value.split(';').next())
.map(str::trim);
let size = media_type
.filter(|media| media.eq_ignore_ascii_case(SIZE_MEDIA_TYPE))
.filter(|_| body.len() <= MAX_SIZE_BYTES)
.and_then(|_| core::str::from_utf8(body).ok())
.and_then(|text| text.strip_suffix('\n'))
.and_then(parse_decimal);
size.map_or(WitnessError::MalformedReply, |size| {
WitnessError::Conflict { size }
})
}
fn cosignature(
body: &[u8],
checkpoint: &SignedCheckpoint,
witness: &Vkey,
) -> Result<Cosignature, Error> {
let malformed = || Error::from(WitnessError::MalformedReply);
if body.is_empty() || body.len() > MAX_NOTE_BYTES {
return Err(malformed());
}
let text = core::str::from_utf8(body).map_err(|_| malformed())?;
let lines = text.strip_suffix('\n').ok_or_else(malformed)?;
let mut read: Vec<NoteSignature> = Vec::new();
for (at, line) in lines.split('\n').enumerate() {
if read.len() == MAX_SIGNATURES {
return Err(malformed());
}
let number = u32::try_from(at.saturating_add(1)).unwrap_or(u32::MAX);
let line = NoteSignature::parse_line(line, number).map_err(|_| malformed())?;
if read
.iter()
.any(|seen| (seen.name(), seen.key_id()) == (line.name(), line.key_id()))
{
return Err(malformed());
}
read.push(line);
}
let Some(line) = read.iter().find(|line| witness.signed(line)) else {
return Err(WitnessError::NoCosignature {
name: witness.name().clone(),
}
.into());
};
match verify_line(witness, line, checkpoint.note().text()) {
Ok(Verified::Cosigner(cosignature)) => Ok(cosignature),
_ => Err(WitnessError::BadCosignature {
name: line.name().clone(),
key_id: line.key_id(),
}
.into()),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Next {
Cosigned {
cosignature: Cosignature,
witness_size: u64,
},
Retry,
}
#[derive(Debug, Clone)]
pub struct WitnessSession {
checkpoint: SignedCheckpoint,
witness: Vkey,
old: u64,
conflicts: Vec<u64>,
finished: bool,
}
const MAX_CONFLICTS: usize = 3;
impl WitnessSession {
pub fn new(checkpoint: SignedCheckpoint, witness: Vkey, old: u64) -> Result<Self, Error> {
cosigner(&witness)?;
if old > checkpoint.checkpoint().head().size {
return Err(Error::ProofShape);
}
Ok(Self {
checkpoint,
witness,
old,
conflicts: Vec::new(),
finished: false,
})
}
#[must_use]
pub const fn old(&self) -> u64 {
self.old
}
#[must_use]
pub fn needed_proof(&self) -> Option<(u64, u64)> {
let size = self.checkpoint.checkpoint().head().size;
(self.old != 0 && self.old != size).then_some((self.old, size))
}
pub fn request(&mut self, proof: Option<&ConsistencyProof>) -> Result<Vec<u8>, Error> {
self.live()?;
Ok(AddCheckpoint::new(&self.checkpoint, self.old, proof)?.to_bytes())
}
pub fn receive(
&mut self,
status: u16,
content_type: Option<&str>,
body: &[u8],
) -> Result<Next, Error> {
self.live()?;
let size = self.checkpoint.checkpoint().head().size;
let read =
WitnessReply::read(status, content_type, body, &self.checkpoint, &self.witness);
let next = match read {
Ok(cosignature) => Ok(Next::Cosigned {
cosignature,
witness_size: size,
}),
Err(Error::Witness(WitnessError::Conflict { size: witness })) => {
let previous = self.conflicts.iter().copied().max();
self.conflicts.push(witness);
if let Some(previous) = previous.filter(|previous| witness < *previous) {
Err(WitnessError::WitnessRegressed {
previous,
size: witness,
}
.into())
} else if witness > size {
Err(WitnessError::WitnessAhead {
witness,
checkpoint: size,
}
.into())
} else if self.conflicts.len() >= MAX_CONFLICTS {
Err(WitnessError::TooManyConflicts {
sizes: self.conflicts.clone(),
}
.into())
} else {
self.old = witness;
return Ok(Next::Retry);
}
}
Err(error) => Err(error),
};
self.finished = true;
next
}
fn live(&self) -> Result<(), Error> {
if self.finished {
Err(WitnessError::SessionFinished.into())
} else {
Ok(())
}
}
}
}