Nostr key/address generation

Examples:

    # Generate 10 Nostr addresses from your default wallet:
    $ mmgen-addrgen --coin=nostr 1-10

    # Generate 10 Nostr key/address pairs from your default wallet:
    $ mmgen-keygen --coin=nostr 1-10

    # Generate a random Nostr key/address pair:
    $ mmgen-tool --coin=nostr randpair

    # Convert a Nostr address to a hexadecimal pubkey:
    $ mmgen-tool --coin=nostr addr2pubhex npub1r7m96hpfeegjcj8un2wjgkw4rwv36x9mtmtek5duv3uq29tcwqgq93nwkz

    # Convert a Nostr hexadecimal pubkey to an address:
    $ mmgen-tool --coin=nostr pubhex2addr 1fb65d5c29ce512c48fc9a9d2459d51b991d18bb5ed79b51bc64780515787010

Testing:

    $ test/modtest.py -v nostr
    $ test/cmdtest.py --coin=nostr ref3_addr
    $ test/tooltest2.py --coin=nostr
This commit is contained in:
The MMGen Project 2026-06-04 10:41:42 +00:00
commit e322b4338c
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
16 changed files with 201 additions and 6 deletions

View file

@ -45,12 +45,14 @@ class MMGenAddrType(HiliteStr, InitErrors, MMGenObject):
desc = ImmutableAttr(str)
pubhash_fmts = ('p2pkh', 'bech32', 'ethereum')
pubkey_fmts = ('bech32pk')
mmtypes = {
'L': ati('legacy', 'std', False,'p2pkh', 'p2pkh', 'wif', (), 'Legacy uncompressed address'),
'C': ati('compressed','std', True, 'p2pkh', 'p2pkh', 'wif', (), 'Compressed P2PKH address'),
'S': ati('segwit', 'std', True, 'segwit', 'p2sh', 'wif', (), 'Segwit P2SH-P2WPKH address'),
'B': ati('bech32', 'std', True, 'bech32', 'bech32', 'wif', (), 'Native Segwit (Bech32) address'),
'X': ati('bech32x', 'std', True, 'p2pkh', 'bech32', 'wif', (), 'Cross-chain Bech32 address'),
'K': ati('bech32pk', 'std', True, 'bech32pk','bech32', 'wif', (), 'Cross-chain pubkey Bech32 address'),
'E': ati('ethereum', 'std', False,'ethereum','p2pkh', 'privkey', ('wallet_passwd',),'Ethereum address'),
'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif', ('viewkey',), 'Zcash z-address'),
'M': ati('monero', 'monero', False,'monero', 'monero', 'spendkey',('viewkey','wallet_passwd'),'Monero address')}

View file

@ -60,6 +60,7 @@ def AddrGenerator(cfg, proto, addr_type):
'segwit': 'btc',
'bech32': 'btc',
'bech32x': 'xchain',
'bech32pk': 'xchain',
'monero': 'xmr',
'ethereum': 'eth',
'zcash_z': 'zec'}

View file

@ -136,7 +136,7 @@ def check_privkey(key_int):
class BipHDConfig(Lockable):
supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch', 'rune')
supported_coins = ('btc', 'eth', 'doge', 'ltc', 'bch', 'rune', 'nostr')
def __init__(self, base_cfg, coin, *, network, addr_type, from_path, no_path_checks):
@ -149,7 +149,8 @@ class BipHDConfig(Lockable):
'coin': coin,
'network': network,
'type': addr_type or None,
'quiet': True})
'quiet': True},
need_amt = False)
dfl_type = base_cfg._proto.dfl_mmtype
addr_type = MMGenAddrType(

View file

@ -111,6 +111,7 @@ IDX CHAIN CURVE NW ADDR_CLS VB_PRV VB_PUB VB_WIF VB_ADDR DFL_PA
996 OKT x m Okex x x - - x OKChain Token
1023 ONE x m One x x - - x HARMONY-ONE (Legacy)
1024 ONT nist m Neo x x - spec x Ontology
1237 NOSTR x m BechPK x x - h:npub x Nostr
1729 XTZ edw m Xtz x x - spec 0'/0' Tezos
1815 ADA khol m AdaByronIcarus 0f4331d4 x - spec x Cardano
9000 AVAX x m AvaxXChain x x - - x Avalanche
@ -958,7 +959,6 @@ IDX CHAIN NAME
1170 HOO - Hoo Smart Chain
1234 ALPH - Alephium
1236 --- - Masca
1237 --- - Nostr
1280 --- - Kudos Setler
1284 GLMR - Moonbeam
1285 MOVR - Moonriver

View file

@ -1 +1 @@
May 2026
June 2026

View file

@ -1 +1 @@
16.2.dev1
16.2.dev2

View file

@ -113,6 +113,7 @@ mods = {
),
'coin': (
'addr2pubhash',
'addr2pubhex',
'addr2scriptpubkey',
'eth_checksummed_addr',
'hex2wif',

63
mmgen/proto/nostr/params.py Executable file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env python3
#
# MMGen Wallet, a terminal-based cryptocurrency wallet
# Copyright (C)2013-2026 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.nostr.params: Nostr protocol
"""
from ...protocol import CoinProtocol, decoded_wif, decoded_addr, _nw
from ...addr import CoinAddr
from ...contrib import bech32
class mainnet(CoinProtocol.Secp256k1):
mod_clsname = 'Nostr'
network_names = _nw('mainnet', None, None)
mmtypes = ('K',)
dfl_mmtype = 'K'
base_proto = 'Nostr'
base_proto_coin = 'NOSTR'
base_coin = 'NOSTR'
bech32_hrp = 'npub'
bech32_hrp_prv = 'nsec'
def encode_wif(self, privbytes, pubkey_type, *, compressed): # input is preprocessed
return bech32.bech32_encode(
hrp = self.bech32_hrp_prv,
data = bech32.convertbits(privbytes, 8, 5))
def decode_wif(self, wif):
hrp, data = bech32.bech32_decode(wif)
assert hrp == self.bech32_hrp_prv, f'Bech32 HRP does not match! ({hrp} != {self.bech32_hrp_prv})'
return decoded_wif(
sec = bytes(bech32.convertbits(data, 5, 8, False)),
pubkey_type = None,
compressed = True) if data else False
def decode_addr(self, addr):
hrp, data = bech32.bech32_decode(addr)
assert hrp == self.bech32_hrp, f'Bech32 HRP does not match! ({hrp} != {self.bech32_hrp})'
return decoded_addr(
bytes(bech32.convertbits(data, 5, 8, False)),
ver_bytes = None,
fmt = 'bech32pk') if data else False
def encode_addr_bech32pk(self, pubkey):
match len(pubkey):
case 33:
assert pubkey[0] in (2, 3), f'{pubkey[0]}: invalid first byte for {self.name} protocol public key'
case 32:
pass
case x:
raise ValueError(f'{x}: invalid public key length for {self.name} protocol')
return CoinAddr(
self,
bech32.bech32_encode(
hrp = self.bech32_hrp,
data = bech32.convertbits(pubkey[-32:], 8, 5)))

View file

@ -20,3 +20,9 @@ class bech32x(addr_generator.base):
@check_data
def to_addr(self, data):
return self.proto.encode_addr_bech32x(hash160(data.pubkey))
class bech32pk(addr_generator.base):
@check_data
def to_addr(self, data):
return self.proto.encode_addr_bech32pk(data.pubkey)

View file

@ -47,6 +47,7 @@ class CoinProtocol(MMGenObject):
'etc': proto_info('EthereumClassic', 4),
'zec': proto_info('Zcash', 2),
'xmr': proto_info('Monero', 5),
'nostr': proto_info('Nostr', 4),
'rune': proto_info('THORChain', 4)}
class Base(Lockable):

View file

@ -183,6 +183,10 @@ class tool_cmd(tool_cmd_base):
die(2, f'{ap.fmt} addresses cannot be converted to {ptype}')
return ap.bytes.hex()
def addr2pubhex(self, addr: 'sstr'):
"convert coin address to public key"
return self._addr2pub(addr, ptype='pubkey')
def addr2pubhash(self, addr: 'sstr'):
"convert coin address to public key hash"
return self._addr2pub(addr, ptype='pubhash')

View file

@ -96,6 +96,7 @@ packages =
mmgen.proto.eth.tx
mmgen.proto.eth.tw
mmgen.proto.ltc
mmgen.proto.nostr
mmgen.proto.rune
mmgen.proto.rune.rpc
mmgen.proto.rune.tw

View file

@ -214,7 +214,7 @@ class CmdTestRef3Seed(CmdTestBase, CmdTestShared):
class CmdTestRef3Addr(CmdTestRef3Seed):
'generated reference address and key-address files for 128-, 192- and 256-bit seeds'
networks = ('btc', 'btc_tn', 'ltc', 'ltc_tn', 'bch', 'bch_tn')
networks = ('btc', 'btc_tn', 'ltc', 'ltc_tn', 'bch', 'bch_tn', 'nostr')
passthru_opts = ('coin', 'testnet', 'cashaddr')
tmpdir_nums = [26, 27, 28]
shared_deps = ['mmdat', pwfile]
@ -330,6 +330,24 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
'btc': ('D0DD BDE3 87BE 15AE', '7552 D70C AAB8 DEAA'),
'ltc': ('74A0 7DD5 963B 6326', '2CDA A007 4B9F E9A5'),
},
'refaddrgen_bech32pk_1': {
'nostr': ('005B CFEC B290 32FE',),
},
'refaddrgen_bech32pk_2': {
'nostr': ('9BB5 F044 6048 2007',),
},
'refaddrgen_bech32pk_3': {
'nostr': ('652F D99A 174F 9055',),
},
'refkeyaddrgen_bech32pk_1': {
'nostr': ('84B5 4714 A15C D72E',),
},
'refkeyaddrgen_bech32pk_2': {
'nostr': ('3802 94B3 27D3 F42A',),
},
'refkeyaddrgen_bech32pk_3': {
'nostr': ('9A54 4897 DC3D 80D0',),
},
}
cmd_group = (
@ -338,10 +356,12 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
('refaddrgen_compressed', 'new refwallet addr chksum (compressed)'),
('refaddrgen_segwit', 'new refwallet addr chksum (segwit)'),
('refaddrgen_bech32', 'new refwallet addr chksum (bech32)'),
('refaddrgen_bech32pk', 'new refwallet addr chksum (bech32pk)'),
('refkeyaddrgen_legacy', 'new refwallet key-addr chksum (uncompressed)'),
('refkeyaddrgen_compressed', 'new refwallet key-addr chksum (compressed)'),
('refkeyaddrgen_segwit', 'new refwallet key-addr chksum (segwit)'),
('refkeyaddrgen_bech32', 'new refwallet key-addr chksum (bech32)'),
('refkeyaddrgen_bech32pk', 'new refwallet key-addr chksum (bech32pk)'),
)
def call_addrgen(self, mmtype, name='addrgen'):
@ -362,6 +382,9 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
def refaddrgen_bech32(self):
return self.call_addrgen('bech32')
def refaddrgen_bech32pk(self):
return self.call_addrgen('bech32pk')
def refkeyaddrgen_legacy(self):
return self.call_addrgen('legacy', 'keyaddrgen')
@ -374,6 +397,9 @@ class CmdTestRef3Addr(CmdTestRef3Seed):
def refkeyaddrgen_bech32(self):
return self.call_addrgen('bech32', 'keyaddrgen')
def refkeyaddrgen_bech32pk(self):
return self.call_addrgen('bech32pk', 'keyaddrgen')
class CmdTestRef3Passwd(CmdTestRef3Seed):
'generated reference password files for 128-, 192- and 256-bit seeds'
tmpdir_nums = [26, 27, 28]

63
test/modtest_d/nostr.py Executable file
View file

@ -0,0 +1,63 @@
#!/usr/bin/env python3
"""
test.modtest_d.nostr: Nostr unit test for the MMGen suite
"""
from collections import namedtuple
from mmgen.bip_hd import BipHDNode, MasterNode
from mmgen.bip39 import bip39
from ..include.common import cfg, vmsg
# Source: https://nostr-nips.com/nip-06
nv = namedtuple('nostr_test_vector', 'mnemonic privhex nsec pubhex npub')
vecs = [
nv( 'leader monkey parrot ring guide accident before fence cannon height naive bean',
'7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a',
'nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp',
'17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917',
'npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu'),
nv( 'what bleak badge arrange retreat wolf trade produce cricket blur garlic valid '
'proud rude strong choose busy staff weather area salt hollow arm fade',
'c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add',
'nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel',
'd41b22899549e1f3d335a31002cfd382174006e166d3e658e3a5eecdb6463573',
'npub16sdj9zv4f8sl85e45vgq9n7nsgt5qphpvmf7vk8r5hhvmdjxx4es8rq74h')]
path = "m/44'/1237'/0'/0/0"
class unit_tests:
def path(self, name, ut):
for v in vecs:
vmsg(f'mnemonic: {v.mnemonic}')
seed = bip39().generate_seed(v.mnemonic.split())
res = BipHDNode.from_path(cfg, seed, path, coin='nostr')
xprv = res.key_extended(public=False)
xpub = res.key_extended(public=True)
vmsg(f'prv: {xprv.key.hex()}')
vmsg(f'pub: {xpub.key.hex()}')
vmsg(f'addr: {res.address}')
vmsg(f'wif: {res.privkey.wif}')
assert res.privkey.hex() == v.privhex
assert res.address.bytes.hex() == v.pubhex
assert xprv.key.hex() == v.privhex
assert xpub.key.hex()[2:] == v.pubhex
assert res.address == v.npub
assert res.privkey.wif == v.nsec
vmsg('')
return True
def derive(self, name, ut):
for v in vecs:
vmsg(f'mnemonic: {v.mnemonic}')
seed = bip39().generate_seed(v.mnemonic.split())
res = MasterNode(cfg, seed).to_chain(idx=0, coin='nostr').derive_private(0)
vmsg(f'addr: {res.address}')
vmsg(f'wif: {res.privkey.wif}')
assert res.address == v.npub
assert res.privkey.wif == v.nsec
vmsg('')
return True

View file

@ -135,6 +135,7 @@ init_tests() {
- $gentest_py --coin=xmr --use-internal-keccak-module 1 $rounds10x
- $gentest_py --coin=zec 1 $rounds10x
- $gentest_py --coin=zec --type=zcash_z 1 $rounds10x
- $gentest_py --coin=nostr 1 $rounds10x
- # verification against external libraries and tools:
- # pycoin
- $gentest_py --all-coins --type=legacy 1:pycoin $rounds
@ -321,6 +322,7 @@ init_tests() {
e $tooltest2_py --coin=eth --token=mm1 --testnet=1
e $tooltest2_py --coin=etc
t $tooltest2_py --coin=rune
t $tooltest2_py --coin=nostr
- $tooltest2_py --fork # run once with --fork so commands are actually executed
"
[ "$SKIP_ALT_DEP" ] && t_tool2_skip='a e t'

View file

@ -113,6 +113,16 @@ zec_pubhex1 = 'e6a4edbff547f21bcc2a825b6cf70f06e266a452d2da9d6dc5c1da3d99d7e996f
rune_addr1 = 'thor1xptlvmwaymaxa7pxkr2u5pn7c0508stcr9tw2z'
nostr_privhex1 = 'c15d739894c81a2fcfd3a2df85a0d2c0dbc47a280d092799f144d73d7ae78add'
nostr_pubhex1 = 'd41b22899549e1f3d335a31002cfd382174006e166d3e658e3a5eecdb6463573'
nostr_wif1 = 'nsec1c9wh8xy5eqdzln7n5t0ctgxjcrdug73gp5yj0x03gntn67h83twssdfhel'
nostr_addr1 = 'npub16sdj9zv4f8sl85e45vgq9n7nsgt5qphpvmf7vk8r5hhvmdjxx4es8rq74h'
nostr_privhex2 = '7f7ff03d123792d6ac594bfa67bf6d0c0ab55b6b1fdb6249303fe861f1ccba9a'
nostr_pubhex2 = '17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917'
nostr_wif2 = 'nsec10allq0gjx7fddtzef0ax00mdps9t2kmtrldkyjfs8l5xruwvh2dq0lhhkp'
nostr_addr2 = 'npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu'
tests = {
'Mnemonic': {
'hex2mn': (
@ -414,6 +424,12 @@ tests = {
([rune_addr1], pubhash2),
],
},
'addr2pubhex': {
'nostr_mainnet': [
([nostr_addr1], nostr_pubhex1),
([nostr_addr2], nostr_pubhex2),
],
},
'eth_checksummed_addr': {
'eth_mainnet': [
(['00a329c0648769a73afac7f9381e08fb43dbea72'], '00a329c0648769A73afAc7F9381E08FB43dBEA72'),
@ -462,6 +478,10 @@ tests = {
([privhex7], [btc_wif2, btc_addr3], ['--type=segwit'], 'segwit'),
([privhex7], [btc_wif2, btc_addr4], ['--type=bech32'], 'bech32'),
],
'nostr_mainnet': [
([nostr_privhex1], [nostr_wif1, nostr_addr1], None, 'bech32pk'),
([nostr_privhex2], [nostr_wif2, nostr_addr2], None, 'bech32pk'),
],
},
'privhex2addr': {
'btc_mainnet': [
@ -529,6 +549,10 @@ tests = {
'zec_mainnet': [
([zec_pubhex1], zec_addr1, ['--type=zcash_z'], 'zcash_z'),
],
'nostr_mainnet': [
([nostr_pubhex1], nostr_addr1, None, 'bech32pk'),
([nostr_pubhex2], nostr_addr2, None, 'bech32pk'),
],
},
'pubhex2redeem_script': {
'btc_mainnet': [