1003 lines
40 KiB
JavaScript
1003 lines
40 KiB
JavaScript
"use strict";
|
|
var __extends = (this && this.__extends) || (function () {
|
|
var extendStatics = function (d, b) {
|
|
extendStatics = Object.setPrototypeOf ||
|
|
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
|
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
|
return extendStatics(d, b);
|
|
};
|
|
return function (d, b) {
|
|
extendStatics(d, b);
|
|
function __() { this.constructor = d; }
|
|
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
|
};
|
|
})();
|
|
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());
|
|
});
|
|
};
|
|
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
function step(op) {
|
|
if (f) throw new TypeError("Generator is already executing.");
|
|
while (_) try {
|
|
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
switch (op[0]) {
|
|
case 0: case 1: t = op; break;
|
|
case 4: _.label++; return { value: op[1], done: false };
|
|
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
default:
|
|
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
if (t[2]) _.ops.pop();
|
|
_.trys.pop(); continue;
|
|
}
|
|
op = body.call(thisArg, _);
|
|
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
}
|
|
};
|
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
};
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
// @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
|
|
var path_1 = require("path");
|
|
var vm_1 = __importDefault(require("vm"));
|
|
var ethers_1 = require("ethers");
|
|
var opcodes_1 = require("./opcodes");
|
|
var _parser_1 = require("./_parser");
|
|
var _version_1 = require("./_version");
|
|
var logger = new ethers_1.ethers.utils.Logger(_version_1.version);
|
|
var Guard = {};
|
|
function hexConcat(values) {
|
|
return ethers_1.ethers.utils.hexlify(ethers_1.ethers.utils.concat(values.map(function (v) {
|
|
if (v instanceof opcodes_1.Opcode) {
|
|
return [v.value];
|
|
}
|
|
return v;
|
|
})));
|
|
}
|
|
function repeat(char, length) {
|
|
var result = char;
|
|
while (result.length < length) {
|
|
result += result;
|
|
}
|
|
return result.substring(0, length);
|
|
}
|
|
var Script = /** @class */ (function () {
|
|
function Script(filename, callback) {
|
|
ethers_1.ethers.utils.defineReadOnly(this, "filename", filename);
|
|
ethers_1.ethers.utils.defineReadOnly(this, "contextObject", this._baseContext(callback));
|
|
ethers_1.ethers.utils.defineReadOnly(this, "context", vm_1.default.createContext(this.contextObject));
|
|
}
|
|
Script.prototype._baseContext = function (callback) {
|
|
var _this = this;
|
|
return new Proxy({
|
|
__filename: this.filename,
|
|
__dirname: path_1.dirname(this.filename),
|
|
console: console,
|
|
Uint8Array: Uint8Array,
|
|
ethers: ethers_1.ethers,
|
|
utils: ethers_1.ethers.utils,
|
|
BigNumber: ethers_1.ethers.BigNumber,
|
|
arrayify: ethers_1.ethers.utils.arrayify,
|
|
concat: hexConcat,
|
|
hexlify: ethers_1.ethers.utils.hexlify,
|
|
zeroPad: function (value, length) {
|
|
return ethers_1.ethers.utils.hexlify(ethers_1.ethers.utils.zeroPad(value, length));
|
|
},
|
|
id: ethers_1.ethers.utils.id,
|
|
keccak256: ethers_1.ethers.utils.keccak256,
|
|
namehash: ethers_1.ethers.utils.namehash,
|
|
sha256: ethers_1.ethers.utils.sha256,
|
|
parseEther: ethers_1.ethers.utils.parseEther,
|
|
formatEther: ethers_1.ethers.utils.formatEther,
|
|
parseUnits: ethers_1.ethers.utils.parseUnits,
|
|
formatUnits: ethers_1.ethers.utils.formatUnits,
|
|
randomBytes: function (length) {
|
|
return ethers_1.ethers.utils.hexlify(ethers_1.ethers.utils.randomBytes(length));
|
|
},
|
|
toUtf8Bytes: ethers_1.ethers.utils.toUtf8Bytes,
|
|
toUtf8String: ethers_1.ethers.utils.toUtf8String,
|
|
formatBytes32String: ethers_1.ethers.utils.formatBytes32String,
|
|
parseBytes32String: ethers_1.ethers.utils.parseBytes32String,
|
|
Opcode: opcodes_1.Opcode,
|
|
sighash: function (signature) {
|
|
return ethers_1.ethers.utils.id(ethers_1.ethers.utils.FunctionFragment.from(signature).format()).substring(0, 10);
|
|
},
|
|
topichash: function (signature) {
|
|
return ethers_1.ethers.utils.id(ethers_1.ethers.utils.EventFragment.from(signature).format());
|
|
},
|
|
assemble: assemble,
|
|
disassemble: disassemble
|
|
}, {
|
|
get: function (obj, key) {
|
|
if (obj[key]) {
|
|
return obj[key];
|
|
}
|
|
if (!callback) {
|
|
return undefined;
|
|
}
|
|
return callback(key, _this._context.context);
|
|
}
|
|
});
|
|
};
|
|
Script.prototype.evaluate = function (code, context) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var script, result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
if (this._context) {
|
|
throw new Error("evaluation collision");
|
|
}
|
|
this._context = { context: context };
|
|
script = new vm_1.default.Script(code, { filename: this.filename });
|
|
result = script.runInContext(this.context);
|
|
if (!(result instanceof Promise)) return [3 /*break*/, 2];
|
|
return [4 /*yield*/, result];
|
|
case 1:
|
|
result = _a.sent();
|
|
_a.label = 2;
|
|
case 2:
|
|
this._context = null;
|
|
return [2 /*return*/, result];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
return Script;
|
|
}());
|
|
var nextTag = 1;
|
|
var Node = /** @class */ (function () {
|
|
function Node(guard, location, options) {
|
|
var _newTarget = this.constructor;
|
|
if (guard !== Guard) {
|
|
throw new Error("cannot instantiate class");
|
|
}
|
|
logger.checkAbstract(_newTarget, Node);
|
|
ethers_1.ethers.utils.defineReadOnly(this, "location", location);
|
|
ethers_1.ethers.utils.defineReadOnly(this, "tag", "node-" + nextTag++ + "-" + this.constructor.name);
|
|
ethers_1.ethers.utils.defineReadOnly(this, "warnings", []);
|
|
for (var key in options) {
|
|
ethers_1.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
|
|
Node.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
return __generator(this, function (_a) {
|
|
assembler.start(this);
|
|
visit(this, "0x");
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
});
|
|
});
|
|
};
|
|
Node.prototype.children = function () {
|
|
return [];
|
|
};
|
|
Node.prototype.visit = function (visit) {
|
|
visit(this);
|
|
};
|
|
Node.from = function (options) {
|
|
var Factories = {
|
|
data: DataNode,
|
|
decimal: LiteralNode,
|
|
eval: EvaluationNode,
|
|
exec: ExecutionNode,
|
|
hex: LiteralNode,
|
|
label: LabelNode,
|
|
length: LinkNode,
|
|
offset: LinkNode,
|
|
opcode: OpcodeNode,
|
|
scope: ScopeNode,
|
|
};
|
|
var factory = Factories[options.type];
|
|
if (!factory) {
|
|
throw new Error("uknown type: " + options.type);
|
|
}
|
|
return factory.from(options);
|
|
};
|
|
return Node;
|
|
}());
|
|
exports.Node = Node;
|
|
/*
|
|
export abstract class CodeNode extends Node {
|
|
constructor(guard: any, location: Location, options: { [ key: string ]: any }) {
|
|
logger.checkAbstract(new.target, CodeNode);
|
|
super(guard, location, options);
|
|
}
|
|
}
|
|
*/
|
|
var ValueNode = /** @class */ (function (_super) {
|
|
__extends(ValueNode, _super);
|
|
function ValueNode(guard, location, options) {
|
|
var _newTarget = this.constructor;
|
|
var _this = this;
|
|
logger.checkAbstract(_newTarget, ValueNode);
|
|
_this = _super.call(this, guard, location, options) || this;
|
|
return _this;
|
|
}
|
|
return ValueNode;
|
|
}(Node));
|
|
exports.ValueNode = ValueNode;
|
|
function pushLiteral(value) {
|
|
// Convert value into a hexstring
|
|
var hex = ethers_1.ethers.utils.hexlify(value);
|
|
if (hex === "0x") {
|
|
throw new Error("invalid literal: 0x");
|
|
}
|
|
// Make sure it will fit into a push
|
|
var length = ethers_1.ethers.utils.hexDataLength(hex);
|
|
if (length === 0 || length > 32) {
|
|
throw new Error("literal out of range: " + hex);
|
|
}
|
|
return hexConcat([opcodes_1.Opcode.from("PUSH" + String(length)), hex]);
|
|
}
|
|
var LiteralNode = /** @class */ (function (_super) {
|
|
__extends(LiteralNode, _super);
|
|
function LiteralNode(guard, location, value, verbatim) {
|
|
return _super.call(this, guard, location, { value: value, verbatim: verbatim }) || this;
|
|
}
|
|
LiteralNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
return __generator(this, function (_a) {
|
|
assembler.start(this);
|
|
if (this.verbatim) {
|
|
if (this.value.substring(0, 2) === "0x") {
|
|
visit(this, this.value);
|
|
}
|
|
else {
|
|
visit(this, ethers_1.ethers.BigNumber.from(this.value).toHexString());
|
|
}
|
|
}
|
|
else {
|
|
visit(this, pushLiteral(ethers_1.ethers.BigNumber.from(this.value)));
|
|
}
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
});
|
|
});
|
|
};
|
|
LiteralNode.from = function (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);
|
|
};
|
|
return LiteralNode;
|
|
}(ValueNode));
|
|
exports.LiteralNode = LiteralNode;
|
|
var LinkNode = /** @class */ (function (_super) {
|
|
__extends(LinkNode, _super);
|
|
function LinkNode(guard, location, type, label) {
|
|
return _super.call(this, guard, location, { type: type, label: label }) || this;
|
|
}
|
|
LinkNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var value, target, result;
|
|
return __generator(this, function (_a) {
|
|
assembler.start(this);
|
|
value = null;
|
|
target = assembler.getTarget(this.label);
|
|
if (target instanceof LabelNode) {
|
|
if (this.type === "offset") {
|
|
//value = assembler.getOffset(this.label);
|
|
value = (assembler.getLinkValue(target, this));
|
|
}
|
|
}
|
|
else {
|
|
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);
|
|
return [2 /*return*/];
|
|
});
|
|
});
|
|
};
|
|
LinkNode.from = function (options) {
|
|
// @TODO: Verify type is offset or link...
|
|
return new LinkNode(Guard, options.loc, options.type, options.label);
|
|
};
|
|
return LinkNode;
|
|
}(ValueNode));
|
|
exports.LinkNode = LinkNode;
|
|
var OpcodeNode = /** @class */ (function (_super) {
|
|
__extends(OpcodeNode, _super);
|
|
function OpcodeNode(guard, location, opcode, operands) {
|
|
var _this = _super.call(this, guard, location, { opcode: opcode, operands: operands }) || this;
|
|
if (opcode.isPush()) {
|
|
_this.warnings.push("the PUSH opcode modifies program flow - use literals instead");
|
|
}
|
|
return _this;
|
|
}
|
|
OpcodeNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var i;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
assembler.start(this);
|
|
i = this.operands.length - 1;
|
|
_a.label = 1;
|
|
case 1:
|
|
if (!(i >= 0)) return [3 /*break*/, 4];
|
|
return [4 /*yield*/, this.operands[i].assemble(assembler, visit)];
|
|
case 2:
|
|
_a.sent();
|
|
_a.label = 3;
|
|
case 3:
|
|
i--;
|
|
return [3 /*break*/, 1];
|
|
case 4:
|
|
// Append this opcode
|
|
visit(this, ethers_1.ethers.utils.hexlify(this.opcode.value));
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
OpcodeNode.prototype.children = function () {
|
|
return this.operands;
|
|
};
|
|
OpcodeNode.prototype.visit = function (visit) {
|
|
for (var i = this.operands.length - 1; i >= 0; i--) {
|
|
this.operands[i].visit(visit);
|
|
}
|
|
visit(this);
|
|
};
|
|
OpcodeNode.from = function (options) {
|
|
if (options.type !== "opcode") {
|
|
throw new Error("expected opcode type");
|
|
}
|
|
var opcode = opcodes_1.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");
|
|
}
|
|
}
|
|
var operands = Object.freeze(options.operands.map(function (o) {
|
|
var operand = Node.from(o);
|
|
if (!(operand instanceof ValueNode)) {
|
|
throw new Error("invalid operand");
|
|
}
|
|
return operand;
|
|
}));
|
|
return new OpcodeNode(Guard, options.loc, opcode, operands);
|
|
};
|
|
return OpcodeNode;
|
|
}(ValueNode));
|
|
exports.OpcodeNode = OpcodeNode;
|
|
var LabelledNode = /** @class */ (function (_super) {
|
|
__extends(LabelledNode, _super);
|
|
function LabelledNode(guard, location, name, values) {
|
|
var _newTarget = this.constructor;
|
|
var _this = this;
|
|
logger.checkAbstract(_newTarget, LabelledNode);
|
|
values = ethers_1.ethers.utils.shallowCopy(values || {});
|
|
values.name = name;
|
|
_this = _super.call(this, guard, location, values) || this;
|
|
return _this;
|
|
}
|
|
return LabelledNode;
|
|
}(Node));
|
|
exports.LabelledNode = LabelledNode;
|
|
var LabelNode = /** @class */ (function (_super) {
|
|
__extends(LabelNode, _super);
|
|
function LabelNode() {
|
|
return _super !== null && _super.apply(this, arguments) || this;
|
|
}
|
|
LabelNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
return __generator(this, function (_a) {
|
|
assembler.start(this);
|
|
visit(this, ethers_1.ethers.utils.hexlify(opcodes_1.Opcode.from("JUMPDEST").value));
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
});
|
|
});
|
|
};
|
|
LabelNode.from = function (options) {
|
|
if (options.type !== "label") {
|
|
throw new Error("expected label type");
|
|
}
|
|
return new LabelNode(Guard, options.loc, options.name);
|
|
};
|
|
return LabelNode;
|
|
}(LabelledNode));
|
|
exports.LabelNode = LabelNode;
|
|
var DataNode = /** @class */ (function (_super) {
|
|
__extends(DataNode, _super);
|
|
function DataNode(guard, location, name, data) {
|
|
return _super.call(this, guard, location, name, { data: data }) || this;
|
|
}
|
|
DataNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var i_1, bytecode, i, opcode, padding;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
assembler.start(this);
|
|
i_1 = 0;
|
|
_a.label = 1;
|
|
case 1:
|
|
if (!(i_1 < this.data.length)) return [3 /*break*/, 4];
|
|
return [4 /*yield*/, this.data[i_1].assemble(assembler, visit)];
|
|
case 2:
|
|
_a.sent();
|
|
_a.label = 3;
|
|
case 3:
|
|
i_1++;
|
|
return [3 /*break*/, 1];
|
|
case 4:
|
|
bytecode = ethers_1.ethers.utils.arrayify(assembler.getPendingBytecode(this));
|
|
i = 0;
|
|
while (i < bytecode.length) {
|
|
opcode = opcodes_1.Opcode.from(bytecode[i++]);
|
|
if (opcode) {
|
|
i += opcode.isPush();
|
|
}
|
|
}
|
|
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_1.ethers.utils.hexlify(padding));
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
DataNode.prototype.children = function () {
|
|
return this.data;
|
|
};
|
|
DataNode.prototype.visit = function (visit) {
|
|
visit(this);
|
|
for (var i = 0; i < this.data.length; i++) {
|
|
this.data[i].visit(visit);
|
|
}
|
|
};
|
|
DataNode.from = function (options) {
|
|
if (options.type !== "data") {
|
|
throw new Error("expected data type");
|
|
}
|
|
return new DataNode(Guard, options.loc, options.name, Object.freeze(options.data.map(function (d) { return Node.from(d); })));
|
|
};
|
|
return DataNode;
|
|
}(LabelledNode));
|
|
exports.DataNode = DataNode;
|
|
var EvaluationNode = /** @class */ (function (_super) {
|
|
__extends(EvaluationNode, _super);
|
|
function EvaluationNode(guard, location, script, verbatim) {
|
|
return _super.call(this, guard, location, { script: script, verbatim: verbatim }) || this;
|
|
}
|
|
EvaluationNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var result;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
assembler.start(this);
|
|
return [4 /*yield*/, assembler.evaluate(this.script, this)];
|
|
case 1:
|
|
result = _a.sent();
|
|
if (this.verbatim) {
|
|
if (typeof (result) === "number") {
|
|
visit(this, ethers_1.ethers.BigNumber.from(result).toHexString());
|
|
}
|
|
else {
|
|
visit(this, ethers_1.ethers.utils.hexlify(result));
|
|
}
|
|
}
|
|
else {
|
|
visit(this, pushLiteral(result));
|
|
}
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
EvaluationNode.from = function (options) {
|
|
if (options.type !== "eval") {
|
|
throw new Error("expected eval type");
|
|
}
|
|
return new EvaluationNode(Guard, options.loc, options.script, !!options.verbatim);
|
|
};
|
|
return EvaluationNode;
|
|
}(ValueNode));
|
|
exports.EvaluationNode = EvaluationNode;
|
|
var ExecutionNode = /** @class */ (function (_super) {
|
|
__extends(ExecutionNode, _super);
|
|
function ExecutionNode(guard, location, script) {
|
|
return _super.call(this, guard, location, { script: script }) || this;
|
|
}
|
|
ExecutionNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
assembler.start(this);
|
|
return [4 /*yield*/, assembler.evaluate(this.script, this)];
|
|
case 1:
|
|
_a.sent();
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
ExecutionNode.from = function (options) {
|
|
if (options.type !== "exec") {
|
|
throw new Error("expected exec type");
|
|
}
|
|
return new ExecutionNode(Guard, options.loc, options.script);
|
|
};
|
|
return ExecutionNode;
|
|
}(Node));
|
|
exports.ExecutionNode = ExecutionNode;
|
|
var ScopeNode = /** @class */ (function (_super) {
|
|
__extends(ScopeNode, _super);
|
|
function ScopeNode(guard, location, name, statements) {
|
|
return _super.call(this, guard, location, name, { statements: statements }) || this;
|
|
}
|
|
ScopeNode.prototype.assemble = function (assembler, visit) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var i;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
assembler.start(this);
|
|
visit(this, "0x");
|
|
i = 0;
|
|
_a.label = 1;
|
|
case 1:
|
|
if (!(i < this.statements.length)) return [3 /*break*/, 4];
|
|
return [4 /*yield*/, this.statements[i].assemble(assembler, visit)];
|
|
case 2:
|
|
_a.sent();
|
|
_a.label = 3;
|
|
case 3:
|
|
i++;
|
|
return [3 /*break*/, 1];
|
|
case 4:
|
|
assembler.end(this);
|
|
return [2 /*return*/];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
ScopeNode.prototype.children = function () {
|
|
return this.statements;
|
|
};
|
|
ScopeNode.prototype.visit = function (visit) {
|
|
visit(this);
|
|
for (var i = 0; i < this.statements.length; i++) {
|
|
this.statements[i].visit(visit);
|
|
}
|
|
};
|
|
ScopeNode.from = function (options) {
|
|
if (options.type !== "scope") {
|
|
throw new Error("expected scope type");
|
|
}
|
|
return new ScopeNode(Guard, options.loc, options.name, Object.freeze(options.statements.map(function (s) { return Node.from(s); })));
|
|
};
|
|
return ScopeNode;
|
|
}(LabelledNode));
|
|
exports.ScopeNode = ScopeNode;
|
|
function parse(code) {
|
|
// Since jison allows \n, \r or \r\n line endings, we need some
|
|
// twekaing to get the correct position
|
|
var lines = [];
|
|
var offset = 0;
|
|
code.split(/(\r\n?|\n)/g).forEach(function (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
|
|
var getOffset = function (line, column) {
|
|
var 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_1.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
|
|
});
|
|
}
|
|
var offset = getOffset(loc.first_line, loc.first_column);
|
|
var end = getOffset(loc.last_line, loc.last_column);
|
|
return Object.freeze({
|
|
offset: offset,
|
|
length: (end - offset),
|
|
source: code.substring(offset, end)
|
|
});
|
|
};
|
|
var result = Node.from(_parser_1.parse(code));
|
|
// Nuke the source code lookup callback
|
|
_parser_1.parser.yy._ethersLocation = null;
|
|
return result;
|
|
}
|
|
exports.parse = parse;
|
|
function disassemble(bytecode) {
|
|
var ops = [];
|
|
var offsets = {};
|
|
var bytes = ethers_1.ethers.utils.arrayify(bytecode, { allowMissingPrefix: true });
|
|
var i = 0;
|
|
var oob = false;
|
|
while (i < bytes.length) {
|
|
var opcode = opcodes_1.Opcode.from(bytes[i]);
|
|
if (!opcode) {
|
|
opcode = new opcodes_1.Opcode("unknown (" + ethers_1.ethers.utils.hexlify(bytes[i]) + ")", bytes[i], 0, 0);
|
|
}
|
|
else if (oob && opcode.mnemonic === "JUMPDEST") {
|
|
opcode = new opcodes_1.Opcode("JUMPDEST (invalid; OOB!!)", bytes[i], 0, 0);
|
|
}
|
|
var op = {
|
|
opcode: opcode,
|
|
offset: i
|
|
};
|
|
offsets[i] = op;
|
|
ops.push(op);
|
|
i++;
|
|
var push = opcode.isPush();
|
|
if (push) {
|
|
var data = ethers_1.ethers.utils.hexlify(bytes.slice(i, i + push));
|
|
if (ethers_1.ethers.utils.hexDataLength(data) === push) {
|
|
op.pushValue = data;
|
|
i += push;
|
|
}
|
|
else {
|
|
oob = true;
|
|
}
|
|
}
|
|
}
|
|
ops.getOperation = function (offset) {
|
|
return (offsets[offset] || null);
|
|
};
|
|
return ops;
|
|
}
|
|
exports.disassemble = disassemble;
|
|
function formatBytecode(bytecode) {
|
|
var lines = [];
|
|
bytecode.forEach(function (op) {
|
|
var opcode = op.opcode;
|
|
var offset = ethers_1.ethers.utils.hexZeroPad(ethers_1.ethers.utils.hexlify(op.offset), 2);
|
|
if (opcode.isValidJumpDest()) {
|
|
offset += "*";
|
|
}
|
|
else {
|
|
offset += " ";
|
|
}
|
|
var operation = opcode.mnemonic;
|
|
var 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");
|
|
}
|
|
exports.formatBytecode = formatBytecode;
|
|
// @TODO: Rename to Assembler?
|
|
var Assembler = /** @class */ (function () {
|
|
function Assembler(root, options) {
|
|
ethers_1.ethers.utils.defineReadOnly(this, "positionIndependentCode", !!options.positionIndependentCode);
|
|
ethers_1.ethers.utils.defineReadOnly(this, "retry", ((options.retry != null) ? options.retry : 512));
|
|
ethers_1.ethers.utils.defineReadOnly(this, "filename", path_1.resolve(options.filename || "./contract.asm"));
|
|
ethers_1.ethers.utils.defineReadOnly(this, "defines", Object.freeze(options.defines || {}));
|
|
ethers_1.ethers.utils.defineReadOnly(this, "root", root);
|
|
var nodes = {};
|
|
var labels = {};
|
|
var parents = {};
|
|
// Link labels to their target node
|
|
root.visit(function (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_1.ethers.utils.Logger.errors.UNSUPPORTED_OPERATION, {});
|
|
}
|
|
labels[node.name] = node;
|
|
}
|
|
});
|
|
root.visit(function (node) {
|
|
// Check all labels exist
|
|
if (node instanceof LinkNode) {
|
|
var target = labels[node.label];
|
|
if (!target) {
|
|
logger.throwError(("missing label: " + node.label), ethers_1.ethers.utils.Logger.errors.UNSUPPORTED_OPERATION, {});
|
|
}
|
|
}
|
|
// Build the parent structure
|
|
node.children().forEach(function (child) {
|
|
parents[child.tag] = node;
|
|
});
|
|
});
|
|
ethers_1.ethers.utils.defineReadOnly(this, "labels", Object.freeze(labels));
|
|
ethers_1.ethers.utils.defineReadOnly(this, "nodes", Object.freeze(nodes));
|
|
ethers_1.ethers.utils.defineReadOnly(this, "_parents", Object.freeze(parents));
|
|
ethers_1.ethers.utils.defineReadOnly(this, "_stack", []);
|
|
this.reset();
|
|
}
|
|
Object.defineProperty(Assembler.prototype, "changed", {
|
|
get: function () {
|
|
return this._changed;
|
|
},
|
|
enumerable: true,
|
|
configurable: true
|
|
});
|
|
// Link operations
|
|
Assembler.prototype.getTarget = function (name) {
|
|
return this.labels[name];
|
|
};
|
|
// Reset the assmebler for another run with updated values
|
|
Assembler.prototype.reset = function () {
|
|
var _this = this;
|
|
this._changed = false;
|
|
for (var tag in this.nodes) {
|
|
delete this.nodes[tag].object;
|
|
}
|
|
this._script = new Script(this.filename, function (name, context) {
|
|
return _this.get(name, context);
|
|
});
|
|
};
|
|
Assembler.prototype.evaluate = function (script, source) {
|
|
return this._script.evaluate(script, source);
|
|
};
|
|
Assembler.prototype.start = function (node) {
|
|
this._stack.push(node);
|
|
var info = this.nodes[node.tag];
|
|
info.pending = "0x";
|
|
};
|
|
Assembler.prototype.end = function (node) {
|
|
if (this._stack.pop() !== node) {
|
|
throw new Error("missing push/pop pair");
|
|
}
|
|
var info = this.nodes[node.tag];
|
|
if (info.pending !== info.bytecode) {
|
|
this._didChange();
|
|
}
|
|
info.bytecode = info.pending;
|
|
};
|
|
Assembler.prototype.getPendingBytecode = function (node) {
|
|
return this.nodes[node.tag].pending;
|
|
};
|
|
Assembler.prototype._appendBytecode = function (bytecode) {
|
|
var _this = this;
|
|
this._stack.forEach(function (node) {
|
|
var info = _this.nodes[node.tag];
|
|
info.pending = hexConcat([info.pending, bytecode]);
|
|
});
|
|
};
|
|
Assembler.prototype.getAncestor = function (node, cls) {
|
|
node = this._parents[node.tag];
|
|
while (node) {
|
|
if (node instanceof cls) {
|
|
return node;
|
|
}
|
|
node = this._parents[node.tag];
|
|
}
|
|
return null;
|
|
};
|
|
Assembler.prototype.getLinkValue = function (target, source) {
|
|
var sourceScope = ((source instanceof ScopeNode) ? source : this.getAncestor(source, ScopeNode));
|
|
var 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
|
|
var 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;
|
|
}
|
|
var info = this.nodes[target.tag];
|
|
// Return the offset is relative to its scope
|
|
var bytes = Array.prototype.slice.call(ethers_1.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
|
|
var safeOffset = (sourceScope == targetScope);
|
|
if (!safeOffset) {
|
|
sourceScope.visit(function (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);
|
|
};
|
|
Assembler.prototype.get = function (name, source) {
|
|
if (name === "defines") {
|
|
return this.defines;
|
|
}
|
|
var node = this.labels[name];
|
|
if (!node) {
|
|
return undefined;
|
|
}
|
|
var info = this.nodes[node.tag];
|
|
if (info.object == null) {
|
|
info.object = this.getLinkValue(node, source);
|
|
}
|
|
return info.object;
|
|
};
|
|
Assembler.prototype._didChange = function () {
|
|
this._changed = true;
|
|
};
|
|
Assembler.prototype._assemble = function () {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var offset, bytecodes;
|
|
var _this = this;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0:
|
|
offset = 0;
|
|
bytecodes = [];
|
|
return [4 /*yield*/, this.root.assemble(this, function (node, bytecode) {
|
|
var 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_1.ethers.utils.hexDataLength(bytecode);
|
|
})];
|
|
case 1:
|
|
_a.sent();
|
|
return [2 /*return*/, hexConcat(bytecodes)];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
Assembler.prototype.assemble = function () {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var bytecode, i, adjusted;
|
|
return __generator(this, function (_a) {
|
|
switch (_a.label) {
|
|
case 0: return [4 /*yield*/, this._assemble()];
|
|
case 1:
|
|
bytecode = _a.sent();
|
|
i = 0;
|
|
_a.label = 2;
|
|
case 2:
|
|
if (!(i < this.retry)) return [3 /*break*/, 5];
|
|
// Regenerate the code with the updated assembler values
|
|
this.reset();
|
|
return [4 /*yield*/, this._assemble()];
|
|
case 3:
|
|
adjusted = _a.sent();
|
|
// Generated bytecode is stable!! :)
|
|
if (!this.changed) {
|
|
console.log("Assembled in " + i + " attempts");
|
|
return [2 /*return*/, bytecode];
|
|
}
|
|
// Try again...
|
|
bytecode = adjusted;
|
|
_a.label = 4;
|
|
case 4:
|
|
i++;
|
|
return [3 /*break*/, 2];
|
|
case 5:
|
|
// 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 [2 /*return*/, logger.throwError("unable to assemble; " + this.retry + " attempts failed to generate stable bytecode", ethers_1.ethers.utils.Logger.errors.UNKNOWN_ERROR, {})];
|
|
}
|
|
});
|
|
});
|
|
};
|
|
return Assembler;
|
|
}());
|
|
function assemble(ast, options) {
|
|
return __awaiter(this, void 0, void 0, function () {
|
|
var assembler;
|
|
return __generator(this, function (_a) {
|
|
assembler = new Assembler(ast, options || {});
|
|
return [2 /*return*/, assembler.assemble()];
|
|
});
|
|
});
|
|
}
|
|
exports.assemble = assemble;
|