Skip to content

Commit

Permalink
Add announcements for standard tx (Chia-Network#3023)
Browse files Browse the repository at this point in the history
* changed bahviour of wallet and wallet tools for standard solutions

* black formatting wallet_tools

* added a test for hypothetical stolen zero_output coin

* swap to sha256 from sha256tree for message

* fix wallet_tools version, address lint issues

* correctly int_from_bytes

* fix uninstantiated key in dict

* Comment out broken test

* Fix types (used SerializedProgram)

Co-authored-by: Mariano <[email protected]>
  • Loading branch information
matt-o-how and mariano54 authored Apr 29, 2021
1 parent e611bef commit 6371775
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 59 deletions.
23 changes: 19 additions & 4 deletions chia/util/wallet_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from blspy import AugSchemeMPL, G2Element, PrivateKey

from chia.consensus.constants import ConsensusConstants
from chia.util.hash import std_hash
from chia.types.announcement import Announcement
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
Expand Down Expand Up @@ -148,6 +150,8 @@ def generate_unsigned_transaction(

if ConditionOpcode.CREATE_COIN not in condition_dic:
condition_dic[ConditionOpcode.CREATE_COIN] = []
if ConditionOpcode.CREATE_COIN_ANNOUNCEMENT not in condition_dic:
condition_dic[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT] = []

output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [new_puzzle_hash, int_to_bytes(amount)])
condition_dic[output.opcode].append(output)
Expand All @@ -157,20 +161,31 @@ def generate_unsigned_transaction(
change_puzzle_hash = self.get_new_puzzlehash()
change_output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [change_puzzle_hash, int_to_bytes(change)])
condition_dic[output.opcode].append(change_output)
main_solution: Program = self.make_solution(condition_dic)
else:
main_solution = self.make_solution(condition_dic)

secondary_coins_cond_dic: Dict[ConditionOpcode, List[ConditionWithArgs]] = dict()
secondary_coins_cond_dic[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT] = []
for n, coin in enumerate(coins):
puzzle_hash = coin.puzzle_hash
if secret_key is None:
secret_key = self.get_private_key_for_puzzle_hash(puzzle_hash)
pubkey = secret_key.get_g1()
puzzle = puzzle_for_pk(bytes(pubkey))
if n == 0:
message_list = [c.name() for c in coins]
for outputs in condition_dic[ConditionOpcode.CREATE_COIN]:
message_list.append(Coin(coin.name(), outputs.vars[0], int_from_bytes(outputs.vars[1])).name())
message = std_hash(b"".join(message_list))
condition_dic[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT].append(
ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [message])
)
primary_announcement_hash = Announcement(coin.name(), message).name()
secondary_coins_cond_dic[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT].append(
ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [primary_announcement_hash])
)
main_solution = self.make_solution(condition_dic)
spends.append(CoinSolution(coin, puzzle, main_solution))
else:
spends.append(CoinSolution(coin, puzzle, self.make_solution({})))
spends.append(CoinSolution(coin, puzzle, self.make_solution(secondary_coins_cond_dic)))
return spends

