@ceramicnetwork/core
Version:
Typescript implementation of the Ceramic protocol
171 lines • 7.47 kB
JavaScript
import * as uint8arrays from 'uint8arrays';
import * as providers from '@ethersproject/providers';
import { LRUCache } from 'least-recent';
import { Interface } from '@ethersproject/abi';
import { create as createMultihash } from 'multiformats/hashes/digest';
import { CID } from 'multiformats/cid';
import { convertCidToEthHash } from '@ceramicnetwork/anchor-utils';
const SHA256_CODE = 0x12;
const DAG_CBOR_CODE = 0x71;
const ETH_CHAIN_ID_MAPPINGS = {
'eip155:1': { network: 'mainnet', chain: 'ETH', chainId: 1, networkId: 1, type: 'Production' },
'eip155:3': { network: 'ropsten', chain: 'ETH', chainId: 3, networkId: 3, type: 'Test' },
'eip155:4': { network: 'rinkeby', chain: 'ETH', chainId: 4, networkId: 4, type: 'Test' },
'eip155:5': { network: 'goerli', chain: 'ETH', chainId: 5, networkId: 5, type: 'Test' },
'eip155:100': {
network: 'mainnet',
chain: 'Gnosis',
chainId: 100,
networkId: 100,
type: 'Test',
endpoint: 'https://rpc.ankr.com/gnosis',
},
'eip155:11155111': {
network: 'sepolia',
chain: 'ETH',
chainId: 11155111,
networkId: 11155111,
type: 'Test',
},
};
const BASE_CHAIN_ID = 'eip155';
const MAX_PROVIDERS_COUNT = 100;
const TRANSACTION_CACHE_SIZE = 50;
const BLOCK_CACHE_SIZE = 50;
const V1_PROOF_TYPE = 'f(bytes32)';
const ABI = ['function anchorDagCbor(bytes32)'];
const iface = new Interface(ABI);
const BLOCK_THRESHHOLDS = {
'eip155:1': 16688195,
'eip155:3': 1000000000,
'eip155:5': 8498671,
'eip155:100': 26509835,
'eip155:11155111': 5518585,
'eip155:1337': 1,
};
const ANCHOR_CONTRACT_ADDRESS = '0x231055A0852D67C7107Ad0d0DFeab60278fE6AdC';
const getCidFromV0Transaction = (txResponse) => {
const withoutPrefix = txResponse.data.replace(/^(0x0?)/, '');
return CID.decode(uint8arrays.fromString(withoutPrefix.slice(1), 'base16'));
};
const getCidFromV1Transaction = (txResponse) => {
const decodedArgs = iface.decodeFunctionData('anchorDagCbor', txResponse.data);
const rootCID = decodedArgs[0];
const multihash = createMultihash(SHA256_CODE, uint8arrays.fromString(rootCID.slice(2), 'base16'));
return CID.create(1, DAG_CBOR_CODE, multihash);
};
const getCidFromTransaction = (txType, txResponse) => {
if (txType === V1_PROOF_TYPE) {
return getCidFromV1Transaction(txResponse);
}
else {
return getCidFromV0Transaction(txResponse);
}
};
export class EthereumAnchorValidator {
constructor(ethereumRpcEndpoint, logger) {
this.ethereumRpcEndpoint = ethereumRpcEndpoint;
this.providersCache = new LRUCache(MAX_PROVIDERS_COUNT);
this._transactionCache = new LRUCache(TRANSACTION_CACHE_SIZE);
this._blockCache = new LRUCache(BLOCK_CACHE_SIZE);
this._logger = logger;
}
async init(chainId) {
if (!chainId) {
return;
}
const provider = this._getEthProvider(chainId);
const provider_chain_idnum = (await provider.getNetwork()).chainId;
const provider_chain = BASE_CHAIN_ID + ':' + provider_chain_idnum;
if (chainId != provider_chain) {
throw new Error(`Configured eth provider is for chainId ${provider_chain}, but our anchor service uses chain ${chainId}`);
}
this._chainId = chainId;
}
get chainId() {
return this._chainId;
}
async _getTransaction(provider, txHash) {
let tx = this._transactionCache.get(txHash);
if (!tx) {
tx = await provider.getTransaction(txHash);
this._transactionCache.set(txHash, tx);
}
return tx;
}
async _getTransactionAndBlockInfo(chainId, txHash) {
try {
const provider = this._getEthProvider(chainId);
const transaction = await this._getTransaction(provider, txHash);
if (!transaction) {
if (!this.ethereumRpcEndpoint) {
throw new Error(`Failed to load transaction data for transaction ${txHash}. Try providing an ethereum rpc endpoint`);
}
else {
throw new Error(`Failed to load transaction data for transaction ${txHash}`);
}
}
let block = this._blockCache.get(transaction.blockHash);
if (!block) {
block = await provider.getBlock(transaction.blockHash);
this._blockCache.set(transaction.blockHash, block);
}
if (!block) {
if (!this.ethereumRpcEndpoint) {
throw new Error(`Failed to load transaction data for block with block number ${transaction.blockNumber} and block hash ${transaction.blockHash}. Try providing an ethereum rpc endpoint`);
}
else {
throw new Error(`Failed to load transaction data for block with block number ${transaction.blockNumber} and block hash ${transaction.blockHash}`);
}
}
return [transaction, block];
}
catch (e) {
this._logger.err(`Error loading transaction info for transaction ${txHash} from Ethereum: ${e}`);
throw e;
}
}
async validateChainInclusion(anchorProof) {
const txHash = convertCidToEthHash(anchorProof.txHash);
const [txResponse, block] = await this._getTransactionAndBlockInfo(anchorProof.chainId, txHash);
const txCid = getCidFromTransaction(anchorProof.txType, txResponse);
if (!txCid.equals(anchorProof.root)) {
throw new Error(`The root CID ${anchorProof.root} is not in the transaction`);
}
if (txResponse.blockNumber <= BLOCK_THRESHHOLDS[anchorProof.chainId]) {
return block.timestamp;
}
if (anchorProof.txType !== V1_PROOF_TYPE) {
throw new Error(`Any anchor proofs created after block ${BLOCK_THRESHHOLDS[anchorProof.chainId]} must include the txType field. Anchor txn blockNumber: ${txResponse.blockNumber}`);
}
if (txResponse.to != ANCHOR_CONTRACT_ADDRESS) {
throw new Error(`Anchor was created using address ${txResponse.to}. This is not the official anchoring contract address ${ANCHOR_CONTRACT_ADDRESS}`);
}
return block.timestamp;
}
_getEthProvider(chain) {
const fromCache = this.providersCache.get(chain);
if (fromCache)
return fromCache;
if (!chain.startsWith('eip155')) {
throw new Error(`Unsupported chainId '${chain}' - must be eip155 namespace`);
}
if (this._chainId && this._chainId != chain) {
throw new Error(`Unsupported chainId '${chain}'. Configured anchor service only supports '${this._chainId}'`);
}
const ethNetwork = ETH_CHAIN_ID_MAPPINGS[chain];
const endpoint = this.ethereumRpcEndpoint || ethNetwork?.endpoint;
if (endpoint) {
const provider = new providers.StaticJsonRpcProvider(endpoint);
this.providersCache.set(chain, provider);
return provider;
}
if (!ethNetwork) {
throw new Error(`No ethereum provider available for chainId ${chain}`);
}
const provider = providers.getDefaultProvider(ethNetwork.network);
this.providersCache.set(chain, provider);
return provider;
}
}
//# sourceMappingURL=ethereum-anchor-validator.js.map