UNPKG

@0xpolygonid/js-sdk

Version:
527 lines (466 loc) 18.1 kB
import { RootInfo, StateProof } from './../entities/state'; import { ZKProof } from '@iden3/js-jwz'; import { IStateStorage, UserStateTransitionInfo } from '../interfaces/state'; import { Contract, JsonRpcProvider, Signer, TransactionRequest } from 'ethers'; import { StateInfo } from '../entities/state'; import { StateTransitionPubSignals } from '../../circuits'; import { byteEncoder, getIsGenesisStateById } from '../../utils'; import abi from './abi/State.json'; import { DID, getChainId, Id } from '@iden3/js-iden3-core'; import { ITransactionService, TransactionService } from '../../blockchain'; import { prepareZkpProof } from './common'; import { ICache, createInMemoryCache } from '../memory'; import { PROTOCOL_CONSTANTS } from '../../iden3comm'; import { DEFAULT_CACHE_MAX_SIZE, VerifiableConstants } from '../../verifiable'; import { isIdentityDoesNotExistError, isStateDoesNotExistError } from './errors'; /** * Configuration options for caching behavior */ export type ResolverCacheOptions = { /** TTL in milliseconds for latest states/roots (shorter since they can change) */ notReplacedTtl?: number; /** TTL in milliseconds for historical states/roots (longer since they can change with less probability) */ replacedTtl?: number; /** Maximum number of entries to store in cache */ maxSize?: number; }; /** * Simplified cache config with a single TTL (used for latest/gist proofs etc.) */ export type SimpleCacheOptions = { /** TTL in milliseconds for latest states/gist proof */ ttl?: number; /** Maximum number of entries to store in cache */ maxSize?: number; }; /** * EthStateStorageOptions options for the Ethereum state storage. */ export type EthStateStorageOptions = { /** Disable caching */ disableCache?: boolean; /** Configuration for latest state resolution caching */ latestStateCacheOptions?: { /** Custom cache implementation (if not provided, uses in-memory cache) */ cache?: ICache<StateInfo>; } & SimpleCacheOptions; /** Configuration for state resolution caching */ stateCacheOptions?: { /** Custom cache implementation (if not provided, uses in-memory cache) */ cache?: ICache<StateInfo>; } & ResolverCacheOptions; /** Configuration for GIST root resolution caching */ rootCacheOptions?: { /** Custom cache implementation (if not provided, uses in-memory cache) */ cache?: ICache<RootInfo>; } & ResolverCacheOptions; gistProofCacheOptions?: { /** Custom cache implementation for GIST proofs (if not provided, uses in-memory cache) */ cache?: ICache<StateProof>; } & SimpleCacheOptions; }; /** * Configuration of ethereum based blockchain connection * * @public * @interface EthConnectionConfig */ export interface EthConnectionConfig { url: string; defaultGasLimit: number; minGasPrice?: string; maxGasPrice?: string; // eip-1559 transaction do not support gasPrice maxFeePerGas?: string; maxPriorityFeePerGas?: string; confirmationBlockCount: number; confirmationTimeout: number; contractAddress: string; receiptTimeout: number; rpcResponseTimeout: number; waitReceiptCycleTime: number; waitBlockCycleTime: number; chainId?: number; } export /** @type {EthConnectionConfig} - default configuration for EthConnectionConfig */ const defaultEthConnectionConfig: EthConnectionConfig = { url: 'http://localhost:8545', defaultGasLimit: 600000, minGasPrice: '0', maxGasPrice: '100000000000', confirmationBlockCount: 5, confirmationTimeout: 600000, contractAddress: '', receiptTimeout: 600000, rpcResponseTimeout: 5000, waitReceiptCycleTime: 30000, waitBlockCycleTime: 3000 }; const defaultStateInfo: Partial<StateInfo> = { state: 0n, replacedByState: 0n, createdAtTimestamp: 0n, replacedAtTimestamp: 0n, createdAtBlock: 0n, replacedAtBlock: 0n }; /** * * * @public * @class EthStateStorage * @implements implements IStateStorage interface */ export class EthStateStorage implements IStateStorage { public readonly stateContract: Contract; private readonly provider: JsonRpcProvider; private readonly _transactionService: ITransactionService; private _latestStateResolveCache: ICache<StateInfo>; private _stateResolveCache: ICache<StateInfo>; private _rootResolveCache: ICache<RootInfo>; private _gistProofResolveCache: ICache<StateProof>; private _latestStateCacheOptions: Required<SimpleCacheOptions>; private _stateCacheOptions: Required<ResolverCacheOptions>; private _rootCacheOptions: Required<ResolverCacheOptions>; private _gistProofCacheOptions: Required<SimpleCacheOptions>; private _disableCache = false; /** * Creates an instance of EthStateStorage. * @param {EthConnectionConfig} [ethConfig=defaultEthConnectionConfig] */ constructor( private readonly ethConfig: EthConnectionConfig | EthConnectionConfig[], options?: EthStateStorageOptions ) { const config = Array.isArray(ethConfig) ? ethConfig[0] : ethConfig; this.provider = new JsonRpcProvider(config.url); this.stateContract = new Contract(config.contractAddress, abi, this.provider); this._transactionService = new TransactionService(this.getRpcProvider()); // Store cache options for later use this._latestStateCacheOptions = { ttl: options?.latestStateCacheOptions?.ttl ?? PROTOCOL_CONSTANTS.DEFAULT_PROOF_VERIFY_DELAY / 2, maxSize: options?.latestStateCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE }; this._stateCacheOptions = { notReplacedTtl: options?.stateCacheOptions?.notReplacedTtl ?? PROTOCOL_CONSTANTS.DEFAULT_PROOF_VERIFY_DELAY / 2, replacedTtl: options?.stateCacheOptions?.replacedTtl ?? PROTOCOL_CONSTANTS.DEFAULT_PROOF_VERIFY_DELAY, maxSize: options?.stateCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE }; this._rootCacheOptions = { replacedTtl: options?.rootCacheOptions?.replacedTtl ?? PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY, notReplacedTtl: options?.rootCacheOptions?.notReplacedTtl ?? PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY / 2, maxSize: options?.rootCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE }; this._gistProofCacheOptions = { ttl: PROTOCOL_CONSTANTS.DEFAULT_AUTH_VERIFY_DELAY / 2, maxSize: options?.gistProofCacheOptions?.maxSize ?? DEFAULT_CACHE_MAX_SIZE }; // Initialize cache instances this._latestStateResolveCache = options?.latestStateCacheOptions?.cache ?? createInMemoryCache({ maxSize: this._latestStateCacheOptions.maxSize, ttl: this._latestStateCacheOptions.ttl }); this._stateResolveCache = options?.stateCacheOptions?.cache ?? createInMemoryCache({ maxSize: this._stateCacheOptions.maxSize, ttl: this._stateCacheOptions.replacedTtl }); this._rootResolveCache = options?.rootCacheOptions?.cache ?? createInMemoryCache({ maxSize: this._rootCacheOptions.maxSize, ttl: this._rootCacheOptions.replacedTtl }); this._gistProofResolveCache = options?.gistProofCacheOptions?.cache ?? createInMemoryCache({ maxSize: this._gistProofCacheOptions.maxSize, ttl: this._gistProofCacheOptions.ttl }); this._disableCache = options?.disableCache ?? false; } /** {@inheritdoc IStateStorage.getLatestStateById} */ async getLatestStateById(id: bigint): Promise<StateInfo> { const cacheKey = this.getLatestStateCacheKey(id); if (!this._disableCache) { const cachedResult = await this._latestStateResolveCache?.get(cacheKey); if (cachedResult) { // If cached result indicates non-existence, throw error if (cachedResult.state === 0n && cachedResult.createdAtTimestamp === 0n) { throw new Error(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST_CUSTOM_ERROR); } return cachedResult; } } const { stateContract } = this.getStateContractAndProviderForId(id); let rawData: string[] = []; try { rawData = await stateContract.getStateInfoById(id); } catch (e) { if (isIdentityDoesNotExistError(e) && !this._disableCache) { // Cache a placeholder to avoid repeated calls for non-existing identities await this._latestStateResolveCache?.set( cacheKey, { id, ...defaultStateInfo }, this._latestStateCacheOptions.ttl ); } throw e; } const stateInfo: StateInfo = { id: BigInt(rawData[0]), state: BigInt(rawData[1]), replacedByState: BigInt(rawData[2]), createdAtTimestamp: BigInt(rawData[3]), replacedAtTimestamp: BigInt(rawData[4]), createdAtBlock: BigInt(rawData[5]), replacedAtBlock: BigInt(rawData[6]) }; !this._disableCache && (await this._latestStateResolveCache?.set( cacheKey, stateInfo, this._latestStateCacheOptions.ttl )); return stateInfo; } /** {@inheritdoc IStateStorage.getStateInfoByIdAndState} */ async getStateInfoByIdAndState(id: bigint, state: bigint): Promise<StateInfo> { const cacheKey = this.getStateCacheKey(id, state); if (!this._disableCache) { // Check cache first const cachedResult = await this._stateResolveCache?.get(cacheKey); if (cachedResult) { return cachedResult; } } const { stateContract } = this.getStateContractAndProviderForId(id); let stateInfo: StateInfo; try { const rawData = await stateContract.getStateInfoByIdAndState(id, state); stateInfo = { id: BigInt(rawData[0]), state: BigInt(rawData[1]), replacedByState: BigInt(rawData[2]), createdAtTimestamp: BigInt(rawData[3]), replacedAtTimestamp: BigInt(rawData[4]), createdAtBlock: BigInt(rawData[5]), replacedAtBlock: BigInt(rawData[6]) }; } catch (e) { if (!isStateDoesNotExistError(e)) { throw e; } const isGenesis = getIsGenesisStateById(Id.fromBigInt(id), state); if (!isGenesis) { throw new Error( `State ${state} for identity ${id} is not genesis and not registered in the smart contract` ); } stateInfo = { id, ...defaultStateInfo, state }; } const ttl = stateInfo.replacedAtTimestamp === 0n ? this._stateCacheOptions.notReplacedTtl : this._stateCacheOptions.replacedTtl; !this._disableCache && (await this._stateResolveCache?.set(cacheKey, stateInfo, ttl)); return stateInfo; } /** {@inheritdoc IStateStorage.publishState} */ async publishState(proof: ZKProof, signer: Signer): Promise<string> { const stateTransitionPubSig = new StateTransitionPubSignals(); stateTransitionPubSig.pubSignalsUnmarshal( byteEncoder.encode(JSON.stringify(proof.pub_signals)) ); const { userId, oldUserState, newUserState, isOldStateGenesis } = stateTransitionPubSig; const { stateContract, provider } = this.getStateContractAndProviderForId(userId.bigInt()); const contract = stateContract.connect(signer) as Contract; const preparedZkpProof = prepareZkpProof(proof.proof); const payload = [ userId.bigInt().toString(), oldUserState.bigInt().toString(), newUserState.bigInt().toString(), isOldStateGenesis, preparedZkpProof.a, preparedZkpProof.b, preparedZkpProof.c ]; const feeData = await provider.getFeeData(); const maxFeePerGas = defaultEthConnectionConfig.maxFeePerGas ? BigInt(defaultEthConnectionConfig.maxFeePerGas) : feeData.maxFeePerGas; const maxPriorityFeePerGas = defaultEthConnectionConfig.maxPriorityFeePerGas ? BigInt(defaultEthConnectionConfig.maxPriorityFeePerGas) : feeData.maxPriorityFeePerGas; const gasLimit = await contract.transitState.estimateGas(...payload); const txData = await contract.transitState.populateTransaction(...payload); const request: TransactionRequest = { to: txData.to, data: txData.data, gasLimit, maxFeePerGas, maxPriorityFeePerGas }; const { txnHash } = await this._transactionService.sendTransactionRequest(signer, request); await this._latestStateResolveCache?.delete(this.getLatestStateCacheKey(userId.bigInt())); return txnHash; } /** {@inheritdoc IStateStorage.publishStateGeneric} */ async publishStateGeneric( signer: Signer, userStateTransitionInfo: UserStateTransitionInfo ): Promise<string> { const { userId, oldUserState, newUserState, isOldStateGenesis, methodId, methodParams } = userStateTransitionInfo; const { stateContract, provider } = this.getStateContractAndProviderForId(userId.bigInt()); const contract = stateContract.connect(signer) as Contract; const feeData = await provider.getFeeData(); const maxFeePerGas = defaultEthConnectionConfig.maxFeePerGas ? BigInt(defaultEthConnectionConfig.maxFeePerGas) : feeData.maxFeePerGas; const maxPriorityFeePerGas = defaultEthConnectionConfig.maxPriorityFeePerGas ? BigInt(defaultEthConnectionConfig.maxPriorityFeePerGas) : feeData.maxPriorityFeePerGas; const payload = [ userId.bigInt().toString(), oldUserState.bigInt().toString(), newUserState.bigInt().toString(), isOldStateGenesis, methodId, //BigInt(1), methodParams //'0x' ]; const gasLimit = await contract.transitStateGeneric.estimateGas(...payload); const txData = await contract.transitStateGeneric.populateTransaction(...payload); const request: TransactionRequest = { to: txData.to, data: txData.data, gasLimit, maxFeePerGas, maxPriorityFeePerGas }; const { txnHash } = await this._transactionService.sendTransactionRequest(signer, request); await this._latestStateResolveCache?.delete(this.getLatestStateCacheKey(userId.bigInt())); return txnHash; } /** {@inheritdoc IStateStorage.getGISTProof} */ async getGISTProof(id: bigint): Promise<StateProof> { const cacheKey = this.getGistProofCacheKey(id); if (!this._disableCache) { // Check cache first const cachedResult = await this._gistProofResolveCache?.get(cacheKey); if (cachedResult) { return cachedResult; } } const { stateContract } = this.getStateContractAndProviderForId(id); const data = await stateContract.getGISTProof(id); const stateProof = { root: BigInt(data.root.toString()), existence: data.existence, siblings: data.siblings?.map( (sibling: { toString: () => string | number | bigint | boolean }) => BigInt(sibling.toString()) ), index: BigInt(data.index.toString()), value: BigInt(data.value.toString()), auxExistence: data.auxExistence, auxIndex: BigInt(data.auxIndex.toString()), auxValue: BigInt(data.auxValue.toString()) }; !this._disableCache && (await this._gistProofResolveCache?.set( cacheKey, stateProof, this._gistProofCacheOptions.ttl )); return stateProof; } /** {@inheritdoc IStateStorage.getGISTRootInfo} */ async getGISTRootInfo(root: bigint, id: bigint): Promise<RootInfo> { const idTyped = Id.fromBigInt(id as bigint); const chainId = getChainId(DID.blockchainFromId(idTyped), DID.networkIdFromId(idTyped)); const cacheKey = this.getRootCacheKey(chainId, root); if (!this._disableCache) { // Check cache first const cachedResult = await this._rootResolveCache?.get(cacheKey); if (cachedResult) { return cachedResult; } } const { stateContract } = this.getStateContractAndProviderForId(id); const data = await stateContract.getGISTRootInfo(root); const rootInfo = { root: BigInt(data.root.toString()), replacedByRoot: BigInt(data.replacedByRoot.toString()), createdAtTimestamp: BigInt(data.createdAtTimestamp.toString()), replacedAtTimestamp: BigInt(data.replacedAtTimestamp.toString()), createdAtBlock: BigInt(data.createdAtBlock.toString()), replacedAtBlock: BigInt(data.replacedAtBlock.toString()) }; const ttl = rootInfo.replacedAtTimestamp == 0n ? this._rootCacheOptions.notReplacedTtl : this._rootCacheOptions.replacedTtl; !this._disableCache && (await this._rootResolveCache?.set(cacheKey, rootInfo, ttl)); return rootInfo; } /** {@inheritdoc IStateStorage.getRpcProvider} */ getRpcProvider(): JsonRpcProvider { return this.provider; } /** enable caching */ enableCache(): void { this._disableCache = false; } /** disable caching */ disableCache(): void { this._disableCache = true; } private getStateContractAndProviderForId(id: bigint): { stateContract: Contract; provider: JsonRpcProvider; } { const idTyped = Id.fromBigInt(id as bigint); const chainId = getChainId(DID.blockchainFromId(idTyped), DID.networkIdFromId(idTyped)); const config = this.networkByChainId(chainId); const provider = new JsonRpcProvider(config.url); const stateContract = new Contract(config.contractAddress, abi, provider); return { stateContract, provider }; } private networkByChainId(chainId: number): EthConnectionConfig { const config = Array.isArray(this.ethConfig) ? this.ethConfig : [this.ethConfig]; const network = config.find((c) => c.chainId === chainId); if (!network) { throw new Error(`chainId "${chainId}" not supported`); } return network; } private getGistProofCacheKey(id: bigint): string { return `gist:${id.toString()}`; } private getLatestStateCacheKey(id: bigint): string { return `latest-state:${id.toString()}`; } private getStateCacheKey(id: bigint, state: bigint): string { return `state:${id.toString()}-${state.toString()}`; } private getRootCacheKey(chainId: number, root: bigint): string { return `root:${chainId.toString()}-${root.toString()}`; } }