Skip to content

Commit

Permalink
Ms.offline signing2 (Chia-Network#1530)
Browse files Browse the repository at this point in the history
* Offline transaction signing

* Create signed transaction from python

* More work on offline

* Get transaction signing working for many outputs.
  • Loading branch information
mariano54 authored Mar 31, 2021
1 parent 474c99d commit 05b4659
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 17 deletions.
36 changes: 36 additions & 0 deletions src/rpc/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from src.protocols.protocol_message_types import ProtocolMessageTypes
from src.server.outbound_message import NodeType, make_msg
from src.simulator.simulator_protocol import FarmNewBlockProtocol
from src.types.blockchain_format.coin import Coin
from src.types.blockchain_format.sized_bytes import bytes32
from src.util.bech32m import decode_puzzle_hash, encode_puzzle_hash
from src.util.byte_types import hexstr_to_bytes
Expand Down Expand Up @@ -70,6 +71,7 @@ def get_routes(self) -> Dict[str, Callable]:
"/create_backup": self.create_backup,
"/get_transaction_count": self.get_transaction_count,
"/get_farmed_amount": self.get_farmed_amount,
"/create_signed_transaction": self.create_signed_transaction,
# Coloured coins and trading
"/cc_set_name": self.cc_set_name,
"/cc_get_name": self.cc_get_name,
Expand Down Expand Up @@ -728,3 +730,37 @@ async def get_farmed_amount(self, request):
"fee_amount": fee_amount,
"last_height_farmed": last_height_farmed,
}

async def create_signed_transaction(self, request):
if "additions" not in request or len(request["additions"]) < 1:
raise ValueError("Specify additions list")

additions: List[Dict] = request["additions"]
amount_0: uint64 = uint64(additions[0]["amount"])
assert amount_0 <= self.service.constants.MAX_COIN_AMOUNT
puzzle_hash_0 = hexstr_to_bytes(additions[0]["puzzle_hash"])
if len(puzzle_hash_0) != 32:
raise ValueError(f"Address must be 32 bytes. {puzzle_hash_0}")

additional_outputs = []
for addition in additions[1:]:
receiver_ph = hexstr_to_bytes(addition["puzzle_hash"])
if len(receiver_ph) != 32:
raise ValueError(f"Address must be 32 bytes. {receiver_ph}")
amount = uint64(addition["amount"])
if amount > self.service.constants.MAX_COIN_AMOUNT:
raise ValueError(f"Coin amount cannot exceed {self.service.constants.MAX_COIN_AMOUNT}")
additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount})

fee = uint64(0)
if "fee" in request:
fee = uint64(request["fee"])

coins = None
if "coins" in request and len(request["coins"]) > 0:
coins = set([Coin.from_json_dict(coin_json) for coin_json in request["coins"]])

signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction(
amount_0, puzzle_hash_0, fee, coins=coins, ignore_max_send_amount=True, primaries=additional_outputs
)
return {"signed_tx": signed_tx}
14 changes: 14 additions & 0 deletions src/rpc/wallet_rpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Dict, List

from src.rpc.rpc_client import RpcClient
from src.types.blockchain_format.coin import Coin
from src.types.blockchain_format.sized_bytes import bytes32
from src.util.bech32m import decode_puzzle_hash
from src.util.ints import uint32, uint64
Expand Down Expand Up @@ -129,5 +130,18 @@ async def create_backup(self, file_path: Path) -> None:
async def get_farmed_amount(self) -> Dict:
return await self.fetch("get_farmed_amount", {})

async def create_signed_transaction(
self, additions: List[Dict], coins: List[Coin] = None, fee: uint64 = uint64(0)
) -> Dict:
# Converts bytes to hex for puzzle hashes
additions_hex = [{"amount": ad["amount"], "puzzle_hash": ad["puzzle_hash"].hex()} for ad in additions]
if coins is not None and len(coins) > 0:
coins_json = [c.to_json_dict() for c in coins]
return await self.fetch(
"create_signed_transaction", {"additions": additions_hex, "coins": coins_json, "fee": fee}
)
else:
return await self.fetch("create_signed_transaction", {"additions": additions_hex, "fee": fee})


# TODO: add APIs for coloured coins and RL wallet
4 changes: 2 additions & 2 deletions src/util/streamable.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def recurse_jsonify(d):
if isinstance(item, Enum):
item = item.name
if isinstance(item, int) and type(item) in big_ints:
item = str(item)
item = int(item)
new_list.append(item)
d = new_list

Expand All @@ -110,7 +110,7 @@ def recurse_jsonify(d):
if isinstance(value, Enum):
d[key] = value.name
if isinstance(value, int) and type(value) in big_ints:
d[key] = str(value)
d[key] = int(value)
return d


Expand Down
11 changes: 4 additions & 7 deletions src/wallet/puzzles/prefarm/spend_prefarm.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,8 @@ async def main():
print(farmer_prefarm.amount, farmer_amounts)
assert farmer_amounts == farmer_prefarm.amount // 2
assert pool_amounts == pool_prefarm.amount // 2

