diff --git a/README.md b/README.md index 685b7c5..f7ee88d 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,102 @@ const newParams = { monitor.reset(newParams); ``` + +## Pegout Tracker + +You can use the `tool/pegout-tracker/pegout-tracker.js` to track a pegout request, from start to finish. + +To use it, you only need a pegout transaction hash and a network. Then, you can call `PegoutTarcker::trackPegout` and subscribe to the `PEGOUT_TRACKER_EVENTS.pegoutStagesFound` event, like this: + +```js +const util = require('util'); +const PegoutTracker = require('./pegout-tracker'); +const { PEGOUT_TRACKER_EVENTS } = require('./pegout-tracker-utils'); + +const pegoutTxHash = '0x...'; // Needs to be the tx hash that the user got when sending funds to the bridge to request a pegout. +const network = 'mainnet'; // Can be 'mainnet' or 'testnet' + +const pegoutTracker = new PegoutTracker(); + +// This will be executed once all the pegout information has been gathered. +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.pegoutStagesFound, bridgeTxDetails => { + console.info('pegoutStagesFound: ') + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.trackPegout(pegoutTxHash, network); + +``` + +If no `network` is provided, `mainnet` will be the default. + +Or, you can subscribe to all the stages events, like this: + +```js + +const util = require('util'); +const PegoutTracker = require('./pegout-tracker'); +const { PEGOUT_TRACKER_EVENTS } = require('./pegout-tracker-utils'); + +const pegoutTxHash = '0x...'; // Needs to be the tx hash that the user got when sending funds to the bridge to request a pegout. +const network = 'mainnet'; // Can be 'mainnet' or 'testnet' + +const pegoutTracker = new PegoutTracker(); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseRequestRejectedEventFound, bridgeTxDetails => { + console.info('Pegout stage 1 (pegout request rejected) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseRequestReceivedEventFound, bridgeTxDetails => { + console.info('Pegout stage 1 (pegout request) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseRequestedEventFound, bridgeTxDetails => { + console.info('Pegout stage 2 (pegout created) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.batchPegoutCreatedEventFound, bridgeTxDetails => { + console.info('Pegout stage 2 (batch pegout created) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.pegoutConfirmedEventFound, bridgeTxDetails => { + console.info('Pegout stage 3 (confirmations) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.addSignatureEventFound, bridgeTxDetails => { + console.info('Pegout stage 4 (signatures) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseBtcEventFound, bridgeTxDetails => { + console.info('Pegout stage 4 (release) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.pegoutStagesFound, bridgeTxDetails => { + console.info('pegoutStagesFound: ') + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.trackPegout(pegoutTxHash, network); + +``` + +Or, you can use the `cli-pegout-tracker` tool, like this: + +```sh +node tool/pegout-tracker/cli-pegout-tracker.js --pegoutTxHash=0xa6397d264cae18a1b3ace7a33b24580fd759c075ee27feb7eb9e6d19f50ff3ee --network=mainnet +``` + +If no `--network` is provided, `mainnet` will be the default. + +Note: Before using the tool to find a pegout information, make sure the pegout has already been completed. Because the tool will try skiping blocks, will go to future blocks based on `nextPegoutHeight` and the amount of confirmations required for each network. If it tries to go to a non existing block, then the tool will throw an error. + ## Contributing Any comments or suggestions feel free to contribute or reach out at our [open slack](https://dev.rootstock.io//slack). diff --git a/tool/pegout-tracker/cli-pegout-tracker.js b/tool/pegout-tracker/cli-pegout-tracker.js new file mode 100644 index 0000000..22281de --- /dev/null +++ b/tool/pegout-tracker/cli-pegout-tracker.js @@ -0,0 +1,64 @@ +const util = require('util'); +const PegoutTracker = require('./pegout-tracker'); +const { PEGOUT_TRACKER_EVENTS } = require('./pegout-tracker-utils'); + +const getParsedParams = () => { + const params = process.argv.filter(param => param.startsWith('--')) + .reduce((params, param) => { + if(param.startsWith('--pegoutTxHash')) { + params.pegoutTxHash = param.slice(param.indexOf('=') + 1); + } else if(param.startsWith('--network')) { + params.network = param.slice(param.indexOf('=') + 1); + } + return params; + }, {}); + return params; +}; + +const params = getParsedParams(); + +const pegoutTracker = new PegoutTracker(); + +const { pegoutTxHash, network } = params; + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseRequestRejectedEventFound, bridgeTxDetails => { + console.info('Pegout stage 1 (pegout request rejected) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseRequestReceivedEventFound, bridgeTxDetails => { + console.info('Pegout stage 1 (pegout request) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseRequestedEventFound, bridgeTxDetails => { + console.info('Pegout stage 2 (pegout created) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.batchPegoutCreatedEventFound, bridgeTxDetails => { + console.info('Pegout stage 2 (batch pegout created) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.pegoutConfirmedEventFound, bridgeTxDetails => { + console.info('Pegout stage 3 (confirmations) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.addSignatureEventFound, bridgeTxDetails => { + console.info('Pegout stage 4 (signatures) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.releaseBtcEventFound, bridgeTxDetails => { + console.info('Pegout stage 4 (release) transaction found: '); + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.on(PEGOUT_TRACKER_EVENTS.pegoutStagesFound, bridgeTxDetails => { + console.info('pegoutStagesFound: ') + console.info(util.inspect(bridgeTxDetails, {depth: null, colors: true})); +}); + +pegoutTracker.trackPegout(pegoutTxHash, network); diff --git a/tool/pegout-tracker/pegout-tracker-utils.js b/tool/pegout-tracker/pegout-tracker-utils.js new file mode 100644 index 0000000..ba3482e --- /dev/null +++ b/tool/pegout-tracker/pegout-tracker-utils.js @@ -0,0 +1,20 @@ +const PEGOUT_TRACKER_EVENTS = { + releaseRequestRejectedEventFound: 'releaseRequestRejectedEventFound', + releaseRequestReceivedEventFound: 'releaseRequetReceivedEventFound', + releaseRequestedEventFound: 'releaseRequestedEventFound', + batchPegoutCreatedEventFound: 'batchPegoutCreatedEventFound', + pegoutConfirmedEventFound: 'pegoutConfirmedEventFound', + addSignatureEventFound: 'addSignatureEventFound', + releaseBtcEventFound: 'releaseBtcEventFound', + pegoutStagesFound: 'pegoutStagesFound', +}; + +const NETWORK_REQUIRED_CONFIRMATIONS = { + mainnet: 4000, + testnet: 10, +}; + +module.exports = { + PEGOUT_TRACKER_EVENTS, + NETWORK_REQUIRED_CONFIRMATIONS, +}; diff --git a/tool/pegout-tracker/pegout-tracker.js b/tool/pegout-tracker/pegout-tracker.js new file mode 100644 index 0000000..7ae4340 --- /dev/null +++ b/tool/pegout-tracker/pegout-tracker.js @@ -0,0 +1,175 @@ + +const Web3 = require('web3'); +const BridgeTransactionParser = require('../../index'); +const EventEmitter = require('node:events'); +const LiveMonitor = require('../live-monitor/live-monitor'); +const { MONITOR_EVENTS } = require('../live-monitor/live-monitor-utils'); + +const { + isPegoutRequestRejectedTx, + isPegoutRequestReceivedTx, + isPegoutCreatedTx, + isPegoutConfirmedTx, + isAddSignatureTx, + isReleaseBtcTx, + getBridgeStorageValueDecodedToNumber, + bridgeStateKeysToStorageIndexMap, + getBridgeStorageAtBlock, +} = require('../../utils'); +const networkParser = require('../network-parser'); + +const { + PEGOUT_TRACKER_EVENTS, + NETWORK_REQUIRED_CONFIRMATIONS, +} = require('./pegout-tracker-utils'); + +const defaultLiveMonitorParamsValues = { + pegout: true, + network: 'https://public-node.rsk.co/', + checkEveryMilliseconds: 100, + retryOnError: true, + retryOnErrorAttempts: 3, +}; + +class PegoutTracker extends EventEmitter { + + constructor() { + super(); + this.started = false; + } + + /** + * + * @param {string} pegoutTxHash + * @param {mainnet | testnet} network + */ + async trackPegout(pegoutTxHash, network = 'mainnet') { + + if(!pegoutTxHash) { + throw new Error('pegoutTxHash is required'); + } + + if(this.started) { + console.warn(`Pegout tracker is already started. Stop before calling ${trackPegout.name} again. Ignoring...`); + return; + } + + this.started = true; + + pegoutTxHash = pegoutTxHash.toLowerCase(); + + const pegoutInfo = []; + + const networkUrl = networkParser(network); + + const rskClient = new Web3(networkUrl); + + const bridgeTransactionParser = new BridgeTransactionParser(rskClient); + + const rskTx = await bridgeTransactionParser.getBridgeTransactionByTxHash(pegoutTxHash); + + if(isPegoutRequestRejectedTx(rskTx)) { + this.emit(PEGOUT_TRACKER_EVENTS.releaseRequestRejectedEventFound, rskTx); + return; + } + + if(!isPegoutRequestReceivedTx(rskTx)) { + throw new Error(`Transaction ${pegoutTxHash} is not a release request received transaction`); + } + + pegoutInfo.push(rskTx); + + this.emit(PEGOUT_TRACKER_EVENTS.releaseRequestReceivedEventFound, rskTx); + + const nextPegoutCreationHeightRlpEncoded = await getBridgeStorageAtBlock(rskClient, bridgeStateKeysToStorageIndexMap.nextPegoutHeight.bytes, rskTx.blockNumber); + const nextPegoutCreationHeight = getBridgeStorageValueDecodedToNumber(nextPegoutCreationHeightRlpEncoded); + + const latestBlockNumber = await rskClient.eth.getBlockNumber(); + + if(nextPegoutCreationHeight > latestBlockNumber) { + throw new Error(`Next pegout creation height ${nextPegoutCreationHeight} is greater than latest block number ${latestBlockNumber}`); + } + + const params = { ...defaultLiveMonitorParamsValues, network: networkUrl, fromBlock: nextPegoutCreationHeight }; + + let stage = 2; + + const liveMonitor = new LiveMonitor(params); + + let stage2TxHash; + let stage2BlockNumber; + let stage2BtcTxHash; + + this.on('stop', () => { + liveMonitor.stop(); + this.started = false; + }); + + liveMonitor.on(MONITOR_EVENTS.filterMatched, async bridgeTxDetails => { + switch(stage) { + case 2: + if(isPegoutCreatedTx(bridgeTxDetails)) { + const batchPegoutCreatedEventArguments = bridgeTxDetails.events[2].arguments; + if(batchPegoutCreatedEventArguments.releaseRskTxHashes.includes(pegoutTxHash.slice(2))) { + stage = 3; + pegoutInfo.push(bridgeTxDetails); + stage2TxHash = bridgeTxDetails.txHash; + stage2BlockNumber = bridgeTxDetails.blockNumber; + stage2BtcTxHash = batchPegoutCreatedEventArguments.btcTxHash; + + const afterConfirmationBlock = stage2BlockNumber + NETWORK_REQUIRED_CONFIRMATIONS[network]; + + const latestBlockNumber = await rskClient.eth.getBlockNumber(); + + if(afterConfirmationBlock > latestBlockNumber) { + throw new Error(`Expected after confirmation height ${afterConfirmationBlock} is greater than latest block number ${latestBlockNumber}`); + } + + const newParams = { ...params, fromBlock: stage2BlockNumber + NETWORK_REQUIRED_CONFIRMATIONS[network] }; + liveMonitor.reset(newParams); + this.emit(PEGOUT_TRACKER_EVENTS.releaseRequestedEventFound, bridgeTxDetails); + } + } + break; + case 3: + if(isPegoutConfirmedTx(bridgeTxDetails)) { + const pegoutConfirmedEventArguments = bridgeTxDetails.events[1].arguments; + if(pegoutConfirmedEventArguments.btcTxHash === stage2BtcTxHash) { + stage = 4; + pegoutInfo.push(bridgeTxDetails); + this.emit(PEGOUT_TRACKER_EVENTS.pegoutConfirmedEventFound, bridgeTxDetails); + } + } + break; + case 4: + if(isAddSignatureTx(bridgeTxDetails)) { + const pegoutAddSignatureEventArguments = bridgeTxDetails.events[0].arguments; + if(pegoutAddSignatureEventArguments.releaseRskTxHash === stage2TxHash) { + pegoutInfo.push(bridgeTxDetails); + this.emit(PEGOUT_TRACKER_EVENTS.addSignatureEventFound, bridgeTxDetails); + } + } else if(isReleaseBtcTx(bridgeTxDetails)) { + const pegoutReleaseBtcEventArguments = bridgeTxDetails.events[1].arguments; + if(pegoutReleaseBtcEventArguments.releaseRskTxHash === stage2TxHash) { + pegoutInfo.push(bridgeTxDetails); + this.emit(PEGOUT_TRACKER_EVENTS.releaseBtcEventFound, bridgeTxDetails); + this.emit(PEGOUT_TRACKER_EVENTS.pegoutStagesFound, pegoutInfo); + this.started = false; + liveMonitor.stop(); + } + } + break; + } + }); + + liveMonitor.start(params); + + } + + stop() { + this.emit('stop'); + } + +} + +module.exports = PegoutTracker; diff --git a/utils.js b/utils.js index c9579b7..7202d1f 100644 --- a/utils.js +++ b/utils.js @@ -1,13 +1,112 @@ +const RLP = require('rlp'); +const Bridge = require('@rsksmart/rsk-precompiled-abis').bridge; + const verifyHashOrBlockNumber = (blockHashOrBlockNumber) => { - if (typeof blockHashOrBlockNumber === "string" && - blockHashOrBlockNumber.indexOf("0x") === 0 && + if (typeof blockHashOrBlockNumber === 'string' && + blockHashOrBlockNumber.indexOf('0x') === 0 && blockHashOrBlockNumber.length !== 66) { throw new Error('Hash must be of length 66 starting with "0x"'); } else if (isNaN(blockHashOrBlockNumber) || blockHashOrBlockNumber <= 0) { - throw new Error("Block number must be greater than 0"); + throw new Error('Block number must be greater than 0'); } }; +const bridgeStateKeysToStorageIndexMap = { + newFederationBtcUTXOs: { label: 'newFederationBtcUTXOs', bytes: '0x00000000000000000000006e657746656465726174696f6e4274635554584f73' }, + oldFederationBtcUTXOs: { label: 'oldFederationBtcUTXOs', bytes: '0x00000000000000000000006f6c6446656465726174696f6e4274635554584f73' }, + releaseRequestQueue: { label: 'pegoutRequests', bytes: '0x0000000000000000000000000072656c65617365526571756573745175657565'}, + releaseRequestQueueWithTxHash: { label: 'pegoutRequests', bytes: '0x00000072656c6561736552657175657374517565756557697468547848617368'}, + releaseTransactionSet: { label: 'pegoutsWaitingForConfirmations', bytes: '0x000000000000000000000072656c656173655472616e73616374696f6e536574' }, // + releaseTransactionSetWithTxHash: { label: 'pegoutsWaitingForConfirmations', bytes: '0x0072656c656173655472616e73616374696f6e53657457697468547848617368' }, + rskTxsWaitingFS: { label: 'pegoutsWaitingForSignatures', bytes: '0x000000000000000000000000000000000072736b54787357616974696e674653'}, + lockingCap: { label: 'lockingCap', bytes: '0x000000000000000000000000000000000000000000006c6f636b696e67436170' }, + nextPegoutHeight: { label: 'nextPegoutHeight', bytes: '0x000000000000000000000000000000006e6578745065676f7574486569676874' }, + newFederation: { label: 'newFederation', bytes: '0x000000000000000000000000000000000000006e657746656465726174696f6e' }, +}; + +const PEGOUT_EVENTS = { + release_request_rejected: 'release_request_rejected', + release_request_received: 'release_request_received', + release_requested: 'release_requested', + batch_pegout_created: 'batch_pegout_created', + pegout_confirmed: 'pegout_confirmed', + add_signature: 'add_signature', + release_btc: 'release_btc', +}; + +const removeEmptyLeftBytes = (storageValue) => { + return `0x${storageValue.replaceAll(/^0x0+/g, '')}`; +}; + +const bytesToHexString = (bytes) => { + return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); +}; + +const decodeRlp = (rlpEncoded) => { + const uint8ArrayDecoded = RLP.decode(rlpEncoded); + const bytesStr = bytesToHexString(uint8ArrayDecoded); + return bytesStr; +}; + +const bytesStrToNumber = (bytesStr) => { + return parseInt(bytesStr, 16); +}; + +const getBridgeStorageValueDecodedHexString = (bridgeStorageValueEncodedAsRlp, append0xPrefix = true) => { + const rlpBytesWithoutEmptyBytes = removeEmptyLeftBytes(bridgeStorageValueEncodedAsRlp); + const decodedHexFromRlp = decodeRlp(rlpBytesWithoutEmptyBytes); + return append0xPrefix ? `0x${decodedHexFromRlp}` : decodedHexFromRlp; +}; + +const getBridgeStorageValueDecodedToNumber = (bridgeStorageValueEncodedAsRlp) => { + const rlpBytesWithoutEmptyBytes = removeEmptyLeftBytes(bridgeStorageValueEncodedAsRlp); + const decodedHexFromRlp = decodeRlp(rlpBytesWithoutEmptyBytes); + return bytesStrToNumber(decodedHexFromRlp); +}; + +const isPegoutRequestRejectedTx = (tx) => { + return tx && tx.method === '' && tx.events.length === 1 && tx.events[0].name === PEGOUT_EVENTS.release_request_rejected; +}; + +const isPegoutRequestReceivedTx = (tx) => { + return tx && tx.method === '' && tx.events.length === 1 && tx.events[0].name === PEGOUT_EVENTS.release_request_received; +}; + +const isPegoutCreatedTx = (tx) => { + return tx && tx.method.name === 'updateCollections' && tx.events.length === 3 && tx.events[1].name === PEGOUT_EVENTS.release_requested; +}; + +const isPegoutConfirmedTx = (tx) => { + return tx && tx.method.name === 'updateCollections' && tx.events.length === 2 && tx.events[1].name === PEGOUT_EVENTS.pegout_confirmed; +}; + +const isAddSignatureTx = (tx) => { + return tx && tx.method.name === 'addSignature' && tx.events.length === 1 && tx.events[0].name === PEGOUT_EVENTS.add_signature; +}; + +const isReleaseBtcTx = (tx) => { + return tx && tx.method.name === 'addSignature' && tx.events.length === 2 && tx.events[1].name === PEGOUT_EVENTS.release_btc; +}; + +const getBridgeStorageAtBlock = async (web3, storageKey, atBlock) => { + const result = await web3.eth.getStorageAt(Bridge.address, storageKey, atBlock); + return result; +}; + module.exports = { verifyHashOrBlockNumber, + removeEmptyLeftBytes, + bytesToHexString, + decodeRlp, + bytesStrToNumber, + getBridgeStorageValueDecodedHexString, + getBridgeStorageValueDecodedToNumber, + isPegoutRequestRejectedTx, + isPegoutRequestReceivedTx, + isPegoutCreatedTx, + isPegoutConfirmedTx, + isAddSignatureTx, + isReleaseBtcTx, + bridgeStateKeysToStorageIndexMap, + getBridgeStorageAtBlock, };