Skip to content

Commit

Permalink
migrate crates to ext
Browse files Browse the repository at this point in the history
  • Loading branch information
cryptoAtwill committed Jan 17, 2025
1 parent 18f32ca commit d237baa
Show file tree
Hide file tree
Showing 55 changed files with 9,211 additions and 110 deletions.
682 changes: 581 additions & 101 deletions Cargo.lock

Large diffs are not rendered by default.

13 changes: 5 additions & 8 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ members = [

# merkle
"ext/merkle-tree-rs",
"ext/gcra-rs",
"ext/libp2p-bitswap",
"ext/frc42_dispatch",

# ipc
"ipc/cli",
Expand Down Expand Up @@ -81,7 +84,6 @@ fnv = "1.0"
futures = "0.3"
futures-core = "0.3"
futures-util = "0.3"
gcra = "0.4"
hex = "0.4"
hex-literal = "0.4.1"
http = "0.2.12"
Expand Down Expand Up @@ -113,8 +115,7 @@ libp2p = { version = "0.53", default-features = false, features = [
"plaintext",
] }
libp2p-mplex = { version = "0.41" }
# libp2p-bitswap = "0.25.1"
libp2p-bitswap = { git = "https://github.com/consensus-shipyard/libp2p-bitswap.git", branch = "chore-upgrade-libipld" } # Updated to libipld 0.16
libp2p-bitswap = { path = "ext/libp2p-bitswap" }
libsecp256k1 = "0.7"
literally = "0.1.3"
log = "0.4"
Expand Down Expand Up @@ -230,7 +231,7 @@ cid = { version = "0.10.1", default-features = false, features = [
"std",
] }

frc42_dispatch = { git = "https://github.com/filecoin-project/actors-utils", rev = "0f8365151f44785f7bead4e5500258fd5c8944e6" }
frc42_dispatch = { path = "./ext/frc42_dispatch" }

# Using the same tendermint-rs dependency as tower-abci. From both we are interested in v037 modules.
tower-abci = { version = "0.7" }
Expand All @@ -244,10 +245,6 @@ tendermint-rpc = { version = "0.31", features = [
] }
tendermint-proto = { version = "0.31" }

[patch.crates-io]
# Use stable-only features.
gcra = { git = "https://github.com/consensus-shipyard/gcra-rs.git", branch = "main" }

[profile.wasm]
inherits = "release"
panic = "abort"
Expand Down
22 changes: 22 additions & 0 deletions ext/frc42_dispatch/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "frc42_dispatch"
description = "Filecoin FRC-0042 calling convention/dispatch support library"
version = "7.0.0"
license = "MIT OR Apache-2.0"
keywords = ["filecoin", "dispatch", "frc-0042"]
repository = "https://github.com/helix-onchain/filecoin/"
edition = "2021"


[dependencies]
fvm_ipld_encoding = { workspace = true }
fvm_sdk = { workspace = true, optional = true }
fvm_shared = { workspace = true }
frc42_hasher = { version = "5.0.0", path = "hasher" }
frc42_macros = { version = "5.0.0", path = "macros" }
thiserror = { version = "1.0.31" }

[features]
# disable default features to avoid dependence on fvm_sdk (for proc macro and similar purposes)
default = ["use_sdk"]
use_sdk = ["dep:fvm_sdk"]
5 changes: 5 additions & 0 deletions ext/frc42_dispatch/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# frc42_dispatch

Helper library to work with [FRC-0042](https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md) method hashing

There's an example of it in use [here](https://github.com/helix-onchain/filecoin/tree/main/dispatch_examples/greeter)
27 changes: 27 additions & 0 deletions ext/frc42_dispatch/generate_hashes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from hashlib import blake2b

# See https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0042.md#method-number-computation
def method_number(name):
name = '1|' + name
hash = blake2b(name.encode('ascii'), digest_size=64)
#print('digest: ' + hash.hexdigest())
#print(f'{len(hash.digest())} bytes long')

digest = hash.digest()
while digest:
chunk = digest[:4]
num = int.from_bytes(chunk, byteorder='big')
if num >= 1<<24:
return num
digest = digest[4:]
raise Exception("Method ID could not be determined, please change it")


# these are all the method names used in the example token actor
methods = ['Name', 'Symbol', 'TotalSupply', 'BalanceOf', 'Allowance', 'IncreaseAllowance',
'DecreaseAllowance', 'RevokeAllowance', 'Burn', 'TransferFrom', 'Transfer', 'Mint']
for method in methods:
num = method_number(method)
#print(f'{num:08x}\t{method}')
# print out Rust code for use in a test
print(f'assert_eq!(method_hash!("{method}"), 0x{num:08x});')
17 changes: 17 additions & 0 deletions ext/frc42_dispatch/hasher/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "frc42_hasher"
version = "5.0.0"
license = "MIT OR Apache-2.0"
description = "Filecoin FRC-0042 calling convention method hashing"
repository = "https://github.com/helix-onchain/filecoin/"
edition = "2021"

[dependencies]
fvm_sdk = { workspace = true, optional = true }
fvm_shared = { workspace = true, optional = true }
thiserror = { version = "1.0.31" }

[features]
# The fvm dependencies are optional. Useful for proc macro and similar purposes.
default = ["use_sdk"]
use_sdk = ["dep:fvm_sdk", "dep:fvm_shared"]
230 changes: 230 additions & 0 deletions ext/frc42_dispatch/hasher/src/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
use thiserror::Error;

/// Minimal interface for a hashing function.
///
/// [`Hasher::hash()`] must return a digest that is at least 4 bytes long so that it can be cast to
/// a [`u32`].
pub trait Hasher {
/// For an input of bytes return a digest that is at least 4 bytes long.
fn hash(&self, bytes: &[u8]) -> Vec<u8>;
}

/// Hasher that uses the blake2b hash syscall provided by the FVM.
#[cfg(feature = "use_sdk")]
#[derive(Default)]
pub struct Blake2bSyscall {}

#[cfg(feature = "use_sdk")]
impl Hasher for Blake2bSyscall {
// fvm_sdk dependence can be removed by setting default-features to false
fn hash(&self, bytes: &[u8]) -> Vec<u8> {
use fvm_shared::crypto::hash::SupportedHashes;
fvm_sdk::crypto::hash_owned(SupportedHashes::Blake2b512, bytes)
}
}

/// Uses an underlying hashing function (blake2b by convention) to generate method numbers from
/// method names.
#[derive(Default)]
pub struct MethodResolver<T: Hasher> {
hasher: T,
}

#[derive(Error, PartialEq, Eq, Debug)]
pub enum MethodNameErr {
#[error("empty method name provided")]
EmptyString,
#[error("method name does not conform to the FRC-0042 convention {0}")]
IllegalName(#[from] IllegalNameErr),
#[error("unable to calculate method id, choose a another method name")]
IndeterminableId,
}

#[derive(Error, PartialEq, Eq, Debug)]
pub enum IllegalNameErr {
#[error("method name doesn't start with capital letter or _")]
NotValidStart,
#[error("method name contains letters outside [a-zA-Z0-9_]")]
IllegalCharacters,
}

impl<T: Hasher> MethodResolver<T> {
const CONSTRUCTOR_METHOD_NAME: &'static str = "Constructor";
const CONSTRUCTOR_METHOD_NUMBER: u64 = 1_u64;
const FIRST_METHOD_NUMBER: u64 = 1 << 24;
const DIGEST_CHUNK_LENGTH: usize = 4;

/// Creates a [`MethodResolver`] with an instance of a hasher (blake2b by convention).
pub fn new(hasher: T) -> Self {
Self { hasher }
}

/// Generates a standard FRC-0042 compliant method number.
///
/// The method number is calculated as the first four bytes of `hash(method-name)`.
/// The name `Constructor` is always hashed to 1 and other method names that hash to
/// 0 or 1 are avoided via rejection sampling.
pub fn method_number(&self, method_name: &str) -> Result<u64, MethodNameErr> {
check_method_name(method_name)?;

if method_name == Self::CONSTRUCTOR_METHOD_NAME {
return Ok(Self::CONSTRUCTOR_METHOD_NUMBER);
}

let method_name = format!("1|{method_name}");
let digest = self.hasher.hash(method_name.as_bytes());

for chunk in digest.chunks(Self::DIGEST_CHUNK_LENGTH) {
if chunk.len() < Self::DIGEST_CHUNK_LENGTH {
// last chunk may be smaller than 4 bytes
break;
}

let method_id = as_u32(chunk) as u64;
// Method numbers below FIRST_METHOD_NUMBER are reserved for other use
if method_id >= Self::FIRST_METHOD_NUMBER {
return Ok(method_id);
}
}

Err(MethodNameErr::IndeterminableId)
}
}

/// Checks that a method name is valid and compliant with the FRC-0042 standard recommendations.
///
/// - Only ASCII characters in `[a-zA-Z0-9_]` are allowed.
/// - Starts with a character in `[A-Z_]`.
fn check_method_name(method_name: &str) -> Result<(), MethodNameErr> {
if method_name.is_empty() {
return Err(MethodNameErr::EmptyString);
}

// Check starts with capital letter
let first_letter = method_name.chars().next().unwrap(); // safe because we checked for empty string
if !(first_letter.is_ascii_uppercase() || first_letter == '_') {
return Err(IllegalNameErr::NotValidStart.into());
}

// Check that all characters are legal
if !method_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
return Err(IllegalNameErr::IllegalCharacters.into());
}

Ok(())
}

/// Takes a byte array and interprets it as a u32 number.
///
/// Using big-endian order interperets the first four bytes to an int.
///
/// The slice passed to this must be at least length 4.
fn as_u32(bytes: &[u8]) -> u32 {
u32::from_be_bytes(
bytes[0..4]
.try_into()
.expect("bytes was not at least length 4"),
)
}

#[cfg(test)]
mod tests {

use super::{Hasher, IllegalNameErr, MethodNameErr, MethodResolver};

#[derive(Clone, Copy)]
struct FakeHasher {}
impl Hasher for FakeHasher {
fn hash(&self, bytes: &[u8]) -> Vec<u8> {
bytes.to_vec()
}
}

#[test]
fn constructor_is_1() {
let method_hasher = MethodResolver::new(FakeHasher {});
assert_eq!(method_hasher.method_number("Constructor").unwrap(), 1);
}

#[test]
fn normal_method_is_hashed() {
let fake_hasher = FakeHasher {};
let method_hasher = MethodResolver::new(fake_hasher);
// note that the method hashing prepends each name with "1|" as a domain separator
assert_eq!(
method_hasher.method_number("NormalMethod").unwrap(),
super::as_u32(&fake_hasher.hash(b"1|NormalMethod")) as u64
);

assert_eq!(
method_hasher.method_number("NormalMethod2").unwrap(),
super::as_u32(&fake_hasher.hash(b"1|NormalMethod2")) as u64
);
}

#[test]
fn disallows_invalid_method_names() {
let method_hasher = MethodResolver::new(FakeHasher {});
assert_eq!(
method_hasher.method_number("Invalid|Method").unwrap_err(),
MethodNameErr::IllegalName(IllegalNameErr::IllegalCharacters)
);
assert_eq!(
method_hasher.method_number("").unwrap_err(),
MethodNameErr::EmptyString
);
assert_eq!(
method_hasher.method_number("invalidMethod").unwrap_err(),
MethodNameErr::IllegalName(IllegalNameErr::NotValidStart)
);
}

/// Fake hasher that always returns a digest beginning with b"\0\0\0\0".
#[derive(Clone, Copy)]
struct FakeHasher0 {}
impl Hasher for FakeHasher0 {
fn hash(&self, bytes: &[u8]) -> Vec<u8> {
let mut hash: Vec<u8> = vec![0, 0, 0, 0];
let mut suffix = bytes.to_vec();
hash.append(suffix.as_mut());
hash
}
}

/// Fake hasher that always returns a digest beginning with b"\0\0\0\1".
#[derive(Clone, Copy)]
struct FakeHasher1 {}
impl Hasher for FakeHasher1 {
fn hash(&self, bytes: &[u8]) -> Vec<u8> {
let mut hash: Vec<u8> = vec![0, 0, 0, 1];
let mut suffix = bytes.to_vec();
hash.append(suffix.as_mut());
hash
}
}

#[test]
fn avoids_disallowed_method_numbers() {
let hasher_0 = FakeHasher0 {};
let method_hasher_0 = MethodResolver::new(hasher_0);

// This simulates a method name that would hash to 0
let contrived_0 = "MethodName";
let contrived_0_digest = hasher_0.hash(contrived_0.as_bytes());
assert_eq!(super::as_u32(&contrived_0_digest), 0);
// But the method number is not a collision
assert_ne!(method_hasher_0.method_number(contrived_0).unwrap(), 0);

let hasher_1 = FakeHasher1 {};
let method_hasher_1 = MethodResolver::new(hasher_1);
// This simulates a method name that would hash to 1
let contrived_1 = "MethodName";
let contrived_1_digest = hasher_1.hash(contrived_1.as_bytes());
assert_eq!(super::as_u32(&contrived_1_digest), 1);
// But the method number is not a collision
assert_ne!(method_hasher_1.method_number(contrived_1).unwrap(), 1);
}
}
1 change: 1 addition & 0 deletions ext/frc42_dispatch/hasher/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod hash;
20 changes: 20 additions & 0 deletions ext/frc42_dispatch/macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "frc42_macros"
version = "5.0.0"
license = "MIT OR Apache-2.0"
description = "Filecoin FRC-0042 calling convention procedural macros"
repository = "https://github.com/helix-onchain/filecoin/"
edition = "2021"

[lib]
proc-macro = true

[dependencies]
blake2b_simd = { version = "1.0.0" }
frc42_hasher = { version = "5.0.0", path = "../hasher", default-features = false }
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "1.0", features = ["full"] }

[dev-dependencies]
trybuild = "1.0"
8 changes: 8 additions & 0 deletions ext/frc42_dispatch/macros/example/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "example"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
frc42_macros = { version = "5.0.0", path = ".." }
9 changes: 9 additions & 0 deletions ext/frc42_dispatch/macros/example/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use frc42_macros::method_hash;

fn main() {
let str_hash = method_hash!("Method");
println!("String hash: {str_hash:x}");

// this one breaks naming rules and will fail to compile
//println!("error hash: {}", method_hash!("some_function"));
}
Loading

0 comments on commit d237baa

Please sign in to comment.