leap-node
Version:
[](https://codecov.io/gh/leapdao/leap-node) [](https://quay.io/re
355 lines (316 loc) • 11.2 kB
JavaScript
/**
* Copyright (c) 2018-present, Leap DAO (leapdao.org)
*
* This source code is licensed under the Mozilla Public License Version 2.0
* found in the LICENSE file in the root directory of this source tree.
*/
const Web3 = require('web3');
const TinyQueue = require('tinyqueue');
const { Period, Block, Outpoint } = require('leap-core');
const ContractsEventsSubscription = require('./utils/ContractsEventsSubscription');
const { handleEvents } = require('./utils');
const { GENESIS } = require('./utils/constants');
const { logBridge, logNode, logVerbose } = require('./utils/debug');
const saveSubmission = require('./utils/saveSubmission');
const bridgeABI = require('./abis/bridgeAbi');
const exitABI = require('./abis/exitHandler');
const singleOperatorABI = require('./abis/singleOperator');
let operatorABI = require('./abis/operator');
const proxyABI = require('./abis/proxy');
const { NFT_COLOR_BASE, NST_COLOR_BASE } = require('./api/methods/constants');
const flags = require('./flags');
module.exports = class BridgeState {
constructor(db, privKey, config, eventsRelay, sender) {
this.config = config;
this.web3 = new Web3(config.rootNetwork);
this.sender = sender;
this.exitHandlerContract = new this.web3.eth.Contract(
exitABI.concat(proxyABI),
config.exitHandlerAddr
);
this.bridgeContract = new this.web3.eth.Contract(
bridgeABI.concat(proxyABI),
config.bridgeAddr
);
// if theta mainnet or theta testnet, use old operator ABI
if (
config.rootNetworkId === 448747062 ||
config.rootNetworkId === 218508104
) {
operatorABI = singleOperatorABI;
}
this.operatorContract = new this.web3.eth.Contract(
operatorABI.concat(proxyABI),
config.operatorAddr
);
this.account = privKey
? this.web3.eth.accounts.privateKeyToAccount(privKey)
: this.web3.eth.accounts.create();
this.db = db;
this.blockHeight = 0;
this.currentState = null;
this.networkId = config.networkId;
this.deposits = {};
this.exits = {};
this.tokens = {
erc20: [],
erc721: [],
erc1948: [],
};
this.epochLengths = [];
this.minGasPrices = [];
this.flags = flags(this, config.flagHeights);
this.onNewBlock = this.onNewBlock.bind(this);
this.eventsBuffer = new TinyQueue([], (a, b) => {
if (a.blockNumber === b.blockNumber) {
return a.logIndex - b.logIndex;
}
return a.blockNumber - b.blockNumber;
});
this.bridgeDelay = config.bridgeDelay;
this.eventsRelay = eventsRelay;
this.logsCache = {};
this.exitingUtxos = {};
this.currentPeriod = null;
this.periodProposal = null;
this.submissions = [];
this.lastBlocksRoot = null;
this.lastPeriodRoot = null;
this.lastProcessedPeriodRoot = GENESIS;
this.handleEvents = handleEvents({
NewDeposit: ({ returnValues: event }) => {
logBridge(
`NewDeposit. id: ${event.depositId} color: ${event.color} amount: ${event.amount} owner: ${event.depositor}`
);
this.deposits[event.depositId] = {
depositor: event.depositor,
color: event.color,
amount: event.amount,
};
},
NewDepositV2: ({ returnValues: event }) => {
logBridge(
`NewDepositV2. id: ${event.depositId} color: ${event.color} amount: ${event.amount} owner: ${event.depositor}`
);
this.deposits[event.depositId] = {
depositor: event.depositor,
color: event.color,
amount: event.amount,
data: event.data,
};
},
ExitStarted: ({ returnValues: event }) => {
logBridge(
`ExitStarted. color: ${event.color} amount: ${event.amount} utxoId: ${event.txHash}:${event.outIndex}`
);
const outpoint = new Outpoint(event.txHash, Number(event.outIndex));
this.exits[outpoint.getUtxoId()] = {
txHash: event.txHash,
outIndex: Number(event.outIndex),
exitor: event.exitor,
color: event.color,
amount: event.amount,
};
},
ExitStartedV2: ({ returnValues: event }) => {
logBridge(
`ExitStartedV2. color: ${event.color} amount: ${event.amount} utxoId: ${event.txHash}:${event.outIndex}`
);
const outpoint = new Outpoint(event.txHash, Number(event.outIndex));
this.exits[outpoint.getUtxoId()] = {
txHash: event.txHash,
outIndex: Number(event.outIndex),
exitor: event.exitor,
color: event.color,
amount: event.amount,
data: event.data,
};
},
NewToken: ({ returnValues: event }) => {
logBridge(`NewToken. color: ${event.color} addr: ${event.tokenAddr}`);
let array = this.tokens.erc20;
if (event.color >= NST_COLOR_BASE) {
array = this.tokens.erc1948;
} else if (event.color >= NFT_COLOR_BASE) {
array = this.tokens.erc721;
}
if (array.indexOf(event.tokenAddr) === -1) {
array.push(event.tokenAddr);
}
},
EpochLength: event => {
logBridge(
`EpochLength. epochLength: ${event.returnValues.epochLength}`
);
const { blockNumber, returnValues } = event;
this.epochLengths.push([
Number(returnValues.epochLength),
Number(blockNumber),
]);
},
MinGasPrice: ({ returnValues: event }) => {
logBridge(`MinGasPrice. minGasPrice: ${event.minGasPrice}`);
this.minGasPrices.push(Number(event.minGasPrice));
},
Submission: async ({ returnValues: event }) => {
logBridge(
`Submission. blocksRoot: ${event.blocksRoot} periodRoot: ${event.periodRoot}` +
` slotId: ${event.slotId} validator: ${event.owner} casBitmap: ${event.casBitmap}`
);
this.lastBlocksRoot = event.blocksRoot;
this.lastPeriodRoot = event.periodRoot;
const submission = {
casBitmap: event.casBitmap,
slotId: event.slotId,
validatorAddress: event.owner,
blocksRoot: event.blocksRoot,
periodRoot: event.periodRoot,
};
if (
this.periodProposal &&
this.stalePeriodProposal &&
this.stalePeriodProposal.blocksRoot === event.blocksRoot
) {
this.periodProposal.prevPeriodRoot = event.periodRoot;
await saveSubmission(this.stalePeriodProposal, submission, this.db);
} else if (
this.periodProposal &&
this.periodProposal.blocksRoot === event.blocksRoot
) {
await saveSubmission(this.periodProposal, submission, this.db);
} else {
this.submissions[event.blocksRoot] = submission;
}
},
});
this.handleExitingUtxos = handleEvents({
ExitStarted: ({ returnValues: event }) => {
const outpoint = new Outpoint(event.txHash, Number(event.outIndex));
this.exitingUtxos[outpoint.hex()] = {
txHash: event.txHash,
outIndex: Number(event.outIndex),
exitor: event.exitor,
color: event.color,
amount: event.amount,
};
},
});
}
async init() {
this.lastBlockSynced = await this.db.getLastBlockSynced();
const genesisBlock = await this.bridgeContract.methods
.genesisBlockNumber()
.call();
this.genesisBlockHeight = parseInt(genesisBlock, 10);
const contracts = [
this.operatorContract,
this.bridgeContract,
this.exitHandlerContract,
];
const nodeState = (await this.db.getNodeState()) || {};
logVerbose(`Restored node state`, nodeState);
this.periodProposal = nodeState.periodProposal || null;
this.stalePeriodProposal = await this.db.getStalePeriodProposal();
this.lastSeenRootChainBlock =
nodeState.rootChainBlockAtProposal || this.genesisBlockHeight;
this.blockHeight = nodeState.blockHeight;
logNode(`Syncing events from height ${this.lastSeenRootChainBlock}...`);
this.eventsSubscription = new ContractsEventsSubscription(
this.web3,
contracts,
this.eventsBuffer,
parseInt(genesisBlock, 10)
);
const blockNumber = await this.web3.eth.getBlockNumber();
const periodPrevHash =
nodeState.lastSeenBlocksRoot || this.lastBlocksRoot || GENESIS;
logNode('Current period prev root:', periodPrevHash);
this.currentPeriod = new Period(periodPrevHash);
await this.eventsSubscription.init();
await this.onNewBlock(blockNumber);
if (periodPrevHash === GENESIS) {
await this.initBlocks();
}
this.exitEventSubscription = new ContractsEventsSubscription(
this.web3,
[this.exitHandlerContract],
[],
blockNumber - this.bridgeDelay,
'ExitStarted'
);
this.exitEventSubscription.on('newEvents', this.handleExitingUtxos);
this.exitEventSubscription.init();
logNode('Synced');
}
async initBlocks() {
const blockOptions = {
timestamp: Math.round(Date.now() / 1000),
};
const blocks = [new Block(0, blockOptions), new Block(1, blockOptions)];
blocks.forEach(b => this.currentPeriod.addBlock(b));
await Promise.all(blocks.map(b => this.db.storeBlock(b)));
}
async onNewBlock(blockNumber) {
this.lastSeenRootChainBlock = blockNumber;
if (this.eventsBuffer.length === 0) {
return;
}
const events = [];
while (
this.eventsBuffer.peek().blockNumber <=
blockNumber - this.bridgeDelay
) {
const event = this.eventsBuffer.pop();
events.push(event);
if (this.eventsBuffer.length === 0) {
break;
}
}
await this.handleEvents(events);
// now push to second buffer
for (const event of events) {
this.eventsRelay.relayBuffer.push(event);
}
}
async saveNodeState() {
return this.db.storeNodeState({
periodProposal: this.periodProposal,
rootChainBlockAtProposal: this.lastSeenRootChainBlock,
lastSeenBlocksRoot: this.lastBlocksRoot,
blockHeight: this.currentState.blockHeight,
});
}
async saveLastSeenRootChainBlock() {
const lastRelayedBlock = this.eventsRelay.blockHeight;
return this.db.setLastSeenRootChainBlock(lastRelayedBlock);
}
async getLastSeenRootChainBlock() {
return this.db.getLastSeenRootChainBlock() || this.genesisBlockHeight;
}
async saveConsensusState() {
this.currentState.blockHeight = this.blockHeight;
await this.db.storeChainState(this.currentState);
}
async loadConsensusState() {
const res = await this.db.getChainState();
this.currentState = res || {
blockHeight: 0,
mempool: [],
balances: {}, // stores account balances like this { [colorIndex]: { address1: 0, ... } }
owners: {}, // index for NFT ownerOf call
unspent: {}, // stores unspent outputs (deposits, transfers)
slots: [],
epoch: {
epoch: 0,
lastEpochHeight: 0,
epochLength: null,
epochLengthIndex: -1,
},
gas: {
minPrice: 0,
minPriceIndex: -1,
},
};
return this.currentState;
}
};