Implement a Pseudo-Random Number Generator in 26 bytes smart contract
I've been following Decentralized Random Number Generator (DRNG) topic since 2015. It's a long and joyfully journey since I have time at the R&D department to research and get involved in development. Now, it becomes a force of habit to read the new papers and see how people prove their approach.
I've done some experiments with Ethereum Virtual Machine Opcode recently and an idea kicked in, how to write a minimal smart contract that performs Pseudo-Random Number Generator (PRNG). The idea is pretty simple, If you're using a hash function over a static value in n
times you will receive different results since its collision resistance property. You might have some more detail here, http://www.ijcee.org/papers/439-JE503.pdf. They proved and passed the given static test by NIST.
I propose a deadly simple idea, where we maintain an immortal
value by performing a hash on a combination of previous immortal
and 32nd older block hash.
Immortal := keccak256(Immortal XOR blockhash(block.number - 32))
What is the point here?
In the idea from above apparently, the result is predictable since the algorithm and inputs are publishing on the blockchain. Anyone could able to perform off-chain computation. The thing is they can't predict how many times the other smart contract would trigger this contract. It made the cost to precalculate overwhelming transaction's value.
To actually manipulate the result miner would able to perform mine selected transactions by a given order but in the exchange, they need to risk their coinbase reward.
In case the adversary isn't a miner, they would able to have an advantage by creating a bunch of transactions by sequence with the highest gasPrice
to make sure that they can occupy a range of values.
How could this proposal help?
The algorithm is safe and acceptable for a wide range of PRNG applications and situations where the cost to manipulate outweigh the transaction value.
We could use the PRNG as an additional salt to improve the result which was provided by the oracle or commit scheme. Oracle and commit scheme work as a black box on blockchain to maintain unpredictable property of randomness meanwhile PRNG improve trustworthiness.
Randomness := RPNGResult XOR OracleResult
Implementation
EVM is quite a simple virtual machine, it has a stack with 1024 in the depth. Of course, it has memory and storage. The number of opcodes isn’t too much.
In the attempt to write a pure opcode smart contract, I wrote my own assembler to compile from assembly to EVM opcode.
Here is a file that contained the list of opcodes:
We define following this guideline:
My assembler:
Implement:
; EVM Assembly The Divine by Chiro Hiro <chiro8x@gmail.com>PUSH1 0x20
RETURNDATASIZE
CALLER
ORIGIN
XOR
PUSH1 0x0a
JUMPI
REVERT
JUMPDEST
DUP2
DUP2
DUP1
SLOAD
DUP3
NUMBER
SUB
BLOCKHASH
XOR
DUP2
MSTORE
SHA3
DUP2
SSTORE
RETURN
PUSH1 0x20
push0x20
to the top of the stackRETURNDATASIZE
push0x00
to stack, because we didn’t perform any call then its result is0x00
. It’s cheaper thanPUSH1 0x00
CALLER
pushmsg.sender
to sackORIGIN
pushtx.origin
to stackXOR
consume two items on the top of the stack and pushing the resultmsg.sender xor tx.origin
to stackPUSH1 0x0a
pushJUMPDEST
to stackJUMPI
consume two items on the top of the stack, ifmsg.sender == tx.origin
doREVERT
otherwise, jump toJUMPDEST
DUP2
duplicate the 2nd item in the stackDUP2
duplicate the 2nd item in the stackDUP1
duplicate the item on the top stack, our stack would look like this[0x00,0x00,0x20,0x00,0x20]
, we assume that the top of the stack in the left.SLOAD
load storage at index0x00
returnimmortal
on the top of the stackDUP3
we usedDUP3
to duplicate0x20
sinceBLOCKHASH
can’t perform with current block, the idea is we perform it on 32nd older block.NUMBER
return current block heightblock.number
to stackSUB
consume two value on the top of the stack then pushblock.number - 32
to stackBLOCKHASH
get block hash ofblock.number - 32
push to stack,blockhash
andimmortal
are on the top of the stackXOR
EVM has wordsize is of 256 bits so we better use XOR to combine two 256 bits values instead of performing con cat in the memory. After this stepblockhash xor imomrtal
will be on the top of the stack.DUP2
will duplicate0x00
, now0x00
is on top of the stackMSTORE
assign memorymemory[0, 32] = blockhash xor imomrtal
, stack right now[0x00,0x20,0x00,0x20]
SHA3
compute the digest of value in the memory at[0x00:0x20]
then push the result to the stackDUP2
duplicate0x00
value in stack, the stack is[0x00,keccak256(blockhash xor immortal),0x00,0x20]
SSTORE
consume two items on the top of the stack and overwriteimmortal
at0x00
RETURN
consume two last items in the stack and leave.
When we run the assembler, the result of compile the assembly is shown:
Output:
60203d333218600a57fd5b8181805482430340188152208155f3Tx deploy data:
601a803d90600a8239f360203d333218600a57fd5b8181805482430340188152208155f3
Conclusion
- We prevent normal address to trigger PRNG
- Code was optimized to minimal gas consumption
- Each call cost around
5190
gas, mostly fromSLOAD
andSSTORE
- This code work on any EVM
- The more the other contracts use this PRNG the less chance for an adversary to predict or manipulate the result
Source code available here: