From 423dd03a3b2d9d133a0d53d1ea64eb6b177842be Mon Sep 17 00:00:00 2001 From: Jerry Date: Mon, 10 Apr 2023 14:40:11 -0700 Subject: [PATCH] Contract state migration This commit allows us to read contract states from one chain and write them to contracts on another chain. --- contracts/root/RootChain.sol | 19 +++++++ contracts/root/stateSyncer/StateSender.sol | 4 ++ scripts/migrate-states/README.md | 22 ++++++++ scripts/migrate-states/rootChain.js | 58 ++++++++++++++++++++ scripts/migrate-states/stakingInfo.js | 62 ++++++++++++++++++++++ scripts/migrate-states/stateSync.js | 42 +++++++++++++++ scripts/migrate-states/util.js | 18 +++++++ 7 files changed, 225 insertions(+) create mode 100644 scripts/migrate-states/README.md create mode 100644 scripts/migrate-states/rootChain.js create mode 100644 scripts/migrate-states/stakingInfo.js create mode 100644 scripts/migrate-states/stateSync.js create mode 100644 scripts/migrate-states/util.js diff --git a/contracts/root/RootChain.sol b/contracts/root/RootChain.sol index eefc8d7df..ddc8544b1 100644 --- a/contracts/root/RootChain.sol +++ b/contracts/root/RootChain.sol @@ -116,6 +116,25 @@ contract RootChain is RootChainStorage, IRootChain { emit ResetHeaderBlock(msg.sender, _nextHeaderBlock); } + function overrideHeaderBlock( + uint256 _headerBlockId, + address _proposer, + uint256 _start, + uint256 _end, + bytes32 _rootHash) public onlyOwner { + _nextHeaderBlock = _headerBlockId; + + HeaderBlock memory headerBlock = HeaderBlock({ + root: _rootHash, + start: _start, + end: _end, + createdAt: now, + proposer: _proposer + }); + + headerBlocks[currentHeaderBlock()] = headerBlock; + } + // Housekeeping function. @todo remove later function setHeimdallId(string memory _heimdallId) public onlyOwner { heimdallId = keccak256(abi.encodePacked(_heimdallId)); diff --git a/contracts/root/stateSyncer/StateSender.sol b/contracts/root/stateSyncer/StateSender.sol index 0525dc404..b43ff8ccd 100644 --- a/contracts/root/stateSyncer/StateSender.sol +++ b/contracts/root/stateSyncer/StateSender.sol @@ -38,6 +38,10 @@ contract StateSender is Ownable { emit StateSynced(counter, receiver, data); } + function setCounter(uint256 _counter) public onlyOwner { + counter = _counter; + } + // register new contract for state sync function register(address sender, address receiver) public { require( diff --git a/scripts/migrate-states/README.md b/scripts/migrate-states/README.md new file mode 100644 index 000000000..89a7d913b --- /dev/null +++ b/scripts/migrate-states/README.md @@ -0,0 +1,22 @@ +Scripts that read contract states from a chain and write the states to another chain. + +Pre-requisite: contracts are already deployed on both chains. + +Steps: + +1. Modify contract address in the script. +2. Run the script with custom environment variables. + +Usage example +```bash +PRIVATE_KEY="0x..." NEW_CHAIN_ID=1337 npm run truffle exec scripts/migrate-states/rootChain.js +``` + +Environment variables: + +``` +PRIVATE_KEY: The private key of the account that will be used to send transactions. +NEW_CHAIN_ID: The chain id of the new chain. +OLD_CHAIN_PROVIDER: The provider of the old chain. Default: http://localhost:9546 +NEW_CHAIN_PROVIDER: The provider of the new chain. Default: http://localhost:9545 +``` \ No newline at end of file diff --git a/scripts/migrate-states/rootChain.js b/scripts/migrate-states/rootChain.js new file mode 100644 index 000000000..a4dd35ea0 --- /dev/null +++ b/scripts/migrate-states/rootChain.js @@ -0,0 +1,58 @@ +const Web3 = require('web3'); +const { oldChain, newChain, fromAccount, privateKey, newChainId } = require('./util'); + +const contractJson = require('../../build/contracts/RootChain.json'); +const abi = contractJson.abi; + +const contractAddress1 = '0x47bf9dc50D5D8d676AFBE714766dFF84C1828AE9'; // RootChain proxy contract address on Chain 1 +const contractAddress2 = '0xcB6cb787A98448C586F2f74d55f1B3d0EA29F1e7'; // RootChain proxy contract address on Chain 2 + +const oldContract = new oldChain.eth.Contract(abi, contractAddress1); +const newContract = new newChain.eth.Contract(abi, contractAddress2); + +module.exports = async function (callback) { + try { + // Read internal state from RootChain contract on Chain 1 + const currentHeaderBlock = await oldContract.methods.currentHeaderBlock().call(); + const headerBlock = await oldContract.methods.headerBlocks(currentHeaderBlock).call(); + + console.log(`Current header block on old chain: ${currentHeaderBlock}`) + console.log(`Current header block. Start: ${headerBlock.start}, end: ${headerBlock.end}`); + + // Write the state to RootChain contract on Chain 2 + const gasPrice = await newChain.eth.getGasPrice(); + + // Call overrideHeaderBlock + const overrideHeaderBlockTx = newContract.methods.overrideHeaderBlock( + parseInt(currentHeaderBlock) + 10000, + headerBlock.proposer, + headerBlock.start, + headerBlock.end, + headerBlock.root + ); + const gasLimit = await overrideHeaderBlockTx.estimateGas({ from: fromAccount }); + + const txData = { + to: contractAddress2, + data: overrideHeaderBlockTx.encodeABI(), + gas: gasLimit, + gasPrice: gasPrice, + chainId: newChainId, + }; + + const signedTx = await newChain.eth.accounts.signTransaction(txData, privateKey); + const receipt = await newChain.eth.sendSignedTransaction(signedTx.rawTransaction); + + console.log(`OverrideHeaderBlock transaction receipt: ${JSON.stringify(receipt)}`); + + const currentHeaderBlockNewChain = parseInt(await newContract.methods.currentHeaderBlock().call()); + console.log(`Current header block on new chain: ${currentHeaderBlockNewChain}`); + const headerBlockNewChain = await newContract.methods.headerBlocks(currentHeaderBlockNewChain).call(); + + console.log(`Current header block: Start: ${headerBlockNewChain.start}, end: ${headerBlockNewChain.end}`); + } catch (e) { + console.log(e); + } + + callback(); +}; \ No newline at end of file diff --git a/scripts/migrate-states/stakingInfo.js b/scripts/migrate-states/stakingInfo.js new file mode 100644 index 000000000..864285888 --- /dev/null +++ b/scripts/migrate-states/stakingInfo.js @@ -0,0 +1,62 @@ +const Web3 = require('web3'); +const { oldChain, newChain, fromAccount, privateKey, newChainId } = require('./util'); + +const contractJson = require('../../build/contracts/StakingInfo.json'); +const abi = contractJson.abi; + +const contractAddress1 = '0x88B97C24099185FFa54DC8e78293B9D7a60463c2'; // StakingInfo contract address on Chain 1 +const contractAddress2 = '0x88B97C24099185FFa54DC8e78293B9D7a60463c2'; // StakingInfo contract address on Chain 2 + +const oldContract = new oldChain.eth.Contract(abi, contractAddress1); +const newContract = new newChain.eth.Contract(abi, contractAddress2); + +module.exports = async function (callback) { + try { + + const validatorIds = []; + const nonces = []; + + for (let validatorId = 1; validatorId <= 150; validatorId++) { + const nonce = await oldContract.methods.validatorNonce(validatorId).call(); + if (nonce === '0' || nonce === 0) { + continue; + } + validatorIds.push(validatorId); + nonces.push(nonce); + } + + console.log('validatorIds', validatorIds); + console.log('nonces', nonces); + + const tx = newContract.methods.updateNonce(validatorIds, nonces); + const gasPrice = await newChain.eth.getGasPrice(); + const gasLimit = await tx.estimateGas({ from: fromAccount }); + + const txData = { + to: contractAddress2, + data: tx.encodeABI(), + gas: gasLimit, + gasPrice: gasPrice, + chainId: newChainId, + }; + + const signedTx = await newChain.eth.accounts.signTransaction(txData, privateKey); + const receipt = await newChain.eth.sendSignedTransaction(signedTx.rawTransaction); + + console.log(`Transaction receipt: ${JSON.stringify(receipt)}`); + for (let i = 0; i < validatorIds.length; i++) { + const validatorId = validatorIds[i]; + const nonce = nonces[i]; + const newNonce = await newContract.methods.validatorNonce(validatorId).call(); + if (newNonce !== nonce) { + throw new Error(`Nonce for validator ${validatorId} is not updated. Expected ${nonce}, got ${newNonce}`); + } + } + + console.log('All nonces are updated'); + } catch (e) { + console.log(e); + } + + callback(); +}; \ No newline at end of file diff --git a/scripts/migrate-states/stateSync.js b/scripts/migrate-states/stateSync.js new file mode 100644 index 000000000..dc6bd3392 --- /dev/null +++ b/scripts/migrate-states/stateSync.js @@ -0,0 +1,42 @@ +const Web3 = require('web3'); +const { oldChain, newChain, fromAccount, privateKey, newChainId } = require('./util'); + +const contractJson = require('../../build/contracts/StateSender.json'); +const abi = contractJson.abi; + +const contractAddress1 = '0x6E3DfF195E068A6dDc24D845014469D03bF17cb9'; // StateSender contract address on Chain 1 +const contractAddress2 = '0x6E3DfF195E068A6dDc24D845014469D03bF17cb9'; // StateSender contract address on Chain 2 + +const oldContract = new oldChain.eth.Contract(abi, contractAddress1); +const newContract = new newChain.eth.Contract(abi, contractAddress2); + +module.exports = async function (callback) { + try { + // Read internal state from StateSender contract on Chain 1 + const counter = await oldContract.methods.counter().call(); + console.log(`Counter value: ${counter}`); + + // Write the state to StateSender contract on Chain 2 + const gasPrice = await newChain.eth.getGasPrice(); + const setCounterTx = newContract.methods.setCounter(counter); + const gasLimit = await setCounterTx.estimateGas({ from: fromAccount }); + + const txData = { + to: contractAddress2, + data: setCounterTx.encodeABI(), + gas: gasLimit, + chainId: newChainId, + }; + + const signedTx = await newChain.eth.accounts.signTransaction(txData, privateKey); + const receipt = await newChain.eth.sendSignedTransaction(signedTx.rawTransaction); + + console.log(`Transaction receipt: ${JSON.stringify(receipt)}`); + const newCounter = await newContract.methods.counter().call(); + console.log(`Counter value for new contract: ${newCounter}`); + } catch (e) { + console.log(e); + } + + callback(); +}; \ No newline at end of file diff --git a/scripts/migrate-states/util.js b/scripts/migrate-states/util.js new file mode 100644 index 000000000..f8fb5800a --- /dev/null +++ b/scripts/migrate-states/util.js @@ -0,0 +1,18 @@ +const Web3 = require('web3'); + +const oldChainProvider = new Web3.providers.HttpProvider(process.env.OLD_CHAIN_PROVIDER || 'http://localhost:9546'); +const newChainProvider = new Web3.providers.HttpProvider(process.env.NEW_CHAIN_PROVIDER || 'http://localhost:9545'); + +const oldChain = new Web3(oldChainProvider); +const newChain = new Web3(newChainProvider); + +const privateKey = process.env.PRIVATE_KEY; // Private key of the account that will call the functions +const fromAccount = newChain.eth.accounts.privateKeyToAccount(privateKey).address; + +const newChainId = process.env.NEW_CHAIN_ID || 11155111; // Chain ID of sepolia + +exports.oldChain = oldChain; +exports.newChain = newChain; +exports.fromAccount = fromAccount; +exports.privateKey = privateKey; +exports.newChainId = newChainId; \ No newline at end of file