rune: sign transactions with libsecp256k1

This commit is contained in:
The MMGen Project 2025-06-29 14:04:46 +00:00
commit 7a20e5edc7
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
4 changed files with 43 additions and 25 deletions

View file

@ -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)

View file

@ -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
mmgen/proto/secp256k1/util.py Executable file
View file

@ -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]

View file

@ -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)