UNPKG

@atomiqlabs/chain-starknet

Version:
537 lines (463 loc) 23.2 kB
import {Buffer} from "buffer"; import {StarknetBtcHeader} from "./headers/StarknetBtcHeader"; import {BitcoinNetwork, BitcoinRpc, BtcBlock, BtcRelay, RelaySynchronizer, StatePredictorUtils} from "@atomiqlabs/base"; import { bigNumberishToBuffer, bufferToU32Array, getLogger, toHex, u32ReverseEndianness } from "../../utils/Utils"; import {StarknetContractBase} from "../contract/StarknetContractBase"; import {StarknetBtcStoredHeader} from "./headers/StarknetBtcStoredHeader"; import {StarknetTx} from "../chain/modules/StarknetTransactions"; import {StarknetSigner} from "../wallet/StarknetSigner"; import {BtcRelayAbi} from "./BtcRelayAbi"; import {BigNumberish, hash} from "starknet"; import {StarknetFees} from "../chain/modules/StarknetFees"; import {StarknetChainInterface} from "../chain/StarknetChainInterface"; import {StarknetAction} from "../chain/StarknetAction"; function serializeBlockHeader(e: BtcBlock): StarknetBtcHeader { return new StarknetBtcHeader({ reversed_version: u32ReverseEndianness(e.getVersion()), previous_blockhash: bufferToU32Array(Buffer.from(e.getPrevBlockhash(), "hex").reverse()), merkle_root: bufferToU32Array(Buffer.from(e.getMerkleRoot(), "hex").reverse()), reversed_timestamp: u32ReverseEndianness(e.getTimestamp()), nbits: u32ReverseEndianness(e.getNbits()), nonce: u32ReverseEndianness(e.getNonce()), hash: Buffer.from(e.getHash(), "hex").reverse() }); } const GAS_PER_BLOCKHEADER = {l1DataGas: 600, l2Gas: 24_000_000, l1Gas: 0}; const GAS_PER_BLOCKHEADER_FORK_REORG = {l1DataGas: 1000, l2Gas: 4_000_000, l1Gas: 0}; const btcRelayAddreses: {[network in BitcoinNetwork]?: string} = { [BitcoinNetwork.TESTNET4]: "0x0099b63f39f0cabb767361de3d8d3e97212351a51540e2687c2571f4da490dbe", [BitcoinNetwork.TESTNET]: "0x068601c79da2231d21e015ccfd59c243861156fa523a12c9f987ec28eb8dbc8c", [BitcoinNetwork.MAINNET]: "0x057b14a4231b82f1e525ff35a722d893ca3dd2bde0baa6cee97937c5be861dbc" }; const btcRelayDeploymentHeights: {[network in BitcoinNetwork]?: number} = { [BitcoinNetwork.TESTNET4]: 760719, [BitcoinNetwork.TESTNET]: 633915, [BitcoinNetwork.MAINNET]: 1278562 }; function serializeCalldata(headers: StarknetBtcHeader[], storedHeader: StarknetBtcStoredHeader, span: BigNumberish[]) { span.push(toHex(headers.length)); headers.forEach(header => { span.push(...header.serialize()); }); span.push(...storedHeader.serialize()); return span; } const logger = getLogger("StarknetBtcRelay: "); /** * Starknet BTC Relay bitcoin light client contract representation * * @category BTC Relay */ export class StarknetBtcRelay<B extends BtcBlock> extends StarknetContractBase<typeof BtcRelayAbi> implements BtcRelay<StarknetBtcStoredHeader, StarknetTx, B, StarknetSigner> { /** * Returns a {@link StarknetAction} that submits new main chain bitcoin blockheaders to the light client * * @param signer Starknet signer's address * @param mainHeaders New bitcoin blockheaders to submit * @param storedHeader Current latest committed and stored bitcoin blockheader in the light client */ private SaveMainHeaders(signer: string, mainHeaders: StarknetBtcHeader[], storedHeader: StarknetBtcStoredHeader): StarknetAction { return new StarknetAction(signer, this.Chain, { contractAddress: this.contract.address, entrypoint: "submit_main_blockheaders", calldata: serializeCalldata(mainHeaders, storedHeader, []) }, StarknetFees.starknetGasMul(GAS_PER_BLOCKHEADER, mainHeaders.length) ) } /** * Returns a {@link StarknetAction} for submitting a short fork bitcoin blockheaders to the light client, * forking the chain from the provided `storedHeader` param's blockheight. For a successful fork the * submitted chain needs to have higher total chainwork than the current cannonical chain * * @param signer Starknet signer's address * @param forkHeaders Fork bitcoin blockheaders to submit * @param storedHeader Committed and stored bitcoin blockheader from which to fork the light client */ private SaveShortForkHeaders(signer: string, forkHeaders: StarknetBtcHeader[], storedHeader: StarknetBtcStoredHeader): StarknetAction { return new StarknetAction(signer, this.Chain, { contractAddress: this.contract.address, entrypoint: "submit_short_fork_blockheaders", calldata: serializeCalldata(forkHeaders, storedHeader, []) }, StarknetFees.starknetGasMul(GAS_PER_BLOCKHEADER, forkHeaders.length) ) } /** * Returns a {@link StarknetAction} for submitting a long fork of bitcoin blockheaders to the light client. * * @param signer Starknet signer's address * @param forkId Fork ID to submit the fork blockheaders to * @param forkHeaders Fork bitcoin blockheaders to submit * @param storedHeader Either a committed and stored bitcoin blockheader from which to fork the light client (when * creating the fork), or the tip of the fork (when adding more blockheaders to the fork) * @param totalForkHeaders Total blockheaders in the fork - used to estimate the gas usage when re-org happens */ private SaveLongForkHeaders(signer: string, forkId: number, forkHeaders: StarknetBtcHeader[], storedHeader: StarknetBtcStoredHeader, totalForkHeaders: number = 100): StarknetAction { return new StarknetAction(signer, this.Chain, { contractAddress: this.contract.address, entrypoint: "submit_fork_blockheaders", calldata: serializeCalldata(forkHeaders, storedHeader, [toHex(forkId)]) }, StarknetFees.starknetGasAdd( StarknetFees.starknetGasMul(GAS_PER_BLOCKHEADER, forkHeaders.length), StarknetFees.starknetGasMul(GAS_PER_BLOCKHEADER_FORK_REORG, totalForkHeaders) ) ) } /** * @internal */ readonly _bitcoinRpc: BitcoinRpc<B>; readonly maxHeadersPerTx: number = 40; readonly maxForkHeadersPerTx: number = 25; readonly maxShortForkHeadersPerTx: number = 40; constructor( chainInterface: StarknetChainInterface, bitcoinRpc: BitcoinRpc<B>, bitcoinNetwork: BitcoinNetwork, contractAddress: string | undefined = btcRelayAddreses[bitcoinNetwork], contractDeploymentHeight?: number ) { if(contractAddress==null) throw new Error("No BtcRelay address specified!"); super( chainInterface, contractAddress, BtcRelayAbi, contractDeploymentHeight ?? (btcRelayAddreses[bitcoinNetwork]===contractAddress ? btcRelayDeploymentHeights[bitcoinNetwork] : undefined) ); 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: StarknetBtcStoredHeader, syncedHeaders: StarknetBtcHeader[]) { 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 Starknet signer's address * @param headers Bitcoin blockheaders to submit to the btc relay * @param storedHeader Current latest stored block header for a given fork or main chain * @param forkId Fork ID to submit to, `forkId`=0 means main chain, `forkId`=-1 means short fork * @param feeRate Fee rate for the transaction * @private */ private async _saveHeaders( signer: string, headers: BtcBlock[], storedHeader: StarknetBtcStoredHeader, forkId: number, feeRate: string ) { const blockHeaderObj = headers.map(serializeBlockHeader); let starknetAction: StarknetAction; switch(forkId) { case -1: starknetAction = this.SaveShortForkHeaders(signer, blockHeaderObj, storedHeader); break; case 0: starknetAction = this.SaveMainHeaders(signer, blockHeaderObj, storedHeader); break; default: starknetAction = this.SaveLongForkHeaders(signer, forkId, blockHeaderObj, storedHeader); break; } const tx = await starknetAction.tx(feeRate); const computedCommitedHeaders = this.computeCommitedHeaders(storedHeader, blockHeaderObj); const lastStoredHeader = computedCommitedHeaders[computedCommitedHeaders.length-1]; return { forkId: forkId, lastStoredHeader, tx, computedCommitedHeaders } } /** * Returns a committed bitcoin blockheader based on the provided `commitHash` or `blockHash` * * @param commitHash Commitment hash of the stored blockheader * @param blockHash Block's hash * @private */ private getBlock(commitHash?: BigNumberish, blockHash?: Buffer): Promise<[StarknetBtcStoredHeader, bigint] | null> { const keys = [commitHash == null ? null : toHex(commitHash)]; if (blockHash != null) { const starknetBlockHash = hash.computePoseidonHashOnElements(bufferToU32Array(Buffer.from([...blockHash]).reverse())); keys.push(starknetBlockHash); } return this._Events.findInContractEvents( ["btc_relay::events::StoreHeader", "btc_relay::events::StoreForkHeader"], keys, (event) => { return Promise.resolve([StarknetBtcStoredHeader.fromSerializedFeltArray(event.data), BigInt(event.params.commit_hash)]); } ); } /** * Returns the current main chain blockheight of the BTC Relay * * @private */ private async getBlockHeight(): Promise<number> { return Number(await this.contract.get_blockheight()); } /** * @inheritDoc */ public async getTipData(): Promise<{ commitHash: string; blockhash: string, chainWork: Buffer, blockheight: number } | null> { const commitHash = await this.contract.get_tip_commit_hash(); if(commitHash==null || BigInt(commitHash)===BigInt(0)) return null; const result = await this.getBlock(commitHash); if(result==null) return null; const [storedBlockHeader] = result; return { blockheight: storedBlockHeader.getBlockheight(), commitHash: bigNumberishToBuffer(commitHash, 32).toString("hex"), blockhash: storedBlockHeader.getBlockHash().toString("hex"), chainWork: storedBlockHeader.getChainWork() }; } /** * @inheritDoc */ public async retrieveLogAndBlockheight(blockData: {blockhash: string}, requiredBlockheight?: number): Promise<{ header: StarknetBtcStoredHeader, height: number } | null> { //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(undefined, 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.get_commit_hash(storedBlockHeader.getBlockheight()); if(BigInt(chainCommitment)!==BigInt(commitHash)) return null; logger.debug("retrieveLogAndBlockheight(): block found," + " commit hash: "+toHex(commitHash)+" blockhash: "+blockData.blockhash+" current btc relay height: "+blockHeight); return {header: storedBlockHeader, height: blockHeight}; } /** * @inheritDoc */ public async retrieveLogByCommitHash(commitmentHash: string, blockData: {blockhash: string}): Promise<StarknetBtcStoredHeader | null> { const result = await this.getBlock(commitmentHash, 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.get_commit_hash(storedBlockHeader.getBlockheight()); if(BigInt(chainCommitment)!==BigInt(commitHash)) return null; logger.debug("retrieveLogByCommitHash(): block found," + " commit hash: "+commitmentHash+" blockhash: "+blockData.blockhash+" height: "+storedBlockHeader.getBlockheight()); return storedBlockHeader; } /** * @inheritDoc */ public async retrieveLatestKnownBlockLog(): Promise<{ resultStoredHeader: StarknetBtcStoredHeader, resultBitcoinHeader: B } | null> { const data = await this._Events.findInContractEvents( ["btc_relay::events::StoreHeader", "btc_relay::events::StoreForkHeader"], null, async (event) => { const storedHeader = StarknetBtcStoredHeader.fromSerializedFeltArray(event.data); const blockHashHex = storedHeader.getBlockHash().toString("hex"); const commitHash = event.params.commit_hash; const [isInBtcMainChain, btcRelayCommitHash] = await Promise.all([ this._bitcoinRpc.isInMainChain(blockHashHex).catch(() => false), this.contract.get_commit_hash(storedHeader.getBlockheight()) ]); if(!isInBtcMainChain) return null; if(BigInt(commitHash)!==BigInt(btcRelayCommitHash)) return null; const bitcoinBlockHeader = await this._bitcoinRpc.getBlockHeader(blockHashHex); if(bitcoinBlockHeader==null) return null; return { resultStoredHeader: storedHeader, resultBitcoinHeader: bitcoinBlockHeader, commitHash: commitHash } } ) if(data!=null) logger.debug("retrieveLatestKnownBlockLog(): block found," + " commit hash: "+toHex(data.commitHash)+" blockhash: "+data.resultBitcoinHeader.getHash()+ " height: "+data.resultStoredHeader.getBlockheight()); return data; } /** * @inheritDoc */ public async saveMainHeaders(signer: string, mainHeaders: BtcBlock[], storedHeader: StarknetBtcStoredHeader, feeRate?: string) { feeRate ??= await this.getMainFeeRate(signer); logger.debug("saveMainHeaders(): submitting main blockheaders, count: "+mainHeaders.length); return this._saveHeaders(signer, mainHeaders, storedHeader, 0, feeRate); } /** * @inheritDoc */ public async saveNewForkHeaders(signer: string, forkHeaders: BtcBlock[], storedHeader: StarknetBtcStoredHeader, tipWork: Buffer, feeRate?: string) { let forkId: number = Math.floor(Math.random() * 0xFFFFFFFFFFFF); feeRate ??= await this.getForkFeeRate(signer, forkId); logger.debug("saveNewForkHeaders(): submitting new fork & blockheaders," + " count: "+forkHeaders.length+" forkId: 0x"+forkId.toString(16)); const result = await this._saveHeaders(signer, forkHeaders, storedHeader, forkId, feeRate); if(result.forkId!==0 && StatePredictorUtils.gtBuffer(result.lastStoredHeader.getChainWork(), tipWork)) { //Fork's work is higher than main chain's work, this fork will become a main chain result.forkId = 0; } return result; } /** * @inheritDoc */ public async saveForkHeaders(signer: string, forkHeaders: BtcBlock[], storedHeader: StarknetBtcStoredHeader, forkId: number, tipWork: Buffer, feeRate?: string) { feeRate ??= await this.getForkFeeRate(signer, forkId); logger.debug("saveForkHeaders(): submitting blockheaders to existing fork," + " count: "+forkHeaders.length+" forkId: 0x"+forkId.toString(16)); const result = await this._saveHeaders(signer, forkHeaders, storedHeader, forkId, feeRate); if(result.forkId!==0 && StatePredictorUtils.gtBuffer(result.lastStoredHeader.getChainWork(), tipWork)) { //Fork's work is higher than main chain's work, this fork will become a main chain result.forkId = 0; } return result; } /** * @inheritDoc */ public async saveShortForkHeaders(signer: string, forkHeaders: BtcBlock[], storedHeader: StarknetBtcStoredHeader, tipWork: Buffer, feeRate?: string) { feeRate ??= await this.getMainFeeRate(signer); logger.debug("saveShortForkHeaders(): submitting short fork blockheaders," + " count: "+forkHeaders.length); const result = await this._saveHeaders(signer, forkHeaders, storedHeader, -1, feeRate); if(result.forkId!==0 && StatePredictorUtils.gtBuffer(result.lastStoredHeader.getChainWork(), tipWork)) { //Fork's work is higher than main chain's work, this fork will become a main chain result.forkId = 0; } return result; } /** * @inheritDoc */ public async estimateSynchronizeFee(requiredBlockheight: number, feeRate?: string): Promise<bigint> { const tipData = await this.getTipData(); if(tipData==null) throw new Error("Cannot get relay tip data, relay not initialized?"); const currBlockheight = tipData.blockheight; const blockheightDelta = requiredBlockheight-currBlockheight; if(blockheightDelta<=0) return 0n; const synchronizationFee = BigInt(blockheightDelta) * await this.getFeePerBlock(feeRate); logger.debug("estimateSynchronizeFee(): required blockheight: "+requiredBlockheight+ " blockheight delta: "+blockheightDelta+" fee: "+synchronizationFee.toString(10)); return synchronizationFee; } /** * @inheritDoc */ public async getFeePerBlock(feeRate?: string): Promise<bigint> { feeRate ??= await this.Chain.Fees.getFeeRate(); return StarknetFees.getGasFee(GAS_PER_BLOCKHEADER, feeRate); } /** * @inheritDoc */ public getMainFeeRate(signer: string | null): Promise<string> { return this.Chain.Fees.getFeeRate(); } /** * @inheritDoc */ public getForkFeeRate(signer: string, forkId: number): Promise<string> { return this.Chain.Fees.getFeeRate(); } /** * @inheritDoc */ saveInitialHeader(signer: string, header: B, epochStart: number, pastBlocksTimestamps: number[], feeRate?: string): Promise<StarknetTx> { throw new Error("Not supported, starknet contract is initialized with constructor!"); } /** * Gets committed headers, identified by blockhash & blockheight, determines required BTC relay blockheight based on * requiredConfirmations. * If synchronizer is passed & some blockhash is not found (or blockhash doesn't have enough confirmations), * it produces transactions to sync up the btc relay to the current chain tip & adds them to the passed txs array. * * @param signer A signer's address to use for the transactions * @param btcRelay BtcRelay contract to use for retrieving committed headers * @param btcTxs Bitcoin transactions to fetch the stored blockheaders for * @param txs Transactions 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: StarknetBtcRelay<any>, btcTxs: {blockheight: number, requiredConfirmations: number, blockhash: string}[], txs: StarknetTx[], synchronizer?: RelaySynchronizer<StarknetBtcStoredHeader, StarknetTx, any>, feeRate?: string ): Promise<{ [blockhash: string]: StarknetBtcStoredHeader } | null> { const leavesTxs: {blockheight: number, requiredConfirmations: number, blockhash: string}[] = []; const blockheaders: { [blockhash: string]: StarknetBtcStoredHeader } = {}; for(let btcTx of btcTxs) { const requiredBlockheight = btcTx.blockheight+btcTx.requiredConfirmations-1; const result = await 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; } }