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

[CHIA-1552] Proposed set of drivers for multi-sig custody #18686

Draft
wants to merge 80 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
461e28d
Initial draft of custody architecture
Quexington Oct 4, 2024
0189e3f
Rename CustodyType -> Puzzle
Quexington Oct 7, 2024
ace0072
Rename UnknownCustody -> UnknownPuzzle
Quexington Oct 7, 2024
c41889a
Rename CustodyHint -> PuzzleHint
Quexington Oct 7, 2024
d247f7e
Rename CustodyWithRestrictions -> PuzzleWithRestrictions
Quexington Oct 7, 2024
b23519d
Reorganize for clarity
Quexington Oct 7, 2024
8381bbd
Add support for identifying and filling in unknown puzzles
Quexington Oct 7, 2024
d38331a
rename custody -> puzzle
Quexington Oct 7, 2024
5d7a00d
MofN puzzles
Quexington Oct 8, 2024
e5b995d
Restriction layer
Quexington Oct 9, 2024
e5443ce
Fix PlaceholderPuzzle with MofN restriction
Quexington Oct 9, 2024
29dea66
Add indexing
Quexington Oct 9, 2024
632428f
Elaborate on potential optimizations
Quexington Oct 9, 2024
624cf5b
Light comment improvements
Quexington Oct 9, 2024
e3273eb
Add spec namespace
Quexington Oct 9, 2024
89e091b
3.10 compatibility I think?
Quexington Oct 9, 2024
c482b4d
pylint I hate you
Quexington Oct 10, 2024
253647a
pylint I really hate you
Quexington Oct 10, 2024
ab8e33b
Add an optimized puzzle for 1ofN
Quexington Oct 10, 2024
78d9e38
Coverage ignores
Quexington Oct 10, 2024
c546246
[CHIA-1619] Introduce a 1 of N optimization for multi sig drivers (#1…
Quexington Oct 11, 2024
745a45e
Add optimization for NofN
Quexington Oct 11, 2024
3705bfa
[CHIA-1622] Introduce an N of N optimization for multi sig drivers (#…
Quexington Oct 15, 2024
b976541
Add support for `PuzzleWithRestrictions` with no MofN
Quexington Oct 15, 2024
3db92f4
Add support for `PuzzleWithRestrictions` with no MofN (#18709)
Quexington Oct 16, 2024
39a3954
comments
Quexington Oct 16, 2024
e4cf7e7
Fix members with restrictions inside of MofNs
Quexington Oct 16, 2024
c10d6e5
pylint
Quexington Oct 16, 2024
6947bbc
Fix members with restrictions inside of MofNs (#18722)
Quexington Oct 17, 2024
b720bb7
Rework restrictions
Quexington Oct 17, 2024
f2723af
Rework restrictions to restrict dpuzs in and member conditions out (#…
Quexington Oct 17, 2024
8c6d44e
Use feeder pattern for MofNs as well
Quexington Oct 17, 2024
633f6b1
f you pylint
Quexington Oct 18, 2024
919fe9a
Use feeder pattern for MofNs as well (#18728)
Quexington Oct 18, 2024
dcaf04f
initial commit
matt-o-how Oct 21, 2024
406c0b5
start work on tests
matt-o-how Oct 21, 2024
cf4077d
fix all but signature in test
matt-o-how Oct 21, 2024
db265a9
fix test
matt-o-how Oct 21, 2024
7784b83
black, isort, flake8
matt-o-how Oct 21, 2024
62b2c16
migrate changes to new files / folders
matt-o-how Oct 22, 2024
f0fbd5e
mypy fixes
matt-o-how Oct 22, 2024
bb392dc
remove unused imports
matt-o-how Oct 22, 2024
2093c73
add bls_member to deployed_puzzle_hashes
matt-o-how Oct 22, 2024
d544e24
no memo for BLS Member
matt-o-how Oct 22, 2024
81a77c5
change comments on BLSMember
matt-o-how Oct 22, 2024
80516a0
chialispp bls_member
matt-o-how Oct 22, 2024
0ab7039
update test comment
matt-o-how Oct 22, 2024
5613000
black again
matt-o-how Oct 22, 2024
e21e3f8
pylint disable
matt-o-how Oct 22, 2024
81b73aa
add a more minimal test for blsmember
matt-o-how Oct 22, 2024
b6e3519
fix quex comments
matt-o-how Oct 22, 2024
b34ba20
flake8
matt-o-how Oct 22, 2024
8aa659c
Add basic framework for delegated puzzle validators
Quexington Oct 22, 2024
73667f2
code coverage for memo()
matt-o-how Oct 23, 2024
8ba7685
don't reuse variable of different type
matt-o-how Oct 23, 2024
805c41a
Add a BLSMember puzzle, a corresponding class, and a test (#18738)
Quexington Oct 23, 2024
f4c12f8
coverage
Quexington Oct 23, 2024
6c80e86
Merge remote-tracking branch 'origin/quex.multi_sig_chialisp_drivers'…
Quexington Oct 23, 2024
e6e749d
pre-commit
Quexington Oct 23, 2024
7520fd6
Add basic framework for delegated puzzle validators (#18751)
Quexington Oct 23, 2024
1658b3b
Merge remote-tracking branch 'origin/main' into quex.multi_sig_chiali…
Quexington Nov 5, 2024
9aa4dbb
pre-commit
Quexington Nov 5, 2024
00a7f1a
Fix spend sim import
Quexington Nov 5, 2024
907728d
Add passkey member puzzle
Quexington Nov 6, 2024
c77a5fe
Dedup a bit
Quexington Nov 6, 2024
9c41a49
Add `Timelock` restriction
Quexington Nov 8, 2024
adeb3ed
Add `Timelock` restriction (#18838)
Quexington Nov 8, 2024
0b98bc0
Rename variable
Quexington Nov 12, 2024
e37fc67
[CHIA-1616] Add passkey member puzzle (#18832)
Quexington Nov 12, 2024
5fc78e0
Add coin announcement restriction
Quexington Nov 12, 2024
95fd655
initial commit
matt-o-how Nov 13, 2024
bcfac76
Add comments per @matt-o-how
Quexington Nov 13, 2024
88f0e70
typo
Quexington Nov 13, 2024
b3e9ec5
correct formatting for using secp_verify
matt-o-how Nov 13, 2024
bd25bd0
secp256r1 fix
matt-o-how Nov 13, 2024
97c9dc6
pretty print lisp
matt-o-how Nov 13, 2024
d66c2c6
add to deployed puzzle hashes
matt-o-how Nov 13, 2024
9b1aafa
ruff formatted
matt-o-how Nov 13, 2024
fea40d7
[CHIA-1719] Add coin announcement restriction (#18853)
Quexington Nov 13, 2024
08f13b9
Secp256r1 Member Puzzle (#18863)
Quexington Nov 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
382 changes: 382 additions & 0 deletions chia/_tests/clvm/test_custody_architecture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,382 @@
from __future__ import annotations

import itertools
from dataclasses import dataclass, field, replace
from typing import List, Literal

import pytest
from chia_rs import G2Element

from chia._tests.util.spend_sim import CostLogger, sim_and_client
from chia.types.blockchain_format.program import Program
from chia.types.blockchain_format.sized_bytes import bytes32
from chia.types.coin_spend import make_spend
from chia.types.mempool_inclusion_status import MempoolInclusionStatus
from chia.wallet.conditions import CreateCoinAnnouncement
from chia.wallet.puzzles.custody.custody_architecture import (
DelegatedPuzzleAndSolution,
MemberOrDPuz,
MofN,
ProvenSpend,
Puzzle,
PuzzleHint,
PuzzleWithRestrictions,
Restriction,
RestrictionHint,
UnknownPuzzle,
UnknownRestriction,
)
from chia.wallet.wallet_spend_bundle import WalletSpendBundle

BUNCH_OF_ZEROS = bytes32([0] * 32)
BUNCH_OF_ONES = bytes32([1] * 32)
BUNCH_OF_TWOS = bytes32([2] * 32)
BUNCH_OF_THREES = bytes32([3] * 32)
ANY_PROGRAM = Program.to(None)


@pytest.mark.parametrize(
"restrictions",
[
# no restrictions
[],
# member validator
[UnknownRestriction(RestrictionHint(True, BUNCH_OF_ZEROS, ANY_PROGRAM))],
# dpuz validator
[UnknownRestriction(RestrictionHint(False, BUNCH_OF_ZEROS, ANY_PROGRAM))],
# multiple restrictions of various types
[
UnknownRestriction(RestrictionHint(True, BUNCH_OF_ZEROS, ANY_PROGRAM)),
UnknownRestriction(RestrictionHint(False, BUNCH_OF_ZEROS, ANY_PROGRAM)),
],
],
)
@pytest.mark.parametrize(
"puzzle",
[
# Custody puzzle
UnknownPuzzle(PuzzleHint(BUNCH_OF_ZEROS, ANY_PROGRAM)),
# 1 of 2 (w/ & w/o restrictions)
MofN(
1,
[
PuzzleWithRestrictions(1, [], UnknownPuzzle(PuzzleHint(BUNCH_OF_ZEROS, ANY_PROGRAM))),
PuzzleWithRestrictions(
2,
[
UnknownRestriction(RestrictionHint(True, BUNCH_OF_ZEROS, ANY_PROGRAM)),
UnknownRestriction(RestrictionHint(True, BUNCH_OF_ZEROS, ANY_PROGRAM)),
],
UnknownPuzzle(PuzzleHint(BUNCH_OF_ONES, ANY_PROGRAM)),
),
],
),
# 2 of 2 (further 1 of 1s)
MofN(
2,
[
PuzzleWithRestrictions(
1,
[],
MofN(1, [PuzzleWithRestrictions(3, [], UnknownPuzzle(PuzzleHint(BUNCH_OF_ZEROS, ANY_PROGRAM)))]),
),
PuzzleWithRestrictions(
4,
[],
MofN(1, [PuzzleWithRestrictions(5, [], UnknownPuzzle(PuzzleHint(BUNCH_OF_ONES, ANY_PROGRAM)))]),
),
],
),
],
)
def test_back_and_forth_hint_parsing(restrictions: List[Restriction[MemberOrDPuz]], puzzle: Puzzle) -> None:
"""
This tests that a PuzzleWithRestrictions can be exported to a clvm program to be reimported from.

This is necessary functionality to sync an unknown inner puzzle from on chain.
"""
cwr = PuzzleWithRestrictions(
nonce=0,
restrictions=restrictions,
puzzle=puzzle,
)

assert PuzzleWithRestrictions.from_memo(cwr.memo()) == cwr


def test_unknown_puzzle_behavior() -> None:
"""
Once an inner puzzle is loaded from chain, all of its nodes are of the UnknownPuzzle type. To spend the puzzle,
at least one of these nodes must be replaced with a Puzzle that implements the `.puzzle(nonce: int)` method.

This test tests the ability to replace one or many of these nodes.
"""

@dataclass(frozen=True)
class PlaceholderPuzzle:
@property
def member_not_dpuz(self) -> bool:
raise NotImplementedError() # pragma: no cover

def memo(self, nonce: int) -> Program:
raise NotImplementedError() # pragma: no cover

def puzzle(self, nonce: int) -> Program:
raise NotImplementedError() # pragma: no cover

def puzzle_hash(self, nonce: int) -> bytes32:
return bytes32([nonce] * 32)

# First a simple PuzzleWithRestrictions that is really just a Puzzle
unknown_puzzle_0 = UnknownPuzzle(PuzzleHint(BUNCH_OF_ZEROS, ANY_PROGRAM))
pwr = PuzzleWithRestrictions(0, [], unknown_puzzle_0)
assert pwr.unknown_puzzles == {BUNCH_OF_ZEROS: unknown_puzzle_0}
known_puzzles = {BUNCH_OF_ZEROS: PlaceholderPuzzle()}
assert pwr.fill_in_unknown_puzzles(known_puzzles) == PuzzleWithRestrictions(0, [], PlaceholderPuzzle())

# Now we add some restrictions
unknown_restriction_1 = UnknownRestriction(RestrictionHint(True, BUNCH_OF_ONES, ANY_PROGRAM))
unknown_restriction_2 = UnknownRestriction(RestrictionHint(False, BUNCH_OF_TWOS, ANY_PROGRAM))
pwr = replace(pwr, restrictions=[unknown_restriction_1, unknown_restriction_2])
assert pwr.unknown_puzzles == {
BUNCH_OF_ZEROS: unknown_puzzle_0,
BUNCH_OF_ONES: unknown_restriction_1,
BUNCH_OF_TWOS: unknown_restriction_2,
}
known_puzzles = {
BUNCH_OF_ZEROS: PlaceholderPuzzle(),
BUNCH_OF_ONES: PlaceholderPuzzle(),
BUNCH_OF_TWOS: PlaceholderPuzzle(),
}
assert pwr.fill_in_unknown_puzzles(known_puzzles) == PuzzleWithRestrictions(
0, [PlaceholderPuzzle(), PlaceholderPuzzle()], PlaceholderPuzzle()
)

# Now we do test an MofN recursion
unknown_puzzle_3 = UnknownPuzzle(PuzzleHint(BUNCH_OF_THREES, ANY_PROGRAM))
pwr = replace(
pwr,
puzzle=MofN(
m=1,
members=[PuzzleWithRestrictions(0, [], unknown_puzzle_0), PuzzleWithRestrictions(1, [], unknown_puzzle_3)],
),
)
assert pwr.unknown_puzzles == {
BUNCH_OF_ZEROS: unknown_puzzle_0,
BUNCH_OF_ONES: unknown_restriction_1,
BUNCH_OF_TWOS: unknown_restriction_2,
BUNCH_OF_THREES: unknown_puzzle_3,
}
known_puzzles = {
BUNCH_OF_ZEROS: PlaceholderPuzzle(),
BUNCH_OF_ONES: PlaceholderPuzzle(),
BUNCH_OF_TWOS: PlaceholderPuzzle(),
BUNCH_OF_THREES: PlaceholderPuzzle(),
}
assert pwr.fill_in_unknown_puzzles(known_puzzles) == PuzzleWithRestrictions(
0,
[PlaceholderPuzzle(), PlaceholderPuzzle()],
MofN(
m=1,
members=[
PuzzleWithRestrictions(0, [], PlaceholderPuzzle()),
PuzzleWithRestrictions(1, [], PlaceholderPuzzle()),
],
),
)


# (mod (delegated_puzzle . rest) rest)
ACS_MEMBER = Program.to(3)
ACS_MEMBER_PH = ACS_MEMBER.get_tree_hash()


@dataclass(frozen=True)
class ACSMember:
def memo(self, nonce: int) -> Program:
raise NotImplementedError() # pragma: no cover

def puzzle(self, nonce: int) -> Program:
# (r (c (q . nonce) ACS_MEMBER_PH))
return Program.to([6, [4, (1, nonce), ACS_MEMBER]])

def puzzle_hash(self, nonce: int) -> bytes32:
return self.puzzle(nonce).get_tree_hash()


@dataclass(frozen=True)
class ACSDPuzValidator:
member_not_dpuz: Literal[False] = field(init=False, default=False)

def memo(self, nonce: int) -> Program:
raise NotImplementedError() # pragma: no cover

def puzzle(self, nonce: int) -> Program:
# (mod (dpuz . program) (a program conditions))
return Program.to([2, 3, 2])

def puzzle_hash(self, nonce: int) -> bytes32:
return self.puzzle(nonce).get_tree_hash()


@pytest.mark.anyio
@pytest.mark.parametrize(
"with_restrictions",
[True, False],
)
async def test_m_of_n(cost_logger: CostLogger, with_restrictions: bool) -> None:
"""
This tests the various functionality of the MofN drivers including that m of n puzzles can be constructed and solved
for every combination of its nodes from size 1 - 5.
"""
restrictions: List[Restriction[MemberOrDPuz]] = [ACSDPuzValidator()] if with_restrictions else []
async with sim_and_client() as (sim, client):
for m in range(1, 6): # 1 - 5 inclusive
for n in range(2, 6):
m_of_n = PuzzleWithRestrictions(
0, [], MofN(m, [PuzzleWithRestrictions(n_i, restrictions, ACSMember()) for n_i in range(0, n)])
)

# Farm and find coin
await sim.farm_block(m_of_n.puzzle_hash())
m_of_n_coin = (
await client.get_coin_records_by_puzzle_hashes([m_of_n.puzzle_hash()], include_spent_coins=False)
)[0].coin
block_height = sim.block_height

# Create two announcements to be asserted from a) the delegated puzzle b) the puzzle in the MofN
announcement_1 = CreateCoinAnnouncement(msg=b"foo", coin_id=m_of_n_coin.name())
announcement_2 = CreateCoinAnnouncement(msg=b"bar", coin_id=m_of_n_coin.name())

# Test a spend of every combination of m of n
for indexes in itertools.combinations(range(0, n), m):
proven_spends = {
PuzzleWithRestrictions(index, restrictions, ACSMember()).puzzle_hash(
_top_level=False
): ProvenSpend(
PuzzleWithRestrictions(index, restrictions, ACSMember()).puzzle_reveal(_top_level=False),
PuzzleWithRestrictions(index, restrictions, ACSMember()).solve(
[],
[Program.to(None)] if with_restrictions else [],
Program.to(
[announcement_1.to_program(), announcement_2.corresponding_assertion().to_program()]
),
),
)
for index in indexes
}
assert isinstance(m_of_n.puzzle, MofN)
result = await client.push_tx(
cost_logger.add_cost(
f"M={m}, N={n}, indexes={indexes}{'w/ res.' if with_restrictions else ''}",
WalletSpendBundle(
[
make_spend(
m_of_n_coin,
m_of_n.puzzle_reveal(),
m_of_n.solve(
[],
[],
m_of_n.puzzle.solve(proven_spends), # pylint: disable=no-member
DelegatedPuzzleAndSolution(
Program.to(1),
Program.to(
[
announcement_2.to_program(),
announcement_1.corresponding_assertion().to_program(),
]
),
),
),
)
],
G2Element(),
),
)
)
assert result == (MempoolInclusionStatus.SUCCESS, None)
await sim.farm_block()
await sim.rewind(block_height)


@dataclass(frozen=True)
class ACSMemberValidator:
member_not_dpuz: Literal[True] = field(init=False, default=True)

def memo(self, nonce: int) -> Program:
raise NotImplementedError() # pragma: no cover

def puzzle(self, nonce: int) -> Program:
# (mod (conditions . program) (a program conditions))
return Program.to([2, 3, 2])

def puzzle_hash(self, nonce: int) -> bytes32:
return self.puzzle(nonce).get_tree_hash()


@pytest.mark.anyio
async def test_restriction_layer(cost_logger: CostLogger) -> None:
"""
This tests the capabilities of the optional restriction layer placed on inner puzzles.
"""
async with sim_and_client() as (sim, client):
pwr = PuzzleWithRestrictions(
0, [ACSMemberValidator(), ACSMemberValidator(), ACSDPuzValidator(), ACSDPuzValidator()], ACSMember()
)

# Farm coin with puzzle inside
await sim.farm_block(pwr.puzzle_hash())
pwr_coin = (await client.get_coin_records_by_puzzle_hashes([pwr.puzzle_hash()], include_spent_coins=False))[
0
].coin

# Some announcements to make a ring between the delegated puzzle and the inner puzzle
announcement_1 = CreateCoinAnnouncement(msg=b"foo", coin_id=pwr_coin.name())
announcement_2 = CreateCoinAnnouncement(msg=b"bar", coin_id=pwr_coin.name())

dpuz = Program.to(1)
dpuzhash = dpuz.get_tree_hash()
result = await client.push_tx(
cost_logger.add_cost(
"Puzzle with 4 restrictions (2 member validators & 2 dpuz validators) all ACS",
WalletSpendBundle(
[
make_spend(
pwr_coin,
pwr.puzzle_reveal(),
pwr.solve(
[
Program.to(None),
# (mod conditions (r (r conditions)))
# checks length >= 2
Program.to(7),
],
[
Program.to(None),
# (mod dpuzhash (if (= dpuzhash <dpuzhash>) () (x)))
# (a (i (= 1 (q . <dpuzhash>)) () (q 8)) 1)
Program.to([2, [3, [9, 1, (1, dpuzhash)], None, [1, 8]], 1]),
],
Program.to(
[
announcement_1.to_program(),
announcement_2.corresponding_assertion().to_program(),
]
),
DelegatedPuzzleAndSolution(
dpuz,
Program.to(
[
announcement_2.to_program(),
announcement_1.corresponding_assertion().to_program(),
]
),
),
),
)
],
G2Element(),
),
)
)
assert result == (MempoolInclusionStatus.SUCCESS, None)
Loading
Loading