From 9b1f56c9687d3fffb2f59fa98d619ead90c1b910 Mon Sep 17 00:00:00 2001 From: Greg Hysen Date: Mon, 12 Nov 2018 17:10:32 -0800 Subject: Decoding works for some basic typs --- packages/order-utils/test/abi/calldata.ts | 102 ++++++++++++++++++++++- packages/order-utils/test/abi/data_type.ts | 43 +++++++++- packages/order-utils/test/abi/evm_data_types.ts | 103 +++++++++++++++++++++++- packages/order-utils/test/abi_encoder_test.ts | 40 ++++++--- 4 files changed, 274 insertions(+), 14 deletions(-) diff --git a/packages/order-utils/test/abi/calldata.ts b/packages/order-utils/test/abi/calldata.ts index 94f6a5571..0445f68ec 100644 --- a/packages/order-utils/test/abi/calldata.ts +++ b/packages/order-utils/test/abi/calldata.ts @@ -164,6 +164,12 @@ class Queue { mergeFront(q: Queue) { 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 { @@ -203,7 +209,29 @@ export class Calldata { return blockQueue; } - public toHexString(): string { + /* + + // 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) { + + } + }) + }*/ + + public toHexString(optimize: boolean = false): string { let selectorBuffer = ethUtil.toBuffer(this.selector); if (this.root === undefined) { throw new Error('expected root'); @@ -223,6 +251,8 @@ export class Calldata { valueBufs.push(block.toBuffer()); } + // if (optimize) this.optimize(valueQueue.getStore()); + const combinedBuffers = Buffer.concat(valueBufs); const hexValue = ethUtil.bufferToHex(combinedBuffers); return hexValue; @@ -259,4 +289,74 @@ export class Calldata { } 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; + + 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(); + 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; + } } \ No newline at end of file diff --git a/packages/order-utils/test/abi/data_type.ts b/packages/order-utils/test/abi/data_type.ts index 532407f52..af170f7e6 100644 --- a/packages/order-utils/test/abi/data_type.ts +++ b/packages/order-utils/test/abi/data_type.ts @@ -1,4 +1,4 @@ -import { Calldata, CalldataBlock, PayloadCalldataBlock, DependentCalldataBlock, MemberCalldataBlock } from "./calldata"; +import { RawCalldata, Calldata, CalldataBlock, PayloadCalldataBlock, DependentCalldataBlock, MemberCalldataBlock } from "./calldata"; import { MethodAbi, DataItem } from 'ethereum-types'; import { BigNumber } from '@0x/utils'; import ethUtil = require('ethereumjs-util'); @@ -18,6 +18,7 @@ export abstract class DataType { } public abstract generateCalldataBlock(value: any, parentBlock?: CalldataBlock): CalldataBlock; + public abstract generateValue(calldata: RawCalldata): any; public abstract encode(value: any, calldata: Calldata): void; public abstract getSignature(): string; public abstract isStatic(): boolean; @@ -46,12 +47,18 @@ export abstract class PayloadDataType extends DataType { // calldata.setRoot(block); } + public generateValue(calldata: RawCalldata): any { + const value = this.decodeValue(calldata); + return value; + } + public isStatic(): boolean { // If a payload has a constant size then it's static return this.hasConstantSize; } public abstract encodeValue(value: any): Buffer; + public abstract decodeValue(calldata: RawCalldata): any; } export abstract class DependentDataType extends DataType { @@ -81,6 +88,17 @@ export abstract class DependentDataType extends DataType { //calldata.setRoot(block); } + public generateValue(calldata: RawCalldata): any { + const destinationOffsetBuf = calldata.popWord(); + const currentOffset = calldata.getOffset(); + const destinationOffsetRelative = parseInt(ethUtil.bufferToHex(destinationOffsetBuf), 16); + const destinationOffsetAbsolute = calldata.toAbsoluteOffset(destinationOffsetRelative); + calldata.setOffset(destinationOffsetAbsolute); + const value = this.dependency.generateValue(calldata); + calldata.setOffset(currentOffset); + return value; + } + public isStatic(): boolean { return true; } @@ -179,7 +197,6 @@ export abstract class MemberDataType extends DataType { methodBlock.setHeader(lenBuf); } - const memberBlocks: CalldataBlock[] = []; _.each(members, (member: DataType, idx: number) => { const block = member.generateCalldataBlock(value[idx], methodBlock); @@ -220,6 +237,28 @@ export abstract class MemberDataType extends DataType { calldata.setRoot(block); } + public generateValue(calldata: RawCalldata): any[] { + let members = this.members; + if (this.isArray && this.arrayLength === undefined) { + const arrayLengthBuf = calldata.popWord(); + const arrayLengthHex = ethUtil.bufferToHex(arrayLengthBuf); + const hexBase = 16; + const arrayLength = new BigNumber(arrayLengthHex, hexBase); + + [members,] = this.createMembersWithLength(this.getDataItem(), arrayLength.toNumber()); + } + + calldata.startScope(); + const decodedValue: any[] = []; + _.each(members, (member: DataType, idx: number) => { + let memberValue = member.generateValue(calldata); + decodedValue.push(memberValue); + }); + calldata.endScope(); + + return decodedValue; + } + protected computeSignatureOfMembers(): string { // Compute signature of members let signature = `(`; diff --git a/packages/order-utils/test/abi/evm_data_types.ts b/packages/order-utils/test/abi/evm_data_types.ts index dfa541e80..a05c29b28 100644 --- a/packages/order-utils/test/abi/evm_data_types.ts +++ b/packages/order-utils/test/abi/evm_data_types.ts @@ -4,7 +4,7 @@ import { MethodAbi, DataItem } from 'ethereum-types'; import ethUtil = require('ethereumjs-util'); -import { Calldata } from './calldata'; +import { Calldata, RawCalldata } from './calldata'; import { BigNumber } from '@0x/utils'; @@ -13,6 +13,7 @@ var _ = require('lodash'); export interface DataTypeStaticInterface { matchGrammar: (type: string) => boolean; encodeValue: (value: any) => Buffer; + // decodeValue: (value: Buffer) => [any, Buffer]; } export class Address extends PayloadDataType { @@ -38,6 +39,13 @@ export class Address extends PayloadDataType { const encodedValueBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(value), evmWordWidth); return encodedValueBuf; } + + public decodeValue(calldata: RawCalldata): string { + const paddedValueBuf = calldata.popWord(); + const valueBuf = paddedValueBuf.slice(12); + const value = ethUtil.bufferToHex(valueBuf); + return value; + } } export class Bool extends PayloadDataType { @@ -64,6 +72,17 @@ export class Bool extends PayloadDataType { const encodedValueBuf = ethUtil.setLengthLeft(ethUtil.toBuffer(encodedValue), evmWordWidth); return encodedValueBuf; } + + public decodeValue(calldata: RawCalldata): boolean { + const valueBuf = calldata.popWord(); + const valueHex = ethUtil.bufferToHex(valueBuf); + const valueNumber = new BigNumber(valueHex, 16); + let value: boolean = (valueNumber.equals(0)) ? false : true; + if (!(valueNumber.equals(0) || valueNumber.equals(1))) { + throw new Error(`Failed to decode boolean. Expected 0x0 or 0x1, got ${valueHex}`); + } + return value; + } } abstract class Number extends PayloadDataType { @@ -126,6 +145,37 @@ abstract class Number extends PayloadDataType { return valueBuf; } + public decodeValue(calldata: RawCalldata): BigNumber { + const decodedValueBuf = calldata.popWord(); + const decodedValueHex = ethUtil.bufferToHex(decodedValueBuf); + let decodedValue = new BigNumber(decodedValueHex, 16); + if (this instanceof Int) { + // Check if we're negative + const binBase = 2; + const decodedValueBin = decodedValue.toString(binBase); + if (decodedValueBin[0] === '1') { + // Negative + // Step 1/3: Invert binary value + const bitsInEvmWord = 256; + let invertedValueBin = '1'.repeat(bitsInEvmWord - decodedValueBin.length); + _.each(decodedValueBin, (bit: string) => { + invertedValueBin += bit === '1' ? '0' : '1'; + }); + const invertedValue = new BigNumber(invertedValueBin, binBase); + + // Step 2/3: Add 1 to inverted value + // The result is the two's-complement represent of the input value. + const positiveDecodedValue = invertedValue.plus(binBase); + + // Step 3/3: Invert positive value + const negativeDecodedValue = positiveDecodedValue.times(-1); + decodedValue = negativeDecodedValue; + } + } + + return decodedValue; + } + public abstract getMaxValue(): BigNumber; public abstract getMinValue(): BigNumber; } @@ -228,6 +278,13 @@ export class Byte extends PayloadDataType { return paddedValue; } + public decodeValue(calldata: RawCalldata): string { + const paddedValueBuf = calldata.popWord(); + const valueBuf = paddedValueBuf.slice(0, this.width); + const value = ethUtil.bufferToHex(valueBuf); + return value; + } + public static matchGrammar(type: string): boolean { return this.matcher.test(type); } @@ -262,6 +319,17 @@ export class Bytes extends PayloadDataType { return encodedValueBuf; } + public decodeValue(calldata: RawCalldata): string { + const lengthBuf = calldata.popWord(); + const lengthHex = ethUtil.bufferToHex(lengthBuf); + const length = parseInt(lengthHex, 16); + const wordsForValue = Math.ceil(length / 32); + const paddedValueBuf = calldata.popWords(wordsForValue); + const valueBuf = paddedValueBuf.slice(0, length); + const decodedValue = ethUtil.bufferToHex(valueBuf); + return decodedValue; + } + public getSignature(): string { return 'bytes'; } @@ -289,6 +357,18 @@ export class SolString extends PayloadDataType { return encodedValueBuf; } + public decodeValue(calldata: RawCalldata): string { + const lengthBuf = calldata.popWord(); + const lengthHex = ethUtil.bufferToHex(lengthBuf); + const length = parseInt(lengthHex, 16); + const wordsForValue = Math.ceil(length / 32); + const paddedValueBuf = calldata.popWords(wordsForValue); + const valueBuf = paddedValueBuf.slice(0, length); + console.log('LENGTH UPINYA === ', length); + const value = valueBuf.toString('ascii'); + return value; + } + public getSignature(): string { return 'string'; } @@ -415,6 +495,27 @@ export class Method extends MemberDataType { return calldata.toHexString(); } + /* + protected decodeValue(value: Buffer): any[] { + const selectorBuf = value.slice(4); + const selectorHex = ethUtil.bufferToHex(selectorBuf); + if (this.selector !== selectorHex) { + throw new Error(`Tried to decode calldata with mismatched selector. Expected '${this.selector}', got '${selectorHex}'`); + } + const remainingValue = value.slice(9); + const decodedValue = super.decodeValue(remainingValue); + return decodedValue; + }*/ + + public decode(calldata: string): any[] { + const calldata_ = new RawCalldata(calldata); + if (this.selector !== calldata_.getSelector()) { + throw new Error(`Tried to decode calldata with mismatched selector. Expected '${this.selector}', got '${calldata_.getSelector()}'`); + } + const value = super.generateValue(calldata_); + return value; + } + public getSignature(): string { return this.methodSignature; } diff --git a/packages/order-utils/test/abi_encoder_test.ts b/packages/order-utils/test/abi_encoder_test.ts index 0db5f4281..e8ebd7b98 100644 --- a/packages/order-utils/test/abi_encoder_test.ts +++ b/packages/order-utils/test/abi_encoder_test.ts @@ -310,6 +310,12 @@ describe.only('ABI Encoder', () => { const expectedCalldata = '0xf68ade72000000000000000000000000000000000000000000000000000000000000007f000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000036'; expect(calldata).to.be.equal(expectedCalldata); + + // Test decoding + const expectedDecodedValueJson = JSON.stringify(args); + const decodedValue = method.decode(calldata); + const decodedValueJson = JSON.stringify(decodedValue); + expect(decodedValueJson).to.be.equal(expectedDecodedValueJson); }); @@ -327,38 +333,52 @@ describe.only('ABI Encoder', () => { const expectedCalldata = '0x7ac2bd96af000000000000000000000000000000000000000000000000000000000000000001020304050607080911121314151617181920212223242526272829303132000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000047616161616161616161616161616161616161616161616161616161616161616161616161616161611114f3245678384756473829384756774488993384576688990020202020200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000414d79206669727374206e616d65206973204772656720616e64206d79206c617374206e616d6520697320487973656e2c207768617420646f207961206b6e6f772100000000000000000000000000000000000000000000000000000000000000'; expect(calldata).to.be.equal(expectedCalldata); - }); - it('Yessir', async () => { - const method = new AbiEncoder.Method(AbiSamples.simpleAbi); - const calldata = method.encode([new BigNumber(5), 'five']); - console.log(calldata); - expect(true).to.be.true(); + // Test decoding + const expectedDecodedValueJson = JSON.stringify(args); + const decodedValue = method.decode(calldata); + const decodedValueJson = JSON.stringify(decodedValue); + expect(decodedValueJson).to.be.equal(expectedDecodedValueJson); }); - it('Array ABI', async () => { + it.only('Array ABI', async () => { const method = new AbiEncoder.Method(AbiSamples.stringAbi); console.log(method); - const calldata = method.encode([['five', 'six', 'seven']]); + const args = [['five', 'six', 'seven']]; + const calldata = method.encode(args); console.log(method.getSignature()); console.log(method.selector); + /* console.log(calldata); const expectedCalldata = '0x13e751a900000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000046669766500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000373697800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005736576656e000000000000000000000000000000000000000000000000000000'; - expect(calldata).to.be.equal(expectedCalldata); + expect(calldata).to.be.equal(expectedCalldata);*/ + + // Test decoding + const expectedDecodedValueJson = JSON.stringify(args); + const decodedValue = method.decode(calldata); + const decodedValueJson = JSON.stringify(decodedValue); + expect(decodedValueJson).to.be.equal(expectedDecodedValueJson); }); it('Static Tuple', async () => { // This is dynamic because it has dynamic members const method = new AbiEncoder.Method(AbiSamples.staticTupleAbi); - const calldata = method.encode([[new BigNumber(5), new BigNumber(10), new BigNumber(15), false]]); + const args = [[new BigNumber(5), new BigNumber(10), new BigNumber(15), false]]; + const calldata = method.encode(args); console.log(method.getSignature()); console.log(method.selector); console.log(calldata); const expectedCalldata = '0xa9125e150000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000f0000000000000000000000000000000000000000000000000000000000000000'; expect(calldata).to.be.equal(expectedCalldata); + + // Test decoding + const expectedDecodedValueJson = JSON.stringify(args); + const decodedValue = method.decode(calldata); + const decodedValueJson = JSON.stringify(decodedValue); + expect(decodedValueJson).to.be.equal(expectedDecodedValueJson); }); it('Dynamic Tuple (Array input)', async () => { -- cgit