Skip to content

Commit 296d0bd

Browse files
authored
Merge pull request #374 from OffchainLabs/stylus-tools-project-hash
add project hash implementation to stylus-tools
2 parents 49b065b + 5e6cfa3 commit 296d0bd

File tree

6 files changed

+151
-8
lines changed

6 files changed

+151
-8
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stylus-tools/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ brotli2 = "0.3.2"
3636
bytesize = "1.2.0"
3737
cargo_metadata = "0.20"
3838
escargot = "0.5"
39+
glob = "0.3"
3940
rust-toolchain-file = "0.1"
4041
rustc-host = "0.1"
4142
serde = { version = "1.0", features = ["derive"] }

stylus-tools/src/core/check.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2025, Offchain Labs, Inc.
22
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
33

4-
use std::path::Path;
4+
use std::{env, path::Path};
55

66
use alloy::{primitives::Address, providers::Provider};
77
use bytesize::ByteSize;
@@ -28,6 +28,8 @@ pub struct CheckConfig {
2828

2929
#[derive(Debug, thiserror::Error)]
3030
pub enum CheckError {
31+
#[error("io error: {0}")]
32+
Io(#[from] std::io::Error),
3133
#[error("cargo metadata error: {0}")]
3234
CargoMetadata(#[from] cargo_metadata::Error),
3335

@@ -53,7 +55,8 @@ pub async fn check_contract(
5355
provider: &impl Provider,
5456
) -> Result<ContractStatus, CheckError> {
5557
let wasm_file = build_contract(contract, &config.build)?;
56-
let project_hash = hash_project(&config.project)?;
58+
let dir = env::current_dir()?;
59+
let project_hash = hash_project(dir, &config.project, &config.build)?;
5760
let status = check_wasm_file(&wasm_file, project_hash, address, config, provider).await?;
5861
Ok(status)
5962
}
Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,138 @@
11
// Copyright 2025, Offchain Labs, Inc.
22
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
33

4-
use tiny_keccak::{Hasher, Keccak};
4+
use std::{
5+
fs,
6+
io::Read,
7+
path::{Path, PathBuf},
8+
sync::mpsc,
9+
thread,
10+
};
511

6-
use crate::utils::cargo;
12+
use glob::glob;
13+
use tiny_keccak::{Hasher, Keccak};
714

815
use super::{ProjectConfig, ProjectError};
16+
use crate::{
17+
core::build::{BuildConfig, OptLevel},
18+
utils::{cargo, toolchain::find_toolchain_file},
19+
};
920

1021
pub type ProjectHash = [u8; 32];
1122

12-
pub fn hash_project(_config: &ProjectConfig) -> Result<ProjectHash, ProjectError> {
23+
pub fn hash_project(
24+
dir: impl AsRef<Path>,
25+
config: &ProjectConfig,
26+
build: &BuildConfig,
27+
) -> Result<ProjectHash, ProjectError> {
1328
let cargo_version = cargo::version()?;
1429

1530
let mut keccak = Keccak::v256();
1631
keccak.update(cargo_version.as_bytes());
17-
// TODO: hash project files
32+
if matches!(build.opt_level, OptLevel::Z) {
33+
keccak.update(&[0]);
34+
} else {
35+
keccak.update(&[1]);
36+
}
37+
38+
// Fetch the Rust toolchain toml file from the project root. Assert that it exists and add it to the
39+
// files in the directory to hash.
40+
let toolchain_file_path = find_toolchain_file(dir.as_ref())?;
41+
42+
let mut paths = all_paths(dir, config.source_file_patterns.clone())?;
43+
paths.push(toolchain_file_path);
44+
paths.sort();
45+
46+
// Read the file contents in another thread and process the keccak in the main thread.
47+
let (tx, rx) = mpsc::channel();
48+
thread::spawn(move || {
49+
for filename in paths.iter() {
50+
greyln!(
51+
"File used for deployment hash: {}",
52+
filename.as_os_str().to_string_lossy()
53+
);
54+
tx.send(read_file_preimage(filename))
55+
.expect("failed to send preimage (impossible)");
56+
}
57+
});
58+
for result in rx {
59+
keccak.update(result?.as_slice());
60+
}
61+
62+
let mut project_hash = ProjectHash::default();
63+
keccak.finalize(&mut project_hash);
64+
greyln!(
65+
"project metadata hash computed on deployment: {:?}",
66+
hex::encode(project_hash)
67+
);
68+
Ok(project_hash)
69+
}
70+
71+
fn all_paths(
72+
root_dir: impl AsRef<Path>,
73+
source_file_patterns: Vec<String>,
74+
) -> Result<Vec<PathBuf>, ProjectError> {
75+
let mut files = Vec::<PathBuf>::new();
76+
let mut directories = Vec::<PathBuf>::new();
77+
directories.push(root_dir.as_ref().to_path_buf()); // Using `from` directly
78+
79+
let glob_paths = expand_glob_patterns(source_file_patterns)?;
80+
81+
while let Some(dir) = directories.pop() {
82+
for entry in fs::read_dir(&dir).map_err(|e| ProjectError::DirectoryRead(dir.clone(), e))? {
83+
let entry = entry.map_err(|e| ProjectError::DirectoryEntry(dir.clone(), e))?;
84+
let path = entry.path();
85+
86+
if path.is_dir() {
87+
if path.ends_with("target") || path.ends_with(".git") {
88+
continue; // Skip "target" and ".git" directories
89+
}
90+
directories.push(path);
91+
} else if path.file_name().is_some_and(|f| {
92+
// If the user has has specified a list of source file patterns, check if the file
93+
// matches the pattern.
94+
if !glob_paths.is_empty() {
95+
for glob_path in glob_paths.iter() {
96+
if glob_path == &path {
97+
return true;
98+
}
99+
}
100+
false
101+
} else {
102+
// Otherwise, by default include all rust files, Cargo.toml and Cargo.lock files.
103+
f == "Cargo.toml" || f == "Cargo.lock" || f.to_string_lossy().ends_with(".rs")
104+
}
105+
}) {
106+
files.push(path);
107+
}
108+
}
109+
}
110+
Ok(files)
111+
}
112+
113+
fn expand_glob_patterns(patterns: Vec<String>) -> Result<Vec<PathBuf>, ProjectError> {
114+
let mut files_to_include = Vec::new();
115+
for pattern in patterns {
116+
let paths = glob(&pattern).map_err(|e| ProjectError::GlobPattern(pattern.clone(), e))?;
117+
for path_result in paths {
118+
let path = path_result?;
119+
files_to_include.push(path);
120+
}
121+
}
122+
Ok(files_to_include)
123+
}
18124

19-
Ok(ProjectHash::default())
125+
fn read_file_preimage(filename: &Path) -> Result<Vec<u8>, ProjectError> {
126+
let mut contents = Vec::with_capacity(1024);
127+
{
128+
let filename = filename.as_os_str();
129+
contents.extend_from_slice(&(filename.len() as u64).to_be_bytes());
130+
contents.extend_from_slice(filename.as_encoded_bytes());
131+
}
132+
let mut file = std::fs::File::open(filename)
133+
.map_err(|e| ProjectError::FileOpen(filename.to_path_buf(), e))?;
134+
contents.extend_from_slice(&file.metadata().unwrap().len().to_be_bytes());
135+
file.read_to_end(&mut contents)
136+
.map_err(|e| ProjectError::FileRead(filename.to_path_buf(), e))?;
137+
Ok(contents)
20138
}

stylus-tools/src/core/project/mod.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Copyright 2025, Offchain Labs, Inc.
22
// For licensing, see https://github.com/OffchainLabs/stylus-sdk-rs/blob/main/licenses/COPYRIGHT.md
33

4+
use std::path::PathBuf;
5+
46
pub use hash::{hash_project, ProjectHash};
57
pub use init::{init_contract, init_workspace, InitError};
68
pub use new::{new_contract, new_workspace};
@@ -25,6 +27,24 @@ pub enum ProjectKind {
2527

2628
#[derive(Debug, thiserror::Error)]
2729
pub enum ProjectError {
30+
#[error("io error: {0}")]
31+
Io(#[from] std::io::Error),
32+
#[error("Error processing path: {0}")]
33+
Glob(#[from] glob::GlobError),
34+
2835
#[error("{0}")]
2936
Command(#[from] crate::error::CommandError),
37+
#[error("rust toolchain error: {0}")]
38+
Toolchain(#[from] crate::utils::toolchain::ToolchainError),
39+
40+
#[error("Unable to read directory {dir}: {1}", dir = .0.display())]
41+
DirectoryRead(PathBuf, std::io::Error),
42+
#[error("Error finding file in {dir}: {1}", dir = .0.display())]
43+
DirectoryEntry(PathBuf, std::io::Error),
44+
#[error("Failed to read glob pattern '{0}': {1}")]
45+
GlobPattern(String, glob::PatternError),
46+
#[error("failed to open file {filename}: {1}", filename = .0.display())]
47+
FileOpen(PathBuf, std::io::Error),
48+
#[error("failed to read file {filename}: {1}", filename = .0.display())]
49+
FileRead(PathBuf, std::io::Error),
3050
}

stylus-tools/src/utils/toolchain.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub fn get_toolchain_channel(package: &Package) -> Result<String, ToolchainError
4141
Ok(channel)
4242
}
4343

44-
fn find_toolchain_file(dir: impl Into<PathBuf>) -> Result<PathBuf, ToolchainError> {
44+
pub fn find_toolchain_file(dir: impl Into<PathBuf>) -> Result<PathBuf, ToolchainError> {
4545
let mut path = dir.into();
4646
while !path.join(TOOLCHAIN_FILE_NAME).exists() {
4747
path = path.parent().ok_or(ToolchainError::NotFound)?.to_path_buf();

0 commit comments

Comments
 (0)