From 7a7a9be1f897d95f2bc5c26104510a99fe6289bd Mon Sep 17 00:00:00 2001 From: ppedziwiatr Date: Tue, 25 Jan 2022 17:17:55 +0100 Subject: [PATCH] fix: sdk should not cache on requested block height if interactions come from redstone-sequencer --- src/contract/Contract.ts | 2 +- src/contract/HandlerBasedContract.ts | 19 +++-- src/core/ExecutionContext.ts | 9 ++- src/core/SmartWeaveTags.ts | 1 + src/core/modules/CreateContract.ts | 8 -- .../modules/impl/CacheableStateEvaluator.ts | 31 +++++++- .../modules/impl/DefaultCreateContract.ts | 5 +- .../impl/LexicographicalInteractionsSorter.ts | 17 +++-- src/legacy/create-tx.ts | 11 +-- src/legacy/gqlResult.ts | 5 +- tools/bundle.ts | 73 +++++++++++++++++++ tools/contract.ts | 9 ++- 12 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 tools/bundle.ts diff --git a/src/contract/Contract.ts b/src/contract/Contract.ts index 7a1875c..9bf0de1 100644 --- a/src/contract/Contract.ts +++ b/src/contract/Contract.ts @@ -146,7 +146,7 @@ export interface Contract { tags?: Tags, transfer?: ArTransfer, strict?: boolean - ): Promise; + ): Promise; /** * Returns the full call tree report the last diff --git a/src/contract/HandlerBasedContract.ts b/src/contract/HandlerBasedContract.ts index 1bb306d..8509eec 100644 --- a/src/contract/HandlerBasedContract.ts +++ b/src/contract/HandlerBasedContract.ts @@ -16,6 +16,7 @@ import { EvaluationOptions, Evolve, ExecutionContext, + GQLEdgeInterface, GQLNodeInterface, HandlerApi, InnerWritesEvaluator, @@ -26,6 +27,7 @@ import { sleep, SmartWeave, SmartWeaveTags, + SourceType, Tags } from '@smartweave'; import { TransactionStatusResponse } from 'arweave/node/transactions'; @@ -388,8 +390,8 @@ export class HandlerBasedContract implements Contract { const evolvedSrcTxId = Evolve.evolvedSrcTxId(cachedState?.cachedValue?.state); let contractDefinition, - interactions = [], - sortedInteractions = [], + interactions: GQLEdgeInterface[] = [], + sortedInteractions: GQLEdgeInterface[] = [], handler; if (cachedBlockHeight != blockHeight) { [contractDefinition, interactions] = await Promise.all([ @@ -421,17 +423,20 @@ export class HandlerBasedContract implements Contract { } } + const containsInteractionsFromSequencer = interactions.some((i) => i.node.source == SourceType.REDSTONE_SEQUENCER); + this.logger.debug('containsInteractionsFromSequencer', containsInteractionsFromSequencer); + return { contractDefinition, blockHeight, - interactions, sortedInteractions, handler, smartweave: this.smartweave, contract: this, evaluationOptions: this._evaluationOptions, currentNetworkInfo, - cachedState + cachedState, + containsInteractionsFromSequencer }; } @@ -469,17 +474,19 @@ export class HandlerBasedContract implements Contract { this.logger.debug('Creating execution context from tx:', benchmark.elapsed()); + const containsInteractionsFromSequencer = interactions.some((i) => i.node.source == SourceType.REDSTONE_SEQUENCER); + return { contractDefinition, blockHeight, - interactions, sortedInteractions, handler, smartweave: this.smartweave, contract: this, evaluationOptions: this._evaluationOptions, caller, - cachedState + cachedState, + containsInteractionsFromSequencer }; } diff --git a/src/core/ExecutionContext.ts b/src/core/ExecutionContext.ts index b7927cc..6a6a62d 100644 --- a/src/core/ExecutionContext.ts +++ b/src/core/ExecutionContext.ts @@ -34,10 +34,6 @@ export type ExecutionContext = { * block height used for all operations - either requested block height or current network block height */ blockHeight: number; - /** - * all the interactions registered for this contract - */ - interactions: GQLEdgeInterface[]; /** * interaction sorted using either {@link LexicographicalInteractionsSorter} or {@link BlockHeightInteractionsSorter} * - crucial for proper and deterministic state evaluation @@ -57,4 +53,9 @@ export type ExecutionContext = { currentBlockData?: BlockData; caller?: string; // note: this is only set for "viewState" operations cachedState?: BlockHeightCacheResult>; + + // if the interactions list contains interactions from sequencer + // we cannot cache at requested block height - as it may happen that after this state + // evaluation - new transactions will be available on the same block height. + containsInteractionsFromSequencer: boolean; }; diff --git a/src/core/SmartWeaveTags.ts b/src/core/SmartWeaveTags.ts index 16fbe55..9a449e0 100644 --- a/src/core/SmartWeaveTags.ts +++ b/src/core/SmartWeaveTags.ts @@ -8,6 +8,7 @@ export enum SmartWeaveTags { INPUT = 'Input', CONTENT_TYPE = 'Content-Type', CONTRACT_SRC_TX_ID = 'Contract-Src', // note: should be named Contract-Src-Tx-Id + SDK = 'SDK', MIN_FEE = 'Min-Fee', INIT_STATE = 'Init-State', INIT_STATE_TX = 'Init-State-TX', diff --git a/src/core/modules/CreateContract.ts b/src/core/modules/CreateContract.ts index d7af994..0c9e5a3 100644 --- a/src/core/modules/CreateContract.ts +++ b/src/core/modules/CreateContract.ts @@ -33,12 +33,4 @@ export interface CreateContract { deploy(contractData: ContractData): Promise; deployFromSourceTx(contractData: FromSrcTxContractData): Promise; - - /** - * TODO: I would like to add the contract upgrade feature here - * - as an "evolution" of the current "evolve" ;-) - * @param contractTxId - * @param contractData - */ - update(contractTxId: string, contractData: ContractData): Promise; } diff --git a/src/core/modules/impl/CacheableStateEvaluator.ts b/src/core/modules/impl/CacheableStateEvaluator.ts index 7678ee8..772f436 100644 --- a/src/core/modules/impl/CacheableStateEvaluator.ts +++ b/src/core/modules/impl/CacheableStateEvaluator.ts @@ -118,7 +118,15 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { const contractTxId = executionContext.contractDefinition.txId; this.cLogger.debug(`onStateEvaluated: cache update for contract ${contractTxId} [${transaction.block.height}]`); - await this.putInCache(contractTxId, transaction, state); + + // TODO: this will be problematic if we decide to cache only "onStateEvaluated" and containsInteractionsFromSequencer = true + await this.putInCache( + contractTxId, + transaction, + state, + executionContext.blockHeight, + executionContext.containsInteractionsFromSequencer + ); await this.cache.flush(); } @@ -128,7 +136,13 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { state: EvalStateResult ): Promise { if (executionContext.evaluationOptions.updateCacheForEachInteraction) { - await this.putInCache(executionContext.contractDefinition.txId, transaction, state); + await this.putInCache( + executionContext.contractDefinition.txId, + transaction, + state, + executionContext.blockHeight, + executionContext.containsInteractionsFromSequencer + ); } } @@ -175,7 +189,9 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { protected async putInCache( contractTxId: string, transaction: GQLNodeInterface, - state: EvalStateResult + state: EvalStateResult, + requestedBlockHeight: number = null, + containsInteractionsFromSequencer = false ): Promise { if (transaction.dry) { return; @@ -183,8 +199,15 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator { if (transaction.confirmationStatus !== undefined && transaction.confirmationStatus !== 'confirmed') { return; } - const transactionId = transaction.id; + const blockHeight = transaction.block.height; + + if (requestedBlockHeight !== null && requestedBlockHeight == blockHeight && containsInteractionsFromSequencer) { + this.cLogger.debug('skipping caching of the last block'); + return; + } + const transactionId = transaction.id; + const stateToCache = new EvalStateResult(state.state, state.validity, transactionId, transaction.block.id); await this.cache.put(new BlockHeightKey(contractTxId, blockHeight), stateToCache); diff --git a/src/core/modules/impl/DefaultCreateContract.ts b/src/core/modules/impl/DefaultCreateContract.ts index 0e1fc7c..9c0b634 100644 --- a/src/core/modules/impl/DefaultCreateContract.ts +++ b/src/core/modules/impl/DefaultCreateContract.ts @@ -66,6 +66,7 @@ export class DefaultCreateContract implements CreateContract { contractTX.addTag(SmartWeaveTags.APP_NAME, 'SmartWeaveContract'); contractTX.addTag(SmartWeaveTags.APP_VERSION, '0.3.0'); contractTX.addTag(SmartWeaveTags.CONTRACT_SRC_TX_ID, srcTxId); + contractTX.addTag(SmartWeaveTags.SDK, 'RedStone'); contractTX.addTag('Content-Type', 'application/json'); await this.arweave.transactions.sign(contractTX, wallet); @@ -77,8 +78,4 @@ export class DefaultCreateContract implements CreateContract { throw new Error('Unable to write Contract Initial State'); } } - - update(contractTxId: string, contractData: ContractData): Promise { - throw new Error('Not implemented yet'); - } } diff --git a/src/core/modules/impl/LexicographicalInteractionsSorter.ts b/src/core/modules/impl/LexicographicalInteractionsSorter.ts index 5c6925b..b7ddc78 100644 --- a/src/core/modules/impl/LexicographicalInteractionsSorter.ts +++ b/src/core/modules/impl/LexicographicalInteractionsSorter.ts @@ -1,10 +1,12 @@ -import { arrayToHex, GQLEdgeInterface, InteractionsSorter } from '@smartweave'; +import { arrayToHex, GQLEdgeInterface, InteractionsSorter, LoggerFactory, SourceType } from '@smartweave'; import Arweave from 'arweave'; /** - * implementation that is based on current's SDK sorting alg. (which seems to be wrong ;-)) + * implementation that is based on current's SDK sorting alg. */ export class LexicographicalInteractionsSorter implements InteractionsSorter { + private readonly logger = LoggerFactory.INST.create('LexicographicalInteractionsSorter'); + constructor(private readonly arweave: Arweave) {} async sort(transactions: GQLEdgeInterface[]): Promise { @@ -19,11 +21,12 @@ export class LexicographicalInteractionsSorter implements InteractionsSorter { const { node } = txInfo; // might have been already set by the RedStone Sequencer - if (txInfo.sortKey !== undefined) { - return; + if (txInfo.node.sortKey !== undefined && txInfo.node.source == SourceType.REDSTONE_SEQUENCER) { + this.logger.debug('Using sortkey from sequencer', txInfo.node.sortKey); + txInfo.sortKey = txInfo.node.sortKey; + } else { + txInfo.sortKey = await this.createSortKey(node.block.id, node.id, node.block.height); } - - txInfo.sortKey = await this.createSortKey(node.block.id, node.id, node.block.height); } public async createSortKey(blockId: string, transactionId: string, blockHeight: number) { @@ -31,7 +34,7 @@ export class LexicographicalInteractionsSorter implements InteractionsSorter { const txIdBytes = this.arweave.utils.b64UrlToBuffer(transactionId); const concatenated = this.arweave.utils.concatBuffers([blockHashBytes, txIdBytes]); const hashed = arrayToHex(await this.arweave.crypto.hash(concatenated)); - const blockHeightString = `000000${blockHeight}`.slice(-12); + const blockHeightString = `${blockHeight}`.padStart(12, '0'); return `${blockHeightString},${hashed}`; } diff --git a/src/legacy/create-tx.ts b/src/legacy/create-tx.ts index 3ebf4f5..d6c1e95 100644 --- a/src/legacy/create-tx.ts +++ b/src/legacy/create-tx.ts @@ -1,5 +1,5 @@ import Arweave from 'arweave'; -import { ArWallet, GQLNodeInterface, GQLTagInterface } from '@smartweave'; +import { ArWallet, GQLNodeInterface, GQLTagInterface, SmartWeaveTags } from '@smartweave'; import Transaction from 'arweave/node/lib/transaction'; import { CreateTransactionInterface } from 'arweave/node/common'; import { BlockData } from 'arweave/node/blocks'; @@ -35,11 +35,12 @@ export async function createTx( interactionTx.addTag(tag.name.toString(), tag.value.toString()); } } - interactionTx.addTag('App-Name', 'SmartWeaveAction'); + interactionTx.addTag(SmartWeaveTags.APP_NAME, 'SmartWeaveAction'); // use real SDK version here? - interactionTx.addTag('App-Version', '0.3.0'); - interactionTx.addTag('Contract', contractId); - interactionTx.addTag('Input', JSON.stringify(input)); + interactionTx.addTag(SmartWeaveTags.APP_VERSION, '0.3.0'); + interactionTx.addTag(SmartWeaveTags.SDK, 'RedStone'); + interactionTx.addTag(SmartWeaveTags.CONTRACT_TX_ID, contractId); + interactionTx.addTag(SmartWeaveTags.INPUT, JSON.stringify(input)); await arweave.transactions.sign(interactionTx, wallet); return interactionTx; diff --git a/src/legacy/gqlResult.ts b/src/legacy/gqlResult.ts index 19d0ec0..4250c08 100644 --- a/src/legacy/gqlResult.ts +++ b/src/legacy/gqlResult.ts @@ -47,11 +47,14 @@ export interface GQLNodeInterface { id: string; }; dry?: boolean; - sortKey?: string; //added dynamically by the LexicographicalInteractionsSorter + sortKey?: string; // added dynamically by RedStone Sequencer confirmationStatus?: string; + source?: string; } export interface GQLEdgeInterface { + // added dynamically by the LexicographicalInteractionsSorter + // or rewritten from GQLNodeInterface.sortKey (if added there by RedStone Sequencer) sortKey?: string; cursor: string; node: GQLNodeInterface; diff --git a/tools/bundle.ts b/tools/bundle.ts new file mode 100644 index 0000000..41386aa --- /dev/null +++ b/tools/bundle.ts @@ -0,0 +1,73 @@ +/* eslint-disable */ +import Arweave from 'arweave'; +import { + LoggerFactory, + MemCache, + RedstoneGatewayContractDefinitionLoader, + RedstoneGatewayInteractionsLoader, + SmartWeaveNodeFactory +} from '../src'; +import { readJSON } from '../../redstone-smartweave-examples/src/_utils'; +import { TsLogFactory } from '../src/logging/node/TsLogFactory'; + +const logger = LoggerFactory.INST.create('Contract'); + +LoggerFactory.use(new TsLogFactory()); +LoggerFactory.INST.logLevel('error'); +LoggerFactory.INST.logLevel('info', 'Contract'); +LoggerFactory.INST.logLevel('debug', 'RedstoneGatewayInteractionsLoader'); +LoggerFactory.INST.logLevel('error', 'DefaultStateEvaluator'); +LoggerFactory.INST.logLevel('error', 'LexicographicalInteractionsSorter'); + +async function main() { + const arweave = Arweave.init({ + host: 'arweave.net', // Hostname or IP address for a Arweave host + port: 443, // Port + protocol: 'https', // Network protocol http or https + timeout: 60000, // Network request timeouts in milliseconds + logging: false // Enable network request logging + }); + + const smartweave = SmartWeaveNodeFactory.memCachedBased(arweave) + .setInteractionsLoader( + new RedstoneGatewayInteractionsLoader('http://localhost:5666', { notCorrupted: true }) + ) + .setDefinitionLoader( + new RedstoneGatewayContractDefinitionLoader('https://gateway.redstone.finance', arweave, new MemCache()) + ) + .build(); + + const jwk = readJSON('../redstone-node/.secrets/redstone-jwk.json'); + // connecting to a given contract + const token = smartweave + .contract("OrO8n453N6bx921wtsEs-0OCImBLCItNU5oSbFKlFuU") + .setEvaluationOptions({ + sequencerAddress: "http://localhost:5666/" + }) + // connecting wallet to a contract. It is required before performing any "writeInteraction" + // calling "writeInteraction" without connecting to a wallet first will cause a runtime error. + .connect(jwk); + + const result1 = await token.readState(); + + logger.info("Amount of computed interactions before 'bundleInteraction':", Object.keys(result1.validity).length); + + const result = await token.bundleInteraction({ + function: "transfer", + data: { + target: "fake-from-bundle", + qty: 18599333, + }, + }); + + logger.info("Result from the sequencer", result.id); + + // the new transaction is instantly available - ie. during the state read operation + const result2 = await token.readState(); + + logger.info("Amount of computed interactions after 'bundleInteraction':", Object.keys(result2.validity).length); + +} + + +main().catch((e) => console.error(e)); diff --git a/tools/contract.ts b/tools/contract.ts index ed8333b..a9036a4 100644 --- a/tools/contract.ts +++ b/tools/contract.ts @@ -16,10 +16,11 @@ import {TsLogFactory} from '../src/logging/node/TsLogFactory'; const logger = LoggerFactory.INST.create('Contract'); LoggerFactory.use(new TsLogFactory()); -LoggerFactory.INST.logLevel('fatal'); +LoggerFactory.INST.logLevel('error'); LoggerFactory.INST.logLevel('info', 'Contract'); LoggerFactory.INST.logLevel('debug', 'RedstoneGatewayInteractionsLoader'); LoggerFactory.INST.logLevel('error', 'DefaultStateEvaluator'); +LoggerFactory.INST.logLevel('error', 'DefaultStateEvaluator'); async function main() { printTestInfo(); @@ -66,9 +67,9 @@ async function main() { const jwk = readJSON("../redstone-node/.secrets/redstone-jwk.json"); const contract = smartweave.contract(LOOT_CONTRACT) - /*.setEvaluationOptions({ + .setEvaluationOptions({ sequencerAddress: "http://localhost:5666/" - })*/ + }) .connect(jwk); const bundledInteraction = await contract.bundleInteraction({ function: "generate" @@ -78,7 +79,7 @@ async function main() { // bundlr balance I-5rWUehEv-MjdK9gFw09RxfSLQX9DIHxG614Wf8qo0 -h https://node1.bundlr.network/ -c arweave - //await contract.readState(); + await contract.readState(); const heapUsedAfter = Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100; const rssUsedAfter = Math.round((process.memoryUsage().rss / 1024 / 1024) * 100) / 100;