Skip to content

Commit

Permalink
NDEV-3036 private-rpc: Create a new service for the Private RPC API
Browse files Browse the repository at this point in the history
  • Loading branch information
alfiedotwtf authored and afalaleev committed Jun 22, 2024
1 parent 197d37c commit df6369a
Show file tree
Hide file tree
Showing 31 changed files with 598 additions and 200 deletions.
12 changes: 12 additions & 0 deletions common/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ class Config:
# Statistic configuration
gather_stat_name: Final[str] = "GATHER_STATISTICS"
# Proxy configuration
rpc_private_ip_name: Final[str] = "RPC_PRIVATE_IP"
rpc_private_port_name: Final[str] = "RPC_PRIVATE_PORT"
rpc_public_port_name: Final[str] = "RPC_PUBLIC_PORT"
rpc_process_cnt_name: Final[str] = "RPC_PROCESS_COUNT"
rpc_worker_cnt_name: Final[str] = "RPC_WORKER_COUNT"
Expand Down Expand Up @@ -431,6 +433,14 @@ def gather_stat(self) -> bool:
#########################
# Proxy configuration

@cached_property
def rpc_private_ip(self) -> str:
return os.environ.get(self.rpc_private_ip_name, self.base_service_ip)

@cached_property
def rpc_private_port(self) -> int:
return self._env_num(self.rpc_private_port_name, self.rpc_public_port + 1, 8000, 25000)

@cached_property
def rpc_public_port(self) -> int:
return self._env_num(self.rpc_public_port_name, 9090, 8000, 25000)
Expand Down Expand Up @@ -827,6 +837,8 @@ def to_string(self) -> str:
self.gather_stat_name: self.gather_stat,
self.debug_cmd_line_name: self.debug_cmd_line,
# Proxy configuration
self.rpc_private_ip_name: self.rpc_private_ip,
self.rpc_private_port_name: self.rpc_private_port,
self.rpc_public_port_name: self.rpc_public_port,
self.rpc_process_cnt_name: self.rpc_process_cnt,
self.rpc_worker_cnt_name: self.rpc_worker_cnt,
Expand Down
2 changes: 1 addition & 1 deletion common/ethereum/hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def from_raw(cls, raw: _RawHash) -> Self:
@classmethod
def from_not_none(cls, raw: _RawHash) -> Self:
if raw is None:
raise ValueError(f"Wrong input: null")
raise ValueError("Wrong input: null")
return cls.from_raw(raw)

@property
Expand Down
46 changes: 43 additions & 3 deletions common/ethereum/transaction.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Final
from typing import Annotated, Final

import eth_keys
import rlp
Expand All @@ -9,7 +9,8 @@

from .errors import EthError
from ..utils.cached import cached_property, cached_method
from ..utils.format import hex_to_bytes
from ..utils.format import bytes_to_hex, hex_to_bytes
from ..utils.pydantic import PlainValidator, PlainSerializer


class EthNoChainTx(rlp.Serializable):
Expand Down Expand Up @@ -65,10 +66,14 @@ def __init__(self, *args, **kwargs):

@classmethod
def from_raw(cls, s: bytes | bytearray | str) -> Self:
if isinstance(s, str):
if isinstance(s, cls):
return s
elif isinstance(s, str):
s = hex_to_bytes(s)
elif isinstance(s, bytearray):
s = bytes(s)
elif isinstance(s, dict):
s = cls.from_dict(s)

try:
return rlp.decode(s, cls)
Expand All @@ -87,10 +92,38 @@ def _copy_from_nochain_tx(cls, nochain_tx: EthNoChainTx) -> Self:
value_list += [0, 0, 0]
return cls(*value_list)

@classmethod
def from_dict(cls, d: dict) -> Self:
return cls(
nonce=int(d.get("nonce", 0)),
gas_price=int(d.get("gasPrice", 0)),
gas_limit=int(d.get("gas", 0)),
to_address=bytes.fromhex(d.get("to", "")),
value=int(d.get("value", 0)),
call_data=bytes.fromhex(d.get("data", "")),
v=int(d.get("v", 0)),
r=int(d.get("r", 0)),
s=int(d.get("s", 0)),
)

