Skip to content

Commit 2ee9f8f

Browse files
committed
rpc: eth_estimateGas
1 parent ae5f382 commit 2ee9f8f

File tree

9 files changed

+291
-43
lines changed

9 files changed

+291
-43
lines changed

eth/chains/base.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@
5959
ValidationError,
6060
VMNotFound,
6161
)
62+
from eth.utils.spoof import (
63+
SpoofTransaction,
64+
)
6265
from eth.validation import (
6366
validate_block_number,
6467
validate_uint256,
@@ -248,7 +251,10 @@ def get_canonical_transaction(self, transaction_hash: Hash32) -> BaseTransaction
248251
# Execution API
249252
#
250253
@abstractmethod
251-
def estimate_gas(self, transaction: BaseTransaction, at_header: BlockHeader=None) -> int:
254+
def estimate_gas(
255+
self,
256+
transaction: Union[BaseTransaction, SpoofTransaction],
257+
at_header: BlockHeader=None) -> int:
252258
raise NotImplementedError("Chain classes must implement this method")
253259

254260
@abstractmethod
@@ -544,7 +550,10 @@ def create_unsigned_transaction(self,
544550
#
545551
# Execution API
546552
#
547-
def estimate_gas(self, transaction: BaseTransaction, at_header: BlockHeader=None) -> int:
553+
def estimate_gas(
554+
self,
555+
transaction: Union[BaseTransaction, SpoofTransaction],
556+
at_header: BlockHeader=None) -> int:
548557
"""
549558
Returns an estimation of the amount of gas the given transaction will
550559
use if executed on top of the block specified by the given header.

tests/trinity/conftest.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ async def ipc_server(
7474
chain_with_block_validation):
7575
'''
7676
This fixture runs a single RPC server over IPC over
77-
the course of all tests. It never needs to be actually
78-
used as a fixture, so it doesn't return (yield) a value.
77+
the course of all tests. It yields the IPC server only for monkeypatching purposes
7978
'''
8079

8180
rpc = RPCServer(chain_with_block_validation, p2p_server.peer_pool)
@@ -84,6 +83,6 @@ async def ipc_server(
8483
asyncio.ensure_future(ipc_server.run(), loop=event_loop)
8584

8685
try:
87-
yield
86+
yield ipc_server
8887
finally:
8988
await ipc_server.cancel()

tests/trinity/core/json-rpc/test_ipc.py

Lines changed: 159 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -35,39 +35,66 @@ def __len__(self):
3535
return self.peer_count
3636

3737

38-
@pytest.fixture
39-
def p2p_server(monkeypatch, p2p_server, mock_peer_pool):
40-
monkeypatch.setattr(p2p_server, 'peer_pool', mock_peer_pool)
41-
return p2p_server
38+
def id_from_rpc_request(param):
39+
if isinstance(param, bytes):
40+
request = json.loads(param.decode())
41+
if 'method' in request and 'params' in request:
42+
rpc_params = (repr(p) for p in request['params'])
43+
return '%s(%s)' % (request['method'], ', '.join(rpc_params))
44+
else:
45+
return repr(param)
46+
else:
47+
return repr(param)
48+
49+
50+
def can_decode_json(potential):
51+
try:
52+
json.loads(potential.decode())
53+
return True
54+
except json.decoder.JSONDecodeError:
55+
return False
56+
57+
58+
async def get_ipc_response(
59+
jsonrpc_ipc_pipe_path,
60+
request_msg,
61+
event_loop):
62+
assert wait_for(jsonrpc_ipc_pipe_path), "IPC server did not successfully start with IPC file"
63+
64+
reader, writer = await asyncio.open_unix_connection(str(jsonrpc_ipc_pipe_path), loop=event_loop)
65+
66+
writer.write(request_msg)
67+
await writer.drain()
68+
result_bytes = b''
69+
while not can_decode_json(result_bytes):
70+
result_bytes += await asyncio.tasks.wait_for(reader.readuntil(b'}'), 0.25, loop=event_loop)
71+
72+
writer.close()
73+
return json.loads(result_bytes.decode())
4274

4375

4476
@pytest.mark.asyncio
4577
@pytest.mark.parametrize(
46-
'request_msg, mock_peer_pool, expected',
78+
'request_msg, expected',
4779
(
4880
(
4981
b'{}',
50-
MockPeerPool(),
5182
{'error': "Invalid Request: empty"},
5283
),
5384
(
5485
build_request('notamethod'),
55-
MockPeerPool(),
5686
{'error': "Invalid RPC method: 'notamethod'", 'id': 3, 'jsonrpc': '2.0'},
5787
),
5888
(
5989
build_request('eth_mining'),
60-
MockPeerPool(),
6190
{'result': False, 'id': 3, 'jsonrpc': '2.0'},
6291
),
6392
(
6493
build_request('web3_clientVersion'),
65-
MockPeerPool(),
6694
{'result': construct_trinity_client_identifier(), 'id': 3, 'jsonrpc': '2.0'},
6795
),
6896
(
6997
build_request('web3_sha3', ['0x89987239849872']),
70-
MockPeerPool(),
7198
{
7299
'result': '0xb3406131994d9c859de3c4400e12f630638e1e992c6453358c16d0e6ce2b1a70',
73100
'id': 3,
@@ -76,7 +103,6 @@ def p2p_server(monkeypatch, p2p_server, mock_peer_pool):
76103
),
77104
(
78105
build_request('web3_sha3', ['0x']),
79-
MockPeerPool(),
80106
{
81107
'result': '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470',
82108
'id': 3,
@@ -85,9 +111,119 @@ def p2p_server(monkeypatch, p2p_server, mock_peer_pool):
85111
),
86112
(
87113
build_request('net_version'),
88-
MockPeerPool(),
89114
{'result': '1337', 'id': 3, 'jsonrpc': '2.0'},
90115
),
116+
(
117+
build_request('net_listening'),
118+
{'result': True, 'id': 3, 'jsonrpc': '2.0'},
119+
),
120+
),
121+
)
122+
async def test_ipc_requests(
123+
jsonrpc_ipc_pipe_path,
124+
request_msg,
125+
expected,
126+
event_loop,
127+
ipc_server):
128+
result = await get_ipc_response(jsonrpc_ipc_pipe_path, request_msg, event_loop)
129+
assert result == expected
130+
131+
132+
@pytest.mark.asyncio
133+
@pytest.mark.parametrize(
134+
'request_msg, expected',
135+
(
136+
(
137+
# simple transaction, with all fields inferred
138+
# (except 'to', which must be provided when not creating a contract)
139+
build_request('eth_estimateGas', params=[{'to': '0x' + '00' * 20}, 'latest']),
140+
# simple transactions are correctly identified as 21000 gas during estimation
141+
{'result': hex(21000), 'id': 3, 'jsonrpc': '2.0'},
142+
),
143+
(
144+
# test block number
145+
build_request('eth_estimateGas', params=[{'to': '0x' + '00' * 20}, '0x0']),
146+
{'result': hex(21000), 'id': 3, 'jsonrpc': '2.0'},
147+
),
148+
(
149+
# another simple transaction, with all fields provided
150+
build_request('eth_estimateGas', params=[{
151+
'to': '0x' + '00' * 20,
152+
'from': '0x' + '11' * 20,
153+
'gasPrice': '0x2',
154+
'gas': '0x3',
155+
'value': '0x0',
156+
'data': '0x',
157+
}, 'latest']),
158+
{'result': hex(21000), 'id': 3, 'jsonrpc': '2.0'},
159+
),
160+
(
161+
# try adding garbage data to increase estimate
162+
build_request('eth_estimateGas', params=[{
163+
'to': '0x' + '00' * 20,
164+
'data': '0x01',
165+
}, 'latest']),
166+
{'result': hex(21068), 'id': 3, 'jsonrpc': '2.0'},
167+
),
168+
(
169+
# deploy a tiny contract
170+
build_request('eth_estimateGas', params=[{
171+
'data': '0x3838533838f3',
172+
}, 'latest']),
173+
{'result': hex(65483), 'id': 3, 'jsonrpc': '2.0'},
174+
),
175+
(
176+
# specifying v,r,s is invalid
177+
build_request('eth_estimateGas', params=[{
178+
'v': '0x' + '00' * 20,
179+
'r': '0x' + '00' * 20,
180+
's': '0x' + '00' * 20,
181+
}, 'latest']),
182+
{
183+
'error': "The following invalid fields were given in a transaction: ['r', 's', 'v']. Only ['data', 'from', 'gas', 'gasPrice', 'to', 'value'] are allowed", # noqa: E501
184+
'id': 3,
185+
'jsonrpc': '2.0',
186+
}
187+
),
188+
(
189+
# specifying gas_price is invalid
190+
build_request('eth_estimateGas', params=[{
191+
'gas_price': '0x0',
192+
}, 'latest']),
193+
{
194+
'error': "The following invalid fields were given in a transaction: ['gas_price']. Only ['data', 'from', 'gas', 'gasPrice', 'to', 'value'] are allowed", # noqa: E501
195+
'id': 3,
196+
'jsonrpc': '2.0',
197+
}
198+
),
199+
(
200+
# specifying nonce is invalid
201+
build_request('eth_estimateGas', params=[{
202+
'nonce': '0x01',
203+
}, 'latest']),
204+
{
205+
'error': "The following invalid fields were given in a transaction: ['nonce']. Only ['data', 'from', 'gas', 'gasPrice', 'to', 'value'] are allowed", # noqa: E501
206+
'id': 3,
207+
'jsonrpc': '2.0',
208+
}
209+
),
210+
),
211+
ids=id_from_rpc_request,
212+
)
213+
async def test_estimate_gas_on_ipc(
214+
jsonrpc_ipc_pipe_path,
215+
request_msg,
216+
expected,
217+
event_loop,
218+
ipc_server):
219+
result = await get_ipc_response(jsonrpc_ipc_pipe_path, request_msg, event_loop)
220+
assert result == expected
221+
222+
223+
@pytest.mark.asyncio
224+
@pytest.mark.parametrize(
225+
'request_msg, mock_peer_pool, expected',
226+
(
91227
(
92228
build_request('net_peerCount'),
93229
MockPeerPool(peer_count=1),
@@ -98,31 +234,19 @@ def p2p_server(monkeypatch, p2p_server, mock_peer_pool):
98234
MockPeerPool(peer_count=0),
99235
{'result': '0x0', 'id': 3, 'jsonrpc': '2.0'},
100236
),
101-
(
102-
build_request('net_listening'),
103-
MockPeerPool(),
104-
{'result': True, 'id': 3, 'jsonrpc': '2.0'},
105-
),
106237
),
107238
ids=[
108-
'empty', 'notamethod', 'eth_mining', 'web3_clientVersion',
109-
'web3_sha3_1', 'web3_sha3_2', 'net_version', 'net_peerCount_1',
110-
'net_peerCount_0', 'net_listening_true',
239+
'net_peerCount_1', 'net_peerCount_0',
111240
],
112241
)
113-
async def test_ipc_requests(jsonrpc_ipc_pipe_path,
114-
request_msg,
115-
expected,
116-
event_loop,
117-
ipc_server):
118-
assert wait_for(jsonrpc_ipc_pipe_path), "IPC server did not successfully start with IPC file"
119-
120-
reader, writer = await asyncio.open_unix_connection(str(jsonrpc_ipc_pipe_path), loop=event_loop)
121-
122-
writer.write(request_msg)
123-
await writer.drain()
124-
result_bytes = await asyncio.tasks.wait_for(reader.readuntil(b'}'), 0.25, loop=event_loop)
125-
126-
result = json.loads(result_bytes.decode())
242+
async def test_peer_pool_over_ipc(
243+
monkeypatch,
244+
jsonrpc_ipc_pipe_path,
245+
request_msg,
246+
mock_peer_pool,
247+
expected,
248+
event_loop,
249+
ipc_server):
250+
monkeypatch.setattr(ipc_server.rpc.modules['net'], '_peer_pool', mock_peer_pool)
251+
result = await get_ipc_response(jsonrpc_ipc_pipe_path, request_msg, event_loop)
127252
assert result == expected
128-
writer.close()

tests/trinity/json-fixtures-over-rpc/test_rpc_fixtures.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113

114114
RPC_TRANSACTION_REMAPPERS = {
115115
'data': 'input',
116+
'gasLimit': 'gas',
116117
}
117118

118119
RPC_TRANSACTION_NORMALIZERS = {

trinity/chains/light.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
BaseTransaction,
4747
BaseUnsignedTransaction,
4848
)
49+
from eth.utils.spoof import (
50+
SpoofTransaction,
51+
)
4952
from eth.vm.computation import (
5053
BaseComputation
5154
)
@@ -189,7 +192,10 @@ def apply_transaction(
189192
transaction: BaseTransaction) -> Tuple[BaseBlock, Receipt, BaseComputation]:
190193
raise NotImplementedError("Chain classes must implement " + inspect.stack()[0][3])
191194

192-
def estimate_gas(self, transaction: BaseTransaction, at_header: BlockHeader=None) -> int:
195+
def estimate_gas(
196+
self,
197+
transaction: Union[BaseTransaction, SpoofTransaction],
198+
at_header: BlockHeader=None) -> int:
193199
raise NotImplementedError("Chain classes must implement " + inspect.stack()[0][3])
194200

195201
def import_block(self, block: BaseBlock, perform_validation: bool=True) -> BaseBlock:

trinity/rpc/format.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
)
99
from cytoolz import (
1010
compose,
11+
merge,
1112
)
1213

1314
from eth_utils import (
15+
apply_formatters_to_dict,
16+
decode_hex,
1417
encode_hex,
1518
int_to_big_endian,
1619
)
@@ -20,6 +23,9 @@
2023
from eth.chains.base import (
2124
BaseChain
2225
)
26+
from eth.constants import (
27+
CREATE_CONTRACT_ADDRESS,
28+
)
2329
from eth.rlp.blocks import (
2430
BaseBlock
2531
)
@@ -35,7 +41,7 @@ def transaction_to_dict(transaction: BaseTransaction) -> Dict[str, str]:
3541
return dict(
3642
hash=encode_hex(transaction.hash),
3743
nonce=hex(transaction.nonce),
38-
gasLimit=hex(transaction.gas),
44+
gas=hex(transaction.gas),
3945
gasPrice=hex(transaction.gas_price),
4046
to=encode_hex(transaction.to),
4147
value=hex(transaction.value),
@@ -46,6 +52,31 @@ def transaction_to_dict(transaction: BaseTransaction) -> Dict[str, str]:
4652
)
4753

4854

55+
hexstr_to_int = functools.partial(int, base=16)
56+
57+
58+
TRANSACTION_NORMALIZER = {
59+
'data': decode_hex,
60+
'from': decode_hex,
61+
'gas': hexstr_to_int,
62+
'gasPrice': hexstr_to_int,
63+
'nonce': hexstr_to_int,
64+
'to': decode_hex,
65+
'value': hexstr_to_int,
66+
}
67+
68+
SAFE_TRANSACTION_DEFAULTS = {
69+
'data': b'',
70+
'to': CREATE_CONTRACT_ADDRESS,
71+
'value': 0,
72+
}
73+
74+
75+
def normalize_transaction_dict(transaction_dict: Dict[str, str]) -> Dict[str, Any]:
76+
normalized_dict = apply_formatters_to_dict(TRANSACTION_NORMALIZER, transaction_dict)
77+
return merge(SAFE_TRANSACTION_DEFAULTS, normalized_dict)
78+
79+
4980
def header_to_dict(header: BlockHeader) -> Dict[str, str]:
5081
logs_bloom = encode_hex(int_to_big_endian(header.bloom))[2:]
5182
logs_bloom = '0x' + logs_bloom.rjust(512, '0')

0 commit comments

Comments
 (0)