Skip to content

Commit b030fad

Browse files
authored
Merge pull request #1165 from carver/rpc-eth-call
add chain.get_transaction_result and rpc eth_call
2 parents 138c73c + f94a5ea commit b030fad

File tree

13 files changed

+462
-34
lines changed

13 files changed

+462
-34
lines changed

eth/chains/base.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,13 @@ def get_canonical_transaction(self, transaction_hash: Hash32) -> BaseTransaction
250250
#
251251
# Execution API
252252
#
253+
@abstractmethod
254+
def get_transaction_result(
255+
self,
256+
transaction: Union[BaseTransaction, SpoofTransaction],
257+
at_header: BlockHeader) -> bytes:
258+
raise NotImplementedError("Chain classes must implement this method")
259+
253260
@abstractmethod
254261
def estimate_gas(
255262
self,
@@ -550,6 +557,20 @@ def create_unsigned_transaction(self,
550557
#
551558
# Execution API
552559
#
560+
def get_transaction_result(
561+
self,
562+
transaction: Union[BaseTransaction, SpoofTransaction],
563+
at_header: BlockHeader) -> bytes:
564+
"""
565+
Return the result of running the given transaction.
566+
This is referred to as a `call()` in web3.
567+
"""
568+
with self.get_vm(at_header).state_in_temp_block() as state:
569+
computation = state.costless_execute_transaction(transaction)
570+
571+
computation.raise_if_error()
572+
return computation.output
573+
553574
def estimate_gas(
554575
self,
555576
transaction: Union[BaseTransaction, SpoofTransaction],

eth/utils/spoof.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
from cytoolz import (
2+
merge,
3+
)
4+
15
from eth.constants import (
26
DEFAULT_SPOOF_V,
37
DEFAULT_SPOOF_R,
@@ -45,6 +49,11 @@ def __getattr__(self, attr: str) -> Union[int, Callable, bytes]:
4549
else:
4650
return getattr(self.spoof_target, attr)
4751

52+
def copy(self, **kwargs):
53+
new_target = self.spoof_target.copy(**kwargs)
54+
new_overrides = merge(self.overrides, kwargs)
55+
return type(self)(new_target, **new_overrides)
56+
4857

4958
class SpoofTransaction(SpoofAttributes):
5059
def __init__(self, transaction: BaseTransaction, **overrides: Any) -> None:

eth/vm/computation.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ def is_error(self) -> bool:
163163
"""
164164
return not self.is_success
165165

166+
def raise_if_error(self) -> None:
167+
"""
168+
If there was an error during computation, raise it as an exception immediately.
169+
170+
:raise VMError:
171+
"""
172+
if self._error is not None:
173+
raise self._error
174+
166175
@property
167176
def should_burn_gas(self) -> bool:
168177
"""

eth/vm/forks/frontier/state.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ def validate_transaction(self, transaction):
4646

4747
def build_evm_message(self, transaction):
4848

49-
transaction_context = self.get_transaction_context(transaction)
50-
gas_fee = transaction.gas * transaction_context.gas_price
49+
gas_fee = transaction.gas * transaction.gas_price
5150

5251
# Buy Gas
5352
self.vm_state.account_db.delta_balance(transaction.sender, -1 * gas_fee)
@@ -99,7 +98,7 @@ def build_evm_message(self, transaction):
9998

10099
def build_computation(self, message, transaction):
101100
"""Apply the message to the VM."""
102-
transaction_context = self.get_transaction_context(transaction)
101+
transaction_context = self.vm_state.get_transaction_context(transaction)
103102
if message.is_create:
104103
is_collision = self.vm_state.account_db.account_has_code_or_nonce(
105104
message.storage_address

eth/vm/state.py

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
ABC,
33
abstractmethod
44
)
5+
import contextlib
56
import logging
67
from typing import ( # noqa: F401
78
Type,
@@ -223,21 +224,45 @@ def apply_transaction(self, transaction):
223224
def get_transaction_executor(self):
224225
return self.transaction_executor(self)
225226

227+
def costless_execute_transaction(self, transaction):
228+
with self.override_transaction_context(gas_price=transaction.gas_price):
229+
free_transaction = transaction.copy(gas_price=0)
230+
return self.execute_transaction(free_transaction)
231+
232+
@contextlib.contextmanager
233+
def override_transaction_context(self, gas_price):
234+
original_context = self.get_transaction_context
235+
236+
def get_custom_transaction_context(transaction):
237+
custom_transaction = transaction.copy(gas_price=gas_price)
238+
return original_context(custom_transaction)
239+
240+
self.get_transaction_context = get_custom_transaction_context
241+
try:
242+
yield
243+
finally:
244+
self.get_transaction_context = original_context
245+
226246
@abstractmethod
227-
def execute_transaction(self):
247+
def execute_transaction(self, transaction):
228248
raise NotImplementedError()
229249

250+
@abstractmethod
251+
def validate_transaction(self, transaction):
252+
raise NotImplementedError
230253

231-
class BaseTransactionExecutor(ABC):
232-
def __init__(self, vm_state):
233-
self.vm_state = vm_state
234-
235-
def get_transaction_context(self, transaction):
236-
return self.vm_state.get_transaction_context_class()(
254+
@classmethod
255+
def get_transaction_context(cls, transaction):
256+
return cls.get_transaction_context_class()(
237257
gas_price=transaction.gas_price,
238258
origin=transaction.sender,
239259
)
240260

261+
262+
class BaseTransactionExecutor(ABC):
263+
def __init__(self, vm_state):
264+
self.vm_state = vm_state
265+
241266
def __call__(self, transaction):
242267
valid_transaction = self.validate_transaction(transaction)
243268
message = self.build_evm_message(valid_transaction)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"lru-dict>=1.1.6",
1515
"py-ecc>=1.4.2,<2.0.0",
1616
"pyethash>=0.1.27,<1.0.0",
17-
"rlp>=1.0.1,<2.0.0",
17+
"rlp>=1.0.2,<2.0.0",
1818
"trie>=1.3.5,<2.0.0",
1919
],
2020
# The eth-extra sections is for libraries that the evm does not

tests/conftest.py

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def funded_address_initial_balance():
105105

106106

107107
@pytest.fixture
108-
def chain_with_block_validation(base_db, funded_address, funded_address_initial_balance):
108+
def chain_with_block_validation(base_db, genesis_state):
109109
"""
110110
Return a Chain object containing just the genesis block.
111111
@@ -133,14 +133,6 @@ def chain_with_block_validation(base_db, funded_address, funded_address_initial_
133133
"transaction_root": decode_hex("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421"), # noqa: E501
134134
"uncles_hash": decode_hex("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") # noqa: E501
135135
}
136-
genesis_state = {
137-
funded_address: {
138-
"balance": funded_address_initial_balance,
139-
"nonce": 0,
140-
"code": b"",
141-
"storage": {}
142-
}
143-
}
144136
klass = Chain.configure(
145137
__name__='TestChain',
146138
vm_configuration=(
@@ -156,12 +148,28 @@ def import_block_without_validation(chain, block):
156148
return super(type(chain), chain).import_block(block, perform_validation=False)
157149

158150

151+
@pytest.fixture
152+
def base_genesis_state(funded_address, funded_address_initial_balance):
153+
return {
154+
funded_address: {
155+
'balance': funded_address_initial_balance,
156+
'nonce': 0,
157+
'code': b'',
158+
'storage': {},
159+
}
160+
}
161+
162+
163+
@pytest.fixture
164+
def genesis_state(base_genesis_state):
165+
return base_genesis_state
166+
167+
159168
@pytest.fixture(params=[Chain, MiningChain])
160169
def chain_without_block_validation(
161170
request,
162171
base_db,
163-
funded_address,
164-
funded_address_initial_balance):
172+
genesis_state):
165173
"""
166174
Return a Chain object containing just the genesis block.
167175
@@ -183,6 +191,7 @@ def chain_without_block_validation(
183191
vm_configuration=(
184192
(constants.GENESIS_BLOCK_NUMBER, SpuriousDragonVMForTesting),
185193
),
194+
network_id=1337,
186195
**overrides,
187196
)
188197
genesis_params = {
@@ -196,13 +205,5 @@ def chain_without_block_validation(
196205
'extra_data': constants.GENESIS_EXTRA_DATA,
197206
'timestamp': 1501851927,
198207
}
199-
genesis_state = {
200-
funded_address: {
201-
'balance': funded_address_initial_balance,
202-
'nonce': 0,
203-
'code': b'',
204-
'storage': {},
205-
}
206-
}
207208
chain = klass.from_genesis(base_db, genesis_params, genesis_state)
208209
return chain
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from cytoolz import (
2+
assoc,
3+
)
4+
from tests.core.helpers import (
5+
new_transaction,
6+
)
7+
8+
from eth_utils import (
9+
decode_hex,
10+
function_signature_to_4byte_selector,
11+
to_bytes,
12+
)
13+
import pytest
14+
15+
from eth.exceptions import (
16+
InvalidInstruction,
17+
OutOfGas,
18+
)
19+
20+
21+
@pytest.fixture
22+
def chain(chain_with_block_validation):
23+
return chain_with_block_validation
24+
25+
26+
@pytest.fixture
27+
def simple_contract_address():
28+
return b'\x88' * 20
29+
30+
31+
@pytest.fixture
32+
def genesis_state(base_genesis_state, simple_contract_address):
33+
"""
34+
Includes runtime bytecode of compiled Solidity:
35+
36+
pragma solidity ^0.4.24;
37+
38+
contract GetValues {
39+
function getMeaningOfLife() public pure returns (uint256) {
40+
return 42;
41+
}
42+
function getGasPrice() public view returns (uint256) {
43+
return tx.gasprice;
44+
}
45+
function getBalance() public view returns (uint256) {
46+
return msg.sender.balance;
47+
}
48+
function doRevert() public pure {
49+
revert("always reverts");
50+
}
51+
function useLotsOfGas() public view {
52+
uint size;
53+
for (uint i = 0; i < 2**255; i++){
54+
assembly {
55+
size := extcodesize(0)
56+
}
57+
}
58+
}
59+
}
60+
"""
61+
return assoc(
62+
base_genesis_state,
63+
simple_contract_address,
64+
{
65+
'balance': 0,
66+
'nonce': 0,
67+
'code': decode_hex('60806040526004361061006c5763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166312065fe08114610071578063455259cb14610098578063858af522146100ad57806395dd7a55146100c2578063afc874d2146100d9575b600080fd5b34801561007d57600080fd5b506100866100ee565b60408051918252519081900360200190f35b3480156100a457600080fd5b506100866100f3565b3480156100b957600080fd5b506100866100f7565b3480156100ce57600080fd5b506100d76100fc565b005b3480156100e557600080fd5b506100d7610139565b333190565b3a90565b602a90565b6000805b7f80000000000000000000000000000000000000000000000000000000000000008110156101355760003b9150600101610100565b5050565b604080517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152600e60248201527f616c776179732072657665727473000000000000000000000000000000000000604482015290519081900360640190fd00a165627a7a72305820645df686b4a16d5a69fc6d841fc9ad700528c14b35ca5629e11b154a9d3dff890029'), # noqa: E501
68+
'storage': {},
69+
},
70+
)
71+
72+
73+
def uint256_to_bytes(uint):
74+
return to_bytes(uint).rjust(32, b'\0')
75+
76+
77+
@pytest.mark.parametrize(
78+
'signature, gas_price, expected',
79+
(
80+
(
81+
'getMeaningOfLife()',
82+
0,
83+
uint256_to_bytes(42),
84+
),
85+
(
86+
'getGasPrice()',
87+
0,
88+
uint256_to_bytes(0),
89+
),
90+
(
91+
'getGasPrice()',
92+
9,
93+
uint256_to_bytes(9),
94+
),
95+
(
96+
# make sure that whatever voodoo is used to execute a call, the balance is not inflated
97+
'getBalance()',
98+
1,
99+
uint256_to_bytes(0),
100+
),
101+
),
102+
)
103+
def test_get_transaction_result(
104+
chain,
105+
simple_contract_address,
106+
signature,
107+
gas_price,
108+
expected):
109+
110+
function_selector = function_signature_to_4byte_selector(signature)
111+
call_txn = new_transaction(
112+
chain.get_vm(),
113+
b'\xff' * 20,
114+
simple_contract_address,
115+
gas_price=gas_price,
116+
data=function_selector,
117+
)
118+
result_bytes = chain.get_transaction_result(call_txn, chain.get_canonical_head())
119+
assert result_bytes == expected
120+
121+
122+
@pytest.mark.parametrize(
123+
'signature, expected',
124+
(
125+
(
126+
'doRevert()',
127+
InvalidInstruction,
128+
),
129+
(
130+
'useLotsOfGas()',
131+
OutOfGas,
132+
),
133+
),
134+
)
135+
def test_get_transaction_result_revert(
136+
chain,
137+
simple_contract_address,
138+
signature,
139+
expected):
140+
141+
function_selector = function_signature_to_4byte_selector(signature)
142+
call_txn = new_transaction(
143+
chain.get_vm(),
144+
b'\xff' * 20,
145+
simple_contract_address,
146+
data=function_selector,
147+
)
148+
with pytest.raises(expected):
149+
chain.get_transaction_result(call_txn, chain.get_canonical_head())

0 commit comments

Comments
 (0)