feat: kv storage for contracts

This commit is contained in:
ppe
2022-12-20 20:35:45 +01:00
committed by just_ppe
parent f279c1659c
commit e944cd7c0f
30 changed files with 688 additions and 10085 deletions

View File

@@ -7,7 +7,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '16' node-version: '18'
- name: Install modules - name: Install modules
run: yarn run: yarn
- name: Run unit tests - name: Run unit tests

View File

@@ -21,5 +21,5 @@ module.exports = {
'^.+\\.(ts|js)$': 'ts-jest' '^.+\\.(ts|js)$': 'ts-jest'
}, },
silent: true silent: false
}; };

View File

@@ -112,6 +112,7 @@
"stream-buffers": false, "stream-buffers": false,
"constants": false, "constants": false,
"os": false, "os": false,
"process": false "process": false,
"url": false
} }
} }

View File

@@ -78,7 +78,7 @@ describe('Testing WarpGatewayContractDefinitionLoader', () => {
it('loads contract definition when cache is empty', async () => { it('loads contract definition when cache is empty', async () => {
// Cache is empty // Cache is empty
loader.getCache().delete(contract.txId()); loader.getCache().delete(contract.txId());
expect(await loader.getCache().get(contract.txId(), 'cd')).toBeFalsy(); expect(await loader.getCache().get({ key: contract.txId(), sortKey: 'cd'})).toBeFalsy();
// Load contract // Load contract
const loaded = await loader.load(contract.txId()); const loaded = await loader.load(contract.txId());
@@ -86,12 +86,12 @@ describe('Testing WarpGatewayContractDefinitionLoader', () => {
expect(loaded.src).toBe(contractSrc); expect(loaded.src).toBe(contractSrc);
// Contract is in its cache // Contract is in its cache
expect(await loader.getCache().get(loaded.txId, 'cd')).toBeTruthy(); expect(await loader.getCache().get({ key: loaded.txId, sortKey: 'cd'})).toBeTruthy();
expect(await loader.getSrcCache().get(loaded.txId, 'cd')).toBeFalsy(); expect(await loader.getSrcCache().get({ key: loaded.txId, sortKey: 'cd'})).toBeFalsy();
// Source is in its cache // Source is in its cache
expect(await loader.getCache().get(loaded.srcTxId, 'src')).toBeFalsy(); expect(await loader.getCache().get({ key: loaded.srcTxId, sortKey: 'src'})).toBeFalsy();
expect(await loader.getSrcCache().get(loaded.srcTxId, 'src')).toBeTruthy(); expect(await loader.getSrcCache().get({ key: loaded.srcTxId, sortKey: 'src'})).toBeTruthy();
}); });
it('loads contract definition when cache contains given definition', async () => { it('loads contract definition when cache contains given definition', async () => {
@@ -99,10 +99,10 @@ describe('Testing WarpGatewayContractDefinitionLoader', () => {
let loaded = await loader.load(contract.txId()); let loaded = await loader.load(contract.txId());
// Modify source in cache // Modify source in cache
let source = await loader.getSrcCache().get(loaded.srcTxId, 'src'); let source = await loader.getSrcCache().get({ key: loaded.srcTxId, sortKey: 'src' });
expect(source).toBeTruthy(); expect(source).toBeTruthy();
source!.cachedValue.src = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8'); source!.cachedValue.src = fs.readFileSync(path.join(__dirname, '../data/token-evolve.js'), 'utf8');
await loader.getSrcCache().put({ contractTxId: loaded.srcTxId, sortKey: 'src' }, source!.cachedValue); await loader.getSrcCache().put({ key: loaded.srcTxId, sortKey: 'src' }, source!.cachedValue);
// Load again, modified cache should be returned // Load again, modified cache should be returned
loaded = await loader.load(contract.txId()); loaded = await loader.load(contract.txId());

View File

@@ -9,7 +9,6 @@ import { PstState, PstContract } from '../../../contract/PstContract';
import { Warp } from '../../../core/Warp'; import { Warp } from '../../../core/Warp';
import { WarpFactory } from '../../../core/WarpFactory'; import { WarpFactory } from '../../../core/WarpFactory';
import { LoggerFactory } from '../../../logging/LoggerFactory'; import { LoggerFactory } from '../../../logging/LoggerFactory';
import exp from 'constants';
describe('Testing unsafe client in nested contracts with "skip" option', () => { describe('Testing unsafe client in nested contracts with "skip" option', () => {
let contractSrc: string; let contractSrc: string;

View File

@@ -0,0 +1,134 @@
import fs from 'fs';
import ArLocal from 'arlocal';
import Arweave from 'arweave';
import { JWKInterface } from 'arweave/node/lib/wallet';
import path from 'path';
import { mineBlock } from '../_helpers';
import { PstState, PstContract } from '../../../contract/PstContract';
import { Warp } from '../../../core/Warp';
import { DEFAULT_LEVEL_DB_LOCATION, WarpFactory } from '../../../core/WarpFactory';
import { LoggerFactory } from '../../../logging/LoggerFactory';
describe('Testing the Profit Sharing Token', () => {
let contractSrc: string;
let wallet: JWKInterface;
let walletAddress: string;
let initialState: PstState;
let arweave: Arweave;
let arlocal: ArLocal;
let warp: Warp;
let pst: PstContract;
let contractTxId;
beforeAll(async () => {
// note: each tests suit (i.e. file with tests that Jest is running concurrently
// with another files has to have ArLocal set to a different port!)
arlocal = new ArLocal(2222, false);
await arlocal.start();
LoggerFactory.INST.logLevel('error');
warp = WarpFactory.forLocal(2222);
({ arweave } = warp);
({ jwk: wallet, address: walletAddress } = await warp.generateWallet());
contractSrc = fs.readFileSync(path.join(__dirname, '../data/kv-storage.js'), 'utf8');
const stateFromFile: PstState = JSON.parse(fs.readFileSync(path.join(__dirname, '../data/token-pst.json'), 'utf8'));
initialState = {
...stateFromFile,
...{
owner: walletAddress
}
};
// deploying contract using the new SDK.
({ contractTxId } = await warp.deploy({
wallet,
initState: JSON.stringify(initialState),
src: contractSrc
}));
// connecting to the PST contract
pst = warp.pst(contractTxId).setEvaluationOptions({
useKVStorage: true
}) as PstContract;
pst.connect(wallet);
await mineBlock(warp);
});
afterAll(async () => {
await arlocal.stop();
fs.rmSync(`${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`, { recursive: true });
});
it('should initialize', async () => {
// this is done to "initialize" the state
await pst.writeInteraction({
function: 'mint',
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
qty: 10000000
});
await mineBlock(warp);
await pst.writeInteraction({
function: 'mint',
target: '33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA',
qty: 23111222
});
await mineBlock(warp);
await pst.writeInteraction({
function: 'mint',
target: walletAddress,
qty: 555669
});
await mineBlock(warp);
});
it('should read pst state and balance data', async () => {
expect(await pst.currentState()).toEqual(initialState);
expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000);
expect((await pst.currentBalance('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).balance).toEqual(23111222);
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669);
});
it('should properly transfer tokens', async () => {
await pst.transfer({
target: 'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M',
qty: 555
});
await mineBlock(warp);
expect((await pst.currentBalance(walletAddress)).balance).toEqual(555669 - 555);
expect((await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M')).balance).toEqual(10000000 + 555);
});
it('should properly view contract state', async () => {
const result = await pst.currentBalance('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M');
expect(result.balance).toEqual(10000000 + 555);
expect(result.ticker).toEqual('EXAMPLE_PST_TOKEN');
expect(result.target).toEqual('uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M');
});
it('should properly read storage value', async () => {
expect((await pst.getStorageValues([walletAddress])).cachedValue.get(walletAddress)).toEqual(555669 - 555);
expect(
(await pst.getStorageValues(['uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'])).cachedValue.get(
'uhE-QeYS8i4pmUtnxQyHD7dzXFNaJ9oMK-IM-QPNY6M'
)
).toEqual(10000000 + 555);
expect(
(await pst.getStorageValues(['33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'])).cachedValue.get(
'33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'
)
).toEqual(23111222);
expect((await pst.getStorageValues(['foo'])).cachedValue.get('foo')).toBeNull();
});
});

View File

@@ -0,0 +1,71 @@
export async function handle(state, action) {
const input = action.input;
const caller = action.caller;
if (!state.kvOps) {
// state.kvOps = {};
}
if (input.function === 'mint') {
console.log('mint', input.target, input.qty);
await SmartWeave.kv.put(input.target, input.qty);
// for debug or whatever
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
return {state};
}
if (input.function === 'transfer') {
const target = input.target;
const qty = input.qty;
if (!Number.isInteger(qty)) {
throw new ContractError('Invalid value for "qty". Must be an integer');
}
if (!target) {
throw new ContractError('No target specified');
}
if (qty <= 0 || caller === target) {
throw new ContractError('Invalid token transfer');
}
let callerBalance = await SmartWeave.kv.get(caller);
callerBalance = callerBalance ? callerBalance : 0;
if (callerBalance < qty) {
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
}
// Lower the token balance of the caller
callerBalance -= qty;
await SmartWeave.kv.put(caller, callerBalance);
let targetBalance = await SmartWeave.kv.get(target);
targetBalance = targetBalance ? targetBalance : 0;
targetBalance += qty;
await SmartWeave.kv.put(target, targetBalance);
// for debug or whatever
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
return {state};
}
if (input.function === 'balance') {
const target = input.target;
const ticker = state.ticker;
if (typeof target !== 'string') {
throw new ContractError('Must specify target to get balance for');
}
const result = await SmartWeave.kv.get(target);
console.log('balance', {target: input.target, balance: result});
return {result: {target, ticker, balance: result ? result : 0}};
}
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
}

View File

@@ -1,5 +1,6 @@
import { LevelDbCache } from '../../cache/impl/LevelDbCache'; import { LevelDbCache } from '../../cache/impl/LevelDbCache';
import { defaultCacheOptions, WarpFactory } from '../../core/WarpFactory'; import { defaultCacheOptions } from '../../core/WarpFactory';
import { CacheKey } from '../../cache/SortKeyCache';
const getContractId = (i: number) => `contract${i}`.padStart(43, '0'); const getContractId = (i: number) => `contract${i}`.padStart(43, '0');
const getSortKey = (j: number) => const getSortKey = (j: number) =>
@@ -13,7 +14,7 @@ describe('LevelDB cache prune', () => {
for (let j = 0; j < numRepeatingEntries; j++) { for (let j = 0; j < numRepeatingEntries; j++) {
await sut.put( await sut.put(
{ {
contractTxId: getContractId(i), key: getContractId(i),
sortKey: getSortKey(j) sortKey: getSortKey(j)
}, },
{ result: `contract${i}:${j}` } { result: `contract${i}:${j}` }
@@ -73,12 +74,12 @@ describe('LevelDB cache prune', () => {
for (let i = 0; i < contracts; i++) { for (let i = 0; i < contracts; i++) {
// Check newest elements are present // Check newest elements are present
for (let j = 0; j < toLeave; j++) { for (let j = 0; j < toLeave; j++) {
expect(await sut.get(getContractId(i), getSortKey(entriesPerContract - j - 1))).toBeTruthy(); expect(await sut.get(new CacheKey(getContractId(i), getSortKey(entriesPerContract - j - 1)))).toBeTruthy();
} }
// Check old elements are removed // Check old elements are removed
for (let j = toLeave; j < entriesPerContract; j++) { for (let j = toLeave; j < entriesPerContract; j++) {
expect(await sut.get(getContractId(i), getSortKey(entriesPerContract - j - 1))).toBeFalsy(); expect(await sut.get(new CacheKey(getContractId(i), getSortKey(entriesPerContract - j - 1)))).toBeFalsy();
} }
} }
@@ -94,13 +95,13 @@ describe('LevelDB cache prune', () => {
// Removed elements // Removed elements
for (let j = 0; j < entriesPerContract; j++) { for (let j = 0; j < entriesPerContract; j++) {
expect(await sut.get(getContractId(0), getSortKey(j))).toBeFalsy(); expect(await sut.get(new CacheKey(getContractId(0), getSortKey(j)))).toBeFalsy();
} }
// Remaining elements // Remaining elements
for (let i = 1; i < contracts; i++) { for (let i = 1; i < contracts; i++) {
for (let j = 0; j < entriesPerContract; j++) { for (let j = 0; j < entriesPerContract; j++) {
expect(await sut.get(getContractId(i), getSortKey(j))).toBeTruthy(); expect(await sut.get(new CacheKey(getContractId(i), getSortKey(j)))).toBeTruthy();
} }
} }

View File

@@ -23,6 +23,7 @@ describe('Evaluation options evaluator', () => {
throwOnInternalWriteError: true, throwOnInternalWriteError: true,
unsafeClient: 'throw', unsafeClient: 'throw',
updateCacheForEachInteraction: false, updateCacheForEachInteraction: false,
useKVStorage: false,
useVM2: false, useVM2: false,
waitForConfirmation: false, waitForConfirmation: false,
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
@@ -58,6 +59,7 @@ describe('Evaluation options evaluator', () => {
throwOnInternalWriteError: true, throwOnInternalWriteError: true,
unsafeClient: 'throw', unsafeClient: 'throw',
updateCacheForEachInteraction: false, updateCacheForEachInteraction: false,
useKVStorage: false,
useVM2: true, useVM2: true,
waitForConfirmation: false, waitForConfirmation: false,
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
@@ -87,6 +89,7 @@ describe('Evaluation options evaluator', () => {
throwOnInternalWriteError: true, throwOnInternalWriteError: true,
unsafeClient: 'allow', unsafeClient: 'allow',
updateCacheForEachInteraction: false, updateCacheForEachInteraction: false,
useKVStorage: false,
useVM2: true, useVM2: true,
waitForConfirmation: false, waitForConfirmation: false,
walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/' walletBalanceUrl: 'http://nyc-1.dev.arweave.net:1984/'
@@ -96,9 +99,7 @@ describe('Evaluation options evaluator', () => {
const result = new EvaluationOptionsEvaluator(contract2.evaluationOptions(), { const result = new EvaluationOptionsEvaluator(contract2.evaluationOptions(), {
useVM2: false useVM2: false
}).rootOptions; }).rootOptions;
}).toThrow( }).toThrow('Option {useVM2} differs.');
'Option {useVM2} differs. EvaluationOptions: [true], manifest: [false]. Use contract.setEvaluationOptions({useVM2: false) to evaluate contract state.'
);
}); });
it('should properly set foreign evaluation options - unsafeClient - allow', async () => { it('should properly set foreign evaluation options - unsafeClient - allow', async () => {

View File

@@ -0,0 +1,53 @@
import { DEFAULT_LEVEL_DB_LOCATION } from '../../core/WarpFactory';
import fs from 'fs';
import { SmartWeaveGlobal } from '../../legacy/smartweave-global';
import Arweave from 'arweave';
import { DefaultEvaluationOptions } from '../../core/modules/StateEvaluator';
import { LevelDbCache } from '../../cache/impl/LevelDbCache';
import { GQLNodeInterface } from '../../legacy/gqlResult';
import { CacheKey } from '../../cache/SortKeyCache';
describe('KV database', () => {
describe('with the SmartWeave Global KV implementation', () => {
const arweave = Arweave.init({});
const db = new LevelDbCache({
inMemory: false,
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/KV_TRIE_TEST_SW_GLOBAL`
});
const sut = new SmartWeaveGlobal(arweave, { id: 'a', owner: '' }, new DefaultEvaluationOptions(), db);
it('should set values', async () => {
sut._activeTx = { sortKey: '123' } as GQLNodeInterface;
await sut.kv.put('foo', 'bar');
await sut.kv.put('one', [1]);
await sut.kv.put('one', { val: 1 });
await sut.kv.put('two', { val: 2 });
expect(await sut.kv.get('one')).toEqual({ val: 1 });
expect(await sut.kv.get('ninety')).toBeNull();
await sut.kv.commit();
expect(await sut.kv.get('one')).toEqual({ val: 1 });
sut._activeTx = { sortKey: '222' } as GQLNodeInterface;
await sut.kv.put('one', '1');
await sut.kv.put('three', 3);
await sut.kv.commit();
sut._activeTx = { sortKey: '330' } as GQLNodeInterface;
await sut.kv.put('one', { val: [1] });
await sut.kv.put('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA', 23111222);
await sut.kv.commit();
expect(await sut.kv.get('foo')).toEqual('bar');
expect(await sut.kv.get('one')).toEqual({ val: [1] });
expect(await sut.kv.get('three')).toEqual(3);
expect(await sut.kv.get('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA')).toEqual(23111222);
expect((await db.get(new CacheKey('foo', '123'))).cachedValue).toEqual('bar');
expect((await db.get(new CacheKey('one', '123'))).cachedValue).toEqual({ val: 1 });
expect((await db.get(new CacheKey('one', '222'))).cachedValue).toEqual('1');
expect((await db.get(new CacheKey('one', '330'))).cachedValue).toEqual({ val: [1] });
});
});
});

View File

@@ -1,38 +1,45 @@
/** /**
* A cache that stores its values per contract tx id and sort key. * A cache that stores its values per dedicated key and sort key.
* A sort key is a value that the SmartWeave protocol is using * A sort key is a value that the SmartWeave protocol is using
* to sort contract transactions ({@link LexicographicalInteractionsSorter}. * to sort contract transactions ({@link LexicographicalInteractionsSorter}.
* *
* All values should be stored in a lexicographical order (per contract) - * All values should be stored in a lexicographical order (per key) -
* sorted by the sort key. * sorted by the sort key.
*/ */
export interface SortKeyCache<V> { export interface SortKeyCache<V> {
getLessOrEqual(key: string, sortKey: string): Promise<SortKeyCacheResult<V> | null>; getLessOrEqual(key: string, sortKey: string): Promise<SortKeyCacheResult<V> | null>;
/** /**
* returns latest value stored for given contractTxId * returns value stored for a given key and last sortKey
*/ */
getLast(contractTxId: string): Promise<SortKeyCacheResult<V> | null>; getLast(key: string): Promise<SortKeyCacheResult<V> | null>;
/** /**
* returns last cached sort key - takes all contracts into account * returns last cached sort key - takes all keys into account
*/ */
getLastSortKey(): Promise<string | null>; getLastSortKey(): Promise<string | null>;
/** /**
* returns value for the key and exact blockHeight * returns value for the key and exact sortKey
*/ */
get(contractTxId: string, sortKey: string, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null>; get(cacheKey: CacheKey): Promise<SortKeyCacheResult<V> | null>;
/** /**
* puts new value in cache under given {@link CacheKey.key} and {@link CacheKey.blockHeight}. * puts new value in cache under given {@link CacheKey.key} and {@link CacheKey.sortKey}.
*/ */
put(cacheKey: CacheKey, value: V): Promise<void>; put(cacheKey: CacheKey, value: V): Promise<void>;
/** /**
* removes contract's data * removes all data stored under a specified key
*/ */
delete(contractTxId: string): Promise<void>; delete(key: string): Promise<void>;
/**
* executes a list of stacked operations
*/
batch(opStack: BatchDBOp<V>[]);
open(): Promise<void>;
close(): Promise<void>; close(): Promise<void>;
@@ -43,9 +50,9 @@ export interface SortKeyCache<V> {
dump(): Promise<any>; dump(): Promise<any>;
/** /**
* Return all cached contracts. * Return all cached keys.
*/ */
allContracts(): Promise<string[]>; keys(): Promise<string[]>;
/** /**
* returns underlying storage (LevelDB, LMDB, sqlite...) * returns underlying storage (LevelDB, LMDB, sqlite...)
@@ -55,10 +62,10 @@ export interface SortKeyCache<V> {
/** /**
* leaves n-latest (i.e. with latest (in lexicographic order) sort keys) * leaves n-latest (i.e. with latest (in lexicographic order) sort keys)
* entries for each cached contract * entries for each cached key
* *
* @param entriesStored - how many latest entries should be left * @param entriesStored - how many latest entries should be left
* for each cached contract * for each cached key
* *
* @retun PruneStats if getting them doesn't introduce a delay, null otherwise * @retun PruneStats if getting them doesn't introduce a delay, null otherwise
*/ */
@@ -73,10 +80,21 @@ export interface PruneStats {
} }
export class CacheKey { export class CacheKey {
constructor(readonly contractTxId: string, readonly sortKey: string) {} constructor(readonly key: string, readonly sortKey: string) {}
} }
// tslint:disable-next-line:max-classes-per-file // tslint:disable-next-line:max-classes-per-file
export class SortKeyCacheResult<V> { export class SortKeyCacheResult<V> {
constructor(readonly sortKey: string, readonly cachedValue: V) {} constructor(readonly sortKey: string, readonly cachedValue: V) {}
} }
export declare type BatchDBOp<V> = PutBatch<V> | DelBatch;
export interface PutBatch<V> {
type: 'put';
key: CacheKey;
value: V;
}
export interface DelBatch {
type: 'del';
key: string;
}

View File

@@ -1,4 +1,4 @@
import { SortKeyCache, CacheKey, SortKeyCacheResult, PruneStats } from '../SortKeyCache'; import { BatchDBOp, CacheKey, SortKeyCache, SortKeyCacheResult } from '../SortKeyCache';
import { Level } from 'level'; import { Level } from 'level';
import { MemoryLevel } from 'memory-level'; import { MemoryLevel } from 'memory-level';
import { CacheOptions } from '../../core/WarpFactory'; import { CacheOptions } from '../../core/WarpFactory';
@@ -43,15 +43,15 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
constructor(private readonly cacheOptions: CacheOptions) {} constructor(private readonly cacheOptions: CacheOptions) {}
async get(contractTxId: string, sortKey: string, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null> { async get(cacheKey: CacheKey, returnDeepCopy?: boolean): Promise<SortKeyCacheResult<V> | null> {
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' }); const contractCache = this.db.sublevel<string, any>(cacheKey.key, { valueEncoding: 'json' });
// manually opening to fix https://github.com/Level/level/issues/221 // manually opening to fix https://github.com/Level/level/issues/221
await contractCache.open(); await contractCache.open();
try { try {
const result = await contractCache.get(sortKey); const result = await contractCache.get(cacheKey.sortKey);
return { return {
sortKey: sortKey, sortKey: cacheKey.sortKey,
cachedValue: result cachedValue: result
}; };
} catch (e: any) { } catch (e: any) {
@@ -63,8 +63,8 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
} }
} }
async getLast(contractTxId: string): Promise<SortKeyCacheResult<V> | null> { async getLast(key: string): Promise<SortKeyCacheResult<V> | null> {
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' }); const contractCache = this.db.sublevel<string, any>(key, { valueEncoding: 'json' });
// manually opening to fix https://github.com/Level/level/issues/221 // manually opening to fix https://github.com/Level/level/issues/221
await contractCache.open(); await contractCache.open();
const keys = await contractCache.keys({ reverse: true, limit: 1 }).all(); const keys = await contractCache.keys({ reverse: true, limit: 1 }).all();
@@ -78,8 +78,8 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
} }
} }
async getLessOrEqual(contractTxId: string, sortKey: string): Promise<SortKeyCacheResult<V> | null> { async getLessOrEqual(key: string, sortKey: string): Promise<SortKeyCacheResult<V> | null> {
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' }); const contractCache = this.db.sublevel<string, any>(key, { valueEncoding: 'json' });
// manually opening to fix https://github.com/Level/level/issues/221 // manually opening to fix https://github.com/Level/level/issues/221
await contractCache.open(); await contractCache.open();
const keys = await contractCache.keys({ reverse: true, lte: sortKey, limit: 1 }).all(); const keys = await contractCache.keys({ reverse: true, lte: sortKey, limit: 1 }).all();
@@ -94,20 +94,36 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
} }
async put(stateCacheKey: CacheKey, value: V): Promise<void> { async put(stateCacheKey: CacheKey, value: V): Promise<void> {
const contractCache = this.db.sublevel<string, any>(stateCacheKey.contractTxId, { valueEncoding: 'json' }); const contractCache = this.db.sublevel<string, any>(stateCacheKey.key, { valueEncoding: 'json' });
// manually opening to fix https://github.com/Level/level/issues/221 // manually opening to fix https://github.com/Level/level/issues/221
await contractCache.open(); await contractCache.open();
await contractCache.put(stateCacheKey.sortKey, value); await contractCache.put(stateCacheKey.sortKey, value);
} }
async delete(contractTxId: string): Promise<void> { async delete(key: string): Promise<void> {
const contractCache = this.db.sublevel<string, any>(contractTxId, { valueEncoding: 'json' }); const contractCache = this.db.sublevel<string, any>(key, { valueEncoding: 'json' });
await contractCache.open(); await contractCache.open();
await contractCache.clear(); await contractCache.clear();
} }
close(): Promise<void> { async batch(opStack: BatchDBOp<V>[]) {
return this.db.close(); for (const op of opStack) {
if (op.type === 'put') {
await this.put(op.key, op.value);
} else if (op.type === 'del') {
await this.delete(op.key);
}
}
}
async open(): Promise<void> {
await this.db.open();
}
async close(): Promise<void> {
if (this._db) {
await this._db.close();
}
} }
async dump(): Promise<any> { async dump(): Promise<any> {
@@ -134,7 +150,7 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
return lastSortKey == '' ? null : lastSortKey; return lastSortKey == '' ? null : lastSortKey;
} }
async allContracts(): Promise<string[]> { async keys(): Promise<string[]> {
await this.db.open(); await this.db.open();
const keys = await this.db.keys().all(); const keys = await this.db.keys().all();
@@ -170,7 +186,7 @@ export class LevelDbCache<V = any> implements SortKeyCache<V> {
entriesStored = 1; entriesStored = 1;
} }
const contracts = await this.allContracts(); const contracts = await this.keys();
for (let i = 0; i < contracts.length; i++) { for (let i = 0; i < contracts.length; i++) {
const contractCache = this.db.sublevel<string, any>(contracts[i], { valueEncoding: 'json' }); const contractCache = this.db.sublevel<string, any>(contracts[i], { valueEncoding: 'json' });

View File

@@ -232,4 +232,6 @@ export interface Contract<State = unknown> {
getEoEvaluator(): EvaluationOptionsEvaluator; getEoEvaluator(): EvaluationOptionsEvaluator;
isRoot(): boolean; isRoot(): boolean;
getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, any>>>;
} }

View File

@@ -102,7 +102,8 @@ export class EvaluationOptionsEvaluator {
allowBigInt: () => this.rootOptions['allowBigInt'], // not sure about this allowBigInt: () => this.rootOptions['allowBigInt'], // not sure about this
walletBalanceUrl: () => this.rootOptions['walletBalanceUrl'], walletBalanceUrl: () => this.rootOptions['walletBalanceUrl'],
mineArLocalBlocks: () => this.rootOptions['mineArLocalBlocks'], mineArLocalBlocks: () => this.rootOptions['mineArLocalBlocks'],
cacheEveryNInteractions: () => this.rootOptions['cacheEveryNInteractions'] cacheEveryNInteractions: () => this.rootOptions['cacheEveryNInteractions'],
useKVStorage: (foreignOptions) => foreignOptions['useKVStorage']
}; };
/** /**
@@ -114,9 +115,12 @@ export class EvaluationOptionsEvaluator {
if (manifestOptions) { if (manifestOptions) {
const errors = []; const errors = [];
for (const k in manifestOptions) { for (const k in manifestOptions) {
if (k === 'useKVStorage') {
continue;
}
if (userSetOptions[k] !== manifestOptions[k]) { if (userSetOptions[k] !== manifestOptions[k]) {
errors.push( errors.push(
`Option {${k}} differs. EvaluationOptions: [${userSetOptions[k]}], manifest: [${manifestOptions[k]}]. Use contract.setEvaluationOptions({${k}: ${manifestOptions[k]}) to evaluate contract state.` `Option {${k}} differs. EvaluationOptions: [${userSetOptions[k]}], manifest: [${manifestOptions[k]}]. Use contract.setEvaluationOptions({${k}: ${manifestOptions[k]}}) to evaluate contract state.`
); );
} }
} }

View File

@@ -73,13 +73,16 @@ export class HandlerBasedContract<State> implements Contract<State> {
this._sorter = new LexicographicalInteractionsSorter(warp.arweave); this._sorter = new LexicographicalInteractionsSorter(warp.arweave);
if (_parentContract != null) { if (_parentContract != null) {
this._evaluationOptions = this.getRoot().evaluationOptions(); this._evaluationOptions = this.getRoot().evaluationOptions();
if (_parentContract.evaluationOptions().useKVStorage) {
throw new Error('Foreign writes or reads are forbidden for kv storage contracts');
}
this._callDepth = _parentContract.callDepth() + 1; this._callDepth = _parentContract.callDepth() + 1;
const callingInteraction: InteractionCall = _parentContract const callingInteraction: InteractionCall = _parentContract
.getCallStack() .getCallStack()
.getInteraction(_innerCallData.callingInteraction.id); .getInteraction(_innerCallData.callingInteraction.id);
if (this._callDepth > this._evaluationOptions.maxCallDepth) { if (this._callDepth > this._evaluationOptions.maxCallDepth) {
throw Error( throw new Error(
`Max call depth of ${this._evaluationOptions.maxCallDepth} has been exceeded for interaction ${JSON.stringify( `Max call depth of ${this._evaluationOptions.maxCallDepth} has been exceeded for interaction ${JSON.stringify(
callingInteraction.interactionInput callingInteraction.interactionInput
)}` )}`
@@ -468,7 +471,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
this.logger.debug('State fully cached, not loading interactions.'); this.logger.debug('State fully cached, not loading interactions.');
if (forceDefinitionLoad || evolvedSrcTxId || interactions?.length) { if (forceDefinitionLoad || evolvedSrcTxId || interactions?.length) {
contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId); contractDefinition = await definitionLoader.load<State>(contractTxId, evolvedSrcTxId);
handler = await this.safeGetHandler(contractDefinition);
if (interactions?.length) { if (interactions?.length) {
sortedInteractions = this._sorter.sort(interactions.map((i) => ({ node: i, cursor: null }))); sortedInteractions = this._sorter.sort(interactions.map((i) => ({ node: i, cursor: null })));
} }
@@ -507,7 +509,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
// - as no other contracts will be called. // - as no other contracts will be called.
this._rootSortKey = sortedInteractions[sortedInteractions.length - 1].sortKey; this._rootSortKey = sortedInteractions[sortedInteractions.length - 1].sortKey;
} }
handler = await this.safeGetHandler(contractDefinition);
} }
if (this.isRoot()) { if (this.isRoot()) {
this._eoEvaluator = new EvaluationOptionsEvaluator( this._eoEvaluator = new EvaluationOptionsEvaluator(
@@ -518,9 +519,19 @@ export class HandlerBasedContract<State> implements Contract<State> {
const contractEvaluationOptions = this.isRoot() const contractEvaluationOptions = this.isRoot()
? this._eoEvaluator.rootOptions ? this._eoEvaluator.rootOptions
: this.getEoEvaluator().forForeignContract(contractDefinition.manifest?.evaluationOptions); : this.getEoEvaluator().forForeignContract(contractDefinition.manifest?.evaluationOptions);
if (!this.isRoot() && contractEvaluationOptions.useKVStorage) {
throw new Error('Foreign read/writes cannot be performed on kv storage contracts');
}
this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions); this.ecLogger.debug(`Evaluation options ${contractTxId}:`, contractEvaluationOptions);
if (contractDefinition) {
handler = (await this.warp.executorFactory.create(
contractDefinition,
contractEvaluationOptions,
this.warp
)) as HandlerApi<State>;
}
return { return {
warp: this.warp, warp: this.warp,
contract: this, contract: this,
@@ -533,11 +544,6 @@ export class HandlerBasedContract<State> implements Contract<State> {
}; };
} }
private async safeGetHandler(contractDefinition: ContractDefinition<any>): Promise<HandlerApi<State> | null> {
const { executorFactory } = this.warp;
return (await executorFactory.create(contractDefinition, this._evaluationOptions, this.warp)) as HandlerApi<State>;
}
private getToSortKey(upToSortKey?: string) { private getToSortKey(upToSortKey?: string) {
if (this._parentContract?.rootSortKey) { if (this._parentContract?.rootSortKey) {
if (!upToSortKey) { if (!upToSortKey) {
@@ -815,4 +821,30 @@ export class HandlerBasedContract<State> implements Contract<State> {
isRoot(): boolean { isRoot(): boolean {
return this._parentContract == null; return this._parentContract == null;
} }
async getStorageValues(keys: string[]): Promise<SortKeyCacheResult<Map<string, any>>> {
const lastCached = await this.warp.stateEvaluator.getCache().getLast(this.txId());
if (lastCached == null) {
return {
sortKey: null,
cachedValue: new Map()
};
}
const storage = this.warp.kvStorageFactory(this.txId());
const result: Map<string, any> = new Map();
try {
await storage.open();
for (const key of keys) {
const lastValue = await storage.getLessOrEqual(key, lastCached.sortKey);
result.set(key, lastValue == null ? null : lastValue.cachedValue);
}
return {
sortKey: lastCached.sortKey,
cachedValue: result
};
} finally {
await storage.close();
}
}
} }

View File

@@ -31,8 +31,11 @@ import { ContractDefinition, SrcCache } from './ContractDefinition';
import { CustomSignature } from '../contract/Signature'; import { CustomSignature } from '../contract/Signature';
import { SourceData } from '../contract/deploy/impl/SourceImpl'; import { SourceData } from '../contract/deploy/impl/SourceImpl';
import Transaction from 'arweave/node/lib/transaction'; import Transaction from 'arweave/node/lib/transaction';
import { DEFAULT_LEVEL_DB_LOCATION } from './WarpFactory';
import { LevelDbCache } from '../cache/impl/LevelDbCache';
export type WarpEnvironment = 'local' | 'testnet' | 'mainnet' | 'custom'; export type WarpEnvironment = 'local' | 'testnet' | 'mainnet' | 'custom';
export type KVStorageFactory = (contractTxId: string) => SortKeyCache<any>;
/** /**
* The Warp "motherboard" ;-). * The Warp "motherboard" ;-).
@@ -49,6 +52,7 @@ export class Warp {
*/ */
readonly createContract: CreateContract; readonly createContract: CreateContract;
readonly testing: Testing; readonly testing: Testing;
kvStorageFactory: KVStorageFactory;
private readonly plugins: Map<WarpPluginType, WarpPlugin<unknown, unknown>> = new Map(); private readonly plugins: Map<WarpPluginType, WarpPlugin<unknown, unknown>> = new Map();
@@ -62,6 +66,12 @@ export class Warp {
) { ) {
this.createContract = new DefaultCreateContract(arweave, this); this.createContract = new DefaultCreateContract(arweave, this);
this.testing = new Testing(arweave); this.testing = new Testing(arweave);
this.kvStorageFactory = (contractTxId: string) => {
return new LevelDbCache({
inMemory: false,
dbLocation: `${DEFAULT_LEVEL_DB_LOCATION}/kv/ldb/${contractTxId}`
});
};
} }
static builder( static builder(
@@ -178,4 +188,9 @@ export class Warp {
knownWarpPlugins.includes(value as WarpKnownPluginType) || knownWarpPluginsPartial.some((p) => value.match(p)) knownWarpPlugins.includes(value as WarpKnownPluginType) || knownWarpPluginsPartial.some((p) => value.match(p))
); );
} }
useKVStorageFactory(factory: KVStorageFactory): Warp {
this.kvStorageFactory = factory;
return this;
}
} }

View File

@@ -42,8 +42,6 @@ export const defaultCacheOptions: CacheOptions = {
* All versions use the {@link Evolve} plugin. * All versions use the {@link Evolve} plugin.
*/ */
export class WarpFactory { export class WarpFactory {
private stateCache: SortKeyCache<EvalStateResult<unknown>>;
/** /**
* creates a Warp instance suitable for testing in a local environment * creates a Warp instance suitable for testing in a local environment
* (e.g. usually using ArLocal) * (e.g. usually using ArLocal)
@@ -61,7 +59,7 @@ export class WarpFactory {
...defaultCacheOptions, ...defaultCacheOptions,
inMemory: true inMemory: true
} }
) { ): Warp {
return this.customArweaveGw(arweave, cacheOptions, 'local'); return this.customArweaveGw(arweave, cacheOptions, 'local');
} }
@@ -77,7 +75,7 @@ export class WarpFactory {
port: 443, port: 443,
protocol: 'https' protocol: 'https'
}) })
) { ): Warp {
if (useArweaveGw) { if (useArweaveGw) {
return this.customArweaveGw(arweave, cacheOptions, 'testnet'); return this.customArweaveGw(arweave, cacheOptions, 'testnet');
} else { } else {
@@ -106,7 +104,7 @@ export class WarpFactory {
port: 443, port: 443,
protocol: 'https' protocol: 'https'
}) })
) { ): Warp {
if (useArweaveGw) { if (useArweaveGw) {
return this.customArweaveGw(arweave, cacheOptions, 'mainnet'); return this.customArweaveGw(arweave, cacheOptions, 'mainnet');
} else { } else {

View File

@@ -140,6 +140,8 @@ export class DefaultEvaluationOptions implements EvaluationOptions {
throwOnInternalWriteError = true; throwOnInternalWriteError = true;
cacheEveryNInteractions = -1; cacheEveryNInteractions = -1;
useKVStorage = false;
} }
// an interface for the contract EvaluationOptions - can be used to change the behaviour of some features. // an interface for the contract EvaluationOptions - can be used to change the behaviour of some features.
@@ -217,4 +219,7 @@ export interface EvaluationOptions {
// force SDK to cache the state after evaluating each N interactions // force SDK to cache the state after evaluating each N interactions
// defaults to -1, which effectively turns off this feature // defaults to -1, which effectively turns off this feature
cacheEveryNInteractions: number; cacheEveryNInteractions: number;
// whether a separate key-value storage should be used for the contract
useKVStorage: boolean;
} }

View File

@@ -220,7 +220,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
contractTxId: string, contractTxId: string,
sortKey: string sortKey: string
): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> { ): Promise<SortKeyCacheResult<EvalStateResult<State>> | null> {
return (await this.cache.get(contractTxId, sortKey)) as SortKeyCacheResult<EvalStateResult<State>>; return (await this.cache.get(new CacheKey(contractTxId, sortKey))) as SortKeyCacheResult<EvalStateResult<State>>;
} }
async hasContractCached(contractTxId: string): Promise<boolean> { async hasContractCached(contractTxId: string): Promise<boolean> {
@@ -232,7 +232,7 @@ export class CacheableStateEvaluator extends DefaultStateEvaluator {
} }
async allCachedContracts(): Promise<string[]> { async allCachedContracts(): Promise<string[]> {
return await this.cache.allContracts(); return await this.cache.keys();
} }
setCache(cache: SortKeyCache<EvalStateResult<unknown>>): void { setCache(cache: SortKeyCache<EvalStateResult<unknown>>): void {

View File

@@ -46,13 +46,20 @@ export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknow
evaluationOptions: EvaluationOptions, evaluationOptions: EvaluationOptions,
warp: Warp warp: Warp
): Promise<HandlerApi<State>> { ): Promise<HandlerApi<State>> {
let kvStorage = null;
if (evaluationOptions.useKVStorage) {
kvStorage = warp.kvStorageFactory(contractDefinition.txId);
}
const swGlobal = new SmartWeaveGlobal( const swGlobal = new SmartWeaveGlobal(
this.arweave, this.arweave,
{ {
id: contractDefinition.txId, id: contractDefinition.txId,
owner: contractDefinition.owner owner: contractDefinition.owner
}, },
evaluationOptions evaluationOptions,
kvStorage
); );
const extensionPlugins = warp.matchPlugins(`^smartweave-extension-`); const extensionPlugins = warp.matchPlugins(`^smartweave-extension-`);

View File

@@ -14,7 +14,7 @@ export const sortingFirst = ''.padEnd(64, '0');
export const sortingLast = ''.padEnd(64, 'z'); export const sortingLast = ''.padEnd(64, 'z');
export const genesisSortKey = `${''.padStart(12, '0')},${firstSortKeyMs},${sortingFirst}`; export const genesisSortKey = `${''.padStart(12, '0')},${firstSortKeyMs},${sortingFirst}`;
export const lastPossibleKey = `${''.padStart(12, '9')},${lastSortKeyMs},${sortingLast}`; export const lastPossibleSortKey = `${''.padStart(12, '9')},${lastSortKeyMs},${sortingLast}`;
/** /**
* implementation that is based on current's SDK sorting alg. * implementation that is based on current's SDK sorting alg.

View File

@@ -13,7 +13,7 @@ import { DefinitionLoader } from '../DefinitionLoader';
import { WasmSrc } from './wasm/WasmSrc'; import { WasmSrc } from './wasm/WasmSrc';
import { WarpEnvironment } from '../../Warp'; import { WarpEnvironment } from '../../Warp';
import { TagsParser } from './TagsParser'; import { TagsParser } from './TagsParser';
import { SortKeyCache } from '../../../cache/SortKeyCache'; import { CacheKey, SortKeyCache } from '../../../cache/SortKeyCache';
/** /**
* An extension to {@link ContractDefinitionLoader} that makes use of * An extension to {@link ContractDefinitionLoader} that makes use of
@@ -137,12 +137,12 @@ export class WarpGatewayContractDefinitionLoader implements DefinitionLoader {
// Gets ContractDefinition and ContractSource from two caches and returns a combined structure // Gets ContractDefinition and ContractSource from two caches and returns a combined structure
private async getFromCache(contractTxId: string, srcTxId?: string): Promise<ContractDefinition<any> | null> { private async getFromCache(contractTxId: string, srcTxId?: string): Promise<ContractDefinition<any> | null> {
const contract = await this.definitionCache.get(contractTxId, 'cd'); const contract = await this.definitionCache.get(new CacheKey(contractTxId, 'cd'));
if (!contract) { if (!contract) {
return null; return null;
} }
const src = await this.srcCache.get(srcTxId || contract.cachedValue.srcTxId, 'src'); const src = await this.srcCache.get(new CacheKey(srcTxId || contract.cachedValue.srcTxId, 'src'));
if (!src) { if (!src) {
return null; return null;
} }
@@ -153,7 +153,7 @@ export class WarpGatewayContractDefinitionLoader implements DefinitionLoader {
private async putToCache(contractTxId: string, value: ContractDefinition<any>, srcTxId?: string): Promise<void> { private async putToCache(contractTxId: string, value: ContractDefinition<any>, srcTxId?: string): Promise<void> {
const src = new SrcCache(value); const src = new SrcCache(value);
const contract = new ContractCache(value); const contract = new ContractCache(value);
await this.definitionCache.put({ contractTxId: contractTxId, sortKey: 'cd' }, contract); await this.definitionCache.put({ key: contractTxId, sortKey: 'cd' }, contract);
await this.srcCache.put({ contractTxId: srcTxId || contract.srcTxId, sortKey: 'src' }, src); await this.srcCache.put({ key: srcTxId || contract.srcTxId, sortKey: 'src' }, src);
} }
} }

View File

@@ -3,9 +3,11 @@ import { ContractDefinition } from '../../../../core/ContractDefinition';
import { ExecutionContext } from '../../../../core/ExecutionContext'; import { ExecutionContext } from '../../../../core/ExecutionContext';
import { EvalStateResult } from '../../../../core/modules/StateEvaluator'; import { EvalStateResult } from '../../../../core/modules/StateEvaluator';
import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global'; import { SmartWeaveGlobal } from '../../../../legacy/smartweave-global';
import { timeout, deepCopy } from '../../../../utils/utils'; import { deepCopy, timeout } from '../../../../utils/utils';
import { InteractionData, InteractionResult } from '../HandlerExecutorFactory'; import { InteractionData, InteractionResult } from '../HandlerExecutorFactory';
import { AbstractContractHandler } from './AbstractContractHandler'; import { AbstractContractHandler } from './AbstractContractHandler';
import { Level } from 'level';
import { DEFAULT_LEVEL_DB_LOCATION } from '../../../WarpFactory';
export class JsHandlerApi<State> extends AbstractContractHandler<State> { export class JsHandlerApi<State> extends AbstractContractHandler<State> {
constructor( constructor(
@@ -39,9 +41,11 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
const { warp } = executionContext; const { warp } = executionContext;
await this.swGlobal.kv.open();
const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]); const handlerResult = await Promise.race([timeoutPromise, this.contractFunction(stateCopy, interaction)]);
if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) { if (handlerResult && (handlerResult.state !== undefined || handlerResult.result !== undefined)) {
await this.swGlobal.kv.commit();
return { return {
type: 'ok', type: 'ok',
result: handlerResult.result, result: handlerResult.result,
@@ -52,6 +56,7 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
// Will be caught below as unexpected exception. // Will be caught below as unexpected exception.
throw new Error(`Unexpected result from contract: ${JSON.stringify(handlerResult)}`); throw new Error(`Unexpected result from contract: ${JSON.stringify(handlerResult)}`);
} catch (err) { } catch (err) {
await this.swGlobal.kv.rollback();
switch (err.name) { switch (err.name) {
case 'ContractError': case 'ContractError':
return { return {
@@ -73,6 +78,7 @@ export class JsHandlerApi<State> extends AbstractContractHandler<State> {
}; };
} }
} finally { } finally {
await this.swGlobal.kv.close();
if (timeoutId !== null) { if (timeoutId !== null) {
// it is important to clear the timeout promise // it is important to clear the timeout promise
// - promise.race won't "cancel" it automatically if the "handler" promise "wins" // - promise.race won't "cancel" it automatically if the "handler" promise "wins"

View File

@@ -33,8 +33,9 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
this.assignReadContractState<Input>(executionContext, currentTx, currentResult, interactionTx); this.assignReadContractState<Input>(executionContext, currentTx, currentResult, interactionTx);
this.assignWrite(executionContext, currentTx); this.assignWrite(executionContext, currentTx);
await this.swGlobal.kv.open();
const handlerResult = await this.doHandle(interaction); const handlerResult = await this.doHandle(interaction);
await this.swGlobal.kv.commit();
return { return {
type: 'ok', type: 'ok',
result: handlerResult, result: handlerResult,
@@ -42,6 +43,7 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
gasUsed: this.swGlobal.gasUsed gasUsed: this.swGlobal.gasUsed
}; };
} catch (e) { } catch (e) {
await this.swGlobal.kv.rollback();
// note: as exceptions handling in WASM is currently somewhat non-existent // note: as exceptions handling in WASM is currently somewhat non-existent
// https://www.assemblyscript.org/status.html#exceptions // https://www.assemblyscript.org/status.html#exceptions
// and since we have to somehow differentiate different types of exceptions // and since we have to somehow differentiate different types of exceptions
@@ -70,6 +72,8 @@ export class WasmHandlerApi<State> extends AbstractContractHandler<State> {
type: 'error' type: 'error'
}; };
} }
} finally {
await this.swGlobal.kv.close();
} }
} }

View File

@@ -2,6 +2,7 @@
import Arweave from 'arweave'; import Arweave from 'arweave';
import { EvaluationOptions } from '../core/modules/StateEvaluator'; import { EvaluationOptions } from '../core/modules/StateEvaluator';
import { GQLNodeInterface, GQLTagInterface, VrfData } from './gqlResult'; import { GQLNodeInterface, GQLTagInterface, VrfData } from './gqlResult';
import { BatchDBOp, CacheKey, PutBatch, SortKeyCache } from '../cache/SortKeyCache';
/** /**
* *
@@ -56,7 +57,14 @@ export class SmartWeaveGlobal {
caller?: string; caller?: string;
constructor(arweave: Arweave, contract: { id: string; owner: string }, evaluationOptions: EvaluationOptions) { kv: KV;
constructor(
arweave: Arweave,
contract: { id: string; owner: string },
evaluationOptions: EvaluationOptions,
storage: SortKeyCache<any> | null
) {
this.gasUsed = 0; this.gasUsed = 0;
this.gasLimit = Number.MAX_SAFE_INTEGER; this.gasLimit = Number.MAX_SAFE_INTEGER;
this.unsafeClient = arweave; this.unsafeClient = arweave;
@@ -95,6 +103,8 @@ export class SmartWeaveGlobal {
this.getBalance = this.getBalance.bind(this); this.getBalance = this.getBalance.bind(this);
this.extensions = {}; this.extensions = {};
this.kv = new KV(storage, this.transaction);
} }
useGas(gas: number) { useGas(gas: number) {
@@ -162,6 +172,13 @@ class Transaction {
return this.smartWeaveGlobal._activeTx.tags; return this.smartWeaveGlobal._activeTx.tags;
} }
get sortKey(): string {
if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx');
}
return this.smartWeaveGlobal._activeTx.sortKey;
}
get quantity() { get quantity() {
if (!this.smartWeaveGlobal._activeTx) { if (!this.smartWeaveGlobal._activeTx) {
throw new Error('No current Tx'); throw new Error('No current Tx');
@@ -229,3 +246,69 @@ class Vrf {
return Number(result); return Number(result);
} }
} }
export class KV {
private _kvBatch: BatchDBOp<any>[] = [];
constructor(private readonly _storage: SortKeyCache<any> | null, private readonly _transaction: Transaction) {}
async put(key: string, value: any): Promise<void> {
this.checkStorageAvailable();
this._kvBatch.push({
type: 'put',
key: new CacheKey(key, this._transaction.sortKey),
value: value
});
}
async get<V>(key: string): Promise<V | null> {
this.checkStorageAvailable();
const sortKey = this._transaction.sortKey;
if (this._kvBatch.length > 0) {
const putBatches = this._kvBatch.filter((batchOp) => batchOp.type === 'put') as PutBatch<any>[];
const matchingPutBatch = putBatches.reverse().find((batchOp) => {
return batchOp.key.key === key && batchOp.key.sortKey === sortKey;
}) as PutBatch<V>;
if (matchingPutBatch !== undefined) {
return matchingPutBatch.value;
}
}
const result = await this._storage.getLessOrEqual(key, this._transaction.sortKey);
return result?.cachedValue || null;
}
async commit(): Promise<void> {
if (this._storage) {
await this._storage.batch(this._kvBatch);
this._kvBatch = [];
}
}
rollback(): void {
this._kvBatch = [];
}
ops(): BatchDBOp<any>[] {
return structuredClone(this._kvBatch);
}
open(): Promise<void> {
if (this._storage) {
return this._storage.open();
}
}
close(): Promise<void> {
if (this._storage) {
return this._storage.close();
}
}
private checkStorageAvailable() {
if (!this._storage) {
throw new Error('KV Storage not available');
}
}
}

View File

@@ -0,0 +1,71 @@
export async function handle(state, action) {
const input = action.input;
const caller = action.caller;
if (!state.kvOps) {
state.kvOps = {};
}
if (input.function === 'mint') {
console.log('mint', input.target, input.qty.toString());
await SmartWeave.kv.put(input.target, input.qty.toString());
// for debug or whatever
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
return {state};
}
if (input.function === 'transfer') {
const target = input.target;
const qty = input.qty;
if (!Number.isInteger(qty)) {
throw new ContractError('Invalid value for "qty". Must be an integer');
}
if (!target) {
throw new ContractError('No target specified');
}
if (qty <= 0 || caller === target) {
throw new ContractError('Invalid token transfer');
}
let callerBalance = await SmartWeave.kv.get(caller);
callerBalance = callerBalance ? parseInt(callerBalance) : 0;
if (callerBalance < qty) {
throw new ContractError(`Caller balance not high enough to send ${qty} token(s)!`);
}
// Lower the token balance of the caller
callerBalance -= qty;
await SmartWeave.kv.put(caller, callerBalance.toString());
let targetBalance = await SmartWeave.kv.get(target);
targetBalance = targetBalance ? parseInt(targetBalance) : 0;
targetBalance += qty;
await SmartWeave.kv.put(target, targetBalance.toString());
// for debug or whatever
//state.kvOps[SmartWeave.transaction.id] = SmartWeave.kv.ops();
return {state};
}
if (input.function === 'balance') {
const target = input.target;
const ticker = state.ticker;
if (typeof target !== 'string') {
throw new ContractError('Must specify target to get balance for');
}
console.log('balance', input.target);
const result = await SmartWeave.kv.get(target);
return {result: {target, ticker, balance: result ? parseInt(result) : 0}};
}
throw new ContractError(`No function supplied or function not recognised: "${input.function}"`);
}

View File

@@ -18,10 +18,10 @@ async function main() {
}); });
try { try {
const warp = WarpFactory const warp = WarpFactory
.forMainnet({...defaultCacheOptions, inMemory: true}); .forMainnet({...defaultCacheOptions, inMemory: true});
const jsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.js'), 'utf8'); const jsContractSrc = fs.readFileSync(path.join(__dirname, 'data/js/kv-storage.js'), 'utf8');
const initialState = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.json'), 'utf8'); const initialState = fs.readFileSync(path.join(__dirname, 'data/js/token-pst.json'), 'utf8');
// case 1 - full deploy, js contract // case 1 - full deploy, js contract
@@ -31,8 +31,7 @@ async function main() {
src: jsContractSrc, src: jsContractSrc,
evaluationManifest: { evaluationManifest: {
evaluationOptions: { evaluationOptions: {
unsafeClient: 'skip', useKVStorage: true
internalWrites: true
} }
} }
}); });
@@ -63,31 +62,31 @@ async function main() {
});*/ });*/
const contract = warp.contract<any>(contractTxId) const contract = warp.contract<any>(contractTxId)
.setEvaluationOptions({internalWrites: false, unsafeClient: 'throw', allowBigInt: true}) .setEvaluationOptions({})
.connect(wallet); .connect(wallet);
await Promise.all([ await Promise.all([
contract.writeInteraction<any>({ /*contract.writeInteraction<any>({
function: "transfer", function: "transfer",
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI", target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
qty: 100 qty: 100
}),*/
contract.writeInteraction<any>({
function: "mint",
target: 'follows:0xe0',
qty: 100
}), }),
contract.writeInteraction<any>({ /*contract.writeInteraction<any>({
function: "transfer", function: "transfer",
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI", target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
qty: 100 qty: 100
}), })*/
contract.writeInteraction<any>({
function: "transfer",
target: "M-mpNeJbg9h7mZ-uHaNsa5jwFFRAq0PsTkNWXJ-ojwI",
qty: 100
})
]); ]);
const {cachedValue} = await contract.readState(); //const {cachedValue} = await contract.readState();
logger.info("Result"); //logger.info("Result", await contract.getStorageValue('33F0QHcb22W7LwWR1iRC8Az1ntZG09XQ03YWuw2ABqA'));
console.dir(cachedValue.state); //console.dir(cachedValue.state);
} catch (e) { } catch (e) {
//logger.error(e) //logger.error(e)

View File

@@ -1,32 +1,77 @@
/* eslint-disable */ /* eslint-disable */
import {Level} from "level"; import {Level} from "level";
import { MemoryLevel } from 'memory-level'; import {DEFAULT_LEVEL_DB_LOCATION, sleep, timeout} from "../src";
import fs from "fs";
import {WasmSrc} from "../src";
import {Buffer} from "buffer";
// Create a database class SwGlobalMock {
db: Level;
async function test() {
//const db = new Level<string, any>('./leveldb', {valueEncoding: 'json'});
const db = new MemoryLevel<string, any>({ valueEncoding: 'json' });
const wasmSrc = fs.readFileSync('./tools/data/rust/rust-pst_bg.wasm');
const contractData = {
src: wasmSrc,
id: 'n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno',
srcId: 'foobar'
}
console.log(contractData);
await db.put("n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno", contractData);
const result = await db.get("n05LTiuWcAYjizXAu-ghegaWjL89anZ6VdvuHcU6dno");
console.log(result);
console.log(Buffer.from(result.src.data));
} }
test(); async function test() {
const {handle: handleFn2, swGlobal} = await prepareHandle();
// simulates the code of the JsHandlerAPI.handle
await doHandle(swGlobal, handleFn2);
await doHandle(swGlobal, handleFn2);
await doHandle(swGlobal, handleFn2);
await doHandle(swGlobal, handleFn2);
}
async function prepareHandle() {
await sleep(10);
// simulates contract handle function
const swGlobal = new SwGlobalMock();
const handle = new Function(`
const [swGlobal] = arguments;
async function handle(state, input) {
await sleep(1000);
console.log('from handle:', await swGlobal.db.get('foo'));
}
const sleep = (ms) => {
return new Promise((resolve) => setTimeout(resolve, ms));
}
return handle;
`)(swGlobal);
return {handle, swGlobal};
}
async function doHandle(swGlobal: SwGlobalMock, handleFn: Function) {
const {timeoutId, timeoutPromise} = timeout(10);
let now = new Date();
// the kv storage
const db = new Level(`${DEFAULT_LEVEL_DB_LOCATION}/kv/the_test_${now}`);
swGlobal.db = db;
try {
console.log('======== Connecting to LMDB KV');
await db.open();
} catch (err) {
console.error(err.code) // 'LEVEL_DATABASE_NOT_OPEN'
if (err.cause && err.cause.code === 'LEVEL_LOCKED') {
console.error('LEVEL_LOCKED');
}
throw err;
}
await db.put('foo', 'bar');
try {
// simulates calling single contract interaction
await Promise.race([timeoutPromise, handleFn()]);
// simulates 'commit'
console.log('======== Committing');
await swGlobal.db.batch([]);
} finally {
console.log('======== Disconnecting from LMDB KV');
await db.close()
clearTimeout(timeoutId);
}
}
test().finally(() => {console.log('done')});

View File

@@ -65,7 +65,7 @@ app.get('/:type/:key/:blockHeight', async function (req, res, next) {
const { type, key } = req.params; const { type, key } = req.params;
const blockHeight = parseInt(req.params.blockHeight); const blockHeight = parseInt(req.params.blockHeight);
const result = await caches[type].get(key, blockHeight); const result = await caches[type].get({ key: key, sortKey: blockHeight});
console.log('get', result); console.log('get', result);
res.send(result); res.send(result);

File diff suppressed because one or more lines are too long