Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Endpoint for vault singleton creation #17093

Merged
merged 10 commits into from
Jan 8, 2024
34 changes: 34 additions & 0 deletions chia/rpc/wallet_rpc_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from clvm_tools.binutils import assemble

from chia.consensus.block_rewards import calculate_base_farmer_reward
from chia.consensus.default_constants import DEFAULT_CONSTANTS
from chia.data_layer.data_layer_errors import LauncherCoinNotFoundError
from chia.data_layer.data_layer_wallet import DataLayerWallet
from chia.pools.pool_wallet import PoolWallet
Expand Down Expand Up @@ -105,6 +106,7 @@
from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG, CoinSelectionConfig, CoinSelectionConfigLoader, TXConfig
from chia.wallet.util.wallet_sync_utils import fetch_coin_spend_for_coin_state
from chia.wallet.util.wallet_types import CoinType, WalletType
from chia.wallet.vault.vault_drivers import get_vault_hidden_puzzle_with_index
from chia.wallet.vc_wallet.cr_cat_drivers import ProofsChecker
from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet
from chia.wallet.vc_wallet.vc_store import VCProofs
Expand Down Expand Up @@ -282,6 +284,8 @@ def get_routes(self) -> Dict[str, Endpoint]:
"/vc_revoke": self.vc_revoke,
# CR-CATs
"/crcat_approve_pending": self.crcat_approve_pending,
# VAULT
"/vault_create": self.vault_create,
}

def get_connections(self, request_node_type: Optional[NodeType]) -> List[Dict[str, Any]]:
Expand Down Expand Up @@ -4505,3 +4509,33 @@ class CRCATApprovePending(Streamable):
return {
"transactions": [tx.to_json_dict_convenience(self.service.config) for tx in txs],
}

##########################################################################################
# VAULT
##########################################################################################
@tx_endpoint(push=False)
async def vault_create(
self,
request: Dict[str, Any],
tx_config: TXConfig = DEFAULT_TX_CONFIG,
extra_conditions: Tuple[Condition, ...] = tuple(),
) -> EndpointResult:
"""
Create a new vault
"""
assert self.service.wallet_state_manager
secp_pk = bytes.fromhex(str(request.get("secp_pk")))
hp_index = request.get("hp_index", 0)
hidden_puzzle_hash = get_vault_hidden_puzzle_with_index(hp_index).get_tree_hash()
bls_pk = G1Element.from_bytes(bytes.fromhex(str(request.get("bls_pk"))))
timelock = uint64(request["timelock"])
fee = uint64(request.get("fee", 0))
genesis_challenge = DEFAULT_CONSTANTS.GENESIS_CHALLENGE

vault_record = await self.service.wallet_state_manager.create_vault_wallet(
secp_pk, hidden_puzzle_hash, bls_pk, timelock, genesis_challenge, tx_config, fee=fee
)

return {
"transactions": [vault_record.to_json_dict_convenience(self.service.config)],
}
24 changes: 24 additions & 0 deletions chia/rpc/wallet_rpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1662,3 +1662,27 @@ async def crcat_approve_pending(
},
)
return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]]

async def vault_create(
self,
secp_pk: bytes,
hp_index: uint32,
bls_pk: bytes,
timelock: uint64,
tx_config: TXConfig,
fee: uint64 = uint64(0),
push: bool = True,
) -> List[TransactionRecord]:
response = await self.fetch(
"vault_create",
{
"secp_pk": secp_pk.hex(),
"hp_index": hp_index,
"bls_pk": bls_pk.hex(),
"timelock": timelock,
"fee": fee,
"push": push,
**tx_config.to_json_dict(),
},
)
return [TransactionRecord.from_json_dict_convenience(tx) for tx in response["transactions"]]
44 changes: 33 additions & 11 deletions chia/wallet/vault/vault_drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.util.ints import uint64
from chia.util.ints import uint32, uint64
from chia.wallet.puzzles.load_clvm import load_clvm
from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import DEFAULT_HIDDEN_PUZZLE
from chia.wallet.util.merkle_tree import MerkleTree

# MODS
Expand All @@ -28,27 +29,48 @@ def construct_recovery_finish(timelock: uint64, recovery_conditions: Program) ->
return RECOVERY_FINISH_MOD.curry(timelock, recovery_conditions)


def construct_p2_recovery_puzzle(secp_puzzlehash: bytes32, bls_pk: G1Element, timelock: uint64) -> Program:
return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzlehash, bls_pk, timelock)
def construct_p2_recovery_puzzle(secp_puzzle_hash: bytes32, bls_pk: G1Element, timelock: uint64) -> Program:
return P2_RECOVERY_MOD.curry(P2_1_OF_N_MOD_HASH, RECOVERY_FINISH_MOD_HASH, secp_puzzle_hash, bls_pk, timelock)