address1 = "txch15gx26ndmacfaqlq8m0yajeggzceu7cvmaz4df0hahkukes695rss6lej7h" # Gene wallet (m/12381/8444/2/42):
address2 = (
"txch1c2cguswhvmdyz9hr3q6hak2h6p9dw4rz82g4707k2xy2sarv705qcce4pn" # Mariano address (m/12381/8444/2/0)
)
address1 = "xch1rdatypul5c642jkeh4yp933zu3hw8vv8tfup8ta6zfampnyhjnusxdgns6" # Key 1
address2 = "xch1duvy5ur5eyj7lp5geetfg84cj2d7xgpxt7pya3lr2y6ke3696w9qvda66e" # Key 2

ph1 = decode_puzzle_hash(address1)
ph2 = decode_puzzle_hash(address2)
Expand All @@ -51,8 +48,8 @@ async def main():
sb_pool = SpendBundle([CoinSolution(pool_prefarm, p_pool_2, p_solution)], G2Element())

print(sb_pool, sb_farmer)
# res = await client.push_tx(sb_farmer)
res = await client.push_tx(sb_pool)
res = await client.push_tx(sb_farmer)
# res = await client.push_tx(sb_pool)

print(res)
up = await client.get_coin_records_by_puzzle_hash(farmer_prefarm.puzzle_hash, True)
Expand Down
15 changes: 11 additions & 4 deletions src/wallet/wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ async def get_new_puzzlehash(self) -> bytes32:

