@truffle/codec
Version:
Library for encoding and decoding smart contract data
360 lines • 17.1 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeContext = exports.normalizeContexts = exports.matchContext = exports.findContext = void 0;
const debug_1 = __importDefault(require("debug"));
const debug = (0, debug_1.default)("codec:contexts:utils");
const Evm = __importStar(require("../evm"));
const Conversion = __importStar(require("../conversion"));
const escapeRegExp_1 = __importDefault(require("lodash/escapeRegExp"));
const cbor = __importStar(require("cbor"));
const compile_common_1 = require("@truffle/compile-common");
const Abi = __importStar(require("@truffle/abi-utils"));
const AbiDataUtils = __importStar(require("../abi-data/utils"));
function findContext(contexts, binary) {
const matchingContexts = Object.values(contexts).filter(context => matchContext(context, binary));
//rather than just pick an arbitrary matching context, we're going
//to pick one that isn't a descendant of any of the others.
//(if there are multiple of *those*, then yeah it's arbitrary.)
const context = matchingContexts.find(descendant => !matchingContexts.some(ancestor => descendant.compilationId === ancestor.compilationId &&
descendant.linearizedBaseContracts &&
ancestor.contractId !== undefined &&
descendant.linearizedBaseContracts
.slice(1)
.includes(ancestor.contractId)
//we do slice one because everything is an an ancestor of itself; we only
//care about *proper* ancestors
));
return context || null;
}
exports.findContext = findContext;
function matchContext(context, givenBinary) {
const { binary, compiler, isConstructor } = context;
const lengthDifference = givenBinary.length - binary.length;
//first: if it's not a constructor, and it's not Vyper,
//they'd better be equal in length.
//if it is a constructor, or is Vyper,
//the given binary must be at least as long,
//and the difference must be a multiple of 32 bytes (64 hex digits)
const additionalAllowed = isConstructor || (compiler != undefined && compiler.name === "vyper");
if ((!additionalAllowed && lengthDifference !== 0) ||
lengthDifference < 0 ||
lengthDifference % (2 * Evm.Utils.WORD_SIZE) !== 0) {
return false;
}
for (let i = 0; i < binary.length; i++) {
//note: using strings like arrays is kind of dangerous in general in JS,
//but everything here is ASCII so it's fine
//note that we need to compare case-insensitive, since Solidity will
//put addresses in checksum case in the compiled source
//(we don't actually need that second toLowerCase(), but whatever)
if (binary[i] !== "." &&
binary[i].toLowerCase() !== givenBinary[i].toLowerCase()) {
return false;
}
}
return true;
}
exports.matchContext = matchContext;
function normalizeContexts(contexts) {
//unfortunately, due to our current link references format, we can't
//really use the binary from the artifact directly -- neither for purposes
//of matching, nor for purposes of decoding internal functions. So, we
//need to perform this normalization step on our contexts before using
//them. Once we have truffle-db, this step should largely go away.
debug("normalizing contexts");
//first, let's clone the input
//(let's do a 2-deep clone because we'll be altering binary & compiler)
let newContexts = Object.assign({}, ...Object.entries(contexts).map(([contextHash, context]) => ({
[contextHash]: Object.assign({}, context)
})));
debug("contexts cloned");
//next, we get all the library names and sort them descending by length.
//We're going to want to go in descending order of length so that we
//don't run into problems when one name is a substring of another.
//For simplicity, we'll exclude names of length <38, because we can
//handle these with our more general check for link references at the end
const fillerLength = 2 * Evm.Utils.ADDRESS_SIZE;
let names = Object.values(newContexts)
.filter(context => context.contractKind === "library")
.map(context => context.contractName)
.filter(name => name.length >= fillerLength - 3)
//the -3 is for 2 leading underscores and 1 trailing
.sort((name1, name2) => name2.length - name1.length);
debug("names sorted");
//now, we need to turn all these names into regular expressions, because,
//unfortunately, str.replace() will only replace all if you use a /g regexp;
//note that because names may contain '$', we need to escape them
//(also we prepend "__" because that's the placeholder format)
let regexps = names.map(name => new RegExp((0, escapeRegExp_1.default)("__" + name), "g"));
debug("regexps prepared");
//having done so, we can do the replace for these names!
const replacement = ".".repeat(fillerLength);
for (let regexp of regexps) {
for (let context of Object.values(newContexts)) {
context.binary = context.binary.replace(regexp, replacement);
}
}
debug("long replacements complete");
//now we can do a generic replace that will catch all names of length
//<40, while also catching the Solidity compiler's link reference format
//as well as Truffle's. Hooray!
const genericRegexp = new RegExp("_.{" + (fillerLength - 2) + "}_", "g");
//we're constructing the regexp /_.{38}_/g, but I didn't want to use a
//literal 38 :P
for (let context of Object.values(newContexts)) {
context.binary = context.binary.replace(genericRegexp, replacement);
}
debug("short replacements complete");
//now we must handle the delegatecall guard -- libraries' deployedBytecode will include
//0s in place of their own address instead of a link reference at the
//beginning, so we need to account for that too
const pushAddressInstruction = (0x5f + Evm.Utils.ADDRESS_SIZE).toString(16); //"73"
for (let context of Object.values(newContexts)) {
if (context.contractKind === "library" && !context.isConstructor) {
context.binary = context.binary.replace("0x" + pushAddressInstruction + "00".repeat(Evm.Utils.ADDRESS_SIZE), "0x" + pushAddressInstruction + replacement);
}
}
debug("extra library replacements complete");
//now let's handle immutable references
//(these are much nicer than link references due to not having to deal with the old format)
for (let context of Object.values(newContexts)) {
if (context.immutableReferences) {
for (let variable of Object.values(context.immutableReferences)) {
for (let { start, length } of (variable)) {
//Goddammit TS
let lowerStringIndex = 2 + 2 * start;
let upperStringIndex = 2 + 2 * (start + length);
context.binary =
context.binary.slice(0, lowerStringIndex) +
"..".repeat(length) +
context.binary.slice(upperStringIndex);
}
}
}
}
debug("immutables complete");
//now: extract & decode all the cbor's. we're going to use these for
//two different purposes, so let's just get them all upfront.
let cborInfos = {};
let decodedCbors = {};
//note: invalid cbor will be indicated in decodedCbors by the lack of an entry,
//*not* by undefined or null, since there exists cbor for those :P
for (const [contextHash, context] of Object.entries(newContexts)) {
const cborInfo = extractCborInfo(context.binary);
cborInfos[contextHash] = cborInfo;
if (cborInfo) {
try {
//note this *will* throw if there's data left over,
//which is what we want it to do
const decoded = cbor.decodeFirstSync(cborInfo.cbor);
decodedCbors[contextHash] = decoded;
}
catch (_a) {
//just don't add it
}
}
}
debug("intial cbor processing complete");
//now: if a context lacks a compiler, but a version can be found in the
//cbor, add it.
for (let [contextHash, context] of Object.entries(newContexts)) {
if (!context.compiler && contextHash in decodedCbors) {
context.compiler = detectCompilerInfo(decodedCbors[contextHash]);
}
}
debug("versions complete");
//one last step: where there's CBOR with a metadata hash, we'll allow the
//CBOR to vary, aside from the length (note: ideally here we would *only*
//dot-out the metadata hash part of the CBOR, but, well, it's not worth the
//trouble to detect that; doing that could potentially get pretty involved)
//note that if the code isn't Solidity, that's fine -- we just won't get
//valid CBOR and will not end up adding to our list of regular expressions
const externalCborInfos = Object.entries(cborInfos)
.filter(([contextHash, _cborInfo]) => contextHash in decodedCbors &&
isObjectWithHash(decodedCbors[contextHash]))
.map(([_contextHash, cborInfo]) => cborInfo);
const cborRegexps = externalCborInfos.map(cborInfo => ({
input: new RegExp(cborInfo.cborSegment, "g"),
output: "..".repeat(cborInfo.cborLength) + cborInfo.cborLengthHex
}));
//HACK: we will replace *every* occurrence of *every* external CBOR occurring
//in *every* context, in order to cover created contracts (including if there
//are multiple or recursive ones)
for (let context of Object.values(newContexts)) {
for (let { input, output } of cborRegexps) {
context.binary = context.binary.replace(input, output);
}
}
debug("external wildcards complete");
//finally, return this mess!
return newContexts;
}
exports.normalizeContexts = normalizeContexts;
//returns cbor info if cbor section is found, null if it is not.
//note that it does not account for Vyper 0.3.4's idiosyncratic format
//and so will return null on that. but that's OK, because Vyper 0.3.4's
//CBOR section is always fixed, so there isn't a need to normalize it here
function extractCborInfo(binary) {
debug("extracting cbor segement of %s", binary);
const lastTwoBytes = binary.slice(2).slice(-2 * 2); //2 bytes * 2 for hex
//the slice(2) there may seem unnecessary; it's to handle the possibility that the contract
//has less than two bytes in its bytecode (that won't happen with Solidity, but let's be
//certain)
if (lastTwoBytes.length < 2 * 2) {
return null; //don't try to handle this case!
}
const cborLength = parseInt(lastTwoBytes, 16);
const cborEnd = binary.length - 2 * 2;
const cborStart = cborEnd - cborLength * 2;
//sanity check
if (cborStart < 2) {
//"0x"
return null; //don't try to handle this case!
}
const cbor = binary.slice(cborStart, cborEnd);
return {
cborStart,
cborLength,
cborEnd,
cborLengthHex: lastTwoBytes,
cbor,
cborSegment: cbor + lastTwoBytes
};
}
function isObjectWithHash(decoded) {
if (typeof decoded !== "object" || decoded === null) {
return false;
}
//cbor sometimes returns maps and sometimes objects,
//so let's make things consistent by converting to a map
//(actually, is this true? borc did this, I think cbor
//does too, but I haven't checked recently)
if (!(decoded instanceof Map)) {
decoded = new Map(Object.entries(decoded));
}
const hashKeys = ["bzzr0", "bzzr1", "ipfs"];
return hashKeys.some(key => decoded.has(key));
}
//returns undefined if no valid compiler info detected
//(if it detects solc but no version, it will not return
//a partial result, just undefined)
function detectCompilerInfo(decoded) {
if (typeof decoded !== "object" || decoded === null) {
return undefined;
}
//cbor sometimes returns maps and sometimes objects,
//so let's make things consistent by converting to a map
//(although see note above?)
if (!(decoded instanceof Map)) {
decoded = new Map(Object.entries(decoded));
}
if (!decoded.has("solc")) {
//return undefined if the solc version field is not present
//(this occurs if version <0.5.9)
//currently no other language attaches cbor info, so, yeah
return undefined;
}
const rawVersion = decoded.get("solc");
if (typeof rawVersion === "string") {
//for prerelease versions, the version is stored as a string.
return {
name: "solc",
version: rawVersion
};
}
else if (rawVersion instanceof Uint8Array && rawVersion.length === 3) {
//for release versions, it's stored as a bytestring of length 3, with the
//bytes being major, minor, patch. so we just join them with "." to form
//a version string (although it's missing precise commit & etc).
return {
name: "solc",
version: rawVersion.join(".")
};
}
else {
//return undefined on anything else
return undefined;
}
}
function makeContext(contract, node, compilation, isConstructor = false) {
const abi = Abi.normalize(contract.abi);
const bytecode = isConstructor
? contract.bytecode
: contract.deployedBytecode;
const binary = compile_common_1.Shims.NewToLegacy.forBytecode(bytecode);
const hash = Conversion.toHexString(Evm.Utils.keccak256({
type: "string",
value: binary
}));
debug("hash: %s", hash);
const fallback = abi.find(abiEntry => abiEntry.type === "fallback") ||
null; //TS is failing at inference here
const receive = abi.find(abiEntry => abiEntry.type === "receive") || null; //and here
return {
context: hash,
contractName: contract.contractName,
binary,
contractId: node ? node.id : undefined,
linearizedBaseContracts: node ? node.linearizedBaseContracts : undefined,
contractKind: contractKind(contract, node),
immutableReferences: isConstructor
? undefined
: contract.immutableReferences,
isConstructor,
abi: AbiDataUtils.computeSelectors(abi),
payable: AbiDataUtils.abiHasPayableFallback(abi),
fallbackAbi: { fallback, receive },
compiler: compilation.compiler || contract.compiler,
compilationId: compilation.id
};
}
exports.makeContext = makeContext;
//attempts to determine if the given contract is a library or not
function contractKind(contract, node) {
//first: if we have a node, use its listed contract kind
if (node) {
return node.contractKind;
}
//next: check the contract kind field on the contract object itself, if it exists.
//however this isn't implemented yet so we'll skip it.
//next: if we have no direct info on the contract kind, but we do
//have the deployed bytecode, we'll use a HACK:
//we'll assume it's an ordinary contract, UNLESS its deployed bytecode begins with
//PUSH20 followed by 20 0s, in which case we'll assume it's a library
//(note: this will fail to detect libraries from before Solidity 0.4.20)
if (contract.deployedBytecode) {
const deployedBytecode = compile_common_1.Shims.NewToLegacy.forBytecode(contract.deployedBytecode);
const pushAddressInstruction = (0x5f + Evm.Utils.ADDRESS_SIZE).toString(16); //"73"
const libraryString = "0x" + pushAddressInstruction + "00".repeat(Evm.Utils.ADDRESS_SIZE);
return deployedBytecode.startsWith(libraryString) ? "library" : "contract";
}
//finally, in the absence of anything to go on, we'll assume it's an ordinary contract
return "contract";
}
//# sourceMappingURL=utils.js.map
;