added duplicate sigs where needed
This commit is contained in:
parent
9365cb3bee
commit
c9f3a8cdeb
|
@ -301,7 +301,6 @@ class MempoolManager:
|
|||
added_to_potential = True
|
||||
potential_error = error
|
||||
break
|
||||
|
||||
hash_key_pairs.extend(
|
||||
hash_key_pairs_for_conditions_dict(
|
||||
npc.condition_dict, npc.coin_name
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Tuple
|
||||
from src.types.sized_bytes import bytes32, bytes48, bytes96
|
||||
from src.util.streamable import streamable, Streamable
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@streamable
|
||||
class APInfo(Streamable):
|
||||
authoriser_pubkey: Optional[bytes48]
|
||||
my_pubkey: Optional[bytes48]
|
||||
contacts: Optional[
|
||||
List[Tuple[str, bytes32, bytes96]]
|
||||
] # list of (name, puzhash, BLSSig)
|
||||
change_signature: Optional[bytes96]
|
|
@ -0,0 +1,75 @@
|
|||
from typing import Optional, Tuple
|
||||
|
||||
from clvm_tools import binutils
|
||||
import clvm
|
||||
import string
|
||||
from src.types.BLSSignature import BLSSignature
|
||||
from src.types.program import Program
|
||||
from src.types.coin import Coin
|
||||
from src.types.coin_solution import CoinSolution
|
||||
from src.types.condition_opcodes import ConditionOpcode
|
||||
|
||||
# This is for spending an existing coloured coin
|
||||
from src.types.sized_bytes import bytes32
|
||||
from src.types.spend_bundle import SpendBundle
|
||||
from src.util.ints import uint64
|
||||
from blspy import PublicKey
|
||||
|
||||
|
||||
# this is for wallet A to generate the permitted puzzlehashes and sign them ahead of time
|
||||
# returns a list of tuples of (puzhash, signature)
|
||||
# not sure about how best to communicate/store who/what the puzzlehashes are, or if this is even important
|
||||
def ap_generate_signatures(puzhashes, oldpuzzlehash, a_wallet, a_pubkey_used):
|
||||
puzhash_signature_list = []
|
||||
for p in puzhashes:
|
||||
signature = a_wallet.sign(p, a_pubkey_used)
|
||||
puzhash_signature_list.append((p, signature))
|
||||
return puzhash_signature_list
|
||||
|
||||
|
||||
# this creates our authorised payee puzzle
|
||||
def ap_make_puzzle(a_pubkey, b_pubkey):
|
||||
a_pubkey_formatted = f"0x{bytes(a_pubkey).hex()}"
|
||||
b_pubkey_formatted = f"0x{bytes(b_pubkey).hex()}"
|
||||
|
||||
# Mode one is for spending to one of the approved destinations
|
||||
# Solution contains (option 1 flag, new puzzle, new solution, my_primary_input, wallet_puzzle_hash)
|
||||
|
||||
# Below is the result of compiling ap_wallet.clvm
|
||||
puz = f"((c (q (c (c (q 57) (c (q {b_pubkey_formatted}) (c ((c (r (f (a))) (c (f (a)) (c (c (f (r (a))) (c (f (r (r (a)))) (c (f (r (r (r (a))))) (c (f (r (r (r (r (a)))))) (q ()))))) (q ()))))) (q ())))) ((c (f (f (a))) (c (f (a)) (c ((c (f (r (a))) (f (r (r (a)))))) (c (q ()) (c (f (r (r (r (a))))) (c (f (r (r (r (r (a)))))) (q (()))))))))))) (c (q (((c (i (f (r (a))) (q ((c (i (= (f (f (f (r (a))))) (q 51)) (q ((c (f (f (a))) (c (f (a)) (c (r (f (r (a)))) (c (c (f (f (r (a)))) (c (c (q 50) (c (q {a_pubkey_formatted}) (c (f (r (f (f (r (a)))))) (q ())))) (f (r (r (a)))))) (c (f (r (r (r (a))))) (c (f (r (r (r (r (a)))))) (c (+ (f (r (r (f (f (r (a))))))) (f (r (r (r (r (r (a)))))))) (q ())))))))))) (q ((c (f (f (a))) (c (f (a)) (c (r (f (r (a)))) (c (c (f (f (r (a)))) (f (r (r (a))))) (c (f (r (r (r (a))))) (c (f (r (r (r (r (a)))))) (c (f (r (r (r (r (r (a))))))) (q ()))))))))))) (a)))) (q (c (c (q 53) (c (sha256 (f (r (r (r (a))))) (f (r (r (r (r (a)))))) (f (r (r (r (r (r (a)))))))) (q ()))) (f (r (r (a))))))) (a))) (c (i (l (f (r (a)))) (q (sha256 (q 2) ((c (r (f (a))) (c (f (a)) (c (f (f (r (a)))) (q ()))))) ((c (r (f (a))) (c (f (a)) (c (r (f (r (a)))) (q ()))))))) (q (sha256 (q 1) (f (r (a)))))) (a)))) (a))))"
|
||||
return Program(binutils.assemble(puz))
|
||||
|
||||
|
||||
# returns the ProgramHash of a new puzzle
|
||||
def ap_get_new_puzzlehash(a_pubkey_serialized, b_pubkey_serialized):
|
||||
return ap_make_puzzle(a_pubkey_serialized, b_pubkey_serialized).get_tree_hash()
|
||||
|
||||
|
||||
# this allows wallet A to approve of new puzzlehashes/spends from wallet B that weren't in the original list
|
||||
def ap_sign_output_newpuzzlehash(newpuzzlehash, a_wallet, a_pubkey_used):
|
||||
signature = a_wallet.sign(newpuzzlehash, a_pubkey_used)
|
||||
return signature
|
||||
|
||||
|
||||
# creates the solution that will allow wallet B to spend the coin
|
||||
# Wallet B is allowed to make multiple spends but must spend the coin in its entirety
|
||||
def ap_make_solution(outputs, my_primary_input, my_puzzle_hash):
|
||||
sol = "((a) ("
|
||||
for puzhash, amount in outputs:
|
||||
sol += f"(0x{ConditionOpcode.CREATE_COIN.hex()} 0x{puzhash.hex()} {amount})"
|
||||
sol += f") 0x{my_primary_input.hex()} 0x{my_puzzle_hash.hex()})"
|
||||
return Program(binutils.assemble(sol))
|
||||
|
||||
|
||||
"""
|
||||
Copyright 2020 Chia Network Inc
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
|
@ -0,0 +1,354 @@
|
|||
import logging
|
||||
import clvm
|
||||
from typing import Dict, Optional, List, Any, Set
|
||||
from src.types.BLSSignature import BLSSignature
|
||||
from src.types.coin import Coin
|
||||
from src.types.coin_solution import CoinSolution
|
||||
from src.types.program import Program
|
||||
from src.types.spend_bundle import SpendBundle
|
||||
from src.types.sized_bytes import bytes32
|
||||
from src.util.ints import uint64, uint32
|
||||
from src.wallet.BLSPrivateKey import BLSPrivateKey
|
||||
from src.wallet.ap_wallet.ap_info import APInfo
|
||||
from src.wallet.transaction_record import TransactionRecord
|
||||
from src.wallet.util.wallet_types import WalletType
|
||||
from src.wallet.wallet import Wallet
|
||||
from src.wallet.wallet_coin_record import WalletCoinRecord
|
||||
from src.wallet.wallet_info import WalletInfo
|
||||
from src.wallet.derivation_record import DerivationRecord
|
||||
from src.wallet.ap_wallet import ap_puzzles
|
||||
from blspy import PublicKey
|
||||
from src.util.hash import std_hash
|
||||
|
||||
|
||||
class APWallet:
|
||||
wallet_state_manager: Any
|
||||
log: logging.Logger
|
||||
wallet_info: WalletInfo
|
||||
cc_coin_record: WalletCoinRecord
|
||||
ap_info: APInfo
|
||||
standard_wallet: Wallet
|
||||
base_puzzle_program: Optional[bytes]
|
||||
base_inner_puzzle_hash: Optional[bytes32]
|
||||
|
||||
@staticmethod
|
||||
async def create_wallet_for_ap(
|
||||
wallet_state_manager: Any,
|
||||
wallet: Wallet,
|
||||
authoriser_pubkey: PublicKey,
|
||||
name: str = None,
|
||||
):
|
||||
|
||||
self = APWallet()
|
||||
self.base_puzzle_program = None
|
||||
self.base_inner_puzzle_hash = None
|
||||
self.standard_wallet = wallet
|
||||
if name:
|
||||
self.log = logging.getLogger(name)
|
||||
else:
|
||||
self.log = logging.getLogger(__name__)
|
||||
|
||||
self.wallet_state_manager = wallet_state_manager
|
||||
self.ap_info = APInfo(bytes(authoriser_pubkey), None, [], None)
|
||||
info_as_string = bytes(self.ap_info).hex()
|
||||
self.wallet_info = await wallet_state_manager.user_store.create_wallet(
|
||||
"AP Wallet", WalletType.COLOURED_COIN, info_as_string
|
||||
)
|
||||
if self.wallet_info is None:
|
||||
raise
|
||||
await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id)
|
||||
devrec = await self.wallet_state_manager.get_unused_derivation_record(
|
||||
self.wallet_info.id
|
||||
)
|
||||
pubkey = devrec.pubkey
|
||||
self.ap_info = APInfo(bytes(authoriser_pubkey), bytes(pubkey), [], None)
|
||||
return self
|
||||
|
||||
async def set_sender_values(self, a_pubkey_used, sig=None):
|
||||
if sig is not None:
|
||||
ap_info = APInfo(
|
||||
a_pubkey_used, self.ap_info.my_pubkey, self.ap_info.contacts, sig
|
||||
)
|
||||
else:
|
||||
ap_info = APInfo(
|
||||
a_pubkey_used,
|
||||
self.ap_info.my_pubkey,
|
||||
self.ap_info.contacts,
|
||||
self.ap_info.authorised_signature,
|
||||
)
|
||||
await self.save_info(ap_info)
|
||||
puzzlehash = self.puzzle_for_pk(self.ap_info.my_pubkey).get_tree_hash()
|
||||
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(
|
||||
self.ap_info.my_pubkey
|
||||
)
|
||||
derivation_paths = [
|
||||
DerivationRecord(
|
||||
uint32(index),
|
||||
puzzlehash,
|
||||
self.ap_info.my_pubkey,
|
||||
self.wallet_info.type,
|
||||
uint32(self.wallet_info.id),
|
||||
)
|
||||
]
|
||||
|
||||
await self.wallet_state_manager.puzzle_store.add_derivation_paths(
|
||||
derivation_paths
|
||||
)
|
||||
return
|
||||
|
||||
async def coin_added(
|
||||
self, coin: Coin, height: int, header_hash: bytes32, removals: List[Coin]
|
||||
):
|
||||
|
||||
return
|
||||
|
||||
async def get_confirmed_balance(self) -> uint64:
|
||||
record_list: Set[
|
||||
WalletCoinRecord
|
||||
] = await self.wallet_state_manager.wallet_store.get_unspent_coins_for_wallet(
|
||||
self.wallet_info.id
|
||||
)
|
||||
|
||||
amount: uint64 = uint64(0)
|
||||
for record in record_list:
|
||||
amount = uint64(amount + record.coin.amount)
|
||||
|
||||
self.log.info(f"Confirmed balance for ap wallet is {amount}")
|
||||
return uint64(amount)
|
||||
|
||||
async def get_unconfirmed_balance(self) -> uint64:
|
||||
confirmed = await self.get_confirmed_balance()
|
||||
unconfirmed_tx: List[
|
||||
TransactionRecord
|
||||
] = await self.wallet_state_manager.tx_store.get_unconfirmed_for_wallet(
|
||||
self.wallet_info.id
|
||||
)
|
||||
addition_amount = 0
|
||||
removal_amount = 0
|
||||
|
||||
for record in unconfirmed_tx:
|
||||
if record.incoming:
|
||||
addition_amount += record.amount
|
||||
else:
|
||||
removal_amount += record.amount
|
||||
|
||||
result = confirmed - removal_amount + addition_amount
|
||||
|
||||
self.log.info(f"Unconfirmed balance for ap wallet is {result}")
|
||||
return uint64(result)
|
||||
|
||||
async def add_contact(self, name, puzhash, new_signature):
|
||||
authed_pk = PublicKey.from_bytes(self.ap_info.authoriser_pubkey)
|
||||
assert new_signature.validate([BLSSignature.PkMessagePair(authed_pk, puzhash)])
|
||||
current_contacts = self.ap_info.contacts
|
||||
current_contacts.append((name, puzhash, new_signature))
|
||||
|
||||
new_ap_info = APInfo(
|
||||
self.ap_info.authoriser_pubkey,
|
||||
self.ap_info.my_pubkey,
|
||||
current_contacts,
|
||||
self.ap_info.change_signature,
|
||||
)
|
||||
await self.save_info(new_ap_info)
|
||||
return
|
||||
|
||||
def get_contacts(self):
|
||||
return self.ap_info.contacts
|
||||
|
||||
def puzzle_for_pk(self, pubkey) -> Program:
|
||||
ap_puzzle: Program = ap_puzzles.ap_make_puzzle(
|
||||
self.ap_info.authoriser_pubkey, pubkey
|
||||
)
|
||||
return ap_puzzle
|
||||
|
||||
async def get_new_puzzle_hash(self):
|
||||
return (
|
||||
await self.wallet_state_manager.get_unused_derivation_record(
|
||||
self.wallet_info.id
|
||||
)
|
||||
).puzzle_hash
|
||||
|
||||
async def select_coins(self, amount: uint64) -> Optional[Set[Coin]]:
|
||||
""" Returns a set of coins that can be used for generating a new transaction. """
|
||||
async with self.wallet_state_manager.lock:
|
||||
spendable_am = await self.get_confirmed_balance()
|
||||
|
||||
if amount > spendable_am:
|
||||
self.log.warning(
|
||||
f"Can't select amount higher than our spendable balance {amount}, spendable {spendable_am}"
|
||||
)
|
||||
return None
|
||||
|
||||
self.log.info(f"About to select coins for amount {amount}")
|
||||
unspent: List[WalletCoinRecord] = list(
|
||||
await self.wallet_state_manager.get_spendable_coins_for_wallet(
|
||||
self.wallet_info.id
|
||||
)
|
||||
)
|
||||
sum = 0
|
||||
used_coins: Set = set()
|
||||
|
||||
# Use older coins first
|
||||
unspent.sort(key=lambda r: r.confirmed_block_index)
|
||||
|
||||
# Try to use coins from the store, if there isn't enough of "unused"
|
||||
# coins use change coins that are not confirmed yet
|
||||
unconfirmed_removals: Dict[
|
||||
bytes32, Coin
|
||||
] = await self.wallet_state_manager.unconfirmed_removals_for_wallet(
|
||||
self.wallet_info.id
|
||||
)
|
||||
for coinrecord in unspent:
|
||||
if sum >= amount and len(used_coins) > 0:
|
||||
break
|
||||
if coinrecord.coin.name() in unconfirmed_removals:
|
||||
continue
|
||||
sum += coinrecord.coin.amount
|
||||
used_coins.add(coinrecord.coin)
|
||||
self.log.info(
|
||||
f"Selected coin: {coinrecord.coin.name()} at height {coinrecord.confirmed_block_index}!"
|
||||
)
|
||||
|
||||
# This happens when we couldn't use one of the coins because it's already used
|
||||
# but unconfirmed, and we are waiting for the change. (unconfirmed_additions)
|
||||
if sum < amount:
|
||||
raise ValueError(
|
||||
"Can't make this transaction at the moment. Waiting for the change from the previous transaction."
|
||||
)
|
||||
|
||||
self.log.info(f"Successfully selected coins: {used_coins}")
|
||||
return used_coins
|
||||
|
||||
# this is for sending a recieved ap coin, not creating a new ap coin
|
||||
async def ap_generate_unsigned_transaction(
|
||||
self, coins, puzzlehash_amount_list, my_puz
|
||||
):
|
||||
# we only have/need one coin in this wallet at any time - this code can be improved
|
||||
spent = 0
|
||||
spends = []
|
||||
sigs = []
|
||||
current_puzhash = 0
|
||||
for coin in coins:
|
||||
coin_amount_left = coin.amount
|
||||
puzhash_amount = []
|
||||
while coin_amount_left > 0:
|
||||
if (
|
||||
coin_amount_left + spent
|
||||
>= puzzlehash_amount_list[current_puzhash][1]
|
||||
):
|
||||
new_coin_amount_left = (
|
||||
coin_amount_left + spent
|
||||
) - puzzlehash_amount_list[current_puzhash][1]
|
||||
rest = coin_amount_left - new_coin_amount_left
|
||||
puzhash_amount.append(
|
||||
(puzzlehash_amount_list[current_puzhash][0], rest)
|
||||
)
|
||||
sigs.append(puzzlehash_amount_list[current_puzhash][2])
|
||||
|
||||
coin_amount_left = new_coin_amount_left
|
||||
current_puzhash = current_puzhash + 1
|
||||
spent = 0
|
||||
else:
|
||||
puzhash_amount.append(
|
||||
(puzzlehash_amount_list[current_puzhash][0], coin_amount_left)
|
||||
)
|
||||
sigs.append(puzzlehash_amount_list[current_puzhash][2])
|
||||
spent += coin_amount_left
|
||||
coin_amount_left = 0
|
||||
solution = ap_puzzles.ap_make_solution(
|
||||
puzhash_amount, coin.parent_coin_info, my_puz.get_tree_hash()
|
||||
)
|
||||
spends.append((my_puz, CoinSolution(coin, solution)))
|
||||
return spends, sigs
|
||||
|
||||
# this allows wallet A to approve of new puzzlehashes/spends from wallet B that weren't in the original list
|
||||
def ap_sign_output_newpuzzlehash(self, puzzlehash, newpuzzlehash, b_pubkey_used):
|
||||
pubkey, secretkey = self.get_keys(puzzlehash, None, b_pubkey_used)
|
||||
signature = secretkey.sign(newpuzzlehash)
|
||||
return signature
|
||||
|
||||
# this is for sending a locked coin
|
||||
# Wallet B must sign the whole transaction, and the appropriate puzhash signature from A must be included
|
||||
async def ap_sign_transaction(
|
||||
self, spends: (Program, [CoinSolution]), sigs: List[BLSSignature]
|
||||
):
|
||||
pubkey = PublicKey.from_bytes(self.ap_info.my_pubkey)
|
||||
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(
|
||||
pubkey
|
||||
)
|
||||
private = self.wallet_state_manager.private_key.private_child(
|
||||
index
|
||||
).get_private_key()
|
||||
pk = BLSPrivateKey(private)
|
||||
|
||||
for puzzle, solution in spends:
|
||||
# sign for AGG_SIG_ME
|
||||
message = std_hash(
|
||||
Program(solution.solution).get_tree_hash() + bytes(solution.coin.name())
|
||||
)
|
||||
signature = pk.sign(message)
|
||||
assert signature.validate([signature.PkMessagePair(pubkey, message)])
|
||||
sigs.append(signature)
|
||||
|
||||
aggsig = BLSSignature.aggregate(sigs)
|
||||
solution_list = [
|
||||
CoinSolution(
|
||||
coin_solution.coin, clvm.to_sexp_f([puzzle, coin_solution.solution])
|
||||
)
|
||||
for (puzzle, coin_solution) in spends
|
||||
]
|
||||
spend_bundle = SpendBundle(solution_list, aggsig)
|
||||
return spend_bundle
|
||||
|
||||
# this is for sending a recieved ap coin, not sending a new ap coin
|
||||
async def ap_generate_signed_transaction(self, amount: int, puzzlehash: Program):
|
||||
|
||||
# calculate amount of transaction and change
|
||||
coins = await self.select_coins(amount)
|
||||
if coins is None or coins == set():
|
||||
return None
|
||||
change = sum([coin.amount for coin in coins]) - amount
|
||||
|
||||
# We could take this out and just let the transaction fail, but its probably better to have the sanity check
|
||||
auth_sig = None
|
||||
for name_address in self.ap_info.contacts:
|
||||
if puzzlehash == name_address[1]:
|
||||
auth_sig = BLSSignature.from_bytes(name_address[2])
|
||||
assert auth_sig.validate(
|
||||
[
|
||||
auth_sig.PkMessagePair(
|
||||
PublicKey.from_bytes(self.ap_info.authoriser_pubkey),
|
||||
puzzlehash,
|
||||
)
|
||||
]
|
||||
)
|
||||
break
|
||||
if auth_sig is None:
|
||||
return None
|
||||
|
||||
ap_puzzle = ap_puzzles.ap_make_puzzle(
|
||||
self.ap_info.authoriser_pubkey, self.ap_info.my_pubkey
|
||||
)
|
||||
if change > 0:
|
||||
puzzlehash_amount_sig_list = [
|
||||
(puzzlehash, amount, auth_sig),
|
||||
(ap_puzzle.get_tree_hash(), change, BLSSignature.from_bytes(self.ap_info.change_signature)),
|
||||
]
|
||||
else:
|
||||
puzzlehash_amount_sig_list = [(puzzlehash, amount, auth_sig)]
|
||||
|
||||
transaction, sigs = await self.ap_generate_unsigned_transaction(
|
||||
coins, puzzlehash_amount_sig_list, ap_puzzle
|
||||
)
|
||||
signed_tx = await self.ap_sign_transaction(transaction, sigs)
|
||||
return signed_tx
|
||||
|
||||
async def save_info(self, ap_info: APInfo):
|
||||
self.ap_info = ap_info
|
||||
current_info = self.wallet_info
|
||||
data_str = bytes(ap_info).hex()
|
||||
wallet_info = WalletInfo(
|
||||
current_info.id, current_info.name, current_info.type, data_str
|
||||
)
|
||||
self.wallet_info = wallet_info
|
||||
await self.wallet_state_manager.user_store.update_wallet(wallet_info)
|
|
@ -406,6 +406,17 @@ class Wallet:
|
|||
""" Use this API to send transactions. """
|
||||
await self.wallet_state_manager.add_pending_transaction(tx)
|
||||
|
||||
async def sign(self, value, pubkey):
|
||||
index = await self.wallet_state_manager.puzzle_store.index_for_pubkey(pubkey)
|
||||
private = self.wallet_state_manager.private_key.private_child(
|
||||
index
|
||||
).get_private_key()
|
||||
pk = BLSPrivateKey(private)
|
||||
|
||||
sig = pk.sign(value)
|
||||
assert sig.validate([sig.PkMessagePair(pubkey, value)])
|
||||
return sig
|
||||
|
||||
# This is also defined in CCWallet as get_sigs()
|
||||
# I think this should be a the default way the wallet gets signatures in sign_transaction()
|
||||
async def get_sigs_for_innerpuz_with_innersol(
|
||||
|
|
|
@ -0,0 +1,404 @@
|
|||
import asyncio
|
||||
import time
|
||||
from pathlib import Path
|
||||
from secrets import token_bytes
|
||||
|
||||
import pytest
|
||||
|
||||
from src.protocols import full_node_protocol
|
||||
from src.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol
|
||||
from src.types.peer_info import PeerInfo
|
||||
from src.util.ints import uint16, uint32, uint64
|
||||
from src.wallet.trade_manager import TradeManager
|
||||
from tests.setup_nodes import setup_simulators_and_wallets
|
||||
from src.consensus.block_rewards import calculate_base_fee, calculate_block_reward
|
||||
from src.wallet.ap_wallet.ap_wallet import APWallet
|
||||
from src.wallet.ap_wallet import ap_puzzles
|
||||
from src.wallet.wallet_coin_record import WalletCoinRecord
|
||||
from src.wallet.transaction_record import TransactionRecord
|
||||
from typing import List
|
||||
from src.types.BLSSignature import BLSSignature
|
||||
from src.types.coin_solution import CoinSolution
|
||||
from blspy import PublicKey
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def event_loop():
|
||||
loop = asyncio.get_event_loop()
|
||||
yield loop
|
||||
|
||||
|
||||
class TestWalletSimulator:
|
||||
@pytest.fixture(scope="function")
|
||||
async def wallet_node(self):
|
||||
async for _ in setup_simulators_and_wallets(1, 1, {}):
|
||||
yield _
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def two_wallet_nodes(self):
|
||||
async for _ in setup_simulators_and_wallets(
|
||||
1, 2, {"COINBASE_FREEZE_PERIOD": 0}
|
||||
):
|
||||
yield _
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def two_wallet_nodes_five_freeze(self):
|
||||
async for _ in setup_simulators_and_wallets(
|
||||
1, 2, {"COINBASE_FREEZE_PERIOD": 5}
|
||||
):
|
||||
yield _
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
async def three_sim_two_wallets(self):
|
||||
async for _ in setup_simulators_and_wallets(
|
||||
3, 2, {"COINBASE_FREEZE_PERIOD": 0}
|
||||
):
|
||||
yield _
|
||||
|
||||
async def time_out_assert(self, timeout: int, function, value, arg=None):
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
if arg is None:
|
||||
function_result = await function()
|
||||
else:
|
||||
function_result = await function(arg)
|
||||
if value == function_result:
|
||||
return
|
||||
await asyncio.sleep(1)
|
||||
assert False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ap_spend(self, two_wallet_nodes):
|
||||
num_blocks = 10
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_1, server_1 = full_nodes[0]
|
||||
wallet_node, server_2 = wallets[0]
|
||||
wallet_node_2, server_3 = wallets[1]
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
wallet2 = wallet_node_2.wallet_state_manager.main_wallet
|
||||
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
funds = sum(
|
||||
[
|
||||
calculate_base_fee(uint32(i)) + calculate_block_reward(uint32(i))
|
||||
for i in range(1, num_blocks - 2)
|
||||
]
|
||||
)
|
||||
|
||||
await self.time_out_assert(15, wallet.get_confirmed_balance, funds)
|
||||
|
||||
# Get pubkeys for creating the puzzle
|
||||
devrec = await wallet.wallet_state_manager.get_unused_derivation_record(
|
||||
wallet.wallet_info.id
|
||||
)
|
||||
ap_pubkey_a = devrec.pubkey
|
||||
ap_wallet: APWallet = await APWallet.create_wallet_for_ap(
|
||||
wallet_node_2.wallet_state_manager, wallet2, ap_pubkey_a
|
||||
)
|
||||
ap_pubkey_b = ap_wallet.ap_info.my_pubkey
|
||||
|
||||
ap_puz = ap_puzzles.ap_make_puzzle(ap_pubkey_a, ap_pubkey_b)
|
||||
sig = await wallet.sign(ap_puz.get_tree_hash(), bytes(ap_pubkey_a))
|
||||
assert sig is not None
|
||||
await ap_wallet.set_sender_values(ap_pubkey_a, sig)
|
||||
assert ap_wallet.ap_info.change_signature is not None
|
||||
assert BLSSignature.from_bytes(ap_wallet.ap_info.change_signature).validate(
|
||||
[BLSSignature.PkMessagePair(ap_pubkey_a, ap_puz.get_tree_hash())]
|
||||
)
|
||||
assert BLSSignature.from_bytes(ap_wallet.ap_info.change_signature).validate(
|
||||
[
|
||||
BLSSignature.PkMessagePair(
|
||||
PublicKey.from_bytes(ap_wallet.ap_info.authoriser_pubkey),
|
||||
ap_puz.get_tree_hash(),
|
||||
)
|
||||
]
|
||||
)
|
||||
tx = await wallet.generate_signed_transaction(100, ap_puz.get_tree_hash())
|
||||
await wallet.push_transaction(tx)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 100)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 100)
|
||||
|
||||
# Generate contact for ap_wallet
|
||||
|
||||
ph2 = await wallet2.get_new_puzzlehash()
|
||||
sig = await wallet.sign(ph2, ap_pubkey_a)
|
||||
assert sig.validate([sig.PkMessagePair(ap_pubkey_a, ph2)])
|
||||
await ap_wallet.add_contact("wallet2", ph2, sig)
|
||||
|
||||
tx = await ap_wallet.ap_generate_signed_transaction(20, ph2)
|
||||
assert tx is not None
|
||||
|
||||
tx_record = TransactionRecord(
|
||||
confirmed_at_index=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=ph,
|
||||
amount=uint64(20),
|
||||
fee_amount=uint64(0),
|
||||
incoming=False,
|
||||
confirmed=False,
|
||||
sent=uint32(0),
|
||||
spend_bundle=tx,
|
||||
additions=tx.additions(),
|
||||
removals=tx.removals(),
|
||||
wallet_id=ap_wallet.wallet_info.id,
|
||||
sent_to=[],
|
||||
)
|
||||
await ap_wallet.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 80)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 80)
|
||||
await self.time_out_assert(15, wallet2.get_confirmed_balance, 20)
|
||||
await self.time_out_assert(15, wallet2.get_unconfirmed_balance, 20)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_siphon_value_from_spend(self, two_wallet_nodes):
|
||||
num_blocks = 10
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_1, server_1 = full_nodes[0]
|
||||
wallet_node, server_2 = wallets[0]
|
||||
wallet_node_2, server_3 = wallets[1]
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
wallet2 = wallet_node_2.wallet_state_manager.main_wallet
|
||||
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
funds = sum(
|
||||
[
|
||||
calculate_base_fee(uint32(i)) + calculate_block_reward(uint32(i))
|
||||
for i in range(1, num_blocks - 2)
|
||||
]
|
||||
)
|
||||
|
||||
await self.time_out_assert(15, wallet.get_confirmed_balance, funds)
|
||||
|
||||
# Get pubkeys for creating the puzzle
|
||||
devrec = await wallet.wallet_state_manager.get_unused_derivation_record(
|
||||
wallet.wallet_info.id
|
||||
)
|
||||
ap_pubkey_a = devrec.pubkey
|
||||
ap_wallet: APWallet = await APWallet.create_wallet_for_ap(
|
||||
wallet_node_2.wallet_state_manager, wallet2, ap_pubkey_a
|
||||
)
|
||||
ap_pubkey_b = ap_wallet.ap_info.my_pubkey
|
||||
|
||||
ap_puz = ap_puzzles.ap_make_puzzle(ap_pubkey_a, ap_pubkey_b)
|
||||
sig = await wallet.sign(ap_puz.get_tree_hash(), bytes(ap_pubkey_a))
|
||||
assert sig is not None
|
||||
await ap_wallet.set_sender_values(ap_pubkey_a, sig)
|
||||
assert ap_wallet.ap_info.change_signature is not None
|
||||
assert BLSSignature.from_bytes(ap_wallet.ap_info.change_signature).validate(
|
||||
[BLSSignature.PkMessagePair(ap_pubkey_a, ap_puz.get_tree_hash())]
|
||||
)
|
||||
assert BLSSignature.from_bytes(ap_wallet.ap_info.change_signature).validate(
|
||||
[
|
||||
BLSSignature.PkMessagePair(
|
||||
PublicKey.from_bytes(ap_wallet.ap_info.authoriser_pubkey),
|
||||
ap_puz.get_tree_hash(),
|
||||
)
|
||||
]
|
||||
)
|
||||
tx = await wallet.generate_signed_transaction(100, ap_puz.get_tree_hash())
|
||||
await wallet.push_transaction(tx)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 100)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 100)
|
||||
|
||||
# Try and spend some money with not all output accounted for
|
||||
|
||||
ph2 = await wallet2.get_new_puzzlehash()
|
||||
sig = await wallet.sign(ph2, ap_pubkey_a)
|
||||
assert sig.validate([sig.PkMessagePair(ap_pubkey_a, ph2)])
|
||||
await ap_wallet.add_contact("wallet2", ph2, sig)
|
||||
|
||||
amount = 20
|
||||
puzzlehash = ph2
|
||||
# calculate amount of transaction and change
|
||||
coins = await ap_wallet.select_coins(amount)
|
||||
assert coins is not None
|
||||
assert coins is not set()
|
||||
# We could take this out and just let the transaction fail, but its probably better to have the sanity check
|
||||
auth_sig = None
|
||||
for name_address in ap_wallet.ap_info.contacts:
|
||||
if puzzlehash == name_address[1]:
|
||||
auth_sig = BLSSignature.from_bytes(name_address[2])
|
||||
assert auth_sig.validate(
|
||||
[
|
||||
auth_sig.PkMessagePair(
|
||||
PublicKey.from_bytes(ap_wallet.ap_info.authoriser_pubkey),
|
||||
puzzlehash,
|
||||
)
|
||||
]
|
||||
)
|
||||
break
|
||||
if auth_sig is None:
|
||||
return None
|
||||
|
||||
sigs = [auth_sig]
|
||||
spends = []
|
||||
coin = coins.pop()
|
||||
solution = ap_puzzles.ap_make_solution(
|
||||
[(puzzlehash, amount)], coin.parent_coin_info, ap_puz.get_tree_hash()
|
||||
)
|
||||
spends.append((ap_puz, CoinSolution(coin, solution)))
|
||||
|
||||
tx = await ap_wallet.ap_sign_transaction(spends, sigs)
|
||||
|
||||
assert tx is not None
|
||||
|
||||
tx_record = TransactionRecord(
|
||||
confirmed_at_index=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=ph,
|
||||
amount=uint64(20),
|
||||
fee_amount=uint64(0),
|
||||
incoming=False,
|
||||
confirmed=False,
|
||||
sent=uint32(0),
|
||||
spend_bundle=tx,
|
||||
additions=tx.additions(),
|
||||
removals=tx.removals(),
|
||||
wallet_id=ap_wallet.wallet_info.id,
|
||||
sent_to=[],
|
||||
)
|
||||
await ap_wallet.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 100)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 80)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ap_spend_multiple(self, two_wallet_nodes):
|
||||
num_blocks = 10
|
||||
full_nodes, wallets = two_wallet_nodes
|
||||
full_node_1, server_1 = full_nodes[0]
|
||||
wallet_node, server_2 = wallets[0]
|
||||
wallet_node_2, server_3 = wallets[1]
|
||||
wallet = wallet_node.wallet_state_manager.main_wallet
|
||||
wallet2 = wallet_node_2.wallet_state_manager.main_wallet
|
||||
|
||||
ph = await wallet.get_new_puzzlehash()
|
||||
|
||||
await server_2.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
await server_3.start_client(PeerInfo("localhost", uint16(server_1._port)), None)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
funds = sum(
|
||||
[
|
||||
calculate_base_fee(uint32(i)) + calculate_block_reward(uint32(i))
|
||||
for i in range(1, num_blocks - 2)
|
||||
]
|
||||
)
|
||||
|
||||
await self.time_out_assert(15, wallet.get_confirmed_balance, funds)
|
||||
|
||||
# Get pubkeys for creating the puzzle
|
||||
devrec = await wallet.wallet_state_manager.get_unused_derivation_record(
|
||||
wallet.wallet_info.id
|
||||
)
|
||||
ap_pubkey_a = devrec.pubkey
|
||||
ap_wallet: APWallet = await APWallet.create_wallet_for_ap(
|
||||
wallet_node_2.wallet_state_manager, wallet2, ap_pubkey_a
|
||||
)
|
||||
ap_pubkey_b = ap_wallet.ap_info.my_pubkey
|
||||
|
||||
ap_puz = ap_puzzles.ap_make_puzzle(ap_pubkey_a, ap_pubkey_b)
|
||||
sig = await wallet.sign(ap_puz.get_tree_hash(), bytes(ap_pubkey_a))
|
||||
assert sig is not None
|
||||
await ap_wallet.set_sender_values(ap_pubkey_a, sig)
|
||||
assert ap_wallet.ap_info.change_signature is not None
|
||||
assert BLSSignature.from_bytes(ap_wallet.ap_info.change_signature).validate(
|
||||
[BLSSignature.PkMessagePair(ap_pubkey_a, ap_puz.get_tree_hash())]
|
||||
)
|
||||
assert BLSSignature.from_bytes(ap_wallet.ap_info.change_signature).validate(
|
||||
[
|
||||
BLSSignature.PkMessagePair(
|
||||
PublicKey.from_bytes(ap_wallet.ap_info.authoriser_pubkey),
|
||||
ap_puz.get_tree_hash(),
|
||||
)
|
||||
]
|
||||
)
|
||||
tx = await wallet.generate_signed_transaction(100, ap_puz.get_tree_hash())
|
||||
await wallet.push_transaction(tx)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 100)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 100)
|
||||
tx = await wallet.generate_signed_transaction(50, ap_puz.get_tree_hash())
|
||||
await wallet.push_transaction(tx)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 150)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 150)
|
||||
|
||||
tx = await wallet.generate_signed_transaction(25, ap_puz.get_tree_hash())
|
||||
await wallet.push_transaction(tx)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 175)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 175)
|
||||
|
||||
# Generate contact for ap_wallet
|
||||
|
||||
ph2 = await wallet2.get_new_puzzlehash()
|
||||
sig = await wallet.sign(ph2, ap_pubkey_a)
|
||||
assert sig.validate([sig.PkMessagePair(ap_pubkey_a, ph2)])
|
||||
await ap_wallet.add_contact("wallet2", ph2, sig)
|
||||
|
||||
tx = await ap_wallet.ap_generate_signed_transaction(170, ph2)
|
||||
assert tx is not None
|
||||
|
||||
tx_record = TransactionRecord(
|
||||
confirmed_at_index=uint32(0),
|
||||
created_at_time=uint64(int(time.time())),
|
||||
to_puzzle_hash=ph,
|
||||
amount=uint64(20),
|
||||
fee_amount=uint64(0),
|
||||
incoming=False,
|
||||
confirmed=False,
|
||||
sent=uint32(0),
|
||||
spend_bundle=tx,
|
||||
additions=tx.additions(),
|
||||
removals=tx.removals(),
|
||||
wallet_id=ap_wallet.wallet_info.id,
|
||||
sent_to=[],
|
||||
)
|
||||
await ap_wallet.wallet_state_manager.add_pending_transaction(tx_record)
|
||||
|
||||
for i in range(1, num_blocks):
|
||||
await full_node_1.farm_new_block(FarmNewBlockProtocol(ph))
|
||||
|
||||
await self.time_out_assert(15, ap_wallet.get_confirmed_balance, 5)
|
||||
await self.time_out_assert(15, ap_wallet.get_unconfirmed_balance, 5)
|
||||
await self.time_out_assert(15, wallet2.get_confirmed_balance, 170)
|
||||
await self.time_out_assert(15, wallet2.get_unconfirmed_balance, 170)
|
Loading…
Reference in New Issue