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

L2 Support #92

Merged
merged 5 commits into from
Jun 19, 2024
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
67 changes: 67 additions & 0 deletions contracts/EthStorageContractL2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./EthStorageContract2.sol";

interface IL1Block {
function blockHash(uint256 _historyNumber) external view returns (bytes32);

function number() external view returns (uint64);

function timestamp() external view returns (uint64);
}

contract EthStorageContractL2 is EthStorageContract2 {
IL1Block public constant l1Block = IL1Block(0x4200000000000000000000000000000000000015);

constructor(
Config memory _config,
uint256 _startTime,
uint256 _storageCost,
uint256 _dcfFactor
) EthStorageContract2(_config, _startTime, _storageCost, _dcfFactor) {}

function getRandao(uint256 l1BlockNumber, bytes calldata headerRlpBytes) internal view returns (bytes32) {
bytes32 bh = l1Block.blockHash(l1BlockNumber);
require(bh != bytes32(0), "failed to obtain blockhash");

return RandaoLib.verifyHeaderAndGetRandao(bh, headerRlpBytes);
}

/// @dev We are still using L1 block number, timestamp, and blockhash to mine.
/// @param blockNumber L1 blocknumber.
/// @param randaoProof L1 block header RLP bytes.
function _mine(
uint256 blockNumber,
uint256 shardId,
address miner,
uint256 nonce,
bytes32[] memory encodedSamples,
uint256[] memory masks,
bytes calldata randaoProof,
bytes[] calldata inclusiveProofs,
bytes[] calldata decodeProof
) internal override {
// Obtain the blockhash of the block number of recent blocks
require(l1Block.number() - blockNumber <= maxL1MiningDrift, "block number too old");
// To avoid stack too deep, we resue the hash0 instead of using randao

bytes32 hash0 = getRandao(blockNumber, randaoProof);
// Estimate block timestamp
uint256 mineTs = l1Block.timestamp() - (l1Block.number() - blockNumber) * 12;

// Given a blockhash and a miner, we only allow sampling up to nonce limit times.
require(nonce < nonceLimit, "nonce too big");

// Check if the data matches the hash in metadata and obtain the solution hash.
hash0 = keccak256(abi.encode(miner, hash0, nonce));
hash0 = verifySamples(shardId, hash0, miner, encodedSamples, masks, inclusiveProofs, decodeProof);

// Check difficulty
uint256 diff = _calculateDiffAndInitHashSingleShard(shardId, mineTs);
uint256 required = uint256(2 ** 256 - 1) / diff;
require(uint256(hash0) <= required, "diff not match");

_rewardMiner(shardId, miner, mineTs, diff);
}
}
60 changes: 31 additions & 29 deletions contracts/StorageContract.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ abstract contract StorageContract is DecentralizedKV {
}

uint256 public constant sampleSizeBits = 5; // 32 bytes per sample
uint8 public constant maxL1MiningDrift = 64; // 64 blocks

