use cid::Cid;
use fvm_ipld_encoding::{CBOR, DAG_CBOR, IPLD_RAW};
use fvm_shared::commcid::{FIL_COMMITMENT_SEALED, FIL_COMMITMENT_UNSEALED};
use num_traits::Zero;
use crate::gas::{Gas, GasTimer, GasTracker, PriceList};
use crate::kernel::{ExecutionError, Result};
use crate::syscall_error;
mod cbor;
struct LinkVisitor<'a> {
pub price_list: &'a PriceList,
gas_available: Gas,
gas_remaining: Gas,
links: Vec<Cid>,
}
pub const ALLOWED_CODECS: &[u64] = &[CBOR, DAG_CBOR, IPLD_RAW];
pub const IGNORED_CODECS: &[u64] = &[FIL_COMMITMENT_UNSEALED, FIL_COMMITMENT_SEALED];
const BLAKE2B_256: u64 = 0xb220;
impl<'a> LinkVisitor<'a> {
pub fn new(price_list: &'a PriceList, gas_available: Gas) -> Self {
Self {
price_list,
gas_available,
gas_remaining: gas_available,
links: Vec::new(),
}
}
pub fn finish(mut self) -> Vec<Cid> {
self.links.shrink_to_fit();
self.links
}
pub fn gas_used(&self) -> Gas {
self.gas_available - self.gas_remaining
}
#[cold]
fn out_of_gas(&mut self) -> Result<()> {
self.gas_remaining = Gas::zero();
Err(ExecutionError::OutOfGas)
}
#[inline(always)]
pub fn charge_gas(&mut self, gas: Gas) -> Result<()> {
if self.gas_remaining < gas {
self.out_of_gas()
} else {
self.gas_remaining -= gas;
Ok(())
}
}
pub fn visit_cid(&mut self, cid: &Cid) -> Result<()> {
let codec = cid.codec();
if IGNORED_CODECS.contains(&codec) {
return Ok(());
}
if !ALLOWED_CODECS.contains(&codec) {
return Err(
syscall_error!(NotFound; "block links to CID with forbidden codec {codec}").into(),
);
}
if cid.hash().code() == fvm_shared::IDENTITY_HASH {
return scan_for_links_inner(self, cid.codec(), cid.hash().digest());
}
if cid.hash().code() != BLAKE2B_256 || cid.hash().size() != 32 {
return Err(syscall_error!(
NotFound; "block links to CID with forbidden multihash type (code: {}, len: {})",
cid.hash().code(), cid.hash().size()
)
.into());
}
self.links.push(*cid);
Ok(())
}
}
fn scan_for_links_inner(visitor: &mut LinkVisitor, codec: u64, data: &[u8]) -> Result<()> {
match codec {
DAG_CBOR => cbor::scan_for_reachable_links(visitor, data),
IPLD_RAW | CBOR => Ok(()),
codec => Err(syscall_error!(IllegalCodec; "codec {} not allowed", codec).into()),
}
}
pub fn scan_for_reachable_links(
codec: u64,
data: &[u8],
price_list: &PriceList,
gas_tracker: &GasTracker,
) -> Result<Vec<Cid>> {
let start = GasTimer::start();
let mut visitor = LinkVisitor::new(price_list, gas_tracker.gas_available());
let ret = scan_for_links_inner(&mut visitor, codec, data);
let t = gas_tracker.charge_gas("OnScanIpldLinks", visitor.gas_used())?;
let ret = ret.map(|_| visitor.finish());
t.stop_with(start);
ret
}
#[cfg(test)]
mod test {
use crate::gas::{Gas, GasTracker, price_list_by_network_version};
use crate::kernel::{ExecutionError, Result};
use cid::Cid;
use fvm_ipld_encoding::{CBOR, DAG_CBOR, IPLD_RAW};
use fvm_shared::commcid::FIL_COMMITMENT_UNSEALED;
use fvm_shared::version::NetworkVersion;
use multihash_codetable::{Multihash, MultihashDigest};
use num_traits::Zero;
use serde::{Deserialize, Serialize};
fn scan_for_links(
codec: u64,
data: &[u8],
cbor_field_count: u32,
cbor_link_count: u32,
) -> Result<Vec<Cid>> {
let mut price_list = price_list_by_network_version(NetworkVersion::V21).clone();
price_list.ipld_cbor_scan_per_field = Gas::new(1);
price_list.ipld_cbor_scan_per_cid = Gas::new(1 << 16);
let expected_gas = price_list.ipld_cbor_scan_per_field * cbor_field_count
+ price_list.ipld_cbor_scan_per_cid * cbor_link_count;
let tracker = GasTracker::new(expected_gas, Gas::zero(), false);
let res = super::scan_for_reachable_links(codec, data, &price_list, &tracker);
assert!(
tracker.gas_available().is_zero(),
"expected to run out of gas"
);
res
}
#[derive(Serialize, Deserialize)]
struct Test(u64, Cid, u64);
#[test]
fn skip_raw() {
assert!(
scan_for_links(IPLD_RAW, &[1, 2, 3], 0, 0)
.unwrap()
.is_empty()
);
}
#[test]
fn basic_cbor() {
let test_cid = Cid::new_v1(
IPLD_RAW,
multihash_codetable::Code::Blake2b256.digest(b"foobar"),
);
let data = fvm_ipld_encoding::to_vec(&Test(0, test_cid, 1)).unwrap();
assert_eq!(
vec![test_cid],
scan_for_links(DAG_CBOR, &data, 4, 1).unwrap()
);
assert!(scan_for_links(CBOR, &data, 0, 0).unwrap().is_empty());
assert!(scan_for_links(IPLD_RAW, &data, 0, 0).unwrap().is_empty());
assert!(matches!(
scan_for_links(DAG_CBOR, &data, 4, 0).unwrap_err(),
ExecutionError::OutOfGas
));
}
#[test]
fn recursive_cbor() {
let test_cid = Cid::new_v1(
IPLD_RAW,
multihash_codetable::Code::Blake2b256.digest(b"foobar"),
);
let inlined_data = fvm_ipld_encoding::to_vec(&Test(0, test_cid, 1)).unwrap();
let inline_cid = Cid::new_v1(DAG_CBOR, Multihash::wrap(0, &inlined_data).unwrap());
let data = fvm_ipld_encoding::to_vec(&Test(0, inline_cid, 1)).unwrap();
assert_eq!(
vec![test_cid],
scan_for_links(DAG_CBOR, &data, 8, 2).unwrap()
);
}
#[test]
fn ignores_pieces() {
let test_cid = Cid::new_v1(
FIL_COMMITMENT_UNSEALED,
multihash_codetable::Code::Blake2b256.digest(b"foobar"),
);
let data = fvm_ipld_encoding::to_vec(&Test(0, test_cid, 1)).unwrap();
assert!(scan_for_links(DAG_CBOR, &data, 4, 1).unwrap().is_empty());
}
}