531 lines
17 KiB
TypeScript
531 lines
17 KiB
TypeScript
import ethUtil = require('ethereumjs-util');
|
|
import CommunicationChatBubbleOutline from 'material-ui/SvgIcon';
|
|
var _ = require('lodash');
|
|
|
|
export abstract class CalldataBlock {
|
|
private name: string;
|
|
private signature: string;
|
|
private offsetInBytes: number;
|
|
private headerSizeInBytes: number;
|
|
private bodySizeInBytes: number;
|
|
private relocatable: boolean;
|
|
private parentName: string;
|
|
|
|
constructor(name: string, signature: string, parentName: string, /*offsetInBytes: number,*/ headerSizeInBytes: number, bodySizeInBytes: number, relocatable: boolean) {
|
|
this.name = name;
|
|
this.signature = signature;
|
|
this.parentName = parentName;
|
|
this.offsetInBytes = 0;
|
|
this.headerSizeInBytes = headerSizeInBytes;
|
|
this.bodySizeInBytes = bodySizeInBytes;
|
|
this.relocatable = relocatable;
|
|
}
|
|
|
|
protected setHeaderSize(headerSizeInBytes: number) {
|
|
this.headerSizeInBytes = headerSizeInBytes;
|
|
}
|
|
|
|
protected setBodySize(bodySizeInBytes: number) {
|
|
this.bodySizeInBytes = bodySizeInBytes;
|
|
}
|
|
|
|
protected setName(name: string) {
|
|
this.name = name;
|
|
}
|
|
|
|
public getName(): string {
|
|
return this.name;
|
|
}
|
|
|
|
public getParentName(): string {
|
|
return this.parentName;
|
|
}
|
|
|
|
public getSignature(): string {
|
|
return this.signature;
|
|
}
|
|
|
|
public isRelocatable(): boolean {
|
|
return this.relocatable;
|
|
}
|
|
|
|
public getHeaderSizeInBytes(): number {
|
|
return this.headerSizeInBytes;
|
|
}
|
|
|
|
public getBodySizeInBytes(): number {
|
|
return this.bodySizeInBytes;
|
|
}
|
|
|
|
public getSizeInBytes(): number {
|
|
return this.headerSizeInBytes + this.bodySizeInBytes;
|
|
}
|
|
|
|
public getOffsetInBytes(): number {
|
|
return this.offsetInBytes;
|
|
}
|
|
|
|
public setOffset(offsetInBytes: number) {
|
|
this.offsetInBytes = offsetInBytes;
|
|
}
|
|
|
|
public computeHash(): Buffer {
|
|
const rawData = this.getRawData();
|
|
const hash = ethUtil.sha3(rawData);
|
|
return hash;
|
|
}
|
|
|
|
public abstract toBuffer(): Buffer;
|
|
public abstract getRawData(): Buffer;
|
|
}
|
|
|
|
export class PayloadCalldataBlock extends CalldataBlock {
|
|
private payload: Buffer;
|
|
|
|
constructor(name: string, signature: string, parentName: string, /*offsetInBytes: number,*/ relocatable: boolean, payload: Buffer) {
|
|
const headerSizeInBytes = 0;
|
|
const bodySizeInBytes = payload.byteLength;
|
|
super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes, relocatable);
|
|
this.payload = payload;
|
|
}
|
|
|
|
public toBuffer(): Buffer {
|
|
return this.payload;
|
|
}
|
|
|
|
public getRawData(): Buffer {
|
|
return this.payload;
|
|
}
|
|
}
|
|
|
|
export class DependentCalldataBlock extends CalldataBlock {
|
|
public static DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32;
|
|
public static RAW_DATA_START = new Buffer('<');
|
|
public static RAW_DATA_END = new Buffer('>');
|
|
private parent: CalldataBlock;
|
|
private dependency: CalldataBlock;
|
|
private aliasFor: CalldataBlock | undefined;
|
|
|
|
constructor(name: string, signature: string, parentName: string, relocatable: boolean, dependency: CalldataBlock, parent: CalldataBlock) {
|
|
const headerSizeInBytes = 0;
|
|
const bodySizeInBytes = DependentCalldataBlock.DEPENDENT_PAYLOAD_SIZE_IN_BYTES;
|
|
super(name, signature, parentName, headerSizeInBytes, bodySizeInBytes, relocatable);
|
|
this.parent = parent;
|
|
this.dependency = dependency;
|
|
this.aliasFor = undefined;
|
|
}
|
|
|
|
public toBuffer(): Buffer {
|
|
const destinationOffset = (this.aliasFor !== undefined) ? this.aliasFor.getOffsetInBytes() : this.dependency.getOffsetInBytes();
|
|
const parentOffset = this.parent.getOffsetInBytes();
|
|
const parentHeaderSize = this.parent.getHeaderSizeInBytes();
|
|
const pointer: number = destinationOffset - (parentOffset + parentHeaderSize);
|
|
const pointerBuf = ethUtil.toBuffer(`0x${pointer.toString(16)}`);
|
|
const evmWordWidthInBytes = 32;
|
|
const pointerBufPadded = ethUtil.setLengthLeft(pointerBuf, evmWordWidthInBytes);
|
|
return pointerBufPadded;
|
|
}
|
|
|
|
public getDependency(): CalldataBlock {
|
|
return this.dependency;
|
|
}
|
|
|
|
public setAlias(block: CalldataBlock) {
|
|
this.aliasFor = block;
|
|
this.setName(`${this.getName()} (alias for ${block.getName()})`);
|
|
}
|
|
|
|
public getAlias(): CalldataBlock | undefined {
|
|
return this.aliasFor;
|
|
}
|
|
|
|
public getRawData(): Buffer {
|
|
const dependencyRawData = this.dependency.getRawData();
|
|
const rawDataComponents: Buffer[] = [];
|
|
rawDataComponents.push(DependentCalldataBlock.RAW_DATA_START);
|
|
rawDataComponents.push(dependencyRawData);
|
|
rawDataComponents.push(DependentCalldataBlock.RAW_DATA_END);
|
|
const rawData = Buffer.concat(rawDataComponents);
|
|
return rawData;
|
|
}
|
|
}
|
|
|
|
export class MemberCalldataBlock extends CalldataBlock {
|
|
private static DEPENDENT_PAYLOAD_SIZE_IN_BYTES = 32;
|
|
private header: Buffer | undefined;
|
|
private members: CalldataBlock[];
|
|
private contiguous: boolean;
|
|
|
|
constructor(name: string, signature: string, parentName: string, relocatable: boolean, contiguous: boolean) {
|
|
super(name, signature, parentName, 0, 0, relocatable);
|
|
this.members = [];
|
|
this.header = undefined;
|
|
this.contiguous = contiguous;
|
|
}
|
|
|
|
public getRawData(): Buffer {
|
|
const rawDataComponents: Buffer[] = [];
|
|
if (this.header !== undefined) {
|
|
rawDataComponents.push(this.header);
|
|
}
|
|
_.each(this.members, (member: CalldataBlock) => {
|
|
const memberBuffer = member.getRawData();
|
|
rawDataComponents.push(memberBuffer);
|
|
});
|
|
|
|
const rawData = Buffer.concat(rawDataComponents);
|
|
return rawData;
|
|
}
|
|
|
|
public setMembers(members: CalldataBlock[]) {
|
|
let bodySizeInBytes = 0;
|
|
_.each(members, (member: CalldataBlock) => {
|
|
bodySizeInBytes += member.getSizeInBytes();
|
|
});
|
|
this.members = members;
|
|
this.setBodySize(0);
|
|
}
|
|
|
|
public isContiguous(): boolean {
|
|
return true;
|
|
}
|
|
|
|
public setHeader(header: Buffer) {
|
|
this.setHeaderSize(header.byteLength);
|
|
this.header = header;
|
|
}
|
|
|
|
public toBuffer(): Buffer {
|
|
if (this.header !== undefined) return this.header;
|
|
return new Buffer('');
|
|
}
|
|
|
|
public getMembers(): CalldataBlock[] {
|
|
return this.members;
|
|
}
|
|
}
|
|
|
|
class Queue<T> {
|
|
private store: T[] = [];
|
|
push(val: T) {
|
|
this.store.push(val);
|
|
}
|
|
pushFront(val: T) {
|
|
this.store.unshift(val);
|
|
}
|
|
pop(): T | undefined {
|
|
return this.store.shift();
|
|
}
|
|
merge(q: Queue<T>) {
|
|
this.store = this.store.concat(q.store);
|
|
}
|
|
mergeFront(q: Queue<T>) {
|
|
this.store = q.store.concat(this.store);
|
|
}
|
|
getStore(): T[] {
|
|
return this.store;
|
|
}
|
|
peek(): T | undefined {
|
|
return this.store.length >= 0 ? this.store[0] : undefined;
|
|
}
|
|
}
|
|
|
|
export class Calldata {
|
|
private selector: string;
|
|
private sizeInBytes: number;
|
|
private root: MemberCalldataBlock | undefined;
|
|
|
|
constructor() {
|
|
this.selector = '0x';
|
|
this.sizeInBytes = 0;
|
|
this.root = undefined;
|
|
}
|
|
|
|
private createQueue(memberBlock: MemberCalldataBlock): Queue<CalldataBlock> {
|
|
const blockQueue = new Queue<CalldataBlock>();
|
|
_.eachRight(memberBlock.getMembers(), (member: CalldataBlock) => {
|
|
if (member instanceof MemberCalldataBlock) {
|
|
blockQueue.mergeFront(this.createQueue(member));
|
|
} else {
|
|
blockQueue.pushFront(member);
|
|
}
|
|
});
|
|
|
|
// Children
|
|
_.each(memberBlock.getMembers(), (member: CalldataBlock) => {
|
|
if (member instanceof DependentCalldataBlock && member.getAlias() === undefined) {
|
|
let dependency = member.getDependency();
|
|
if (dependency instanceof MemberCalldataBlock) {
|
|
blockQueue.merge(this.createQueue(dependency));
|
|
} else {
|
|
blockQueue.push(dependency);
|
|
}
|
|
}
|
|
});
|
|
|
|
blockQueue.pushFront(memberBlock);
|
|
return blockQueue;
|
|
}
|
|
|
|
/*
|
|
|
|
// Basic optimize method that prunes duplicate branches of the tree
|
|
// Notes:
|
|
// 1. Pruning is at the calldata block level, so it is independent of type
|
|
// 2.
|
|
private optimize(blocks: CalldataBlock[]) {
|
|
// Build hash table of blocks
|
|
const blockLookupTable: { [key: string]: string } = {};
|
|
_.each(blocks, (block: CalldataBlock) => {
|
|
if (blocks instanceof DependentCalldataBlock === false) {
|
|
|
|
return;
|
|
}
|
|
|
|
const leavesHash = block.hashLeaves();
|
|
if (leavesHash in blockLookupTable) {
|
|
|
|
}
|
|
})
|
|
}*/
|
|
|
|
private generateAnnotatedHexString(): string {
|
|
let hexValue = `${this.selector}`;
|
|
if (this.root === undefined) {
|
|
throw new Error('expected root');
|
|
}
|
|
|
|
const valueQueue = this.createQueue(this.root);
|
|
|
|
let block: CalldataBlock | undefined;
|
|
let offset = 0;
|
|
const functionBlock = valueQueue.peek();
|
|
let functionName: string = functionBlock === undefined ? '' : functionBlock.getName();
|
|
while ((block = valueQueue.pop()) !== undefined) {
|
|
// Set f
|
|
|
|
// Process each block 1 word at a time
|
|
const size = block.getSizeInBytes();
|
|
const name = block.getName();
|
|
const parentName = block.getParentName();
|
|
console.log('*'.repeat(50), parentName, ' vs ', name);
|
|
|
|
//const ancestrialNamesOffset = name.startsWith('ptr<') ? 4 : 0;
|
|
//const parentOffset = name.lastIndexOf(parentName);
|
|
const prettyName = name.replace(`${parentName}.`, '').replace(`${functionName}.`, '');//.replace(`${parentName}[`, '[');
|
|
const signature = block.getSignature();
|
|
|
|
// Current offset
|
|
let offsetStr = '';
|
|
|
|
// If this block is empty then it's a newline
|
|
let value = '';
|
|
let nameStr = '';
|
|
let line = '';
|
|
if (size === 0) {
|
|
offsetStr = ' '.repeat(10);
|
|
value = ' '.repeat(74);
|
|
nameStr = `### ${prettyName.padEnd(80)}`;
|
|
line = `\n${offsetStr}${value}${nameStr}`;
|
|
} else {
|
|
offsetStr = `0x${offset.toString(16)}`.padEnd(10, ' ');
|
|
value = ethUtil.stripHexPrefix(ethUtil.bufferToHex(block.toBuffer().slice(0, 32))).padEnd(74);
|
|
if (block instanceof MemberCalldataBlock) {
|
|
nameStr = `### ${prettyName.padEnd(80)}`;
|
|
line = `\n${offsetStr}${value}${nameStr}`;
|
|
} else {
|
|
nameStr = ` ${prettyName.padEnd(80)}`;
|
|
line = `${offsetStr}${value}${nameStr}`;
|
|
}
|
|
}
|
|
|
|
for (let j = 32; j < size; j += 32) {
|
|
offsetStr = `0x${(offset + j).toString(16)}`.padEnd(10, ' ');
|
|
value = ethUtil.stripHexPrefix(ethUtil.bufferToHex(block.toBuffer().slice(j, j + 32))).padEnd(74);
|
|
nameStr = ' '.repeat(40);
|
|
|
|
line = `${line}\n${offsetStr}${value}${nameStr}`;
|
|
}
|
|
|
|
// Append to hex value
|
|
hexValue = `${hexValue}\n${line}`;
|
|
offset += size;
|
|
}
|
|
|
|
return hexValue;
|
|
}
|
|
|
|
private generateCondensedHexString(): string {
|
|
let selectorBuffer = ethUtil.toBuffer(this.selector);
|
|
if (this.root === undefined) {
|
|
throw new Error('expected root');
|
|
}
|
|
|
|
const valueQueue = this.createQueue(this.root);
|
|
const valueBufs: Buffer[] = [selectorBuffer];
|
|
let block: CalldataBlock | undefined;
|
|
while ((block = valueQueue.pop()) !== undefined) {
|
|
valueBufs.push(block.toBuffer());
|
|
}
|
|
|
|
const combinedBuffers = Buffer.concat(valueBufs);
|
|
const hexValue = ethUtil.bufferToHex(combinedBuffers);
|
|
return hexValue;
|
|
}
|
|
|
|
public optimize() {
|
|
if (this.root === undefined) {
|
|
throw new Error('expected root');
|
|
}
|
|
|
|
const subtreesByHash: { [key: string]: DependentCalldataBlock[] } = {};
|
|
|
|
// 1. Create a queue of subtrees by hash
|
|
// Note that they are ordered the same as
|
|
const subtreeQueue = this.createQueue(this.root);
|
|
let block: CalldataBlock | undefined;
|
|
while ((block = subtreeQueue.pop()) !== undefined) {
|
|
console.log(block.getName());
|
|
|
|
if (block instanceof DependentCalldataBlock === false) continue;
|
|
const blockHashBuf = block.computeHash();
|
|
const blockHashHex = ethUtil.bufferToHex(blockHashBuf);
|
|
if (blockHashHex in subtreesByHash === false) {
|
|
subtreesByHash[blockHashHex] = [block as DependentCalldataBlock];
|
|
} else {
|
|
subtreesByHash[blockHashHex].push(block as DependentCalldataBlock);
|
|
}
|
|
}
|
|
|
|
// Iterate through trees that have the same hash and
|
|
_.each(subtreesByHash, (subtrees: DependentCalldataBlock[], hash: string) => {
|
|
if (subtrees.length === 1) return; // No optimization
|
|
// Optimize
|
|
const lastSubtree = subtrees[subtrees.length - 1];
|
|
for (let i = 0; i < subtrees.length - 1; ++i) {
|
|
subtrees[i].setAlias(lastSubtree.getDependency());
|
|
}
|
|
});
|
|
}
|
|
|
|
public toHexString(optimize: boolean = false, annotate: boolean = false): string {
|
|
if (this.root === undefined) {
|
|
throw new Error('expected root');
|
|
}
|
|
|
|
if (optimize) this.optimize();
|
|
|
|
const offsetQueue = this.createQueue(this.root);
|
|
let block: CalldataBlock | undefined;
|
|
let offset = 0;
|
|
while ((block = offsetQueue.pop()) !== undefined) {
|
|
block.setOffset(offset);
|
|
offset += block.getSizeInBytes();
|
|
}
|
|
|
|
const hexValue = annotate ? this.generateAnnotatedHexString() : this.generateCondensedHexString();
|
|
return hexValue;
|
|
}
|
|
|
|
public getSelectorHex(): string {
|
|
return this.selector;
|
|
}
|
|
|
|
public getSizeInBytes(): number {
|
|
return this.sizeInBytes;
|
|
}
|
|
|
|
public toAnnotatedString(): string {
|
|
return "";
|
|
}
|
|
|
|
public setRoot(block: MemberCalldataBlock) {
|
|
this.root = block;
|
|
this.sizeInBytes += block.getSizeInBytes();
|
|
}
|
|
|
|
public setSelector(selector: string) {
|
|
// Ensure we have a 0x prefix
|
|
if (selector.startsWith('0x')) {
|
|
this.selector = selector;
|
|
} else {
|
|
this.selector = `$0x${selector}`;
|
|
}
|
|
|
|
// The selector must be 10 characters: '0x' followed by 4 bytes (two hex chars per byte)
|
|
if (this.selector.length !== 10) {
|
|
throw new Error(`Invalid selector '${this.selector}'`);
|
|
}
|
|
this.sizeInBytes += 8;
|
|
}
|
|
}
|
|
|
|
export class RawCalldata {
|
|
private value: Buffer;
|
|
private offset: number; // tracks current offset into raw calldata; used for parsing
|
|
private selector: string;
|
|
private scopes: Queue<number>;
|
|
|
|
constructor(value: string | Buffer) {
|
|
if (typeof value === 'string' && !value.startsWith('0x')) {
|
|
throw new Error(`Expected raw calldata to start with '0x'`);
|
|
}
|
|
const valueBuf = ethUtil.toBuffer(value);
|
|
this.selector = ethUtil.bufferToHex(valueBuf.slice(0, 4));
|
|
this.value = valueBuf.slice(4); // disregard selector
|
|
this.offset = 0;
|
|
this.scopes = new Queue<number>();
|
|
this.scopes.push(0);
|
|
}
|
|
|
|
public popBytes(lengthInBytes: number): Buffer {
|
|
const value = this.value.slice(this.offset, this.offset + lengthInBytes);
|
|
this.setOffset(this.offset + lengthInBytes);
|
|
return value;
|
|
}
|
|
|
|
public popWord(): Buffer {
|
|
const wordInBytes = 32;
|
|
return this.popBytes(wordInBytes);
|
|
}
|
|
|
|
public popWords(length: number): Buffer {
|
|
const wordInBytes = 32;
|
|
return this.popBytes(length * wordInBytes);
|
|
}
|
|
|
|
public readBytes(from: number, to: number): Buffer {
|
|
const value = this.value.slice(from, to);
|
|
return value;
|
|
}
|
|
|
|
public setOffset(offsetInBytes: number) {
|
|
this.offset = offsetInBytes;
|
|
console.log('0'.repeat(100), this.offset);
|
|
}
|
|
|
|
public startScope() {
|
|
this.scopes.pushFront(this.offset);
|
|
}
|
|
|
|
public endScope() {
|
|
this.scopes.pop();
|
|
}
|
|
|
|
public getOffset(): number {
|
|
return this.offset;
|
|
}
|
|
|
|
public toAbsoluteOffset(relativeOffset: number) {
|
|
const scopeOffset = this.scopes.peek();
|
|
if (scopeOffset === undefined) {
|
|
throw new Error(`Tried to access undefined scope.`);
|
|
}
|
|
const absoluteOffset = relativeOffset + scopeOffset;
|
|
return absoluteOffset;
|
|
}
|
|
|
|
public getSelector(): string {
|
|
return this.selector;
|
|
}
|
|
} |