Skip to main content

urma_runtime/
disk_plan.rs

1use crate::error::{Context, Error, ensure};
2use crate::publication::PublicPlan;
3use crate::storage;
4use crate::{disk_order::Order, disk_writer::DiskWriter};
5use crate::{
6    node::Node,
7    plan::{PlanLimits, PublicationPlan},
8    planner::Planner,
9    validation::{self, Validation},
10};
11use serde::{Deserialize, Serialize};
12use sha2::{Digest, Sha256};
13use std::{
14    fs::File,
15    io::{Read, Seek, SeekFrom},
16    path::{Path, PathBuf},
17};
18use urma_chain::observation::Chain;
19use urma_core::multipart::Geometry;
20use urma_identity::identity::IdentitySigner;
21use urma_wallet::account::Account;
22
23#[derive(Clone, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct DiskPlan {
26    pub version: u8,
27    pub chain: Chain,
28    pub author: String,
29    pub return_script: String,
30    pub root_txid: String,
31    pub total_fee: u64,
32    pub maximum_fee: u64,
33    pub record_count: u32,
34    pub records_hash: String,
35    pub index_hash: String,
36    #[serde(skip)]
37    directory: PathBuf,
38}
39
40impl DiskPlan {
41    pub const MAX_RECORDS: u32 = Geometry::MAX_NODES;
42    pub(crate) const MAX_PAIR_BYTES: usize = 2 * 1024 * 1024;
43
44    pub fn prepare_multipart(
45        node: &Node,
46        author: &impl IdentitySigner,
47        funder: &Account,
48        reader: &mut impl Read,
49        length: u64,
50        profile: [u8; 8],
51        limits: PlanLimits,
52        directory: &Path,
53    ) -> Result<Self, Error> {
54        let geometry = urma_core::multipart::Geometry::new(length)?;
55        let planner = Planner::streaming(node, author, funder, limits, geometry.nodes())?;
56        let writer = DiskWriter::new(directory, planner)?;
57        writer.prepare(reader, geometry, length, profile)
58    }
59
60    pub(crate) fn from_metadata(
61        metadata: &PublicationPlan,
62        count: u32,
63        records_hash: String,
64        index_hash: String,
65        directory: &Path,
66    ) -> Self {
67        Self {
68            version: 1,
69            chain: metadata.chain,
70            author: metadata.author.clone(),
71            return_script: metadata.return_script.clone(),
72            root_txid: metadata.root_txid.clone(),
73            total_fee: metadata.total_fee,
74            maximum_fee: metadata.maximum_fee,
75            record_count: count,
76            records_hash,
77            index_hash,
78            directory: directory.to_owned(),
79        }
80    }
81
82    pub fn directory(&self) -> &Path {
83        &self.directory
84    }
85
86    pub fn load(directory: &Path) -> Result<Self, Error> {
87        let mut plan: Self =
88            serde_json::from_slice(&storage::read_bounded(&directory.join("plan.json"), 16384)?)?;
89        plan.directory = directory.to_owned();
90        plan.validate()?;
91        Ok(plan)
92    }
93
94    pub fn id(&self) -> Result<String, Error> {
95        Ok(hex::encode(Sha256::digest(serde_json::to_vec(self)?)))
96    }
97
98    pub fn funded_by(&self, funder: &Account) -> Result<bool, Error> {
99        validation::funded_by(&self.return_script, funder)
100    }
101
102    pub fn require_funder(&self, funder: &Account) -> Result<(), Error> {
103        ensure!(
104            self.funded_by(funder)?,
105            "plan returns to another funding account"
106        );
107        Ok(())
108    }
109
110    pub fn record(&self, index: u32) -> Result<PublicPlan, Error> {
111        ensure!(index < self.record_count, "record index exceeds plan");
112        let mut offsets = File::open(self.directory.join("index.bin"))?;
113        offsets.seek(SeekFrom::Start(u64::from(index) * 12))?;
114        let (offset, length) = read_index(&mut offsets)?;
115        let mut records = File::open(self.directory.join("records.bin"))?;
116        records.seek(SeekFrom::Start(offset))?;
117        read_pair(&mut records, length)
118    }
119
120    pub fn validate(&self) -> Result<(), Error> {
121        ensure!(
122            self.version == 1 && self.record_count > 0 && self.record_count <= Self::MAX_RECORDS,
123            "invalid disk plan version or record count"
124        );
125        ensure!(
126            self.maximum_fee > 0 && self.total_fee <= self.maximum_fee,
127            "plan fee budget exceeded"
128        );
129        let mut index = File::open(self.directory.join("index.bin"))?;
130        let mut records = File::open(self.directory.join("records.bin"))?;
131        ensure!(
132            records.metadata()?.len()
133                <= u64::from(self.record_count) * u64::try_from(Self::MAX_PAIR_BYTES)?,
134            "disk plan exceeds bounded record capacity"
135        );
136        ensure!(
137            index.metadata()?.len() == u64::from(self.record_count) * 12,
138            "invalid disk plan index length"
139        );
140        ensure!(
141            digest(&mut index)? == self.index_hash && digest(&mut records)? == self.records_hash,
142            "disk plan digest mismatch"
143        );
144        let mut offset = 0u64;
145        let return_script = validation::return_script(&self.return_script)?;
146        let mut state = Validation::new();
147        let mut order = Order::new();
148        for _ in 0..self.record_count {
149            let (actual, length) = read_index(&mut index)?;
150            ensure!(actual == offset, "noncanonical disk plan offset");
151            let bytes = state.append(
152                &read_pair(&mut records, length)?,
153                self.chain,
154                &self.author,
155                &return_script,
156            )?;
157            order.append(&bytes, state.last_txid()?)?;
158            offset = offset
159                .checked_add(u64::from(length))
160                .context("disk plan offset overflow")?;
161        }
162        ensure!(
163            offset == records.metadata()?.len(),
164            "trailing disk plan bytes"
165        );
166        order.finish(self.record_count)?;
167        state.finish(self.total_fee, &self.root_txid)
168    }
169}
170
171fn read_index(reader: &mut impl Read) -> Result<(u64, u32), Error> {
172    let mut offset = [0u8; 8];
173    let mut length = [0u8; 4];
174    reader.read_exact(&mut offset)?;
175    reader.read_exact(&mut length)?;
176    Ok((u64::from_le_bytes(offset), u32::from_le_bytes(length)))
177}
178
179fn read_pair(reader: &mut impl Read, length: u32) -> Result<PublicPlan, Error> {
180    ensure!(
181        length > 0 && usize::try_from(length)? <= DiskPlan::MAX_PAIR_BYTES,
182        "disk plan record exceeds capacity"
183    );
184    let mut bytes = vec![0; usize::try_from(length)?];
185    reader.read_exact(&mut bytes)?;
186    Ok(serde_json::from_slice(&bytes)?)
187}
188
189fn digest(file: &mut File) -> Result<String, Error> {
190    let hash = urma_io::digest(file)?;
191    file.rewind()?;
192    Ok(hex::encode(hash))
193}