Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add opcode json ref #781

Merged
merged 8 commits into from
Sep 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ from `algosdk` and sends it to the network.
- Added replace2 and replace3 opcode to `runtime`.
- Added sha3_256 opcode to `Runtime`
- Added ed25519verify_bare opcode to `Runtime`
- Added json_ref opcode to `Runtime`

#### @algo-builder/web
- Added support for logic signature to `executeTx` method of `Webmode` for AlgoSigner, MyAlgo Wallet and Wallet Connect.
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"dependencies": {
"@algo-builder/web": "workspace:*",
"@nodelib/fs.walk": "^1.2.8",
"@types/json-bigint": "^1.0.1",
"algosdk": "^1.19.0",
"chalk": "^4.1.2",
"debug": "^4.3.4",
Expand Down
18 changes: 18 additions & 0 deletions packages/runtime/src/errors/errors-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,24 @@ maximun uint128`,
title: "replace opcode error",
description: "Can not replace bytes due to invalid start or end index",
},
UNKNOWN_JSON_TYPE: {
number: 1062,
message: "JSON TYPE must be {JSONString, JSONUint64 or JSONObject}, got :%jsonType%, at line %line%",
title: "Unknown JSON type",
description: "Unknown JSON type",
},
INVALID_JSON_PARSING: {
number: 1063,
message: "Invalid JSON parsing at line %line%",
title: "Invalid JSON parsing",
description: "Invalid JSON parsing",
},
UNKNOWN_KEY_JSON: {
number: 1064,
message: "Unkown key %key% in JSON object at line %line%",
title: "Unknown key JSON",
description: "Unknown key JSON"
}
};

const runtimeGeneralErrors = {
Expand Down
112 changes: 112 additions & 0 deletions packages/runtime/src/interpreter/opcode-list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint sonarjs/no-identical-functions: 0 */
import { parsing, tx as webTx, types } from "@algo-builder/web";
import { stringToBytes } from "@algo-builder/web/build/lib/parsing";
import algosdk, {
decodeAddress,
decodeUint64,
Expand All @@ -16,6 +17,7 @@ import chalk from "chalk";
import { ec as EC } from "elliptic";
import { Hasher, Message, sha256 } from "js-sha256";
import { sha512_256 } from "js-sha512";
import JSONbig from "json-bigint";
import cloneDeep from "lodash.clonedeep";
import { Keccak, SHA3 } from "sha3";
import { buffer } from "stream/consumers";
Expand All @@ -32,6 +34,7 @@ import {
AssetParamMap,
GlobalFields,
ITxArrFields,
json_refTypes,
MathOp,
MAX_APP_PROGRAM_COST,
MAX_CONCAT_SIZE,
Expand All @@ -51,6 +54,7 @@ import { bigintSqrt } from "../lib/math";
import {
assertBase64,
assertBase64Url,
assertJSON,
assertLen,
assertNumber,
assertOnlyDigits,
Expand Down Expand Up @@ -5098,3 +5102,111 @@ export class Replace3 extends Replace {
return super.execute(stack);
}
}

/**
* Opcode: json_ref r
* Stack: ..., A: []byte, B: []byte → ..., any
* return key B's value from a valid utf-8 encoded json object A
*/
export class Json_ref extends Op {
readonly line: number;
readonly jsonType: string;
length = 1;

/**
* Asserts 1 argument is passed.
* @param args Expected arguments: [e], where e = {JSONString, JSONUint64 and JSONObject}.
* @param line line number in TEAL file
*/
constructor(args: string[], line: number) {
super();
this.line = line;
assertLen(args.length, 1, line);
const argument = args[0];
switch (argument) {
case json_refTypes.JSONString: {
this.jsonType = "byte";
break;
}
case json_refTypes.JSONUint64: {
this.jsonType = "int";
break;
}
case json_refTypes.JSONObject: {
this.jsonType = "object";
break;
}
default: {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.UNKNOWN_JSON_TYPE, {
jsonType: argument,
line: this.line,
});
}
}
}

computeCost(): number {
return 25 + 2 * Math.ceil(this.length / 7); // cost = 25 + ceil(bytes / 7)
}

execute(stack: TEALStack): number {
this.assertMinStackLen(stack, 2, this.line);
const key = this.assertBytes(stack.pop(), this.line);
const object = this.assertBytes(stack.pop(), this.line);
this.length = object.length;
const utf8decoder = new TextDecoder("utf-8");
const decodedObj = utf8decoder.decode(object);
const keyString = utf8decoder.decode(key);
assertJSON(decodedObj, this.line);
const nativeBigJSON = JSONbig({ useNativeBigInt: true });
const jsonObject = nativeBigJSON.parse(decodedObj);
if (jsonObject[keyString] === undefined) {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.UNKNOWN_KEY_JSON, {
key: keyString,
line: this.line,
});
}
switch (this.jsonType) {
case "byte": {
if (typeof jsonObject[keyString] === 'string') {
stack.push(stringToBytes(jsonObject[keyString]));
} else {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.INVALID_TYPE, {
expected: 'byte',
actual: typeof jsonObject[keyString],
line: this.line,
});
}
break;
}
case "int": {
if (typeof jsonObject[keyString] === 'number' ||
typeof jsonObject[keyString] === 'bigint') {
const result = BigInt(jsonObject[keyString]);
this.checkOverflow(result, this.line, MAX_UINT64);
stack.push(result);
} else {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.INVALID_TYPE, {
expected: 'Uint64',
actual: typeof jsonObject[keyString],
line: this.line,
});
}
break;
}
case "object": {
if (typeof jsonObject[keyString] === 'object') {
stack.push(stringToBytes(nativeBigJSON.stringify(jsonObject[keyString])));
} else {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.INVALID_TYPE, {
expected: 'object',
actual: typeof jsonObject[keyString],
line: this.line,
});
}
break;
}
}
return this.computeCost();
}
}
6 changes: 6 additions & 0 deletions packages/runtime/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,9 @@ export enum TransactionTypeEnum {
ASSET_FREEZE = "afrz",
APPLICATION_CALL = "appl",
}

export const json_refTypes = {
JSONString: "JSONString",
JSONUint64: "JSONUint64",
JSONObject: "JSONObject",
}
19 changes: 17 additions & 2 deletions packages/runtime/src/lib/parsing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { parsing } from "@algo-builder/web";
import * as base32 from "hi-base32";
import JSONbig from "json-bigint";

import { RUNTIME_ERRORS } from "../errors/errors-list";
import { RuntimeError } from "../errors/runtime-errors";
Expand Down Expand Up @@ -283,8 +284,7 @@ export function bigEndianBytesToBigInt(bytes: Uint8Array | Buffer): bigint {
export function strHexToBytes(str: string): Uint8Array {
return new Uint8Array(Buffer.from(str.slice(2), "hex"));
}

/**
/*
* Function taken from algosdk.utils
* ConcatArrays takes n number arrays and returns a joint Uint8Array
* @param arrs - An arbitrary number of n array-like number list arguments
Expand All @@ -302,3 +302,18 @@ export function concatArrays(...arrs: ArrayLike<number>[]) {

return c;
}

/**
* assert if given string is a valid JSON object
* @param jsonString
*/
export function assertJSON(jsonString: string, line: number): void {
const strictBigJSON = JSONbig({ strict: true });
try {
strictBigJSON.parse(jsonString);
} catch (e) {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.INVALID_JSON_PARSING, {
line: line
});
}
}
2 changes: 2 additions & 0 deletions packages/runtime/src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import {
ITxnField,
ITxnNext,
ITxnSubmit,
Json_ref,
Keccak256,
Label,
Len,
Expand Down Expand Up @@ -404,6 +405,7 @@ opCodeMap[7] = {
replace3: Replace3,
sha3_256: Sha3_256,
ed25519verify_bare: Ed25519verify_bare,
json_ref: Json_ref,
};

// list of opcodes with exactly one parameter.
Expand Down
64 changes: 64 additions & 0 deletions packages/runtime/test/src/interpreter/opcode-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ import {
Itob,
ITxn,
ITxnas,
Json_ref,
Keccak256,
Label,
Len,
Expand Down Expand Up @@ -7310,4 +7311,67 @@ describe("Teal Opcodes", function () {
assert.equal(1900, op.execute(stack));
});
});

describe("json_ref", () => {
const stack = new Stack<StackElem>();
const jsonByte = "{\"key0\": 0,\"key1\": \"algo\",\"key2\":{\"key3\": \"teal\", \"key4\": {\"key40\": 10}}, \"key5\": 18446744073709551615 }";

it("Should return correct JSONUint64", function () {
stack.push(parsing.stringToBytes("{\"maxUint64\": 18446744073709551615}"));
stack.push(parsing.stringToBytes("maxUint64"));
const op = new Json_ref(["JSONUint64"], 1);
op.execute(stack);

const top = stack.pop();
assert.deepEqual(18446744073709551615n, top);
});

it("Should throw when get wrong JSON type(expect byte but got uint64)", function () {
stack.push(parsing.stringToBytes("{\"maxUint64\": 18446744073709551615}"));
stack.push(parsing.stringToBytes("maxUint64"));
const op = new Json_ref(["JSONString"], 1);
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.INVALID_TYPE);
});

it("Should return correct JSONString", function () {
stack.push(parsing.stringToBytes(jsonByte));
stack.push(parsing.stringToBytes("key1"));
const op = new Json_ref(["JSONString"], 1);
op.execute(stack);

const top = stack.pop();
const expected = parsing.stringToBytes("algo");
assert.deepEqual(expected, top);
});

it("Should return correct JSONObject", function () {
stack.push(parsing.stringToBytes(jsonByte));
stack.push(parsing.stringToBytes("key2"));
const op1 = new Json_ref(["JSONObject"], 1);
op1.execute(stack);
stack.push(parsing.stringToBytes("key4"));
const op2 = new Json_ref(["JSONObject"], 1);
op2.execute(stack);

const top = stack.pop();
const expected = parsing.stringToBytes("{\"key40\":10}");
assert.deepEqual(top, expected);
});

it("Should throw error when parsing invalid JSON object(missing comma in JSON object)", function () {
const jsonByte = "{\"key0\": 0 \"key1\": 2}";
stack.push(parsing.stringToBytes(jsonByte));
stack.push(parsing.stringToBytes("key1"));
const op = new Json_ref(["JSONObject"], 1);
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.INVALID_JSON_PARSING);
});

it("Should throw error when parsing invalid JSON object(duplicate key is not allowed in JSON object)", function () {
const jsonByte = "{\"key0\": 0,\"key1\": \"algo\",\"key2\":{\"key3\": \"teal\", \"key4\": {\"key40\": 10, \"key40\": \"should fail!\"}}}";
stack.push(parsing.stringToBytes(jsonByte));
stack.push(parsing.stringToBytes("key1"));
const op = new Json_ref(["JSONObject"], 1);
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.INVALID_JSON_PARSING);
});
});
});
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ __metadata:
"@types/chai": ^4.3.0
"@types/debug": ^4.1.7
"@types/elliptic": ^6.4.14
"@types/json-bigint": ^1.0.1
"@types/lodash.clonedeep": ^4.5.6
"@types/mocha": ^9.1.0
"@types/node": ^17.0.21
Expand Down Expand Up @@ -725,6 +726,13 @@ __metadata:
languageName: node
linkType: hard

"@types/json-bigint@npm:^1.0.1":
version: 1.0.1
resolution: "@types/json-bigint@npm:1.0.1"
checksum: b39e55a811f554bd25f1d991bc4fc70655216dff466f21f97160097573a4bc7b478c1907aa5194c79022c115f509f8e4712083c51f57665e7a2de7412ff7801f
languageName: node
linkType: hard

"@types/json-schema@npm:^7.0.9":
version: 7.0.9
resolution: "@types/json-schema@npm:7.0.9"
Expand Down