From d6c48e4a9421336095740a4bde16d37d387d9d41 Mon Sep 17 00:00:00 2001 From: 0xf333 <0x333@tuta.io> Date: Mon, 3 Nov 2025 19:31:48 +0200 Subject: [PATCH] feat: added support for the IPFS hash type addressing issue#379 implementation test ------------------- cargo test -p resolc ipfs cargo test -p revive-solc-json-interface ipfs Signed-off-by: 0xf333 <0x333@tuta.io> --- Cargo.lock | 1 + Cargo.toml | 1 + .../llvm-context/src/polkavm/context/build.rs | 4 +- .../llvm-context/src/polkavm/context/mod.rs | 9 +-- crates/llvm-context/src/polkavm/mod.rs | 5 +- crates/resolc/Cargo.toml | 1 + crates/resolc/src/project/contract/mod.rs | 15 +++- crates/resolc/src/resolc/arguments.rs | 9 --- crates/resolc/src/tests/unit/ipfs_metadata.rs | 68 +++++++++++++++++++ crates/resolc/src/tests/unit/mod.rs | 1 + .../input/settings/metadata_hash.rs | 28 +------- .../tests/metadata_ipfs.rs | 8 +++ package-lock.json | 5 -- 13 files changed, 99 insertions(+), 56 deletions(-) create mode 100644 crates/resolc/src/tests/unit/ipfs_metadata.rs create mode 100644 crates/solc-json-interface/tests/metadata_ipfs.rs diff --git a/Cargo.lock b/Cargo.lock index 7e937c39..96954d9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8978,6 +8978,7 @@ dependencies = [ "semver 1.0.27", "serde", "serde_json", + "sha2 0.10.9", "tempfile", "which", ] diff --git a/Cargo.toml b/Cargo.toml index 27498e82..d0ea93ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ once_cell = "1.21" num = "0.4.3" sha1 = "0.10" sha3 = "0.10" +sha2 = "0.10" thiserror = "2.0" which = "8.0" path-slash = "0.2" diff --git a/crates/llvm-context/src/polkavm/context/build.rs b/crates/llvm-context/src/polkavm/context/build.rs index 2cbf2e58..7657564a 100644 --- a/crates/llvm-context/src/polkavm/context/build.rs +++ b/crates/llvm-context/src/polkavm/context/build.rs @@ -10,7 +10,7 @@ pub struct Build { /// The PolkaVM text assembly. pub assembly_text: Option, /// The metadata hash. - pub metadata_hash: Option<[u8; BYTE_LENGTH_WORD]>, + pub metadata_hash: Option>, /// The PolkaVM binary bytecode. pub bytecode: Vec, /// The PolkaVM bytecode hash. Unlinked builds don't have a hash yet. @@ -19,7 +19,7 @@ pub struct Build { impl Build { /// A shortcut constructor. - pub fn new(metadata_hash: Option<[u8; BYTE_LENGTH_WORD]>, bytecode: Vec) -> Self { + pub fn new(metadata_hash: Option>, bytecode: Vec) -> Self { Self { assembly_text: None, metadata_hash, diff --git a/crates/llvm-context/src/polkavm/context/mod.rs b/crates/llvm-context/src/polkavm/context/mod.rs index 4cc8eb92..9756cf0a 100644 --- a/crates/llvm-context/src/polkavm/context/mod.rs +++ b/crates/llvm-context/src/polkavm/context/mod.rs @@ -260,7 +260,7 @@ impl<'ctx> Context<'ctx> { pub fn build( self, contract_path: &str, - metadata_hash: Option, + metadata_hash: Option>, ) -> anyhow::Result { self.link_polkavm_exports(contract_path)?; self.link_immutable_data(contract_path)?; @@ -325,12 +325,7 @@ impl<'ctx> Context<'ctx> { self.debug_config.dump_object(contract_path, &object)?; - crate::polkavm::build( - &object, - metadata_hash - .as_ref() - .map(|hash| hash.as_bytes().try_into().unwrap()), - ) + crate::polkavm::build(&object, metadata_hash) } /// Verifies the current LLVM IR module. diff --git a/crates/llvm-context/src/polkavm/mod.rs b/crates/llvm-context/src/polkavm/mod.rs index 8160e00e..f7e5d125 100644 --- a/crates/llvm-context/src/polkavm/mod.rs +++ b/crates/llvm-context/src/polkavm/mod.rs @@ -25,10 +25,7 @@ pub mod context; pub mod evm; /// Get a [Build] from contract bytecode and its auxilliary data. -pub fn build( - bytecode: &[u8], - metadata_hash: Option<[u8; BYTE_LENGTH_WORD]>, -) -> anyhow::Result { +pub fn build(bytecode: &[u8], metadata_hash: Option>) -> anyhow::Result { Ok(Build::new(metadata_hash, bytecode.to_owned())) } diff --git a/crates/resolc/Cargo.toml b/crates/resolc/Cargo.toml index 7a2758cb..e75f262d 100644 --- a/crates/resolc/Cargo.toml +++ b/crates/resolc/Cargo.toml @@ -30,6 +30,7 @@ serde = { workspace = true } serde_json = { workspace = true } which = { workspace = true } normpath = { workspace = true } +sha2 = { workspace = true } revive-common = { workspace = true } revive-llvm-context = { workspace = true } diff --git a/crates/resolc/src/project/contract/mod.rs b/crates/resolc/src/project/contract/mod.rs index e428daf6..46634d60 100644 --- a/crates/resolc/src/project/contract/mod.rs +++ b/crates/resolc/src/project/contract/mod.rs @@ -16,6 +16,7 @@ use revive_llvm_context::PolkaVMContextYulData; use revive_solc_json_interface::SolcStandardJsonInputSettingsPolkaVMMemory; use serde::Deserialize; use serde::Serialize; +use sha2::Digest; use revive_llvm_context::PolkaVMWriteLLVM; @@ -84,8 +85,18 @@ impl Contract { let metadata_json = serde_json::to_value(&metadata).expect("Always valid"); let metadata_json_bytes = serde_json::to_vec(&metadata_json).expect("Always valid"); let metadata_bytes = match metadata_hash { - MetadataHash::Keccak256 => Keccak256::from_slice(&metadata_json_bytes).into(), - MetadataHash::IPFS => todo!("IPFS hash isn't supported yet"), + MetadataHash::Keccak256 => { + let digest = Keccak256::from_slice(&metadata_json_bytes); + Some(digest.to_vec()) + } + MetadataHash::IPFS => { + // IPFS multihash: 0x12 (sha2-256) 0x20 (32 bytes) + sha2-256 digest + let digest = sha2::Sha256::digest(&metadata_json_bytes); + let mut mh = Vec::with_capacity(34); + mh.extend_from_slice(&[0x12, 0x20]); + mh.extend_from_slice(&digest); + Some(mh) + } MetadataHash::None => None, }; debug_config.set_contract_path(&self.identifier.full_path); diff --git a/crates/resolc/src/resolc/arguments.rs b/crates/resolc/src/resolc/arguments.rs index 58fc161c..6d93a398 100644 --- a/crates/resolc/src/resolc/arguments.rs +++ b/crates/resolc/src/resolc/arguments.rs @@ -6,7 +6,6 @@ use std::path::PathBuf; use clap::Parser; use path_slash::PathExt; -use revive_common::MetadataHash; use revive_solc_json_interface::SolcStandardJsonOutputError; /// Compiles the provided Solidity input files (or use the standard input if no files @@ -226,14 +225,6 @@ impl Arguments { )); } - if self.metadata_hash == Some(MetadataHash::IPFS.to_string()) { - messages.push(SolcStandardJsonOutputError::new_error( - "`IPFS` metadata hash type is not supported. Please use `keccak256` instead.", - None, - None, - )); - } - let modes = [ self.yul, self.combined_json.is_some(), diff --git a/crates/resolc/src/tests/unit/ipfs_metadata.rs b/crates/resolc/src/tests/unit/ipfs_metadata.rs new file mode 100644 index 00000000..47690802 --- /dev/null +++ b/crates/resolc/src/tests/unit/ipfs_metadata.rs @@ -0,0 +1,68 @@ +use std::path::PathBuf; + +use revive_common::MetadataHash; +use revive_llvm_context::{initialize_llvm, DebugConfig, OptimizerSettings, PolkaVMTarget}; +use revive_solc_json_interface::SolcStandardJsonInputSettingsLibraries; + +use crate::process::native_process::EXECUTABLE; +use crate::project::Project; +use crate::DEFAULT_EXECUTABLE_NAME; + +#[test] +fn compiles_with_ipfs_metadata_hash_and_emits_multihash() { + let debug = DebugConfig::new(None, true); + + let resolc_path = std::env::var("CARGO_BIN_EXE_resolc") + .ok() + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(DEFAULT_EXECUTABLE_NAME)); + let _ = EXECUTABLE.set(resolc_path); + + initialize_llvm(PolkaVMTarget::PVM, DEFAULT_EXECUTABLE_NAME, &[]); + let project = Project::try_from_yul_paths( + &[PathBuf::from("src/tests/data/yul/Test.yul")], + None, + SolcStandardJsonInputSettingsLibraries::default(), + &debug, + ) + .expect("project from yul"); + + let mut messages = Vec::new(); + let build = project + .compile( + &mut messages, + OptimizerSettings::none(), + MetadataHash::IPFS, + &debug, + &[], + Default::default(), + ) + .expect("compile should succeed"); + + assert!( + messages.is_empty(), + "No errors expected, got: {:?}", + messages + ); + + let (.., result) = build + .results + .into_iter() + .next() + .expect("one contract result"); + let contract = result.expect("contract built successfully"); + + let bytes = contract + .build + .metadata_hash + .as_ref() + .expect("metadata hash should be present"); + let slice: &[u8] = &bytes[..]; + + assert_eq!(slice.len(), 34, "multihash length must be 34 bytes"); + assert_eq!( + &slice[0..2], + &[0x12, 0x20], + "multihash prefix must be sha2-256 (0x12 0x20)" + ); +} diff --git a/crates/resolc/src/tests/unit/mod.rs b/crates/resolc/src/tests/unit/mod.rs index 48b3b4fa..2b9d06b5 100644 --- a/crates/resolc/src/tests/unit/mod.rs +++ b/crates/resolc/src/tests/unit/mod.rs @@ -1,6 +1,7 @@ //! The Solidity compiler unit tests. mod factory_dependency; +mod ipfs_metadata; mod ir_artifacts; mod libraries; mod messages; diff --git a/crates/solc-json-interface/src/standard_json/input/settings/metadata_hash.rs b/crates/solc-json-interface/src/standard_json/input/settings/metadata_hash.rs index cbd569db..58475618 100644 --- a/crates/solc-json-interface/src/standard_json/input/settings/metadata_hash.rs +++ b/crates/solc-json-interface/src/standard_json/input/settings/metadata_hash.rs @@ -1,29 +1,3 @@ //! The metadata hash mode. -use std::str::FromStr; - -use serde::Deserialize; -use serde::Serialize; - -/// The metadata hash mode. -#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Hash)] -pub enum MetadataHash { - /// Do not include bytecode hash. - #[serde(rename = "none")] - None, - /// The default keccak256 hash. - #[serde(rename = "keccak256")] - Keccak256, -} - -impl FromStr for MetadataHash { - type Err = anyhow::Error; - - fn from_str(string: &str) -> Result { - match string { - "none" => Ok(Self::None), - "keccak256" => Ok(Self::Keccak256), - _ => anyhow::bail!("Unknown bytecode hash mode: `{}`", string), - } - } -} +pub use revive_common::MetadataHash; diff --git a/crates/solc-json-interface/tests/metadata_ipfs.rs b/crates/solc-json-interface/tests/metadata_ipfs.rs new file mode 100644 index 00000000..8a996450 --- /dev/null +++ b/crates/solc-json-interface/tests/metadata_ipfs.rs @@ -0,0 +1,8 @@ +use revive_solc_json_interface::SolcStandardJsonInputSettingsMetadataHash; + +#[test] +fn accepts_ipfs_metadata_hash_in_standard_json() { + let parsed: SolcStandardJsonInputSettingsMetadataHash = + serde_json::from_str("\"ipfs\"").expect("should deserialize 'ipfs'"); + assert_eq!(parsed, SolcStandardJsonInputSettingsMetadataHash::IPFS); +} diff --git a/package-lock.json b/package-lock.json index 3939dc2b..b817cac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1585,7 +1585,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz", "integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.32.1", "@typescript-eslint/types": "8.32.1", @@ -1753,7 +1752,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2363,7 +2361,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -2522,7 +2519,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@ethersproject/abi": "5.8.0", "@ethersproject/abstract-provider": "5.8.0", @@ -4397,7 +4393,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"