Skip to content
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

Merged
merged 8 commits into from
Dec 18, 2023
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
241 changes: 241 additions & 0 deletions abis/0.8.21/HashCheckpoint.json

Large diffs are not rendered by default.

125 changes: 125 additions & 0 deletions contracts/utils/HashCheckpoint.sol
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]>
Copy link
Collaborator

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

Copy link
Contributor Author

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

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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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))));
}
}
2 changes: 1 addition & 1 deletion docs/configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
},
{
"name": "GnosisSafeSameAddressMultisig",
"artifact": "abis/0.8.19/GnosisSafeSameAddressMultisig.json",
"artifact": "abis/0.8.21/GnosisSafeSameAddressMultisig.json",
"address": "0x6e7f594f680f7aBad18b7a63de50F0FeE47dfD06"
}
]
Expand Down
86 changes: 86 additions & 0 deletions scripts/deployment/l2/deploy_18_hash_checkpoint_l2.js
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);
});
3 changes: 2 additions & 1 deletion scripts/deployment/l2/globals_gnosis_mainnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@
"agentIds": ["12"],
"threshold": "0",
"configHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}
},
"hashCheckpoint": "0x694e62BDF7Ff510A4EE66662cf4866A961a31653"
}
9 changes: 9 additions & 0 deletions scripts/deployment/l2/verify_18_hash_checkpoint.js
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
];
52 changes: 52 additions & 0 deletions test/HashCheckpoint.js
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);
});

});
});
Loading