Skip to content

Store/deploy execution traces #602

@igormcoelho

Description

@igormcoelho

This proposal is very abstract now, but perhaps can be useful in two aspects: (1) provide a good way of achieving parallelism on existing contracts, being Native or not (2) allow users to deploy of Native Contracts or parts of it ("Execution Traces" or "Native Functions").
The idea is to store the Execution Trace over an existing (or perhaps new) contract. Over an execution trace, besides being deterministic, all branch decisions should also be pre-determined, meaning that NeoVM/NeoContract will know exactly which opcodes to be executed, only varying some input parameters. The scripthash of the trace could consist of the set of operations executed on the contract (for specific calls of interest), and a binary could be automatically generated and cached on nodes, after trace creation is requested (perhaps someone could pay a price to "deploy" that, don't know yet...). The price payed by the user executing the trace could be the same as executed without it (the only advantage is faster execution), or a discounted/fixed price to incentive this model.

Consider this example (if x is more than 10, double it, else subtracts one):

        public static int Twice(int x) {
            return x*2;
        }
        
        public static int Minus1(int x) {
            return x-1;
        }
        
        public static int Main(int x) {
            if(x > 10) return Twice(x);
            else return Minus1(x);
        }

It has the following opcodes:

51c56b6c766b00527ac46a00c35aa1630e006a00c361651200616c75666a
00c361651a00616c756651c56b6c766b00527ac46a00c35295616c756651
c56b6c766b00527ac46a00c35194616c7566
scripthash (0x8c2074abcdba9fec8a7559e6d1448fa64eda0d3d)

Meaning that, if x = 30 (and all x>10), only a few opcodes will be used (asterisk are removed):

51 PUSH1  # The number 1 is pushed onto the stack.
c5 NEWARRAY  #
6b TOALTSTACK  # Puts the input onto the top of the alt stack. Removes it from the main stack.
6c FROMALTSTACK  # Puts the input onto the top of the main stack. Removes it from the alt stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alt stack. Removes it from the main stack.
00 PUSH0  #An empty array of bytes is pushed onto the stack
52 PUSH2  # The number 2 is pushed onto the stack.
7a ROLL  # The item n back in the stack is moved to the top.
c4 SETITEM  #
6a DUPFROMALTSTACK  #
00 PUSH0  #An empty array of bytes is pushed onto the stack
c3 PICKITEM  #
5a PUSH10  # The number 10 is pushed onto the stack.
a1 LTE  # Returns 1 if a is less than or equal to b, 0 otherwise.
63 JMPIF 0e00 # 14     <============== THIS IS JUMP FOR ELSE CASE <= 10 (ALWAYS FAILS ON THIS TRACE)
6a DUPFROMALTSTACK  # <========== CODE ALWAYS COMES HERE ON THIS TRACE
00 PUSH0  #An empty array of bytes is pushed onto the stack
c3 PICKITEM  #
61 NOP  # Does nothing.
65 CALL 1200 # 18 <============== THIS IS INVOCATION FOR CASE x>10 (ALWAYS CALLED)
61 NOP  # Does nothing. 
6c FROMALTSTACK  # Puts the input onto the top of the main stack. Removes it from the alt stack.
75 DROP  # Removes the top stack item.
66 RET  # <============ ALWAYS FINISHES HERE
* 6a DUPFROMALTSTACK  #    <============= THIS PART IS UNUSED BECAUSE OF ELSE
* 00 PUSH0  #An empty array of bytes is pushed onto the stack
* c3 PICKITEM  #
* 61 NOP  # Does nothing.
* 65 CALL 1a00 # 26
* 61 NOP  # Does nothing.
* 6c FROMALTSTACK  # Puts the input onto the top of the main stack. Removes it from the alt stack.
* 75 DROP  # Removes the top stack item.
* 66 RET  #
51 PUSH1  # <========= THIS IS THE IMPLEMENTATION OF Twice FUNCTION (ALWAYS EXECUTES)
c5 NEWARRAY  #
6b TOALTSTACK  # Puts the input onto the top of the alt stack. Removes it from the main stack.
6c FROMALTSTACK  # Puts the input onto the top of the main stack. Removes it from the alt stack.
76 DUP  # Duplicates the top stack item.
6b TOALTSTACK  # Puts the input onto the top of the alt stack. Removes it from the main stack.
00 PUSH0  #An empty array of bytes is pushed onto the stack
52 PUSH2  # The number 2 is pushed onto the stack.
7a ROLL  # The item n back in the stack is moved to the top.
c4 SETITEM  #
6a DUPFROMALTSTACK  #
00 PUSH0  #An empty array of bytes is pushed onto the stack
c3 PICKITEM  #
52 PUSH2  # The number 2 is pushed onto the stack.
95 MUL  # a is multiplied by b.
61 NOP  # Does nothing.
6c FROMALTSTACK  # Puts the input onto the top of the main stack. Removes it from the alt stack.
75 DROP  # Removes the top stack item.
66 RET  # <====== ALWAYS GO TO THIS PART TOO (END OF Twice FUNCTION)
* 51 PUSH1  # <= THIS PART IS ALSO UNUSED BECAUSE OF ELSE
* c5 NEWARRAY  #
* 6b TOALTSTACK  # Puts the input onto the top of the alt stack. Removes it from the main stack.
* 6c FROMALTSTACK  # Puts the input onto the top of the main stack. Removes it from the alt stack.
* 76 DUP  # Duplicates the top stack item.
* 6b TOALTSTACK  # Puts the input onto the top of the alt stack. Removes it from the main stack.
* 00 PUSH0  #An empty array of bytes is pushed onto the stack
* 52 PUSH2  # The number 2 is pushed onto the stack.
* 7a ROLL  # The item n back in the stack is moved to the top.
* c4 SETITEM  #
* 6a DUPFROMALTSTACK  #
* 00 PUSH0  #An empty array of bytes is pushed onto the stack
* c3 PICKITEM  #
* 51 PUSH1  # The number 1 is pushed onto the stack.
* 94 SUB  # b is subtracted from a.
* 61 NOP  # Does nothing.
* 6c FROMALTSTACK  # Puts the input onto the top of the main stack. Removes it from the alt stack.
* 75 DROP  # Removes the top stack item.
* 66 RET  #

So, on this example, trace would be: 51c56b6c766b00527ac46a00c35aa1630e006a00c36165120051c56b6c766b00527ac46a00c35295616c7566616c7566 (scripthash: 0xcab571c009a140cc7a0879592bcd3003ca3601fc).
Note that trace is smaller than avm, but if code contained loops, these loops could make trace even bigger than original avm. If any branch fails the follow the expected sequence, the trace fails and throws.

This way, we could possibly store the sequence of used opcodes as a Native Contract (or "Native Function" of a contract), and the user provides the desired trace during invocation.
Deploy Trace example: Neo.Contract.DeployTrace Contract_scripthash + trace_operations => Neo.Contract.DeployTrace 0x8c2074abcdba9fec8a7559e6d1448fa64eda0d3d 51c56b6c766b00527ac46a00c35aa1630e006a00c36165120051c56b6c766b00527ac46a00c35295616c7566616c7566.
Invocation could be the same (passing trace scripthash as execution hash): PUSH30, APPCALL 0xcab571c009a140cc7a0879592bcd3003ca3601fc (instead of APPCALL 0x8c2074abcdba9fec8a7559e6d1448fa64eda0d3d). Note that PUSH9, APPCALL 0xcab571c009a140cc7a0879592bcd3003ca3601fc would fail because of unexpected jumps (as x = 9 <= 10).

Advantage: parallel code can explore the trace the predict conflicts and if no storage-write is used, all traces free of storage-put could be done in batch parallel.
Cons: The only extra overhead is the storage of these traces, so it would be important to have a price on that (but so much less than a regular contract...).

Metadata

Metadata

Assignees

No one assigned

    Labels

    DiscussionInitial issue state - proposed but not yet acceptedFeatureType: Large changes or new featuresVMNew features that affect the Neo Virtual Machine or the Interop layer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions