From eb0ababa9a7265aef62f7ea161740fbfff63c7d7 Mon Sep 17 00:00:00 2001 From: Noble Date: Thu, 25 Jun 2026 13:08:43 +0000 Subject: [PATCH 1/2] determine contact version --- app/contract/Cargo.lock | 49 ++++++++- app/contract/contracts/Folder/Cargo.toml | 5 + app/contract/contracts/Folder/build.rs | 99 +++++++++++++++++++ app/contract/contracts/Folder/src/metadata.rs | 46 ++++++++- app/contract/contracts/Folder/src/types.rs | 24 +++++ 5 files changed, 218 insertions(+), 5 deletions(-) create mode 100644 app/contract/contracts/Folder/build.rs diff --git a/app/contract/Cargo.lock b/app/contract/Cargo.lock index 226a82ae4..02a9414f5 100644 --- a/app/contract/Cargo.lock +++ b/app/contract/Cargo.lock @@ -6,7 +6,9 @@ version = 4 name = "RustAcademy" version = "0.1.0" dependencies = [ + "blake3", "proptest", + "serde_json", "soroban-sdk", ] @@ -158,6 +160,18 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" + [[package]] name = "autocfg" version = "1.5.0" @@ -188,6 +202,20 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -260,6 +288,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -275,6 +309,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crate-git-revision" version = "0.0.6" @@ -331,7 +374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -816,7 +859,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1261,7 +1304,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] diff --git a/app/contract/contracts/Folder/Cargo.toml b/app/contract/contracts/Folder/Cargo.toml index 59db00406..8a2558c4d 100644 --- a/app/contract/contracts/Folder/Cargo.toml +++ b/app/contract/contracts/Folder/Cargo.toml @@ -7,6 +7,7 @@ license = "MIT OR Apache-2.0" authors = ["RustAcademy Team"] repository = "https://github.com/ RustAcademy/app" readme = "README.md" +build = "build.rs" [lib] crate-type = ["cdylib", "rlib"] @@ -14,6 +15,10 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "23" +[build-dependencies] +blake3 = "1.5" +serde_json = "1.0" + [dev-dependencies] soroban-sdk = { version = "23", features = ["testutils"] } proptest = { version = "1.5.0", default-features = false, features = ["std"] } diff --git a/app/contract/contracts/Folder/build.rs b/app/contract/contracts/Folder/build.rs new file mode 100644 index 000000000..f0fab6dc9 --- /dev/null +++ b/app/contract/contracts/Folder/build.rs @@ -0,0 +1,99 @@ +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir).join("build_manifest.rs"); + + let git_hash = get_git_hash(); + let build_timestamp = get_build_timestamp(); + let source_hash = get_source_hash(); + let schema_version = "1"; + + let manifest_content = format!( + r#" +/// Build-time manifest embedded in the WASM artifact. +/// +/// This metadata is generated at compile time and provides deterministic +/// correlation between deployed WASM artifacts and their source code. +pub const BUILD_MANIFEST: &str = r#"{}"#; + +/// Build timestamp in seconds since UNIX epoch. +pub const BUILD_TIMESTAMP: u64 = {}; + +/// Full git commit hash of the build source. +pub const GIT_HASH: &str = "{}"; + +/// Hash of the source files used to build this WASM (deterministic build verification). +pub const SOURCE_HASH: &str = "{}"; + +/// Schema version for the build manifest format. +pub const BUILD_MANIFEST_SCHEMA_VERSION: u32 = {}; +"#, + serde_json::json!({ + "git_hash": git_hash, + "build_timestamp": build_timestamp, + "source_hash": source_hash, + "schema_version": schema_version, + }), + build_timestamp, + git_hash, + source_hash, + schema_version + ); + + fs::write(&dest_path, manifest_content).unwrap(); + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=src/"); + println!("cargo:rerun-if-changed=Cargo.toml"); +} + +fn get_git_hash() -> String { + let output = Command::new("git") + .args(&["rev-parse", "HEAD"]) + .output() + .unwrap_or_else(|_| { + Command::new("git") + .args(&["rev-parse", "--short", "HEAD"]) + .output() + .unwrap_or_else(|_| panic!("Failed to get git hash")); + }); + + if output.status.success() { + String::from_utf8_lossy(&output.stdout).trim().to_string() + } else { + "unknown".to_string() + } +} + +fn get_build_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +fn get_source_hash() -> String { + let mut hasher = blake3::Hasher::new(); + hash_directory(&mut hasher, Path::new("src")).unwrap_or(()); + hasher.finalize().to_string() +} + +fn hash_directory(hasher: &mut blake3::Hasher, path: &Path) -> std::io::Result<()> { + for entry in fs::read_dir(path)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension().map_or(false, |ext| ext == "rs") { + let contents = fs::read(&path)?; + hasher.update(path.to_string_lossy().as_bytes()); + hasher.update(&contents); + } else if path.is_dir() { + hash_directory(hasher, &path)?; + } + } + Ok(()) +} \ No newline at end of file diff --git a/app/contract/contracts/Folder/src/metadata.rs b/app/contract/contracts/Folder/src/metadata.rs index cf0bde121..2eda3f060 100644 --- a/app/contract/contracts/Folder/src/metadata.rs +++ b/app/contract/contracts/Folder/src/metadata.rs @@ -10,11 +10,13 @@ use crate::{ self, CURRENT_CONTRACT_VERSION, LEGACY_CONTRACT_VERSION, }, types::{ - ContractHealth, DeploymentMetadata, FeatureFlags, SchemaCompatibility, + BuildManifest, ContractHealth, DeploymentMetadata, FeatureFlags, SchemaCompatibility, SupportedVersions, UpgradeState, }, }; -use soroban_sdk::{Env, Symbol, Vec}; +use soroban_sdk::{Address, BytesN, Env, Symbol, Vec}; + +include!(concat!(env!("OUT_DIR"), "/build_manifest.rs")); /// Compile-time feature flags for this contract build. /// @@ -169,3 +171,43 @@ pub fn event_replay_fields(env: &Env) -> Vec { } fields } + +/// Return build-time manifest embedded in the WASM artifact. +/// +/// This metadata enables deterministic correlation between deployed WASM artifacts +/// and their source code. Tooling can use it to verify integrity and detect drift. +/// +/// ## Fields +/// - `git_hash`: Full git commit hash of the source at build time +/// - `build_timestamp`: UNIX epoch timestamp when the WASM was compiled +/// - `source_hash`: Deterministic hash of all Rust source files (BLAKE3) +/// - `schema_version`: Build manifest format version +pub fn build_manifest() -> BuildManifest { + BuildManifest { + git_hash: BytesN::from_array( + &Env::default(), + &hex::decode(GIT_HASH.trim()).unwrap_or_else(|_| [0u8; 32]), + ), + build_timestamp: BUILD_TIMESTAMP, + source_hash: BytesN::from_array( + &Env::default(), + &hex::decode(SOURCE_HASH.trim()).unwrap_or_else(|_| [0u8; 32]), + ), + schema_version: 1, + } +} + +/// Verify that the on-chain WASM hash matches the expected build. +/// +/// Returns `true` if the stored wasm_hash matches the expected source hash, +/// indicating no unauthorized modifications have been deployed. +pub fn verify_artifact_integrity(env: &Env) -> bool { + let stored_wasm = storage::get_wasm_hash(env); + match stored_wasm { + Some(hash) => { + let manifest = build_manifest(); + hash == manifest.source_hash + } + None => true, + } +} diff --git a/app/contract/contracts/Folder/src/types.rs b/app/contract/contracts/Folder/src/types.rs index 7537a41aa..4915ab25e 100644 --- a/app/contract/contracts/Folder/src/types.rs +++ b/app/contract/contracts/Folder/src/types.rs @@ -478,3 +478,27 @@ pub enum Role { /// Authorized to resolve disputes across escrows. Arbiter = 3, } + +/// Build-time manifest embedded in the WASM artifact. +/// +/// This metadata is generated at compile time and provides deterministic +/// correlation between deployed WASM artifacts and their source code. +/// +/// ## Invariants +/// +/// - `git_hash` is set to the full commit hash if available, otherwise "unknown" +/// - `build_timestamp` is the UNIX epoch time when the WASM was built +/// - `source_hash` is a deterministic hash of all Rust source files +/// - `schema_version` is the manifest format version (increment on breaking changes) +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BuildManifest { + /// Full git commit hash of the build source. + pub git_hash: BytesN<32>, + /// Build timestamp in seconds since UNIX epoch. + pub build_timestamp: u64, + /// Hash of the source files (first 32 bytes of BLAKE3 hash). + pub source_hash: BytesN<32>, + /// Schema version for the build manifest format. + pub schema_version: u32, +} From 8b969cedbf6ddea36f840ef205b17b625861604e Mon Sep 17 00:00:00 2001 From: Noble Date: Fri, 26 Jun 2026 07:53:25 +0000 Subject: [PATCH 2/2] fixing ci error --- .gitignore | 5 +++++ app/contract/Cargo.lock | 1 + app/contract/contracts/Folder/Cargo.toml | 1 + app/contract/contracts/Folder/build.rs | 9 +++------ app/contract/contracts/Folder/src/metadata.rs | 20 ++++++++++--------- 5 files changed, 21 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index e77b59ce1..af7130931 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,11 @@ package-json.lock /coverage /.nyc_output +# Test snapshots +**/__snapshots__/* +*.snap +test_snapshots/ + # IDEs and editors /.idea .project diff --git a/app/contract/Cargo.lock b/app/contract/Cargo.lock index 02a9414f5..d36c03a78 100644 --- a/app/contract/Cargo.lock +++ b/app/contract/Cargo.lock @@ -7,6 +7,7 @@ name = "RustAcademy" version = "0.1.0" dependencies = [ "blake3", + "hex", "proptest", "serde_json", "soroban-sdk", diff --git a/app/contract/contracts/Folder/Cargo.toml b/app/contract/contracts/Folder/Cargo.toml index 8a2558c4d..db94c5ad9 100644 --- a/app/contract/contracts/Folder/Cargo.toml +++ b/app/contract/contracts/Folder/Cargo.toml @@ -14,6 +14,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] soroban-sdk = "23" +hex = "0.4" [build-dependencies] blake3 = "1.5" diff --git a/app/contract/contracts/Folder/build.rs b/app/contract/contracts/Folder/build.rs index f0fab6dc9..30879169a 100644 --- a/app/contract/contracts/Folder/build.rs +++ b/app/contract/contracts/Folder/build.rs @@ -14,7 +14,7 @@ fn main() { let schema_version = "1"; let manifest_content = format!( - r#" + r##" /// Build-time manifest embedded in the WASM artifact. /// /// This metadata is generated at compile time and provides deterministic @@ -32,7 +32,7 @@ pub const SOURCE_HASH: &str = "{}"; /// Schema version for the build manifest format. pub const BUILD_MANIFEST_SCHEMA_VERSION: u32 = {}; -"#, +"##, serde_json::json!({ "git_hash": git_hash, "build_timestamp": build_timestamp, @@ -57,10 +57,7 @@ fn get_git_hash() -> String { .args(&["rev-parse", "HEAD"]) .output() .unwrap_or_else(|_| { - Command::new("git") - .args(&["rev-parse", "--short", "HEAD"]) - .output() - .unwrap_or_else(|_| panic!("Failed to get git hash")); + panic!("Failed to get git hash"); }); if output.status.success() { diff --git a/app/contract/contracts/Folder/src/metadata.rs b/app/contract/contracts/Folder/src/metadata.rs index 2eda3f060..b6d6795e1 100644 --- a/app/contract/contracts/Folder/src/metadata.rs +++ b/app/contract/contracts/Folder/src/metadata.rs @@ -14,7 +14,7 @@ use crate::{ SupportedVersions, UpgradeState, }, }; -use soroban_sdk::{Address, BytesN, Env, Symbol, Vec}; +use soroban_sdk::{BytesN, Env, Symbol, Vec}; include!(concat!(env!("OUT_DIR"), "/build_manifest.rs")); @@ -183,16 +183,18 @@ pub fn event_replay_fields(env: &Env) -> Vec { /// - `source_hash`: Deterministic hash of all Rust source files (BLAKE3) /// - `schema_version`: Build manifest format version pub fn build_manifest() -> BuildManifest { + let git_bytes = hex::decode(GIT_HASH.trim()) + .ok() + .and_then(|v| v.try_into().ok()) + .unwrap_or([0u8; 32]); + let source_bytes = hex::decode(SOURCE_HASH.trim()) + .ok() + .and_then(|v| v.try_into().ok()) + .unwrap_or([0u8; 32]); BuildManifest { - git_hash: BytesN::from_array( - &Env::default(), - &hex::decode(GIT_HASH.trim()).unwrap_or_else(|_| [0u8; 32]), - ), + git_hash: BytesN::from_array(&Env::default(), &git_bytes), build_timestamp: BUILD_TIMESTAMP, - source_hash: BytesN::from_array( - &Env::default(), - &hex::decode(SOURCE_HASH.trim()).unwrap_or_else(|_| [0u8; 32]), - ), + source_hash: BytesN::from_array(&Env::default(), &source_bytes), schema_version: 1, } }