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
12 changes: 12 additions & 0 deletions packages/runtime/src/errors/errors-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,18 @@ maximun uint128`,
title: "itxn_next without itxn_begin",
description: `itxn_next without itxn_begin`,
},
UNKNOWN_ENCODING: {
number: 1058,
message: "Encoding e must be {0, 1}, 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
45 changes: 45 additions & 0 deletions packages/runtime/src/interpreter/opcode-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {
import { addInnerTransaction, calculateInnerTxCredit, setInnerTxField } from "../lib/itxn";
import { bigintSqrt } from "../lib/math";
import {
assertBase64,
assertBase64Url,
assertLen,
assertNumber,
assertOnlyDigits,
Expand Down Expand Up @@ -4742,3 +4744,46 @@ 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: number;

/**
* Asserts 1 argument is passed.
* @param args Expected arguments: [e], where e = {0, 1}.
* @param line line number in TEAL file
*/
constructor(args: string[], line: number) {
super();
this.line = line;
assertLen(args.length, 1, line);
this.encoding = Number(args[0]);
if (!(this.encoding === 0 || this.encoding === 1)) {
throw new RuntimeError(RUNTIME_ERRORS.TEAL.UNKNOWN_ENCODING, {
encoding: this.encoding,
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);
if (this.encoding === 0) {
// UrlEncoding
assertBase64Url(convertToString(last), this.line);
stack.push(new Uint8Array(Buffer.from(decoded.toString(), "base64url")));
} else {
// e === 1 StdEncoding
assertBase64(convertToString(last), this.line);
stack.push(new Uint8Array(Buffer.from(decoded.toString(), "base64")));
}
}
}
18 changes: 18 additions & 0 deletions packages/runtime/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,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 @@ -432,6 +445,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
10 changes: 10 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 Expand Up @@ -448,6 +457,7 @@ const interpreterReqList = new Set([
"gitxna",
"gitxnas",
"itxnas",
"base64_decode",
]);

const signatureModeOps = new Set(["arg", "args", "arg_0", "arg_1", "arg_2", "arg_3"]);
Expand Down
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 0
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 @@ -6612,4 +6613,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(["0"], 0);
opUrl.execute(stack);
assert.deepEqual(expectedBytes, stack.pop());
stack.push(toPushStd);
const opStd = new Base64Decode(["1"], 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(["1"], 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(["0"], 0);
expectRuntimeError(() => op.execute(stack), RUNTIME_ERRORS.TEAL.INVALID_BASE64URL);
});

it("Should throw an error when the stack is empty", () => {
const op = new Base64Decode(["1"], 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);
});
});
});
12 changes: 12 additions & 0 deletions packages/runtime/test/src/parser/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
Arg,
Assert,
Balance,
Base64Decode,
BitLen,
BitwiseAnd,
BitwiseNot,
Expand Down Expand Up @@ -2583,6 +2584,17 @@ describe("Parser", function () {

assert.deepEqual(res, expected);
});

it("should return correct opcode list for `teal v7`", async () => {
const file = "teal-v7.teal";
const res = parser(getProgram(file), ExecutionMode.APPLICATION, interpreter);
const expected = [
new Pragma(["version", "7"], 1, interpreter),
new Base64Decode(["0"], 2),
];

assert.deepEqual(expected, res);
});
});

describe("Gas cost of Opcodes from TEAL file", () => {
Expand Down