feat: wasm contracts intergration

This commit is contained in:
ppedziwiatr
2022-02-19 00:28:21 +01:00
committed by Piotr Pędziwiatr
parent 47fb5b1c59
commit 4e38dec7f9
13 changed files with 414 additions and 48 deletions

View File

@@ -51,6 +51,7 @@
},
"homepage": "https://github.com/redstone-finance/redstone-smartweave#readme",
"dependencies": {
"@assemblyscript/loader": "^0.19.23",
"@weavery/clarity": "^0.1.5",
"arweave": "^1.10.16",
"arweave-multihost": "^0.1.0",

View File

@@ -36,6 +36,7 @@ describe('Testing the SmartWeave client for WASM contract', () => {
});
LoggerFactory.INST.logLevel('error');
LoggerFactory.INST.logLevel('debug', 'WasmContractHandlerApi');
smartweave = SmartWeaveNodeFactory.memCached(arweave);
@@ -62,35 +63,29 @@ describe('Testing the SmartWeave client for WASM contract', () => {
await arlocal.stop();
});
it('should properly deploy contract with initial state', async () => {
it('should properly deploy contract', async () => {
const contractTx = await arweave.transactions.get(contractTxId);
expect(contractTx).not.toBeNull();
});
/*
it('should properly add new interaction', async () => {
await contract.writeInteraction({ function: 'add' });
await mineBlock(arweave);
expect((await contract.readState()).state.counter).toEqual(556);
it('should properly read initial state', async () => {
expect((await contract.readState()).state.counter).toEqual(0);
});
it('should properly add another interactions', async () => {
await contract.writeInteraction({ function: 'add' });
it('should properly read state after adding interactions', async () => {
await contract.writeInteraction({ function: 'increment' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'increment' });
await mineBlock(arweave);
await contract.writeInteraction({ function: 'add' });
await contract.writeInteraction({ function: 'increment' });
await mineBlock(arweave);
expect((await contract.readState()).state.counter).toEqual(559);
expect((await contract.readState()).state.counter).toEqual(3);
});
it('should properly view contract state', async () => {
const interactionResult = await contract.viewState<unknown, number>({ function: 'value' });
expect(interactionResult.result).toEqual(559);
});*/
const interactionResult = await contract.viewState<unknown, any>({ function: 'fullName' });
expect(interactionResult.result.fullName).toEqual("first_ppe last_ppe");
});
});

View File

@@ -1,11 +1,14 @@
/**
* This type contains all data and meta-data of the given contact.
*/
import {ContractType} from "./modules/CreateContract";
export type ContractDefinition<State> = {
txId: string;
srcTxId: string;
src: string;
src: ArrayBuffer;
initState: State;
minFee: string;
owner: string;
contractType: ContractType;
};

View File

@@ -2,6 +2,7 @@ import {
ArweaveWrapper,
Benchmark,
ContractDefinition,
ContractType,
DefinitionLoader,
getTag,
LoggerFactory,
@@ -50,6 +51,10 @@ export class ContractDefinitionLoader implements DefinitionLoader {
this.logger.debug('Tags decoding', benchmark.elapsed());
benchmark.reset();
const contractSrcTx = await this.arweaveWrapper.tx(contractSrcTxId);
const contractType: ContractType =
getTag(contractSrcTx, SmartWeaveTags.CONTENT_TYPE) == 'application/javascript' ? 'js' : 'wasm';
const src = await this.arweaveWrapper.txData(contractSrcTxId);
this.logger.debug('Contract src tx load', benchmark.elapsed());
benchmark.reset();
@@ -63,18 +68,19 @@ export class ContractDefinitionLoader implements DefinitionLoader {
src,
initState,
minFee,
owner
owner,
contractType
};
}
private async evalInitialState(contractTx: Transaction) {
private async evalInitialState(contractTx: Transaction): Promise<string> {
if (getTag(contractTx, SmartWeaveTags.INIT_STATE)) {
return getTag(contractTx, SmartWeaveTags.INIT_STATE);
} else if (getTag(contractTx, SmartWeaveTags.INIT_STATE_TX)) {
const stateTX = getTag(contractTx, SmartWeaveTags.INIT_STATE_TX);
return this.arweaveWrapper.txData(stateTX);
return this.arweaveWrapper.txDataString(stateTX);
} else {
return this.arweaveWrapper.txData(contractTx.id);
return this.arweaveWrapper.txDataString(contractTx.id);
}
}
}

View File

@@ -219,4 +219,8 @@ export class ContractHandlerApi<State> implements HandlerApi<State> {
return result?.cachedValue.state;
};
}
initState(state: State): void {
// nth to do in this impl...
}
}