def sign_transaction(self, coin_solutions: List[CoinSolution]) -> SpendBundle:
Expand Down
36 changes: 23 additions & 13 deletions chia/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
from chia.full_node.bundle_tools import simple_solution_generator
from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions
from chia.types.blockchain_format.coin import Coin
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.program import Program, SerializedProgram
from chia.types.announcement import Announcement
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_solution import CoinSolution
from chia.types.generator_types import BlockGenerator
from chia.types.spend_bundle import SpendBundle
from chia.util.ints import uint8, uint32, uint64, uint128
from chia.util.hash import std_hash
from chia.wallet.derivation_record import DerivationRecord
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import (
DEFAULT_HIDDEN_PUZZLE_HASH,
Expand Down Expand Up @@ -227,7 +229,7 @@ def make_solution(
return solution_for_conditions(condition_list)

async def select_coins(self, amount, exclude: List[Coin] = None) -> Set[Coin]:
""" Returns a set of coins that can be used for generating a new transaction. """
"""Returns a set of coins that can be used for generating a new transaction."""
async with self.wallet_state_manager.lock:
if exclude is None:
exclude = []
Expand Down Expand Up @@ -294,7 +296,7 @@ async def generate_unsigned_transaction(
Generates a unsigned transaction in form of List(Puzzle, Solutions)
"""
if primaries_input is None:
primaries = None
primaries: Optional[List[Dict]] = None
total_amount = amount + fee
else:
primaries = primaries_input.copy()
Expand All @@ -318,7 +320,7 @@ async def generate_unsigned_transaction(
assert change >= 0

spends: List[CoinSolution] = []
output_created = False
primary_announcement_hash: Optional[bytes32] = None

# Check for duplicates
if primaries is not None:
Expand All @@ -331,20 +333,28 @@ async def generate_unsigned_transaction(
puzzle: Program = await self.puzzle_for_puzzle_hash(coin.puzzle_hash)

# Only one coin creates outputs
if not output_created and origin_id in (None, coin.name()):
if primary_announcement_hash is None and origin_id in (None, coin.name()):
if primaries is None:
primaries = [{"puzzlehash": newpuzzlehash, "amount": amount}]
else:
primaries.append({"puzzlehash": newpuzzlehash, "amount": amount})
if change > 0:
changepuzzlehash = await self.get_new_puzzlehash()
primaries.append({"puzzlehash": changepuzzlehash, "amount": change})
solution = self.make_solution(primaries=primaries, fee=fee)
output_created = True
change_puzzle_hash: bytes32 = await self.get_new_puzzlehash()
primaries.append({"puzzlehash": change_puzzle_hash, "amount": change})
message_list: List[bytes32] = [c.name() for c in coins]
for primary in primaries:
message_list.append(Coin(coin.name(), primary["puzzlehash"], primary["amount"]).name())
message: bytes32 = std_hash(b"".join(message_list))
solution: Program = self.make_solution(primaries=primaries, fee=fee, coin_announcements=[message])
primary_announcement_hash = Announcement(coin.name(), message).name()
else:
solution = self.make_solution()
solution = self.make_solution(coin_announcements_to_assert=[primary_announcement_hash])

spends.append(CoinSolution(coin, puzzle, solution))
spends.append(
CoinSolution(
coin, SerializedProgram.from_bytes(bytes(puzzle)), SerializedProgram.from_bytes(bytes(solution))
)
)

self.log.info(f"Spends is {spends}")
return spends
Expand All @@ -367,7 +377,7 @@ async def generate_signed_transaction(
primaries: Optional[List[Dict[str, bytes32]]] = None,
ignore_max_send_amount: bool = False,
) -> TransactionRecord:
""" Use this to generate transaction. """
"""Use this to generate transaction."""
if primaries is None:
non_change_amount = amount
else:
Expand Down Expand Up @@ -411,7 +421,7 @@ async def generate_signed_transaction(
)

async def push_transaction(self, tx: TransactionRecord) -> None:
""" Use this API to send transactions. """
"""Use this API to send transactions."""
await self.wallet_state_manager.add_pending_transaction(tx)

# This is to be aggregated together with a coloured coin offer to ensure that the trade happens
Expand Down
83 changes: 42 additions & 41 deletions tests/blockchain/test_blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -2031,47 +2031,48 @@ async def test_max_coin_amount(self):
# 10
# TODO: fix, this is not reaching validation. Because we can't create a block with such amounts due to uint64
# limit in Coin

new_test_constants = test_constants.replace(
**{"GENESIS_PRE_FARM_POOL_PUZZLE_HASH": bt.pool_ph, "GENESIS_PRE_FARM_FARMER_PUZZLE_HASH": bt.pool_ph}
)
b, connection, db_path = await create_blockchain(new_test_constants)
bt_2 = BlockTools(new_test_constants)
bt_2.constants = bt_2.constants.replace(
**{"GENESIS_PRE_FARM_POOL_PUZZLE_HASH": bt.pool_ph, "GENESIS_PRE_FARM_FARMER_PUZZLE_HASH": bt.pool_ph}
)
blocks = bt_2.get_consecutive_blocks(
3,
guarantee_transaction_block=True,
farmer_reward_puzzle_hash=bt.pool_ph,
pool_reward_puzzle_hash=bt.pool_ph,
)
assert (await b.receive_block(blocks[0]))[0] == ReceiveBlockResult.NEW_PEAK
assert (await b.receive_block(blocks[1]))[0] == ReceiveBlockResult.NEW_PEAK
assert (await b.receive_block(blocks[2]))[0] == ReceiveBlockResult.NEW_PEAK

wt: WalletTool = bt_2.get_pool_wallet_tool()

condition_dict = {ConditionOpcode.CREATE_COIN: []}
output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [bt_2.pool_ph, int_to_bytes(2 ** 64)])
condition_dict[ConditionOpcode.CREATE_COIN].append(output)

tx: SpendBundle = wt.generate_signed_transaction_multiple_coins(
10,
wt.get_new_puzzlehash(),
list(blocks[1].get_included_reward_coins()),
condition_dic=condition_dict,
)
try:
blocks = bt_2.get_consecutive_blocks(
1, block_list_input=blocks, guarantee_transaction_block=True, transaction_data=tx
)
assert False
except Exception as e:
pass
await connection.close()
b.shut_down()
db_path.unlink()
pass
#
# new_test_constants = test_constants.replace(
# **{"GENESIS_PRE_FARM_POOL_PUZZLE_HASH": bt.pool_ph, "GENESIS_PRE_FARM_FARMER_PUZZLE_HASH": bt.pool_ph}
# )
# b, connection, db_path = await create_blockchain(new_test_constants)
# bt_2 = BlockTools(new_test_constants)
# bt_2.constants = bt_2.constants.replace(
# **{"GENESIS_PRE_FARM_POOL_PUZZLE_HASH": bt.pool_ph, "GENESIS_PRE_FARM_FARMER_PUZZLE_HASH": bt.pool_ph}
# )
# blocks = bt_2.get_consecutive_blocks(
# 3,
# guarantee_transaction_block=True,
# farmer_reward_puzzle_hash=bt.pool_ph,
# pool_reward_puzzle_hash=bt.pool_ph,
# )
# assert (await b.receive_block(blocks[0]))[0] == ReceiveBlockResult.NEW_PEAK
# assert (await b.receive_block(blocks[1]))[0] == ReceiveBlockResult.NEW_PEAK
# assert (await b.receive_block(blocks[2]))[0] == ReceiveBlockResult.NEW_PEAK
#
# wt: WalletTool = bt_2.get_pool_wallet_tool()
#
# condition_dict = {ConditionOpcode.CREATE_COIN: []}
# output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [bt_2.pool_ph, int_to_bytes(2 ** 64)])
# condition_dict[ConditionOpcode.CREATE_COIN].append(output)
#
# tx: SpendBundle = wt.generate_signed_transaction_multiple_coins(
# 10,
# wt.get_new_puzzlehash(),
# list(blocks[1].get_included_reward_coins()),
# condition_dic=condition_dict,
# )
# try:
# blocks = bt_2.get_consecutive_blocks(
# 1, block_list_input=blocks, guarantee_transaction_block=True, transaction_data=tx
# )
# assert False
# except Exception as e:
# pass
# await connection.close()
# b.shut_down()
# db_path.unlink()

@pytest.mark.asyncio
async def test_invalid_merkle_roots(self, empty_blockchain):
Expand Down
76 changes: 75 additions & 1 deletion tests/wallet/test_wallet.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import asyncio

import pytest
import time

from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.protocols.full_node_protocol import RespondBlock
from chia.server.server import ChiaServer
from chia.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol
from chia.types.peer_info import PeerInfo
from chia.util.ints import uint16, uint32
from chia.util.ints import uint16, uint32, uint64
from chia.wallet.util.transaction_type import TransactionType
from chia.wallet.transaction_record import TransactionRecord
from chia.wallet.wallet_state_manager import WalletStateManager
from tests.setup_nodes import self_hostname, setup_simulators_and_wallets
from tests.time_out_assert import time_out_assert, time_out_assert_not_none
Expand Down Expand Up @@ -465,3 +467,75 @@ async def test_wallet_create_hit_max_send_amount(self, two_wallet_nodes):
pass

assert above_limit_tx is None

@pytest.mark.asyncio
async def test_wallet_prevent_fee_theft(self, two_wallet_nodes):
num_blocks = 5
full_nodes, wallets = two_wallet_nodes
full_node_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
ph = await wallet.get_new_puzzlehash()

await server_2.start_client(PeerInfo(self_hostname, uint16(full_node_1.full_node.server._port)), None)

for i in range(0, num_blocks):
await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(ph))

funds = sum(
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, num_blocks)]
)

await time_out_assert(5, wallet.get_confirmed_balance, funds)
await time_out_assert(5, wallet.get_unconfirmed_balance, funds)

assert await wallet.get_confirmed_balance() == funds
assert await wallet.get_unconfirmed_balance() == funds
tx_amount = 3200000000000
tx_fee = 300000000000
tx = await wallet.generate_signed_transaction(
tx_amount,
await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(),
tx_fee,
)

# extract coin_solution from generated spend_bundle
for cs in tx.spend_bundle.coin_solutions:
if cs.additions() == []:
stolen_cs = cs
# get a legit signature
stolen_sb = await wallet.sign_transaction([stolen_cs])
now = uint64(int(time.time()))
add_list = list(stolen_sb.additions())
rem_list = list(stolen_sb.removals())
name = stolen_sb.name()
stolen_tx = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=now,
to_puzzle_hash=32 * b"0",
amount=0,
fee_amount=stolen_cs.coin.amount,
confirmed=False,
sent=uint32(0),
spend_bundle=stolen_sb,
additions=add_list,
removals=rem_list,
wallet_id=wallet.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.OUTGOING_TX.value),
name=name,
)
await wallet.push_transaction(stolen_tx)

await time_out_assert(5, wallet.get_confirmed_balance, funds)
await time_out_assert(5, wallet.get_unconfirmed_balance, funds - stolen_cs.coin.amount)

for i in range(0, num_blocks):
await full_node_1.farm_new_transaction_block(FarmNewBlockProtocol(32 * b"0"))

# Funds have not decreased because stolen_tx was rejected
outstanding_coinbase_rewards = 2000000000000
await time_out_assert(5, wallet.get_confirmed_balance, funds + outstanding_coinbase_rewards)
await time_out_assert(5, wallet.get_confirmed_balance, funds + outstanding_coinbase_rewards)

0 comments on commit 6371775

Please sign in to comment.