def construct_vault_puzzle(secp_puzzlehash: bytes32, recovery_puzzlehash: bytes32) -> Program:
return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzlehash, recovery_puzzlehash]).calculate_root())
def construct_vault_puzzle(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> Program:
return P2_1_OF_N_MOD.curry(MerkleTree([secp_puzzle_hash, recovery_puzzle_hash]).calculate_root())


def get_vault_hidden_puzzle_with_index(index: uint32, hidden_puzzle: Program = DEFAULT_HIDDEN_PUZZLE) -> Program:
hidden_puzzle_with_index: Program = Program.to([6, (index, hidden_puzzle)])
return hidden_puzzle_with_index


def get_vault_inner_puzzle(
secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64
) -> Program:
secp_puzzle_hash = construct_p2_delegated_secp(secp_pk, genesis_challenge, entropy).get_tree_hash()
recovery_puzzle_hash = construct_p2_recovery_puzzle(secp_puzzle_hash, bls_pk, timelock).get_tree_hash()
return construct_vault_puzzle(secp_puzzle_hash, recovery_puzzle_hash)


def get_vault_inner_puzzle_hash(
secp_pk: bytes, genesis_challenge: bytes32, entropy: bytes, bls_pk: G1Element, timelock: uint64
) -> bytes32:
vault_puzzle = get_vault_inner_puzzle(secp_pk, genesis_challenge, entropy, bls_pk, timelock)
vault_puzzle_hash: bytes32 = vault_puzzle.get_tree_hash()
return vault_puzzle_hash


# MERKLE
def construct_vault_merkle_tree(secp_puzzlehash: bytes32, recovery_puzzlehash: bytes32) -> MerkleTree:
return MerkleTree([secp_puzzlehash, recovery_puzzlehash])
def construct_vault_merkle_tree(secp_puzzle_hash: bytes32, recovery_puzzle_hash: bytes32) -> MerkleTree:
return MerkleTree([secp_puzzle_hash, recovery_puzzle_hash])


def get_vault_proof(merkle_tree: MerkleTree, puzzlehash: bytes32) -> Program:
proof = merkle_tree.generate_proof(puzzlehash)
def get_vault_proof(merkle_tree: MerkleTree, puzzle_hash: bytes32) -> Program:
proof = merkle_tree.generate_proof(puzzle_hash)
vault_proof: Program = Program.to((proof[0], proof[1][0]))
return vault_proof


# SECP SIGNATURE
def construct_secp_message(
delegated_puzzlehash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, entropy: bytes
delegated_puzzle_hash: bytes32, coin_id: bytes32, genesis_challenge: bytes32, entropy: bytes
) -> bytes:
return delegated_puzzlehash + coin_id + genesis_challenge + entropy
return delegated_puzzle_hash + coin_id + genesis_challenge + entropy
79 changes: 76 additions & 3 deletions chia/wallet/wallet_state_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
)

import aiosqlite
from chia_rs import G1Element, G2Element, PrivateKey
from chia_rs import AugSchemeMPL, G1Element, G2Element, PrivateKey

from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward
from chia.consensus.coinbase import farmer_parent_id, pool_parent_id
Expand All @@ -47,7 +47,7 @@
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_record import CoinRecord
from chia.types.coin_spend import CoinSpend, compute_additions
from chia.types.coin_spend import CoinSpend, compute_additions, make_spend
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.types.spend_bundle import SpendBundle
from chia.util.bech32m import encode_puzzle_hash
Expand Down Expand Up @@ -110,7 +110,12 @@
puzzle_hash_for_synthetic_public_key,
)
from chia.wallet.sign_coin_spends import sign_coin_spends
from chia.wallet.singleton import create_singleton_puzzle, get_inner_puzzle_from_singleton, get_singleton_id_from_puzzle
from chia.wallet.singleton import (
SINGLETON_LAUNCHER_PUZZLE,
create_singleton_puzzle,
get_inner_puzzle_from_singleton,
get_singleton_id_from_puzzle,
)
from chia.wallet.trade_manager import TradeManager
from chia.wallet.trading.trade_status import TradeStatus
from chia.wallet.transaction_record import TransactionRecord
Expand All @@ -128,6 +133,7 @@
last_change_height_cs,
)
from chia.wallet.util.wallet_types import CoinType, WalletIdentifier, WalletType
from chia.wallet.vault.vault_drivers import get_vault_inner_puzzle_hash
from chia.wallet.vc_wallet.cr_cat_drivers import CRCAT, ProofsChecker, construct_pending_approval_state
from chia.wallet.vc_wallet.cr_cat_wallet import CRCATWallet
from chia.wallet.vc_wallet.vc_drivers import VerifiedCredential
Expand Down Expand Up @@ -2547,3 +2553,70 @@ async def sign_transaction(self, coin_spends: List[CoinSpend]) -> SpendBundle:
self.constants.MAX_BLOCK_COST_CLVM,
[puzzle_hash_for_synthetic_public_key],
)

