diff --git a/EIPS/blockhash_refactoring.md b/EIPS/blockhash_refactoring.md index 35d3cc1faf651..c5464f63c4004 100644 --- a/EIPS/blockhash_refactoring.md +++ b/EIPS/blockhash_refactoring.md @@ -7,7 +7,7 @@ Category: Core Status: Draft Created: 2017-02-10 - + ### Summary Stores blockhashes in the state, reducing the protocol complexity and the need for client implementation complexity in order to process the BLOCKHASH opcode. Also extends the range of how far back blockhash checking can go, with the side effect of creating direct links between blocks with very distant block numbers, facilitating much more efficient initial light client syncing. @@ -41,12 +41,49 @@ If `block.number >= METROPOLIS_FORK_BLKNUM + 256`, then the BLOCKHASH opcode ins Also, for blocks where `block.number >= METROPOLIS_FORK_BLKNUM`, the gas cost is increased from 20 to 800 to reflect the higher costs of processing the algorithm in the contract code. + +### Contract model + +The contract, when called by the SYSTEM account, records provided block hashes +selectively on different levels. The levels differs by intervals (frequency) +at what the block hashes are stored. + +Let `n` be the current block number. +Let `p = n - 1` be the previous block number. +Let `h` be the block hash of the previous `p` block. +Let `B` be the base -- the number of records kept on every level. + +The levels are numbered with `k`, starting from 0. +A given level `k` of stored block hashes has the _interval_ of `B**k` blocks. + +The recursive level update formula is: + +```python +def update(k, p): + n = p - B**k # The number of the block hash to be moved. + i = (n / B**k) % B # The index of the storage slot where to move the block hash. + if i == 0: + update(k + 1, n) # Update higher level. + storage[k][i] = storage[k - 1][0] # Move the block hash from lower level. +``` + +where +- `storage[k]` is contract storage dedicated to level `k`, +- `storage[-1][0]` is `h`. + + +### Implementation parameters + +- `B` is `256`, +- `k` is max 2 (3 levels in total). + + ### BLOCKHASH_CONTRACT_CODE BLOCKHASH_CONTRACT_CODE is set to: ``` -0x73fffffffffffffffffffffffffffffffffffffffe33141561006a5760014303600035610100820755610100810715156100455760003561010061010083050761010001555b6201000081071515610064576000356101006201000083050761020001555b5061013e565b4360003512151561008457600060405260206040f361013d565b61010060003543031315156100a857610100600035075460605260206060f361013c565b6101006000350715156100c55762010000600035430313156100c8565b60005b156100ea576101006101006000350507610100015460805260206080f361013b565b620100006000350715156101095763010000006000354303131561010c565b60005b1561012f57610100620100006000350507610200015460a052602060a0f361013a565b600060c052602060c0f35b5b5b5b5b +0x73fffffffffffffffffffffffffffffffffffffffe3314156100995760014303602052610100602051076040526000604051141561008d576101006020510360605261010061010060605105076080526000608051141561008157620100006060510360a0526101006201000060a051050760c0526101005460c05161020001555b60005460805161010001555b60003560405155610171565b60003560e05260e0514313156100b557600060e05112156100b8565b60005b156101645760e051430361010052610100610100511315156100e75761010060e0510754610120526020610120f35b600061010060e0510714156101635762010100610100511315156101205761010061010060e05105076101000154610140526020610140f35b6201000060e05107151561013e576301010100610100511315610141565b60005b15610162576101006201000060e05105076102000154610160526020610160f35b5b5b6000610180526020610180f35b ``` The Serpent source code is: @@ -54,31 +91,50 @@ The Serpent source code is: ```python # Setting the block hash if msg.sender == 2**160 - 2: - with prev_block_number = block.number - 1: - # Use storage fields 0..255 to store the last 256 hashes - ~sstore(prev_block_number % 256, ~calldataload(0)) - # Use storage fields 256..511 to store the hashes of the last 256 - # blocks with block.number % 256 == 0 - if not (prev_block_number % 256): - ~sstore(256 + (prev_block_number / 256) % 256, ~calldataload(0)) - # Use storage fields 512..767 to store the hashes of the last 256 - # blocks with block.number % 65536 == 0 - if not (prev_block_number % 65536): - ~sstore(512 + (prev_block_number / 65536) % 256, ~calldataload(0)) + + # Level 0 + # Use storage fields 0..255 to store the hashes of the last 256 + # blocks. + n0 = block.number - 1 + i0 = n0 % 256 + + if i0 == 0: + # Level 1 + # Use storage fields 256..511 to store the hashes of 256 + # blocks with block.number % 256 == 0. + n1 = n0 - 256 + i1 = (n1 / 256) % 256 + + if i1 == 0: + # Level 2 + # Use storage fields 512..767 to store the hashes of 256 + # blocks with block.number % 65536 == 0. + n2 = n1 - 256*256 + i2 = (n2 / (256*256)) % 256 + # Move to be replaced record from level 1 to level 2. + ~sstore(512 + i2, ~sload(256)) + + # Move to be replaced record from level 0 to level 1. + ~sstore(256 + i1, ~sload(0)) + + # Save the provided hash of the previous block. + ~sstore(i0, ~calldataload(0)) + # Getting the block hash else: - if ~calldataload(0) >= block.number: - return(0) - elif block.number - ~calldataload(0) <= 256: - return(~sload(~calldataload(0) % 256)) - elif (not (~calldataload(0) % 256) and block.number - ~calldataload(0) <= 65536): - return(~sload(256 + (~calldataload(0) / 256) % 256)) - elif (not (~calldataload(0) % 65536) and block.number - ~calldataload(0) <= 16777216): - return(~sload(512 + (~calldataload(0) / 65536) % 256)) - else: - return(0) + number = ~calldataload(0) + if block.number > number and number >= 0: + distance = block.number - number + if distance <= 256: + return(~sload(number % 256)) + if number % 256 == 0: + if distance <= 65792: + return(~sload(256 + (number / 256) % 256)) + if (not (number % 65536) and distance <= 16843008): + return(~sload(512 + (number / 65536) % 256)) + return(0) ``` ### Rationale -This removes the need for implementaitons to have an explicit way to look into historical block hashes, simplifying the protocol definition and removing a large component of the "implied state" (information that is technically state but is not part of the state tree) and thereby making the protocol more "pure". Additionally, it allows blocks to directly point to blocks far behind them, which enables extremely efficient and secure light client protocols. +This removes the need for implementations to have an explicit way to look into historical block hashes, simplifying the protocol definition and removing a large component of the "implied state" (information that is technically state but is not part of the state tree) and thereby making the protocol more "pure". Additionally, it allows blocks to directly point to blocks far behind them, which enables extremely efficient and secure light client protocols. diff --git a/EIPS/eip-96/tests/.gitignore b/EIPS/eip-96/tests/.gitignore new file mode 100644 index 0000000000000..3333fdcd42287 --- /dev/null +++ b/EIPS/eip-96/tests/.gitignore @@ -0,0 +1,3 @@ +/*.egg-info/ +/.cache/ +/__pycache__/ diff --git a/EIPS/eip-96/tests/README.md b/EIPS/eip-96/tests/README.md new file mode 100644 index 0000000000000..084f11a1723b2 --- /dev/null +++ b/EIPS/eip-96/tests/README.md @@ -0,0 +1,5 @@ +### Usage + +1. Setup virtualenv +2. `pip install -r requirements.txt` +3. `py.test` diff --git a/EIPS/eip-96/tests/requirements.txt b/EIPS/eip-96/tests/requirements.txt new file mode 100644 index 0000000000000..36f771f8629c1 --- /dev/null +++ b/EIPS/eip-96/tests/requirements.txt @@ -0,0 +1,3 @@ +six +pytest +ethereum==1.6.1 diff --git a/EIPS/eip-96/tests/test_blockhash_contract.py b/EIPS/eip-96/tests/test_blockhash_contract.py new file mode 100644 index 0000000000000..3ca3c297affd9 --- /dev/null +++ b/EIPS/eip-96/tests/test_blockhash_contract.py @@ -0,0 +1,309 @@ +import pytest +import os +import random +from ethereum import tester, utils +from ethereum.config import default_config +from rlp.utils import decode_hex + +# Compile with serpent: +# serpent compile blockhash.se +# The extract runtime code 73fffffffffffffffffffffffffffffffffffffff...f35b +EIP_BLOCKHASH_CODE = decode_hex( + b'73fffffffffffffffffffffffffffffffffffffffe3314156100995760014303602052610100602051076040526000604051141561008d576101006020510360605261010061010060605105076080526000608051141561008157620100006060510360a0526101006201000060a051050760c0526101005460c05161020001555b60005460805161010001555b60003560405155610171565b60003560e05260e0514313156100b557600060e05112156100b8565b60005b156101645760e051430361010052610100610100511315156100e75761010060e0510754610120526020610120f35b600061010060e0510714156101635762010100610100511315156101205761010061010060e05105076101000154610140526020610140f35b6201000060e05107151561013e576301010100610100511315610141565b60005b15610162576101006201000060e05105076102000154610160526020610160f35b5b5b6000610180526020610180f35b' # noqa +) + +BLOCKHASH_ADDR = decode_hex(b'00000000000000000000000000000000000000f0') +EIP_SYSTEM_ADDR = decode_hex(b'fffffffffffffffffffffffffffffffffffffffe') + +SYSTEM_PRIV = os.urandom(32) +SYSTEM_ADDR = utils.privtoaddr(SYSTEM_PRIV) +SYSTEM_GAS_LIMIT = 1000000 +SYSTEM_GAS_PRICE = 0 +BLOCKHASH_CODE = EIP_BLOCKHASH_CODE.replace(EIP_SYSTEM_ADDR, SYSTEM_ADDR, 1) +NULL_HASH = b'\0' * 32 + +BLOCKHASH_LEVEL0_COST = 411 +BLOCKHASH_LEVEL1_COST = 496 +BLOCKHASH_LEVEL2_COST = 576 +BLOCKHASH_INVALID_COST = 140 +BLOCKHASH_INVALID1_COST = 248 +BLOCKHASH_INVALID2_COST = 335 +BLOCKHASH_INVALID_ARG_COST = 162 + +# Configure execution in pre-Metropolis mode. +default_config['HOMESTEAD_FORK_BLKNUM'] = 0 +default_config['DAO_FORK_BLKNUM'] = 0 +default_config['ANTI_DOS_FORK_BLKNUM'] = 0 +default_config['CLEARING_FORK_BLKNUM'] = 0 + + +class State(tester.state): + def exec_system(self): + """Execute BLOCKHASH contract from SYSTEM account""" + + prev_block_hash = self.block.get_parent().hash + assert len(prev_block_hash) == 32 + + gas_limit = tester.gas_limit + tester.gas_limit = SYSTEM_GAS_LIMIT + + gas_price = tester.gas_price + tester.gas_price = SYSTEM_GAS_PRICE + + output = self.send(sender=SYSTEM_PRIV, to=BLOCKHASH_ADDR, value=0, + evmdata=prev_block_hash) + assert len(output) == 0 + + tester.gas_limit = gas_limit + tester.gas_price = gas_price + + def get_slot(self, index): + """Get storage entry of BLOCKHASH_ADDR of given index""" + int_value = self.block.get_storage_data(BLOCKHASH_ADDR, index) + return utils.zpad(utils.coerce_to_bytes(int_value), 32) + + +def fake_slot(index): + value = b'BLOCKHASH slot {:17}'.format(index) + assert len(value) == 32 + return value + + +@pytest.fixture(scope='module') +def state(): + state = State() + state.block._set_acct_item(BLOCKHASH_ADDR, 'code', BLOCKHASH_CODE) + + for i in range(257): + state.mine() + state.exec_system() + + return state + + +@pytest.fixture +def fake_state(): + """Create BLOCKHASH contract state. "Mining" more than 256 blocks is not + feasible with ethereum.tester.""" + state = State() + state.block.number = 16777216 + 65536 + 256 + 1 + state.block._set_acct_item(BLOCKHASH_ADDR, 'code', BLOCKHASH_CODE) + + for i in range(3 * 256): + int_value = utils.big_endian_to_int(fake_slot(i)) + state.block.set_storage_data(BLOCKHASH_ADDR, i, int_value) + + return state + + +def test_setup(state): + assert state.block.get_code(BLOCKHASH_ADDR) == BLOCKHASH_CODE + assert state.block.get_balance(SYSTEM_ADDR) == 0 + assert state.block.get_nonce(SYSTEM_ADDR) > 0 + + assert state.get_slot(0) == state.blocks[256].hash + for i in range(1, 256): + assert state.get_slot(i) == state.blocks[i].hash + + assert state.get_slot(256) == state.blocks[0].hash + for i in range(257, 256 + 256): + assert state.get_slot(i) == NULL_HASH + + for i in range(512, 512 + 256): + assert state.get_slot(i) == NULL_HASH + + +def test_get_prev_block_hash(state): + prev = state.block.number - 1 + expected_hash = state.blocks[prev].hash + arg = utils.zpad(utils.coerce_to_bytes(prev), 32) + out = state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['output'] == expected_hash + assert out['gas'] == BLOCKHASH_LEVEL0_COST + + +def test_get_current_block_hash(state): + arg = utils.zpad(utils.coerce_to_bytes(state.block.number), 32) + out = state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['output'] == b'\0' * 32 + assert out['gas'] == BLOCKHASH_INVALID_COST + + +def test_get_future_block_hash(state): + arg = utils.zpad(utils.coerce_to_bytes(3**11 + 13), 32) + out = state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['output'] == b'\0' * 32 + assert out['gas'] == BLOCKHASH_INVALID_COST + + +def test_first256th_slot(state): + n = state.block.number + state.block.number = 60000 # Allow accessing 256th block hashes + + arg = utils.zpad(utils.coerce_to_bytes(0), 32) + out = state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['output'] == state.blocks[0].hash + assert out['gas'] == BLOCKHASH_LEVEL1_COST + + state.block.number = n + + +def test_overflow(state): + n = state.block.number + state.block.number = 1 + + arg = utils.zpad(utils.coerce_to_bytes(2**256 - 256), 32) + out = state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['output'] == NULL_HASH + assert out['gas'] == BLOCKHASH_INVALID_ARG_COST + + state.block.number = n + + +def test_fake_state_setup(fake_state): + for i in range(3 * 256): + assert fake_state.get_slot(i) == fake_slot(i) + assert fake_state.get_slot(3 * 256) == NULL_HASH + + +def test_overflow2(fake_state): + fake_state.block.number = 255 + + arg = '\xff' * 31 + out = fake_state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['output'] == NULL_HASH + assert out['gas'] == BLOCKHASH_INVALID_ARG_COST + + +def test_system_level1(fake_state): + tester.gas_price = SYSTEM_GAS_PRICE + + for n in range(256 + 1, 300 * 256 + 2, 256): + fake_state.block.number = n + arg = os.urandom(32) + slot_to_move = fake_state.get_slot(0) + out = fake_state.profile(sender=SYSTEM_PRIV, to=BLOCKHASH_ADDR, + value=0, evmdata=arg) + assert out['gas'] > 10000 # TODO + assert fake_state.get_slot(0) == arg + block_to_move = n - 1 - 256 + index = (block_to_move / 256) % 256 + print n, index + assert fake_state.get_slot(256 + index) == slot_to_move + + +def test_system_level2(fake_state): + tester.gas_price = SYSTEM_GAS_PRICE + + for n in range(65536 + 256 + 1, 300 * 65536 + 2, 65536): + fake_state.block.number = n + arg = os.urandom(32) + slot_to_move0 = fake_state.get_slot(0) + slot_to_move1 = fake_state.get_slot(256) + out = fake_state.profile(sender=SYSTEM_PRIV, to=BLOCKHASH_ADDR, + value=0, evmdata=arg) + assert out['gas'] > 5000 # TODO + assert fake_state.get_slot(0) == arg + block_to_move = n - 1 - 65536 - 256 + assert block_to_move % 65536 == 0 + index = (block_to_move / 65536) % 256 + print n, block_to_move, index + assert fake_state.get_slot(512 + index) == slot_to_move1 + assert fake_state.get_slot(256) == slot_to_move0 + + +def test_blockhash_last256(fake_state): + start_block = fake_state.block.number - 256 + for n in range(start_block, fake_state.block.number): + arg = utils.zpad(utils.coerce_to_bytes(n), 32) + out = fake_state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['gas'] == BLOCKHASH_LEVEL0_COST + assert out['output'] == fake_slot(n % 256) + + +def test_blockhash_level1(fake_state): + n = fake_state.block.number + last_block = n - (n % 256) - 256 + first_block = last_block - 255 * 256 + + for n in range(first_block, last_block + 1, 256): + arg = utils.zpad(utils.coerce_to_bytes(n), 32) + out = fake_state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['gas'] == BLOCKHASH_LEVEL1_COST + level1_offset = (n / 256) % 256 + assert out['output'] == fake_slot(256 + level1_offset) + + +def test_blockhash_level2(fake_state): + n = fake_state.block.number + last_block = n - (n % 65536) - 65536 + first_block = last_block - 255 * 65536 + + for n in range(first_block, last_block + 1, 65536): + arg = utils.zpad(utils.coerce_to_bytes(n), 32) + out = fake_state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['gas'] == BLOCKHASH_LEVEL2_COST + level2_offset = (n / 65536) % 256 + assert out['output'] == fake_slot(512 + level2_offset) + + +def test_blockhash_future_blocks(fake_state): + for n in range(fake_state.block.number, fake_state.block.number + 10): + arg = utils.zpad(utils.coerce_to_bytes(n), 32) + out = fake_state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['gas'] == BLOCKHASH_INVALID_COST + assert out['output'] == NULL_HASH + + +def test_blockhash_not_covered_blocks_level1(fake_state): + start_block = fake_state.block.number - 256 - 2 + assert start_block % 256 != 0 + for n in range(start_block, start_block - 255, -1): + arg = utils.zpad(utils.coerce_to_bytes(n), 32) + out = fake_state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + assert out['gas'] == BLOCKHASH_INVALID1_COST + assert out['output'] == NULL_HASH + + +def test_blockhash_not_covered_blocks_level2(fake_state): + start_block = fake_state.block.number - 65536 - 256 - 2 + assert start_block % 256 != 0 + for n in range(start_block, start_block - 1000, -1): + arg = utils.zpad(utils.coerce_to_bytes(n), 32) + out = fake_state.profile(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + if n % 256 == 0: + assert out['gas'] == BLOCKHASH_INVALID2_COST + else: + assert out['gas'] == BLOCKHASH_INVALID1_COST + assert out['output'] == NULL_HASH + + +def test_blockhash_not_covered_blocks_random_access(fake_state): + current_n = fake_state.block.number + for _ in range(1000): + n = random.randint(0, current_n - 256) + arg = utils.zpad(utils.coerce_to_bytes(n), 32) + output = fake_state.send(sender=tester.k1, to=BLOCKHASH_ADDR, value=0, + evmdata=arg) + if current_n - n < 65536 + 256: + if n % 256 == 0: + assert output != NULL_HASH + else: + assert output == NULL_HASH + else: + if n % 65536 == 0: + assert output != NULL_HASH + else: + assert output == NULL_HASH