Skip to content

Commit 447e57e

Browse files
committed
Merge branch 'develop' into ci-test-block-develop
2 parents b0f0aaa + 69dd300 commit 447e57e

12 files changed

+370
-62
lines changed

proxy/common_neon/address.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import random
2+
import base64
23

34
from eth_keys import keys as eth_keys
45
from hashlib import sha256
@@ -9,6 +10,8 @@
910
from .layouts import ACCOUNT_INFO_LAYOUT
1011
from ..environment import neon_cli, ETH_TOKEN_MINT_ID, EVM_LOADER_ID
1112
from .constants import ACCOUNT_SEED_VERSION
13+
from solana.rpc.commitment import Confirmed
14+
1215

1316
class EthereumAddress:
1417
def __init__(self, data, private=None):
@@ -61,8 +64,31 @@ class AccountInfo(NamedTuple):
6164
ether: eth_keys.PublicKey
6265
trx_count: int
6366
code_account: PublicKey
67+
state: int
6468

6569
@staticmethod
6670
def frombytes(data):
6771
cont = ACCOUNT_INFO_LAYOUT.parse(data)
68-
return AccountInfo(cont.ether, int.from_bytes(cont.trx_count, 'little'), PublicKey(cont.code_account))
72+
return AccountInfo(cont.ether, int.from_bytes(cont.trx_count, 'little'), PublicKey(cont.code_account), cont.state)
73+
74+
75+
def _getAccountData(client, account, expected_length, owner=None):
76+
info = client.get_account_info(account, commitment=Confirmed)['result']['value']
77+
if info is None:
78+
raise Exception("Can't get information about {}".format(account))
79+
80+
data = base64.b64decode(info['data'][0])
81+
if len(data) < expected_length:
82+
raise Exception("Wrong data length for account data {}".format(account))
83+
return data
84+
85+
86+
def getAccountInfo(client, eth_account: EthereumAddress):
87+
account_sol, nonce = ether2program(eth_account)
88+
info = _getAccountData(client, account_sol, ACCOUNT_INFO_LAYOUT.sizeof())
89+
return AccountInfo.frombytes(info)
90+
91+
92+
def isPayed(client, address: str):
93+
acc_info: AccountInfo = getAccountInfo(client, EthereumAddress(address))
94+
return acc_info.state != 0

