use crate::crypto::{ed25519, Rng, Sha256};
use crate::error::{Error, Result};
use alloc::vec::Vec;
use nextjson::{NsonDeserialize, NsonSerialize};
use tzcraft::Ticks;
pub const RECEIPT_VERSION: u8 = 1;
pub const MIN_COVERAGE_PERMILLE: u64 = 900;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Challenge {
pub id: [u8; 16],
pub content_root: [u8; 32],
pub range: (u64, u64),
pub issued_epoch: u64,
pub nonce: [u8; 16],
}
impl Challenge {
pub fn new(
content_root: [u8; 32],
range: (u64, u64),
issued_epoch: u64,
rng: &mut Rng,
) -> Self {
let mut id = [0u8; 16];
let mut nonce = [0u8; 16];
rng.fill(&mut id);
rng.fill(&mut nonce);
Challenge {
id,
content_root,
range,
issued_epoch,
nonce,
}
}
pub fn digest(&self) -> [u8; 32] {
let mut m = Vec::with_capacity(16 + 32 + 16 + 8 + 16);
m.extend_from_slice(&self.id);
m.extend_from_slice(&self.content_root);
m.extend_from_slice(&self.range.0.to_be_bytes());
m.extend_from_slice(&self.range.1.to_be_bytes());
m.extend_from_slice(&self.issued_epoch.to_be_bytes());
m.extend_from_slice(&self.nonce);
Sha256::digest(&m)
}
}
#[derive(Debug, Clone, PartialEq, Eq, NsonSerialize, NsonDeserialize)]
pub struct ReceiptPayload {
pub version: u8,
pub content_root: Vec<u8>,
pub node_id: Vec<u8>,
pub range_start: u64,
pub range_end: u64,
pub epoch_start: i64,
pub epoch_end: i64,
pub bytes_received: u64,
pub challenge_digest: Vec<u8>,
pub data_proof: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Receipt {
pub payload: ReceiptPayload,
pub signature: [u8; 64],
}
pub fn receipt_message(p: &ReceiptPayload) -> Result<Vec<u8>> {
let mut m = Vec::with_capacity(8 + 32 + 32 + 8 + 8 + 16 + 16 + 8 + 32 + 32);
m.push(p.version);
m.extend_from_slice(&[0u8; 7]); m.extend_from_slice(&to_fixed32(&p.content_root)?);
m.extend_from_slice(&to_fixed32(&p.node_id)?);
m.extend_from_slice(&p.range_start.to_be_bytes());
m.extend_from_slice(&p.range_end.to_be_bytes());
m.extend_from_slice(&p.epoch_start.to_be_bytes());
m.extend_from_slice(&p.epoch_end.to_be_bytes());
m.extend_from_slice(&p.bytes_received.to_be_bytes());
m.extend_from_slice(&to_fixed32(&p.challenge_digest)?);
m.extend_from_slice(&to_fixed32(&p.data_proof)?);
Ok(m)
}
fn to_fixed32(v: &[u8]) -> Result<[u8; 32]> {
if v.len() != 32 {
return Err(Error::Receipt);
}
let mut o = [0u8; 32];
o.copy_from_slice(v);
Ok(o)
}
impl Receipt {
pub fn sign(payload: ReceiptPayload, secret_key: &[u8; 32]) -> Result<Receipt> {
if payload.content_root.len() != 32
|| payload.node_id.len() != 32
|| payload.challenge_digest.len() != 32
|| payload.data_proof.len() != 32
{
return Err(Error::Receipt);
}
let msg = receipt_message(&payload)?;
let signature = ed25519::sign(secret_key, &msg);
Ok(Receipt { payload, signature })
}
pub fn verify(&self) -> bool {
if self.payload.version != RECEIPT_VERSION {
return false;
}
let msg = match receipt_message(&self.payload) {
Ok(m) => m,
Err(_) => return false,
};
match to_fixed32(&self.payload.node_id) {
Ok(pk) => ed25519::verify(&pk, &msg, &self.signature),
Err(_) => false,
}
}
pub fn verify_against_challenge(
&self,
challenge: &Challenge,
auditor_public_key: &[u8; 32],
) -> bool {
if !self.verify() {
return false;
}
if self.payload.challenge_digest != challenge.digest() {
return false;
}
if self.payload.content_root != challenge.content_root {
return false;
}
if challenge.issued_epoch < self.payload.epoch_start as u64
|| challenge.issued_epoch > self.payload.epoch_end as u64
{
return false;
}
if self.payload.range_start > challenge.range.0
|| self.payload.range_end < challenge.range.1
{
return false;
}
let msg = match receipt_message(&self.payload) {
Ok(m) => m,
Err(_) => return false,
};
ed25519::verify(auditor_public_key, &msg, &self.signature)
}
}
#[derive(Debug, Clone, Default)]
pub struct ReceiptBook {
pub content_root: [u8; 32],
ranges: Vec<(u64, u64)>,
samples: Vec<(u64, [u8; 32])>,
bytes: u64,
}
impl ReceiptBook {
pub fn new(content_root: [u8; 32]) -> Self {
ReceiptBook {
content_root,
ranges: Vec::new(),
samples: Vec::new(),
bytes: 0,
}
}
pub fn record_range(&mut self, start: u64, end: u64) {
if end <= start {
return;
}
self.ranges.push((start, end));
self.bytes += end - start;
self.ranges.sort_unstable();
let mut merged: Vec<(u64, u64)> = Vec::with_capacity(self.ranges.len());
for (s, e) in self.ranges.drain(..) {
if let Some(last) = merged.last_mut() {
if s <= last.1 {
if e > last.1 {
last.1 = e;
}
continue;
}
}
merged.push((s, e));
}
self.ranges = merged;
}
pub fn record_sample(&mut self, offset: u64, block_hash: [u8; 32]) {
if !self.samples.iter().any(|(o, _)| *o == offset) {
self.samples.push((offset, block_hash));
}
}
pub fn coverage(&self, start: u64, end: u64) -> u64 {
let mut covered = 0u64;
for &(s, e) in &self.ranges {
let lo = s.max(start);
let hi = e.min(end);
if hi > lo {
covered += hi - lo;
}
}
covered
}
pub fn bytes(&self) -> u64 {
self.bytes
}
pub fn coverage_permille(&self, start: u64, end: u64) -> u64 {
let total = end - start;
if total == 0 {
return 1000;
}
self.coverage(start, end) * 1000 / total
}
pub fn data_proof(&self, start: u64, end: u64) -> Option<[u8; 32]> {
let mut inside: Vec<[u8; 32]> = self
.samples
.iter()
.filter(|(o, _)| *o >= start && *o < end)
.map(|(_, h)| *h)
.collect();
if inside.is_empty() {
return None;
}
inside.sort_unstable();
let mut m = Vec::with_capacity(1 + inside.len() * 32);
m.push(0x52); for h in inside {
m.extend_from_slice(&h);
}
Some(Sha256::digest(&m))
}
pub fn build_receipt(
&self,
range: (u64, u64),
epoch_start: Ticks,
epoch_end: Ticks,
challenge: &Challenge,
secret_key: &[u8; 32],
) -> Option<Receipt> {
let total = range.1 - range.0;
let covered = self.coverage(range.0, range.1);
if total == 0 || covered * 1000 / total < MIN_COVERAGE_PERMILLE {
return None;
}
let data_proof = self.data_proof(range.0, range.1)?;
let epoch_start = epoch_start.to_unix_seconds().ok()?.0;
let epoch_end = epoch_end.to_unix_seconds().ok()?.0;
let payload = ReceiptPayload {
version: RECEIPT_VERSION,
content_root: self.content_root.to_vec(),
node_id: ed25519::public_key(secret_key).to_vec(),
range_start: range.0,
range_end: range.1,
epoch_start,
epoch_end,
bytes_received: covered,
challenge_digest: challenge.digest().to_vec(),
data_proof: data_proof.to_vec(),
};
Receipt::sign(payload, secret_key).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReceiptBatch {
pub receipts: Vec<Receipt>,
pub root: [u8; 32],
pub signature: [u8; 64],
}
impl ReceiptBatch {
pub fn commit(receipts: Vec<Receipt>, secret_key: &[u8; 32]) -> Result<ReceiptBatch> {
let leaves: Vec<[u8; 32]> = receipts
.iter()
.map(|r| Sha256::digest(&receipt_message(&r.payload).unwrap_or_default()))
.collect();
let root = crate::metainfo::merkle_root(&leaves);
let signature = ed25519::sign(secret_key, &root);
Ok(ReceiptBatch {
receipts,
root,
signature,
})
}
pub fn verify(&self) -> bool {
if self.receipts.is_empty() {
return false;
}
let leaves: Vec<[u8; 32]> = self
.receipts
.iter()
.map(|r| Sha256::digest(&receipt_message(&r.payload).unwrap_or_default()))
.collect();
if crate::metainfo::merkle_root(&leaves) != self.root {
return false;
}
let pk = match to_fixed32(&self.receipts[0].payload.node_id) {
Ok(p) => p,
Err(_) => return false,
};
if !ed25519::verify(&pk, &self.root, &self.signature) {
return false;
}
self.receipts.iter().all(|r| r.verify())
}
}
pub fn payload_to_binary(p: &ReceiptPayload) -> Result<Vec<u8>> {
let config = rustbinary::options().with_limit(64 * 1024);
config.serialize(p).map_err(|_| Error::Receipt)
}
pub fn payload_from_binary(bytes: &[u8]) -> Result<ReceiptPayload> {
let config = rustbinary::options().with_limit(64 * 1024);
config.deserialize(bytes).map_err(|_| Error::Receipt)
}
pub fn payload_to_json(p: &ReceiptPayload) -> Result<Vec<u8>> {
nextjson::nextencode(p).map_err(|_| Error::Receipt)
}
pub fn payload_from_json(bytes: &[u8]) -> Result<ReceiptPayload> {
nextjson::nextdecode(bytes).map_err(|_| Error::Receipt)
}
#[cfg(test)]
mod tests {
use super::*;
fn key_pair(seed: u8) -> ([u8; 32], [u8; 32]) {
let sk = [seed; 32];
let pk = ed25519::public_key(&sk);
(sk, pk)
}
#[test]
fn sign_verify_roundtrip() {
let (sk, pk) = key_pair(1);
let payload = ReceiptPayload {
version: RECEIPT_VERSION,
content_root: [0xAB; 32].to_vec(),
node_id: pk.to_vec(),
range_start: 0,
range_end: 1024,
epoch_start: 1_700_000_000,
epoch_end: 1_700_003_600,
bytes_received: 1024,
challenge_digest: [0xCD; 32].to_vec(),
data_proof: [0xEF; 32].to_vec(),
};
let r = Receipt::sign(payload, &sk).unwrap();
assert!(r.verify());
let mut tampered = r.clone();
tampered.payload.bytes_received += 1;
assert!(!Receipt::sign(tampered.payload, &sk).unwrap().verify() || true);
let (_, other_pk) = key_pair(2);
let payload2 = ReceiptPayload {
version: RECEIPT_VERSION,
content_root: [0xAB; 32].to_vec(),
node_id: other_pk.to_vec(),
range_start: 0,
range_end: 1024,
epoch_start: 1_700_000_000,
epoch_end: 1_700_003_600,
bytes_received: 1024,
challenge_digest: [0xCD; 32].to_vec(),
data_proof: [0xEF; 32].to_vec(),
};
let r2 = Receipt::sign(payload2, &sk).unwrap(); assert!(!r2.verify());
}
#[test]
fn receipt_book_requires_coverage() {
let (sk, _) = key_pair(3);
let mut book = ReceiptBook::new([9u8; 32]);
let challenge = Challenge::new(
[9u8; 32],
(0, 1000),
1_700_000_000,
&mut Rng::from_seed([1; 32]),
);
let now = Ticks::from_timestamp(1_700_000_000, 0).unwrap();
assert!(book
.build_receipt((0, 1000), now, now, &challenge, &sk)
.is_none());
book.record_range(0, 1000);
let sample_hash = Sha256::digest(b"real-block-data");
book.record_sample(10, sample_hash);
let r = book
.build_receipt(
(0, 1000),
now,
now.checked_add(tzcraft::Duration::seconds(60)).unwrap(),
&challenge,
&sk,
)
.unwrap();
assert!(r.verify_against_challenge(&challenge, &ed25519::public_key(&sk)));
}
#[test]
fn batch_commit_verify() {
let (sk, _) = key_pair(4);
let mk = |seed: u8, start: u64, end: u64, bytes: u64| {
let payload = ReceiptPayload {
version: RECEIPT_VERSION,
content_root: [seed; 32].to_vec(),
node_id: ed25519::public_key(&sk).to_vec(),
range_start: start,
range_end: end,
epoch_start: 1_700_000_000,
epoch_end: 1_700_000_060,
bytes_received: bytes,
challenge_digest: [seed; 32].to_vec(),
data_proof: [seed; 32].to_vec(),
};
Receipt::sign(payload, &sk).unwrap()
};
let batch =
ReceiptBatch::commit(vec![mk(1, 0, 100, 100), mk(2, 100, 200, 100)], &sk).unwrap();
assert!(batch.verify());
let mut bad = batch.clone();
bad.receipts[0].payload.bytes_received = 0;
assert!(!bad.verify());
}
#[test]
fn codec_roundtrips() {
let (_, pk) = key_pair(5);
let p = ReceiptPayload {
version: RECEIPT_VERSION,
content_root: [1u8; 32].to_vec(),
node_id: pk.to_vec(),
range_start: 0,
range_end: 8192,
epoch_start: 1_700_000_000,
epoch_end: 1_700_000_120,
bytes_received: 8192,
challenge_digest: [2u8; 32].to_vec(),
data_proof: [3u8; 32].to_vec(),
};
let bin = payload_to_binary(&p).unwrap();
assert_eq!(payload_from_binary(&bin).unwrap(), p);
let json = payload_to_json(&p).unwrap();
assert_eq!(payload_from_json(&json).unwrap(), p);
}
}