UNPKG

@atomiqlabs/chain-evm

Version:

EVM specific base implementation

538 lines (459 loc) 23.6 kB
import {BitcoinNetwork, BitcoinRpc, BtcBlock, BtcRelay, RelaySynchronizer, StatePredictorUtils} from "@atomiqlabs/base"; import {EVMBtcHeader} from "./headers/EVMBtcHeader"; import {getLogger, tryWithRetries} from "../../utils/Utils"; import {EVMContractBase, TypedFunctionCall} from "../contract/EVMContractBase"; import {BtcRelay as BtcRelayTypechain} from "./BtcRelayTypechain"; import {EVMBtcStoredHeader} from "./headers/EVMBtcStoredHeader"; import {EVMSigner} from "../wallet/EVMSigner"; import {EVMTx, EVMTxTrace} from "../chain/modules/EVMTransactions"; import {EVMFees} from "../chain/modules/EVMFees"; import {EVMChainInterface} from "../chain/EVMChainInterface"; import {BtcRelayAbi} from "./BtcRelayAbi"; import {AbiCoder, hexlify} from "ethers"; import {PromiseLruCache} from "promise-cache-ts"; function serializeBlockHeader(e: BtcBlock): EVMBtcHeader { return new EVMBtcHeader({ version: e.getVersion(), previousBlockhash: Buffer.from(e.getPrevBlockhash(), "hex").reverse(), merkleRoot: Buffer.from(e.getMerkleRoot(), "hex").reverse(), timestamp: e.getTimestamp(), nbits: e.getNbits(), nonce: e.getNonce(), hash: Buffer.from(e.getHash(), "hex").reverse() }); } const logger = getLogger("EVMBtcRelay: "); export class EVMBtcRelay<B extends BtcBlock> extends EVMContractBase<BtcRelayTypechain> implements BtcRelay<EVMBtcStoredHeader, EVMTx, B, EVMSigner> { public static GasCosts = { GAS_PER_BLOCKHEADER: 30_000, GAS_BASE_MAIN: 15_000 + 21_000, GAS_PER_BLOCKHEADER_FORK: 65_000, GAS_PER_BLOCKHEADER_FORKED: 10_000, GAS_BASE_FORK: 25_000 + 21_000 } public async SaveMainHeaders(signer: string, mainHeaders: EVMBtcHeader[], storedHeader: EVMBtcStoredHeader, feeRate: string): Promise<EVMTx> { const tx = await this.contract.submitMainBlockheaders.populateTransaction(Buffer.concat([ storedHeader.serialize(), Buffer.concat(mainHeaders.map(header => header.serializeCompact())) ])); tx.from = signer; EVMFees.applyFeeRate(tx, EVMBtcRelay.GasCosts.GAS_BASE_MAIN + (EVMBtcRelay.GasCosts.GAS_PER_BLOCKHEADER * mainHeaders.length), feeRate); return tx; } public async SaveShortForkHeaders(signer: string, forkHeaders: EVMBtcHeader[], storedHeader: EVMBtcStoredHeader, feeRate: string): Promise<EVMTx> { const tx = await this.contract.submitShortForkBlockheaders.populateTransaction(Buffer.concat([ storedHeader.serialize(), Buffer.concat(forkHeaders.map(header => header.serializeCompact())) ])); tx.from = signer; EVMFees.applyFeeRate(tx, EVMBtcRelay.GasCosts.GAS_BASE_MAIN + (EVMBtcRelay.GasCosts.GAS_PER_BLOCKHEADER * forkHeaders.length), feeRate); return tx; } public async SaveLongForkHeaders(signer: string, forkId: number, forkHeaders: EVMBtcHeader[], storedHeader: EVMBtcStoredHeader, feeRate: string, totalForkHeaders: number = 100): Promise<EVMTx> { const tx = await this.contract.submitForkBlockheaders.populateTransaction(forkId, Buffer.concat([ storedHeader.serialize(), Buffer.concat(forkHeaders.map(header => header.serializeCompact())) ])); tx.from = signer; EVMFees.applyFeeRate(tx, EVMBtcRelay.GasCosts.GAS_BASE_FORK + (EVMBtcRelay.GasCosts.GAS_PER_BLOCKHEADER_FORK * forkHeaders.length) + (EVMBtcRelay.GasCosts.GAS_PER_BLOCKHEADER_FORKED * totalForkHeaders), feeRate); return tx; } bitcoinRpc: BitcoinRpc<B>; readonly maxHeadersPerTx: number = 100; readonly maxForkHeadersPerTx: number = 50; readonly maxShortForkHeadersPerTx: number = 100; constructor( chainInterface: EVMChainInterface<any>, bitcoinRpc: BitcoinRpc<B>, bitcoinNetwork: BitcoinNetwork, contractAddress: string, contractDeploymentHeight?: number ) { super(chainInterface, contractAddress, BtcRelayAbi, contractDeploymentHeight); this.bitcoinRpc = bitcoinRpc; } /** * Computes subsequent commited headers as they will appear on the blockchain when transactions * are submitted & confirmed * * @param initialStoredHeader * @param syncedHeaders * @private */ private computeCommitedHeaders(initialStoredHeader: EVMBtcStoredHeader, syncedHeaders: EVMBtcHeader[]) { const computedCommitedHeaders = [initialStoredHeader]; for(let blockHeader of syncedHeaders) { computedCommitedHeaders.push(computedCommitedHeaders[computedCommitedHeaders.length-1].computeNext(blockHeader)); } return computedCommitedHeaders; } /** * A common logic for submitting blockheaders in a transaction * * @param signer * @param headers headers to sync to the btc relay * @param storedHeader current latest stored block header for a given fork * @param tipWork work of the current tip in a given fork * @param forkId forkId to submit to, forkId=0 means main chain, forkId=-1 means short fork * @param feeRate feeRate for the transaction * @param totalForkHeaders Total number of headers in a fork * @private */ private async _saveHeaders( signer: string, headers: BtcBlock[], storedHeader: EVMBtcStoredHeader, tipWork: Buffer, forkId: number, feeRate: string, totalForkHeaders: number ) { const blockHeaderObj = headers.map(serializeBlockHeader); let tx: EVMTx; switch(forkId) { case -1: tx = await this.SaveShortForkHeaders(signer, blockHeaderObj, storedHeader, feeRate); break; case 0: tx = await this.SaveMainHeaders(signer, blockHeaderObj, storedHeader, feeRate); break; default: tx = await this.SaveLongForkHeaders(signer, forkId, blockHeaderObj, storedHeader, feeRate, totalForkHeaders); break; } const computedCommitedHeaders = this.computeCommitedHeaders(storedHeader, blockHeaderObj); const lastStoredHeader = computedCommitedHeaders[computedCommitedHeaders.length-1]; if(forkId!==0 && StatePredictorUtils.gtBuffer(lastStoredHeader.getChainWork(), tipWork)) { //Fork's work is higher than main chain's work, this fork will become a main chain forkId = 0; } return { forkId: forkId, lastStoredHeader, tx, computedCommitedHeaders } } private async findStoredBlockheaderInTraces(txTrace: EVMTxTrace, commitHash: string): Promise<EVMBtcStoredHeader> { if(txTrace.to.toLowerCase() === (await this.contract.getAddress()).toLowerCase()) { let dataBuffer: Buffer; if(txTrace.type==="CREATE") { dataBuffer = Buffer.from(txTrace.input.substring(txTrace.input.length-384, txTrace.input.length-64), "hex"); } else { const result = this.parseCalldata(txTrace.input); if(result!=null) { if(result.name==="submitMainBlockheaders" || result.name==="submitShortForkBlockheaders") { const functionCall: TypedFunctionCall< typeof this.contract.submitMainBlockheaders | typeof this.contract.submitShortForkBlockheaders > = result; dataBuffer = Buffer.from(hexlify(functionCall.args[0]).substring(2), "hex"); } else if(result.name==="submitForkBlockheaders") { const functionCall: TypedFunctionCall<typeof this.contract.submitForkBlockheaders> = result; dataBuffer = Buffer.from(hexlify(functionCall.args[1]).substring(2), "hex"); } } } if(dataBuffer!=null) { let storedHeader = EVMBtcStoredHeader.deserialize(dataBuffer.subarray(0, 160)); if(storedHeader.getCommitHash()===commitHash) return storedHeader; for(let i = 160; i < dataBuffer.length; i+=48) { const blockHeader = EVMBtcHeader.deserialize(dataBuffer.subarray(i, i + 48)); storedHeader = storedHeader.computeNext(blockHeader); if(storedHeader.getCommitHash()===commitHash) return storedHeader; } } } if(txTrace.calls!=null) { for(let call of txTrace.calls) { const result = await this.findStoredBlockheaderInTraces(call, commitHash); if(result!=null) return result; } } return null; } private commitHashCache: PromiseLruCache<string, [EVMBtcStoredHeader, string]> = new PromiseLruCache<string, [EVMBtcStoredHeader, string]>(1000); private blockHashCache: PromiseLruCache<string, [EVMBtcStoredHeader, string]> = new PromiseLruCache<string, [EVMBtcStoredHeader, string]>(1000); private getBlock(commitHash?: string, blockHash?: Buffer): Promise<[EVMBtcStoredHeader, string] | null> { const blockHashString = blockHash==null ? null : "0x"+Buffer.from([...blockHash]).reverse().toString("hex"); const generator = () => this.Events.findInContractEvents<[EVMBtcStoredHeader, string], "StoreHeader" | "StoreForkHeader">( ["StoreHeader", "StoreForkHeader"], [ commitHash, blockHashString ], async (event) => { const txTrace = await this.Chain.Transactions.traceTransaction(event.transactionHash); const storedBlockheader = await this.findStoredBlockheaderInTraces(txTrace, event.args.commitHash); if(storedBlockheader==null) return null; this.commitHashCache.set(event.args.commitHash, Promise.resolve([storedBlockheader, event.args.commitHash])); this.blockHashCache.set(event.args.blockHash, Promise.resolve([storedBlockheader, event.args.commitHash])); return [storedBlockheader, event.args.commitHash]; } ); if(commitHash!=null) return this.commitHashCache.getOrComputeAsync(commitHash, generator); if(blockHashString!=null) return this.blockHashCache.getOrComputeAsync(blockHashString, generator); return null; } private async getBlockHeight(): Promise<number> { return Number(await this.contract.getBlockheight()); } /** * Returns data about current main chain tip stored in the btc relay */ public async getTipData(): Promise<{ commitHash: string; blockhash: string, chainWork: Buffer, blockheight: number }> { const commitHash = await this.contract.getTipCommitHash(); if(commitHash==null || BigInt(commitHash)===BigInt(0)) return null; const result = await this.getBlock(commitHash); if(result==null) return null; const storedBlockHeader = result[0]; return { blockheight: storedBlockHeader.getBlockheight(), commitHash: commitHash, blockhash: storedBlockHeader.getBlockHash().toString("hex"), chainWork: storedBlockHeader.getChainWork() }; } /** * Retrieves blockheader with a specific blockhash, returns null if requiredBlockheight is provided and * btc relay contract is not synced up to the desired blockheight * * @param blockData * @param requiredBlockheight */ public async retrieveLogAndBlockheight(blockData: {blockhash: string}, requiredBlockheight?: number): Promise<{ header: EVMBtcStoredHeader, height: number }> { //TODO: we can fetch the blockheight and events in parallel const blockHeight = await this.getBlockHeight(); if(requiredBlockheight!=null && blockHeight < requiredBlockheight) { return null; } const result = await this.getBlock(null, Buffer.from(blockData.blockhash, "hex")); if(result==null) return null; const [storedBlockHeader, commitHash] = result; //Check if block is part of the main chain const chainCommitment = await this.contract.getCommitHash(storedBlockHeader.blockHeight); if(chainCommitment!==commitHash) return null; logger.debug("retrieveLogAndBlockheight(): block found," + " commit hash: "+commitHash+" blockhash: "+blockData.blockhash+" current btc relay height: "+blockHeight); return {header: storedBlockHeader, height: blockHeight}; } /** * Retrieves blockheader data by blockheader's commit hash, * * @param commitmentHashStr * @param blockData */ public async retrieveLogByCommitHash(commitmentHashStr: string, blockData: {blockhash: string}): Promise<EVMBtcStoredHeader> { const result = await this.getBlock(commitmentHashStr, Buffer.from(blockData.blockhash, "hex")); if(result==null) return null; const [storedBlockHeader, commitHash] = result; //Check if block is part of the main chain const chainCommitment = await this.contract.getCommitHash(storedBlockHeader.blockHeight); if(chainCommitment!==commitHash) return null; logger.debug("retrieveLogByCommitHash(): block found," + " commit hash: "+commitmentHashStr+" blockhash: "+blockData.blockhash+" height: "+storedBlockHeader.blockHeight); return storedBlockHeader; } /** * Retrieves latest known stored blockheader & blockheader from bitcoin RPC that is in the main chain */ public async retrieveLatestKnownBlockLog(): Promise<{ resultStoredHeader: EVMBtcStoredHeader, resultBitcoinHeader: B }> { const data = await this.Events.findInContractEvents( ["StoreHeader", "StoreForkHeader"], null, async (event) => { const blockHashHex = Buffer.from(event.args.blockHash.substring(2), "hex").reverse().toString("hex"); const commitHash = event.args.commitHash; const isInBtcMainChain = await this.bitcoinRpc.isInMainChain(blockHashHex).catch(() => false); if(!isInBtcMainChain) return null; const blockHeader = await this.bitcoinRpc.getBlockHeader(blockHashHex); if(commitHash !== await this.contract.getCommitHash(blockHeader.getHeight())) return null; const txTrace = await this.Chain.Transactions.traceTransaction(event.transactionHash); const storedHeader = await this.findStoredBlockheaderInTraces(txTrace, commitHash); if(storedHeader==null) return null; return { resultStoredHeader: storedHeader, resultBitcoinHeader: blockHeader, commitHash: commitHash } } ) if(data!=null) logger.debug("retrieveLatestKnownBlockLog(): block found," + " commit hash: "+data.commitHash+" blockhash: "+data.resultBitcoinHeader.getHash()+ " height: "+data.resultStoredHeader.getBlockheight()); return data; } /** * Saves blockheaders as a bitcoin main chain to the btc relay * * @param signer * @param mainHeaders * @param storedHeader * @param feeRate */ public async saveMainHeaders(signer: string, mainHeaders: BtcBlock[], storedHeader: EVMBtcStoredHeader, feeRate?: string) { feeRate ??= await this.Chain.Fees.getFeeRate(); logger.debug("saveMainHeaders(): submitting main blockheaders, count: "+mainHeaders.length); return this._saveHeaders(signer, mainHeaders, storedHeader, null, 0, feeRate, 0); } /** * Creates a new long fork and submits the headers to it * * @param signer * @param forkHeaders * @param storedHeader * @param tipWork * @param feeRate */ public async saveNewForkHeaders(signer: string, forkHeaders: BtcBlock[], storedHeader: EVMBtcStoredHeader, tipWork: Buffer, feeRate?: string) { let forkId: number = Math.floor(Math.random() * 0xFFFFFFFFFFFF); feeRate ??= await this.Chain.Fees.getFeeRate(); logger.debug("saveNewForkHeaders(): submitting new fork & blockheaders," + " count: "+forkHeaders.length+" forkId: 0x"+forkId.toString(16)); return await this._saveHeaders(signer, forkHeaders, storedHeader, tipWork, forkId, feeRate, forkHeaders.length); } /** * Continues submitting blockheaders to a given fork * * @param signer * @param forkHeaders * @param storedHeader * @param forkId * @param tipWork * @param feeRate */ public async saveForkHeaders(signer: string, forkHeaders: BtcBlock[], storedHeader: EVMBtcStoredHeader, forkId: number, tipWork: Buffer, feeRate?: string) { feeRate ??= await this.Chain.Fees.getFeeRate(); logger.debug("saveForkHeaders(): submitting blockheaders to existing fork," + " count: "+forkHeaders.length+" forkId: 0x"+forkId.toString(16)); return this._saveHeaders(signer, forkHeaders, storedHeader, tipWork, forkId, feeRate, 100); } /** * Submits short fork with given blockheaders * * @param signer * @param forkHeaders * @param storedHeader * @param tipWork * @param feeRate */ public async saveShortForkHeaders(signer: string, forkHeaders: BtcBlock[], storedHeader: EVMBtcStoredHeader, tipWork: Buffer, feeRate?: string) { feeRate ??= await this.Chain.Fees.getFeeRate(); logger.debug("saveShortForkHeaders(): submitting short fork blockheaders," + " count: "+forkHeaders.length); return this._saveHeaders(signer, forkHeaders, storedHeader, tipWork, -1, feeRate, 0); } /** * Estimate required synchronization fee (worst case) to synchronize btc relay to the required blockheight * * @param requiredBlockheight * @param feeRate */ public async estimateSynchronizeFee(requiredBlockheight: number, feeRate?: string): Promise<bigint> { feeRate ??= await this.Chain.Fees.getFeeRate(); const tipData = await this.getTipData(); const currBlockheight = tipData.blockheight; const blockheightDelta = requiredBlockheight-currBlockheight; if(blockheightDelta<=0) return 0n; const synchronizationFee = (BigInt(blockheightDelta) * await this.getFeePerBlock(feeRate)) + EVMFees.getGasFee(EVMBtcRelay.GasCosts.GAS_BASE_MAIN * Math.ceil(blockheightDelta / this.maxHeadersPerTx), feeRate); logger.debug("estimateSynchronizeFee(): required blockheight: "+requiredBlockheight+ " blockheight delta: "+blockheightDelta+" fee: "+synchronizationFee.toString(10)); return synchronizationFee; } /** * Returns fee required (in native token) to synchronize a single block to btc relay * * @param feeRate */ public async getFeePerBlock(feeRate?: string): Promise<bigint> { feeRate ??= await this.Chain.Fees.getFeeRate(); return EVMFees.getGasFee(EVMBtcRelay.GasCosts.GAS_PER_BLOCKHEADER, feeRate); } /** * Gets fee rate required for submitting blockheaders to the main chain */ public getMainFeeRate(signer: string | null): Promise<string> { return this.Chain.Fees.getFeeRate(); } /** * Gets fee rate required for submitting blockheaders to the specific fork */ public getForkFeeRate(signer: string, forkId: number): Promise<string> { return this.Chain.Fees.getFeeRate(); } saveInitialHeader(signer: string, header: B, epochStart: number, pastBlocksTimestamps: number[], feeRate?: string): Promise<EVMTx> { throw new Error("Not supported, EVM contract is initialized with constructor!"); } /** * Gets committed header, identified by blockhash & blockheight, determines required BTC relay blockheight based on * requiredConfirmations * If synchronizer is passed & blockhash is not found, it produces transactions to sync up the btc relay to the * current chain tip & adds them to the txs array * * @param signer * @param btcRelay * @param btcTxs * @param txs solana transaction array, in case we need to synchronize the btc relay ourselves the synchronization * txns are added here * @param synchronizer optional synchronizer to use to synchronize the btc relay in case it is not yet synchronized * to the required blockheight * @param feeRate Fee rate to use for synchronization transactions * @private */ static async getCommitedHeadersAndSynchronize( signer: string, btcRelay: EVMBtcRelay<any>, btcTxs: {blockheight: number, requiredConfirmations: number, blockhash: string}[], txs: EVMTx[], synchronizer?: RelaySynchronizer<EVMBtcStoredHeader, EVMTx, any>, feeRate?: string ): Promise<{ [blockhash: string]: EVMBtcStoredHeader }> { const leavesTxs: {blockheight: number, requiredConfirmations: number, blockhash: string}[] = []; const blockheaders: { [blockhash: string]: EVMBtcStoredHeader } = {}; for(let btcTx of btcTxs) { const requiredBlockheight = btcTx.blockheight+btcTx.requiredConfirmations-1; const result = await tryWithRetries( () => btcRelay.retrieveLogAndBlockheight({ blockhash: btcTx.blockhash }, requiredBlockheight) ); if(result!=null) { blockheaders[result.header.getBlockHash().toString("hex")] = result.header; } else { leavesTxs.push(btcTx); } } if(leavesTxs.length===0) return blockheaders; //Need to synchronize if(synchronizer==null) return null; //TODO: We don't have to synchronize to tip, only to our required blockheight const resp = await synchronizer.syncToLatestTxs(signer.toString(), feeRate); logger.debug("getCommitedHeaderAndSynchronize(): BTC Relay not synchronized to required blockheight, "+ "synchronizing ourselves in "+resp.txs.length+" txs"); logger.debug("getCommitedHeaderAndSynchronize(): BTC Relay computed header map: ",resp.computedHeaderMap); txs.push(...resp.txs); for(let key in resp.computedHeaderMap) { const header = resp.computedHeaderMap[key]; blockheaders[header.getBlockHash().toString("hex")] = header; } //Check that blockhashes of all the rest txs are included for(let btcTx of leavesTxs) { if(blockheaders[btcTx.blockhash]==null) return null; } //Retrieve computed headers return blockheaders; } }