async def create_vault_wallet(
self,
secp_pk: bytes,
hidden_puzzle_hash: bytes32,
bls_pk: G1Element,
timelock: uint64,
genesis_challenge: bytes32,
tx_config: TXConfig,
fee: uint64 = uint64(0),
) -> TransactionRecord:
"""
Returns a tx record for creating a new vault
"""
wallet = self.main_wallet
vault_inner_puzzle_hash = get_vault_inner_puzzle_hash(
secp_pk, genesis_challenge, hidden_puzzle_hash, bls_pk, timelock
)
# Get xch coin
amount = uint64(1)
coins = await wallet.select_coins(uint64(amount + fee), tx_config.coin_selection_config)

# Create singleton launcher
origin = next(iter(coins))
launcher_coin = Coin(origin.name(), SINGLETON_LAUNCHER_HASH, amount)

genesis_launcher_solution = Program.to([vault_inner_puzzle_hash, amount, [secp_pk, hidden_puzzle_hash]])
announcement_message = genesis_launcher_solution.get_tree_hash()

[tx_record] = await wallet.generate_signed_transaction(
amount,
SINGLETON_LAUNCHER_HASH,
tx_config,
fee,
coins,
None,
origin_id=origin.name(),
extra_conditions=(
AssertCoinAnnouncement(asserted_id=launcher_coin.name(), asserted_msg=announcement_message),
),
)

launcher_cs = make_spend(launcher_coin, SINGLETON_LAUNCHER_PUZZLE, genesis_launcher_solution)
launcher_sb = SpendBundle([launcher_cs], AugSchemeMPL.aggregate([]))
assert tx_record.spend_bundle is not None
full_spend = SpendBundle.aggregate([tx_record.spend_bundle, launcher_sb])

vault_record = TransactionRecord(
confirmed_at_height=uint32(0),
created_at_time=uint64(int(time.time())),
amount=uint64(amount),
to_puzzle_hash=vault_inner_puzzle_hash,
fee_amount=fee,
confirmed=False,
sent=uint32(0),
spend_bundle=full_spend,
additions=full_spend.additions(),
removals=full_spend.removals(),
wallet_id=wallet.id(),
sent_to=[],
trade_id=None,
type=uint32(TransactionType.INCOMING_TX.value),
name=full_spend.name(),
memos=[],
valid_times=ConditionValidTimes(),
)
return vault_record
4 changes: 4 additions & 0 deletions tests/wallet/vault/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from __future__ import annotations

job_timeout = 90
checkout_blocks_and_plots = True
70 changes: 70 additions & 0 deletions tests/wallet/vault/test_vault_wallet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import annotations

from hashlib import sha256

import pytest
from ecdsa import NIST256p, SigningKey

from chia.util.ints import uint32, uint64
from chia.wallet.util.tx_config import DEFAULT_TX_CONFIG
from tests.conftest import ConsensusMode
from tests.environments.wallet import WalletStateTransition, WalletTestFramework

SECP_SK = SigningKey.generate(curve=NIST256p, hashfunc=sha256)
SECP_PK = SECP_SK.verifying_key.to_string("compressed")


@pytest.mark.parametrize(
"wallet_environments",
[
{
"num_environments": 1,
"blocks_needed": [1],
}
],
indirect=True,
)
@pytest.mark.limit_consensus_modes(allowed=[ConsensusMode.HARD_FORK_2_0], reason="requires secp")
@pytest.mark.anyio
async def test_vault_creation(wallet_environments: WalletTestFramework) -> None:
# Setup
env = wallet_environments.environments[0]
client = env.rpc_client

fingerprint = (await client.get_public_keys())[0]
bls_pk_hex = (await client.get_private_key(fingerprint))["pk"]
bls_pk = bytes.fromhex(bls_pk_hex)

timelock = uint64(1000)
hp_index = uint32(1)

res = await client.vault_create(SECP_PK, hp_index, bls_pk, timelock, tx_config=DEFAULT_TX_CONFIG, fee=uint64(10))
vault_tx = res[0]
assert vault_tx

eve_coin = [item for item in vault_tx.additions if item not in vault_tx.removals and item.amount == 1][0]
assert eve_coin

await env.wallet_state_manager.add_pending_transactions(res)

await wallet_environments.process_pending_states(
[
WalletStateTransition(
pre_block_balance_updates={
1: {
"unconfirmed_wallet_balance": -11, # 1 for vault singleton, 10 for fee
"pending_coin_removal_count": 2,
"<=#spendable_balance": -11,
"<=#max_send_amount": -11,
"set_remainder": True,
}
},
post_block_balance_updates={
1: {
"confirmed_wallet_balance": -11,
"set_remainder": True,
}
},
)
]
)
Loading