def make_solution(
self,
primaries: Optional[List[Dict[str, bytes32]]] = None,
primaries: Optional[List[Dict[str, Any]]] = None,
min_time=0,
me=None,
announcements=None,
Expand Down Expand Up @@ -272,15 +272,17 @@ async def generate_unsigned_transaction(
fee: uint64 = uint64(0),
origin_id: bytes32 = None,
coins: Set[Coin] = None,
primaries: Optional[List[Dict[str, bytes32]]] = None,
primaries_input: Optional[List[Dict[str, Any]]] = None,
ignore_max_send_amount: bool = False,
) -> List[CoinSolution]:
"""
Generates a unsigned transaction in form of List(Puzzle, Solutions)
"""
if primaries is None:
if primaries_input is None:
primaries = None
total_amount = amount + fee
else:
primaries = primaries_input.copy()
primaries_amount = 0
for prim in primaries:
primaries_amount += prim["amount"]
Expand Down Expand Up @@ -339,6 +341,10 @@ async def generate_signed_transaction(
ignore_max_send_amount: bool = False,
) -> TransactionRecord:
""" Use this to generate transaction. """
if primaries is None:
non_change_amount = amount
else:
non_change_amount = uint64(amount + sum(p["amount"] for p in primaries))

transaction = await self.generate_unsigned_transaction(
amount, puzzle_hash, fee, origin_id, coins, primaries, ignore_max_send_amount
Expand All @@ -354,12 +360,13 @@ async def generate_signed_transaction(
now = uint64(int(time.time()))
add_list: List[Coin] = list(spend_bundle.additions())
rem_list: List[Coin] = list(spend_bundle.removals())
assert sum(a.amount for a in add_list) + fee == sum(r.amount for r in rem_list)

return TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=now,
to_puzzle_hash=puzzle_hash,
amount=uint64(amount),
amount=uint64(non_change_amount),
fee_amount=uint64(fee),
confirmed=False,
sent=uint32(0),
Expand Down
1 change: 0 additions & 1 deletion src/wallet/wallet_state_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ async def create(
assert main_wallet_info is not None

self.private_key = private_key

self.main_wallet = await Wallet.create(self, main_wallet_info)

self.wallets = {main_wallet_info.id: self.main_wallet}
Expand Down
89 changes: 86 additions & 3 deletions tests/wallet/rpc/test_wallet_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
import pytest

from src.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from src.rpc.full_node_rpc_api import FullNodeRpcApi
from src.rpc.full_node_rpc_client import FullNodeRpcClient
from src.rpc.rpc_server import start_rpc_server
from src.rpc.wallet_rpc_api import WalletRpcApi
from src.rpc.wallet_rpc_client import WalletRpcClient
from src.simulator.simulator_protocol import FarmNewBlockProtocol
from src.types.blockchain_format.coin import Coin
from src.types.peer_info import PeerInfo
from src.types.spend_bundle import SpendBundle
from src.util.bech32m import encode_puzzle_hash
from src.util.ints import uint16, uint32
from tests.setup_nodes import bt, setup_simulators_and_wallets
from tests.setup_nodes import bt, setup_simulators_and_wallets, self_hostname
from tests.time_out_assert import time_out_assert

log = logging.getLogger(__name__)
Expand All @@ -27,6 +31,7 @@ async def two_wallet_nodes(self):
@pytest.mark.asyncio
async def test_wallet_make_transaction(self, two_wallet_nodes):
test_rpc_port = uint16(21529)
test_rpc_port_node = uint16(21530)
num_blocks = 5
full_nodes, wallets = two_wallet_nodes
full_node_api = full_nodes[0]
Expand Down Expand Up @@ -62,6 +67,18 @@ async def test_wallet_make_transaction(self, two_wallet_nodes):
def stop_node_cb():
pass

full_node_rpc_api = FullNodeRpcApi(full_node_api.full_node)

rpc_cleanup_node = await start_rpc_server(
full_node_rpc_api,
hostname,
daemon_port,
test_rpc_port_node,
stop_node_cb,
bt.root_path,
config,
connect_to_daemon=False,
)
rpc_cleanup = await start_rpc_server(
wallet_rpc_api,
hostname,
Expand All @@ -76,7 +93,8 @@ def stop_node_cb():
await time_out_assert(5, wallet.get_confirmed_balance, initial_funds)
await time_out_assert(5, wallet.get_unconfirmed_balance, initial_funds)

client = await WalletRpcClient.create("localhost", test_rpc_port, bt.root_path, config)
client = await WalletRpcClient.create(self_hostname, test_rpc_port, bt.root_path, config)
client_node = await FullNodeRpcClient.create(self_hostname, test_rpc_port_node, bt.root_path, config)
try:
addr = encode_puzzle_hash(await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash(), "xch")
tx_amount = 15600000
Expand All @@ -86,6 +104,7 @@ def stop_node_cb():
except ValueError:
pass

# Tests sending a basic transaction
tx = await client.send_transaction("1", tx_amount, addr)
transaction_id = tx.name

Expand All @@ -100,13 +119,74 @@ async def tx_in_mempool():

for i in range(0, 5):
await client.farm_block(encode_puzzle_hash(ph_2, "xch"))
await asyncio.sleep(1)
await asyncio.sleep(0.5)

async def eventual_balance():
return (await client.get_wallet_balance("1"))["confirmed_wallet_balance"]

await time_out_assert(5, eventual_balance, initial_funds_eventually - tx_amount)

# Tests offline signing
ph_3 = await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash()
ph_4 = await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash()
ph_5 = await wallet_node_2.wallet_state_manager.main_wallet.get_new_puzzlehash()

# Test basic transaction to one output
signed_tx_amount = 888000
tx_res = await client.create_signed_transaction([{"amount": signed_tx_amount, "puzzle_hash": ph_3}])

assert tx_res["success"]
assert tx_res["signed_tx"]["fee_amount"] == 0
assert tx_res["signed_tx"]["amount"] == signed_tx_amount
assert len(tx_res["signed_tx"]["additions"]) == 2 # The output and the change
assert any([addition["amount"] == signed_tx_amount for addition in tx_res["signed_tx"]["additions"]])

push_res = await client_node.push_tx(SpendBundle.from_json_dict(tx_res["signed_tx"]["spend_bundle"]))
assert push_res["success"]
assert (await client.get_wallet_balance("1"))[
"confirmed_wallet_balance"
] == initial_funds_eventually - tx_amount

for i in range(0, 5):
await client.farm_block(encode_puzzle_hash(ph_2, "xch"))
await asyncio.sleep(0.5)

await time_out_assert(5, eventual_balance, initial_funds_eventually - tx_amount - signed_tx_amount)

# Test transaction to two outputs, from a specified coin, with a fee
coin_to_spend = None
for addition in tx_res["signed_tx"]["additions"]:
if addition["amount"] != signed_tx_amount:
coin_to_spend = Coin.from_json_dict(addition)
assert coin_to_spend is not None

tx_res = await client.create_signed_transaction(
[{"amount": 444, "puzzle_hash": ph_4}, {"amount": 999, "puzzle_hash": ph_5}],
coins=[coin_to_spend],
fee=100,
)
assert tx_res["success"]
assert tx_res["signed_tx"]["fee_amount"] == 100
assert tx_res["signed_tx"]["amount"] == 444 + 999
assert len(tx_res["signed_tx"]["additions"]) == 3 # The outputs and the change
assert any([addition["amount"] == 444 for addition in tx_res["signed_tx"]["additions"]])
assert any([addition["amount"] == 999 for addition in tx_res["signed_tx"]["additions"]])
assert (
sum([rem["amount"] for rem in tx_res["signed_tx"]["removals"]])
- sum([ad["amount"] for ad in tx_res["signed_tx"]["additions"]])
== 100
)

push_res = await client_node.push_tx(SpendBundle.from_json_dict(tx_res["signed_tx"]["spend_bundle"]))
assert push_res["success"]
for i in range(0, 5):
await client.farm_block(encode_puzzle_hash(ph_2, "xch"))
await asyncio.sleep(0.5)

await time_out_assert(
5, eventual_balance, initial_funds_eventually - tx_amount - signed_tx_amount - 444 - 999 - 100
)

address = await client.get_next_address("1", True)
assert len(address) > 10

Expand Down Expand Up @@ -164,5 +244,8 @@ async def eventual_balance():
finally:
# Checks that the RPC manages to stop the node
client.close()
client_node.close()
await client.await_closed()
await client_node.await_closed()
await rpc_cleanup()
await rpc_cleanup_node()

0 comments on commit 05b4659

Please sign in to comment.