UNPKG

@axiom-crypto/tools

Version:

Useful data, field, and byte manipulation tools for Axiom.

443 lines (442 loc) 21.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getSolidityNestedMappingValue = exports.getReceiptFieldValue = exports.getTxFieldValue = exports.getStorageFieldValue = exports.getAccountFieldValue = exports.getHeaderFieldValue = void 0; const ethers_1 = require("ethers"); const utils_1 = require("../utils"); const codec_1 = require("../codec"); const getters_1 = require("./getters"); const constants_1 = require("../constants"); const constants_2 = require("../constants/common/constants"); async function getHeaderFieldValue(provider, { blockNumber, fieldIdx }, logger = console) { const block = await (0, getters_1.getFullBlock)(provider, blockNumber); if (!block) { logger.info(`Block ${blockNumber} does not exist`); return null; } const getValue = () => { if (fieldIdx >= constants_1.AxiomV2FieldConstant.Header.LogsBloomFieldIdxOffset && fieldIdx < constants_1.AxiomV2FieldConstant.Header.LogsBloomFieldIdxOffset + 8) { const logIdx = fieldIdx - constants_1.AxiomV2FieldConstant.Header.LogsBloomFieldIdxOffset; if (!block.logsBloom || logIdx >= block.logsBloom.slice(2).length / 64) { return null; } const reader = new utils_1.ByteStringReader(block.logsBloom); for (let i = 0; i < logIdx; i++) { reader.readBytes("bytes32"); } const logsBloomValue = reader.readBytes("bytes32"); return logsBloomValue; } switch (fieldIdx) { case codec_1.HeaderField.ParentHash: return block.parentHash ?? null; case codec_1.HeaderField.Sha3Uncles: return block.sha3Uncles ?? null; case codec_1.HeaderField.Miner: return block.miner ?? null; case codec_1.HeaderField.StateRoot: return block.stateRoot ?? null; case codec_1.HeaderField.TransactionsRoot: return block.transactionsRoot ?? null; case codec_1.HeaderField.ReceiptsRoot: return block.receiptsRoot ?? null; case codec_1.HeaderField.LogsBloom: return block.logsBloom?.slice(0, 66) ?? null; case codec_1.HeaderField.Difficulty: return block.difficulty ?? null; case codec_1.HeaderField.Number: return block.number ?? null; case codec_1.HeaderField.GasLimit: return block.gasLimit ?? null; case codec_1.HeaderField.GasUsed: return block.gasUsed ?? null; case codec_1.HeaderField.Timestamp: return block.timestamp ?? null; case codec_1.HeaderField.ExtraData: // Get the first 32 bytes of extra data, and right pad with 0s return block.extraData?.slice(0, 66).padEnd(66, "0") ?? null; case codec_1.HeaderField.MixHash: return block.mixHash ?? null; case codec_1.HeaderField.Nonce: return block.nonce ?? null; case codec_1.HeaderField.BaseFeePerGas: return block.baseFeePerGas ?? null; case codec_1.HeaderField.WithdrawalsRoot: return block.withdrawalsRoot ?? null; // case HeaderField.BlobGasUsed: // return block.blobGasUsed ?? null; // case HeaderField.ExcessBlobGas: // return block.excessBlobGas ?? null; // case HeaderField.ParentBeaconBlockRoot: // return block.parentBeaconBlockRoot ?? null; case constants_1.AxiomV2FieldConstant.Header.HashFieldIdx: return block.hash ?? null; case constants_1.AxiomV2FieldConstant.Header.HeaderSizeFieldIdx: return (0, utils_1.getNumBytes)((0, utils_1.rlpEncodeBlockHeader)(block)); case constants_1.AxiomV2FieldConstant.Header.ExtraDataLenFieldIdx: if (!block.extraData) { return 0; } return (0, utils_1.getNumBytes)(block.extraData); default: logger.info(`Invalid header field: ${fieldIdx}`); return null; } }; const value = getValue(); if (!value) return null; return (0, utils_1.bytes32)(value); } exports.getHeaderFieldValue = getHeaderFieldValue; async function getAccountFieldValue(provider, { blockNumber, addr, fieldIdx }, logger = console) { const block = await (0, getters_1.getFullBlock)(provider, blockNumber); if (!block) { logger.info(`Block ${blockNumber} does not exist`); return null; } if (!ethers_1.ethers.isAddress(addr)) { logger.info(`Address ${addr} is not a correctly-formed EVM address`); return getNonexistentAccountFieldValue(fieldIdx); } const account = await (0, getters_1.getAccountData)(provider, blockNumber, addr, []); if (!account) { logger.info(`proof is null for block ${blockNumber}, address ${addr}`); return getNonexistentAccountFieldValue(fieldIdx); } const accountProof = account.accountProof; if (!(0, utils_1.isAssignedSlot)(addr, accountProof)) { return getNonexistentAccountFieldValue(fieldIdx); } const stateRoot = ethers_1.ethers.keccak256(accountProof[0]); if (stateRoot !== block.stateRoot) { logger.info(`State root mismatch: ${block.stateRoot} (block) ${stateRoot} (accountProof)`); return null; } const getValue = () => { switch (fieldIdx) { case codec_1.AccountField.Nonce: return account.nonce ?? 0; case codec_1.AccountField.Balance: return account.balance ?? 0; case codec_1.AccountField.StorageRoot: return account.storageHash ?? constants_2.CommonConstants.StorageHashEmpty; case codec_1.AccountField.CodeHash: return account.codeHash ?? constants_2.CommonConstants.CodeHashEmpty; default: logger.info(`Invalid account field: ${fieldIdx}`); return null; } }; const value = getValue(); if (!value) return null; return (0, utils_1.bytes32)(value); } exports.getAccountFieldValue = getAccountFieldValue; async function getStorageFieldValue(provider, { blockNumber, addr, slot }, logger = console) { const block = await (0, getters_1.getFullBlock)(provider, blockNumber); if (!block) { logger.info(`Block ${blockNumber} does not exist`); return null; } if (!ethers_1.ethers.isAddress(addr)) { logger.info(`Address ${addr} is not a valid address`); return null; } slot = (0, utils_1.bytes32)(slot); let slots = [slot]; const account = await (0, getters_1.getAccountData)(provider, blockNumber, addr, slots); if (!account) { logger.info(`proof is null for block ${blockNumber}, address ${addr}`); return null; } const accountProof = account.accountProof; const stateRoot = ethers_1.ethers.keccak256(accountProof[0]); if (stateRoot !== block.stateRoot) { logger.info(`State root mismatch: ${block.stateRoot} (block) ${stateRoot} (accountProof)`); return null; } const storageProof = account.storageProof[0]; const proof = storageProof.proof; let value = storageProof.value; if (proof.length === 0) { logger.info(`Slot ${slot} has empty MPT proof in account ${addr} at block ${blockNumber}`); return ethers_1.ZeroHash; } value = (0, utils_1.bytes32)(value); if (!(0, utils_1.isAssignedSlot)(slot, proof) && value !== ethers_1.ZeroHash) { logger.warn(`eth_getProof returned non-zero value on an empty slot`); return null; } const storageHash = ethers_1.ethers.keccak256(proof[0]); if (account.storageHash !== storageHash) { logger.info(`Storage hash mismatch: ${account.storageHash} (account) ${storageHash} (storageProof)`); return null; } return value; } exports.getStorageFieldValue = getStorageFieldValue; async function getTxFieldValue(provider, { blockNumber, txIdx, fieldOrCalldataIdx }, logger = console, tx) { if (!tx || tx?.hash === undefined || tx.hash === "") { // To preserve compatibility w/ ethers provder interface and return object flow, we'll // use one RPC call to first get the txHash const txHash = await (0, getters_1.getTxHash)(provider, blockNumber, txIdx); if (!txHash) { logger.info(`Failed to find txHash for block ${blockNumber} txIdx ${txIdx}`); return null; } tx = await (0, getters_1.getRawTransaction)(provider, txHash); if (!tx) { logger.info(`Failed to find tx for txHash ${txHash}`); return null; } } const getValue = (tx, type) => { if (fieldOrCalldataIdx < constants_1.AxiomV2FieldConstant.Tx.CalldataIdxOffset) { switch (fieldOrCalldataIdx) { case codec_1.TxField.ChainId: if (type === codec_1.TxType.Legacy) { return null; } return tx?.chainId ?? null; case codec_1.TxField.Nonce: return tx?.nonce ?? null; case codec_1.TxField.MaxPriorityFeePerGas: if (type === codec_1.TxType.Legacy || type === codec_1.TxType.Eip2930) { return null; } return tx?.maxPriorityFeePerGas ?? null; case codec_1.TxField.MaxFeePerGas: if (type === codec_1.TxType.Legacy || type === codec_1.TxType.Eip2930) { return null; } return tx?.maxFeePerGas ?? null; case codec_1.TxField.GasLimit: return tx?.gas ?? null; case codec_1.TxField.To: return tx?.to ?? null; case codec_1.TxField.Value: return tx?.value ?? null; case codec_1.TxField.Data: if (tx?.input === "0x") { return "0x0"; } return tx?.input?.slice(0, 66) ?? null; case codec_1.TxField.GasPrice: if (type === codec_1.TxType.Eip1559) { return null; } return tx?.gasPrice ?? null; case codec_1.TxField.v: return tx?.v ?? null; case codec_1.TxField.r: return tx?.r ?? null; case codec_1.TxField.s: return tx?.s ?? null; case constants_1.AxiomV2FieldConstant.Tx.TxTypeFieldIdx: return tx?.type ?? null; case constants_1.AxiomV2FieldConstant.Tx.BlockNumberFieldIdx: return tx?.blockNumber ?? null; case constants_1.AxiomV2FieldConstant.Tx.TxIndexFieldIdx: return tx?.transactionIndex ?? null; case constants_1.AxiomV2FieldConstant.Tx.FunctionSelectorFieldIdx: if (!tx.to && !!tx.input) { return constants_1.AxiomV2FieldConstant.Tx.ContractDeploySelectorValue; } if (tx.input === "0x") { return constants_1.AxiomV2FieldConstant.Tx.NoCalldataSelectorValue; } if (tx.input.length < 10) { return null; } const selectorReader = new utils_1.ByteStringReader(tx.input); const selector = selectorReader.readBytes("bytes4"); // function selector return selector; case constants_1.AxiomV2FieldConstant.Tx.CalldataHashFieldIdx: return ethers_1.ethers.keccak256(tx.input); case constants_1.AxiomV2FieldConstant.Tx.DataLengthFieldIdx: return (0, utils_1.getNumBytes)(tx.input); default: logger.info(`Invalid tx field index: ${fieldOrCalldataIdx}`); return null; } } if (fieldOrCalldataIdx < constants_1.AxiomV2FieldConstant.Tx.ContractDataIdxOffset) { // Parse calldata blob (ignoring function selector) to get calldata at specified idx const calldata = tx.input; const calldataIdx = fieldOrCalldataIdx - constants_1.AxiomV2FieldConstant.Tx.CalldataIdxOffset; if (!tx.input || // Slice off the 0x plus 4 bytes of the function signature. The EVM encodes all calldata datatypes // as 32-bytes regardless of size, so the remaining items minus function signature are 32 bytes each. calldataIdx >= tx.input.slice(10).length / 64) { return null; } const reader = new utils_1.ByteStringReader(calldata); const _functionSignature = reader.readBytes("bytes4"); // unused for (let i = 0; i < calldataIdx; i++) { reader.readBytes("bytes32"); } const calldataValue = reader.readBytes("bytes32"); return calldataValue; } // Get contractData Idx // Contract construction: https://blog.smlxl.io/evm-contract-construction-93c98cc4ca96 const contractDataIdx = fieldOrCalldataIdx - constants_1.AxiomV2FieldConstant.Tx.ContractDataIdxOffset; const contractData = tx.input; const numSlots = Math.ceil(tx.input.slice(2).length / 64); if (!tx.input || contractDataIdx >= numSlots) { return null; } if (contractDataIdx === numSlots - 1) { // Special case for last slot: we need to pad the last slot with 0s to 32 bytes return (0, utils_1.toLeHex)(tx.input.slice(2 + (numSlots - 1) * 64), 32); } const reader = new utils_1.ByteStringReader(contractData); for (let i = 0; i < contractDataIdx; i++) { reader.readBytes("bytes32"); } const contractDataValue = reader.readBytes("bytes32"); return contractDataValue; }; const type = Number(tx?.type); if (type === codec_1.TxType.Eip4844) { logger.error(`EIP-4844 transactions are not yet supported`); return null; } else if (type === codec_1.TxType.OpSystem) { logger.error(`OP stack System transactions are not yet supported`); return null; } const value = getValue(tx, type); if (!value) return null; return (0, utils_1.bytes32)(value); } exports.getTxFieldValue = getTxFieldValue; async function getReceiptFieldValue(provider, { blockNumber, txIdx, fieldOrLogIdx, topicOrDataOrAddressIdx, eventSchema }, logger = console, receipt) { if (!receipt || receipt?.transactionHash === undefined || receipt.transactionHash === "") { // To preserve compatibility w/ ethers provder interface and return object flow, we'll // use one RPC call to first get the txHash const txHash = await (0, getters_1.getTxHash)(provider, blockNumber, txIdx); if (!txHash) { logger.info(`Failed to find txHash for block ${blockNumber} txIdx ${txIdx}`); return null; } // const receipt = await provider.getTransactionReceipt(txHash); receipt = await (0, getters_1.getRawReceipt)(provider, txHash); if (!receipt) { logger.info(`Failed to find receipt for txHash ${txHash}`); return null; } } const getValue = (receipt) => { if (fieldOrLogIdx >= constants_1.AxiomV2FieldConstant.Receipt.LogsBloomIdxOffset && fieldOrLogIdx < constants_1.AxiomV2FieldConstant.Receipt.LogsBloomIdxOffset + 8) { const logIdx = fieldOrLogIdx - constants_1.AxiomV2FieldConstant.Receipt.LogsBloomIdxOffset; if (!receipt.logsBloom || logIdx >= receipt.logsBloom.slice(2).length / 64) { return null; } const reader = new utils_1.ByteStringReader(receipt.logsBloom); for (let i = 0; i < logIdx; i++) { reader.readBytes("bytes32"); } const logsBloomValue = reader.readBytes("bytes32"); return logsBloomValue; } if (fieldOrLogIdx < constants_1.AxiomV2FieldConstant.Receipt.LogIdxOffset) { switch (fieldOrLogIdx) { case codec_1.ReceiptField.Status: return receipt?.status ?? null; case codec_1.ReceiptField.PostState: return receipt?.root ?? null; case codec_1.ReceiptField.CumulativeGas: return receipt?.cumulativeGasUsed ?? null; case codec_1.ReceiptField.LogsBloom: return receipt?.logsBloom ?? null; case codec_1.ReceiptField.Logs: logger.info("Use `getFieldIdxReceiptLogIdx(idx) to get a log at index `idx` in this transaction"); return null; case constants_1.AxiomV2FieldConstant.Receipt.TxTypeFieldIdx: return receipt?.type ?? null; case constants_1.AxiomV2FieldConstant.Receipt.BlockNumberFieldIdx: return receipt?.blockNumber ?? null; case constants_1.AxiomV2FieldConstant.Receipt.TxIndexFieldIdx: return receipt?.transactionIndex ?? null; default: logger.info(`Invalid receipt field index: ${fieldOrLogIdx}`); return null; } } const logIdx = fieldOrLogIdx - constants_1.AxiomV2FieldConstant.Receipt.LogIdxOffset; const logItem = receipt.logs[logIdx] ?? null; if (!logItem) { logger.info(`log is null for block ${blockNumber} txIdx ${txIdx} fieldOrLogIdx ${fieldOrLogIdx}`); return null; } const topics = logItem.topics; if (eventSchema !== ethers_1.ZeroHash && eventSchema !== topics[0]) { logger.info(`eventSchema do not match topics`); return null; } if (topicOrDataOrAddressIdx < constants_1.AxiomV2FieldConstant.Receipt.DataIdxOffset) { // Return topic if (topicOrDataOrAddressIdx < logItem.topics.length) { return logItem.topics[topicOrDataOrAddressIdx]; } // Return address if (topicOrDataOrAddressIdx === constants_1.AxiomV2FieldConstant.Receipt.AddressIdx) { return logItem.address ?? null; } logger.info("Invalid topic index: ", topicOrDataOrAddressIdx); return null; } // Return data const dataIdx = topicOrDataOrAddressIdx - constants_1.AxiomV2FieldConstant.Receipt.DataIdxOffset; if (!logItem.data || dataIdx >= logItem.data.slice(2).length / 64) { return null; } const reader = new utils_1.ByteStringReader(logItem.data); for (let i = 0; i < dataIdx; i++) { reader.readBytes("bytes32"); } const dataValue = reader.readBytes("bytes32"); return dataValue; }; if (Number(receipt?.type) === codec_1.TxType.Eip4844) { logger.error(`EIP-4844 transaction receipts are not yet supported`); return null; } else if (Number(receipt?.type) === codec_1.TxType.OpSystem) { logger.error(`OP stack System transaction receipts are not yet supported`); return null; } const value = getValue(receipt); if (!value) return null; return (0, utils_1.bytes32)(value); } exports.getReceiptFieldValue = getReceiptFieldValue; async function getSolidityNestedMappingValue(provider, { blockNumber, addr, mappingSlot, mappingDepth, keys }, logger = console) { let slot = (0, utils_1.bytes32)(mappingSlot); for (let i = 0; i < mappingDepth; i++) { const key = (0, utils_1.bytes32)(keys[i]); slot = ethers_1.ethers.keccak256(ethers_1.ethers.concat([key, slot])); } return await getStorageFieldValue(provider, { blockNumber, addr, slot }, logger); } exports.getSolidityNestedMappingValue = getSolidityNestedMappingValue; function getNonexistentAccountFieldValue(fieldIdx) { switch (fieldIdx) { case codec_1.AccountField.Nonce: return (0, utils_1.bytes32)(0); case codec_1.AccountField.Balance: return (0, utils_1.bytes32)(0); case codec_1.AccountField.StorageRoot: return constants_2.CommonConstants.StorageHashEmpty; case codec_1.AccountField.CodeHash: return (0, utils_1.bytes32)(0); default: throw new Error(`Unknown AccountField index: ${fieldIdx}`); } }