added duplicate sigs where needed

This commit is contained in:
Matthew Howard 2020-05-27 17:07:33 +01:00 committed by wjblanke
parent 9365cb3bee
commit c9f3a8cdeb
6 changed files with 859 additions and 1 deletions

View File

@ -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

View File

@ -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]

View File

@ -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.
"""

View File

@ -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)

View File

@ -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(

View File

@ -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)