proxy/common_neon/estimate.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import os
2+
import math
3+
from web3.auto import w3
4+
5+
from solana.publickey import PublicKey
6+
from logged_groups import logged_group
7+
from typing import Tuple
8+
9+
from ..common_neon.utils import get_holder_msg
10+
from ..common_neon.transaction_sender import NeonTxSender, NeonCreateContractTxStage, NeonCreateAccountTxStage
11+
from ..environment import EXTRA_GAS, EVM_STEPS, EVM_BYTE_COST, HOLDER_MSG_SIZE, LAMPORTS_PER_SIGNATURE, \
12+
ACCOUNT_MAX_SIZE, SPL_TOKEN_ACCOUNT_SIZE, PAYMENT_TO_TREASURE, ACCOUNT_STORAGE_OVERHEAD
13+
from eth_keys import keys as eth_keys
14+
from .eth_proto import Trx as EthTrx
15+
16+
17+
def evm_step_cost(signature_cnt):
18+
operator_expences = PAYMENT_TO_TREASURE + LAMPORTS_PER_SIGNATURE * signature_cnt
19+
return math.ceil(operator_expences / EVM_STEPS)
20+
21+
22+
@logged_group("neon.Proxy")
23+
class GasEstimate:
24+
def __init__(self, request, db, client, evm_step_count):
25+
self.sender: bytes = bytes.fromhex(request.get('from', "0x%040x" % 0x0)[2:])
26+
self.step_count = evm_step_count
27+
28+
contract = request.get('to', None)
29+
contract = bytes.fromhex(contract[2:]) if contract else ""
30+
31+
value = request.get('value', None)
32+
value = int(value, 16) if value else 0
33+
34+
data = request.get('data', None)
35+
data = data[2:] if data else ""
36+
37+
unsigned_trx = {
38+
'to': contract,
39+
'value': value,
40+
'gas': 999999999,
41+
'gasPrice': 1_000_000_000,
42+
'nonce': 0xffff,
43+
'data': data,
44+
'chainId': int('ffffffff', 16)
45+
}
46+
signed_trx = w3.eth.account.sign_transaction(unsigned_trx, eth_keys.PrivateKey(os.urandom(32)))
47+
trx = EthTrx.fromString(signed_trx.rawTransaction)
48+
49+
self.tx_sender = NeonTxSender(db, client, trx, steps=evm_step_count)
50+
51+
def iteration_info(self) -> Tuple[int, int]:
52+
if self.tx_sender.steps_emulated > 0:
53+
full_step_iterations = int(self.tx_sender.steps_emulated / self.step_count)
54+
final_steps = self.tx_sender.steps_emulated % self.step_count
55+
if final_steps > 0 and final_steps < EVM_STEPS:
56+
final_steps = EVM_STEPS
57+
else:
58+
full_step_iterations = 0
59+
final_steps = EVM_STEPS
60+
return final_steps, full_step_iterations
61+
62+
@logged_group("neon.Proxy")
63+
def simple_neon_tx_strategy(self, *, logger):
64+
gas = evm_step_cost(2) * (self.tx_sender.steps_emulated if self.tx_sender.steps_emulated > EVM_STEPS else EVM_STEPS)
65+
logger.debug(f'estimate simple_neon_tx_strategy: {gas}')
66+
return gas
67+
68+
@logged_group("neon.Proxy")
69+
def iterative_neon_tx_strategy(self, *, logger):
70+
begin_iteration = 1
71+
final_steps, full_step_iterations = self.iteration_info()
72+
steps = begin_iteration * EVM_STEPS + full_step_iterations * self.step_count + final_steps
73+
gas = steps * evm_step_cost(1)
74+
logger.debug(f'estimate iterative_neon_tx_strategy: {gas}')
75+
return gas
76+
77+
@logged_group("neon.Proxy")
78+
def holder_neon_tx_strategy(self, *, logger):
79+
begin_iteration = 1
80+
msg = get_holder_msg(self.tx_sender.eth_tx)
81+
holder_iterations = math.ceil(len(msg) / HOLDER_MSG_SIZE)
82+
final_steps, full_step_iterations = self.iteration_info()
83+
steps = (begin_iteration + holder_iterations) * EVM_STEPS + full_step_iterations * self.step_count + final_steps
84+
gas = steps * evm_step_cost(1)
85+
logger.debug(f'estimate holder_neon_tx_strategy: {gas}')
86+
return gas
87+
88+
@logged_group("neon.Proxy")
89+
def allocated_space(self, *, logger):
90+
space = 0
91+
for s in self.tx_sender._create_account_list:
92+
if s.NAME == NeonCreateContractTxStage.NAME:
93+
space += s.size + ACCOUNT_MAX_SIZE + SPL_TOKEN_ACCOUNT_SIZE + ACCOUNT_STORAGE_OVERHEAD*3
94+
elif s.NAME == NeonCreateAccountTxStage.NAME:
95+
space += ACCOUNT_MAX_SIZE + SPL_TOKEN_ACCOUNT_SIZE + ACCOUNT_STORAGE_OVERHEAD * 2
96+
97+
space += self.tx_sender.unpaid_space
98+
logger.debug(f'allocated space: {space}')
99+
return space
100+
101+
@logged_group("neon.Proxy")
102+
def estimate(self, *, logger):
103+
self.tx_sender.operator_key = PublicKey(os.urandom(32))
104+
self.tx_sender._call_emulated(self.sender)
105+
self.tx_sender._parse_accounts_list();
106+
107+
gas_for_trx = max(self.simple_neon_tx_strategy(), self.iterative_neon_tx_strategy(), self.holder_neon_tx_strategy())
108+
gas_for_space = self.allocated_space() * EVM_BYTE_COST
109+
gas = gas_for_trx + gas_for_space + EXTRA_GAS
110+
111+
# TODO: MM restriction. Uncomment ?
112+
# if gas < 21000:
113+
# gas = 21000
114+
115+
logger.debug(f'extra_gas: {EXTRA_GAS}')
116+
logger.debug(f'gas_for_space: {gas_for_space}')
117+
logger.debug(f'gas_for_trx: {gas_for_trx}')
118+
logger.debug(f'estimated gas: {gas}')
119+
return hex(gas)