View File

@@ -61,6 +61,8 @@ export abstract class DefaultStateEvaluator implements StateEvaluator {
let currentState = baseState.state;
const validity = baseState.validity;
executionContext.handler.initState(currentState);
this.logger.info(
`Evaluating state for ${contractDefinition.txId} [${missingInteractions.length} non-cached of ${sortedInteractions.length} all]`
);

View File

@@ -10,6 +10,134 @@ import {
SmartWeaveGlobal
} from '@smartweave';
import { ContractHandlerApi } from './ContractHandlerApi';
import loader from '@assemblyscript/loader/umd';
import { imports } from './wasmImports';
import { WasmContractHandlerApi } from './WasmContractHandlerApi';
/**
* A factory that produces handlers that are compatible with the "current" style of
* writing SW contracts (ie. using "handle" function).
*/
export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknown>> {
private readonly logger = LoggerFactory.INST.create('HandlerExecutorFactory');
constructor(private readonly arweave: Arweave) {}
async create<State>(contractDefinition: ContractDefinition<State>): Promise<HandlerApi<State>> {
const swGlobal = new SmartWeaveGlobal(this.arweave, {
id: contractDefinition.txId,
owner: contractDefinition.owner
});
if (contractDefinition.contractType == 'js') {
const normalizedSource = normalizeContractSource(this.arweave.utils.bufferToString(contractDefinition.src));
const contractFunction = new Function(normalizedSource);
return new ContractHandlerApi(swGlobal, contractFunction, contractDefinition);
} else {
let wasmExports;
const wasmModule = loader.instantiateSync(contractDefinition.src, {
metering: {
usegas: (gas) => {
if (gas < 0) {
return;
}
swGlobal.gasUsed += gas;
if (swGlobal.gasUsed > swGlobal.gasLimit) {
throw new Error(
`[RE:OOG] Out of gas! Limit: ${formatGas(swGlobal.gasUsed)}, used: ${formatGas(swGlobal.gasLimit)}`
);
}
}
},
console: {
'console.log': function (msgPtr) {
console.log(`Contract: ${wasmExports.__getString(msgPtr)}`);
},
'console.logO': function (msgPtr, objPtr) {
console.log(`Contract: ${wasmExports.__getString(msgPtr)}`, JSON.parse(wasmExports.__getString(objPtr)));
}
},
block: {
'Block.height': function () {
return 875290;
},
'Block.indep_hash': function () {
return wasmExports.__newString('iIMsQJ1819NtkEUEMBRl6-7I6xkeDipn1tK4w_cDFczRuD91oAZx5qlgSDcqq1J1');
},
'Block.timestamp': function () {
return 123123123;
}
},
transaction: {
'Transaction.id': function () {
return wasmExports.__newString('Transaction.id');
},
'Transaction.owner': function () {
return wasmExports.__newString('Transaction.owner');
},
'Transaction.target': function () {
return wasmExports.__newString('Transaction.target');
}
},
contract: {
'Contract.id': function () {
return wasmExports.__newString('Contract.id');
},
'Contract.owner': function () {
return wasmExports.__newString('Contract.owner');
}
},
msg: {
'msg.sender': function () {
return wasmExports.__newString('msg.sender');
}
},
api: {
_readContractState: (fnIndex, contractTxIdPtr) => {
const contractTxId = wasmExports.__getString(contractTxIdPtr);
const callbackFn = getFn(fnIndex);
console.log('Simulating read state of', contractTxId);
return setTimeout(() => {
console.log('calling callback');
callbackFn(
wasmExports.__newString(
JSON.stringify({
contractTxId
})
)
);
}, 1000);
},
clearTimeout
},
env: {
abort(messagePtr, fileNamePtr, line, column) {
console.error('--------------------- Error message from AssemblyScript ----------------------');
console.error(' ' + wasmExports.__getString(messagePtr));
console.error(' In file "' + wasmExports.__getString(fileNamePtr) + '"');
console.error(` on line ${line}, column ${column}.`);
console.error('------------------------------------------------------------------------------\n');
}
}
});
function getFn(idx) {
return wasmExports.table.get(idx);
}
function formatGas(gas) {
return gas * 1e-4;
}
wasmExports = wasmModule.exports;
return new WasmContractHandlerApi(swGlobal, contractDefinition, wasmExports);
}
}
}
export interface InteractionData<Input> {
interaction?: ContractInteraction<Input>;
@@ -26,28 +154,8 @@ export interface HandlerApi<State> {
currentResult: EvalStateResult<State>,
interactionData: InteractionData<Input>
): Promise<InteractionResult<State, Result>>;
}
/**
* A factory that produces handlers that are compatible with the "current" style of
* writing SW contracts (ie. using "handle" function).
*/
export class HandlerExecutorFactory implements ExecutorFactory<HandlerApi<unknown>> {
private readonly logger = LoggerFactory.INST.create('HandlerExecutorFactory');
constructor(private readonly arweave: Arweave) {}
async create<State>(contractDefinition: ContractDefinition<State>): Promise<HandlerApi<State>> {
const normalizedSource = normalizeContractSource(contractDefinition.src);
const swGlobal = new SmartWeaveGlobal(this.arweave, {
id: contractDefinition.txId,
owner: contractDefinition.owner
});
const contractFunction = new Function(normalizedSource);
return new ContractHandlerApi(swGlobal, contractFunction, contractDefinition);
}
initState(state: State): void;
}
export type HandlerFunction<State, Input, Result> = (

View File

@@ -0,0 +1,98 @@
/* eslint-disable */
import {
ContractDefinition,
EvalStateResult,
ExecutionContext,
HandlerApi,
InteractionData,
InteractionResult,
LoggerFactory,
RedStoneLogger,
SmartWeaveGlobal
} from '@smartweave';
import stringify from "safe-stable-stringify";
export class WasmContractHandlerApi<State> implements HandlerApi<State> {
private readonly contractLogger: RedStoneLogger;
private readonly logger = LoggerFactory.INST.create('WasmContractHandlerApi');
constructor(
private readonly swGlobal: SmartWeaveGlobal,
private readonly contractDefinition: ContractDefinition<State>,
private readonly wasmExports: any
) {
this.contractLogger = LoggerFactory.INST.create(swGlobal.contract.id);
}
initState(state: State): void {
const statePtr = this.wasmExports.__newString(stringify(state));
this.wasmExports.initState(statePtr);
}
async handle<Input, Result>(
executionContext: ExecutionContext<State>,
currentResult: EvalStateResult<State>,
interactionData: InteractionData<Input>
): Promise<InteractionResult<State, Result>> {
const contractLogger = LoggerFactory.INST.create('Contract');
try {
const {interaction, interactionTx, currentTx} = interactionData;
this.swGlobal._activeTx = interactionTx;
const handlerResult = this.doHandle(interaction);
return {
type: 'ok',
result: handlerResult,
state: this.doGetCurrentState()
};
} catch (e) {
// note: as exceptions handling in WASM is currently somewhat non-existent
// https://www.assemblyscript.org/status.html#exceptions
// and since we have to somehow differentiate different types of exceptions
// - each exception message has to have a proper prefix added.
// exceptions with prefix [RE:] ("Runtime Exceptions") should break the execution immediately
// - eg: [RE:OOG] - [RuntimeException: OutOfGas]
// exception with prefix [CE:] ("Contract Exceptions") should be logged, but should not break
// the state evaluation - as they are considered as contracts' business exception (eg. validation errors)
// - eg: [CE:WTF] - [ContractException: WhatTheFunction] ;-)
if (e.message.startsWith('[RE:')) {
return {
type: 'exception',
errorMessage: e.message,
state: currentResult.state,
result: null
};
} else {
return {
type: 'error',
errorMessage: e.message,
state: currentResult.state,
result: null
};
}
}
}
private doHandle(action: any): any {
this.logger.info("Action", action.input);
const actionPtr = this.wasmExports.__newString(stringify(action.input));
const resultPtr = this.wasmExports.handle(actionPtr);
const result = this.wasmExports.__getString(resultPtr);
this.logger.info("Result", result);
this.logger.info("State", this.doGetCurrentState());
return JSON.parse(result);
}
private doGetCurrentState(): State {
const currentStatePtr = this.wasmExports.currentState();
return JSON.parse(this.wasmExports.__getString(currentStatePtr));
}
}

View File

@@ -0,0 +1,93 @@
import {SmartWeaveGlobal} from "@smartweave";
export function imports(swGlobal: SmartWeaveGlobal, wasmExports: any): any {
return {
metering: {
usegas: (gas) => {
if (gas < 0) {
return;
}
swGlobal.gasUsed += gas;
if (swGlobal.gasUsed > swGlobal.gasLimit) {
throw new Error(`[RE:OOG] Out of gas! Limit: ${formatGas(swGlobal.gasUsed)}, used: ${formatGas(swGlobal.gasLimit)}`);
}
}
},
console: {
"console.log": function (msgPtr) {
console.log(`Contract: ${wasmExports.__getString(msgPtr)}`);
},
"console.logO": function (msgPtr, objPtr) {
console.log(`Contract: ${wasmExports.__getString(msgPtr)}`, JSON.parse(wasmExports.__getString(objPtr)));
},
},
block: {
"Block.height": function () {
return 875290;
},
"Block.indep_hash": function () {
return wasmExports.__newString("iIMsQJ1819NtkEUEMBRl6-7I6xkeDipn1tK4w_cDFczRuD91oAZx5qlgSDcqq1J1");
},
"Block.timestamp": function () {
return 123123123;
},
},
transaction: {
"Transaction.id": function () {
return wasmExports.__newString("Transaction.id");
},
"Transaction.owner": function () {
return wasmExports.__newString("Transaction.owner");
},
"Transaction.target": function () {
return wasmExports.__newString("Transaction.target");
},
},
contract: {
"Contract.id": function () {
return wasmExports.__newString("Contract.id");
},
"Contract.owner": function () {
return wasmExports.__newString("Contract.owner");
},
},
msg: {
"msg.sender": function () {
return wasmExports.__newString("msg.sender");
},
},
api: {
_readContractState: (fnIndex, contractTxIdPtr) => {
const contractTxId = wasmExports.__getString(contractTxIdPtr);
const callbackFn = getFn(fnIndex);
console.log("Simulating read state of", contractTxId);
return setTimeout(() => {
console.log('calling callback');
callbackFn(wasmExports.__newString(JSON.stringify({
contractTxId
})));
}, 1000);
},
clearTimeout,
},
env: {
abort(messagePtr, fileNamePtr, line, column) {
console.error("--------------------- Error message from AssemblyScript ----------------------");
console.error(" " + wasmExports.__getString(messagePtr));
console.error(
' In file "' + wasmExports.__getString(fileNamePtr) + '"'
);
console.error(` on line ${line}, column ${column}.`);
console.error("------------------------------------------------------------------------------\n");
},
}
}
function getFn(idx) {
return wasmExports.table.get(idx);
}
}
function formatGas(gas) {
return gas * 1e-4;
}

View File

@@ -28,6 +28,8 @@ import { GQLNodeInterface, GQLTagInterface } from './gqlResult';
*
*/
export class SmartWeaveGlobal {
gasUsed: number;
gasLimit: number;
transaction: Transaction;
block: Block;
arweave: Pick<Arweave, 'ar' | 'wallets' | 'utils' | 'crypto'>;
@@ -46,7 +48,9 @@ export class SmartWeaveGlobal {
_activeTx?: GQLNodeInterface;
constructor(arweave: Arweave, contract: { id: string; owner: string }) {
constructor(arweave: Arweave, contract: { id: string; owner: string }, gasLimit = Number.MAX_SAFE_INTEGER) {
this.gasUsed = 0;
this.gasLimit = gasLimit;
this.unsafeClient = arweave;
this.arweave = {
ar: arweave.ar,

View File

@@ -16,9 +16,11 @@ export class DebuggableExecutorFactory<Api> implements ExecutorFactory<Api> {
async create<State>(contractDefinition: ContractDefinition<State>): Promise<Api> {
if (Object.prototype.hasOwnProperty.call(this.sourceCode, contractDefinition.txId)) {
const enc = new TextEncoder(); // always utf-8
contractDefinition = {
...contractDefinition,
src: this.sourceCode[contractDefinition.txId]
src: enc.encode(this.sourceCode[contractDefinition.txId])
};
}

View File

@@ -88,12 +88,17 @@ export class ArweaveWrapper {
});
}
async txData(id: string): Promise<string> {
async txData(id: string): Promise<ArrayBuffer> {
const response = await fetch(`${this.baseUrl}/${id}`);
if (!response.ok) {
throw new Error(`Unable to load tx data ${id}`);
}
const buffer = await response.arrayBuffer();
return buffer;
}
async txDataString(id: string): Promise<string> {
const buffer = await this.txData(id);
return Arweave.utils.bufferToString(buffer);
}
}

View File

@@ -51,6 +51,11 @@
http-errors "^1.7.3"
object-path "^0.11.4"
"@assemblyscript/loader@^0.19.23":
version "0.19.23"
resolved "https://registry.yarnpkg.com/@assemblyscript/loader/-/loader-0.19.23.tgz#7fccae28d0a2692869f1d1219d36093bc24d5e72"
integrity sha512-ulkCYfFbYj01ie1MDOyxv2F6SpRN1TOj7fQxbP07D6HmeR+gr2JLSmINKjga2emB+b1L2KGrFKBTc+e00p54nw==
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz"
@@ -1769,7 +1774,7 @@ bluebird@^2.6.2, bluebird@^2.8.1:
resolved "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz"
integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=
bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.11.9:
bn.js@^4.0.0, bn.js@^4.11.6, bn.js@^4.11.8, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
@@ -1862,6 +1867,20 @@ buffer-from@^1.0.0:
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buffer-pipe@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/buffer-pipe/-/buffer-pipe-0.0.0.tgz#186ec257d696e8e74c3051160a0e9e9a9811a387"
integrity sha512-PvKbsvQOH4dcUyUEvQQSs3CIkkuPcOHt3gKnXwf4HsPKFDxSN7bkmICVIWgOmW/jx/fAEGGn4mIayIJPLs7G8g==
dependencies:
safe-buffer "^5.1.1"
buffer-pipe@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/buffer-pipe/-/buffer-pipe-0.0.2.tgz#960678ab517ca926dc1bfe231f402cbe2fe74442"
integrity sha512-YlqzbWVqMv+xEeRyg0OXAJym3zAFTAIuku9l7okwxOXNDxbmSlL5o3QaF5k6IQ2iHO9o1OCo6tT4UkrQkI5VbQ==
dependencies:
safe-buffer "^5.1.1"
buffer-writer@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz"
@@ -4693,6 +4712,14 @@ koa@^2.13.1:
type-is "^1.6.16"
vary "^1.1.2"
leb128@0.0.4, leb128@^0.0.4:
version "0.0.4"
resolved "https://registry.yarnpkg.com/leb128/-/leb128-0.0.4.tgz#f96d698cf3ba5b677423abfe50b7e9b2df1463ff"
integrity sha512-2zejk0fCIgY8RVcc/KzvyfpDio5Oo8HgPZmkrOmdwmbk0KpKpgD+JKwikxKk8cZYkANIhwHK50SNukkCm3XkCQ==
dependencies:
bn.js "^4.11.6"
buffer-pipe "0.0.0"
leven@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz"
@@ -6760,6 +6787,24 @@ walker@^1.0.7:
dependencies:
makeerror "1.0.12"
wasm-json-toolkit@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/wasm-json-toolkit/-/wasm-json-toolkit-0.2.3.tgz#51207da907a343152b02d0e97ee30c4a6e9f2308"
integrity sha512-W0pESOST9hHFEmHq9kzMxAEhcPYuASdYCDw4FavKSyQKh3uOmH2slRXR/MhTKJY+gp1AauUDNd9DeE0cS4bV4A==
dependencies:
bn.js "^4.11.8"
buffer-pipe "0.0.2"
leb128 "0.0.4"
safe-buffer "^5.1.1"
wasm-metering@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/wasm-metering/-/wasm-metering-0.2.1.tgz#08220896e5e74b4ee3286636fd31d84e40544083"
integrity sha512-YDlTPY4jspknNyDaVBQhLTuTYBh+39qI0P9F0grmR88NR4oh7qfgpTwZ2ly4oX2hHCj9KlIwhy2Yyez+3/wY2Q==
dependencies:
leb128 "^0.0.4"
wasm-json-toolkit "0.2.3"
wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz"