Quex.offer mod tests (#14478)

* add test

* do some forwards compat stuff

* Add a forwards compat example

* fix test

* Add more forwards compat tests

* Add more forwads compat tests

* Add forwards compatibility for nft1s

* add forward compatibility for nft offers

* Add forwards compatibility tests for DL offers

* Update DL test offers

* lint

* isort

* offer mod bytes

* Fix compression test

* isort again

* Add special offers for <=3.7 (CATs)

* Add special offers for <=3.7 (NFT1s)

* Add special offers for <=3.7 (NFT0s)

* Add special offers for <=3.7 (DLs)

* Check for conflicting items during aggregation

* isort

* Return created old offers properly marked
This commit is contained in:
Matt Hauff 2023-02-06 21:04:39 -07:00 committed by GitHub
parent ba74a983eb
commit 9cc25cc820
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 725 additions and 266 deletions

View File

@ -1144,6 +1144,7 @@ class DataLayerWallet:
driver_dict: Dict[bytes32, PuzzleInfo],
solver: Solver,
fee: uint64 = uint64(0),
old: bool = False,
) -> Offer:
dl_wallet = None
for wallet in wallet_state_manager.wallets.values():
@ -1206,7 +1207,7 @@ class DataLayerWallet:
for k, v in offer_dict.items()
if v > 0
}
return Offer(requested_payments, SpendBundle.aggregate(all_bundles), driver_dict)
return Offer(requested_payments, SpendBundle.aggregate(all_bundles), driver_dict, old)
@staticmethod
async def finish_graftroot_solutions(offer: Offer, solver: Solver) -> Offer:
@ -1280,7 +1281,7 @@ class DataLayerWallet:
spend = new_spend
new_spends.append(spend)
return Offer({}, SpendBundle(new_spends, offer.bundle.aggregated_signature), offer.driver_dict)
return Offer({}, SpendBundle(new_spends, offer.bundle.aggregated_signature), offer.driver_dict, offer.old)
@staticmethod
async def get_offer_summary(offer: Offer) -> Dict[str, Any]:

View File

