diff --git a/compile.go b/compile.go index baa8483..9594add 100644 --- a/compile.go +++ b/compile.go @@ -109,7 +109,7 @@ CodeLoop: continue CodeLoop case Inverted: - toInvert := opCode(op) + toInvert := types.OpCode(op) // All DUP have the same upper nibble 0x8 and SWAP have 0x9. base := toInvert & 0xf0 if base != vm.DUP1 && base != vm.SWAP1 { @@ -117,7 +117,7 @@ CodeLoop: } offset := toInvert - base - last := opCode(min(16, stackDepth)) + last := types.OpCode(min(16, stackDepth)) if base == SWAP1 { last-- } @@ -127,8 +127,8 @@ CodeLoop: use = base + last - offset - 1 - if b := use.(opCode) & 0xf0; b != base { - panic(fmt.Sprintf("BUG: bad inversion %v -> %v", vm.OpCode(op), vm.OpCode(use.(opCode)))) + if b := use.(types.OpCode) & 0xf0; b != base { + panic(fmt.Sprintf("BUG: bad inversion %v -> %v", vm.OpCode(op), vm.OpCode(use.(types.OpCode)))) } case PUSHJUMPDEST: diff --git a/examples_test.go b/examples_test.go index 76afc94..6ef4e37 100644 --- a/examples_test.go +++ b/examples_test.go @@ -33,14 +33,9 @@ func Example_helloWorld() { // Hello world } -func ExampleCode_wellKnown() { - // This example demonstrates some well-known bytecode examples implemented - // with `specops`: - // - // - EIP-1167 Minimal Proxy Contract - // - 0age/metamorphic Metamorphic contract constructor https://github.com/0age/metamorphic/blob/55adac1d2487046002fc33a5dff7d669b5419a3a/contracts/MetamorphicContractFactory.sol#L55 - // - // The compiled bytecode is identical to the originals. +func ExampleCode_eip1167() { + // Demonstrates verbatim recreation of EIP-1167 Minimal Proxy Contract and a + // modern equivalent with PUSH0. impl := common.HexToAddress("bebebebebebebebebebebebebebebebebebebebe") eip1167 := Code{ @@ -71,7 +66,54 @@ func ExampleCode_wellKnown() { RETURN, } - metamorphic := Code{ + // Using PUSH0, here is a modernised version of EIP-1167, reduced by 1 byte + // and easy to read. + eip1167Modern := Code{ + Fn(CALLDATACOPY, PUSH0, PUSH0, CALLDATASIZE), + Fn(DELEGATECALL, GAS, PUSH(impl), PUSH0, CALLDATASIZE, PUSH0, PUSH0), + stack.ExpectDepth(1), // `success` + Fn(RETURNDATACOPY, PUSH0, PUSH0, RETURNDATASIZE), + + stack.ExpectDepth(1), // unchanged + PUSH0, RETURNDATASIZE, // prepare for the REVERT/RETURN; these are in "human" order because of the next SWAP + Inverted(SWAP1), // bring `success` from the bottom + Fn(JUMPI, PUSH("return")), + + Fn(REVERT, stack.ExpectDepth(2)), + + JUMPDEST("return"), + Fn(RETURN, stack.SetDepth(2)), + } + + for _, eg := range []struct { + name string + code Code + }{ + {"EIP-1167", eip1167}, + {"Modernised EIP-1167", eip1167Modern}, + } { + bytecode, err := eg.code.Compile() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%19s: %#x\n", eg.name, bytecode) + } + + // Output: + // + // EIP-1167: 0x363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 + // Modernised EIP-1167: 0x365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3 +} + +func ExampleCode_metamorphic0ageVerbose() { + // Demonstrates verbatim recreation of 0age's metamorphic contract + // constructor: https://github.com/0age/metamorphic/blob/55adac1d2487046002fc33a5dff7d669b5419a3a/contracts/MetamorphicContractFactory.sol#L55 + // + // Using stack.Transform() automation we also see how the size could have + // been reduced. Granted, only by a single byte, but it also saves a lot of + // development time. + + metamorphicPrelude := Code{ // 0age uses PC to place a 0 on the bottom of the stack and then // duplicates it as necessary. Using `Inverted(DUP1)` makes this // much easier to reason about. This is especially so when @@ -111,37 +153,53 @@ func ExampleCode_wellKnown() { Fn(MLOAD, Inverted(DUP1) /*0*/), // [0, fail?, addr] Fn(EXTCODESIZE, DUP1), // DUP1 as a single argument is like a stack peek - DUP1, // [0, fail?, addr, size, size] - Fn(EXTCODECOPY, SWAP3, SWAP2, DUP1, SWAP4), // TODO: can the arguments be simplified with `Inverted` equivalents? - RETURN, } - // Using PUSH0, here is a modernised version of EIP-1167, reduced by 1 byte - // and easy to read. - eip1167Modern := Code{ - Fn(CALLDATACOPY, PUSH0, PUSH0, CALLDATASIZE), - Fn(DELEGATECALL, GAS, PUSH(impl), PUSH0, CALLDATASIZE, PUSH0, PUSH0), - stack.ExpectDepth(1), // `success` - Fn(RETURNDATACOPY, PUSH0, PUSH0, RETURNDATASIZE), + // For reference, a snippet from 0age's comments to explain the stack + // transformation that now occurs. + // + // * ** get extcodesize on fourth stack item for extcodecopy ** + // * 18 3b extcodesize [0, 0, address, size] <> + // ... + // ... + // * 23 92 swap3 [size, 0, size, 0, 0, address] <> - stack.ExpectDepth(1), // unchanged - PUSH0, RETURNDATASIZE, // prepare for the REVERT/RETURN; these are in "human" order because of the next SWAP - Inverted(SWAP1), // bring `success` from the bottom - Fn(JUMPI, PUSH("return")), + // The stack as it currently stands, labelled top to bottom. + const ( + size = iota + address + callFailed // presumably zero + zero - Fn(REVERT, stack.ExpectDepth(2)), + depth + ) - JUMPDEST("return"), - Fn(RETURN, stack.SetDepth(2)), + metamorphic := Code{ + metamorphicPrelude, + stack.Transform(depth)(address, zero, zero, size, callFailed, size).WithOps( + // The exact opcodes from the original, which the compiler will + // confirm as having the intended result. + DUP1, SWAP4, DUP1, SWAP2, SWAP3, + ), + stack.ExpectDepth(6), + EXTCODECOPY, + RETURN, + } + + autoMetamorphic := Code{ + metamorphicPrelude, + stack.Transform(depth)(address, zero, zero, size, callFailed, size), + stack.ExpectDepth(6), + EXTCODECOPY, + RETURN, } for _, eg := range []struct { name string code Code }{ - {"EIP-1167", eip1167}, - {"Modernised EIP-1167", eip1167Modern}, - {"0age/metamorphic", metamorphic}, + {" 0age/metamorphic", metamorphic}, + {"Auto stack transformation", autoMetamorphic}, } { bytecode, err := eg.code.Compile() if err != nil { @@ -152,9 +210,65 @@ func ExampleCode_wellKnown() { // Output: // - // EIP-1167: 0x363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3 - // Modernised EIP-1167: 0x365f5f375f5f365f73bebebebebebebebebebebebebebebebebebebebe5af43d5f5f3e5f3d91602a57fd5bf3 - // 0age/metamorphic: 0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3 + // 0age/metamorphic: 0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3 + // Auto stack transformation: 0x5860208158601c335a63aaf10f428752fa158151803b928084923cf3 +} + +func ExampleCode_metamorphic0ageClean() { + // Identical to the other metamorphic example, but with explanatory comments + // removed to demonstrate succinct but readable production usage. + + const zero = Inverted(DUP1) // see first opcode + + metamorphic := Code{ + // Keep a zero at the bottom of the stack + PC, + // Prepare a STATICCALL signature + Fn( /*STATICCALL*/ GAS, CALLER, PUSH(28), PC /*4*/, zero, PUSH(32)), + + Fn(MSTORE, zero, PUSHSelector("getImplementation()")), // stack unchanged + + Fn(ISZERO, STATICCALL), // consumes all values except the zero + stack.ExpectDepth(2), // [0, fail?] + + Fn(MLOAD, zero), // [0, fail?, addr] + Fn(EXTCODESIZE, DUP1), // [0, fail?, addr, size] + } + + { + // Current stack, top to bottom + const ( + size = iota + address + callFailed // presumed to be 0 + zero + + depth + ) + metamorphic = append( + metamorphic, + stack.Transform(depth)( + /*EXTCODECOPY*/ address, zero, zero, size, + /*RETURN*/ callFailed /*0*/, size, + ).WithOps( + // In reality we wouldn't override the ops, but let the + // stack.Transformation find an optimal path. + DUP1, SWAP4, DUP1, SWAP2, SWAP3, + ), + EXTCODECOPY, + RETURN, + ) + } + + bytecode, err := metamorphic.Compile() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%#x", bytecode) + + // Output: + // + // 0x5860208158601c335a63aaf10f428752fa158151803b80938091923cf3 } func ExampleCode_monteCarloPi() { diff --git a/internal/opcopy/main.go b/internal/opcopy/main.go index 8bc6a9e..67918f4 100644 --- a/internal/opcopy/main.go +++ b/internal/opcopy/main.go @@ -69,12 +69,16 @@ func run() error { // GENERATED CODE - DO NOT EDIT // -import "github.com/ethereum/go-ethereum/core/vm" +import ( + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/solidifylabs/specops/types" +) // Aliases of all regular vm.OpCode constants that don't have "special" replacements. const ( {{- range .}}{{if not .Special}} - {{.Op.String}} = opCode(vm.{{.Op.String}}) + {{.Op.String}} = types.OpCode(vm.{{.Op.String}}) {{- end}}{{end}} ) diff --git a/opcodes.gen.go b/opcodes.gen.go index f0574e6..fd779b6 100644 --- a/opcodes.gen.go +++ b/opcodes.gen.go @@ -4,126 +4,130 @@ package specops // GENERATED CODE - DO NOT EDIT // -import "github.com/ethereum/go-ethereum/core/vm" +import ( + "github.com/ethereum/go-ethereum/core/vm" + + "github.com/solidifylabs/specops/types" +) // Aliases of all regular vm.OpCode constants that don't have "special" replacements. const ( - STOP = opCode(vm.STOP) - ADD = opCode(vm.ADD) - MUL = opCode(vm.MUL) - SUB = opCode(vm.SUB) - DIV = opCode(vm.DIV) - SDIV = opCode(vm.SDIV) - MOD = opCode(vm.MOD) - SMOD = opCode(vm.SMOD) - ADDMOD = opCode(vm.ADDMOD) - MULMOD = opCode(vm.MULMOD) - EXP = opCode(vm.EXP) - SIGNEXTEND = opCode(vm.SIGNEXTEND) - LT = opCode(vm.LT) - GT = opCode(vm.GT) - SLT = opCode(vm.SLT) - SGT = opCode(vm.SGT) - EQ = opCode(vm.EQ) - ISZERO = opCode(vm.ISZERO) - AND = opCode(vm.AND) - OR = opCode(vm.OR) - XOR = opCode(vm.XOR) - NOT = opCode(vm.NOT) - BYTE = opCode(vm.BYTE) - SHL = opCode(vm.SHL) - SHR = opCode(vm.SHR) - SAR = opCode(vm.SAR) - KECCAK256 = opCode(vm.KECCAK256) - ADDRESS = opCode(vm.ADDRESS) - BALANCE = opCode(vm.BALANCE) - ORIGIN = opCode(vm.ORIGIN) - CALLER = opCode(vm.CALLER) - CALLVALUE = opCode(vm.CALLVALUE) - CALLDATALOAD = opCode(vm.CALLDATALOAD) - CALLDATASIZE = opCode(vm.CALLDATASIZE) - CALLDATACOPY = opCode(vm.CALLDATACOPY) - CODESIZE = opCode(vm.CODESIZE) - CODECOPY = opCode(vm.CODECOPY) - GASPRICE = opCode(vm.GASPRICE) - EXTCODESIZE = opCode(vm.EXTCODESIZE) - EXTCODECOPY = opCode(vm.EXTCODECOPY) - RETURNDATASIZE = opCode(vm.RETURNDATASIZE) - RETURNDATACOPY = opCode(vm.RETURNDATACOPY) - EXTCODEHASH = opCode(vm.EXTCODEHASH) - BLOCKHASH = opCode(vm.BLOCKHASH) - COINBASE = opCode(vm.COINBASE) - TIMESTAMP = opCode(vm.TIMESTAMP) - NUMBER = opCode(vm.NUMBER) - DIFFICULTY = opCode(vm.DIFFICULTY) - GASLIMIT = opCode(vm.GASLIMIT) - CHAINID = opCode(vm.CHAINID) - SELFBALANCE = opCode(vm.SELFBALANCE) - BASEFEE = opCode(vm.BASEFEE) - BLOBHASH = opCode(vm.BLOBHASH) - BLOBBASEFEE = opCode(vm.BLOBBASEFEE) - POP = opCode(vm.POP) - MLOAD = opCode(vm.MLOAD) - MSTORE = opCode(vm.MSTORE) - MSTORE8 = opCode(vm.MSTORE8) - SLOAD = opCode(vm.SLOAD) - SSTORE = opCode(vm.SSTORE) - JUMP = opCode(vm.JUMP) - JUMPI = opCode(vm.JUMPI) - PC = opCode(vm.PC) - MSIZE = opCode(vm.MSIZE) - GAS = opCode(vm.GAS) - TLOAD = opCode(vm.TLOAD) - TSTORE = opCode(vm.TSTORE) - MCOPY = opCode(vm.MCOPY) - PUSH0 = opCode(vm.PUSH0) - DUP1 = opCode(vm.DUP1) - DUP2 = opCode(vm.DUP2) - DUP3 = opCode(vm.DUP3) - DUP4 = opCode(vm.DUP4) - DUP5 = opCode(vm.DUP5) - DUP6 = opCode(vm.DUP6) - DUP7 = opCode(vm.DUP7) - DUP8 = opCode(vm.DUP8) - DUP9 = opCode(vm.DUP9) - DUP10 = opCode(vm.DUP10) - DUP11 = opCode(vm.DUP11) - DUP12 = opCode(vm.DUP12) - DUP13 = opCode(vm.DUP13) - DUP14 = opCode(vm.DUP14) - DUP15 = opCode(vm.DUP15) - DUP16 = opCode(vm.DUP16) - SWAP1 = opCode(vm.SWAP1) - SWAP2 = opCode(vm.SWAP2) - SWAP3 = opCode(vm.SWAP3) - SWAP4 = opCode(vm.SWAP4) - SWAP5 = opCode(vm.SWAP5) - SWAP6 = opCode(vm.SWAP6) - SWAP7 = opCode(vm.SWAP7) - SWAP8 = opCode(vm.SWAP8) - SWAP9 = opCode(vm.SWAP9) - SWAP10 = opCode(vm.SWAP10) - SWAP11 = opCode(vm.SWAP11) - SWAP12 = opCode(vm.SWAP12) - SWAP13 = opCode(vm.SWAP13) - SWAP14 = opCode(vm.SWAP14) - SWAP15 = opCode(vm.SWAP15) - SWAP16 = opCode(vm.SWAP16) - LOG0 = opCode(vm.LOG0) - LOG1 = opCode(vm.LOG1) - LOG2 = opCode(vm.LOG2) - LOG3 = opCode(vm.LOG3) - LOG4 = opCode(vm.LOG4) - CREATE = opCode(vm.CREATE) - CALL = opCode(vm.CALL) - CALLCODE = opCode(vm.CALLCODE) - RETURN = opCode(vm.RETURN) - DELEGATECALL = opCode(vm.DELEGATECALL) - CREATE2 = opCode(vm.CREATE2) - STATICCALL = opCode(vm.STATICCALL) - REVERT = opCode(vm.REVERT) - INVALID = opCode(vm.INVALID) - SELFDESTRUCT = opCode(vm.SELFDESTRUCT) + STOP = types.OpCode(vm.STOP) + ADD = types.OpCode(vm.ADD) + MUL = types.OpCode(vm.MUL) + SUB = types.OpCode(vm.SUB) + DIV = types.OpCode(vm.DIV) + SDIV = types.OpCode(vm.SDIV) + MOD = types.OpCode(vm.MOD) + SMOD = types.OpCode(vm.SMOD) + ADDMOD = types.OpCode(vm.ADDMOD) + MULMOD = types.OpCode(vm.MULMOD) + EXP = types.OpCode(vm.EXP) + SIGNEXTEND = types.OpCode(vm.SIGNEXTEND) + LT = types.OpCode(vm.LT) + GT = types.OpCode(vm.GT) + SLT = types.OpCode(vm.SLT) + SGT = types.OpCode(vm.SGT) + EQ = types.OpCode(vm.EQ) + ISZERO = types.OpCode(vm.ISZERO) + AND = types.OpCode(vm.AND) + OR = types.OpCode(vm.OR) + XOR = types.OpCode(vm.XOR) + NOT = types.OpCode(vm.NOT) + BYTE = types.OpCode(vm.BYTE) + SHL = types.OpCode(vm.SHL) + SHR = types.OpCode(vm.SHR) + SAR = types.OpCode(vm.SAR) + KECCAK256 = types.OpCode(vm.KECCAK256) + ADDRESS = types.OpCode(vm.ADDRESS) + BALANCE = types.OpCode(vm.BALANCE) + ORIGIN = types.OpCode(vm.ORIGIN) + CALLER = types.OpCode(vm.CALLER) + CALLVALUE = types.OpCode(vm.CALLVALUE) + CALLDATALOAD = types.OpCode(vm.CALLDATALOAD) + CALLDATASIZE = types.OpCode(vm.CALLDATASIZE) + CALLDATACOPY = types.OpCode(vm.CALLDATACOPY) + CODESIZE = types.OpCode(vm.CODESIZE) + CODECOPY = types.OpCode(vm.CODECOPY) + GASPRICE = types.OpCode(vm.GASPRICE) + EXTCODESIZE = types.OpCode(vm.EXTCODESIZE) + EXTCODECOPY = types.OpCode(vm.EXTCODECOPY) + RETURNDATASIZE = types.OpCode(vm.RETURNDATASIZE) + RETURNDATACOPY = types.OpCode(vm.RETURNDATACOPY) + EXTCODEHASH = types.OpCode(vm.EXTCODEHASH) + BLOCKHASH = types.OpCode(vm.BLOCKHASH) + COINBASE = types.OpCode(vm.COINBASE) + TIMESTAMP = types.OpCode(vm.TIMESTAMP) + NUMBER = types.OpCode(vm.NUMBER) + DIFFICULTY = types.OpCode(vm.DIFFICULTY) + GASLIMIT = types.OpCode(vm.GASLIMIT) + CHAINID = types.OpCode(vm.CHAINID) + SELFBALANCE = types.OpCode(vm.SELFBALANCE) + BASEFEE = types.OpCode(vm.BASEFEE) + BLOBHASH = types.OpCode(vm.BLOBHASH) + BLOBBASEFEE = types.OpCode(vm.BLOBBASEFEE) + POP = types.OpCode(vm.POP) + MLOAD = types.OpCode(vm.MLOAD) + MSTORE = types.OpCode(vm.MSTORE) + MSTORE8 = types.OpCode(vm.MSTORE8) + SLOAD = types.OpCode(vm.SLOAD) + SSTORE = types.OpCode(vm.SSTORE) + JUMP = types.OpCode(vm.JUMP) + JUMPI = types.OpCode(vm.JUMPI) + PC = types.OpCode(vm.PC) + MSIZE = types.OpCode(vm.MSIZE) + GAS = types.OpCode(vm.GAS) + TLOAD = types.OpCode(vm.TLOAD) + TSTORE = types.OpCode(vm.TSTORE) + MCOPY = types.OpCode(vm.MCOPY) + PUSH0 = types.OpCode(vm.PUSH0) + DUP1 = types.OpCode(vm.DUP1) + DUP2 = types.OpCode(vm.DUP2) + DUP3 = types.OpCode(vm.DUP3) + DUP4 = types.OpCode(vm.DUP4) + DUP5 = types.OpCode(vm.DUP5) + DUP6 = types.OpCode(vm.DUP6) + DUP7 = types.OpCode(vm.DUP7) + DUP8 = types.OpCode(vm.DUP8) + DUP9 = types.OpCode(vm.DUP9) + DUP10 = types.OpCode(vm.DUP10) + DUP11 = types.OpCode(vm.DUP11) + DUP12 = types.OpCode(vm.DUP12) + DUP13 = types.OpCode(vm.DUP13) + DUP14 = types.OpCode(vm.DUP14) + DUP15 = types.OpCode(vm.DUP15) + DUP16 = types.OpCode(vm.DUP16) + SWAP1 = types.OpCode(vm.SWAP1) + SWAP2 = types.OpCode(vm.SWAP2) + SWAP3 = types.OpCode(vm.SWAP3) + SWAP4 = types.OpCode(vm.SWAP4) + SWAP5 = types.OpCode(vm.SWAP5) + SWAP6 = types.OpCode(vm.SWAP6) + SWAP7 = types.OpCode(vm.SWAP7) + SWAP8 = types.OpCode(vm.SWAP8) + SWAP9 = types.OpCode(vm.SWAP9) + SWAP10 = types.OpCode(vm.SWAP10) + SWAP11 = types.OpCode(vm.SWAP11) + SWAP12 = types.OpCode(vm.SWAP12) + SWAP13 = types.OpCode(vm.SWAP13) + SWAP14 = types.OpCode(vm.SWAP14) + SWAP15 = types.OpCode(vm.SWAP15) + SWAP16 = types.OpCode(vm.SWAP16) + LOG0 = types.OpCode(vm.LOG0) + LOG1 = types.OpCode(vm.LOG1) + LOG2 = types.OpCode(vm.LOG2) + LOG3 = types.OpCode(vm.LOG3) + LOG4 = types.OpCode(vm.LOG4) + CREATE = types.OpCode(vm.CREATE) + CALL = types.OpCode(vm.CALL) + CALLCODE = types.OpCode(vm.CALLCODE) + RETURN = types.OpCode(vm.RETURN) + DELEGATECALL = types.OpCode(vm.DELEGATECALL) + CREATE2 = types.OpCode(vm.CREATE2) + STATICCALL = types.OpCode(vm.STATICCALL) + REVERT = types.OpCode(vm.REVERT) + INVALID = types.OpCode(vm.INVALID) + SELFDESTRUCT = types.OpCode(vm.SELFDESTRUCT) ) // stackDeltas maps all valid vm.OpCode values to the number of values they diff --git a/specops.go b/specops.go index 3e453d4..5dc227e 100644 --- a/specops.go +++ b/specops.go @@ -15,7 +15,6 @@ import ( "math/bits" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" "github.com/holiman/uint256" @@ -53,19 +52,6 @@ func Fn(bcs ...types.Bytecoder) types.BytecodeHolder { return c } -// An opCode is a regular Ethereum vm.OpCode. It isn't exported so as to limit -// the set raw opcodes that can be used. The constants are generated by the -// opcopy binary. -type opCode vm.OpCode - -func (o opCode) Bytecode() ([]byte, error) { - return []byte{byte(o)}, nil -} - -func (o opCode) String() string { - return vm.OpCode(o).String() -} - // Raw is a Bytecoder that bypasses all compiler checks and simply appends its // contents to bytecode. It can be used for raw data, not meant to be executed. type Raw []byte diff --git a/specops_test.go b/specops_test.go index 007dabb..02106a0 100644 --- a/specops_test.go +++ b/specops_test.go @@ -114,13 +114,13 @@ func TestRunCompiled(t *testing.T) { // Starting bytecode with `n` PC opcodes results in <0 … n-1> on the stack. pcs := make(Code, 20) for i := range pcs { - pcs[i] = opCode(PC) + pcs[i] = types.OpCode(PC) } // stackTopReturner returns a contract that pushes `depth` PC values to the // stack, pulls one of them to the top with `Inverted(toInvert)`, and // returns it as a single byte. - stackTopReturner := func(depth int, toInvert opCode) Code { + stackTopReturner := func(depth int, toInvert types.OpCode) Code { return append( append(Code{ /*guarantee fresh memory*/ }, pcs[:depth]...), // <0 … 15> Inverted(toInvert), @@ -132,7 +132,7 @@ func TestRunCompiled(t *testing.T) { // DUP with smaller stack returns the nth value. for depth := 12; depth < 16; depth++ { for i := 0; i < depth; i++ { - toInvert := DUP1 + opCode(i) + toInvert := DUP1 + types.OpCode(i) tests = append(tests, test{ name: fmt.Sprintf("inverted %v with stack depth %d (<16)", toInvert, depth), code: stackTopReturner(depth, toInvert), @@ -145,7 +145,7 @@ func TestRunCompiled(t *testing.T) { // than 16 values the stack is. for depth := 16; depth <= len(pcs); depth++ { for i := 0; i < 16; i++ { - toInvert := DUP1 + opCode(i) + toInvert := DUP1 + types.OpCode(i) tests = append(tests, test{ name: fmt.Sprintf("inverted %v with stack depth %d (>=16)", toInvert, depth), code: stackTopReturner(depth, toInvert), @@ -160,7 +160,7 @@ func TestRunCompiled(t *testing.T) { // SWAP with smaller stack returns the nth value. for depth := 12; depth <= 16; depth++ { for i := 0; i < depth-1; i++ { - toInvert := SWAP1 + opCode(i) + toInvert := SWAP1 + types.OpCode(i) tests = append(tests, test{ name: fmt.Sprintf("inverted %v with stack depth %d (<16)", toInvert, depth), code: stackTopReturner(depth, toInvert), @@ -173,7 +173,7 @@ func TestRunCompiled(t *testing.T) { // than 16 values the stack is. for depth := 16; depth <= len(pcs); depth++ { for i := 0; i < 15; i++ { - toInvert := SWAP1 + opCode(i) + toInvert := SWAP1 + types.OpCode(i) tests = append(tests, test{ name: fmt.Sprintf("inverted %v with stack depth %d (>=16)", toInvert, depth), code: stackTopReturner(depth, toInvert), diff --git a/stack/BUILD.bazel b/stack/BUILD.bazel index ed16d29..6933366 100644 --- a/stack/BUILD.bazel +++ b/stack/BUILD.bazel @@ -8,7 +8,10 @@ go_library( ], importpath = "github.com/solidifylabs/specops/stack", visibility = ["//visibility:public"], - deps = ["@com_github_ethereum_go_ethereum//core/vm"], + deps = [ + "//types", + "@com_github_ethereum_go_ethereum//core/vm", + ], ) go_test( diff --git a/stack/transform.go b/stack/transform.go index 066b746..a138b76 100644 --- a/stack/transform.go +++ b/stack/transform.go @@ -1,11 +1,11 @@ package stack import ( - "errors" "fmt" "strings" "github.com/ethereum/go-ethereum/core/vm" + "github.com/solidifylabs/specops/types" ) type xFormType int @@ -19,10 +19,10 @@ const ( // A Transformation transforms the stack by modifying its order, growing, and/or // shrinking it. type Transformation struct { - typ xFormType - depth uint8 - indices []uint8 - cache string + typ xFormType + depth uint8 + indices []uint8 + override []types.OpCode } // Permute returns a Transformation that permutes the order of the stack. The @@ -54,64 +54,106 @@ func Transform(depth uint8) func(indices ...uint8) *Transformation { } } +// WithOps sets the exact opcodes that t.Bytecode() MUST return. Possible use +// cases include: +// - Caching: worst-case performance of Permute() is n! while worst-case +// Transform() may be higher. WithOps is linear in the number of ops. +// - Intent signalling: if an exact sequence of opcodes is required but they +// are opaque, the Transformation setup will inform the reader of the +// outcome. +// +// When Bytecode() is called on the returned value, it confirms that the ops +// result in the expected transformation and then returns them verbatim. +// +// WithOps modifies t and then returns it. +func (t *Transformation) WithOps(ops ...types.OpCode) *Transformation { + t.override = ops + return t +} + // Bytecode returns the stack-transforming opcodes (SWAP, DUP, etc) necessary to // achieve the transformation in the most efficient manner. func (t *Transformation) Bytecode() ([]byte, error) { - if t.cache != "" { - return t.cached() - } + var sizer func() (int, error) switch t.typ { case permutation: - return t.permute() + sizer = t.permutationSize case general: - return t.general() + sizer = t.generalSize default: return nil, fmt.Errorf("invalid %T.typ = %d", t, t.typ) } + + size, err := sizer() + if err != nil { + return nil, err + } + + if len(t.override) != 0 { + return t.overriden() + } + return t.bfs(size) } -func (t *Transformation) cached() ([]byte, error) { - return nil, errors.New("cached transformations unimplemented") +// overriden confirms that the overriding opcodes passed to t.WithOps() result +// in the expected opcode and then returns them verbatim (as bytes). +func (t *Transformation) overriden() ([]byte, error) { + n := rootNode(uint8(t.depth)) + var err error + for _, o := range t.override { + n, err = n.apply(vm.OpCode(o)) + if err != nil { + return nil, err + } + } + if got, want := n, nodeFromIndices(t.indices); got != want { + return nil, fmt.Errorf("invalid WithOps() config; transformed stack = %v; want %v", got, want) + } + + out := make([]byte, len(t.override)) + for i, o := range t.override { + out[i] = byte(o) + } + return out, nil } -// permute checks that t.indices is valid for a permutation and then returns -// t.bfs(). -func (t *Transformation) permute() ([]byte, error) { +// permutationSize confirms t.indices is valid for a permutation and then +// returns the size to be passed to bfs(). +func (t *Transformation) permutationSize() (int, error) { if n := len(t.indices); n > 16 { - return nil, fmt.Errorf("can only permute up to 16 stack items; got %d", n) + return 0, fmt.Errorf("can only permute up to 16 stack items; got %d", n) } t.depth = uint8(len(t.indices)) set := make(map[uint8]bool) for _, idx := range t.indices { if set[idx] { - return nil, fmt.Errorf("duplicate index %d in permutation %v", idx, t.indices) + return 0, fmt.Errorf("duplicate index %d in permutation %v", idx, t.indices) } set[idx] = true } for i := range t.indices { // explicitly not `_, i` like last loop if !set[uint8(i)] { - return nil, fmt.Errorf("non-contiguous indices in permutation %v; missing %d", t.indices, i) + return 0, fmt.Errorf("non-contiguous indices in permutation %v; missing %d", t.indices, i) } } - return t.bfs(len(t.indices)) + return len(t.indices), nil } -// general checks that t.depth and t.indices are valid for any transformation -// and then returns t.bfs(). -func (t *Transformation) general() ([]byte, error) { +// generalSize confirms that t.depth and t.indices are valid for any +// transformation and then returns the size to be passed to bfs(). +func (t *Transformation) generalSize() (int, error) { if t.depth > 16 { - return nil, fmt.Errorf("transformation depth %d > 16", t.depth) + return 0, fmt.Errorf("transformation depth %d > 16", t.depth) } for _, idx := range t.indices { if idx >= t.depth { - return nil, fmt.Errorf("stack index %d beyond transformation depth of %d", idx, t.depth) + return 0, fmt.Errorf("stack index %d beyond transformation depth of %d", idx, t.depth) } } - - return t.bfs(int(t.depth)) + return int(t.depth), nil } // bfs performs a breadth-first search over a graph of stack-value orders, diff --git a/stack/transform_test.go b/stack/transform_test.go index 4674c0d..8385ef1 100644 --- a/stack/transform_test.go +++ b/stack/transform_test.go @@ -11,9 +11,10 @@ import ( "github.com/ethereum/go-ethereum/core/vm" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/solidifylabs/specops" "github.com/solidifylabs/specops/evmdebug" "github.com/solidifylabs/specops/stack" + + . "github.com/solidifylabs/specops" ) func ExampleTransformation() { @@ -173,6 +174,22 @@ func TestTransformations(t *testing.T) { depth: 6, indices: []uint8{1, 3, 0, 3}, }, + { + name: "WithOps override", + fn: func(indices ...uint8) *stack.Transformation { + return stack.Transform(4)(indices...).WithOps( + // Only this is actually needed + SWAP1, + // Arbitrary, resulting in a noop + DUP2, DUP1, DUP1, SWAP1, + POP, POP, POP, + SWAP1, SWAP1, // undoes itself + ) + }, + depth: 4, + indices: []uint8{1, 0, 2, 3}, + wantNumSteps: intPtr(10), + }, } rng := rand.New(rand.NewSource(42)) @@ -211,9 +228,9 @@ func TestTransformations(t *testing.T) { t.Run(fmt.Sprintf("%s n=%d %v", tt.name, tt.depth, tt.indices), func(t *testing.T) { t.Parallel() - var code specops.Code + var code Code for i := tt.depth; i > 0; i-- { - code = append(code, specops.PUSH(i-1)) // {0 … depth-1} top to bottom + code = append(code, PUSH(i-1)) // {0 … depth-1} top to bottom } xform := tt.fn(tt.indices...) diff --git a/types/types.go b/types/types.go index 34c96d0..364db77 100644 --- a/types/types.go +++ b/types/types.go @@ -15,6 +15,20 @@ type Bytecoder interface { Bytecode() ([]byte, error) } +// An OpCode converts a standard geth OpCode into a Bytecoder that returns +// itself. +type OpCode vm.OpCode + +// Bytecode returns `[]byte{o}, nil`. +func (o OpCode) Bytecode() ([]byte, error) { + return []byte{byte(o)}, nil +} + +// String returns `vm.OpCode(o).String()`. +func (o OpCode) String() string { + return vm.OpCode(o).String() +} + // A BytecodeHolder is a concatenation of Bytecoders. type BytecodeHolder interface { Bytecoder