diff --git a/chia/_tests/clvm/test_member_puzzles.py b/chia/_tests/clvm/test_member_puzzles.py index 5932388933e4..ae41bf0b7790 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 +from chia.wallet.puzzles.custody.member_puzzles.member_puzzles import ( + BLSMember, + PasskeyMember, + SECPK1Member, + SECPR1Member, +) from chia.wallet.wallet_spend_bundle import WalletSpendBundle @@ -355,3 +360,82 @@ 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 + 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) + + 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) + 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( + [ + 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) diff --git a/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py b/chia/wallet/puzzles/custody/member_puzzles/member_puzzles.py index 346bc227a8d2..827e262fb0a9 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,17 @@ 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() 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..78b4d37f9046 --- /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) + ) +) 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 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",