Skip to content
Open
5 changes: 4 additions & 1 deletion app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
CMD ["node", "index.js"]
1 change: 1 addition & 0 deletions app/abi/blockRegistry.json
Original file line number Diff line number Diff line change
@@ -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"}]
81 changes: 80 additions & 1 deletion app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ const fs = require('fs')
const HDWalletProvider = require('truffle-hdwallet-provider')
const EthWallet = require('ethereumjs-wallet')
const Web3 = require('web3')
const ethers = require('ethers')
const { sign, signFuse } = require('./utils')

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 account
let consensus, blockReward
let consensus, blockReward, blockRegistry
let blockchains = {}

function initWalletProvider() {
logger.info(`initWalletProvider`)
Expand All @@ -34,6 +39,35 @@ function initWalletProvider() {
web3 = new Web3(walletProvider)
}
}
function initBlockchain(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()
const numValidators = validators.length
blockchains[chainId].blocks[block.hash] = await signFuse(block, chainId, blockchain.provider, 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 getNonce() {
try {
Expand All @@ -55,6 +89,13 @@ function initBlockRewardContract() {
logger.info(`initBlockRewardContract`, process.env.BLOCK_REWARD_ADDRESS)
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)
initBlockchain(1, process.env.ETH_RPC || throw "Missing ETH_RPC in environment")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use cloudflare eth endpoint

initBlockchain(56, process.env.BSC_RPC || throw "Missing BSC_RPC in environment"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bsc has public rpc

initBlockchain(122, process.env.FUSE_RPC || 'https://rpc.fuse.io/')
}

function emitInitiateChange() {
return new Promise(async (resolve, reject) => {
Expand Down Expand Up @@ -120,6 +161,40 @@ function emitRewardedOnCycle() {
})
}

async function emitRegistry() {
try {
logger.info('emitRegistry')
const currentBlock = (await web3.eth.getBlockNumber()).toNumber()
const chains = await blockRegistry.methods.getRpcs.call()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be getRpcs()
and that's why I want unit tests or testnet deployment with mock contracts to see that this whole process is working

await Promise.all(chains.filter(chain => !blockchains[chain[0]] || blockchains[chain[0]].rpc != chain[1]).map(async (chain) => initBlockchain(...chain)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if one initblockchain fail it shouldnt fail the whole function

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still not done

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`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think that should prevent from calling addSignedBlocks part

}
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)
}
}
}

async function runMain() {
try {
logger.info(`runMain`)
Expand All @@ -132,13 +207,17 @@ 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`)
return
}
await emitInitiateChange()
await emitRewardedOnCycle()
await emitRegistry()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think an exception here should cause the fuseapp to exit

} catch (e) {
logger.error(e)
process.exit(1)
Expand Down
5 changes: 4 additions & 1 deletion app/scripts/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ POLLING_INTERVAL=5000 \
RPC=http://127.0.0.1:8545 \
CONSENSUS_ADDRESS=0x3014ca10b91cb3D0AD85fEf7A3Cb95BCAc9c0f79 \
BLOCK_REWARD_ADDRESS=0x63D4efeD2e3dA070247bea3073BCaB896dFF6C9B \
node index.js
BLOCK_REGISTRY_ADDRESS=0xa7BfeDBf11a488EcA3838F03A93d8d96EAba9a02 \
ETH_RPC="" \
BSC_RPC="" \
node index.js
29 changes: 29 additions & 0 deletions app/utils.js
Original file line number Diff line number Diff line change
@@ -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]
}