From 95ba042b5dfa38b698c64c9b641a1fce43d3d34e Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 15:05:59 +0000 Subject: [PATCH 1/9] initial commit --- chia/_tests/clvm/test_member_puzzles.py | 75 ++++++++++++++++++- .../custody/member_puzzles/member_puzzles.py | 17 +++++ .../member_puzzles/secp256k1_member.clsp | 11 +++ .../member_puzzles/secp256k1_member.clsp.hex | 1 + 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp create mode 100644 chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp.hex diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 5932388933e4..1fcce17cf149 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -28,7 +28,7 @@ PuzzleWithRestrictions, Restriction, ) -from chia.wallet.puzzles.custody.member_puzzles.member_puzzles import BLSMember, PasskeyMember, SECPR1Member +from chia.wallet.puzzles.custody.member_puzzles.member_puzzles import BLSMember, PasskeyMember, SECPR1Member, SECPK1Member from chia.wallet.wallet_spend_bundle import WalletSpendBundle @@ -355,3 +355,76 @@ async def test_secp256r1_member(cost_logger: CostLogger) -> None: assert result == (MempoolInclusionStatus.SUCCESS, None) await sim.farm_block() await sim.rewind(block_height) + +@pytest.mark.anyio +async def test_secp256k1_member(cost_logger: CostLogger) -> None: + async with sim_and_client() as (sim, client): + delegated_puzzle = Program.to(1) + delegated_puzzle_hash = delegated_puzzle.get_tree_hash() + + # setup keys + seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 + secp_sk = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) + secp_pk = secp_sk.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + + secpk1_member = SECPK1Member(secp_pk) + + secpk1_puzzle = PuzzleWithRestrictions(0, [], secpk1_member) + + # Farm and find coin + await sim.farm_block(secpk1_puzzle.puzzle_hash()) + coin = ( + await client.get_coin_records_by_puzzle_hashes([secpk1_puzzle.puzzle_hash()], include_spent_coins=False) + )[0].coin + block_height = sim.block_height + + # Create an announcements to be asserted in the delegated puzzle + announcement = CreateCoinAnnouncement(msg=b"foo", coin_id=coin.name()) + + # Get signature for AGG_SIG_ME + coin_id = coin.name() + signature_message = delegated_puzzle_hash + coin_id + der_sig = secp_sk.sign( + signature_message, + # The type stubs are weird here, `deterministic_signing` is assuredly an argument + ec.ECDSA(hashes.SHA256(), deterministic_signing=True), # type: ignore[call-arg] + ) + r, s = decode_dss_signature(der_sig) + sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") + sb = WalletSpendBundle( + [ + make_spend( + coin, + secpk1_puzzle.puzzle_reveal(), + secpk1_puzzle.solve( + [], + [], + Program.to( + [ + coin_id, + sig, + ] + ), + DelegatedPuzzleAndSolution( + delegated_puzzle, + Program.to( + [ + announcement.to_program(), + announcement.corresponding_assertion().to_program(), + ] + ), + ), + ), + ) + ], + G2Element(), + ) + result = await client.push_tx( + cost_logger.add_cost( + "secp spendbundle", + sb, + ) + ) + assert result == (MempoolInclusionStatus.SUCCESS, None) + await sim.farm_block() + await sim.rewind(block_height) \ No newline at end of file diff --git a/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py b/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py index 346bc227a8d2..68e679ca9253 100644 --- a/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py +++ b/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py @@ -24,6 +24,10 @@ "secp256r1_member.clsp", package_or_requirement="chia.wallet.puzzles.custody.member_puzzles" ) +SECPK1_MEMBER_MOD = load_clvm_maybe_recompile( + "secp256k1_member.clsp", package_or_requirement="chia.wallet.puzzles.custody.member_puzzles" +) + @dataclass(frozen=True) class BLSMember(Puzzle): @@ -80,3 +84,16 @@ def puzzle(self, nonce: int) -> Program: def puzzle_hash(self, nonce: int) -> bytes32: return self.puzzle(nonce).get_tree_hash() + +@dataclass(frozen=True) +class SECPK1Member(Puzzle): + secp_pk: bytes + + def memo(self, nonce: int) -> Program: + return Program.to(0) + + def puzzle(self, nonce: int) -> Program: + return SECPK1_MEMBER_MOD.curry(self.secp_pk) + + def puzzle_hash(self, nonce: int) -> bytes32: + return self.puzzle(nonce).get_tree_hash() \ No newline at end of file diff --git a/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp b/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp new file mode 100644 index 000000000000..665297eec9cd --- /dev/null +++ b/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp @@ -0,0 +1,11 @@ +; this puzzle follows the Managed Inner Puzzle Spec MIPS01 as a Member Puzzle +; this code offers a secure approval of a delegated puzzle passed in as a Truth to be run elsewhere + +(mod (SECP_PK Delegated_Puzzle_Hash my_id signature) ; delegated puzzle is passed in from the above M of N layer + (include condition_codes.clib) + + (c + (list ASSERT_MY_COIN_ID my_id) + (secp256k1_verify SECP_PK (sha256 Delegated_Puzzle_Hash my_id) signature) + ) +) \ No newline at end of file diff --git a/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp.hex b/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp.hex new file mode 100644 index 000000000000..82e6adf3b0c9 --- /dev/null +++ b/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff04ffff04ff02ffff04ff17ff808080ffff8413d61f00ff05ffff0bff0bff1780ff2f8080ffff04ffff0146ff018080 From 59219cd61517b4da4f18cac78c95c65b0227d266 Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 15:37:34 +0000 Subject: [PATCH 2/9] fix test --- chia/_tests/clvm/test_member_puzzles.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 1fcce17cf149..3c52dfe85b74 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -363,8 +363,7 @@ async def test_secp256k1_member(cost_logger: CostLogger) -> None: delegated_puzzle_hash = delegated_puzzle.get_tree_hash() # setup keys - seed = 0x1A62C9636D1C9DB2E7D564D0C11603BF456AAD25AA7B12BDFD762B4E38E7EDC6 - secp_sk = ec.derive_private_key(seed, ec.SECP256R1(), default_backend()) + secp_sk = ec.generate_private_key(ec.SECP256R1()) secp_pk = secp_sk.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) secpk1_member = SECPK1Member(secp_pk) From b500daa0205b11130652cc96145ae377853c939f Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 15:38:23 +0000 Subject: [PATCH 3/9] fix_test_2 --- chia/_tests/clvm/test_member_puzzles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 3c52dfe85b74..c9e42f9208b9 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -363,7 +363,7 @@ async def test_secp256k1_member(cost_logger: CostLogger) -> None: delegated_puzzle_hash = delegated_puzzle.get_tree_hash() # setup keys - secp_sk = ec.generate_private_key(ec.SECP256R1()) + secp_sk = ec.generate_private_key(ec.SECP256K1()) secp_pk = secp_sk.public_key().public_bytes(Encoding.X962, PublicFormat.CompressedPoint) secpk1_member = SECPK1Member(secp_pk) From e8b321922d22235b89254b467b4f20b116d7c6ab Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 15:43:58 +0000 Subject: [PATCH 4/9] ruff format --- chia/_tests/clvm/test_member_puzzles.py | 10 ++++++++-- .../puzzles/custody/member_puzzles/member_puzzles.py | 3 ++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index c9e42f9208b9..89640552b670 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -28,7 +28,12 @@ PuzzleWithRestrictions, Restriction, ) -from chia.wallet.puzzles.custody.member_puzzles.member_puzzles import BLSMember, PasskeyMember, SECPR1Member, SECPK1Member +from chia.wallet.puzzles.custody.member_puzzles.member_puzzles import ( + BLSMember, + PasskeyMember, + SECPR1Member, + SECPK1Member, +) from chia.wallet.wallet_spend_bundle import WalletSpendBundle @@ -356,6 +361,7 @@ async def test_secp256r1_member(cost_logger: CostLogger) -> None: await sim.farm_block() await sim.rewind(block_height) + @pytest.mark.anyio async def test_secp256k1_member(cost_logger: CostLogger) -> None: async with sim_and_client() as (sim, client): @@ -426,4 +432,4 @@ async def test_secp256k1_member(cost_logger: CostLogger) -> None: ) assert result == (MempoolInclusionStatus.SUCCESS, None) await sim.farm_block() - await sim.rewind(block_height) \ No newline at end of file + await sim.rewind(block_height) diff --git a/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py b/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py index 68e679ca9253..827e262fb0a9 100644 --- a/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py +++ b/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py @@ -85,6 +85,7 @@ def puzzle(self, nonce: int) -> Program: def puzzle_hash(self, nonce: int) -> bytes32: return self.puzzle(nonce).get_tree_hash() + @dataclass(frozen=True) class SECPK1Member(Puzzle): secp_pk: bytes @@ -96,4 +97,4 @@ def puzzle(self, nonce: int) -> Program: return SECPK1_MEMBER_MOD.curry(self.secp_pk) def puzzle_hash(self, nonce: int) -> bytes32: - return self.puzzle(nonce).get_tree_hash() \ No newline at end of file + return self.puzzle(nonce).get_tree_hash() From e030ac13f576059f0703d40828d18caa0361d3f4 Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 15:50:44 +0000 Subject: [PATCH 5/9] add secp256k1_member to deployed puzzle hashes --- chia/wallet/puzzles/deployed_puzzle_hashes.json | 1 + 1 file changed, 1 insertion(+) diff --git a/chia/wallet/puzzles/deployed_puzzle_hashes.json b/chia/wallet/puzzles/deployed_puzzle_hashes.json index ae6b717e428c..2c85b660cfbc 100644 --- a/chia/wallet/puzzles/deployed_puzzle_hashes.json +++ b/chia/wallet/puzzles/deployed_puzzle_hashes.json @@ -66,6 +66,7 @@ "restrictions": "a28d59d39f964a93159c986b1914694f6f2f1c9901178f91e8b0ba4045980eef", "rom_bootstrap_generator": "161bade1f822dcd62ab712ebaf30f3922a301e48a639e4295c5685f8bece7bd9", "secp256r1_member": "05aaa1f2fb6c48b5bce952b09f3da99afa4241989878a9919aafb7d74b70ac54", + "secp256k1_member": "2b05daf134c9163acc8f2ac05b61f7d8328fca3dcc963154a28e89bcfc4dbfca", "settlement_payments": "cfbfdeed5c4ca2de3d0bf520b9cb4bb7743a359bd2e6a188d19ce7dffc21d3e7", "singleton_launcher": "eff07522495060c066f66f32acc2a77e3a3e737aca8baea4d1a64ea4cdc13da9", "singleton_top_layer": "24e044101e57b3d8c908b8a38ad57848afd29d3eecc439dba45f4412df4954fd", From 95e24bbf155d4eef4886800274e765bc6bf34c9d Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 15:58:58 +0000 Subject: [PATCH 6/9] pretty print lisp --- .../wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp b/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp index 665297eec9cd..78b4d37f9046 100644 --- a/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp +++ b/chia/wallet/puzzles/custody/member_puzzles/secp256k1_member.clsp @@ -8,4 +8,4 @@ (list ASSERT_MY_COIN_ID my_id) (secp256k1_verify SECP_PK (sha256 Delegated_Puzzle_Hash my_id) signature) ) -) \ No newline at end of file +) From 6afbb613cd4506ac64c62a9a561020f96c7f05e6 Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 16:11:42 +0000 Subject: [PATCH 7/9] ruff check fix. --- chia/_tests/clvm/test_member_puzzles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 89640552b670..967dcdfb5389 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -31,8 +31,8 @@ from chia.wallet.puzzles.custody.member_puzzles.member_puzzles import ( BLSMember, PasskeyMember, - SECPR1Member, SECPK1Member, + SECPR1Member, ) from chia.wallet.wallet_spend_bundle import WalletSpendBundle From 73c82b22ed8c72d15533513e9d51ec753475d118 Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 16:38:55 +0000 Subject: [PATCH 8/9] curve_order fix --- chia/_tests/clvm/test_member_puzzles.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 967dcdfb5389..92c66386d581 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -394,7 +394,13 @@ async def test_secp256k1_member(cost_logger: CostLogger) -> None: # The type stubs are weird here, `deterministic_signing` is assuredly an argument ec.ECDSA(hashes.SHA256(), deterministic_signing=True), # type: ignore[call-arg] ) - r, s = decode_dss_signature(der_sig) + r, _s = decode_dss_signature(der_sig) + curve_order = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + if _s > curve_order // 2: + s = -_s % curve_order + else: + s = _s + sig = r.to_bytes(32, byteorder='big') + s.to_bytes(32, byteorder='big') sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") sb = WalletSpendBundle( [ From 85daa78305da4c27f13056cbd0027d008a76e0a1 Mon Sep 17 00:00:00 2001 From: Matthew Howard Date: Thu, 14 Nov 2024 16:55:47 +0000 Subject: [PATCH 9/9] ruff format --- chia/_tests/clvm/test_member_puzzles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 92c66386d581..ae41bf0b7790 100644 --- a/chia/_tests/clvm/test_member_puzzles.py +++ b/chia/_tests/clvm/test_member_puzzles.py @@ -395,12 +395,12 @@ async def test_secp256k1_member(cost_logger: CostLogger) -> None: ec.ECDSA(hashes.SHA256(), deterministic_signing=True), # type: ignore[call-arg] ) r, _s = decode_dss_signature(der_sig) - curve_order = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + curve_order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 if _s > curve_order // 2: s = -_s % curve_order else: s = _s - sig = r.to_bytes(32, byteorder='big') + s.to_bytes(32, byteorder='big') + sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") sig = r.to_bytes(32, byteorder="big") + s.to_bytes(32, byteorder="big") sb = WalletSpendBundle( [