SmartContract/Native: implement adjustable opcode pricing#2045
SmartContract/Native: implement adjustable opcode pricing#2045erikzhang merged 44 commits intoneo-project:masterfrom
Conversation
This defines a new Policy contract setting, BaseExecFee which contains a price of basic VM execution unit --- NOP instruction. It then redefines all opcode prices as coefficients applied to that base fee. The following invariants hold for this: * transaction fees are expressed in GAS * BaseExecFee is expressed in GAS * opcode price is a coefficient now * syscall price is expressed in GAS * ApplicationEngine.AddGas() also accepts GAS values, so it's a caller's responsibility to calculate that in whatever fashion he likes. Caveats: * while coefficients are based on table from neo-project#1875 (which is built on instruction execution time relations for two complete VM implementations), some modifications were applied: - it's impossible for SYSCALL to have non-0 cost now (tests just hang) - all slot operations have the same price * block and consensus payloads are adjusted to use BaseExecFee, but probably ECDsaVerifyPrice is still a constant * it's not really tested other than unit tests In general, this allows to define any execution cost in NOPs and then use BaseExecFee to get the price. The same principle could be applied to storage pricing based on FeePerByte (StoragePrice could just be `100 * FeePerByte`), but that's a bit different topic. Closes neo-project#1875.
ECDsaVerifyPrice is still a constant and it dominates here.
Concentrate on adjustability for now.
| @@ -62,6 +62,7 @@ public class MemoryPool : IReadOnlyCollection<Transaction> | |||
|
|
|||
There was a problem hiding this comment.
Not only price of opcode, but also price of interop services should be included.
| @@ -256,7 +256,6 @@ public void FeeIsSignatureContractDetailed() | |||
| verificationGas += engine.GasConsumed; | |||
| } | |||
There was a problem hiding this comment.
Should add UT for cases where fee ratio is changed.
| @@ -62,6 +62,7 @@ public class MemoryPool : IReadOnlyCollection<Transaction> | |||
|
|
|||
| private int _maxTxPerBlock; | |||
There was a problem hiding this comment.
Should taken into account various hard coded verification prices.
There was a problem hiding this comment.
As it is right now, it looks a good and clean PR, @roman-khimov, that is a good feature and will make maintenance simpler as well as increase protection of the network.
Use currentShapshot we already have.
|
Conflicts |
…rices Resolve opcode price conflict (neo-project#2020).
|
Fixed. |
|
Let's move on. |
MemoryPool contents is always valid (verified) against some snapshot. This snapshot is only changed when new block is added. Between blocks we only have one valid chain state that can be read by multiple threads without any issues, thus we can execute concurrently not only state-independent, but also state-dependent parts of transaction verification. To simplify execution flow (minimize single-threaded Blockchain message handling and eliminate duplicate DB accesses for ContainsTransaction) TransactionRouter is an independent entity now, though of course we can also attach the same actor functionality to MemoryPool itself. TransactionVerificationContext balance checking is moved to MemoryPool from Transaction with this, Transaction shouldn't care (it can check overall GAS balance though). This is directly related to neo-project#2045 work (it solves state access problem there) and in some sense is an alternative to neo-project#2054 (makes fee calculation easier, though IsStandardContract() trick to optimize out these contracts during reverification is still relevant and can be added here). At this stage it's just a prototype, some additional optimizations and simplifications are possible of course, but this prototype shows the direction and the main question for now is whether this direction is interesting for us.
| [OpCode.PUSHINT8] = 1 << 0, | ||
| [OpCode.PUSHINT16] = 1 << 0, | ||
| [OpCode.PUSHINT32] = 1 << 0, | ||
| [OpCode.PUSHINT64] = 1 << 0, |
There was a problem hiding this comment.
These PUSHes (as well as PUSHM1..PUSH16) do cost more than NOP, I'd suggest 1<<2 here based on data we have (and it's very similar for two implementations with real costs around 4-5 NOPs).
| [OpCode.JMPIF_L] = 1 << 1, | ||
| [OpCode.JMPIFNOT] = 1 << 1, | ||
| [OpCode.JMPIFNOT_L] = 1 << 1, | ||
| [OpCode.JMPEQ] = 1 << 1, |
There was a problem hiding this comment.
JMPs with embedded comparisons also reliably use more execution time than simple JMPs, not a lot, but enough to justify 1<<2 vs 1<<1.
There was a problem hiding this comment.
Should be good for JMPEQ, JMPEQ_L, JMPNE and JMPNE_L
| [OpCode.ISNULL] = 60, | ||
| [OpCode.ISTYPE] = 60, | ||
| [OpCode.CONVERT] = 80000, | ||
| [OpCode.DEPTH] = 1 << 1, |
There was a problem hiding this comment.
It's similar to PUSHINT, so if we're to change them, it needs to be changed also.
| [OpCode.DEPTH] = 1 << 1, | ||
| [OpCode.DROP] = 1 << 1, | ||
| [OpCode.NIP] = 1 << 1, | ||
| [OpCode.XDROP] = 1 << 4, |
There was a problem hiding this comment.
I'd suggest 1<<6 here, there are edge cases for it.
There was a problem hiding this comment.
I think 10 to 20 times is OK
| [OpCode.ROLL] = 1 << 4, | ||
| [OpCode.REVERSE3] = 1 << 1, | ||
| [OpCode.REVERSE4] = 1 << 1, | ||
| [OpCode.REVERSEN] = 1 << 4, |
There was a problem hiding this comment.
Suggesting at least 1 << 5 here, N can be quite big.
| [OpCode.TUCK] = 1 << 1, | ||
| [OpCode.SWAP] = 1 << 1, | ||
| [OpCode.ROT] = 1 << 1, | ||
| [OpCode.ROLL] = 1 << 4, |
There was a problem hiding this comment.
It has the same problem as XDROP.
|
Just to get things clear, we can argue about specific prices for a long time, but that won't bring us closer to preview4, so I'm OK with almost any values just to be done with this release. Still, we do have a lot of real performance data for these opcodes and we should take that into account, because if we don't do it now, some well-known disproportions will be hardcoded for eternity. The most important of them to me is |
erikzhang
left a comment
There was a problem hiding this comment.
We can merge it first and adjust the price value in the future if necessary.
erikzhang
left a comment
There was a problem hiding this comment.
I have updated some price values.
| [ContractMethod(0_03000000, CallFlags.WriteStates)] | ||
| private bool SetExecFeeFactor(ApplicationEngine engine, uint value) | ||
| { | ||
| if (value == 0 || value > MaxExecFeeFactor) throw new ArgumentOutOfRangeException(nameof(value)); |
There was a problem hiding this comment.
Maybe if we allow 0 (checking the magic), we can configure a free private network easier.
There was a problem hiding this comment.
It is too dangerous. Maybe a minimum value should be added to protocol.json.
There was a problem hiding this comment.
This certainly can be done later if needed.
This defines a new Policy contract setting, BaseExecFee which contains the price
of basic VM execution unit --- NOP instruction. It then redefines all opcode
prices as coefficients applied to that base fee.
The following invariants hold for this:
responsibility to calculate that in whatever fashion he likes.
Caveats:
instruction execution time relations for two complete VM implementations),
some modifications were applied:
ECDsaVerifyPrice is still a constant
In general, this allows to define any execution cost in NOPs and then use
BaseExecFee to get the price. The same principle could be applied to storage
pricing based on FeePerByte (StoragePrice could just be
100 * FeePerByte),but that's a bit different topic.
Closes #1875.