fix: sdk should not cache on requested block height if interactions come from redstone-sequencer

This commit is contained in:
ppedziwiatr
2022-01-25 17:17:55 +01:00
committed by Piotr Pędziwiatr
parent c9924a7443
commit 7a7a9be1f8
12 changed files with 146 additions and 44 deletions

View File

@@ -146,7 +146,7 @@ export interface Contract<State = unknown> {
tags?: Tags,
transfer?: ArTransfer,
strict?: boolean
): Promise<string | null>;
): Promise<any | null>;
/**
* Returns the full call tree report the last

View File

@@ -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<State> implements Contract<State> {
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<State> implements Contract<State> {
}
}
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<State> implements Contract<State> {
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
};
}

View File

@@ -34,10 +34,6 @@ export type ExecutionContext<State, Api = unknown> = {
* 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<State, Api = unknown> = {
currentBlockData?: BlockData;
caller?: string; // note: this is only set for "viewState" operations
cachedState?: BlockHeightCacheResult<EvalStateResult<State>>;
// 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;
};

View File

@@ -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',

View File

@@ -33,12 +33,4 @@ export interface CreateContract {
deploy(contractData: ContractData): Promise<string>;
deployFromSourceTx(contractData: FromSrcTxContractData): Promise<string>;
/**
* 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<void>;
}

View File

@@ -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<State>
): Promise<void> {
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<State>(
contractTxId: string,
transaction: GQLNodeInterface,
state: EvalStateResult<State>
state: EvalStateResult<State>,
requestedBlockHeight: number = null,
containsInteractionsFromSequencer = false
): Promise<void> {
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);

View File

@@ -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<void> {
throw new Error('Not implemented yet');
}
}

View File

@@ -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<GQLEdgeInterface[]> {
@@ -19,19 +21,20 @@ 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);
}
}
public async createSortKey(blockId: string, transactionId: string, blockHeight: number) {
const blockHashBytes = this.arweave.utils.b64UrlToBuffer(blockId);
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}`;
}

View File

@@ -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;

View File

@@ -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;

73
tools/bundle.ts Normal file
View File

@@ -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<any>({
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));

View File

@@ -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;