uint256 public immutable maxKvSizeBits;
uint256 public immutable shardSizeBits;
Expand Down Expand Up @@ -170,6 +171,7 @@ abstract contract StorageContract is DecentralizedKV {
// Update mining info.
MiningLib.update(infos[shardId], minedTs, diff);

require(treasuryReward + minerReward <= address(this).balance, "not enough balance");
// TODO: avoid reentrancy attack
payable(treasury).transfer(treasuryReward);
payable(miner).transfer(minerReward);
Expand Down Expand Up @@ -203,6 +205,33 @@ abstract contract StorageContract is DecentralizedKV {
return minerReward;
}

function mine(
uint256 blockNumber,
uint256 shardId,
address miner,
uint256 nonce,
bytes32[] memory encodedSamples,
uint256[] memory masks,
bytes calldata randaoProof,
bytes[] calldata inclusiveProofs,
bytes[] calldata decodeProof
) public virtual {
return
_mine(blockNumber, shardId, miner, nonce, encodedSamples, masks, randaoProof, inclusiveProofs, decodeProof);
}

function setNonceLimit(uint256 _nonceLimit) public onlyOwner {
nonceLimit = _nonceLimit;
}

function setPrepaidAmount(uint256 _prepaidAmount) public onlyOwner {
prepaidAmount = _prepaidAmount;
}

function setMinimumDiff(uint256 _minimumDiff) public onlyOwner {
minimumDiff = _minimumDiff;
}

/*
* On-chain verification of storage proof of sufficient sampling.
* On-chain verifier will go same routine as off-chain data host, will check the encoded samples by decoding
Expand All @@ -220,9 +249,9 @@ abstract contract StorageContract is DecentralizedKV {
bytes calldata randaoProof,
bytes[] calldata inclusiveProofs,
bytes[] calldata decodeProof
) internal {
) internal virtual {
// Obtain the blockhash of the block number of recent blocks
require(block.number - blockNumber <= 64, "block number too old");
require(block.number - blockNumber <= maxL1MiningDrift, "block number too old");
// To avoid stack too deep, we resue the hash0 instead of using randao
bytes32 hash0 = RandaoLib.verifyHistoricalRandao(blockNumber, randaoProof);
// Estimate block timestamp
Expand All @@ -242,31 +271,4 @@ abstract contract StorageContract is DecentralizedKV {

_rewardMiner(shardId, miner, mineTs, diff);
}

function mine(
uint256 blockNumber,
uint256 shardId,
address miner,
uint256 nonce,
bytes32[] memory encodedSamples,
uint256[] memory masks,
bytes calldata randaoProof,
bytes[] calldata inclusiveProofs,
bytes[] calldata decodeProof
) public virtual {
return
_mine(blockNumber, shardId, miner, nonce, encodedSamples, masks, randaoProof, inclusiveProofs, decodeProof);
}

function setNonceLimit(uint256 _nonceLimit) public onlyOwner {
nonceLimit = _nonceLimit;
}

function setPrepaidAmount(uint256 _prepaidAmount) public onlyOwner {
prepaidAmount = _prepaidAmount;
}

function setMinimumDiff(uint256 _minimumDiff) public onlyOwner {
minimumDiff = _minimumDiff;
}
}
7 changes: 6 additions & 1 deletion hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ module.exports = {
url: process.env.SEPOLIA_URL || "",
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
},
qkc: {
url: process.env.QKC_URL || "",
accounts:
process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
},
kovan: {
url: process.env.KOVAN_URL || "",
accounts:
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,12 @@
"test": "hardhat test",
"prettier:check": "prettier-check contracts/**/*.sol",
"prettier:fix": "prettier --write contracts/**/*.sol test/**/*.js scripts/**/*.js",
"deploy": "npx hardhat run scripts/deploy.js --network sepolia"
"deploy": "npx hardhat run scripts/deploy.js --network sepolia",
"deployL2": "npx hardhat run scripts/deployL2.js --network qkc"
},
"workspaces": {
"packages": [
"packages/arb-shared-dependencies"
]
}
}
}
131 changes: 131 additions & 0 deletions scripts/deployL2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
const hre = require("hardhat");
const dotenv = require("dotenv")
dotenv.config()


let ownerAddress = null;
let treasuryAddress = null;
const adminContractAddr = "";
const storageContractProxy = "";
const gasPrice = null;

const config = [
17, // maxKvSizeBits, 131072
39, // shardSizeBits ~ 512G
2, // randomChecks
7200, // cutoff = 2/3 * target internal (3 hours), 3 * 3600 * 2/3
32, // diffAdjDivisor
100, // treasuryShare, means 1%
];
const storageCost = 1500000000000000; // storageCost - 1,500,000Gwei forever per blob - https://ethresear.ch/t/ethstorage-scaling-ethereum-storage-via-l2-and-da/14223/6#incentivization-for-storing-m-physical-replicas-1
const dcfFactor = 340282366367469178095360967382638002176n; // dcfFactor, it mean 0.95 for yearly discount

