rune: sign transactions with libsecp256k1
This commit is contained in:
parent
641c8437c0
commit
7a20e5edc7
4 changed files with 43 additions and 25 deletions
|
|
@ -134,7 +134,7 @@ class Tx(BaseMessage):
|
||||||
return sha256(bytes(self.raw)).hexdigest()
|
return sha256(bytes(self.raw)).hexdigest()
|
||||||
|
|
||||||
# raises exception on failure:
|
# 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(
|
sign_doc = SignDoc(
|
||||||
bodyBytes = bytes(self.body),
|
bodyBytes = bytes(self.body),
|
||||||
authInfoBytes = bytes(self.authInfo),
|
authInfoBytes = bytes(self.authInfo),
|
||||||
|
|
@ -144,29 +144,15 @@ class Tx(BaseMessage):
|
||||||
pubkey = self.authInfo.signerInfos[0].publicKey.key.data
|
pubkey = self.authInfo.signerInfos[0].publicKey.key.data
|
||||||
msghash = sha256(bytes(sign_doc)).digest()
|
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():
|
# ecdsa.keys.VerifyingKey.verify_digest():
|
||||||
# raises BadSignatureError if the signature is invalid or malformed
|
# raises BadSignatureError if the signature is invalid or malformed
|
||||||
import ecdsa
|
import ecdsa
|
||||||
ec_pubkey = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1)
|
ec_pubkey = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1)
|
||||||
ec_pubkey.verify_digest(sig, msghash)
|
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:
|
else:
|
||||||
raise ValueError(f'verify_sig(): {backend}: unrecognized backend')
|
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)
|
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ from typing import Annotated, List, Optional
|
||||||
from pure_protobuf.annotations import Field
|
from pure_protobuf.annotations import Field
|
||||||
from pure_protobuf.message import BaseMessage
|
from pure_protobuf.message import BaseMessage
|
||||||
|
|
||||||
|
from ...secp256k1.util import sign_message
|
||||||
|
|
||||||
from ...cosmos.tx.protobuf import (
|
from ...cosmos.tx.protobuf import (
|
||||||
ModeInfo,
|
ModeInfo,
|
||||||
SignDoc,
|
SignDoc,
|
||||||
|
|
@ -32,8 +34,7 @@ from ...cosmos.tx.protobuf import (
|
||||||
Fee,
|
Fee,
|
||||||
AuthInfo,
|
AuthInfo,
|
||||||
Tx,
|
Tx,
|
||||||
TxMsg,
|
TxMsg)
|
||||||
make_sig)
|
|
||||||
|
|
||||||
send_tx_parms = namedtuple(
|
send_tx_parms = namedtuple(
|
||||||
'rune_send_tx_parms', [
|
'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))
|
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(
|
sign_doc = SignDoc(
|
||||||
bodyBytes = bytes(body),
|
bodyBytes = bytes(body),
|
||||||
authInfoBytes = bytes(auth_info),
|
authInfoBytes = bytes(auth_info),
|
||||||
|
|
|
||||||
20
mmgen/proto/secp256k1/util.py
Executable file
20
mmgen/proto/secp256k1/util.py
Executable 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]
|
||||||
|
|
@ -90,11 +90,12 @@ parse_vectors = [
|
||||||
'4ca00a66119b07c5168c6c78e22299becc82d5f311ee79ae9183776b0dff3269'))
|
'4ca00a66119b07c5168c6c78e22299becc82d5f311ee79ae9183776b0dff3269'))
|
||||||
]
|
]
|
||||||
|
|
||||||
_bv = namedtuple('build_vector', ['txid', 'parms', 'null_fee'], defaults=[None])
|
_bv = namedtuple('build_vector', ['txid', 'txid2', 'parms', 'null_fee'], defaults=[None])
|
||||||
|
|
||||||
build_vectors = [
|
build_vectors = [
|
||||||
_bv(
|
_bv(
|
||||||
'3939612d0ddc55fd4d1c6ef118d1b2085a6655cb57d55ac9efd658467e039e0c',
|
'3939612d0ddc55fd4d1c6ef118d1b2085a6655cb57d55ac9efd658467e039e0c',
|
||||||
|
'e783ced14909a9e2a21c99b6b3b66fb38ba3bdf985876bb8d6cea02813d603a9',
|
||||||
send_tx_parms(
|
send_tx_parms(
|
||||||
'thor1tx3nm6xfynq3re5ehtm6530z0pah9qjeu0r9nd',
|
'thor1tx3nm6xfynq3re5ehtm6530z0pah9qjeu0r9nd',
|
||||||
'thor1j5u6vlr8kzt76fe7896hsmurkhgn68j0z4qa6w',
|
'thor1j5u6vlr8kzt76fe7896hsmurkhgn68j0z4qa6w',
|
||||||
|
|
@ -105,6 +106,7 @@ build_vectors = [
|
||||||
wifkey = 'L5nWojqqMLq7wh3CfhxUNYQ38acABD6sUao9dfb8i5B5wSefCJXe')),
|
wifkey = 'L5nWojqqMLq7wh3CfhxUNYQ38acABD6sUao9dfb8i5B5wSefCJXe')),
|
||||||
_bv(
|
_bv(
|
||||||
'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
|
'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
|
||||||
|
'444e026fe5d0988da602dc22f0ff6172c080f1d2e2d66012d86f4afd314b78d6',
|
||||||
deposit_tx_parms(
|
deposit_tx_parms(
|
||||||
'THOR', 'RUNE', 'RUNE',
|
'THOR', 'RUNE', 'RUNE',
|
||||||
'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
|
'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
|
||||||
|
|
@ -120,6 +122,7 @@ build_vectors = [
|
||||||
swap_build_vectors = [
|
swap_build_vectors = [
|
||||||
_bv(
|
_bv(
|
||||||
'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
|
'0d41e0ee40cd18a991cd8f0ef0e60e4c5bea898c53d54e00b6dddc0c9ce7edb7',
|
||||||
|
'444e026fe5d0988da602dc22f0ff6172c080f1d2e2d66012d86f4afd314b78d6',
|
||||||
swap_tx_parms(
|
swap_tx_parms(
|
||||||
'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
|
'thor18ug6p4zs5dsy0m3u69gf5md5ssdg8hqkk8aya4',
|
||||||
'123.456789',
|
'123.456789',
|
||||||
|
|
@ -161,10 +164,14 @@ def test_tx(src, cfg, vec):
|
||||||
tx.verify_sig(proto, parms.account_number)
|
tx.verify_sig(proto, parms.account_number)
|
||||||
|
|
||||||
pubkey = tx.authInfo.signerInfos[0].publicKey.key.data
|
pubkey = tx.authInfo.signerInfos[0].publicKey.key.data
|
||||||
|
vec_txid2 = getattr(vec, 'txid2', None)
|
||||||
assert hash160(pubkey) == getattr(
|
assert hash160(pubkey) == getattr(
|
||||||
tx.body.messages[0].body,
|
tx.body.messages[0].body,
|
||||||
'fromAddress' if msg_type == 'MsgSend' else 'signer')
|
'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:
|
if src == 'parse' and parms.from_addr:
|
||||||
built_tx = build_tx(cfg, proto, parms, null_fee=vec.null_fee)
|
built_tx = build_tx(cfg, proto, parms, null_fee=vec.null_fee)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue