Skip to content

Commit

Permalink
feature NetworkID (#520)
Browse files Browse the repository at this point in the history
Added network id to the base transaction
Added network id to the server_info response
Added network id to the binary codec
Added network id to autofill if custom network id was supplied to client.

Co-authored-by: pdp2121 <71317875+pdp2121@users.noreply.github.com>
  • Loading branch information
dangell7 and pdp2121 committed Jun 16, 2023
1 parent 8133bbe commit 972177d
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 2 deletions.
68 changes: 68 additions & 0 deletions tests/unit/models/transactions/test_autofill.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from unittest import TestCase

from xrpl.asyncio.transaction.main import _RESTRICTED_NETWORKS
from xrpl.clients import JsonRpcClient, WebsocketClient
from xrpl.models.transactions import AccountSet
from xrpl.transaction import autofill
from xrpl.wallet.wallet_generation import generate_faucet_wallet

_FEE = "0.00001"


class TestAutofill(TestCase):
# Autofill should override tx networkID for network with ID > 1024
# and build_version from 1.11.0 or later.
def test_networkid_override(self):
client = JsonRpcClient("https://sidechain-net1.devnet.rippletest.net:51234")
wallet = generate_faucet_wallet(client, debug=True)
# Override client build_version since 1.11.0 is not released yet.
client.build_version = "1.11.0"
tx = AccountSet(
account=wallet.classic_address,
fee=_FEE,
domain="www.example.com",
)
tx_autofilled = autofill(tx, client)
self.assertGreaterEqual(client.network_id, _RESTRICTED_NETWORKS)
self.assertEqual(tx_autofilled.network_id, client.network_id)

# Autofill should ignore tx network_id for build version earlier than 1.11.0.
def test_networkid_ignore_early_version(self):
client = JsonRpcClient("https://sidechain-net1.devnet.rippletest.net:51234")
wallet = generate_faucet_wallet(client, debug=True)
# Override client build_version since 1.11.0 is not released yet.
client.build_version = "1.10.0"
tx = AccountSet(
account=wallet.classic_address,
fee=_FEE,
domain="www.example.com",
)
tx_autofilled = autofill(tx, client)
self.assertEqual(tx_autofilled.network_id, None)

# Autofill should ignore tx network_id for networks with ID <= 1024.
def test_networkid_ignore_restricted_networks(self):
client = JsonRpcClient("https://s.altnet.rippletest.net:51234")
wallet = generate_faucet_wallet(client, debug=True)
# Override client build_version since 1.11.0 is not released yet.
client.build_version = "1.11.0"
tx = AccountSet(
account=wallet.classic_address,
fee=_FEE,
domain="www.example.com",
)
tx_autofilled = autofill(tx, client)
self.assertLessEqual(client.network_id, _RESTRICTED_NETWORKS)
self.assertEqual(tx_autofilled.network_id, None)

# Autofill should override tx networkID for hooks-testnet.
def test_networkid_override_hooks_testnet(self):
with WebsocketClient("wss://hooks-testnet-v3.xrpl-labs.com") as client:
wallet = generate_faucet_wallet(client, debug=True)
tx = AccountSet(
account=wallet.classic_address,
fee=_FEE,
domain="www.example.com",
)
tx_autofilled = autofill(tx, client)
self.assertEqual(tx_autofilled.network_id, client.network_id)
3 changes: 3 additions & 0 deletions xrpl/asyncio/clients/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Optional

from xrpl.models.requests.request import Request
from xrpl.models.response import Response
Expand All @@ -22,6 +23,8 @@ def __init__(self: Client, url: str) -> None:
url: The url to which this client will connect
"""
self.url = url
self.network_id: Optional[int] = None
self.build_version: Optional[str] = None

@abstractmethod
async def _request_impl(self: Client, request: Request) -> Response:
Expand Down
114 changes: 112 additions & 2 deletions xrpl/asyncio/transaction/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from xrpl.core.addresscodec import is_valid_xaddress, xaddress_to_classic_address
from xrpl.core.binarycodec import encode, encode_for_multisigning, encode_for_signing
from xrpl.core.keypairs.main import sign as keypairs_sign
from xrpl.models.requests import ServerState, SubmitOnly
from xrpl.models.requests import ServerInfo, ServerState, SubmitOnly
from xrpl.models.response import Response
from xrpl.models.transactions import EscrowFinish
from xrpl.models.transactions.transaction import Signer, Transaction
Expand All @@ -23,7 +23,14 @@
from xrpl.wallet.main import Wallet

_LEDGER_OFFSET: Final[int] = 20

# Sidechains are expected to have network IDs above this.
# Networks with ID above this restricted number are expected to specify an
# accurate NetworkID field in every transaction to that chain to prevent replay attacks.
# Mainnet and testnet are exceptions.
# More context: https://github.com/XRPLF/rippled/pull/4370
_RESTRICTED_NETWORKS = 1024
_REQUIRED_NETWORKID_VERSION = "1.11.0"
_HOOKS_TESTNET_ID = 21338
# TODO: make this dynamic based on the current ledger fee
_ACCOUNT_DELETE_FEE: Final[int] = int(xrp_to_drops(2))

Expand Down Expand Up @@ -235,6 +242,10 @@ async def autofill(
The autofilled transaction.
"""
transaction_json = transaction.to_dict()
if not client.network_id:
await _get_network_id_and_build_version(client)
if "network_id" not in transaction_json and _tx_needs_networkID(client):
transaction_json["network_id"] = client.network_id
if "sequence" not in transaction_json:
sequence = await get_next_valid_seq_number(transaction_json["account"], client)
transaction_json["sequence"] = sequence
Expand All @@ -248,6 +259,105 @@ async def autofill(
return Transaction.from_dict(transaction_json)


async def _get_network_id_and_build_version(client: Client) -> None:
"""
Get the network id and build version of the connected server.
Args:
client: The network client to use to send the request.
Raises:
XRPLRequestFailureException: if the rippled API call fails.
"""
response = await client._request_impl(ServerInfo())
if response.is_successful():
if "network_id" in response.result["info"]:
client.network_id = response.result["info"]["network_id"]
if not client.build_version and "build_version" in response.result["info"]:
client.build_version = response.result["info"]["build_version"]
return

raise XRPLRequestFailureException(response.result)


def _tx_needs_networkID(client: Client) -> bool:
"""
Determines whether the transactions required network ID to be valid.
Transaction needs networkID if later than restricted ID and either
the network is hooks testnet or build version is >= 1.11.0.
More context: https://github.com/XRPLF/rippled/pull/4370
Args:
client (Client): The network client to use to send the request.
Returns:
bool: whether the transactions required network ID to be valid
"""
if client.network_id and client.network_id > _RESTRICTED_NETWORKS:
# TODO: remove the buildVersion logic when 1.11.0 is out and widely used.
# Issue: https://github.com/XRPLF/xrpl-py/issues/595
if (
client.build_version
and _is_not_later_rippled_version(
_REQUIRED_NETWORKID_VERSION, client.build_version
)
) or client.network_id == _HOOKS_TESTNET_ID:
return True
return False


def _is_not_later_rippled_version(source: str, target: str) -> bool:
"""
Determines whether the source version is not a later release than the
target version.
Args:
source: the source rippled version.
target: the target rippled version.
Returns:
bool: true if source is earlier, false otherwise.
"""
if source == target:
return True
source_decomp = source.split(".")
target_decomp = target.split(".")
source_major, source_minor = int(source_decomp[0]), int(source_decomp[1])
target_major, target_minor = int(target_decomp[0]), int(target_decomp[1])

# Compare major version
if source_major != target_major:
return source_major < target_major

# Compare minor version
if source_minor != target_minor:
return source_minor < target_minor

source_patch = source_decomp[2].split("-")
target_patch = target_decomp[2].split("-")
source_patch_version = int(source_patch[0])
target_patch_version = int(target_patch[0])

# Compare patch version
if source_patch_version != target_patch_version:
return source_patch_version < target_patch_version

# Compare release version
if len(source_patch) != len(target_patch):
return len(source_patch) > len(target_patch)

if len(source_patch) == 2:
# Compare release types
if not source_patch[1][0].startswith(target_patch[1][0]):
return source_patch[1] < target_patch[1]
# Compare beta versions
if source_patch[1].startswith("b"):
return int(source_patch[1][1:]) < int(target_patch[1][1:])
# Compare rc versions
return int(source_patch[1][2:]) < int(target_patch[1][2:])
return False


def _validate_account_xaddress(
json: Dict[str, Any], account_field: str, tag_field: str
) -> None:
Expand Down
13 changes: 13 additions & 0 deletions xrpl/core/binarycodec/definitions/definitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,16 @@
"type": "UInt16"
}
],
[
"NetworkID",
{
"nth": 1,
"isVLEncoded": false,
"isSerialized": true,
"isSigningField": true,
"type": "UInt32"
}
],
[
"Flags",
{
Expand Down Expand Up @@ -2176,6 +2186,9 @@
"telCAN_NOT_QUEUE_BLOCKED": -389,
"telCAN_NOT_QUEUE_FEE": -388,
"telCAN_NOT_QUEUE_FULL": -387,
"telWRONG_NETWORK": -386,
"telREQUIRES_NETWORK_ID": -385,
"telNETWORK_ID_MAKES_TX_NON_CANONICAL": -384,

"temMALFORMED": -299,
"temBAD_AMOUNT": -298,
Expand Down
3 changes: 3 additions & 0 deletions xrpl/models/transactions/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ class Transaction(BaseModel):
transaction. Automatically added during signing.
"""

network_id: Optional[int] = None
"""The network id of the transaction."""

def _get_errors(self: Transaction) -> Dict[str, str]:
errors = super()._get_errors()
if self.ticket_sequence is not None and (
Expand Down

0 comments on commit 972177d

Please sign in to comment.