Skip to content

Commit

Permalink
Basis for rune multisig fuzz test
Browse files Browse the repository at this point in the history
  • Loading branch information
koirikivi committed Apr 29, 2024
1 parent 02aa8b7 commit 3be74dc
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 5 deletions.
12 changes: 10 additions & 2 deletions bridge_node/bridge/common/ord/multisig.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,12 +202,20 @@ def get_rune_balance_at_output(self, *, txid: str, vout: int, rune_name: str) ->
vout=vout,
).get_rune_balance(rune_name)

def send_runes(self, transfers: list[RuneTransfer]):
def send_runes(
self,
transfers: list[RuneTransfer],
*,
fee_rate_sat_per_vbyte: int = 50,
):
if self._num_required_signers != 1:
raise ValueError(
"send_runes can only be used with a 1-of-m multisig",
)
psbt = self.create_rune_psbt(transfers)
psbt = self.create_rune_psbt(
transfers,
fee_rate_sat_per_vbyte=fee_rate_sat_per_vbyte,
)
signed_psbt = self.sign_psbt(psbt, finalize=True)
return self.broadcast_psbt(signed_psbt)

Expand Down
45 changes: 44 additions & 1 deletion bridge_node/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions bridge_node/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pre-commit = "^3.6.0"
ipython = "^8.19.0"
ipdb = "^0.13.13"
pyyaml = "^6.0.1"
hypothesis = "^6.100.2"

