Skip to content

Feat (cheatcodes): Introducing ipfs cid v0 to calculate file content #10348

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ url = "2"
vergen = { version = "8", default-features = false }
yansi = { version = "1.0", features = ["detect-tty", "detect-env"] }
path-slash = "0.2"
sha2 = "0.10"

[patch.crates-io]
## alloy-core
Expand Down
1 change: 1 addition & 0 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,4 @@ tracing.workspace = true
walkdir.workspace = true
proptest.workspace = true
serde.workspace = true
sha2.workspace=true
20 changes: 20 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

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

4 changes: 4 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,10 @@ interface Vm {
#[cheatcode(group = Filesystem)]
function promptUint(string calldata promptText) external returns (uint256);

/// Calculates the IPFS CID V0 of a file's content.
#[cheatcode(group = Filesystem)]
function ipfsCidV0(string calldata filepath) external returns (bytes32 cid);
Copy link
Member

@DaniPopes DaniPopes Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this have to be a path? It just reads the file then hashes it, we could just accept a bytes directly.

Also, sha256 exists in the EVM itself (precompile 0x00...02), why can this not be a simple utility function written in Solidity where it is needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point — accepting bytes directly would give more flexibility.
It depends on the user though — some might prefer to pass a filepath (especially for testing existing files).

Maybe we can support both?
I'm fine with either approach for now — happy to go with what you think makes the most sense! @sakulstra @grandizzy

Copy link
Contributor

@sakulstra sakulstra Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, too strong opinion.
If it accepted string or similar, we would build our own wrapped which is perfectly fine either.

For us the main usecase is hashing content in external files which is why i suggested path, but i guess there could be usecases that would require hashing strings from solidity. So perhaps string input is more flexible as we could just use vm.ipfsCidV0 (vm.readFile(path)) and others can skip the file reading and hash content immediately?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaniPopes for sha256,

That’s a good point, and normally sha256() in Solidity would be sufficient — but this cheatcode targets a different use case.

We need to compute the SHA-256 hash of an off-chain file in the testing environment, prefix it per the multihash spec (IPFS CIDv0), and then pass that result into the EVM. Since Solidity's sha256() only operates on in-memory bytes at runtime, it can’t handle file reads or multihash formatting.
So this utility is kinda necessary

Copy link
Collaborator

@grandizzy grandizzy Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks to be more complex than initial draft, the only way I was able to get same CID v0 as with npx ipfs-only-hash was to use https://crates.io/crates/ipfs-cid crate which chunks it and hash last chunk https://github.com/omarabid/ipfs-cid/blob/e60fae3ee3222916da0c1dfec5eda1a4c26dec5b/src/lib.rs#L8 This uses an archived / unmaintained crate https://github.com/rs-ipfs/rust-ipfs and adds in bunch of other dependencies which would like to avoid. If we cannot get to a clean impl (that doesn't require pulling in so many deps) we should hold off on this for now.

Copy link
Contributor Author

@Ayushdubey86 Ayushdubey86 Apr 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what we can do for now is, users can easily wrap vm.readFile(path) with a future ipfsCidV0(bytes) if needed, and for now, hashing files directly can be done externally if necessary. can we finalize this approach for now , later open another issue, if need arises?


// ======== Environment Variables ========

/// Sets environment variables.
Expand Down
14 changes: 14 additions & 0 deletions crates/cheatcodes/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use foundry_config::fs_permissions::FsAccessKind;
use revm::interpreter::CreateInputs;
use revm_inspectors::tracing::types::CallKind;
use semver::Version;
use sha2::{Digest, Sha256};
use std::{
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
Expand Down Expand Up @@ -587,6 +588,19 @@ impl Cheatcode for promptUintCall {
}
}

impl Cheatcode for ipfsCidV0Call {
fn apply(&self, _state: &mut Cheatcodes) -> Result {
let contents = fs::read(&self.filepath).unwrap();
let hash = Sha256::digest(&contents);
let mut multihash = Vec::with_capacity(2 + hash.len());
multihash.push(0x12);
multihash.push(0x20);
multihash.extend_from_slice(&hash);

Ok(multihash)
}
}

pub(super) fn write_file(state: &Cheatcodes, path: &Path, contents: &[u8]) -> Result {
let path = state.config.ensure_path_allowed(path, FsAccessKind::Write)?;
// write access to foundry.toml is not allowed
Expand Down
1 change: 1 addition & 0 deletions testdata/cheats/Vm.sol

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

19 changes: 19 additions & 0 deletions testdata/default/cheats/IpfsCidV0.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: MIT OR Apache-2.0
pragma solidity ^0.8.18;

import "ds-test/test.sol";
import "cheats/Vm.sol";

contract IpfsCidV0Test is DSTest {
Vm constant vm = Vm(HEVM_ADDRESS);

function testIpfsCidV0() public {
string memory filePath = "testdata/fixtures/File/test.txt";

bytes32 cid = vm.ipfsCidV0(filePath);

bytes32 expectedCid = 0x94da694df5cf2e139206cddcdd6f855baa45e519c5fdbc2e6aa1cf803cfd65d5;

assertEq(cid, expectedCid);
}
}
5 changes: 5 additions & 0 deletions testdata/fixtures/File/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: TestTitle
discussions: TestDiscussion
author: TestAuthor
---
Loading