proxy/common_neon/layouts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"rw_blocked_acc" / Bytes(32),
2929
"eth_token_account" / Bytes(32),
3030
"ro_blocked_cnt" / Int8ul,
31+
"state" / Int8ul,
3132
)
3233

3334
CODE_ACCOUNT_INFO_LAYOUT = Struct(

proxy/common_neon/transaction_sender.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from solana.blockhash import Blockhash
1818
from solana.account import Account as SolanaAccount
1919

20-
from ..common_neon.address import accountWithSeed, getTokenAddr, EthereumAddress
20+
from .address import accountWithSeed, getTokenAddr, EthereumAddress, isPayed
2121
from ..common_neon.errors import EthereumError
2222
from .constants import STORAGE_SIZE, EMPTY_STORAGE_TAG, FINALIZED_STORAGE_TAG, ACCOUNT_SEED_VERSION
2323
from .emulator_interactor import call_emulated
@@ -27,11 +27,14 @@
2727
from .solana_interactor import check_if_big_transaction, check_if_program_exceeded_instructions
2828
from ..common_neon.eth_proto import Trx as EthTx
2929
from ..common_neon.utils import NeonTxResultInfo, NeonTxInfo
30+
3031
from ..environment import RETRY_ON_FAIL, EVM_LOADER_ID, PERM_ACCOUNT_LIMIT, ACCOUNT_PERMISSION_UPDATE_INT
3132
from ..environment import MIN_OPERATOR_BALANCE_TO_WARN, MIN_OPERATOR_BALANCE_TO_ERR
33+
from ..environment import ACCOUNT_MAX_SIZE, SPL_TOKEN_ACCOUNT_SIZE, HOLDER_MSG_SIZE, ACCOUNT_STORAGE_OVERHEAD
3234
from ..memdb.memdb import MemDB, NeonPendingTxInfo
3335
from ..environment import get_solana_accounts
3436
from ..common_neon.account_whitelist import AccountWhitelist
37+
from proxy.common_neon.utils import get_holder_msg
3538

3639

3740
class NeonTxStage(metaclass=abc.ABCMeta):
@@ -398,6 +401,7 @@ def __init__(self, db: MemDB, solana: SolanaInteractor, eth_tx: EthTx, steps: in
398401
self._resize_contract_list = []
399402
self._create_account_list = []
400403
self._eth_meta_list = []
404+
self.unpaid_space = 0
401405

402406
def execute(self) -> NeonTxResultInfo:
403407
try:
@@ -517,10 +521,9 @@ def _prepare_execution(self):
517521
self.builder.init_eth_trx(self.eth_tx, self._eth_meta_list, self._caller_token)
518522
self.builder.init_iterative(self.resource.storage, self.resource.holder, self.resource.rid)
519523

520-
def _call_emulated(self):
521-
self.debug(f'sender address: {self.eth_sender}')
522-
523-
src = self.eth_sender[2:]
524+
def _call_emulated(self, sender=None):
525+
src = sender.hex() if sender else self.eth_sender[2:]
526+
self.debug(f'sender address: 0x{src}')
524527
if self.deployed_contract:
525528
dst = 'deploy'
526529
self.debug(f'deploy contract: {self.deployed_contract}')
@@ -544,14 +547,27 @@ def _parse_accounts_list(self):
544547

545548
for account_desc in self._emulator_json['accounts']:
546549
if account_desc['new']:
547-
if account_desc['code_size']:
550+
if account_desc['deploy']:
548551
stage = NeonCreateContractTxStage(self, account_desc)
549552
self._create_account_list.append(stage)
550553
elif account_desc['writable']:
551554
stage = NeonCreateAccountTxStage(self, account_desc)
552555
self._create_account_list.append(stage)
553-
elif account_desc['code_size'] and (account_desc['code_size_current'] < account_desc['code_size']):
554-
self._resize_contract_list.append(NeonResizeContractTxStage(self, account_desc))
556+
# TODO: this section may be moved to the estimate_gas() method
557+
elif account_desc["writable"]:
558+
resize_stage = None
559+
if account_desc['code_size'] and (account_desc['code_size_current'] < account_desc['code_size']):
560+
resize_stage = NeonResizeContractTxStage(self, account_desc)
561+
self._resize_contract_list.append(resize_stage)
562+
563+
if account_desc["deploy"]:
564+
self.unpaid_space += (resize_stage.size if resize_stage else account_desc["code_size_current"]) + ACCOUNT_STORAGE_OVERHEAD
565+
elif account_desc["storage_increment"]:
566+
self.unpaid_space += account_desc["storage_increment"]
567+
568+
if not isPayed(self.solana.client, account_desc['address']):
569+
self.debug(f'found losted account {account_desc["account"]}')
570+
self.unpaid_space += ACCOUNT_MAX_SIZE + SPL_TOKEN_ACCOUNT_SIZE + ACCOUNT_STORAGE_OVERHEAD * 2
555571

556572
eth_address = account_desc['address']
557573
sol_account = account_desc["account"]
@@ -873,16 +889,13 @@ def _build_preparation_txs(self) -> [Transaction]:
873889
tx_list = super()._build_preparation_txs()
874890

875891
# write eth transaction to the holder account
876-
unsigned_msg = self.s.eth_tx.unsigned_msg()
877-
msg = self.s.eth_tx.signature()
878-
msg += len(unsigned_msg).to_bytes(8, byteorder="little")
879-
msg += unsigned_msg
892+
msg = get_holder_msg(self.s.eth_tx)
880893

881894
offset = 0
882895
rest = msg
883896
cnt = 0
884897
while len(rest):
885-
(part, rest) = (rest[:1000], rest[1000:])
898+
(part, rest) = (rest[:HOLDER_MSG_SIZE], rest[HOLDER_MSG_SIZE:])
886899
tx_list.append(self.s.builder.make_write_transaction(offset, part))
887900
offset += len(part)
888901
cnt += 1

proxy/common_neon/utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22
from typing import Dict, Optional, Any
3+
from .eth_proto import Trx as EthTx
34

45
import json
56
import base58
@@ -243,3 +244,8 @@ def get_from_dict(src: Dict, *path) -> Optional[Any]:
243244
if val is None:
244245
return None
245246
return val
247+
248+
249+
def get_holder_msg(eth_trx: EthTx):
250+
unsigned_msg = eth_trx.unsigned_msg()
251+
return eth_trx.signature() + len(unsigned_msg).to_bytes(8, byteorder="little") + unsigned_msg

proxy/docker-compose-test.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ services:
6969
LOG_NEON_CLI_DEBUG: "YES"
7070
FUZZING_BLOCKHASH: "YES"
7171
CONFIG: ci
72-
PP_SOLANA_URL: https://api.devnet.solana.com
73-
PYTH_MAPPING_ACCOUNT: BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2
72+
PP_SOLANA_URL: ${CI_PP_SOLANA_URL:-https://api.devnet.solana.com}
73+
PYTH_MAPPING_ACCOUNT: ${CI_PYTH_MAPPING_ACCOUNT:-BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2}
7474
MIN_OPERATOR_BALANCE_TO_WARN: 4565760000 # = 913152000 * 5 (5 storage accounts) = 4.56576 SOL
7575
MIN_OPERATOR_BALANCE_TO_ERR: 913152000 # = solana rent 131072 (= Rent-exempt minimum: 0.913152 SOL) SOLs to create a storage
7676
MINIMAL_GAS_PRICE: 1
@@ -132,8 +132,8 @@ services:
132132
INDEXER_ERC20_WRAPPER_WHITELIST: ANY
133133
PRICE_UPDATE_INTERVAL: 10
134134
START_SLOT: LATEST
135-
PP_SOLANA_URL: https://api.mainnet-beta.solana.com
136-
PYTH_MAPPING_ACCOUNT: AHtgzX45WTKfkPG53L6WYhGEXwQkN1BVknET3sVsLL8J
135+
PP_SOLANA_URL: ${PP_SOLANA_URL:-https://api.devnet.solana.com}
136+
PYTH_MAPPING_ACCOUNT: ${CI_PYTH_MAPPING_ACCOUNT:-BmA9Z6FjioHJPpjT39QazZyhDRUdZy2ezwx4GiDdE2u2}
137137
MAX_CONFIDENCE_INTERVAL: 0.01
138138
hostname: airdropper
139139
entrypoint: ./run-airdropper.sh

proxy/environment.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,11 @@ def read_elf_params(out_dict):
155155
read_elf_params(ELF_PARAMS)
156156
COLLATERAL_POOL_BASE = ELF_PARAMS.get("NEON_POOL_BASE")
157157
ETH_TOKEN_MINT_ID: PublicKey = PublicKey(ELF_PARAMS.get("NEON_TOKEN_MINT"))
158+
EVM_BYTE_COST = int(ELF_PARAMS.get("NEON_EVM_BYTE_COST"))
159+
EVM_STEPS = int(ELF_PARAMS.get("NEON_EVM_STEPS"))
160+
HOLDER_MSG_SIZE = int(ELF_PARAMS.get("NEON_HOLDER_MSG_SIZE"))
161+
ACCOUNT_MAX_SIZE = int(ELF_PARAMS.get("NEON_ACCOUNT_MAX_SIZE"))
162+
SPL_TOKEN_ACCOUNT_SIZE = int(ELF_PARAMS.get("NEON_SPL_TOKEN_ACCOUNT_SIZE"))
163+
LAMPORTS_PER_SIGNATURE = int(ELF_PARAMS.get("NEON_LAMPORTS_PER_SIGNATURE"))
164+
PAYMENT_TO_TREASURE = int(ELF_PARAMS.get("NEON_PAYMENT_TO_TREASURE"))
165+
ACCOUNT_STORAGE_OVERHEAD = int(ELF_PARAMS.get("NEON_ACCOUNT_STORAGE_OVERHEAD"))

proxy/plugin/solana_rest_api.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,26 +25,27 @@
2525
from ..http.server import HttpWebServerBasePlugin, httpProtocolTypes
2626
from typing import List, Tuple
2727

28-
from .solana_rest_api_tools import neon_config_load, get_token_balance_or_zero, estimate_gas
28+
from .solana_rest_api_tools import neon_config_load, get_token_balance_or_zero
2929
from ..common_neon.transaction_sender import NeonTxSender
3030
from ..common_neon.solana_interactor import SolanaInteractor, SolTxError
3131
from ..common_neon.address import EthereumAddress
3232
from ..common_neon.emulator_interactor import call_emulated
3333
from ..common_neon.errors import EthereumError, PendingTxError
34-
from ..common_neon.eth_proto import Trx as EthTrx
34+
from ..common_neon.estimate import GasEstimate
3535
from ..common_neon.utils import SolanaBlockInfo
36-
from ..environment import SOLANA_URL, PP_SOLANA_URL, PYTH_MAPPING_ACCOUNT
36+
from ..environment import SOLANA_URL, PP_SOLANA_URL, PYTH_MAPPING_ACCOUNT, EVM_STEPS
3737
from ..environment import neon_cli
3838
from ..memdb.memdb import MemDB
3939
from .gas_price_calculator import GasPriceCalculator
40-
40+
from ..common_neon.eth_proto import Trx as EthTrx
4141

4242
modelInstanceLock = threading.Lock()
4343
modelInstance = None
4444

4545
NEON_PROXY_PKG_VERSION = '0.6.0-dev'
4646
NEON_PROXY_REVISION = 'NEON_PROXY_REVISION_TO_BE_REPLACED'
4747

48+
evm_step_count = EVM_STEPS * 5 # number of evm-steps, performed by transaction (should be >= EVM_STEPS)
4849

4950
@logged_group("neon.Proxy")
5051
class EthereumModel:
@@ -93,11 +94,9 @@ def eth_gasPrice(self):
9394

9495
def eth_estimateGas(self, param):
9596
try:
96-
caller_id = param.get('from', "0x0000000000000000000000000000000000000000")
97-
contract_id = param.get('to', "deploy")
98-
data = param.get('data', "None")
99-
value = param.get('value', "")
100-
return estimate_gas(contract_id, EthereumAddress(caller_id), data, value)
97+
calculator = GasEstimate(param, self._db, self._client, evm_step_count)
98+
return calculator.estimate()
99+
101100
except EthereumError:
102101
raise
103102
except Exception as err:
@@ -363,7 +362,7 @@ def eth_sendRawTransaction(self, rawTrx):
363362
eth_signature = '0x' + trx.hash_signed().hex()
364363

365364
try:
366-
tx_sender = NeonTxSender(self._db, self._solana, trx, steps=500)
365+
tx_sender = NeonTxSender(self._db, self._solana, trx, steps=evm_step_count)
367366
tx_sender.execute()
368367
return eth_signature
369368

0 commit comments

Comments
 (0)