Subwallets, Part 1: basic framework and subwallet generation
Beginning with this commit, every MMGen wallet now has a two sets of associated subwallets with “long“ and “short” seeds. MMGen wallets and subwallets are functionally equivalent and externally indistinguishable. This has benefits, especially for real-world security, as well as drawbacks. For more information, see the `mmgen-subwalletgen` help screen: https://github.com/mmgen/mmgen/wiki/subwalletgen-[MMGen-command-help] This patch provides subwallet generation functionality and subseed display utilities. Support for transaction signing and address generation using a subwallet's parent wallet will be added in a forthcoming patch. Examples: # Create a bogus wallet in mnemonic format for testing purposes: $ echo $(yes bee | head -n24) > bogus.mmwords # List the wallet's first five subseed pairs: $ mmgen-tool list_subseeds 1-5 wallet=bogus.mmwords Parent Seed: DF449DA4 (256 bits) Long Subseeds Short Subseeds ------------- -------------- 1L: FC9A8735 1S: 930E1AD5 2L: 62B02F54 2S: DF14AB49 3L: 9E884E99 3S: AD3ABD98 4L: DB595AE1 4S: 3E885EC4 5L: 36D5A0D1 5S: 30D66FF5 # Generate the 5th short (128-bit) subwallet from the wallet: $ mmgen-subwalletgen bogus.mmwords 5S # Same as above, but output subwallet to mnemonic (seed phrase) format: $ mmgen-subwalletgen -o mn bogus.mmwords 5S ... Mnemonic data written to file '30D66FF5[128].mmwords' # View the subwallet's seed phrase: $ cat 30D66FF5[128].mmwords object capture field heart page observe road bond mother loser really army # Generate 10 addresses from the subwallet seed phrase: $ mmgen-addrgen 30D66FF5[128].mmwords 1-10 ... Addresses written to file '30D66FF5[1-10].addrs'
This commit is contained in:
parent
2f274cba2d
commit
7538a9460e
16 changed files with 527 additions and 11 deletions
24
cmds/mmgen-subwalletgen
Executable file
24
cmds/mmgen-subwalletgen
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
|
||||||
|
# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify it under
|
||||||
|
# the terms of the GNU General Public License as published by the Free Software
|
||||||
|
# Foundation, either version 3 of the License, or (at your option) any later
|
||||||
|
# version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
||||||
|
# details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License along with
|
||||||
|
# this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""
|
||||||
|
mmgen-subwalletgen: Generate a subwallet from an MMGen deterministic wallet
|
||||||
|
"""
|
||||||
|
|
||||||
|
from mmgen.main import launch
|
||||||
|
launch("subwalletgen")
|
||||||
|
|
@ -41,6 +41,9 @@
|
||||||
# Set the default hash preset:
|
# Set the default hash preset:
|
||||||
# hash_preset 3
|
# hash_preset 3
|
||||||
|
|
||||||
|
# Set the default number of subseeds:
|
||||||
|
# subseeds 100
|
||||||
|
|
||||||
# Set the default number of entropy characters to get from user.
|
# Set the default number of entropy characters to get from user.
|
||||||
# Must be between 10 and 80.
|
# Must be between 10 and 80.
|
||||||
# A value of 0 disables user entropy, but this is not recommended:
|
# A value of 0 disables user entropy, but this is not recommended:
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from mmgen.opts import opt
|
||||||
from mmgen.util import *
|
from mmgen.util import *
|
||||||
|
|
||||||
def help_notes(k):
|
def help_notes(k):
|
||||||
|
from mmgen.obj import SubSeedIdxRange
|
||||||
from mmgen.seed import SeedSource
|
from mmgen.seed import SeedSource
|
||||||
from mmgen.tx import MMGenTX
|
from mmgen.tx import MMGenTX
|
||||||
def fee_spec_letters(use_quotes=False):
|
def fee_spec_letters(use_quotes=False):
|
||||||
|
|
@ -40,6 +41,42 @@ def help_notes(k):
|
||||||
return {
|
return {
|
||||||
'rel_fee_desc': MMGenTX().rel_fee_desc,
|
'rel_fee_desc': MMGenTX().rel_fee_desc,
|
||||||
'fee_spec_letters': fee_spec_letters(),
|
'fee_spec_letters': fee_spec_letters(),
|
||||||
|
'subwallet': """
|
||||||
|
SUBWALLETS:
|
||||||
|
|
||||||
|
Subwallets (subseeds) are specified by a "Subseed Index" consisting of:
|
||||||
|
|
||||||
|
a) an integer in the range 1-{}, plus
|
||||||
|
b) an optional single letter, 'L' or 'S'
|
||||||
|
|
||||||
|
The letter designates the length of the subseed. If omitted, 'L' is assumed.
|
||||||
|
|
||||||
|
Long ('L') subseeds are the same length as their parent wallet's seed
|
||||||
|
(typically 256 bits), while short ('S') subseeds are always 128-bit.
|
||||||
|
The long and short subseeds for a given index are derived independently,
|
||||||
|
so both may be used.
|
||||||
|
|
||||||
|
MMGen has no notion of "depth", and to an outside observer subwallets are
|
||||||
|
identical to ordinary wallets. This is a feature rather than a bug, as it
|
||||||
|
denies an attacker any way of knowing whether a given wallet has a parent.
|
||||||
|
|
||||||
|
Since subwallets are just wallets, they may be used to generate other
|
||||||
|
subwallets, leading to hierarchies of arbitrary depth. However, this is
|
||||||
|
inadvisable in practice for two reasons: Firstly, it creates accounting
|
||||||
|
complexity, requiring the user to independently keep track of a derivation
|
||||||
|
tree. More importantly, however, it leads to the danger of Seed ID
|
||||||
|
collisions between subseeds at different levels of the hierarchy, as
|
||||||
|
MMGen checks and avoids ID collisions only among sibling subseeds.
|
||||||
|
|
||||||
|
An exception to this caveat would be a multi-user setup where sibling
|
||||||
|
subwallets are distributed to different users as their default wallets.
|
||||||
|
Since the subseeds derived from these subwallets are private to each user,
|
||||||
|
Seed ID collisions among them doesn't present a problem.
|
||||||
|
|
||||||
|
A safe rule of thumb, therefore, is for *each user* to derive all of his/her
|
||||||
|
subwallets from a single parent. This leaves each user with a total of two
|
||||||
|
million subwallets, which should be enough for most practical purposes.
|
||||||
|
""".strip().format(SubSeedIdxRange.max_idx),
|
||||||
'passwd': """
|
'passwd': """
|
||||||
PASSPHRASE NOTE:
|
PASSPHRASE NOTE:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,3 +39,4 @@ class BadMMGenTxID(Exception): mmcode = 4
|
||||||
class IllegalWitnessFlagValue(Exception): mmcode = 4
|
class IllegalWitnessFlagValue(Exception): mmcode = 4
|
||||||
class TxHexParseError(Exception): mmcode = 4
|
class TxHexParseError(Exception): mmcode = 4
|
||||||
class TxHexMismatch(Exception): mmcode = 4
|
class TxHexMismatch(Exception): mmcode = 4
|
||||||
|
class SubSeedNonceRangeExceeded(Exception): mmcode = 4
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ class g(object):
|
||||||
debug_opts = False
|
debug_opts = False
|
||||||
debug_rpc = False
|
debug_rpc = False
|
||||||
debug_addrlist = False
|
debug_addrlist = False
|
||||||
|
debug_subseed = False
|
||||||
quiet = False
|
quiet = False
|
||||||
no_license = False
|
no_license = False
|
||||||
force_256_color = False
|
force_256_color = False
|
||||||
|
|
@ -128,7 +129,7 @@ class g(object):
|
||||||
'quiet','tx_confs','tx_fee_adj','key_generator' )
|
'quiet','tx_confs','tx_fee_adj','key_generator' )
|
||||||
|
|
||||||
# user opt sets global var:
|
# user opt sets global var:
|
||||||
opt_sets_global = ( 'use_internal_keccak_module', )
|
opt_sets_global = ( 'use_internal_keccak_module','subseeds' )
|
||||||
|
|
||||||
# 'long' opts - opt sets global var
|
# 'long' opts - opt sets global var
|
||||||
common_opts = (
|
common_opts = (
|
||||||
|
|
@ -155,7 +156,7 @@ class g(object):
|
||||||
cfg_file_opts = (
|
cfg_file_opts = (
|
||||||
'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port',
|
'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port',
|
||||||
'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
|
'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
|
||||||
'daemon_data_dir','force_256_color','regtest',
|
'daemon_data_dir','force_256_color','regtest','subseeds',
|
||||||
'btc_max_tx_fee','ltc_max_tx_fee','bch_max_tx_fee','eth_max_tx_fee',
|
'btc_max_tx_fee','ltc_max_tx_fee','bch_max_tx_fee','eth_max_tx_fee',
|
||||||
'eth_mainnet_chain_name','eth_testnet_chain_name',
|
'eth_mainnet_chain_name','eth_testnet_chain_name',
|
||||||
'max_tx_file_size','max_input_size'
|
'max_tx_file_size','max_input_size'
|
||||||
|
|
@ -175,6 +176,7 @@ class g(object):
|
||||||
'MMGEN_DEBUG_RPC',
|
'MMGEN_DEBUG_RPC',
|
||||||
'MMGEN_DEBUG_ADDRLIST',
|
'MMGEN_DEBUG_ADDRLIST',
|
||||||
'MMGEN_DEBUG_UTF8',
|
'MMGEN_DEBUG_UTF8',
|
||||||
|
'MMGEN_DEBUG_SUBSEED',
|
||||||
'MMGEN_QUIET',
|
'MMGEN_QUIET',
|
||||||
'MMGEN_FORCE_256_COLOR',
|
'MMGEN_FORCE_256_COLOR',
|
||||||
'MMGEN_MIN_URANDCHARS',
|
'MMGEN_MIN_URANDCHARS',
|
||||||
|
|
@ -201,6 +203,7 @@ class g(object):
|
||||||
|
|
||||||
seed_lens = 128,192,256
|
seed_lens = 128,192,256
|
||||||
scramble_hash_rounds = 10
|
scramble_hash_rounds = 10
|
||||||
|
subseeds = 100
|
||||||
|
|
||||||
mmenc_ext = 'mmenc'
|
mmenc_ext = 'mmenc'
|
||||||
salt_len = 16
|
salt_len = 16
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ main.py - Script launcher for the MMGen suite
|
||||||
|
|
||||||
def launch(mod):
|
def launch(mod):
|
||||||
|
|
||||||
if mod in ('walletgen','walletchk','walletconv','passchg'):
|
if mod in ('walletgen','walletchk','walletconv','passchg','subwalletgen'):
|
||||||
mod = 'wallet'
|
mod = 'wallet'
|
||||||
if mod == 'keygen': mod = 'addrgen'
|
if mod == 'keygen': mod = 'addrgen'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,12 +31,14 @@ nargs = 1
|
||||||
iaction = 'convert'
|
iaction = 'convert'
|
||||||
oaction = 'convert'
|
oaction = 'convert'
|
||||||
do_bw_note = True
|
do_bw_note = True
|
||||||
|
do_sw_note = False
|
||||||
|
|
||||||
invoked_as = {
|
invoked_as = {
|
||||||
'mmgen-walletgen': 'gen',
|
'mmgen-walletgen': 'gen',
|
||||||
'mmgen-walletconv': 'conv',
|
'mmgen-walletconv': 'conv',
|
||||||
'mmgen-walletchk': 'chk',
|
'mmgen-walletchk': 'chk',
|
||||||
'mmgen-passchg': 'passchg',
|
'mmgen-passchg': 'passchg',
|
||||||
|
'mmgen-subwalletgen': 'subgen',
|
||||||
}[g.prog_name]
|
}[g.prog_name]
|
||||||
|
|
||||||
# full: defhHiJkKlLmoOpPqrSvz-
|
# full: defhHiJkKlLmoOpPqrSvz-
|
||||||
|
|
@ -58,6 +60,11 @@ elif invoked_as == 'passchg':
|
||||||
opt_filter = 'efhdiHkKOlLmpPqrSvz-'
|
opt_filter = 'efhdiHkKOlLmpPqrSvz-'
|
||||||
iaction = 'input'
|
iaction = 'input'
|
||||||
do_bw_note = False
|
do_bw_note = False
|
||||||
|
elif invoked_as == 'subgen':
|
||||||
|
desc = 'Generate a subwallet from an {pnm} wallet'
|
||||||
|
opt_filter = 'dehHiJkKlLmoOpPqrSvz-' # omitted: f
|
||||||
|
usage = '[opts] [infile] <Subseed Index>'
|
||||||
|
do_sw_note = True
|
||||||
|
|
||||||
opts_data = {
|
opts_data = {
|
||||||
'text': {
|
'text': {
|
||||||
|
|
@ -96,7 +103,7 @@ opts_data = {
|
||||||
""",
|
""",
|
||||||
'notes': """
|
'notes': """
|
||||||
|
|
||||||
{n_pw}{n_bw}
|
{n_sw}{n_pw}{n_bw}
|
||||||
|
|
||||||
FMT CODES:
|
FMT CODES:
|
||||||
|
|
||||||
|
|
@ -111,6 +118,7 @@ FMT CODES:
|
||||||
),
|
),
|
||||||
'notes': lambda s: s.format(
|
'notes': lambda s: s.format(
|
||||||
f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
|
f='\n '.join(SeedSource.format_fmt_codes().splitlines()),
|
||||||
|
n_sw=('',help_notes('subwallet')+'\n\n')[do_sw_note],
|
||||||
n_pw=help_notes('passwd'),
|
n_pw=help_notes('passwd'),
|
||||||
n_bw=('','\n\n'+help_notes('brainwallet'))[do_bw_note]
|
n_bw=('','\n\n'+help_notes('brainwallet'))[do_bw_note]
|
||||||
)
|
)
|
||||||
|
|
@ -122,11 +130,15 @@ cmd_args = opts.init(opts_data,opt_filter=opt_filter)
|
||||||
if opt.label:
|
if opt.label:
|
||||||
opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")
|
opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")
|
||||||
|
|
||||||
|
if invoked_as == 'subgen':
|
||||||
|
from mmgen.obj import SubSeedIdx
|
||||||
|
ss_idx = SubSeedIdx(cmd_args.pop())
|
||||||
|
|
||||||
sf = get_seed_file(cmd_args,nargs,invoked_as=invoked_as)
|
sf = get_seed_file(cmd_args,nargs,invoked_as=invoked_as)
|
||||||
|
|
||||||
if not invoked_as == 'chk': do_license_msg()
|
if not invoked_as == 'chk': do_license_msg()
|
||||||
|
|
||||||
if invoked_as in ('conv','passchg'):
|
if invoked_as in ('conv','passchg','subgen'):
|
||||||
m1 = green('Processing input wallet')
|
m1 = green('Processing input wallet')
|
||||||
m2 = yellow(' (default wallet)') if sf and os.path.dirname(sf) == g.data_dir else ''
|
m2 = yellow(' (default wallet)') if sf and os.path.dirname(sf) == g.data_dir else ''
|
||||||
msg(m1+m2)
|
msg(m1+m2)
|
||||||
|
|
@ -139,10 +151,19 @@ if invoked_as == 'chk':
|
||||||
# TODO: display creation date
|
# TODO: display creation date
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if invoked_as in ('conv','passchg'):
|
if invoked_as in ('conv','passchg','subgen'):
|
||||||
gmsg('Processing output wallet')
|
gmsg('Processing output wallet')
|
||||||
|
|
||||||
ss_out = SeedSource(ss=ss_in,passchg=invoked_as=='passchg')
|
if invoked_as == 'subgen':
|
||||||
|
msg_r('{} {} of {}...'.format(
|
||||||
|
green('Generating subseed'),
|
||||||
|
ss_idx.hl(),
|
||||||
|
ss_in.seed.sid.hl(),
|
||||||
|
))
|
||||||
|
msg('\b\b\b => {}'.format(ss_in.seed.subseed(ss_idx).sid.hl()))
|
||||||
|
ss_out = SeedSource(seed=ss_in.seed.subseed(ss_idx).data)
|
||||||
|
else:
|
||||||
|
ss_out = SeedSource(ss=ss_in,passchg=invoked_as=='passchg')
|
||||||
|
|
||||||
if invoked_as == 'gen':
|
if invoked_as == 'gen':
|
||||||
qmsg("This wallet's Seed ID: {}".format(ss_out.seed.sid.hl()))
|
qmsg("This wallet's Seed ID: {}".format(ss_out.seed.sid.hl()))
|
||||||
|
|
|
||||||
32
mmgen/obj.py
32
mmgen/obj.py
|
|
@ -361,6 +361,10 @@ class MMGenRange(tuple,InitErrors,MMGenObject):
|
||||||
def items(self):
|
def items(self):
|
||||||
return list(self.iterate())
|
return list(self.iterate())
|
||||||
|
|
||||||
|
class SubSeedIdxRange(MMGenRange):
|
||||||
|
min_idx = 1
|
||||||
|
max_idx = 1000000
|
||||||
|
|
||||||
class UnknownCoinAmt(Decimal): pass
|
class UnknownCoinAmt(Decimal): pass
|
||||||
|
|
||||||
class BTCAmt(Decimal,Hilite,InitErrors):
|
class BTCAmt(Decimal,Hilite,InitErrors):
|
||||||
|
|
@ -534,8 +538,8 @@ class SeedID(str,Hilite,InitErrors):
|
||||||
cls.arg_chk(cls,on_fail)
|
cls.arg_chk(cls,on_fail)
|
||||||
try:
|
try:
|
||||||
if seed:
|
if seed:
|
||||||
from mmgen.seed import Seed
|
from mmgen.seed import Seed,SubSeed
|
||||||
assert type(seed) == Seed,'not a Seed instance'
|
assert type(seed) in (Seed,SubSeed),'not a Seed or SubSeed instance'
|
||||||
from mmgen.util import make_chksum_8
|
from mmgen.util import make_chksum_8
|
||||||
return str.__new__(cls,make_chksum_8(seed.data))
|
return str.__new__(cls,make_chksum_8(seed.data))
|
||||||
elif sid:
|
elif sid:
|
||||||
|
|
@ -547,6 +551,30 @@ class SeedID(str,Hilite,InitErrors):
|
||||||
m = "{!r}: value cannot be converted to SeedID ({})"
|
m = "{!r}: value cannot be converted to SeedID ({})"
|
||||||
return cls.init_fail(m.format(seed or sid,e.args[0]),on_fail)
|
return cls.init_fail(m.format(seed or sid,e.args[0]),on_fail)
|
||||||
|
|
||||||
|
class SubSeedIdx(str,Hilite,InitErrors):
|
||||||
|
color = 'red'
|
||||||
|
trunc_ok = False
|
||||||
|
def __new__(cls,s,on_fail='die'):
|
||||||
|
if type(s) == cls: return s
|
||||||
|
cls.arg_chk(cls,on_fail)
|
||||||
|
try:
|
||||||
|
assert issubclass(type(s),str),'not a string or string subclass'
|
||||||
|
idx = s[:-1] if s[-1] in 'SsLl' else s
|
||||||
|
from mmgen.util import is_int
|
||||||
|
assert is_int(idx),"valid format: an integer, plus optional letter 'S','s','L' or 'l'"
|
||||||
|
idx = int(idx)
|
||||||
|
assert idx >= SubSeedIdxRange.min_idx, 'subseed index < {:,}'.format(SubSeedIdxRange.min_idx)
|
||||||
|
assert idx <= SubSeedIdxRange.max_idx, 'subseed index > {:,}'.format(SubSeedIdxRange.max_idx)
|
||||||
|
|
||||||
|
sstype,ltr = ('short','S') if s[-1] in 'Ss' else ('long','L')
|
||||||
|
me = str.__new__(cls,str(idx)+ltr)
|
||||||
|
me.idx = idx
|
||||||
|
me.type = sstype
|
||||||
|
return me
|
||||||
|
except Exception as e:
|
||||||
|
m = "{!r}: value cannot be converted to {} ({})"
|
||||||
|
return cls.init_fail(m.format(s,cls.__name__,e.args[0]),on_fail)
|
||||||
|
|
||||||
class MMGenID(str,Hilite,InitErrors,MMGenObject):
|
class MMGenID(str,Hilite,InitErrors,MMGenObject):
|
||||||
color = 'orange'
|
color = 'orange'
|
||||||
width = 0
|
width = 0
|
||||||
|
|
|
||||||
110
mmgen/seed.py
110
mmgen/seed.py
|
|
@ -45,7 +45,7 @@ def is_mnemonic(s):
|
||||||
opt.quiet = oq_save
|
opt.quiet = oq_save
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
class Seed(MMGenObject):
|
class SeedBase(MMGenObject):
|
||||||
|
|
||||||
data = MMGenImmutableAttr('data',bytes,typeconv=False)
|
data = MMGenImmutableAttr('data',bytes,typeconv=False)
|
||||||
hexdata = MMGenImmutableAttr('hexdata',str,typeconv=False)
|
hexdata = MMGenImmutableAttr('hexdata',str,typeconv=False)
|
||||||
|
|
@ -64,6 +64,114 @@ class Seed(MMGenObject):
|
||||||
self.sid = SeedID(seed=self)
|
self.sid = SeedID(seed=self)
|
||||||
self.length = len(seed_bin) * 8
|
self.length = len(seed_bin) * 8
|
||||||
|
|
||||||
|
class Seed(SeedBase):
|
||||||
|
|
||||||
|
def __init__(self,seed_bin=None):
|
||||||
|
from collections import OrderedDict
|
||||||
|
self.subseeds = { 'long': OrderedDict(), 'short': OrderedDict() }
|
||||||
|
SeedBase.__init__(self,seed_bin=seed_bin)
|
||||||
|
|
||||||
|
def subseed(self,ss_idx_in):
|
||||||
|
ss_idx = SubSeedIdx(ss_idx_in)
|
||||||
|
if ss_idx.idx > len(self.subseeds['long']):
|
||||||
|
self.gen_subseeds(ss_idx.idx)
|
||||||
|
sid = list(self.subseeds[ss_idx.type].keys())[ss_idx.idx-1]
|
||||||
|
idx,nonce = self.subseeds[ss_idx.type][sid]
|
||||||
|
assert idx == ss_idx.idx, "{} != {}: subseed list idx does not match subseed idx!".format(idx,ss_idx.idx)
|
||||||
|
return SubSeed(self,idx,nonce,length=ss_idx.type)
|
||||||
|
|
||||||
|
def existing_subseed_by_seed_id(self,sid):
|
||||||
|
for k in ('long','short'):
|
||||||
|
if sid in self.subseeds[k]:
|
||||||
|
idx,nonce = self.subseeds[k][sid]
|
||||||
|
return SubSeed(self,idx,nonce,length=k)
|
||||||
|
|
||||||
|
def subseed_by_seed_id(self,sid,last_idx=g.subseeds):
|
||||||
|
|
||||||
|
seed = self.existing_subseed_by_seed_id(sid)
|
||||||
|
if seed: return seed
|
||||||
|
|
||||||
|
if len(self.subseeds['long']) >= last_idx:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.gen_subseeds(last_idx,last_sid=sid)
|
||||||
|
|
||||||
|
return self.existing_subseed_by_seed_id(sid)
|
||||||
|
|
||||||
|
def gen_subseeds(self,last_idx=g.subseeds,last_sid=None):
|
||||||
|
|
||||||
|
first_idx = len(self.subseeds['long']) + 1
|
||||||
|
|
||||||
|
if first_idx > last_idx:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if last_sid != None:
|
||||||
|
last_sid = SeedID(sid=last_sid)
|
||||||
|
|
||||||
|
def add_subseed(idx,length):
|
||||||
|
for nonce in range(SubSeed.max_nonce): # use nonce to handle Seed ID collisions
|
||||||
|
sid = make_chksum_8(SubSeedBase.make_subseed_bin(self,idx,nonce,length))
|
||||||
|
if not (sid in self.subseeds['long'] or sid in self.subseeds['short']):
|
||||||
|
self.subseeds[length][sid] = (idx,nonce)
|
||||||
|
return last_sid == sid
|
||||||
|
elif g.debug_subseed: # should get ≈450 collisions for first 1,000,000 subseeds
|
||||||
|
k = ('long','short')[sid in self.subseeds['short']]
|
||||||
|
m1 = 'add_subseed(idx={},{}):'.format(idx,length)
|
||||||
|
m2 = 'collision with ID {} (idx={},{}),'.format(sid,self.subseeds[k][sid][0],k)
|
||||||
|
msg('{:30} {:46} incrementing nonce to {}'.format(m1,m2,nonce+1))
|
||||||
|
else: # must exit here, as this could leave self.subseeds in inconsistent state
|
||||||
|
raise SubSeedNonceRangeExceeded('add_subseed(): nonce range exceeded')
|
||||||
|
|
||||||
|
for idx in SubSeedIdxRange(first_idx,last_idx).iterate():
|
||||||
|
if add_subseed(idx,'long') + add_subseed(idx,'short'):
|
||||||
|
break
|
||||||
|
|
||||||
|
def fmt_subseeds(self,first_idx=1,last_idx=g.subseeds):
|
||||||
|
|
||||||
|
r = SubSeedIdxRange(first_idx,last_idx)
|
||||||
|
|
||||||
|
if len(self.subseeds['long']) < last_idx:
|
||||||
|
self.gen_subseeds(last_idx)
|
||||||
|
|
||||||
|
fs1 = '{:>18} {:>18}\n'
|
||||||
|
fs2 = '{i:>7}L: {:8} {i:>7}S: {:8}\n'
|
||||||
|
|
||||||
|
hdr = '{:>16} {} ({} bits)\n\n'.format('Parent Seed:',self.sid.hl(),self.length)
|
||||||
|
hdr += fs1.format('Long Subseeds','Short Subseeds')
|
||||||
|
hdr += fs1.format('-------------','--------------')
|
||||||
|
|
||||||
|
sl = tuple(self.subseeds['long'])
|
||||||
|
ss = tuple(self.subseeds['short'])
|
||||||
|
body = (fs2.format(sl[n-1],ss[n-1],i=n) for n in r.iterate())
|
||||||
|
|
||||||
|
return hdr + ''.join(body)
|
||||||
|
|
||||||
|
class SubSeedBase(MMGenObject):
|
||||||
|
|
||||||
|
max_nonce = 1000
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def make_subseed_bin(parent,idx:int,nonce:int,length:str):
|
||||||
|
short = { 'short': True, 'long': False }[length]
|
||||||
|
# field maximums: idx: 4294967295, nonce: 65535, short (bool): 255
|
||||||
|
scramble_key = idx.to_bytes(4,'big',signed=False) + \
|
||||||
|
nonce.to_bytes(2,'big',signed=False) + \
|
||||||
|
short.to_bytes(1,'big',signed=False)
|
||||||
|
byte_len = 16 if short else parent.length // 8
|
||||||
|
return scramble_seed(parent.data,scramble_key,g.scramble_hash_rounds)[:byte_len]
|
||||||
|
|
||||||
|
class SubSeed(SeedBase,SubSeedBase):
|
||||||
|
|
||||||
|
idx = MMGenImmutableAttr('idx',int,typeconv=False)
|
||||||
|
nonce = MMGenImmutableAttr('nonce',int,typeconv=False)
|
||||||
|
ss_idx = MMGenImmutableAttr('ss_idx',str,typeconv=False)
|
||||||
|
|
||||||
|
def __init__(self,parent,idx,nonce,length):
|
||||||
|
self.idx = idx
|
||||||
|
self.nonce = nonce
|
||||||
|
self.ss_idx = str(idx) + { 'long': 'L', 'short': 'S' }[length]
|
||||||
|
SeedBase.__init__(self,seed_bin=SubSeedBase.make_subseed_bin(parent,idx,nonce,length))
|
||||||
|
|
||||||
class SeedSource(MMGenObject):
|
class SeedSource(MMGenObject):
|
||||||
|
|
||||||
desc = g.proj_name + ' seed source'
|
desc = g.proj_name + ' seed source'
|
||||||
|
|
|
||||||
|
|
@ -671,7 +671,29 @@ class MMGenToolCmdFileUtil(MMGenToolCmdBase):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
class MMGenToolCmdWallet(MMGenToolCmdBase):
|
class MMGenToolCmdWallet(MMGenToolCmdBase):
|
||||||
"key or address generation from an MMGen wallet"
|
"key, address or subseed generation from an MMGen wallet"
|
||||||
|
|
||||||
|
def get_subseed(self,subseed_idx:str,wallet=''):
|
||||||
|
"get the Seed ID of a single subseed by Subseed Index for default or specified wallet"
|
||||||
|
opt.quiet = True
|
||||||
|
sf = get_seed_file([wallet] if wallet else [],1)
|
||||||
|
from mmgen.seed import SeedSource
|
||||||
|
return SeedSource(sf).seed.subseed(subseed_idx).sid
|
||||||
|
|
||||||
|
def get_subseed_by_seed_id(self,seed_id:str,wallet='',last_idx=g.subseeds):
|
||||||
|
"get the Subseed Index of a single subseed by Seed ID for default or specified wallet"
|
||||||
|
opt.quiet = True
|
||||||
|
sf = get_seed_file([wallet] if wallet else [],1)
|
||||||
|
from mmgen.seed import SeedSource
|
||||||
|
ret = SeedSource(sf).seed.subseed_by_seed_id(seed_id,last_idx)
|
||||||
|
return ret.ss_idx if ret else None
|
||||||
|
|
||||||
|
def list_subseeds(self,subseed_idx_range:str,wallet=''):
|
||||||
|
"list a range of subseed Seed IDs for default or specified wallet"
|
||||||
|
opt.quiet = True
|
||||||
|
sf = get_seed_file([wallet] if wallet else [],1)
|
||||||
|
from mmgen.seed import SeedSource
|
||||||
|
return SeedSource(sf).seed.fmt_subseeds(*SubSeedIdxRange(subseed_idx_range))
|
||||||
|
|
||||||
def gen_key(self,mmgen_addr:str,wallet=''):
|
def gen_key(self,mmgen_addr:str,wallet=''):
|
||||||
"generate a single MMGen WIF key from default or specified wallet"
|
"generate a single MMGen WIF key from default or specified wallet"
|
||||||
|
|
|
||||||
1
setup.py
1
setup.py
|
|
@ -160,6 +160,7 @@ setup(
|
||||||
'cmds/mmgen-addrimport',
|
'cmds/mmgen-addrimport',
|
||||||
'cmds/mmgen-passchg',
|
'cmds/mmgen-passchg',
|
||||||
'cmds/mmgen-regtest',
|
'cmds/mmgen-regtest',
|
||||||
|
'cmds/mmgen-subwalletgen',
|
||||||
'cmds/mmgen-walletchk',
|
'cmds/mmgen-walletchk',
|
||||||
'cmds/mmgen-walletconv',
|
'cmds/mmgen-walletconv',
|
||||||
'cmds/mmgen-walletgen',
|
'cmds/mmgen-walletgen',
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,15 @@ tests = OrderedDict([
|
||||||
('101,1,3,5,2-7,99',[1,2,3,4,5,6,7,99,101]),
|
('101,1,3,5,2-7,99',[1,2,3,4,5,6,7,99,101]),
|
||||||
({'idx_list':AddrIdxList('1-5')},[1,2,3,4,5])
|
({'idx_list':AddrIdxList('1-5')},[1,2,3,4,5])
|
||||||
)}),
|
)}),
|
||||||
|
('SubSeedIdxRange', {
|
||||||
|
'bad': (33,'x','-11','66,3','0','3-2','8000000','100000000',(1,2,3)),
|
||||||
|
'good': (
|
||||||
|
('3',(3,3)),
|
||||||
|
((3,5),(3,5)),
|
||||||
|
('1-2',(1,2)),
|
||||||
|
(str(g.subseeds),(g.subseeds,g.subseeds)),
|
||||||
|
(str(SubSeedIdxRange.max_idx),(SubSeedIdxRange.max_idx,SubSeedIdxRange.max_idx)),
|
||||||
|
)}),
|
||||||
('BTCAmt', {
|
('BTCAmt', {
|
||||||
'bad': ('-3.2','0.123456789',123,'123L','22000000',20999999.12345678),
|
'bad': ('-3.2','0.123456789',123,'123L','22000000',20999999.12345678),
|
||||||
'good': (('20999999.12345678',Decimal('20999999.12345678')),)
|
'good': (('20999999.12345678',Decimal('20999999.12345678')),)
|
||||||
|
|
@ -147,6 +156,10 @@ tests = OrderedDict([
|
||||||
'я',r32,'abc'),
|
'я',r32,'abc'),
|
||||||
'good': (({'sid':'F00BAA12'},'F00BAA12'),(Seed(r16),Seed(r16).sid))
|
'good': (({'sid':'F00BAA12'},'F00BAA12'),(Seed(r16),Seed(r16).sid))
|
||||||
}),
|
}),
|
||||||
|
('SubSeedIdx', {
|
||||||
|
'bad': (33,'x','я','1x',200,'1ss','L','s','200LS','30ll','s100',str(SubSeedIdxRange.max_idx+1),'0'),
|
||||||
|
'good': (('1','1L'),('1s','1S'),'20S','30L',('300l','300L'),('200','200L'),str(SubSeedIdxRange.max_idx)+'S')
|
||||||
|
}),
|
||||||
('MMGenID', {
|
('MMGenID', {
|
||||||
'bad': ('x',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99'),
|
'bad': ('x',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99'),
|
||||||
'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:99')
|
'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:99')
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
|
||||||
segwit_opts_ok = True
|
segwit_opts_ok = True
|
||||||
cmd_group = (
|
cmd_group = (
|
||||||
('walletgen_dfl_wallet', (15,'wallet generation (default wallet)',[[[],15]])),
|
('walletgen_dfl_wallet', (15,'wallet generation (default wallet)',[[[],15]])),
|
||||||
|
('subwalletgen_dfl_wallet', (15,'subwallet generation (default wallet)',[[[pwfile],15]])),
|
||||||
('export_seed_dfl_wallet',(15,'seed export to mmseed format (default wallet)',[[[pwfile],15]])),
|
('export_seed_dfl_wallet',(15,'seed export to mmseed format (default wallet)',[[[pwfile],15]])),
|
||||||
('addrgen_dfl_wallet',(15,'address generation (default wallet)',[[[pwfile],15]])),
|
('addrgen_dfl_wallet',(15,'address generation (default wallet)',[[[pwfile],15]])),
|
||||||
('txcreate_dfl_wallet',(15,'transaction creation (default wallet)',[[['addrs'],15]])),
|
('txcreate_dfl_wallet',(15,'transaction creation (default wallet)',[[['addrs'],15]])),
|
||||||
|
|
@ -75,6 +76,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
|
||||||
('delete_dfl_wallet',(15,'delete default wallet',[[[pwfile],15]])),
|
('delete_dfl_wallet',(15,'delete default wallet',[[[pwfile],15]])),
|
||||||
|
|
||||||
('walletgen', (1,'wallet generation', [[['del_dw_run'],15]])),
|
('walletgen', (1,'wallet generation', [[['del_dw_run'],15]])),
|
||||||
|
('subwalletgen', (1,'subwallet generation', [[['mmdat'],1]])),
|
||||||
|
('subwalletgen_mnemonic',(1,'subwallet generation (to mnemonic format)',[[['mmdat'],1]])),
|
||||||
# ('walletchk', (1,'wallet check', [[['mmdat'],1]])),
|
# ('walletchk', (1,'wallet check', [[['mmdat'],1]])),
|
||||||
('passchg', (5,'password, label and hash preset change',[[['mmdat',pwfile],1]])),
|
('passchg', (5,'password, label and hash preset change',[[['mmdat',pwfile],1]])),
|
||||||
('passchg_keeplabel',(5,'password, label and hash preset change (keep label)',[[['mmdat',pwfile],1]])),
|
('passchg_keeplabel',(5,'password, label and hash preset change (keep label)',[[['mmdat',pwfile],1]])),
|
||||||
|
|
@ -157,6 +160,9 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
|
||||||
def walletgen_dfl_wallet(self,seed_len=None):
|
def walletgen_dfl_wallet(self,seed_len=None):
|
||||||
return self.walletgen(seed_len=seed_len,gen_dfl_wallet=True)
|
return self.walletgen(seed_len=seed_len,gen_dfl_wallet=True)
|
||||||
|
|
||||||
|
def subwalletgen_dfl_wallet(self,pf):
|
||||||
|
return self.subwalletgen(wf='default')
|
||||||
|
|
||||||
def export_seed_dfl_wallet(self,pf,desc='seed data',out_fmt='seed'):
|
def export_seed_dfl_wallet(self,pf,desc='seed data',out_fmt='seed'):
|
||||||
return self.export_seed(wf=None,desc=desc,out_fmt=out_fmt,pf=pf)
|
return self.export_seed(wf=None,desc=desc,out_fmt=out_fmt,pf=pf)
|
||||||
|
|
||||||
|
|
@ -201,6 +207,29 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
|
||||||
t.written_to_file('MMGen wallet')
|
t.written_to_file('MMGen wallet')
|
||||||
return t
|
return t
|
||||||
|
|
||||||
|
def subwalletgen(self,wf):
|
||||||
|
args = [self.usr_rand_arg,'-p1','-d',self.tr.trash_dir,'-L','Label']
|
||||||
|
if wf != 'default': args += [wf]
|
||||||
|
t = self.spawn('mmgen-subwalletgen', args + ['10s'])
|
||||||
|
t.license()
|
||||||
|
t.passphrase('MMGen wallet',self.cfgs['1']['wpasswd'])
|
||||||
|
t.expect('Generating subseed 10S')
|
||||||
|
t.passphrase_new('new MMGen wallet','foo')
|
||||||
|
t.usr_rand(self.usr_rand_chars)
|
||||||
|
fn = t.written_to_file('MMGen wallet')
|
||||||
|
assert fn[-6:] == '.mmdat','incorrect file extension: {}'.format(fn[-6:])
|
||||||
|
return t
|
||||||
|
|
||||||
|
def subwalletgen_mnemonic(self,wf):
|
||||||
|
args = [self.usr_rand_arg,'-p1','-d',self.tr.trash_dir,'-o','words',wf,'3L']
|
||||||
|
t = self.spawn('mmgen-subwalletgen', args)
|
||||||
|
t.license()
|
||||||
|
t.passphrase('MMGen wallet',self.cfgs['1']['wpasswd'])
|
||||||
|
t.expect('Generating subseed 3L')
|
||||||
|
fn = t.written_to_file('Mnemonic data')
|
||||||
|
assert fn[-8:] == '.mmwords','incorrect file extension: {}'.format(fn[-8:])
|
||||||
|
return t
|
||||||
|
|
||||||
def passchg(self,wf,pf,label_action='cmdline'):
|
def passchg(self,wf,pf,label_action='cmdline'):
|
||||||
silence()
|
silence()
|
||||||
self.write_to_tmpfile(pwfile,get_data_from_file(pf))
|
self.write_to_tmpfile(pwfile,get_data_from_file(pf))
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,10 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
chk_data = {
|
chk_data = {
|
||||||
|
'ref_subwallet_sid': {
|
||||||
|
'98831F3A:32L':'D66B4885',
|
||||||
|
'98831F3A:1S':'20D95B09',
|
||||||
|
},
|
||||||
'ref_addrfile_chksum': {
|
'ref_addrfile_chksum': {
|
||||||
'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
|
'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
|
||||||
'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
|
'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
|
||||||
|
|
@ -76,6 +80,8 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
|
||||||
'ref_passwdfile_chksum': 'A983 DAB9 5514 27FB',
|
'ref_passwdfile_chksum': 'A983 DAB9 5514 27FB',
|
||||||
}
|
}
|
||||||
cmd_group = ( # TODO: move to tooltest2
|
cmd_group = ( # TODO: move to tooltest2
|
||||||
|
('ref_words_to_subwallet_chk1','subwallet generation from reference words file (long subseed)'),
|
||||||
|
('ref_words_to_subwallet_chk2','subwallet generation from reference words file (short subseed)'),
|
||||||
('ref_addrfile_chk', 'saved reference address file'),
|
('ref_addrfile_chk', 'saved reference address file'),
|
||||||
('ref_segwitaddrfile_chk','saved reference address file (segwit)'),
|
('ref_segwitaddrfile_chk','saved reference address file (segwit)'),
|
||||||
('ref_bech32addrfile_chk','saved reference address file (bech32)'),
|
('ref_bech32addrfile_chk','saved reference address file (bech32)'),
|
||||||
|
|
@ -102,6 +108,30 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
|
||||||
def ref_subdir(self):
|
def ref_subdir(self):
|
||||||
return self._get_ref_subdir_by_coin(g.coin)
|
return self._get_ref_subdir_by_coin(g.coin)
|
||||||
|
|
||||||
|
def ref_words_to_subwallet_chk1(self):
|
||||||
|
return self.ref_words_to_subwallet_chk('32L')
|
||||||
|
|
||||||
|
def ref_words_to_subwallet_chk2(self):
|
||||||
|
return self.ref_words_to_subwallet_chk('1S')
|
||||||
|
|
||||||
|
def ref_words_to_subwallet_chk(self,ss_idx):
|
||||||
|
wf = dfl_words_file
|
||||||
|
args = ['-d',self.tr.trash_dir,'-o','words',wf,ss_idx]
|
||||||
|
|
||||||
|
t = self.spawn('mmgen-subwalletgen',args,extra_desc='(generate subwallet)')
|
||||||
|
t.expect('Generating subseed {}'.format(ss_idx))
|
||||||
|
chk_sid = self.chk_data['ref_subwallet_sid']['98831F3A:{}'.format(ss_idx)]
|
||||||
|
fn = t.written_to_file('Mnemonic data')
|
||||||
|
assert chk_sid in fn,'incorrect filename: {} (does not contain {})'.format(fn,chk_sid)
|
||||||
|
ok()
|
||||||
|
|
||||||
|
t = self.spawn('mmgen-walletchk',[fn],extra_desc='(check subwallet)')
|
||||||
|
t.expect(r'Valid mnemonic data for Seed ID ([0-9A-F]*)\b',regex=True)
|
||||||
|
sid = t.p.match.group(1)
|
||||||
|
assert sid == chk_sid,'subseed ID {} does not match expected value {}'.format(sid,chk_sid)
|
||||||
|
t.read()
|
||||||
|
return t
|
||||||
|
|
||||||
def ref_addrfile_chk(self,ftype='addr',coin=None,subdir=None,pfx=None,mmtype=None,add_args=[]):
|
def ref_addrfile_chk(self,ftype='addr',coin=None,subdir=None,pfx=None,mmtype=None,add_args=[]):
|
||||||
af_key = 'ref_{}file'.format(ftype)
|
af_key = 'ref_{}file'.format(ftype)
|
||||||
af_fn = TestSuiteRef.sources[af_key].format(pfx or self.altcoin_pfx,'' if coin else self.tn_ext)
|
af_fn = TestSuiteRef.sources[af_key].format(pfx or self.altcoin_pfx,'' if coin else self.tn_ext)
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,20 @@ tests = {
|
||||||
'3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn'
|
'3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn'
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
'get_subseed': [
|
||||||
|
( ['3s','wallet=test/ref/98831F3A.mmwords'], '4018EB17' ),
|
||||||
|
( ['200','wallet=test/ref/98831F3A.mmwords'], '2B05AE73' ),
|
||||||
|
],
|
||||||
|
'get_subseed_by_seed_id': [
|
||||||
|
( ['4018EB17','wallet=test/ref/98831F3A.mmwords'], '3S' ),
|
||||||
|
( ['2B05AE73','wallet=test/ref/98831F3A.mmwords'], None ),
|
||||||
|
( ['2B05AE73','wallet=test/ref/98831F3A.mmwords','last_idx=200'], '200L' ),
|
||||||
|
],
|
||||||
|
'list_subseeds': [
|
||||||
|
( ['1-5','wallet=test/ref/98831F3A.mmwords'],
|
||||||
|
(md5_hash_strip,'996c047e8543d5dde6f82efc3214a6a1')
|
||||||
|
),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
'Coin': {
|
'Coin': {
|
||||||
'addr2pubhash': {
|
'addr2pubhash': {
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,188 @@ class UnitTests(object):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def subseed(self,name):
|
||||||
|
from mmgen.seed import Seed
|
||||||
|
from mmgen.obj import SubSeedIdxRange
|
||||||
|
|
||||||
|
def basic_ops():
|
||||||
|
msg_r('Testing basic ops...')
|
||||||
|
for a,b,c,d,e,f,h in (
|
||||||
|
(8,'4710FBF0','0C1B0615','803B165C','2669AC64',256,'10L'),
|
||||||
|
(6,'9D07ABBD','EBA9C33F','20787E6A','192E2AA2',192,'10L'),
|
||||||
|
(4,'43670520','04A4CCB3','B5F21D7B','C1934CFF',128,'10L'),
|
||||||
|
):
|
||||||
|
|
||||||
|
seed_bin = bytes.fromhex('deadbeef' * a)
|
||||||
|
seed = Seed(seed_bin)
|
||||||
|
assert seed.sid == b, seed.sid
|
||||||
|
|
||||||
|
subseed = seed.subseed('2s')
|
||||||
|
assert subseed.sid == c, subseed.sid
|
||||||
|
|
||||||
|
subseed = seed.subseed('3')
|
||||||
|
assert subseed.sid == d, subseed.sid
|
||||||
|
|
||||||
|
subseed = seed.subseed_by_seed_id(e)
|
||||||
|
assert subseed.length == f, subseed.length
|
||||||
|
assert subseed.sid == e, subseed.sid
|
||||||
|
assert subseed.idx == 10, subseed.idx
|
||||||
|
assert subseed.ss_idx == h, subseed.ss_idx
|
||||||
|
|
||||||
|
seed2 = Seed(seed_bin)
|
||||||
|
s2s = seed2.subseeds['short']
|
||||||
|
s2l = seed2.subseeds['long']
|
||||||
|
|
||||||
|
seed2.gen_subseeds(1)
|
||||||
|
assert len(s2s) == 1, len(s2s)
|
||||||
|
|
||||||
|
seed2.gen_subseeds(1) # do nothing
|
||||||
|
seed2.gen_subseeds(2) # append one item
|
||||||
|
|
||||||
|
seed2.gen_subseeds(5)
|
||||||
|
assert len(s2s) == 5, len(s2s)
|
||||||
|
|
||||||
|
seed2.gen_subseeds(3) # do nothing
|
||||||
|
assert len(s2l) == 5, len(s2l)
|
||||||
|
|
||||||
|
seed2.gen_subseeds(10)
|
||||||
|
assert len(s2s) == 10, len(s2s)
|
||||||
|
|
||||||
|
assert seed.pformat() == seed2.pformat()
|
||||||
|
|
||||||
|
s = seed.fmt_subseeds()
|
||||||
|
s_lines = s.strip().split('\n')
|
||||||
|
assert len(s_lines) == g.subseeds + 4, s
|
||||||
|
|
||||||
|
a = seed.subseed('2L').sid
|
||||||
|
b = [e for e in s_lines if ' 2L:' in e][0].strip().split()[1]
|
||||||
|
assert a == b, b
|
||||||
|
|
||||||
|
c = seed.subseed('2').sid
|
||||||
|
assert c == a, c
|
||||||
|
|
||||||
|
a = seed.subseed('5S').sid
|
||||||
|
b = [e for e in s_lines if ' 5S:' in e][0].strip().split()[3]
|
||||||
|
assert a == b, b
|
||||||
|
|
||||||
|
s = seed.fmt_subseeds(g.subseeds+1,g.subseeds+2)
|
||||||
|
s_lines = s.strip().split('\n')
|
||||||
|
assert len(s_lines) == 6, s
|
||||||
|
|
||||||
|
ss_idx = str(g.subseeds+2) + 'S'
|
||||||
|
a = seed.subseed(ss_idx).sid
|
||||||
|
b = [e for e in s_lines if ' {}:'.format(ss_idx) in e][0].strip().split()[3]
|
||||||
|
assert a == b, b
|
||||||
|
|
||||||
|
s = seed.fmt_subseeds(1,2)
|
||||||
|
s_lines = s.strip().split('\n')
|
||||||
|
assert len(s_lines) == 6, s
|
||||||
|
|
||||||
|
msg('OK')
|
||||||
|
|
||||||
|
def defaults_and_limits():
|
||||||
|
msg_r('Testing defaults and limits...')
|
||||||
|
|
||||||
|
seed_bin = bytes.fromhex('deadbeef' * 8)
|
||||||
|
seed = Seed(seed_bin)
|
||||||
|
seed.gen_subseeds()
|
||||||
|
ss = seed.subseeds
|
||||||
|
assert len(ss['short']) == g.subseeds, ss['short']
|
||||||
|
assert len(ss['long']) == g.subseeds, ss['long']
|
||||||
|
|
||||||
|
seed = Seed(seed_bin)
|
||||||
|
seed.subseed_by_seed_id('EEEEEEEE')
|
||||||
|
ss = seed.subseeds
|
||||||
|
assert len(ss['short']) == g.subseeds, ss['short']
|
||||||
|
assert len(ss['long']) == g.subseeds, ss['long']
|
||||||
|
|
||||||
|
seed = Seed(seed_bin)
|
||||||
|
subseed = seed.subseed_by_seed_id('803B165C')
|
||||||
|
assert subseed.sid == '803B165C', subseed.sid
|
||||||
|
assert subseed.idx == 3, subseed.idx
|
||||||
|
|
||||||
|
seed = Seed(seed_bin)
|
||||||
|
subseed = seed.subseed_by_seed_id('803B165C',last_idx=1)
|
||||||
|
assert subseed == None, subseed
|
||||||
|
|
||||||
|
r = SubSeedIdxRange('1-5')
|
||||||
|
r2 = SubSeedIdxRange(1,5)
|
||||||
|
assert r2 == r, r2
|
||||||
|
assert r == (r.first,r.last), r
|
||||||
|
assert r.first == 1, r.first
|
||||||
|
assert r.last == 5, r.last
|
||||||
|
assert r.items == [1,2,3,4,5], r.items
|
||||||
|
assert list(r.iterate()) == r.items, list(r.iterate())
|
||||||
|
|
||||||
|
r = SubSeedIdxRange('22')
|
||||||
|
r2 = SubSeedIdxRange(22,22)
|
||||||
|
assert r2 == r, r2
|
||||||
|
assert r == (r.first,r.last), r
|
||||||
|
assert r.first == 22, r.first
|
||||||
|
assert r.last == 22, r.last
|
||||||
|
assert r.items == [22], r
|
||||||
|
assert list(r.iterate()) == r.items, list(r.iterate())
|
||||||
|
|
||||||
|
r = SubSeedIdxRange('3-3')
|
||||||
|
assert r.items == [3], r.items
|
||||||
|
|
||||||
|
r = SubSeedIdxRange('{}-{}'.format(g.subseeds-1,g.subseeds))
|
||||||
|
assert r.items == [g.subseeds-1,g.subseeds], r.items
|
||||||
|
|
||||||
|
for n,e in enumerate(SubSeedIdxRange('1-5').iterate(),1):
|
||||||
|
assert n == e, e
|
||||||
|
|
||||||
|
assert n == 5, n
|
||||||
|
|
||||||
|
msg('OK')
|
||||||
|
|
||||||
|
def collisions():
|
||||||
|
msg_r('Testing Seed ID collisions ({} subseed pairs)...'.format(59344))
|
||||||
|
seed_bin = bytes.fromhex('deadbeef' * 8)
|
||||||
|
seed = Seed(seed_bin)
|
||||||
|
|
||||||
|
subseed = seed.subseed('29429s')
|
||||||
|
assert subseed.sid == 'AE4C5E39', subseed.sid
|
||||||
|
assert subseed.nonce == 1, subseed.nonce
|
||||||
|
|
||||||
|
subseed = seed.subseed('59344')
|
||||||
|
assert subseed.sid == 'FC4AD16F', subseed.sid
|
||||||
|
assert subseed.nonce == 1, subseed.nonce
|
||||||
|
|
||||||
|
subseed2 = seed.subseed_by_seed_id('FC4AD16F')
|
||||||
|
assert subseed.pformat() == subseed2.pformat()
|
||||||
|
|
||||||
|
msg('OK')
|
||||||
|
|
||||||
|
def count_collisions():
|
||||||
|
msg_r('Counting Seed ID collisions ({} subseed pairs)...'.format(SubSeedIdxRange.max_idx))
|
||||||
|
|
||||||
|
seed_bin = bytes.fromhex('12abcdef' * 8)
|
||||||
|
seed = Seed(seed_bin)
|
||||||
|
|
||||||
|
seed.gen_subseeds(SubSeedIdxRange.max_idx)
|
||||||
|
ss = seed.subseeds
|
||||||
|
|
||||||
|
for sid in ss['long']:
|
||||||
|
# msg(sid)
|
||||||
|
assert sid not in ss['short']
|
||||||
|
|
||||||
|
collisions = 0
|
||||||
|
for k in ('short','long'):
|
||||||
|
for sid in ss[k]:
|
||||||
|
collisions += ss[k][sid][1]
|
||||||
|
|
||||||
|
assert collisions == 470, collisions
|
||||||
|
msg_r('({} collisions) '.format(collisions))
|
||||||
|
msg('OK')
|
||||||
|
|
||||||
|
basic_ops()
|
||||||
|
defaults_and_limits()
|
||||||
|
collisions()
|
||||||
|
count_collisions()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
def exit_msg():
|
def exit_msg():
|
||||||
t = int(time.time()) - start_time
|
t = int(time.time()) - start_time
|
||||||
gmsg('All requested tests finished OK, elapsed time: {:02}:{:02}'.format(t//60,t%60))
|
gmsg('All requested tests finished OK, elapsed time: {:02}:{:02}'.format(t//60,t%60))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue