From 7a20e5edc7f3745758adabc4ccd0329315b9c25c Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sun, 29 Jun 2025 14:04:46 +0000 Subject: [PATCH] rune: sign transactions with libsecp256k1 --- mmgen/proto/cosmos/tx/protobuf.py | 26 ++++++-------------------- mmgen/proto/rune/tx/protobuf.py | 11 ++++++++--- mmgen/proto/secp256k1/util.py | 20 ++++++++++++++++++++ test/modtest_d/rune.py | 11 +++++++++-- 4 files changed, 43 insertions(+), 25 deletions(-) create mode 100755 mmgen/proto/secp256k1/util.py diff --git a/mmgen/proto/cosmos/tx/protobuf.py b/mmgen/proto/cosmos/tx/protobuf.py index e72fee1f..a3e05157 100755 --- a/mmgen/proto/cosmos/tx/protobuf.py +++ b/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) diff --git a/mmgen/proto/rune/tx/protobuf.py b/mmgen/proto/rune/tx/protobuf.py index c8e405d5..4e372fbd 100755 --- a/mmgen/proto/rune/tx/protobuf.py +++ b/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), diff --git a/mmgen/proto/secp256k1/util.py b/mmgen/proto/secp256k1/util.py new file mode 100755 index 00000000..58e86687 --- /dev/null +++ b/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 +# 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] diff --git a/test/modtest_d/rune.py b/test/modtest_d/rune.py index 7d1e2304..405d91dd 100755 --- a/test/modtest_d/rune.py +++ b/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)