[tool.pytest.ini_options]
addopts = [
Expand All @@ -71,6 +72,7 @@ testpaths = ["tests"]
python_files = ["test_*.py", "tests.py", "*_tests.py"]
markers = [
"integration: marks tests as integration test, requiring startup of the slow integration testing harness (deselect with '-m \"not integration\"')",
"fuzz: marks tests as a hypothesis fuzz test, which might take a long time (deselect with '-m \"not fuzz\"')",
]
filterwarnings = [
"ignore:Secrets file not found.*:UserWarning:.*",
Expand Down
173 changes: 173 additions & 0 deletions bridge_node/tests/common/ord/test_ord_multisig_fuzz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
from __future__ import annotations

import logging
import time
from collections import defaultdict
from dataclasses import dataclass
from decimal import Decimal

import pytest

from bridge.common.ord.multisig import OrdMultisig
from bridge.common.utils import to_base_units
from tests.services.bitcoind import (
BitcoindService,
BitcoinWallet,
)
from tests.services.ord import (
OrdService,
OrdWallet,
)

logger = logging.getLogger(__name__)
SUPPLY_DECIMAL = Decimal("1000000")
SUPPLY_RAW = to_base_units(SUPPLY_DECIMAL, 18)


class RuneBalanceDict(dict):
def __add__(self, other: RuneBalanceDict) -> RuneBalanceDict:
new = RuneBalanceDict(self)
for rune, amount in other.items():
new[rune] = new.get(rune, 0) + amount
return new

def __sub__(self, other: RuneBalanceDict) -> RuneBalanceDict:
new = RuneBalanceDict(self)
for rune, amount in other.items():
new[rune] = new.get(rune, 0) - amount
return new


class BalanceTester:
def __init__(
self,
ord: OrdService, # noqa
bitcoind: BitcoindService,
):
self._ord = ord
self._bitcoind = bitcoind

def get_satoshi_balance(
self,
wallet: str | BitcoinWallet | OrdWallet | OrdMultisig,
) -> int:
rpc = self._get_wallet_rpc(wallet)
return round(rpc.call("getbalance") * Decimal("1e8"))

def get_rune_balances(
self,
wallet: str | BitcoinWallet | OrdWallet | OrdMultisig,
) -> RuneBalanceDict:
self._ord.sync_with_bitcoind()
rpc = self._get_wallet_rpc(wallet)
utxos = rpc.listunspent(1, 9999999, [], False)
rune_balances = defaultdict(int)
for utxo in utxos:
txid = utxo["txid"]
vout = utxo["vout"]
for i in range(30):
ord_output = self._ord.api_client.get_output(txid, vout)
if ord_output["spent"]:
break
if ord_output["indexed"]:
break
time.sleep(i * 0.1)
else:
raise ValueError("Output not indexed after 30 tries")
for rune, entry in ord_output["runes"]:
rune_balances[rune] += entry["amount"]
return RuneBalanceDict(dict(rune_balances))

def get_total_rune_balances(self, wallets: list[str | BitcoinWallet | OrdWallet | OrdMultisig]) -> RuneBalanceDict:
total_rune_balances = RuneBalanceDict()
for wallet in wallets:
total_rune_balances += self.get_rune_balances(wallet)
return total_rune_balances

def _get_wallet_rpc(self, wallet: str | BitcoinWallet | OrdWallet | OrdMultisig):
if isinstance(wallet, str):
wallet_name = wallet
else:
wallet_name = wallet.name
return self._bitcoind.get_wallet_rpc(wallet_name)


@pytest.fixture(scope="module")
def balance_tester(
ord: OrdService, # noqa
bitcoind: BitcoindService,
) -> BalanceTester:
return BalanceTester(
ord=ord,
bitcoind=bitcoind,
)


@dataclass
class Setup:
multisig: OrdMultisig
runes: list[str]
funder_wallet: OrdWallet
user_wallets: list[OrdWallet]
all_wallet_names: list[str]
initial_rune_balances: RuneBalanceDict


@pytest.fixture(scope="module")
def setup(
ord: OrdService, # noqa A002
bitcoind: BitcoindService,
rune_factory,
multisig_factory,
root_ord_wallet: OrdWallet,
) -> Setup:
# We use a 1-of-2 multisig because it simplifies tests, and the PSBT creation logic
# is unaffected by the number of signers
multisig, _ = multisig_factory(
required=1,
num_signers=2,
)

# TODO: don't need funder wallet
funder_wallet = ord.create_test_wallet("funder")
user_wallets = [ord.create_test_wallet("user") for _ in range(5)]
runes = rune_factory(
"AAAAAA",
"BBBBBB",
"CCCCCC",
"DDDDDD",
"EEEEEE",
receiver=multisig.change_address,
supply=SUPPLY_DECIMAL,
divisibility=18,
)

all_wallet_names = [multisig.name]
all_wallet_names += [wallet.name for wallet in user_wallets]
all_wallet_names += [funder_wallet.name]

initial_rune_balances = RuneBalanceDict()
for rune in runes:
initial_rune_balances[rune] = SUPPLY_RAW

return Setup(
multisig=multisig,
runes=runes,
funder_wallet=funder_wallet,
user_wallets=user_wallets,
all_wallet_names=all_wallet_names,
initial_rune_balances=initial_rune_balances,
)


@pytest.mark.fuzz
def test_ord_multisig_invariants(
setup: Setup,
balance_tester: BalanceTester,
ord: OrdService, # noqa
):
# TODO: implement this
total_rune_balances_before = balance_tester.get_total_rune_balances(setup.all_wallet_names)
# multisig_rune_balances_before = balance_tester.get_rune_balances(setup.multisig)
# multisig_satoshi_balance_before = balance_tester.get_rune_balances(setup.multisig)
assert total_rune_balances_before == setup.initial_rune_balances
10 changes: 8 additions & 2 deletions bridge_node/tests/services/bitcoind.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,12 @@ def fund_addresses(self, *addresses: str, amount_to_send: Decimal = Decimal(1)):

self.mine(1)

def get_wallet_rpc_url(self, wallet_name):
def get_wallet_rpc_url(self, wallet_name: str) -> str:
return f"{self.rpc_url}/wallet/{wallet_name}"

def get_wallet_rpc(self, wallet_name: str) -> BitcoinRPC:
return BitcoinRPC(self.get_wallet_rpc_url(wallet_name))

def load_or_create_wallet(
self, wallet_name: str, *, blank: bool = False, disable_private_keys: bool = False
) -> tuple[BitcoinWallet, bool]:
Expand All @@ -145,7 +148,10 @@ def load_or_create_wallet(
)

logger.info("Created wallet %s", wallet_name)
wallet = BitcoinWallet(name=wallet_name, rpc=BitcoinRPC(self.get_wallet_rpc_url(wallet_name)))
wallet = BitcoinWallet(
name=wallet_name,
rpc=self.get_wallet_rpc(wallet_name),
)

return wallet, True

Expand Down

0 comments on commit 3be74dc

Please sign in to comment.