ethers.js/packages/asm/lib.esm/assembler.js

796 lines
29 KiB
JavaScript
Raw Normal View History

2020-02-04 09:06:47 +03:00
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
// @TODO:
// - PIC
// - warn on opcode non-function iff parameters
// - warn return/revert non-empty, comment ; !assert(+1 @extra)
// - $$
// - In JS add config (positionIndependent)
// - When checking name collisions, verify no collision in javascript
import { dirname, resolve } from "path";
import vm from "vm";
import { ethers } from "ethers";
import { Opcode } from "./opcodes";
import { parse as _parse, parser as _parser } from "./_parser";
import { version } from "./_version";
const logger = new ethers.utils.Logger(version);
const Guard = {};
function hexConcat(values) {
return ethers.utils.hexlify(ethers.utils.concat(values.map((v) => {
if (v instanceof Opcode) {
return [v.value];
}
return v;
})));
}
function repeat(char, length) {
let result = char;
while (result.length < length) {
result += result;
}
return result.substring(0, length);
}
class Script {
constructor(filename, callback) {
ethers.utils.defineReadOnly(this, "filename", filename);
ethers.utils.defineReadOnly(this, "contextObject", this._baseContext(callback));
ethers.utils.defineReadOnly(this, "context", vm.createContext(this.contextObject));
}
_baseContext(callback) {
return new Proxy({
__filename: this.filename,
__dirname: dirname(this.filename),
console: console,
Uint8Array: Uint8Array,
ethers: ethers,
utils: ethers.utils,
BigNumber: ethers.BigNumber,
arrayify: ethers.utils.arrayify,
concat: hexConcat,
hexlify: ethers.utils.hexlify,
zeroPad: function (value, length) {
return ethers.utils.hexlify(ethers.utils.zeroPad(value, length));
},
id: ethers.utils.id,
keccak256: ethers.utils.keccak256,
namehash: ethers.utils.namehash,
sha256: ethers.utils.sha256,
parseEther: ethers.utils.parseEther,
formatEther: ethers.utils.formatEther,
parseUnits: ethers.utils.parseUnits,
formatUnits: ethers.utils.formatUnits,
randomBytes: function (length) {
return ethers.utils.hexlify(ethers.utils.randomBytes(length));
},
toUtf8Bytes: ethers.utils.toUtf8Bytes,
toUtf8String: ethers.utils.toUtf8String,
formatBytes32String: ethers.utils.formatBytes32String,
parseBytes32String: ethers.utils.parseBytes32String,
Opcode: Opcode,
sighash: function (signature) {
return ethers.utils.id(ethers.utils.FunctionFragment.from(signature).format()).substring(0, 10);
},
topichash: function (signature) {
return ethers.utils.id(ethers.utils.EventFragment.from(signature).format());
},
assemble: assemble,
disassemble: disassemble
}, {
get: (obj, key) => {
if (obj[key]) {
return obj[key];
}
if (!callback) {
return undefined;
}
return callback(key, this._context.context);
}
});
}
evaluate(code, context) {
return __awaiter(this, void 0, void 0, function* () {
if (this._context) {
throw new Error("evaluation collision");
}
this._context = { context: context };
const script = new vm.Script(code, { filename: this.filename });
let result = script.runInContext(this.context);
if (result instanceof Promise) {
result = yield result;
}
this._context = null;
return result;
});
}
}
let nextTag = 1;
export class Node {
constructor(guard, location, options) {
if (guard !== Guard) {
throw new Error("cannot instantiate class");
}
logger.checkAbstract(new.target, Node);
ethers.utils.defineReadOnly(this, "location", location);
ethers.utils.defineReadOnly(this, "tag", `node-${nextTag++}-${this.constructor.name}`);
ethers.utils.defineReadOnly(this, "warnings", []);
for (const key in options) {
ethers.utils.defineReadOnly(this, key, options[key]);
}
}
// Note: EVERY node must call assemble with `this`, even if only with
// the bytes "0x" to trigger the offset and bytecode checks
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
visit(this, "0x");
assembler.end(this);
});
}
children() {
return [];
}
visit(visit) {
visit(this);
}
static from(options) {
const Factories = {
data: DataNode,
decimal: LiteralNode,
eval: EvaluationNode,
exec: ExecutionNode,
hex: LiteralNode,
label: LabelNode,
length: LinkNode,
offset: LinkNode,
opcode: OpcodeNode,
scope: ScopeNode,
};
const factory = Factories[options.type];
if (!factory) {
throw new Error("uknown type: " + options.type);
}
return factory.from(options);
}
}
/*
export abstract class CodeNode extends Node {
constructor(guard: any, location: Location, options: { [ key: string ]: any }) {
logger.checkAbstract(new.target, CodeNode);
super(guard, location, options);
}
}
*/
export class ValueNode extends Node {
constructor(guard, location, options) {
logger.checkAbstract(new.target, ValueNode);
super(guard, location, options);
}
}
function pushLiteral(value) {
// Convert value into a hexstring
const hex = ethers.utils.hexlify(value);
if (hex === "0x") {
throw new Error("invalid literal: 0x");
}
// Make sure it will fit into a push
const length = ethers.utils.hexDataLength(hex);
if (length === 0 || length > 32) {
throw new Error(`literal out of range: ${hex}`);
}
return hexConcat([Opcode.from("PUSH" + String(length)), hex]);
}
export class LiteralNode extends ValueNode {
constructor(guard, location, value, verbatim) {
super(guard, location, { value, verbatim });
}
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
if (this.verbatim) {
if (this.value.substring(0, 2) === "0x") {
visit(this, this.value);
}
else {
visit(this, ethers.BigNumber.from(this.value).toHexString());
}
}
else {
visit(this, pushLiteral(ethers.BigNumber.from(this.value)));
}
assembler.end(this);
});
}
static from(options) {
if (options.type !== "hex" && options.type !== "decimal") {
throw new Error("expected hex or decimal type");
}
return new LiteralNode(Guard, options.loc, options.value, !!options.verbatim);
}
}
export class LinkNode extends ValueNode {
constructor(guard, location, type, label) {
super(guard, location, { type, label });
}
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
let value = null;
const target = assembler.getTarget(this.label);
if (target instanceof LabelNode) {
if (this.type === "offset") {
//value = assembler.getOffset(this.label);
value = (assembler.getLinkValue(target, this));
}
}
else {
const result = (assembler.getLinkValue(target, this));
if (this.type === "offset") {
//value = assembler.getOffset(this.label);
value = result.offset;
}
else if (this.type === "length") {
//value = assembler.getLength(this.label);
value = result.length;
}
}
if (value == null) {
throw new Error("labels can only be targetted as offsets");
}
visit(this, pushLiteral(value));
assembler.end(this);
});
}
static from(options) {
// @TODO: Verify type is offset or link...
return new LinkNode(Guard, options.loc, options.type, options.label);
}
}
export class OpcodeNode extends ValueNode {
constructor(guard, location, opcode, operands) {
super(guard, location, { opcode, operands });
if (opcode.isPush()) {
this.warnings.push("the PUSH opcode modifies program flow - use literals instead");
}
}
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
// Compute the bytecode in reverse stack order
for (let i = this.operands.length - 1; i >= 0; i--) {
yield this.operands[i].assemble(assembler, visit);
}
// Append this opcode
visit(this, ethers.utils.hexlify(this.opcode.value));
assembler.end(this);
});
}
children() {
return this.operands;
}
visit(visit) {
for (let i = this.operands.length - 1; i >= 0; i--) {
this.operands[i].visit(visit);
}
visit(this);
}
static from(options) {
if (options.type !== "opcode") {
throw new Error("expected opcode type");
}
const opcode = Opcode.from(options.mnemonic);
if (!opcode) {
throw new Error("unknown opcode: " + options.mnemonic);
}
// Using the function syntax will check the operand count
if (!options.bare) {
if (opcode.mnemonic === "POP" && options.operands.length === 0) {
// This is ok... Pop has a delta of 0, but without operands
}
else if (options.operands.length !== opcode.delta) {
throw new Error(`opcode ${opcode.mnemonic} expects ${opcode.delta} operands`);
}
}
const operands = Object.freeze(options.operands.map((o) => {
const operand = Node.from(o);
if (!(operand instanceof ValueNode)) {
throw new Error("invalid operand");
}
return operand;
}));
return new OpcodeNode(Guard, options.loc, opcode, operands);
}
}
export class LabelledNode extends Node {
constructor(guard, location, name, values) {
logger.checkAbstract(new.target, LabelledNode);
values = ethers.utils.shallowCopy(values || {});
values.name = name;
super(guard, location, values);
}
}
export class LabelNode extends LabelledNode {
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
visit(this, ethers.utils.hexlify(Opcode.from("JUMPDEST").value));
assembler.end(this);
});
}
static from(options) {
if (options.type !== "label") {
throw new Error("expected label type");
}
return new LabelNode(Guard, options.loc, options.name);
}
}
export class DataNode extends LabelledNode {
constructor(guard, location, name, data) {
super(guard, location, name, { data });
}
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
for (let i = 0; i < this.data.length; i++) {
yield this.data[i].assemble(assembler, visit);
}
// We pad data if is contains PUSH opcodes that would overrun
// the data, which could eclipse valid operations (since the
// VM won't execute or jump within PUSH operations)
const bytecode = ethers.utils.arrayify(assembler.getPendingBytecode(this));
// Replay the data as bytecode, skipping PUSH data
let i = 0;
while (i < bytecode.length) {
const opcode = Opcode.from(bytecode[i++]);
if (opcode) {
i += opcode.isPush();
}
}
// The amount we overshot the data by is how much padding we need
const padding = new Uint8Array(i - bytecode.length);
// What makes more sense? INVALID or 0 (i.e. STOP)?
//padding.fill(Opcode.from("INVALID").value);
padding.fill(0);
visit(this, ethers.utils.hexlify(padding));
assembler.end(this);
});
}
children() {
return this.data;
}
visit(visit) {
visit(this);
for (let i = 0; i < this.data.length; i++) {
this.data[i].visit(visit);
}
}
static from(options) {
if (options.type !== "data") {
throw new Error("expected data type");
}
return new DataNode(Guard, options.loc, options.name, Object.freeze(options.data.map((d) => Node.from(d))));
}
}
export class EvaluationNode extends ValueNode {
constructor(guard, location, script, verbatim) {
super(guard, location, { script, verbatim });
}
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
const result = yield assembler.evaluate(this.script, this);
if (this.verbatim) {
if (typeof (result) === "number") {
visit(this, ethers.BigNumber.from(result).toHexString());
}
else {
visit(this, ethers.utils.hexlify(result));
}
}
else {
visit(this, pushLiteral(result));
}
assembler.end(this);
});
}
static from(options) {
if (options.type !== "eval") {
throw new Error("expected eval type");
}
return new EvaluationNode(Guard, options.loc, options.script, !!options.verbatim);
}
}
export class ExecutionNode extends Node {
constructor(guard, location, script) {
super(guard, location, { script });
}
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
yield assembler.evaluate(this.script, this);
assembler.end(this);
});
}
static from(options) {
if (options.type !== "exec") {
throw new Error("expected exec type");
}
return new ExecutionNode(Guard, options.loc, options.script);
}
}
export class ScopeNode extends LabelledNode {
constructor(guard, location, name, statements) {
super(guard, location, name, { statements });
}
assemble(assembler, visit) {
return __awaiter(this, void 0, void 0, function* () {
assembler.start(this);
visit(this, "0x");
for (let i = 0; i < this.statements.length; i++) {
yield this.statements[i].assemble(assembler, visit);
}
assembler.end(this);
});
}
children() {
return this.statements;
}
visit(visit) {
visit(this);
for (let i = 0; i < this.statements.length; i++) {
this.statements[i].visit(visit);
}
}
static from(options) {
if (options.type !== "scope") {
throw new Error("expected scope type");
}
return new ScopeNode(Guard, options.loc, options.name, Object.freeze(options.statements.map((s) => Node.from(s))));
}
}
export function parse(code) {
// Since jison allows \n, \r or \r\n line endings, we need some
// twekaing to get the correct position
const lines = [];
let offset = 0;
code.split(/(\r\n?|\n)/g).forEach((clump, index) => {
if (index % 2) {
lines[lines.length - 1].line += clump;
}
else {
lines.push({ line: clump, offset: offset });
}
offset += clump.length;
});
// Add a mock-EOF to the end of the file so we don't out-of-bounds
// on the last character
if (lines.length) {
lines[lines.length - 1].line += "\n";
}
// Givens a line (1 offset) and column (0 offset) return the byte offset
const getOffset = function (line, column) {
const info = lines[line - 1];
if (!info || column >= info.line.length) {
throw new Error("out of range");
}
return info.offset + column;
};
// We use this in the _parser to convert locations to source
_parser.yy._ethersLocation = function (loc) {
// The _ scope should call with null to get the full source
if (loc == null) {
return Object.freeze({
offset: 0,
length: code.length,
source: code
});
}
const offset = getOffset(loc.first_line, loc.first_column);
const end = getOffset(loc.last_line, loc.last_column);
return Object.freeze({
offset: offset,
length: (end - offset),
source: code.substring(offset, end)
});
};
const result = Node.from(_parse(code));
// Nuke the source code lookup callback
_parser.yy._ethersLocation = null;
return result;
}
export function disassemble(bytecode) {
const ops = [];
const offsets = {};
const bytes = ethers.utils.arrayify(bytecode, { allowMissingPrefix: true });
let i = 0;
let oob = false;
while (i < bytes.length) {
let opcode = Opcode.from(bytes[i]);
if (!opcode) {
opcode = new Opcode(`unknown (${ethers.utils.hexlify(bytes[i])})`, bytes[i], 0, 0);
}
else if (oob && opcode.mnemonic === "JUMPDEST") {
opcode = new Opcode(`JUMPDEST (invalid; OOB!!)`, bytes[i], 0, 0);
}
const op = {
opcode: opcode,
offset: i
};
offsets[i] = op;
ops.push(op);
i++;
const push = opcode.isPush();
if (push) {
const data = ethers.utils.hexlify(bytes.slice(i, i + push));
if (ethers.utils.hexDataLength(data) === push) {
op.pushValue = data;
i += push;
}
else {
oob = true;
}
}
}
ops.getOperation = function (offset) {
return (offsets[offset] || null);
};
return ops;
}
export function formatBytecode(bytecode) {
const lines = [];
bytecode.forEach((op) => {
const opcode = op.opcode;
let offset = ethers.utils.hexZeroPad(ethers.utils.hexlify(op.offset), 2);
if (opcode.isValidJumpDest()) {
offset += "*";
}
else {
offset += " ";
}
let operation = opcode.mnemonic;
const push = opcode.isPush();
if (push) {
if (op.pushValue) {
operation = op.pushValue + `${repeat(" ", 67 - op.pushValue.length)}; #${push} `;
}
else {
operation += `${repeat(" ", 67 - operation.length)}; OOB!! `;
}
}
lines.push(`${offset.substring(2)}: ${operation}`);
});
return lines.join("\n");
}
// @TODO: Rename to Assembler?
class Assembler {
constructor(root, options) {
ethers.utils.defineReadOnly(this, "positionIndependentCode", !!options.positionIndependentCode);
ethers.utils.defineReadOnly(this, "retry", ((options.retry != null) ? options.retry : 512));
ethers.utils.defineReadOnly(this, "filename", resolve(options.filename || "./contract.asm"));
ethers.utils.defineReadOnly(this, "defines", Object.freeze(options.defines || {}));
ethers.utils.defineReadOnly(this, "root", root);
const nodes = {};
const labels = {};
const parents = {};
// Link labels to their target node
root.visit((node) => {
nodes[node.tag] = {
node: node,
offset: 0x0,
bytecode: "0x",
pending: "0x"
};
if (node instanceof LabelledNode) {
// Check for duplicate labels
if (labels[node.name]) {
logger.throwError(("duplicate label: " + node.name), ethers.utils.Logger.errors.UNSUPPORTED_OPERATION, {});
}
labels[node.name] = node;
}
});
root.visit((node) => {
// Check all labels exist
if (node instanceof LinkNode) {
const target = labels[node.label];
if (!target) {
logger.throwError(("missing label: " + node.label), ethers.utils.Logger.errors.UNSUPPORTED_OPERATION, {});
}
}
// Build the parent structure
node.children().forEach((child) => {
parents[child.tag] = node;
});
});
ethers.utils.defineReadOnly(this, "labels", Object.freeze(labels));
ethers.utils.defineReadOnly(this, "nodes", Object.freeze(nodes));
ethers.utils.defineReadOnly(this, "_parents", Object.freeze(parents));
ethers.utils.defineReadOnly(this, "_stack", []);
this.reset();
}
get changed() {
return this._changed;
}
// Link operations
getTarget(name) {
return this.labels[name];
}
// Reset the assmebler for another run with updated values
reset() {
this._changed = false;
for (const tag in this.nodes) {
delete this.nodes[tag].object;
}
this._script = new Script(this.filename, (name, context) => {
return this.get(name, context);
});
}
evaluate(script, source) {
return this._script.evaluate(script, source);
}
start(node) {
this._stack.push(node);
const info = this.nodes[node.tag];
info.pending = "0x";
}
end(node) {
if (this._stack.pop() !== node) {
throw new Error("missing push/pop pair");
}
const info = this.nodes[node.tag];
if (info.pending !== info.bytecode) {
this._didChange();
}
info.bytecode = info.pending;
}
getPendingBytecode(node) {
return this.nodes[node.tag].pending;
}
_appendBytecode(bytecode) {
this._stack.forEach((node) => {
const info = this.nodes[node.tag];
info.pending = hexConcat([info.pending, bytecode]);
});
}
getAncestor(node, cls) {
node = this._parents[node.tag];
while (node) {
if (node instanceof cls) {
return node;
}
node = this._parents[node.tag];
}
return null;
}
getLinkValue(target, source) {
const sourceScope = ((source instanceof ScopeNode) ? source : this.getAncestor(source, ScopeNode));
const targetScope = ((target instanceof ScopeNode) ? target : this.getAncestor(target, ScopeNode));
if (target instanceof LabelNode) {
// Label offset (e.g. "@foo:"); accessible only within its direct scope
//const scope = this.getAncestor(source, Scope);
if (targetScope !== sourceScope) {
throw new Error(`cannot access ${target.name} from ${source.tag}`);
}
// Return the offset relative to its scope
let offset = this.nodes[target.tag].offset - this.nodes[targetScope.tag].offset;
// Offsets are wrong; but we should finish this run and then try again
if (offset < 0) {
offset = 0;
this._didChange();
}
return offset;
}
const info = this.nodes[target.tag];
// Return the offset is relative to its scope
const bytes = Array.prototype.slice.call(ethers.utils.arrayify(info.bytecode));
bytes.ast = target;
bytes.source = target.location.source;
if (!((target instanceof DataNode) || (target instanceof ScopeNode))) {
throw new Error("invalid link value lookup");
}
// Check that target is any descendant (or self) of the source scope
let safeOffset = (sourceScope == targetScope);
if (!safeOffset) {
sourceScope.visit((node) => {
if (node === targetScope) {
safeOffset = true;
}
});
}
// Not safe to access the offset; this will fault if anything tries.
if (!safeOffset) {
Object.defineProperty(bytes, "offset", {
get: function () { throw new Error(`cannot access ${target.name}.offset from ${source.tag}`); }
});
}
// Add the offset relative to the scope; unless the offset has
// been marked as invalid, in which case accessing it will fail
if (safeOffset) {
bytes.offset = info.offset - this.nodes[sourceScope.tag].offset;
// Offsets are wqrong; but we should finish this run and then try again
if (bytes.offset < 0) {
bytes.offset = 0;
this._didChange();
}
}
return Object.freeze(bytes);
}
get(name, source) {
if (name === "defines") {
return this.defines;
}
const node = this.labels[name];
if (!node) {
return undefined;
}
const info = this.nodes[node.tag];
if (info.object == null) {
info.object = this.getLinkValue(node, source);
}
return info.object;
}
_didChange() {
this._changed = true;
}
_assemble() {
return __awaiter(this, void 0, void 0, function* () {
let offset = 0;
const bytecodes = [];
yield this.root.assemble(this, (node, bytecode) => {
const state = this.nodes[node.tag];
// Things have moved; we will need to try again
if (state.offset !== offset) {
state.offset = offset;
this._didChange();
}
this._appendBytecode(bytecode);
bytecodes.push(bytecode);
// The bytecode has changed; we will need to try again
//if (state.bytecode !== bytecode) {
// state.bytecode = bytecode;
// this._didChange();
//}
offset += ethers.utils.hexDataLength(bytecode);
});
return hexConcat(bytecodes);
});
}
assemble() {
return __awaiter(this, void 0, void 0, function* () {
// Continue re-evaluating the bytecode until a stable set of
// offsets, length and values are reached.
let bytecode = yield this._assemble();
for (let i = 0; i < this.retry; i++) {
// Regenerate the code with the updated assembler values
this.reset();
const adjusted = yield this._assemble();
// Generated bytecode is stable!! :)
if (!this.changed) {
console.log(`Assembled in ${i} attempts`);
return bytecode;
}
// Try again...
bytecode = adjusted;
}
// This should not happen; something is wrong with the grammar
// or missing enter/exit call in assemble
if (this._stack.length !== 0) {
throw new Error("bad AST");
}
return logger.throwError(`unable to assemble; ${this.retry} attempts failed to generate stable bytecode`, ethers.utils.Logger.errors.UNKNOWN_ERROR, {});
});
}
}
export function assemble(ast, options) {
return __awaiter(this, void 0, void 0, function* () {
const assembler = new Assembler(ast, options || {});
return assembler.assemble();
});
}