Browse Source

rune: sign transactions with libsecp256k1

The MMGen Project 5 months ago
parent
commit
7a20e5edc7

+ 6 - 20
mmgen/proto/cosmos/tx/protobuf.py

@@ -134,7 +134,7 @@ class Tx(BaseMessage):
 		return sha256(bytes(self.raw)).hexdigest()
 
 	# raises exception on failure:
-	def verify_sig(self, proto, account_number, backend='ecdsa'):
+	def verify_sig(self, proto, account_number, backend='secp256k1'):
 		sign_doc = SignDoc(
 			bodyBytes = bytes(self.body),
 			authInfoBytes = bytes(self.authInfo),
@@ -144,29 +144,15 @@ class Tx(BaseMessage):
 		pubkey = self.authInfo.signerInfos[0].publicKey.key.data
 		msghash = sha256(bytes(sign_doc)).digest()
 
-		if backend == 'ecdsa':
+		if backend == 'secp256k1':
+			from ...secp256k1.secp256k1 import verify_sig
+			if not verify_sig(sig, msghash, pubkey):
+				raise ValueError('signature verification failed')
+		elif backend == 'ecdsa':
 			# ecdsa.keys.VerifyingKey.verify_digest():
 			#   raises BadSignatureError if the signature is invalid or malformed
 			import ecdsa
 			ec_pubkey = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1)
 			ec_pubkey.verify_digest(sig, msghash)
-		elif backend == 'py_ecc':
-			from py_ecc.secp256k1.secp256k1 import ecdsa_raw_recover
-			y_is_odd = pubkey[0] - 2
-			x, y = ecdsa_raw_recover(
-				msghash,
-				(28 - y_is_odd, int.from_bytes(sig[:32]), int.from_bytes(sig[32:])))
-			assert y & 1 == y_is_odd, f'verify_sig(): parity mismatch for TX {self.txid}'
-			assert int.from_bytes(pubkey[1:]) == x, f'verify_sig(): invalid signature for TX {self.txid}'
 		else:
 			raise ValueError(f'verify_sig(): {backend}: unrecognized backend')
-
-# cosmjs/packages/crypto/src/secp256k1signature.ts
-# cosmjs/packages/amino/src/signature.ts
-#   Signature must be 64 bytes long. Cosmos SDK uses a 2x32 byte fixed length
-#   encoding for the secp256k1 signature integers r and s.
-def make_sig(*, sign_doc, sec_bytes):
-	from py_ecc.secp256k1.secp256k1 import ecdsa_raw_sign
-	msghash = sha256(bytes(sign_doc)).digest()
-	_, r, s = ecdsa_raw_sign(msghash, sec_bytes)
-	return r.to_bytes(length=32) + s.to_bytes(length=32)

+ 8 - 3
mmgen/proto/rune/tx/protobuf.py

@@ -21,6 +21,8 @@ from typing import Annotated, List, Optional
 from pure_protobuf.annotations import Field
 from pure_protobuf.message import BaseMessage
 
+from ...secp256k1.util import sign_message
+
 from ...cosmos.tx.protobuf import (
 	ModeInfo,
 	SignDoc,
@@ -32,8 +34,7 @@ from ...cosmos.tx.protobuf import (
 	Fee,
 	AuthInfo,
 	Tx,
-	TxMsg,
-	make_sig)
+	TxMsg)
 
 send_tx_parms = namedtuple(
 	'rune_send_tx_parms', [
@@ -265,7 +266,11 @@ def build_tx(cfg, proto, parms, *, null_fee=False, skip_body_memo=False):
 
 	auth_info = AuthInfo(signerInfos=[signer_info], fee=Fee(gasLimit=p.gas_limit, amount=fee_amt))
 
-	signature = make_sig(
+	# cosmjs/packages/crypto/src/secp256k1signature.ts
+	# cosmjs/packages/amino/src/signature.ts
+	#   Signature must be 64 bytes long. Cosmos SDK uses a 2x32 byte fixed length
+	#   encoding for the secp256k1 signature integers r and s.
+	signature = sign_message(
 		sign_doc = SignDoc(
 			bodyBytes = bytes(body),
 			authInfoBytes = bytes(auth_info),

+ 20 - 0
mmgen/proto/secp256k1/util.py

@@ -0,0 +1,20 @@
+#!/usr/bin/env python3
+#
+# MMGen Wallet, a terminal-based cryptocurrency wallet
+# Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen-wallet
+#   https://gitlab.com/mmgen/mmgen-wallet
+
+"""
+proto.secp256k1.util: secp256k1 elliptic curve utility functions
+"""
+
+def sign_message(*, sign_doc, sec_bytes):
+	from hashlib import sha256
+	from .secp256k1 import sign_msghash
+	return sign_msghash(
+		sha256(bytes(sign_doc)).digest(),
+		sec_bytes)[0]

+ 9 - 2
test/modtest_d/rune.py

@@ -90,11 +90,12 @@ parse_vectors = [
 			'4ca00a66119b07c5168c6c78e22299becc82d5f311ee79ae9183776b0dff3269'))
 ]
 
-_bv = namedtuple('build_vector', ['txid', 'parms', 'null_fee'], defaults=[None])
+_bv = namedtuple('build_vector', ['txid', 'txid2', 'parms', 'null_fee'], defaults=[None])
 
 build_vectors = [
 	_bv(
 		'3939612d0ddc55fd4d1c6ef118d1b2085a6655cb57d55ac9efd658467e039e0c',
+		'e783ced14909a9e2a21c99b6b3b66fb38ba3bdf985876bb8d6cea02813d603a9',
 		send_tx_parms(
 			'thor1tx3nm6xfynq3re5ehtm6530z0pah9qjeu0r9nd',
 			'thor1j5u6vlr8kzt76fe7896hsmurkhgn68j0z4qa6w',
@@ -105,6 +106,7 @@ build_vectors = [
 			wifkey = 'L5nWojqqMLq7wh3CfhxUNYQ38acABD6sUao9dfb8i5B5wSefCJXe')),
 	_bv(
 		'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
+		'444e026fe5d0988da602dc22f0ff6172c080f1d2e2d66012d86f4afd314b78d6',
 		deposit_tx_parms(
 			'THOR', 'RUNE', 'RUNE',
 			'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
@@ -120,6 +122,7 @@ build_vectors = [
 swap_build_vectors = [
 	_bv(
 		'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
+		'444e026fe5d0988da602dc22f0ff6172c080f1d2e2d66012d86f4afd314b78d6',
 		swap_tx_parms(
 			'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
 			'123.456789',
@@ -161,10 +164,14 @@ def test_tx(src, cfg, vec):
 	tx.verify_sig(proto, parms.account_number)
 
 	pubkey = tx.authInfo.signerInfos[0].publicKey.key.data
+	vec_txid2 = getattr(vec, 'txid2', None)
 	assert hash160(pubkey) == getattr(
 		tx.body.messages[0].body,
 		'fromAddress' if msg_type == 'MsgSend' else 'signer')
-	assert tx.txid == vec.txid, tx.txid
+	if tx.txid not in (vec.txid, vec_txid2):
+		raise ValueError(f'{tx.txid} not in ({vec.txid}, {vec_txid2})')
+	if tx.txid == vec_txid2:
+		ymsg('\nWarning: non-standard TxID produced')
 
 	if src == 'parse' and parms.from_addr:
 		built_tx = build_tx(cfg, proto, parms, null_fee=vec.null_fee)