Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ package-json.lock
/coverage
/.nyc_output

# Test snapshots
**/__snapshots__/*
*.snap
test_snapshots/

# IDEs and editors
/.idea
.project
Expand Down
50 changes: 47 additions & 3 deletions app/contract/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions app/contract/contracts/Folder/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ 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"]

[dependencies]
soroban-sdk = "23"
hex = "0.4"

[build-dependencies]
blake3 = "1.5"
serde_json = "1.0"

[dev-dependencies]
soroban-sdk = { version = "23", features = ["testutils"] }
Expand Down
96 changes: 96 additions & 0 deletions app/contract/contracts/Folder/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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(|_| {
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(())
}
48 changes: 46 additions & 2 deletions app/contract/contracts/Folder/src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{BytesN, Env, Symbol, Vec};

include!(concat!(env!("OUT_DIR"), "/build_manifest.rs"));

/// Compile-time feature flags for this contract build.
///
Expand Down Expand Up @@ -169,3 +171,45 @@ pub fn event_replay_fields(env: &Env) -> Vec<Symbol> {
}
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 {
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(), &git_bytes),
build_timestamp: BUILD_TIMESTAMP,
source_hash: BytesN::from_array(&Env::default(), &source_bytes),
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,
}
}
24 changes: 24 additions & 0 deletions app/contract/contracts/Folder/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
Loading