async function verifyContract(contract, args) {
// if (!process.env.ETHERSCAN_API_KEY) {
// return;
// }
// await hre.run("verify:verify", {
// address: contract,
// constructorArguments: args,
// });
}

async function deployContract() {
const startTime = Math.floor(new Date().getTime() / 1000);

const [deployer] = await hre.ethers.getSigners();
ownerAddress = deployer.address;
treasuryAddress = deployer.address;

const StorageContract = await hre.ethers.getContractFactory("EthStorageContractL2");
// refer to https://docs.google.com/spreadsheets/d/11DHhSang1UZxIFAKYw6_Qxxb-V40Wh1lsYjY2dbIP5k/edit#gid=0
const implContract = await StorageContract.deploy(
config,
startTime, // startTime
storageCost,
dcfFactor,
{ gasPrice: gasPrice }
);
await implContract.deployed();
const impl = implContract.address;
console.log("storage impl address is ", impl);

const data = implContract.interface.encodeFunctionData("initialize", [
4718592000, // minimumDiff 5 * 3 * 3600 * 1024 * 1024 / 12 = 4718592000 for 5 replicas that can have 1M IOs in one epoch
3145728000000000000000n, // prepaidAmount - 50% * 2^39 / 131072 * 1500000Gwei, it also means 3145 ETH for half of the shard
1048576, // nonceLimit 1024 * 1024 = 1M samples and finish sampling in 1.3s with IO rate 6144 MB/s: 4k * 2(random checks) / 6144 = 1.3s
treasuryAddress, // treasury
ownerAddress,
]);
console.log(impl, ownerAddress, data);
const EthStorageUpgradeableProxy = await hre.ethers.getContractFactory("EthStorageUpgradeableProxy");
const ethStorageProxy = await EthStorageUpgradeableProxy.deploy(impl, ownerAddress, data, { gasPrice: gasPrice });
await ethStorageProxy.deployed();
const admin = await ethStorageProxy.admin();

console.log("storage admin address is ", admin);
console.log("storage contract address is ", ethStorageProxy.address);
const receipt = await hre.ethers.provider.getTransactionReceipt(ethStorageProxy.deployTransaction.hash);
console.log(
"deployed in block number",
receipt.blockNumber,
"at",
new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })
);

// fund 0.5 eth into the storage contract to give reward for empty mining
const ethStorage = StorageContract.attach(ethStorageProxy.address);
const tx = await ethStorage.sendValue({ value: hre.ethers.utils.parseEther("0.5") });
await tx.wait();
console.log("balance of " + ethStorage.address, await hre.ethers.provider.getBalance(ethStorage.address));

// verify contract
await verifyContract(ethStorageProxy.address);
await verifyContract(impl, [config, startTime, storageCost, dcfFactor]);
}

async function updateContract() {
const StorageContract = await hre.ethers.getContractFactory("TestEthStorageContractKZG");

// get start time
const ethStorage = StorageContract.attach(storageContractProxy);
const startTime = await ethStorage.startTime();

// deploy
const implContract = await StorageContract.deploy(
config,
startTime, // startTime
storageCost,
dcfFactor,
{ gasPrice: gasPrice }
);
await implContract.deployed();
const impl = implContract.address;
console.log("storage impl address is ", impl);

// set impl
const EthStorageAdmin = await hre.ethers.getContractAt("IProxyAdmin", adminContractAddr);
const tx = await EthStorageAdmin.upgradeAndCall(storageContractProxy, impl, "0x");
await tx.wait();
console.log("update contract success!");

// verify contract
await verifyContract(impl, [config, startTime, storageCost, dcfFactor]);
}

async function main() {
if (!storageContractProxy) {
// create
await deployContract();
} else {
// update
await updateContract();
}
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
Loading