@ -37,7 +37,14 @@ from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
calculate_synthetic_secret_key,
puzzle_for_pk,
)
from chia.wallet.trading.offer import OFFER_MOD, OFFER_MOD_HASH, NotarizedPayment, Offer
from chia.wallet.trading.offer import (
OFFER_MOD,
OFFER_MOD_HASH,
OFFER_MOD_OLD,
OFFER_MOD_OLD_HASH,
NotarizedPayment,
Offer,
)
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.uncurried_puzzle import uncurry_puzzle
from chia.wallet.util.compute_memos import compute_memos
@ -782,7 +789,10 @@ class NFTWallet:
fee: uint64,
min_coin_amount: Optional[uint64] = None,
max_coin_amount: Optional[uint64] = None,
old: bool = False,
) -> Offer:
DESIRED_OFFER_MOD = OFFER_MOD_OLD if old else OFFER_MOD
DESIRED_OFFER_MOD_HASH = OFFER_MOD_OLD_HASH if old else OFFER_MOD_HASH
# First, let's take note of all the royalty enabled NFTs
royalty_nft_asset_dict: Dict[bytes32, int] = {}
for asset, amount in offer_dict.items():
@ -815,7 +825,9 @@ class NFTWallet:
for asset, amount in fungible_asset_dict.items(): # requested fungible items
if amount > 0 and offer_side_royalty_split > 0:
settlement_ph: bytes32 = (
OFFER_MOD_HASH if asset is None else construct_puzzle(driver_dict[asset], OFFER_MOD).get_tree_hash()
DESIRED_OFFER_MOD_HASH
if asset is None
else construct_puzzle(driver_dict[asset], DESIRED_OFFER_MOD).get_tree_hash()
)
trade_prices.append([uint64(math.floor(amount / offer_side_royalty_split)), settlement_ph])
@ -881,13 +893,13 @@ class NFTWallet:
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
requested_payments, list(all_offered_coins)
)
announcements_to_assert = Offer.calculate_announcements(notarized_payments, driver_dict)
announcements_to_assert = Offer.calculate_announcements(notarized_payments, driver_dict, old)
for asset, payments in royalty_payments.items():
if asset is None: # xch offer
offer_puzzle = OFFER_MOD
royalty_ph = OFFER_MOD_HASH
offer_puzzle = DESIRED_OFFER_MOD
royalty_ph = DESIRED_OFFER_MOD_HASH
else:
offer_puzzle = construct_puzzle(driver_dict[asset], OFFER_MOD)
offer_puzzle = construct_puzzle(driver_dict[asset], DESIRED_OFFER_MOD)
royalty_ph = offer_puzzle.get_tree_hash()
announcements_to_assert.extend(
[
@ -913,12 +925,12 @@ class NFTWallet:
payments = royalty_payments[asset] if asset in royalty_payments else []
tx = await wallet.generate_signed_transaction(
abs(amount),
Offer.ph(),
DESIRED_OFFER_MOD_HASH,
primaries=[
AmountWithPuzzlehash(
{
"amount": uint64(sum(p.amount for _, p in payments)),
"puzzlehash": Offer.ph(),
"puzzlehash": DESIRED_OFFER_MOD_HASH,
"memos": [],
}
)
@ -931,7 +943,7 @@ class NFTWallet:
elif asset not in fungible_asset_dict:
txs = await wallet.generate_signed_transaction(
[abs(amount)],
[Offer.ph()],
[DESIRED_OFFER_MOD_HASH],
fee=fee_left_to_pay,
coins=offered_coins_by_asset[asset],
puzzle_announcements_to_consume=announcements_to_assert,
@ -941,7 +953,7 @@ class NFTWallet:
payments = royalty_payments[asset] if asset in royalty_payments else []
txs = await wallet.generate_signed_transaction(
[abs(amount), sum(p.amount for _, p in payments)],
[Offer.ph(), Offer.ph()],
[DESIRED_OFFER_MOD_HASH, DESIRED_OFFER_MOD_HASH],
fee=fee_left_to_pay,
coins=offered_coins_by_asset[asset],
puzzle_announcements_to_consume=announcements_to_assert,
@ -980,17 +992,19 @@ class NFTWallet:
None,
[
Payment(
Offer.ph(), uint64(sum(p.amount for _, p in duplicate_payments)), []
DESIRED_OFFER_MOD_HASH,
uint64(sum(p.amount for _, p in duplicate_payments)),
[],
).as_condition_args()
],
)
).cons(inner_royalty_sol)
if asset is None: # xch offer
offer_puzzle = OFFER_MOD
royalty_ph = OFFER_MOD_HASH
offer_puzzle = DESIRED_OFFER_MOD
royalty_ph = DESIRED_OFFER_MOD_HASH
else:
offer_puzzle = construct_puzzle(driver_dict[asset], OFFER_MOD)
offer_puzzle = construct_puzzle(driver_dict[asset], DESIRED_OFFER_MOD)
royalty_ph = offer_puzzle.get_tree_hash()
if royalty_coin is None:
for tx in txs:
@ -1031,7 +1045,7 @@ class NFTWallet:
"sibling_solutions": "()",
}
)
royalty_sol = solve_puzzle(driver_dict[asset], solver, OFFER_MOD, inner_royalty_sol)
royalty_sol = solve_puzzle(driver_dict[asset], solver, DESIRED_OFFER_MOD, inner_royalty_sol)
new_coin_spend = CoinSpend(royalty_coin, offer_puzzle, royalty_sol)
additional_bundles.append(SpendBundle([new_coin_spend], G2Element()))
@ -1047,7 +1061,7 @@ class NFTWallet:
# Finally, assemble the tx records properly
txs_bundle = SpendBundle.aggregate([tx.spend_bundle for tx in all_transactions if tx.spend_bundle is not None])
aggregate_bundle = SpendBundle.aggregate([txs_bundle, *additional_bundles])
offer = Offer(notarized_payments, aggregate_bundle, driver_dict)
offer = Offer(notarized_payments, aggregate_bundle, driver_dict, old)
return offer
async def set_bulk_nft_did(

View File

@ -25,7 +25,7 @@ from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo, Solver
from chia.wallet.puzzles.load_clvm import load_clvm_maybe_recompile
from chia.wallet.trade_record import TradeRecord
from chia.wallet.trading.offer import NotarizedPayment, Offer
from chia.wallet.trading.offer import OFFER_MOD_OLD_HASH, NotarizedPayment, Offer
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.trading.trade_store import TradeStore
from chia.wallet.transaction_record import TransactionRecord
@ -436,6 +436,7 @@ class TradeManager:
fee: uint64 = uint64(0),
min_coin_amount: Optional[uint64] = None,
max_coin_amount: Optional[uint64] = None,
old: bool = False,
) -> Union[Tuple[Literal[True], Offer, None], Tuple[Literal[False], None, str]]:
"""
Offer is dictionary of wallet ids and amount
@ -516,7 +517,7 @@ class TradeManager:
raise ValueError(f"Wallet for asset id {asset_id} is not properly integrated with TradeManager")
potential_special_offer: Optional[Offer] = await self.check_for_special_offer_making(
offer_dict_no_ints, driver_dict, solver, fee, min_coin_amount, max_coin_amount
offer_dict_no_ints, driver_dict, solver, fee, min_coin_amount, max_coin_amount, old
)
if potential_special_offer is not None:
@ -526,7 +527,7 @@ class TradeManager:
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = Offer.notarize_payments(
requested_payments, all_coins
)
announcements_to_assert = Offer.calculate_announcements(notarized_payments, driver_dict)
announcements_to_assert = Offer.calculate_announcements(notarized_payments, driver_dict, old)
all_transactions: List[TransactionRecord] = []
fee_left_to_pay: uint64 = fee
@ -539,7 +540,7 @@ class TradeManager:
if wallet.type() == WalletType.STANDARD_WALLET:
tx = await wallet.generate_signed_transaction(
abs(offer_dict[id]),
Offer.ph(),
OFFER_MOD_OLD_HASH if old else Offer.ph(),
fee=fee_left_to_pay,
coins=set(selected_coins),
puzzle_announcements_to_consume=announcements_to_assert,
@ -552,7 +553,7 @@ class TradeManager:
txs = await wallet.generate_signed_transaction(
# [abs(offer_dict[id])],
amounts,
[Offer.ph()],
[OFFER_MOD_OLD_HASH if old else Offer.ph()],
fee=fee_left_to_pay,
coins=set(selected_coins),
puzzle_announcements_to_consume=announcements_to_assert,
@ -562,7 +563,7 @@ class TradeManager:
# ATTENTION: new_wallets
txs = await wallet.generate_signed_transaction(
[abs(offer_dict[id])],
[Offer.ph()],
[OFFER_MOD_OLD_HASH if old else Offer.ph()],
fee=fee_left_to_pay,
coins=set(selected_coins),
puzzle_announcements_to_consume=announcements_to_assert,
@ -575,7 +576,7 @@ class TradeManager:
[x.spend_bundle for x in all_transactions if x.spend_bundle is not None]
)
offer = Offer(notarized_payments, total_spend_bundle, driver_dict)
offer = Offer(notarized_payments, total_spend_bundle, driver_dict, old)
return True, offer, None
except Exception as e:
@ -735,6 +736,7 @@ class TradeManager:
fee=fee,
min_coin_amount=min_coin_amount,
max_coin_amount=max_coin_amount,
old=offer.old,
)
if not result[0] or result[1] is None:
raise ValueError(result[2])
@ -798,6 +800,7 @@ class TradeManager:
fee: uint64 = uint64(0),
min_coin_amount: Optional[uint64] = None,
max_coin_amount: Optional[uint64] = None,
old: bool = False,
) -> Optional[Offer]:
for puzzle_info in driver_dict.values():
if (
@ -807,7 +810,7 @@ class TradeManager:
== AssetType.ROYALTY_TRANSFER_PROGRAM.value
):
return await NFTWallet.make_nft1_offer(
self.wallet_state_manager, offer_dict, driver_dict, fee, min_coin_amount, max_coin_amount
self.wallet_state_manager, offer_dict, driver_dict, fee, min_coin_amount, max_coin_amount, old
)
elif (
puzzle_info.check_type(
@ -819,7 +822,7 @@ class TradeManager:
and puzzle_info.also()["updater_hash"] == ACS_MU_PH # type: ignore
):
return await DataLayerWallet.make_update_offer(
self.wallet_state_manager, offer_dict, driver_dict, solver, fee
self.wallet_state_manager, offer_dict, driver_dict, solver, fee, old
)
return None

View File

@ -33,7 +33,7 @@ from chia.wallet.util.puzzle_compression import (
)
OFFER_MOD_OLD = load_clvm_maybe_recompile("settlement_payments_old.clvm")
OFFER_MOD = load_clvm_maybe_recompile("settlement_payments_old.clvm")
OFFER_MOD = load_clvm_maybe_recompile("settlement_payments.clvm")
OFFER_MOD_OLD_HASH = OFFER_MOD_OLD.get_tree_hash()
OFFER_MOD_HASH = OFFER_MOD.get_tree_hash()
ZERO_32 = bytes32([0] * 32)
@ -71,6 +71,7 @@ class Offer:
] # The key is the asset id of the asset being requested
bundle: SpendBundle
driver_dict: Dict[bytes32, PuzzleInfo] # asset_id -> asset driver
old: bool = False
@staticmethod
def ph() -> bytes32:
@ -98,16 +99,20 @@ class Offer:
# The announcements returned from this function must be asserted in whatever spend bundle is created by the wallet
@staticmethod
def calculate_announcements(
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]], driver_dict: Dict[bytes32, PuzzleInfo]
notarized_payments: Dict[Optional[bytes32], List[NotarizedPayment]],
driver_dict: Dict[bytes32, PuzzleInfo],
old: bool = False,
) -> List[Announcement]:
announcements: List[Announcement] = []
for asset_id, payments in notarized_payments.items():
if asset_id is not None:
if asset_id not in driver_dict:
raise ValueError("Cannot calculate announcements without driver of requested item")
settlement_ph: bytes32 = construct_puzzle(driver_dict[asset_id], OFFER_MOD).get_tree_hash()
settlement_ph: bytes32 = construct_puzzle(
driver_dict[asset_id], OFFER_MOD_OLD if old else OFFER_MOD
).get_tree_hash()
else:
settlement_ph = OFFER_MOD_HASH
settlement_ph = OFFER_MOD_OLD_HASH if old else OFFER_MOD_HASH
msg: bytes32 = Program.to((payments[0].nonce, [p.as_condition_args() for p in payments])).get_tree_hash()
announcements.append(Announcement(settlement_ph, msg))
@ -374,7 +379,8 @@ class Offer:
total_requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
total_bundle = SpendBundle([], G2Element())
total_driver_dict: Dict[bytes32, PuzzleInfo] = {}
for offer in offers:
old: bool = False
for i, offer in enumerate(offers):
# First check for any overlap in inputs
total_inputs: Set[Coin] = {cs.coin for cs in total_bundle.coin_spends}
offer_inputs: Set[Coin] = {cs.coin for cs in offer.bundle.coin_spends}
@ -394,8 +400,13 @@ class Offer:
total_bundle = SpendBundle.aggregate([total_bundle, offer.bundle])
total_driver_dict.update(offer.driver_dict)
if i == 0:
old = offer.old
else:
if offer.old != old:
raise ValueError("Attempting to aggregate two offers with different mods")
return cls(total_requested_payments, total_bundle, total_driver_dict)
return cls(total_requested_payments, total_bundle, total_driver_dict, old)
# Validity is defined by having enough funds within the offer to satisfy both sides
def is_valid(self) -> bool:
@ -539,7 +550,11 @@ class Offer:
requested_payments: Dict[Optional[bytes32], List[NotarizedPayment]] = {}
driver_dict: Dict[bytes32, PuzzleInfo] = {}
leftover_coin_spends: List[CoinSpend] = []
old: bool = False
for coin_spend in bundle.coin_spends:
if not old and bytes(OFFER_MOD_OLD) in bytes(coin_spend):
old = True
driver = match_puzzle(uncurry_puzzle(coin_spend.puzzle_reveal.to_program()))
if driver is not None:
asset_id = create_asset_id(driver)
@ -560,7 +575,7 @@ class Offer:
else:
leftover_coin_spends.append(coin_spend)
return cls(requested_payments, SpendBundle(leftover_coin_spends, bundle.aggregated_signature), driver_dict)
return cls(requested_payments, SpendBundle(leftover_coin_spends, bundle.aggregated_signature), driver_dict, old)
def name(self) -> bytes32:
return self.to_spend_bundle().name()

View File

@ -13,7 +13,7 @@ from chia.util.ints import uint64
from chia.wallet.cat_wallet.cat_utils import construct_cat_puzzle
from chia.wallet.puzzles.cat_loader import CAT_MOD
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import puzzle_for_pk
from chia.wallet.trading.offer import OFFER_MOD
from chia.wallet.trading.offer import OFFER_MOD, OFFER_MOD_OLD
from chia.wallet.util.puzzle_compression import (
LATEST_VERSION,
compress_object_with_puzzles,
@ -88,7 +88,8 @@ class TestPuzzleCompression:
def test_lowest_best_version(self):
assert lowest_best_version([bytes(CAT_MOD)]) == 4
assert lowest_best_version([bytes(OFFER_MOD)]) == 2
assert lowest_best_version([bytes(OFFER_MOD_OLD)]) == 2
assert lowest_best_version([bytes(OFFER_MOD)]) == 5
def test_version_override(self):
coin_spend = CoinSpend(

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import replace
from typing import Any, Dict, List, Optional
import pytest
@ -22,7 +23,7 @@ from chia.wallet.outer_puzzles import AssetType
from chia.wallet.payment import Payment
from chia.wallet.puzzle_drivers import PuzzleInfo
from chia.wallet.puzzles.cat_loader import CAT_MOD
from chia.wallet.trading.offer import NotarizedPayment, Offer
from chia.wallet.trading.offer import OFFER_MOD, NotarizedPayment, Offer
from tests.clvm.benchmark_costs import cost_of_spend_bundle
acs = Program.to(1)
@ -299,6 +300,30 @@ class TestOfferLifecycle:
}
assert new_offer.is_valid()
# Test preventing TAIL from running during exchange
blue_cat_puz: Program = construct_cat_puzzle(CAT_MOD, str_to_tail_hash("blue"), OFFER_MOD)
blue_spend: CoinSpend = CoinSpend(
Coin(bytes32(32), blue_cat_puz.get_tree_hash(), uint64(0)),
blue_cat_puz,
Program.to([[bytes32(32), [bytes32(32), 200, ["hey there"]]]]),
)
new_spends_list: List[CoinSpend] = [blue_spend, *new_offer.to_spend_bundle().coin_spends]
tail_offer: Offer = Offer.from_spend_bundle(SpendBundle(new_spends_list, G2Element()))
valid_spend = tail_offer.to_valid_spend(bytes32(32))
real_blue_spend = [spend for spend in valid_spend.coin_spends if b"hey there" in bytes(spend)][0]
real_blue_spend_replaced = replace(
real_blue_spend,
solution=real_blue_spend.solution.to_program().replace(
ffrfrf=Program.to(-113), ffrfrr=Program.to([str_to_tail("blue"), []])
),
)
valid_spend = SpendBundle(
[real_blue_spend_replaced, *[spend for spend in valid_spend.coin_spends if spend != real_blue_spend]],
G2Element(),
)
with pytest.raises(ValueError, match="clvm raise"):
valid_spend.additions()
# Test (de)serialization
assert Offer.from_bytes(bytes(new_offer)) == new_offer
@ -308,6 +333,7 @@ class TestOfferLifecycle:
# Make sure we can actually spend the offer once it's valid
arbitrage_ph: bytes32 = Program.to([3, [], [], 1]).get_tree_hash()
offer_bundle: SpendBundle = new_offer.to_valid_spend(arbitrage_ph)
result = await sim_client.push_tx(offer_bundle)
assert result == (MempoolInclusionStatus.SUCCESS, None)
self.cost["complex offer"] = cost_of_spend_bundle(offer_bundle)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long