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

base64_decode opcode from TEALv7 #653

Merged
merged 10 commits into from
Apr 29, 2022
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Added:

- Added support for saving smart contract template params in ASCCache.

- Teal v7 support:
- opcode `base64decode` ([##653](https://github.com/scale-it/algo-builder/pull/653))

- `algob test` now runs tests in `test` directory and all its subdirectories. Before only the files inside `test directory where run `.

Expand All @@ -60,6 +62,7 @@ Added:
console.log("txn1 information: ", receipts[2]);
```


### Template improvements

- We updated the examples/DAO design. We removed treasury Smart Signature to simplify deposit management. Now a DAO app is managing voting, deposits and treasury.
Expand Down
15 changes: 14 additions & 1 deletion packages/runtime/src/errors/errors-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ by an index that does not exist.`,
},
PRAGMA_VERSION_ERROR: {
number: 1017,
message: "Pragma version Error - Expected version: %expected%, got: %got%, Line: %line%",
message:
"Pragma version Error - Expected version up to: %expected%, got: %got%, Line: %line%",
title: PARSE_ERROR,
description: ``,
},
Expand Down Expand Up @@ -398,6 +399,18 @@ maximun uint128`,
title: "itxn_next without itxn_begin",
description: `itxn_next without itxn_begin`,
},
UNKNOWN_ENCODING: {
number: 1058,
message: "Encoding e must be {URLEncoding, StdEncoding}, got :%encoding%, at line %line%",
title: "Unknown encoding",
description: "Unknown encoding",
},
INVALID_BASE64URL: {
number: 1059,
message: "Invalid Base64Url Error - value %val% is not base64Url, Line: %line%",
title: PARSE_ERROR,
description: `value %exp% is not base64Url`,
},
};

const runtimeGeneralErrors = {
Expand Down
9 changes: 7 additions & 2 deletions packages/runtime/src/interpreter/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,7 +536,8 @@ export class Interpreter {

while (this.instructionIndex < this.instructions.length) {
const instruction = this.instructions[this.instructionIndex];
instruction.execute(this.stack);
//TODO this should return cost
const costFromExecute = instruction.execute(this.stack);

if (
this.runtime.ctx.isInnerTx &&
Expand All @@ -550,7 +551,11 @@ export class Interpreter {

// for teal version >= 4, cost is calculated dynamically at the time of execution
// for teal version < 4, cost is handled statically during parsing
this.cost += this.lineToCost[instruction.line];
if (costFromExecute === undefined) {
this.cost += this.lineToCost[instruction.line];
} else {
this.cost = costFromExecute;
}
if (this.tealVersion < 4) {
txReceipt.gas = this.gas;
}
Expand Down
58 changes: 57 additions & 1 deletion packages/runtime/src/interpreter/opcode-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ import {
import { addInnerTransaction, calculateInnerTxCredit, setInnerTxField } from "../lib/itxn";
import { bigintSqrt } from "../lib/math";
import {
assertBase64,
assertBase64Url,
assertLen,
assertNumber,
assertOnlyDigits,
Expand All @@ -61,6 +63,7 @@ import {
txnSpecByField,
} from "../lib/txn";
import {
Base64Encoding,
DecodingMode,
EncodingType,
EncTx,
Expand Down Expand Up @@ -98,7 +101,7 @@ export class Pragma extends Op {
interpreter.tealVersion = this.version;
} else {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.PRAGMA_VERSION_ERROR, {
expected: "till #4",
expected: MaxTEALVersion,
got: args.join(" "),
line: line,
});
Expand Down Expand Up @@ -4754,3 +4757,56 @@ export class Gitxnas extends Gtxnas {
super.execute(stack);
}
}

/**
* Takes the last value from stack and if base64encoded, decodes it acording to the
* encoding e and pushes it back to the stack, otherwise throws an error
*/
export class Base64Decode extends Op {
readonly line: number;
readonly encoding: BufferEncoding;

/**
* Asserts 1 argument is passed.
* @param args Expected arguments: [e], where e = {URLEncoding, StdEncoding}.
* @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 "URLEncoding": {
this.encoding = "base64url";
break;
}
case "StdEncoding": {
this.encoding = "base64";
break;
}
default: {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.UNKNOWN_ENCODING, {
encoding: argument,
line: this.line,
});
}
}
}

execute(stack: TEALStack): void {
this.assertMinStackLen(stack, 1, this.line);
const last = this.assertBytes(stack.pop(), this.line);
const enc = new TextDecoder("utf-8");
const decoded = enc.decode(last);
switch (this.encoding) {
case "base64url":
assertBase64Url(convertToString(last), this.line);
break;
case "base64":
assertBase64(convertToString(last), this.line);
break;
}
stack.push(new Uint8Array(Buffer.from(decoded.toString(), this.encoding)));
}
}
36 changes: 35 additions & 1 deletion packages/runtime/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const MAX_CONCAT_SIZE = 4096;
export const ALGORAND_MIN_TX_FEE = 1000;
// https://github.com/algorand/go-algorand/blob/master/config/consensus.go#L659
export const ALGORAND_ACCOUNT_MIN_BALANCE = 0.1e6; // 0.1 ALGO
export const MaxTEALVersion = 6;
export const MaxTEALVersion = 7;
export const MinVersionSupportC2CCall = 6;

// values taken from: https://developer.algorand.org/docs/features/asc1/stateful/#minimum-balance-requirement-for-a-smart-contract
Expand Down Expand Up @@ -152,6 +152,10 @@ TxnFields[6] = {
...TxnFields[5],
};

TxnFields[7] = {
...TxnFields[6],
};

export const ITxnFields: { [key: number]: { [key: string]: keyOfEncTx | null } } = {
1: {},
2: {},
Expand All @@ -169,6 +173,10 @@ ITxnFields[6] = {
...ITxnFields[5],
};

ITxnFields[7] = {
...ITxnFields[6],
};

// transaction fields of type array
export const TxArrFields: { [key: number]: Set<string> } = {
1: new Set(),
Expand All @@ -178,6 +186,7 @@ TxArrFields[3] = new Set([...TxArrFields[2], "Assets", "Applications"]);
TxArrFields[4] = cloneDeep(TxArrFields[3]);
TxArrFields[5] = cloneDeep(TxArrFields[4]);
TxArrFields[6] = cloneDeep(TxArrFields[5]);
TxArrFields[7] = cloneDeep(TxArrFields[6]);

// itxn fields of type array
export const ITxArrFields: { [key: number]: Set<string> } = {
Expand All @@ -189,6 +198,7 @@ export const ITxArrFields: { [key: number]: Set<string> } = {
};

ITxArrFields[6] = cloneDeep(ITxArrFields[5]);
ITxArrFields[7] = cloneDeep(ITxArrFields[6]);

export const TxFieldDefaults: { [key: string]: any } = {
Sender: ZERO_ADDRESS,
Expand Down Expand Up @@ -275,6 +285,7 @@ AssetParamMap[5] = {
};

AssetParamMap[6] = { ...AssetParamMap[5] };
AssetParamMap[7] = { ...AssetParamMap[6] };

// app param use for app_params_get opcode
export const AppParamDefined: { [key: number]: Set<string> } = {
Expand All @@ -296,6 +307,7 @@ export const AppParamDefined: { [key: number]: Set<string> } = {
};

AppParamDefined[6] = cloneDeep(AppParamDefined[5]);
AppParamDefined[7] = cloneDeep(AppParamDefined[6]);

// param use for query acct_params_get opcode

Expand All @@ -322,6 +334,19 @@ export const reOct = /^0[0-8]+$/;
*/
export const reBase64 = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;

/** is Base64Url regex
* ^ # Start of input
* ([0-9a-zA-Z_-]{4})* # Groups of 4 valid characters decode
* # to 24 bits of data for each group
* ( # Either ending with:
* ([0-9a-zA-Z_-]{2}==) # two valid characters followed by ==
* | # , or
* ([0-9a-zA-Z_-]{3}=) # three valid characters followed by =
* )? # , or nothing
* $ # End of input
*/
export const reBase64Url = /^([0-9a-zA-Z_-]{4})*(([0-9a-zA-Z_-]{2}==)|([0-9a-zA-Z_-]{3}=))?$/;

// A-Z and 2-7 repeated, with optional `=` at the end
export const reBase32 = /^[A-Z2-7]+=*$/;

Expand Down Expand Up @@ -376,6 +401,10 @@ GlobalFields[6] = {
CallerApplicationAddress: null,
};

GlobalFields[7] = {
...GlobalFields[6],
};

// creating map for opcodes whose cost is other than 1
export const OpGasCost: { [key: number]: { [key: string]: number } } = {
// version => opcode => cost
Expand Down Expand Up @@ -432,6 +461,11 @@ OpGasCost[6] = {
...OpGasCost[5],
bsqrt: 40,
};
OpGasCost[7] = {
...OpGasCost[6],
//TODO: calculate the cost for the base64_decode opcode
// https://github.com/algorand/go-algorand/blob/master/data/transactions/logic/TEAL_opcodes.md#base64_decode-e
};

export const enum MathOp {
// arithmetic
Expand Down
13 changes: 12 additions & 1 deletion packages/runtime/src/lib/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as base32 from "hi-base32";
import { RUNTIME_ERRORS } from "../errors/errors-list";
import { RuntimeError } from "../errors/runtime-errors";
import { EncodingType } from "../types";
import { reBase32, reBase64, reDec, reDigit, reHex, reOct } from "./constants";
import { reBase32, reBase64, reBase64Url, reDec, reDigit, reHex, reOct } from "./constants";

/**
* assert if string contains digits only
Expand Down Expand Up @@ -68,6 +68,17 @@ export function assertBase64(str: string, line: number): void {
}
}

/**
* Checks if string is base64Url
* @param str : string that needs to be checked
* @param line : line number in TEAL file
*/
export function assertBase64Url(str: string, line: number): void {
if (!reBase64Url.test(str)) {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.INVALID_BASE64URL, { val: str, line: line });
}
}

/**
* Checks if string is base32
* @param str : string that needs to be checked
Expand Down
9 changes: 9 additions & 0 deletions packages/runtime/src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
Args,
Assert,
Balance,
Base64Decode,
BitLen,
BitwiseAnd,
BitwiseNot,
Expand Down Expand Up @@ -388,6 +389,14 @@ opCodeMap[6] = {
itxnas: ITxnas,
};

/**
* TEALv7
*/
opCodeMap[7] = {
...opCodeMap[6],
base64_decode: Base64Decode,
};

// list of opcodes with exactly one parameter.
const interpreterReqList = new Set([
"#pragma",
Expand Down
5 changes: 5 additions & 0 deletions packages/runtime/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,8 @@ export interface SCParams {
export interface ReplaceParams {
[key: string]: string;
}

export enum Base64Encoding {
URL = 0,
STD = 1,
}
2 changes: 2 additions & 0 deletions packages/runtime/test/fixtures/teal-files/assets/teal-v7.teal
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#pragma version 7
base64_decode URLEncoding
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#pragma version 7
#pragma version 8
int 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#pragma version 7
int 1
56 changes: 56 additions & 0 deletions packages/runtime/test/src/interpreter/opcode-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
Args,
Assert,
Balance,
Base64Decode,
BitLen,
BitwiseAnd,
BitwiseNot,
Expand Down Expand Up @@ -6616,4 +6617,59 @@ describe("Teal Opcodes", function () {
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.ASSERT_STACK_LENGTH);
});
});

describe("Tealv7: base64Decode opcode", function () {
let stack: Stack<StackElem>;
const encoded64BaseStd = "YWJjMTIzIT8kKiYoKSctPUB+";
const encoded64BaseUrl = "YWJjMTIzIT8kKiYoKSctPUB-";
const decoded64Base = "abc123!?$*&()'-=@~";
const toPushStd = Buffer.from(encoded64BaseStd, "utf-8");
const toPushUrl = Buffer.from(encoded64BaseUrl, "utf-8");
const expectedBytes = new Uint8Array(Buffer.from(decoded64Base, "utf-8"));

this.beforeEach(() => {
stack = new Stack<StackElem>();
});

it("Should decode base64 encoded data and push it to stack", () => {
stack.push(toPushUrl);
const opUrl = new Base64Decode(["URLEncoding"], 0);
opUrl.execute(stack);
assert.deepEqual(expectedBytes, stack.pop());
stack.push(toPushStd);
const opStd = new Base64Decode(["StdEncoding"], 0);
opStd.execute(stack);
assert.deepEqual(expectedBytes, stack.pop());
});

it("Should throw an error when last stack element is not base64 encoded", () => {
stack.push(new Uint8Array(Buffer.from(encoded64BaseUrl, "utf-8")));
const op = new Base64Decode(["StdEncoding"], 0);
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.INVALID_BASE64);
});

it("Should throw an error when last stack element is not base64Url encoded", () => {
stack.push(new Uint8Array(Buffer.from(encoded64BaseStd, "utf-8")));
const op = new Base64Decode(["URLEncoding"], 0);
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.INVALID_BASE64URL);
});

it("Should throw an error when the stack is empty", () => {
const op = new Base64Decode(["StdEncoding"], 0);
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.ASSERT_STACK_LENGTH);
});

it("Should throw an error when argument not in bound", () => {
stack.push(toPushStd);
expectRuntimeError(
() => new Base64Decode(["3"], 0),
RUNTIME_ERRORS.TEAL.UNKNOWN_ENCODING
);
});

it("Should throw an error when argument not provided", () => {
stack.push(toPushStd);
expectRuntimeError(() => new Base64Decode([], 0), RUNTIME_ERRORS.TEAL.ASSERT_LENGTH);
});
});
});
Loading