use std::path::Path;
use std::sync::Arc;
use freenet_stdlib::prelude::ContractInstanceId;
use serde::{Deserialize, Serialize};
use super::generator::Corpus;
use super::verifier::Bytes;
pub const BUNDLE_SCHEMA_VERSION: u16 = 1;
const BUNDLE_MAGIC: &[u8; 8] = b"FRNTCNF1";
#[derive(Debug, thiserror::Error)]
pub enum BundleError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("not a conformance bundle (bad magic)")]
BadMagic,
#[error("bundle schema {found} is not supported (this build reads {supported})")]
UnsupportedSchema { found: u16, supported: u16 },
#[error("decode: {0}")]
Decode(String),
#[error("bundle carries no contract code and none was supplied separately")]
MissingCode,
#[error(
"this node's contract store has no code for the bundle's contract (looked \
for {path}). A peer only stores contracts it hosts, so a capture replayed \
on a different node may need the WASM supplied directly."
)]
NotInStore { path: String },
#[error(
"bundle names no contract (code_hash is absent), so the corpus cannot be \
tied to any WASM and replaying it would check an unrelated contract"
)]
UnidentifiedContract,
#[error(
"contract code does not match the bundle: bundle names blake3:{expected}, \
supplied code is blake3:{actual}"
)]
CodeMismatch { expected: String, actual: String },
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Transition {
pub base_state: Vec<u8>,
pub delta: Option<Vec<u8>>,
pub incoming_state: Option<Vec<u8>>,
pub summary: Option<Vec<u8>>,
pub result_state: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReplayBundle {
pub schema_version: u16,
pub code: Option<Vec<u8>>,
pub code_hash: Option<[u8; 32]>,
pub parameters: Vec<u8>,
pub instance: Option<ContractInstanceId>,
pub states: Vec<Vec<u8>>,
pub deltas: Vec<Vec<u8>>,
pub summaries: Vec<Vec<u8>>,
pub transitions: Vec<Transition>,
pub related: Vec<(ContractInstanceId, Vec<u8>)>,
pub note: Option<String>,
}
impl ReplayBundle {
pub fn new(code: Vec<u8>, parameters: Vec<u8>) -> Self {
let code_hash = Some(*blake3::hash(&code).as_bytes());
Self {
schema_version: BUNDLE_SCHEMA_VERSION,
code: Some(code),
code_hash,
parameters,
instance: None,
states: Vec::new(),
deltas: Vec::new(),
summaries: Vec::new(),
transitions: Vec::new(),
related: Vec::new(),
note: None,
}
}
pub fn resolve_code_from_store(&self, store: &Path) -> Result<Vec<u8>, BundleError> {
let Some(hash) = self.code_hash else {
return Err(BundleError::UnidentifiedContract);
};
let path = store
.join(freenet_stdlib::prelude::CodeHash::new(hash).encode())
.with_extension("wasm");
if !path.exists() {
return Err(BundleError::NotInStore {
path: path.display().to_string(),
});
}
let (code, _version) =
freenet_stdlib::prelude::ContractCode::load_versioned_from_path(&path)
.map_err(|e| BundleError::Decode(e.to_string()))?;
let code = code.data().to_vec();
let actual = *blake3::hash(&code).as_bytes();
if actual != hash {
return Err(BundleError::CodeMismatch {
expected: hex::encode(&hash[..8]),
actual: hex::encode(&actual[..8]),
});
}
Ok(code)
}
pub fn resolve_code(&self, supplied: Option<Vec<u8>>) -> Result<Vec<u8>, BundleError> {
let Some(expected) = self.code_hash else {
return Err(BundleError::UnidentifiedContract);
};
let code = match supplied.or_else(|| self.code.clone()) {
Some(code) => code,
None => return Err(BundleError::MissingCode),
};
let actual = *blake3::hash(&code).as_bytes();
if actual != expected {
return Err(BundleError::CodeMismatch {
expected: hex::encode(&expected[..8]),
actual: hex::encode(&actual[..8]),
});
}
Ok(code)
}
pub fn total_bytes(&self) -> usize {
self.code.as_ref().map_or(0, Vec::len)
+ self.states.iter().map(Vec::len).sum::<usize>()
+ self.deltas.iter().map(Vec::len).sum::<usize>()
+ self.summaries.iter().map(Vec::len).sum::<usize>()
+ self
.transitions
.iter()
.map(|t| {
t.base_state.len()
+ t.result_state.len()
+ t.delta.as_ref().map_or(0, Vec::len)
+ t.incoming_state.as_ref().map_or(0, Vec::len)
+ t.summary.as_ref().map_or(0, Vec::len)
})
.sum::<usize>()
}
pub fn to_corpus(&self) -> Corpus {
let mut states: Vec<Bytes> = self
.states
.iter()
.map(|s| Arc::from(s.as_slice()))
.collect();
let mut deltas: Vec<Bytes> = self
.deltas
.iter()
.map(|d| Arc::from(d.as_slice()))
.collect();
let mut delta_bases: Vec<Option<Bytes>> = vec![None; deltas.len()];
let mut summaries: Vec<Bytes> = self
.summaries
.iter()
.map(|s| Arc::from(s.as_slice()))
.collect();
let mut steps: Vec<(Bytes, Bytes)> = Vec::with_capacity(self.transitions.len());
for transition in &self.transitions {
states.push(Arc::from(transition.base_state.as_slice()));
states.push(Arc::from(transition.result_state.as_slice()));
steps.push((
Arc::from(transition.base_state.as_slice()),
Arc::from(transition.result_state.as_slice()),
));
if let Some(state) = &transition.incoming_state {
states.push(Arc::from(state.as_slice()));
}
if let Some(delta) = &transition.delta {
deltas.push(Arc::from(delta.as_slice()));
delta_bases.push(Some(Arc::from(transition.base_state.as_slice())));
}
if let Some(summary) = &transition.summary {
summaries.push(Arc::from(summary.as_slice()));
}
}
let related = self
.related
.iter()
.map(|(id, state)| {
(
*id,
Some(freenet_stdlib::prelude::State::from(state.clone())),
)
})
.collect::<std::collections::HashMap<_, _>>();
Corpus {
delta_bases,
states,
deltas,
summaries,
transitions: steps,
related: freenet_stdlib::prelude::RelatedContracts::from(related),
}
.deduplicated()
}
pub fn encode(&self) -> Result<Vec<u8>, BundleError> {
let mut out = Vec::with_capacity(self.total_bytes() + 64);
out.extend_from_slice(BUNDLE_MAGIC);
out.extend_from_slice(&self.schema_version.to_le_bytes());
let body = bincode::serialize(self).map_err(|e| BundleError::Decode(e.to_string()))?;
out.extend_from_slice(&body);
Ok(out)
}
pub fn decode(bytes: &[u8]) -> Result<Self, BundleError> {
if bytes.len() < BUNDLE_MAGIC.len() + 2 || &bytes[..BUNDLE_MAGIC.len()] != BUNDLE_MAGIC {
return Err(BundleError::BadMagic);
}
let version = u16::from_le_bytes([bytes[8], bytes[9]]);
if version != BUNDLE_SCHEMA_VERSION {
return Err(BundleError::UnsupportedSchema {
found: version,
supported: BUNDLE_SCHEMA_VERSION,
});
}
bincode::deserialize(&bytes[10..]).map_err(|e| BundleError::Decode(e.to_string()))
}
pub fn write_to(&self, path: &Path) -> Result<(), BundleError> {
let bytes = self.encode()?;
let temporary = path.with_extension("bundle.tmp");
std::fs::write(&temporary, bytes)?;
if let Err(err) = std::fs::rename(&temporary, path) {
drop(std::fs::remove_file(&temporary));
return Err(err.into());
}
Ok(())
}
pub fn read_from(path: &Path) -> Result<Self, BundleError> {
Self::decode(&std::fs::read(path)?)
}
}
#[cfg(test)]
mod store_identity_tests {
use super::*;
#[test]
fn a_store_file_whose_bytes_do_not_match_its_name_is_refused() {
let dir = tempfile::TempDir::new().expect("tempdir");
let real = b"the bytes actually on disk".to_vec();
let claimed = *blake3::hash(b"a completely different contract").as_bytes();
let code = freenet_stdlib::prelude::ContractCode::from(real);
let path = dir
.path()
.join(freenet_stdlib::prelude::CodeHash::new(claimed).encode())
.with_extension("wasm");
let versioned = code
.to_bytes_versioned(freenet_stdlib::prelude::APIVersion::Version0_0_1)
.expect("versioned encoding");
std::fs::write(&path, versioned).expect("write versioned code");
let bundle = ReplayBundle {
schema_version: BUNDLE_SCHEMA_VERSION,
code: None,
code_hash: Some(claimed),
parameters: Vec::new(),
instance: None,
states: Vec::new(),
deltas: Vec::new(),
summaries: Vec::new(),
transitions: Vec::new(),
related: Vec::new(),
note: None,
};
match bundle.resolve_code_from_store(dir.path()) {
Err(BundleError::CodeMismatch { .. }) => {}
other => panic!(
"a store file whose contents do not hash to its name was accepted: \
{other:?}"
),
}
}
}