-
Notifications
You must be signed in to change notification settings - Fork 3
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
feat: add hash-checkpoint contract #142
Changes from all commits
b13a8ca
126108c
a718b1d
d133f56
c5edd99
08e2ef7
fdce24f
5ae1acb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.21; | ||
|
||
/// @title Hash Checkpoint - Smart contract for checkpointing IPFS hashes on-chain | ||
/// @author Aleksandr Kuperman - <[email protected]> | ||
contract HashCheckpoint { | ||
error OwnerOnly(address sender, address owner); | ||
DavidMinarsch marked this conversation as resolved.
Show resolved
Hide resolved
|
||
error ZeroAddress(); | ||
error ZeroValue(); | ||
event OwnerUpdated(address indexed owner); | ||
event ManagerUpdated(address indexed manager); | ||
event BaseURIChanged(string baseURI); | ||
event HashUpdated(address indexed emitter, bytes32 hash); | ||
|
||
// Owner address | ||
address public owner; | ||
// Base URI | ||
string public baseURI; | ||
// To better understand the CID anatomy, please refer to: https://proto.school/anatomy-of-a-cid/05 | ||
// CID = <multibase_encoding>multibase_encoding(<cid-version><multicodec><multihash-algorithm><multihash-length><multihash-hash>) | ||
// CID prefix = <multibase_encoding>multibase_encoding(<cid-version><multicodec><multihash-algorithm><multihash-length>) | ||
// to complement the multibase_encoding(<multihash-hash>) | ||
// multibase_encoding = base16 = "f" | ||
// cid-version = version 1 = "0x01" | ||
// multicodec = dag-pb = "0x70" | ||
// multihash-algorithm = sha2-256 = "0x12" | ||
// multihash-length = 256 bits = "0x20" | ||
string public constant CID_PREFIX = "f01701220"; | ||
// Map of address => latest IPFS hash | ||
mapping (address => bytes32) public latestHashes; | ||
|
||
/// @dev Hash checkpoint constructor. | ||
/// @param _baseURI Hash registry base URI. | ||
constructor(string memory _baseURI) { | ||
baseURI = _baseURI; | ||
owner = msg.sender; | ||
} | ||
|
||
/// @dev Changes the owner address. | ||
/// @param newOwner Address of a new owner. | ||
function changeOwner(address newOwner) external virtual { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably no need to be a virtual function? For all of them below as well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess no harm |
||
// Check for the ownership | ||
if (msg.sender != owner) { | ||
revert OwnerOnly(msg.sender, owner); | ||
} | ||
|
||
// Check for the zero address | ||
if (newOwner == address(0)) { | ||
revert ZeroAddress(); | ||
} | ||
|
||
owner = newOwner; | ||
emit OwnerUpdated(newOwner); | ||
} | ||
|
||
/// @dev Sets unit base URI. | ||
/// @param bURI Base URI string. | ||
function setBaseURI(string memory bURI) external virtual { | ||
// Check for the ownership | ||
if (msg.sender != owner) { | ||
revert OwnerOnly(msg.sender, owner); | ||
} | ||
|
||
// Check for the zero value | ||
if (bytes(bURI).length == 0) { | ||
revert ZeroValue(); | ||
} | ||
|
||
baseURI = bURI; | ||
emit BaseURIChanged(bURI); | ||
} | ||
|
||
/// @dev Emits a hash | ||
/// @param hash The hash to be emitted. | ||
function checkpoint(bytes32 hash) external virtual { | ||
latestHashes[msg.sender] = hash; | ||
emit HashUpdated(msg.sender, hash); | ||
} | ||
|
||
// Open sourced from: https://stackoverflow.com/questions/67893318/solidity-how-to-represent-bytes32-as-string | ||
/// @dev Converts bytes16 input data to hex16. | ||
/// @notice This method converts bytes into the same bytes-character hex16 representation. | ||
/// @param data bytes16 input data. | ||
/// @return result hex16 conversion from the input bytes16 data. | ||
function _toHex16(bytes16 data) internal pure returns (bytes32 result) { | ||
result = bytes32 (data) & 0xFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000 | | ||
(bytes32 (data) & 0x0000000000000000FFFFFFFFFFFFFFFF00000000000000000000000000000000) >> 64; | ||
result = result & 0xFFFFFFFF000000000000000000000000FFFFFFFF000000000000000000000000 | | ||
(result & 0x00000000FFFFFFFF000000000000000000000000FFFFFFFF0000000000000000) >> 32; | ||
result = result & 0xFFFF000000000000FFFF000000000000FFFF000000000000FFFF000000000000 | | ||
(result & 0x0000FFFF000000000000FFFF000000000000FFFF000000000000FFFF00000000) >> 16; | ||
result = result & 0xFF000000FF000000FF000000FF000000FF000000FF000000FF000000FF000000 | | ||
(result & 0x00FF000000FF000000FF000000FF000000FF000000FF000000FF000000FF0000) >> 8; | ||
result = (result & 0xF000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000) >> 4 | | ||
(result & 0x0F000F000F000F000F000F000F000F000F000F000F000F000F000F000F000F00) >> 8; | ||
result = bytes32 (0x3030303030303030303030303030303030303030303030303030303030303030 + | ||
uint256 (result) + | ||
(uint256 (result) + 0x0606060606060606060606060606060606060606060606060606060606060606 >> 4 & | ||
0x0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F) * 39); | ||
} | ||
|
||
/// @dev Latest hash | ||
/// @notice Expected multicodec: dag-pb; hashing function: sha2-256, with base16 encoding and leading CID_PREFIX removed. | ||
/// @param _address The address that provided the hash. | ||
/// @return Latest hash string. | ||
function latestHash(address _address) public view virtual returns (string memory) { | ||
bytes32 latest_hash = latestHashes[_address]; | ||
// Parse 2 parts of bytes32 into left and right hex16 representation, and concatenate into string | ||
// adding the base URI and a cid prefix for the full base16 multibase prefix IPFS hash representation | ||
return string(abi.encodePacked(CID_PREFIX, _toHex16(bytes16(latest_hash)), | ||
_toHex16(bytes16(latest_hash << 128)))); | ||
} | ||
|
||
/// @dev Latest hash URI | ||
/// @notice Expected multicodec: dag-pb; hashing function: sha2-256, with base16 encoding and leading CID_PREFIX removed. | ||
/// @param _address The address that provided the hash. | ||
/// @return Latest hash URI string. | ||
function latestHashURI(address _address) public view virtual returns (string memory) { | ||
bytes32 latest_hash = latestHashes[_address]; | ||
// Parse 2 parts of bytes32 into left and right hex16 representation, and concatenate into string | ||
// adding the base URI and a cid prefix for the full base16 multibase prefix IPFS hash representation | ||
return string(abi.encodePacked(baseURI, CID_PREFIX, _toHex16(bytes16(latest_hash)), | ||
_toHex16(bytes16(latest_hash << 128)))); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
/*global process*/ | ||
|
||
const { ethers } = require("hardhat"); | ||
const { LedgerSigner } = require("@anders-t/ethers-ledger"); | ||
|
||
async function main() { | ||
const fs = require("fs"); | ||
const globalsFile = "globals.json"; | ||
const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); | ||
let parsedData = JSON.parse(dataFromJSON); | ||
const useLedger = parsedData.useLedger; | ||
const derivationPath = parsedData.derivationPath; | ||
const providerName = parsedData.providerName; | ||
const gasPriceInGwei = parsedData.gasPriceInGwei; | ||
const baseURI = parsedData.baseURI; | ||
let EOA; | ||
|
||
let networkURL; | ||
if (providerName === "polygon") { | ||
if (!process.env.ALCHEMY_API_KEY_MATIC) { | ||
console.log("set ALCHEMY_API_KEY_MATIC env variable"); | ||
} | ||
networkURL = "https://polygon-mainnet.g.alchemy.com/v2/" + process.env.ALCHEMY_API_KEY_MATIC; | ||
} else if (providerName === "polygonMumbai") { | ||
if (!process.env.ALCHEMY_API_KEY_MUMBAI) { | ||
console.log("set ALCHEMY_API_KEY_MUMBAI env variable"); | ||
return; | ||
} | ||
networkURL = "https://polygon-mumbai.g.alchemy.com/v2/" + process.env.ALCHEMY_API_KEY_MUMBAI; | ||
} else if (providerName === "gnosis") { | ||
if (!process.env.GNOSISSCAN_API_KEY) { | ||
console.log("set GNOSISSCAN_API_KEY env variable"); | ||
return; | ||
} | ||
networkURL = "https://rpc.gnosischain.com"; | ||
} else if (providerName === "chiado") { | ||
networkURL = "https://rpc.chiadochain.net"; | ||
} else { | ||
console.log("Unknown network provider", providerName); | ||
return; | ||
} | ||
|
||
const provider = new ethers.providers.JsonRpcProvider(networkURL); | ||
const signers = await ethers.getSigners(); | ||
|
||
if (useLedger) { | ||
EOA = new LedgerSigner(provider, derivationPath); | ||
} else { | ||
EOA = signers[0]; | ||
} | ||
// EOA address | ||
const deployer = await EOA.getAddress(); | ||
console.log("EOA is:", deployer); | ||
|
||
// Transaction signing and execution | ||
console.log("1. EOA to deploy HashCheckpoint"); | ||
const gasPrice = ethers.utils.parseUnits(gasPriceInGwei, "gwei"); | ||
const HashCheckpoint = await ethers.getContractFactory("HashCheckpoint"); | ||
console.log("You are signing the following transaction: HashCheckpoint.connect(EOA).deploy()"); | ||
const hashCheckpoint = await HashCheckpoint.connect(EOA).deploy(baseURI, { gasPrice }); | ||
const result = await hashCheckpoint.deployed(); | ||
|
||
// Transaction details | ||
console.log("Contract deployment: ServiceRegistryL2"); | ||
console.log("Contract address:", hashCheckpoint.address); | ||
console.log("Transaction:", result.deployTransaction.hash); | ||
// Wait half a minute for the transaction completion | ||
await new Promise(r => setTimeout(r, 30000)); | ||
|
||
// Writing updated parameters back to the JSON file | ||
parsedData.hashCheckpoint = hashCheckpoint.address; | ||
fs.writeFileSync(globalsFile, JSON.stringify(parsedData)); | ||
|
||
// Contract verification | ||
if (parsedData.contractVerification) { | ||
const execSync = require("child_process").execSync; | ||
execSync("npx hardhat verify --contract contracts/utils/HashCheckpoint.sol:HashCheckpoint --constructor-args scripts/deployment/l2/verify_18_hash_checkpoint.js --network " + providerName + " " + hashCheckpoint.address, { encoding: "utf-8" }); | ||
} | ||
} | ||
|
||
main() | ||
.then(() => process.exit(0)) | ||
.catch((error) => { | ||
console.error(error); | ||
process.exit(1); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
const fs = require("fs"); | ||
const globalsFile = "globals.json"; | ||
const dataFromJSON = fs.readFileSync(globalsFile, "utf8"); | ||
const parsedData = JSON.parse(dataFromJSON); | ||
const baseURI = parsedData.baseURI; | ||
|
||
module.exports = [ | ||
baseURI | ||
]; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/*global describe, context, beforeEach, it*/ | ||
|
||
const { expect } = require("chai"); | ||
const { ethers } = require("hardhat"); | ||
|
||
describe("HashCheckpoint", function () { | ||
let hashCheckpoint; | ||
let signers; | ||
const defaultHash = "0x" + "9".repeat(64); | ||
// const dependencies = [1]; | ||
// const AddressZero = "0x" + "0".repeat(40); | ||
beforeEach(async function () { | ||
const HashCheckpoint = await ethers.getContractFactory("HashCheckpoint"); | ||
hashCheckpoint = await HashCheckpoint.deploy("https://localhost/agent/"); | ||
await hashCheckpoint.deployed(); | ||
signers = await ethers.getSigners(); | ||
}); | ||
|
||
context("Initialization", async function () { | ||
it("Checking for arguments passed to the constructor", async function () { | ||
expect(await hashCheckpoint.baseURI()).to.equal("https://localhost/agent/"); | ||
}); | ||
|
||
it("Should return emtpy hash when checking for some hash", async function () { | ||
expect(await hashCheckpoint.latestHashes(signers[1].address)).to.equal("0x0000000000000000000000000000000000000000000000000000000000000000"); | ||
}); | ||
|
||
it("Should return empty hash with prefix when checking for some hash", async function () { | ||
expect(await hashCheckpoint.latestHash(signers[1].address)).to.equal("f017012200000000000000000000000000000000000000000000000000000000000000000"); | ||
}); | ||
|
||
it("Should return empty hash uri when checking for some hash", async function () { | ||
expect(await hashCheckpoint.latestHashURI(signers[1].address)).to.equal("https://localhost/agent/f017012200000000000000000000000000000000000000000000000000000000000000000"); | ||
}); | ||
|
||
it("Setting the base URI", async function () { | ||
await hashCheckpoint.setBaseURI("https://localhost2/agent/"); | ||
expect(await hashCheckpoint.baseURI()).to.equal("https://localhost2/agent/"); | ||
}); | ||
}); | ||
|
||
context("Checkpointing hashes", async function () { | ||
it("Should update latest hash correctly and emit event", async function () { | ||
const tx = await hashCheckpoint.connect(signers[2]).checkpoint(defaultHash); | ||
const result = await tx.wait(); | ||
expect(result.events[0].args.emitter).to.equal(signers[2].address); | ||
expect(result.events[0].args.hash).to.equal(defaultHash); | ||
expect(await hashCheckpoint.latestHashes(signers[2].address)).to.equal(defaultHash); | ||
}); | ||
|
||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to change the author :D
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lol you'll be the author eventually