diff --git a/app/Dockerfile b/app/Dockerfile index 11c2b9b..59113a3 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -5,8 +5,11 @@ ENV LOG_LEVEL=debug ENV RPC=https://rpc.fuse.io ENV CONSENSUS_ADDRESS=0x3014ca10b91cb3D0AD85fEf7A3Cb95BCAc9c0f79 ENV BLOCK_REWARD_ADDRESS=0x63D4efeD2e3dA070247bea3073BCaB896dFF6C9B +ENV BLOCK_REGISTRY_ADDRESS=0xa7BfeDBf11a488EcA3838F03A93d8d96EAba9a02 +ENV ETH_RPC= +ENV BSC_RPC= COPY ./ ./ RUN npm install --only=prod -CMD ["node", "index.js"] \ No newline at end of file +CMD ["node", "index.js"] diff --git a/app/abi/blockRegistry.json b/app/abi/blockRegistry.json new file mode 100644 index 0000000..78da375 --- /dev/null +++ b/app/abi/blockRegistry.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_consensus","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"uint256","name":"blockchainId","type":"uint256"},{"internalType":"string","name":"rpc","type":"string"}],"name":"addBlockchain","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"bytes","name":"rlpHeader","type":"bytes"},{"components":[{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"vs","type":"bytes32"}],"internalType":"struct Signature","name":"signature","type":"tuple"},{"internalType":"uint256","name":"blockchainId","type":"uint256"},{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"internalType":"uint256","name":"cycleEnd","type":"uint256"},{"internalType":"address[]","name":"validators","type":"address[]"}],"internalType":"struct Block[]","name":"blocks","type":"tuple[]"}],"name":"addSignedBlocks","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"blockHashes","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"blockchains","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getRpcs","outputs":[{"components":[{"internalType":"uint256","name":"chainId","type":"uint256"},{"internalType":"string","name":"rpc","type":"string"}],"internalType":"struct BlockHeaderRegistry.Rpc[]","name":"_rpcs","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"blockchainId","type":"uint256"},{"internalType":"uint256","name":"number","type":"uint256"}],"name":"getSignedBlock","outputs":[{"internalType":"bytes32","name":"blockHash","type":"bytes32"},{"components":[{"internalType":"bytes32","name":"parentHash","type":"bytes32"},{"internalType":"bytes32","name":"uncleHash","type":"bytes32"},{"internalType":"address","name":"coinbase","type":"address"},{"internalType":"bytes32","name":"root","type":"bytes32"},{"internalType":"bytes32","name":"txHash","type":"bytes32"},{"internalType":"bytes32","name":"receiptHash","type":"bytes32"},{"internalType":"bytes32[8]","name":"bloom","type":"bytes32[8]"},{"internalType":"uint256","name":"difficulty","type":"uint256"},{"internalType":"uint256","name":"number","type":"uint256"},{"internalType":"uint256","name":"gasLimit","type":"uint256"},{"internalType":"uint256","name":"gasUsed","type":"uint256"},{"internalType":"uint256","name":"time","type":"uint256"},{"internalType":"bytes32","name":"mixDigest","type":"bytes32"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"baseFee","type":"uint256"},{"internalType":"bytes","name":"extra","type":"bytes"}],"internalType":"struct BlockHeader","name":"blockHeader","type":"tuple"},{"components":[{"internalType":"address","name":"creator","type":"address"},{"internalType":"bytes[]","name":"signatures","type":"bytes[]"},{"internalType":"uint256","name":"cycleEnd","type":"uint256"},{"internalType":"address[]","name":"validators","type":"address[]"}],"internalType":"struct SignedBlock","name":"signedBlock","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"},{"internalType":"address","name":"","type":"address"}],"name":"hasValidatorSigned","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"rpcs","outputs":[{"internalType":"uint256","name":"chainId","type":"uint256"},{"internalType":"string","name":"rpc","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"signedBlocks","outputs":[{"internalType":"address","name":"creator","type":"address"},{"internalType":"uint256","name":"cycleEnd","type":"uint256"}],"stateMutability":"view","type":"function"}] diff --git a/app/blockHeaderRegistry.js b/app/blockHeaderRegistry.js new file mode 100644 index 0000000..106410c --- /dev/null +++ b/app/blockHeaderRegistry.js @@ -0,0 +1,138 @@ +const path = require("path"); +const logger = require("pino")({ + level: process.env.LOG_LEVEL || "info", + prettyPrint: { translateTime: true }, +}); +const Web3 = require("web3"); +const ethers = require("ethers"); +const { sign, signFuse } = require("./utils"); + +const blockchains = {}; + +function initBlockchain({ + consensus, + blockRegistry, + signer, + walletProvider, + chainId, + rpc, +}) { + logger.info("initBlockchain"); + try { + blockchains[chainId] = { + account: walletProvider.addresses[0], + web3: new Web3(walletProvider), + rpc, + signer: new ethers.Wallet(pkey), + blocks: {}, + }; + blockchains[chainId].web3.eth.subscribe( + "newBlockHeaders", + async (block) => { + try { + if (chainId == 122) { + let cycleEnd = ( + await consensus.methods.getCurrentCycleEndBlock.call() + ).toNumber(); + let validators = await consensus.methods + .currentValidators() + .call(); + blockchains[chainId].blocks[block.hash] = + await signFuse( + block, + chainId, + blockchain.rpc, + blockchain.signer, + cycleEnd, + validators + ); + } else { + blockchains[chainId].blocks[block.hash] = await sign( + block, + chainId, + blockchain.provider, + blockchain.signer + ); + } + } catch (e) { + logger.error(`newBlockHeaders: ${e.toString()}`); + } + } + ); + } catch (e) { + throw `initBlockchain(${chainId}, ${rpc}) failed: ${e.toString()}`; + } +} + +async function emitRegistry({ + consensus, + blockRegistry, + walletProvider, + signer, + web3, +}) { + try { + logger.info("emitRegistry"); + const currentBlock = (await web3.eth.getBlockNumber()).toNumber(); + const numRpcs = ( + await blockRegistry.methods.getRpcsLength().call() + ).toNumber(); + const chains = await Promise.all( + new Array(numRpcs).map( + async (_, i) => await blockRegistry.methods.rpcs(i) + ) + ); + await Promise.all( + chains + .filter( + (chain) => + !blockchains[chain[0]] || + blockchains[chain[0]].rpc != chain[1] + ) + .map(async (chain) => + initBlockchain({ + consensus, + blockRegistry, + signer, + walletProvider, + chainId: chain[0], + rpc: chain[1], + }) + ) + ); + const blocks = {}; + const chainIds = {}; + Object.entries(blockchains).forEach((chainId, blockchain) => { + Object.entries(blockchain.blocks).forEach((hash, signed) => { + blocks[hash] = signed; + chainIds[hash] = chainId; + delete blockchain.blocks[hash]; + }); + }); + } catch (e) { + throw `emitRegistry failed trying to update rpcs`; + } + try { + const receipt = await blockRegistry.methods + .addSignedBlocks(Object.values(blocks)) + .send({ from: account }); + logger.info(`transactionHash: ${receipt.transactionHash}`); + logger.debug(`receipt: ${JSON.stringify(receipt)}`); + } catch (e) { + if (!e.data) throw e; + else { + logger.error(e); + const data = e.data; + const txHash = Object.keys(data)[0]; + const reason = data[txHash].reason; + Object.entries(blocks) + .filter((hash, signed) => hash != reason) + .forEach( + (hash, signed) => + (blockchains[chainIds[hash]].blocks[hash] = signed) + ); + } + } +} + +module.exports = { initBlockchain, emitRegistry, blockchains } diff --git a/app/index.js b/app/index.js index 9b66b70..2ec9932 100644 --- a/app/index.js +++ b/app/index.js @@ -5,13 +5,15 @@ const fs = require('fs') const HDWalletProvider = require('truffle-hdwallet-provider') const EthWallet = require('ethereumjs-wallet') const Web3 = require('web3') - +const { emitRegistry } = require('./block-header-registry') const configDir = path.join(cwd, process.env.CONFIG_DIR || 'config/') +const {ETH_RPC, BSC_RPC, RPC: FUSE_RPC} = process.env + let web3 -let walletProvider +let walletProvider, signer let account -let consensus, blockReward +let consensus, blockReward, blockRegistry function initWalletProvider() { logger.info(`initWalletProvider`) @@ -32,6 +34,7 @@ function initWalletProvider() { account = walletProvider.addresses[0] logger.info(`account: ${account}`) web3 = new Web3(walletProvider) + signer = new ethers.Wallet(pkey) } } @@ -56,6 +59,16 @@ function initBlockRewardContract() { blockReward = new web3.eth.Contract(require(path.join(cwd, 'abi/blockReward')), process.env.BLOCK_REWARD_ADDRESS) } +function initBlockRegistryContract() { + logger.info(`initBlockRegistryContract`, process.env.BLOCK_REGISTRY_ADDRESS) + blockRegistry = new web3.eth.Contract(require(path.join(cwd, 'abi/blockRegistry')), process.env.BLOCK_REGISTRY_ADDRESS) + if (!ETH_RPC) throw "Missing ETH_RPC in environment" + if (!BSC_RPC) throw "Missing BSC_RPC in environment" + initBlockchain(1, ETH_RPC) + initBlockchain(56, BSC_RPC) + initBlockchain(122, FUSE_RPC || 'https://rpc.fuse.io/') +} + function emitInitiateChange() { return new Promise(async (resolve, reject) => { try { @@ -132,6 +145,9 @@ async function runMain() { if (!blockReward) { initBlockRewardContract() } + if (!blockRegistry) { + initBlockRegistryContract() + } const isValidator = await consensus.methods.isValidator(web3.utils.toChecksumAddress(account)).call() if (!isValidator) { logger.warn(`${account} is not a validator, skipping`) @@ -139,6 +155,13 @@ async function runMain() { } await emitInitiateChange() await emitRewardedOnCycle() + await emitRegistry({ + web3, + consensus, + blockRegistry, + signer, + walletProvider + }) } catch (e) { logger.error(e) process.exit(1) diff --git a/app/scripts/run.sh b/app/scripts/run.sh index af33c4a..8263b39 100755 --- a/app/scripts/run.sh +++ b/app/scripts/run.sh @@ -4,4 +4,7 @@ POLLING_INTERVAL=5000 \ RPC=http://127.0.0.1:8545 \ CONSENSUS_ADDRESS=0x3014ca10b91cb3D0AD85fEf7A3Cb95BCAc9c0f79 \ BLOCK_REWARD_ADDRESS=0x63D4efeD2e3dA070247bea3073BCaB896dFF6C9B \ -node index.js \ No newline at end of file +BLOCK_REGISTRY_ADDRESS=0xa7BfeDBf11a488EcA3838F03A93d8d96EAba9a02 \ +ETH_RPC="" \ +BSC_RPC="" \ +node index.js diff --git a/app/utils.js b/app/utils.js new file mode 100644 index 0000000..fa1e0fc --- /dev/null +++ b/app/utils.js @@ -0,0 +1,29 @@ +const ethers = require('ethers') + +export async function sign(header, chainId, signer) { + const rlpHeader = hashHeader(header) + const payload = ethers.utils.keccak256(rlpHeader); + const { _vs: vs, r } = ethers.utils.splitSignature( + await signer.signMessage(ethers.utils.arrayify(payload)) + ); + return [rlpHeader,[vs,r],chainId,payload,0,[]] +} +export function hashHeader(web3Header) { + const rlpHeader = ethers.utils.RLP.encode( + Object.values(header).map((v) => (v === 0 ? "0x" : v)) + ); + return rlpHeader +} + +export async function signFuse(header, chainId, signer, cycleEnd, validators) { + const rlpHeader = hashHeader(header) + const packed = ethers.utils.solidityPack( + ["bytes32", "address[]", "uint256"], + [blockHash, validators, cycleEnd] + ); + const payload = ethers.utils.keccak256(packed); + const { _vs: vs, r } = ethers.utils.splitSignature( + await signer.signMessage(ethers.utils.arrayify(payload)) + ); + return [rlpHeader,[vs,r],chainId,payload,cycleEnd,validators] +}