use crate::error::{Context, Error, ensure};
use crate::publication::PublicPlan;
use crate::storage;
use crate::{disk_order::Order, disk_writer::DiskWriter};
use crate::{
node::Node,
plan::{PlanLimits, PublicationPlan},
planner::Planner,
validation::{self, Validation},
};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{
fs::File,
io::{Read, Seek, SeekFrom},
path::{Path, PathBuf},
};
use urma_chain::observation::Chain;
use urma_core::multipart::Geometry;
use urma_identity::identity::IdentitySigner;
use urma_wallet::account::Account;
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DiskPlan {
pub version: u8,
pub chain: Chain,
pub author: String,
pub return_script: String,
pub root_txid: String,
pub total_fee: u64,
pub maximum_fee: u64,
pub record_count: u32,
pub records_hash: String,
pub index_hash: String,
#[serde(skip)]
directory: PathBuf,
}
impl DiskPlan {
pub const MAX_RECORDS: u32 = Geometry::MAX_NODES;
pub(crate) const MAX_PAIR_BYTES: usize = 2 * 1024 * 1024;
pub fn prepare_multipart(
node: &Node,
author: &impl IdentitySigner,
funder: &Account,
reader: &mut impl Read,
length: u64,
profile: [u8; 8],
limits: PlanLimits,
directory: &Path,
) -> Result<Self, Error> {
let geometry = urma_core::multipart::Geometry::new(length)?;
let planner = Planner::streaming(node, author, funder, limits, geometry.nodes())?;
let writer = DiskWriter::new(directory, planner)?;
writer.prepare(reader, geometry, length, profile)
}
pub(crate) fn from_metadata(
metadata: &PublicationPlan,
count: u32,
records_hash: String,
index_hash: String,
directory: &Path,
) -> Self {
Self {
version: 1,
chain: metadata.chain,
author: metadata.author.clone(),
return_script: metadata.return_script.clone(),
root_txid: metadata.root_txid.clone(),
total_fee: metadata.total_fee,
maximum_fee: metadata.maximum_fee,
record_count: count,
records_hash,
index_hash,
directory: directory.to_owned(),
}
}
pub fn directory(&self) -> &Path {
&self.directory
}
pub fn load(directory: &Path) -> Result<Self, Error> {
let mut plan: Self =
serde_json::from_slice(&storage::read_bounded(&directory.join("plan.json"), 16384)?)?;
plan.directory = directory.to_owned();
plan.validate()?;
Ok(plan)
}
pub fn id(&self) -> Result<String, Error> {
Ok(hex::encode(Sha256::digest(serde_json::to_vec(self)?)))
}
pub fn funded_by(&self, funder: &Account) -> Result<bool, Error> {
validation::funded_by(&self.return_script, funder)
}
pub fn require_funder(&self, funder: &Account) -> Result<(), Error> {
ensure!(
self.funded_by(funder)?,
"plan returns to another funding account"
);
Ok(())
}
pub fn record(&self, index: u32) -> Result<PublicPlan, Error> {
ensure!(index < self.record_count, "record index exceeds plan");
let mut offsets = File::open(self.directory.join("index.bin"))?;
offsets.seek(SeekFrom::Start(u64::from(index) * 12))?;
let (offset, length) = read_index(&mut offsets)?;
let mut records = File::open(self.directory.join("records.bin"))?;
records.seek(SeekFrom::Start(offset))?;
read_pair(&mut records, length)
}
pub fn validate(&self) -> Result<(), Error> {
ensure!(
self.version == 1 && self.record_count > 0 && self.record_count <= Self::MAX_RECORDS,
"invalid disk plan version or record count"
);
ensure!(
self.maximum_fee > 0 && self.total_fee <= self.maximum_fee,
"plan fee budget exceeded"
);
let mut index = File::open(self.directory.join("index.bin"))?;
let mut records = File::open(self.directory.join("records.bin"))?;
ensure!(
records.metadata()?.len()
<= u64::from(self.record_count) * u64::try_from(Self::MAX_PAIR_BYTES)?,
"disk plan exceeds bounded record capacity"
);
ensure!(
index.metadata()?.len() == u64::from(self.record_count) * 12,
"invalid disk plan index length"
);
ensure!(
digest(&mut index)? == self.index_hash && digest(&mut records)? == self.records_hash,
"disk plan digest mismatch"
);
let mut offset = 0u64;
let return_script = validation::return_script(&self.return_script)?;
let mut state = Validation::new();
let mut order = Order::new();
for _ in 0..self.record_count {
let (actual, length) = read_index(&mut index)?;
ensure!(actual == offset, "noncanonical disk plan offset");
let bytes = state.append(
&read_pair(&mut records, length)?,
self.chain,
&self.author,
&return_script,
)?;
order.append(&bytes, state.last_txid()?)?;
offset = offset
.checked_add(u64::from(length))
.context("disk plan offset overflow")?;
}
ensure!(
offset == records.metadata()?.len(),
"trailing disk plan bytes"
);
order.finish(self.record_count)?;
state.finish(self.total_fee, &self.root_txid)
}
}
fn read_index(reader: &mut impl Read) -> Result<(u64, u32), Error> {
let mut offset = [0u8; 8];
let mut length = [0u8; 4];
reader.read_exact(&mut offset)?;
reader.read_exact(&mut length)?;
Ok((u64::from_le_bytes(offset), u32::from_le_bytes(length)))
}
fn read_pair(reader: &mut impl Read, length: u32) -> Result<PublicPlan, Error> {
ensure!(
length > 0 && usize::try_from(length)? <= DiskPlan::MAX_PAIR_BYTES,
"disk plan record exceeds capacity"
);
let mut bytes = vec![0; usize::try_from(length)?];
reader.read_exact(&mut bytes)?;
Ok(serde_json::from_slice(&bytes)?)
}
fn digest(file: &mut File) -> Result<String, Error> {
let hash = urma_io::digest(file)?;
file.rewind()?;
Ok(hex::encode(hash))
}