def to_dict(self) -> dict:
return {
"nonce": int(self.nonce),
"gasPrice": int(self.gas_price),
"gas": int(self.gas_limit),
"to": self.to_address,
"value": int(self.value),
"data": bytes_to_hex(self.call_data),
}

@cached_method
def to_bytes(self) -> bytes:
return rlp.encode(self)

@cached_method
def to_string(self) -> str:
return bytes_to_hex(self.to_bytes(), prefix="0x")

@property
def has_chain_id(self) -> bool:
return self.chain_id is not None
Expand Down Expand Up @@ -183,3 +216,10 @@ def contract(self) -> bytes | None:

contract_addr = rlp.encode((self.from_address, self.nonce))
return keccak(contract_addr)[-20:]


EthTxField = Annotated[
EthTx,
PlainValidator(EthTx.from_raw),
PlainSerializer(lambda v: v.to_string(), return_type=str),
]
20 changes: 19 additions & 1 deletion common/http/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import hashlib
import inspect
import re
import time
Expand Down Expand Up @@ -66,6 +67,23 @@ def from_raw(

return self

@cached_property
def ctx_id(self) -> str:
if ctx_id := getattr(self, "_ctx_id", None):
return ctx_id

size = len(self.request.body)
raw_value = f"{self.ip_addr}:{size}:{self.start_time_nsec}"
ctx_id = hashlib.md5(bytes(raw_value, "utf-8")).hexdigest()[:8]
self.set_property_value("_ctx_id", ctx_id)
return ctx_id

@cached_property
def chain_id(self) -> int:
chain_id = getattr(self, "_chain_id", None)
assert chain_id is not None
return chain_id

@cached_property
def body(self) -> str:
value = self.request.body
Expand Down Expand Up @@ -173,7 +191,7 @@ def http_validate_method_name(name: str) -> None:
def _validate_request_id(value: HttpRequestId) -> HttpRequestId:
if (value is None) or isinstance(value, int) or isinstance(value, str):
return value
raise ValueError(f"'id' must be a string or integer")
raise ValueError("'id' must be a string or integer")


HttpRequestIdField = Annotated[HttpRequestId, PlainValidator(_validate_request_id)]
11 changes: 11 additions & 0 deletions common/neon/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import random
from typing import Final, Annotated, Union

import eth_account
import eth_keys
import eth_utils
from pydantic.functional_serializers import PlainSerializer
from pydantic.functional_validators import PlainValidator
from typing_extensions import Self

from ..ethereum.transaction import EthTx
from ..ethereum.hash import EthAddress
from ..utils.cached import cached_method, cached_property
from ..utils.format import bytes_to_hex, hex_to_bytes, hex_to_int
Expand Down Expand Up @@ -145,6 +147,15 @@ def private_key(self) -> eth_keys.keys.PrivateKey:
assert self._private_key
return self._private_key

def sign_msg(self, data: bytes) -> eth_keys.keys.Signature:
return self.private_key.sign_msg(data)

def sign_transaction(self, tx: EthTx, chain_id: int) -> str:
tx = tx.to_dict()
tx["chainId"] = chain_id
signed_tx = eth_account.Account.sign_transaction(tx, self.private_key)
return signed_tx.raw_transaction.to_0x_hex()

def __str__(self) -> str:
return self.to_string()

Expand Down
4 changes: 2 additions & 2 deletions proxy/base/mp_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,5 +221,5 @@ class MpGetTxResp(BaseModel):


class MpTxPoolContentResp(BaseModel):
pending_list: tuple[NeonTxModel, ...]
queued_list: tuple[NeonTxModel, ...]
pending_list: list[NeonTxModel]
queued_list: list[NeonTxModel]
25 changes: 25 additions & 0 deletions proxy/base/op_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

from typing_extensions import Self, ClassVar

from common.ethereum.bin_str import EthBinStrField
from common.ethereum.hash import EthAddressField, EthAddress
from common.ethereum.transaction import EthTxField
from common.solana.pubkey import SolPubKey, SolPubKeyField
from common.solana.transaction_model import SolTxModel
from common.utils.cached import cached_method
Expand Down Expand Up @@ -79,6 +81,29 @@ class OpTokenSolAddressModel(BaseModel):
token_sol_address: SolPubKeyField


class OpSignEthMessageRequest(BaseModel):
ctx_id: str
eth_address: EthAddressField
data: EthBinStrField


class OpSignEthMessageResp(BaseModel):
signed_message: str | None = None
error: str | None = None


class OpSignEthTxRequest(BaseModel):
ctx_id: str
tx: EthTxField
eth_address: EthAddressField
chain_id: int


class OpSignEthTxResp(BaseModel):
signed_tx: str | None = None
error: str | None = None


class OpSignSolTxListRequest(BaseModel):
req_id: dict
owner: SolPubKeyField
Expand Down
25 changes: 25 additions & 0 deletions proxy/base/op_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from typing import Sequence

from common.app_data.client import AppDataClient
from common.ethereum.bin_str import EthBinStrField
from common.ethereum.hash import EthAddressField
from common.ethereum.transaction import EthTxField
from common.solana.pubkey import SolPubKey
from common.solana.transaction import SolTx
from common.solana.transaction_model import SolTxModel
Expand All @@ -14,6 +17,10 @@
OpResourceResp,
OpTokenSolAddressModel,
OpGetTokenSolAddressRequest,
OpSignEthMessageRequest,
OpSignEthMessageResp,
OpSignEthTxRequest,
OpSignEthTxResp,
OpSignSolTxListRequest,
OpSolTxListResp,
OpGetSignerKeyListRequest,
Expand Down Expand Up @@ -48,6 +55,18 @@ async def get_token_sol_address(self, req_id: dict, owner: SolPubKey, chain_id:
resp = await self._get_token_sol_address(req)
return resp.token_sol_address

async def sign_eth_message(
self, ctx_id: str, eth_address: EthAddressField, data: EthBinStrField
) -> OpSignEthMessageResp:
req = OpSignEthMessageRequest(ctx_id=ctx_id, eth_address=eth_address, data=data)
return await self._sign_eth_message(req)

async def sign_eth_tx(
self, ctx_id: str, tx: EthTxField, eth_address: EthAddressField, chain_id: int
) -> OpSignEthTxResp:
req = OpSignEthTxRequest(ctx_id=ctx_id, tx=tx, eth_address=eth_address, chain_id=chain_id)
return await self._sign_eth_tx(req)

async def sign_sol_tx_list(self, req_id: dict, owner: SolPubKey, tx_list: Sequence[SolTx]) -> tuple[SolTx, ...]:
model_list = [SolTxModel.from_raw(tx) for tx in tx_list]
req = OpSignSolTxListRequest(req_id=req_id, owner=owner, tx_list=model_list)
Expand Down Expand Up @@ -78,6 +97,12 @@ async def _free_resource(self, request: OpFreeResourceRequest) -> OpResourceResp
@AppDataClient.method(name="getOperatorTokenAddress")
async def _get_token_sol_address(self, request: OpGetTokenSolAddressRequest) -> OpTokenSolAddressModel: ...

@AppDataClient.method(name="signEthMessage")
async def _sign_eth_message(self, request: OpSignEthMessageRequest) -> OpSignEthMessageResp: ...

@AppDataClient.method(name="signEthTransaction")
async def _sign_eth_tx(self, request: OpSignEthTxRequest) -> OpSignEthTxResp: ...

@AppDataClient.method(name="signSolanaTransactionList")
async def _sign_sol_tx_list(self, request: OpSignSolTxListRequest) -> OpSolTxListResp: ...

Expand Down
Loading

0 comments on commit df6369a

Please sign in to comment.