blockchainkit.wallets#
Wallets: keys as users actually hold them.
Error-detecting codes behind addresses, from BCH codes and Verhoeff’s check digit to Base58Check, Bech32 and Bech32m; key stretching with PBKDF2 and scrypt, and the brain wallets that had none; hierarchical deterministic keys, mnemonic seed phrases and derivation paths; and shared control of keys, from pay-to-script-hash multisig and threshold ECDSA to social-recovery wallets and ERC-4337 account abstraction.
Keys as users actually hold them: check digits and the error-detecting codes behind Base58Check, Bech32 and Bech32m addresses; PBKDF2 and scrypt, and the brain wallets that used neither; BIP32 key trees, BIP39 seed phrases and BIP44 paths; and shared control, from P2SH multisig and threshold ECDSA to social recovery and ERC-4337 accounts.
Every public name below is re-exported by the subpackage: import it as
bk.wallets.<name>. The utilities and the plotting helpers are the
exceptions: import them from blockchainkit.wallets.utils and
blockchainkit.wallets.visualizers, which loads Matplotlib.
Results#
Result containers for blockchainkit.wallets.
- class blockchainkit.wallets.core.base.BCHDecoding(message, codeword, errors, corrected)[source]#
Bases:
objectWhat a BCH decoder made of a received word.
- Variables:
message (
tupleofint) – The k message bits read from the corrected word.codeword (
tupleofint) – The n-bit codeword after correction (the received word if nothing could be corrected).errors (
tupleofint) – Positions flipped back, counted from the left.corrected (
bool) – False if the decoder found more errors than it can correct; the word is then returned unchanged. True does not prove the word is right: beyond t errors the decoder can land on a wrong codeword.
- Parameters:
- class blockchainkit.wallets.core.base.DetectionRates(scheme, single_errors, transpositions)[source]#
Bases:
objectThe share of typing errors a check-digit scheme catches.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.CrackResult(secret, guesses, hash_calls)[source]#
Bases:
objectAn exhaustive search for a short secret behind a stretched key.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.RomixCost(n, r, stored, block_mixes)[source]#
Bases:
objectWhat one run of scrypt’s ROMix cost, with some of its table forgotten.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.ExtendedKey(chain_code, public, private, depth, index, parent_fingerprint)[source]#
Bases:
objectA BIP32 extended key: a key and its chain code, and where it sits in the tree.
- Variables:
chain_code (
bytes) – 32 bytes of extra entropy that, with the key, derive the children.private (
intorNone) – The private key, or None for an extended public key.depth (
int) – 0 for the master key, 1 for its children, and so on.index (
int) – This key’s child number; at least2**31if hardened.parent_fingerprint (
bytes) – The first 4 bytes of the parent’sHASH160; zeros for the master.
- Parameters:
- class blockchainkit.wallets.core.base.AccountScan(accounts, found, derived)[source]#
Bases:
objectThe outcome of BIP44 account discovery.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.BrainDrain(wallets, weak, drained, delays, hauls)[source]#
Bases:
objectA simulated population of brain wallets, and the thieves sweeping them.
- Variables:
wallets (
int) – Wallets funded.weak (
int) – Wallets whose passphrase came from the common phrasebook.drained (
int) – Wallets emptied by a thief.delays (
tupleofint) – For each drained wallet, the blocks between deposit and theft.hauls (
tupleofint) – Wallets taken by each thief, in the order the thieves were given.
- Parameters:
- class blockchainkit.wallets.core.base.Bech32Decoding(hrp, data, encoding)[source]#
Bases:
objectA decoded Bech32 or Bech32m string.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.WitnessProgram(version, program)[source]#
Bases:
objectA segregated-witness output: a version and a program.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.EcdsaSignature(r, s)[source]#
Bases:
objectAn ECDSA signature: the x coordinate of the nonce point, and the response.
- Variables:
s (r,) – Both in [1, n);
sis normalized to the lower half, as Bitcoin requires.- Parameters:
Bases:
objectA secp256k1 key generated jointly, held as Shamir shares.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.ThresholdSigning(signature, signers, conversions)[source]#
Bases:
objectAn ECDSA signature produced jointly by a threshold of key holders.
- Variables:
signature (
blockchainkit.wallets.core.base.EcdsaSignature) – An ordinary ECDSA signature under the joint public key.conversions (
int) – Multiplicative-to-additive conversions run, each a two-party protocol: the price of ECDSA’s nonlinearity.
- Parameters:
signature (EcdsaSignature)
conversions (int)
- signature: EcdsaSignature#
- class blockchainkit.wallets.core.base.RecoveryRisk(lockout, theft)[source]#
Bases:
objectHow a guardian scheme fails: by locking its owner out, or by robbing them.
- Variables:
- Parameters:
- class blockchainkit.wallets.core.base.UserOperation(sender, nonce, function, args, fee, signature=None)[source]#
Bases:
objectAn ERC-4337 user operation: what an account owner asks its account to do.
- Variables:
sender (
str) – The smart account that will run it.nonce (
int) – The account’s operation counter, so that each operation runs once.function (
str) – The account function to call, such as"execute".args (
tuple) – Its arguments.fee (
int) – Ether the account pays the bundler, a stand-in for gas times gas price.signature (
blockchainkit.crypto.core.base.SchnorrSignatureorNone) – The owner’s signature over the operation’s hash; None until signed.
- Parameters:
- signature: SchnorrSignature | None = None#
Error-detecting codes#
The finite fields GF(2**m) behind BCH codes.
An element is an integer below 2**m whose bits are the coefficients of
a polynomial over GF(2); addition is XOR, and multiplication is modulo a
fixed primitive polynomial, so that \(\alpha = x\) generates every
nonzero element. Tables of powers and logarithms make multiplication a
lookup.
- blockchainkit.wallets.utils.galois.PRIMITIVE_POLYNOMIALS = {3: 11, 4: 19, 5: 37, 6: 67, 7: 137, 8: 285}#
A primitive polynomial of each degree m from 3 to 8, as a bit mask.
- Type:
- class blockchainkit.wallets.utils.galois.GaloisField(m)[source]#
Bases:
objectGF(2**m) for 3 <= m <= 8, with the primitive element alpha = x.
Examples
>>> from blockchainkit.wallets.utils import GaloisField >>> field = GaloisField(4) >>> field.multiply(field.power(3), field.power(14)) == field.power(2) True
- Parameters:
m (int)
- property order: int#
the multiplicative group’s order.
- Type:
The number of nonzero elements,
2**m - 1
BCH codes (Hocquenghem, 1959; Bose and Ray-Chaudhuri, 1960).
A binary BCH code of length n = 2**m - 1 treats a word of n bits as a
polynomial over GF(2) and accepts exactly the multiples of a generator g(x)
chosen to vanish at alpha, alpha**2, …, alpha**(2t) in GF(2**m). Any
nonzero codeword then has at least 2t + 1 nonzero bits (the BCH bound), so
up to t flipped bits are corrected and up to 2t detected. Decoding computes
the syndromes S_j = r(alpha**j), finds the error-locator polynomial with
the Berlekamp-Massey algorithm, and finds its roots by trying every
position (Chien’s search).
Bech32’s checksum is a BCH code too, over GF(32) rather than GF(2): see
blockchainkit.wallets.systems.bech32.
- blockchainkit.wallets.systems.bch.minimal_polynomial(field_, exponent)[source]#
The minimal polynomial over GF(2) of
alpha ** exponent, as a bit mask.Its roots are the conjugates
alpha ** (exponent * 2**j), the cyclotomic coset of the exponent.Examples
>>> from blockchainkit.wallets import minimal_polynomial >>> from blockchainkit.wallets.utils import GaloisField >>> bin(minimal_polynomial(GaloisField(4), 1)) '0b10011'
- Parameters:
field_ (GaloisField)
exponent (int)
- Return type:
- class blockchainkit.wallets.systems.bch.BCHCode(m, t)[source]#
Bases:
objectA narrow-sense binary BCH code of length
2**m - 1correctingterrors.- Parameters:
Examples
>>> from blockchainkit.wallets import BCHCode >>> code = BCHCode(4, 2) >>> code.n, code.k (15, 7) >>> word = list(code.encode([1, 0, 1, 1, 0, 0, 1])) >>> word[0] ^= 1; word[9] ^= 1 >>> code.decode(word).message (1, 0, 1, 1, 0, 0, 1)
- galois: GaloisField#
- encode(message)[source]#
Append
n - kcheck bits tokmessage bits (systematic encoding).The codeword is
m(x) x**(n-k)plus its remainder modulo g(x), a multiple of g(x) that starts with the message.
Check digits: Luhn’s (1954) and Verhoeff’s (1969).
A check digit is appended so that a number typed with a mistake fails a test. Verhoeff’s study of Dutch post-office data found that single wrong digits and swaps of two adjacent digits make up most typing errors (about 80% and 10%). A sum modulo 10 catches every single error but no swap, because addition commutes. Luhn’s scheme, used on payment cards, doubles every second digit, which catches every swap but one: 09 and 90.
Verhoeff’s scheme catches them all, by computing in a group that does not commute: the dihedral group D5 of the ten symmetries of a pentagon, with the digits 0-4 as rotations and 5-9 as reflections. Digit i (counting from the right, check digit at i = 0) is first permuted by \(\sigma^i\), where \(\sigma\) is a fixed permutation, and the number is valid when the product is the identity:
- blockchainkit.wallets.systems.check_digits.verhoeff_digit(number)[source]#
The Verhoeff check digit to append to
number.Examples
>>> from blockchainkit.wallets import verhoeff_digit, verhoeff_valid >>> verhoeff_digit("236") '3' >>> verhoeff_valid("2363"), verhoeff_valid("2633") (True, False)
- blockchainkit.wallets.systems.check_digits.verhoeff_valid(number)[source]#
True if
numberends with its correct Verhoeff check digit.
- blockchainkit.wallets.systems.check_digits.luhn_digit(number)[source]#
The Luhn check digit to append to
number, as on payment cards.Examples
>>> from blockchainkit.wallets import luhn_digit >>> luhn_digit("7992739871") '3'
- blockchainkit.wallets.systems.check_digits.luhn_valid(number)[source]#
True if
numberends with its correct Luhn check digit.
- blockchainkit.wallets.systems.check_digits.sum_digit(number)[source]#
The digit that makes the digit sum a multiple of 10: catches no swap.
- blockchainkit.wallets.systems.check_digits.sum_valid(number)[source]#
True if the digits of
numberadd up to a multiple of 10.
- blockchainkit.wallets.systems.check_digits.CHECK_SCHEMES: dict[str, tuple[Callable[[str], str], Callable[[str], bool]]] = {'luhn': (<function luhn_digit>, <function luhn_valid>), 'sum': (<function sum_digit>, <function sum_valid>), 'verhoeff': (<function verhoeff_digit>, <function verhoeff_valid>)}#
Each scheme’s name, with its check-digit and validation functions.
- Type:
- blockchainkit.wallets.systems.check_digits.detection_rates(scheme, length=3)[source]#
Measure a scheme on every number of
lengthdigits, against every error.Each number gets its check digit; then every single wrong digit and every swap of two adjacent different digits, check digit included, is tried on it.
Examples
>>> from blockchainkit.wallets import detection_rates >>> detection_rates("verhoeff", 2) DetectionRates(scheme='verhoeff', single_errors=1.0, transpositions=1.0) >>> round(detection_rates("luhn", 2).transpositions, 3) 0.978
- Parameters:
- Return type:
Addresses#
RIPEMD-160 (Dobbertin, Bosselaers and Preneel, 1996), and Bitcoin’s HASH160.
Bitcoin names a key or a script by RIPEMD160(SHA256(data)), a 20-byte
digest. hashlib offers RIPEMD-160 only when the OpenSSL it was built
with does, which OpenSSL 3 and Pyodide do not by default, so it is written
out here from the specification. Two parallel lines of 80 steps each mix
the 16 message words in different orders, and their results are combined.
- blockchainkit.wallets.utils.ripemd160.ripemd160(data)[source]#
Return the 20-byte RIPEMD-160 digest of
data.Examples
>>> from blockchainkit.wallets.utils import ripemd160 >>> ripemd160(b"abc").hex() '8eb208f7e05d987a9b044a8e98c6b087f15a0bfc'
- blockchainkit.wallets.utils.ripemd160.hash160(data)[source]#
Bitcoin’s
HASH160: RIPEMD-160 of SHA-256, the 20 bytes inside an address.Examples
>>> from blockchainkit.wallets.utils import hash160 >>> len(hash160(b"key")) 20
Compressed secp256k1 public keys (SEC 1, section 2.3.3).
A point (x, y) on secp256k1 is fixed by x and the parity of y, since y and
p - y are the two square roots of x**3 + 7. The compressed encoding is one
byte, 0x02 for even y or 0x03 for odd, then x in 32 bytes: 33 bytes instead
of the 65 of encode_point().
Bitcoin wallets have used it since 2012, and BIP32 requires it.
- blockchainkit.wallets.utils.keys.compress(public)[source]#
Encode a secp256k1 point in 33 bytes: its y parity, then x.
Examples
>>> from blockchainkit.crypto import SECP256K1 >>> from blockchainkit.wallets.utils import compress >>> compress(SECP256K1.generator).hex()[:10] '0279be667e'
- blockchainkit.wallets.utils.keys.decompress(data)[source]#
Recover the point from its 33-byte compressed encoding.
secp256k1’s prime is 3 modulo 4, so a square root of
aisa ** ((p + 1) / 4).- Raises:
ValueError – The bytes are not 33 long, start with neither 0x02 nor 0x03, or name an x with no point above it.
- Parameters:
data (bytes)
- Return type:
Base58Check addresses (Nakamoto, 2009).
A Bitcoin address is a version byte and the 20-byte HASH160 of a public
key, followed by a 4-byte checksum, the first bytes of
SHA256(SHA256(version || payload)), and written in base 58. The alphabet
leaves out 0, O, I and l, which look alike, and every leading zero byte
becomes a leading 1. A mistyped address fails the checksum unless the
typo happens to match it, with probability
so a wallet refuses to pay it. The checksum detects errors but cannot say
where they are. blockchainkit.structures names accounts by a bare
SHA-256 hex digest, where every typo is another valid address.
- blockchainkit.wallets.systems.base58.ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'#
Bitcoin’s base-58 digits: the alphanumerics without 0, O, I and l.
- Type:
- blockchainkit.wallets.systems.base58.P2PKH_VERSION = 0#
Version byte of a pay-to-public-key-hash address (starts with 1).
- Type:
- blockchainkit.wallets.systems.base58.P2SH_VERSION = 5#
Version byte of a pay-to-script-hash address (starts with 3).
- Type:
- blockchainkit.wallets.systems.base58.base58_encode(data)[source]#
Write bytes in base 58, one
1per leading zero byte.Examples
>>> from blockchainkit.wallets import base58_encode >>> base58_encode(b"hello world") 'StV1DL6CwTryKyV'
- blockchainkit.wallets.systems.base58.base58_decode(text)[source]#
Read base 58 back into bytes.
- Raises:
ValueError – A character is not a base-58 digit.
- Parameters:
text (str)
- Return type:
- blockchainkit.wallets.systems.base58.base58check_encode(version, payload)[source]#
Base58Check: the version byte, the payload, and a 4-byte double-SHA-256 checksum.
- blockchainkit.wallets.systems.base58.base58check_decode(text)[source]#
Check and remove the checksum; return the version byte and the payload.
- Raises:
ValueError – The string is not base 58, is too short, or fails its checksum.
- Parameters:
text (str)
- Return type:
- blockchainkit.wallets.systems.base58.p2pkh_address(public, compressed=True)[source]#
The Bitcoin address of a public key: Base58Check of version 0 and its
HASH160.Wallets before 2012 hashed the 65-byte uncompressed key, so the same key has two addresses.
Examples
>>> from blockchainkit.crypto import SECP256K1 >>> from blockchainkit.wallets import p2pkh_address >>> p2pkh_address(SECP256K1.generator) # the key of private key 1 '1BgGZ9tcN4rm9KBzDn7KprQz87SZ26SAMH'
- blockchainkit.wallets.systems.base58.is_valid_address(text)[source]#
True if
textis a Base58Check string with a 20-byte payload and a known version.
Bech32 addresses (Wuille and Maxwell, BIP173, 2017) and Bech32m (Wuille, BIP350, 2020).
A Bech32 string is a human-readable part such as bc, the separator
1, and data in 32 lower-case characters chosen to avoid look-alikes,
ending in a 6-character checksum. The checksum is a BCH code over GF(32):
the data and checksum, read as a polynomial with 5-bit coefficients, must
leave a fixed remainder modulo a generator g(x) of degree 6,
g(x) was chosen by a search of 159,605 BCH codes for the best detection of the errors people make: every error affecting at most 4 characters is detected in strings of up to 89 characters, which Base58Check’s 32-bit hash cannot promise.
In 2019 a weakness was found: when a string ends in p, inserting or
deleting q characters just before it leaves the checksum valid, because
q is the value 0 and the constant 1 lets the polynomial absorb factors
of x. Segregated-witness version 0 survives, since its programs must be 20
or 32 bytes, but later versions allow other lengths. Bech32m changes only
the constant, to 0x2bc830a3, and is required from witness version 1
(Taproot) on.
- blockchainkit.wallets.systems.bech32.CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'#
The 32 data characters; character i has the 5-bit value i.
- Type:
- blockchainkit.wallets.systems.bech32.BECH32_CONSTANTS = {'bech32': 1, 'bech32m': 734539939}#
The value a valid checksum leaves, for each encoding.
- Type:
- blockchainkit.wallets.systems.bech32.polymod(values)[source]#
The remainder of a sequence of 5-bit values modulo Bech32’s generator polynomial.
Each step shifts in one value; the five constants are g(x) times 1, 2, 4, 8 and 16 in GF(32), added for each bit that overflows.
- blockchainkit.wallets.systems.bech32.bech32_encode(hrp, data, encoding='bech32')[source]#
Write
hrp,1, the 5-bitdataand a 6-character checksum.Examples
>>> from blockchainkit.wallets import bech32_encode >>> bech32_encode("a", []) 'a12uel5l' >>> bech32_encode("a", [], "bech32m") 'a1lqfn3a'
- blockchainkit.wallets.systems.bech32.bech32_decode(text)[source]#
Check a Bech32 or Bech32m string and split it into its parts.
- Raises:
ValueError – Mixed case, a bad character or length, no separator, or a checksum that matches neither encoding.
- Parameters:
text (str)
- Return type:
- blockchainkit.wallets.systems.bech32.convert_bits(data, source, target, pad=True)[source]#
Regroup a sequence of
source-bit values intotarget-bit values.With
pad, the last group is filled with zero bits; without, leftover bits must be fewer thantargetand zero.
- blockchainkit.wallets.systems.bech32.segwit_encode(hrp, version, program)[source]#
A segregated-witness address: Bech32 for version 0, Bech32m from version 1.
Examples
>>> from blockchainkit.wallets import segwit_encode >>> segwit_encode("bc", 0, bytes.fromhex("751e76e8199196d454941c45d1b3a323f1433bd6")) 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
- blockchainkit.wallets.systems.bech32.segwit_decode(hrp, address)[source]#
Decode and check a segregated-witness address for network
hrp.- Raises:
ValueError – The checksum fails, the network differs, the program has a bad length, or the encoding does not match the version.
- Parameters:
- Return type:
- blockchainkit.wallets.systems.bech32.p2wpkh_address(public, hrp='bc')[source]#
The native segregated-witness address of a key: version 0 and its
HASH160.Examples
>>> from blockchainkit.crypto import SECP256K1 >>> from blockchainkit.wallets import p2wpkh_address >>> p2wpkh_address(SECP256K1.generator) 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'
Key stretching#
PBKDF2 key stretching (Kaliski, RFC 2898, 2000).
A key derived from a password is only as hard to guess as the password. Stretching cannot add entropy, but it can make every guess expensive: PBKDF2 iterates a keyed hash c times, so an attacker trying N passwords pays about N * c hash calls instead of N, while the honest user pays c once. A random salt, mixed into the first iteration, stops one precomputed table from serving every user. Block i of the output is
with HMAC as the pseudorandom function PRF. BIP39 uses PBKDF2 with HMAC-SHA512 and 2048 iterations to turn a mnemonic into a seed.
- blockchainkit.wallets.systems.stretching.PBKDF2_HASHES = ('sha1', 'sha256', 'sha512')#
The hash functions
pbkdf2()accepts.
- blockchainkit.wallets.systems.stretching.pbkdf2(password, salt, iterations, length, hash_name='sha256')[source]#
Derive
lengthbytes from a password with PBKDF2-HMAC (RFC 8018, section 5.2).- Parameters:
- Return type:
Examples
>>> from blockchainkit.wallets import pbkdf2 >>> pbkdf2(b"password", b"salt", 1, 20, "sha1").hex() # RFC 6070 '0c60c80f961f0e71f3a9b524af6012062fe037a6'
- blockchainkit.wallets.systems.stretching.crack_pin(key, salt, iterations, digits, hash_name='sha256')[source]#
Try every PIN of
digitsdigits until one stretches tokey.The attacker’s cost is
guesses * iterationsHMAC calls (per output block), which is what stretching buys.Examples
>>> from blockchainkit.wallets import crack_pin, pbkdf2 >>> key = pbkdf2(b"042", b"salt", 10, 32) >>> result = crack_pin(key, b"salt", 10, 3) >>> result.secret, result.guesses, result.hash_calls ('042', 43, 430)
scrypt, a sequential memory-hard key derivation function (Percival, 2009).
PBKDF2 makes each guess slow, but a guess needs almost no memory, so an attacker with custom chips runs millions of guesses side by side. scrypt makes each guess need a large table as well. ROMix fills a table V of N blocks, each the BlockMix of the last, then makes N reads at addresses that depend on the data:
An attacker can store fewer entries and recompute the missing ones, but
keeping only every k-th entry multiplies the second loop’s work by about
(k + 1)/2: memory times time stays roughly constant. Percival proved that no
algorithm does much better, so the cost of an attack in chip area and time
grows with N squared. BlockMix is built from the Salsa20/8 core. This module
implements RFC 7914 exactly, and romix() adds the trade-off.
- blockchainkit.wallets.systems.scrypt.salsa20_8(block)[source]#
The Salsa20/8 core: 8 rounds on sixteen 32-bit words, added back to the input.
Examples
>>> from blockchainkit.wallets import salsa20_8 >>> salsa20_8(bytes(64)) == bytes(64) True
- blockchainkit.wallets.systems.scrypt.block_mix(block)[source]#
scrypt’s BlockMix: chain Salsa20/8 through the 2r 64-byte pieces of a block.
The outputs are interleaved: even-numbered results first, then odd.
- blockchainkit.wallets.systems.scrypt.romix(block, n, keep_every=1)[source]#
scrypt’s ROMix, storing only every
keep_every-th table entry.A missing entry
V_jis recomputed from the storedV_{j - j mod k}. The output is the same for everykeep_every; only the cost changes.- Parameters:
- Returns:
The mixed block, and a
RomixCost.- Return type:
Examples
>>> from blockchainkit.wallets import romix >>> fast, cost = romix(bytes(range(128)), 64) >>> slow, frugal = romix(bytes(range(128)), 64, keep_every=8) >>> fast == slow, cost.stored, frugal.stored, cost.block_mixes < frugal.block_mixes (True, 64, 8, True)
- blockchainkit.wallets.systems.scrypt.scrypt(password, salt, n, r, p, length)[source]#
scrypt (RFC 7914): PBKDF2, then ROMix on each of p blocks, then PBKDF2 again.
- Parameters:
password (
bytes) – The secret and a per-user random value.salt (
bytes) – The secret and a per-user random value.n (
int) – CPU and memory cost: a power of two above 1.r (
int) – Block size: each table entry is 128 * r bytes.p (
int) – Parallelization: independent ROMix runs.length (
int) – Output bytes.
- Return type:
Examples
>>> from blockchainkit.wallets import scrypt >>> scrypt(b"", b"", 16, 1, 1, 64).hex()[:16] # RFC 7914, section 12 '77d6576238657b20'
Brain wallets and the thieves who drain them (Vasek et al., 2016).
A brain wallet keeps no file: its private key is the SHA-256 of a passphrase the owner remembers,
so the coins can be carried in one’s head across a border. But anyone can compute the same key. Thieves hash dictionaries, quotations, song lyrics and leaked passwords, keep a table from address to key, and watch the chain: when a deposit reaches an address in the table, they move the coins away. Vasek et al. searched the Bitcoin chain for such addresses, found hundreds of funded brain wallets, and found that most were emptied, many within minutes of their first deposit, by thieves racing one another. The passphrase has the entropy a person chooses, not the 128 bits a random key has, and there is no salt and no stretching to slow the thieves down.
- blockchainkit.wallets.systems.brainwallets.PHRASEBOOK = ('password', 'bitcoin', 'satoshi nakamoto', 'satoshi', 'hello world', 'correct horse battery staple', 'to be or not to be', 'one two three four five', 'the quick brown fox jumps over the lazy dog', 'how much wood could a woodchuck chuck', 'i love you', 'letmein', 'qwerty', '123456', 'abc123', 'money', 'god', 'love', 'secret', 'brainwallet', 'the answer is 42', 'all your base are belong to us', 'may the force be with you', 'in god we trust', 'carpe diem', 'veni vidi vici', 'e equals m c squared', 'open sesame', 'chancellor on brink of second bailout for banks', 'bitcoin is the future')#
Passphrases of the kind thieves hash first: the most common choices.
- blockchainkit.wallets.systems.brainwallets.brain_wallet_key(passphrase)[source]#
The private key of a brain wallet: SHA-256 of the UTF-8 passphrase.
Examples
>>> from blockchainkit.wallets import brain_wallet_key >>> hex(brain_wallet_key("password"))[:10] '0x5e884898'
- blockchainkit.wallets.systems.brainwallets.brain_wallet_address(passphrase)[source]#
The address of a brain wallet, from its uncompressed key as wallets of 2011-2015 used.
- blockchainkit.wallets.systems.brainwallets.crack_brain_wallet(address, candidates)[source]#
Hash candidate passphrases until one gives
address; None if none does.Examples
>>> from blockchainkit.wallets import brain_wallet_address, crack_brain_wallet >>> crack_brain_wallet(brain_wallet_address("letmein"), ["123456", "letmein"]) 'letmein'
- blockchainkit.wallets.systems.brainwallets.simulate_brain_drain(wallets=60, *, weak_share=0.5, thieves=((0.7, 2.0), (0.5, 10.0), (0.3, 50.0)), seed=0)[source]#
Fund brain wallets and let thieves with precomputed tables race to empty them.
- Parameters:
wallets (
int) – Wallets funded, each by a different owner.weak_share (
float) – Probability that an owner picks a phrase fromPHRASEBOOK; the others use a random 128-bit key written as hex.thieves (
collections.abc.Sequenceoftuple) – Each thief’s coverage, the share of the phrasebook in its table, and its mean delay in blocks between a deposit and its sweep.seed (
int) – Seeds the owners’ choices, the tables and the delays.
- Return type:
Examples
>>> from blockchainkit.wallets import simulate_brain_drain >>> drain = simulate_brain_drain(20, seed=1) >>> drain.drained <= drain.weak True
Key derivation#
Hierarchical deterministic wallets (Wuille, BIP32, 2012).
Early wallets drew every key at random, so a backup was stale as soon as the next key was made. BIP32 derives a whole tree of secp256k1 keys from one seed. An extended key is a key k with a 32-byte chain code c, and child i comes from
where the data is the parent’s compressed public key and i for a normal child, or the private key and i for a hardened child, i >= 2**31. Because \(k_i G = I_L G + K\), a normal child’s public key follows from the parent’s public key alone: a shop can hold the extended public key and make a fresh address for every order, with no private key on its server.
The price is that the extended public key and any one normal child’s private key reveal the parent’s private key, \(k = k_i - I_L\). Hardened children cut that link, which is why account keys are hardened.
- blockchainkit.wallets.systems.hd.EXTENDED_KEY_VERSIONS = {'xprv': 76066276, 'xpub': 76067358}#
The 4-byte version prefixes of serialized mainnet extended keys.
- Type:
- blockchainkit.wallets.systems.hd.master_key(seed)[source]#
The master extended private key: HMAC-SHA512 of the seed under
"Bitcoin seed".- Parameters:
seed (
bytes) – 16 to 64 bytes, such as a BIP39 seed.- Return type:
Examples
>>> from blockchainkit.wallets import master_key >>> master = master_key(bytes(range(16))) # BIP32 test vector 1 >>> hex(master.private)[:10] '0xe8f32e72'
- blockchainkit.wallets.systems.hd.fingerprint(key)[source]#
The first 4 bytes of
HASH160of the key’s compressed public key.- Parameters:
key (ExtendedKey)
- Return type:
- blockchainkit.wallets.systems.hd.derive_private(parent, index)[source]#
BIP32’s CKDpriv: child
indexof an extended private key.Indices from
2**31(writteni') are hardened.- Parameters:
parent (ExtendedKey)
index (int)
- Return type:
- blockchainkit.wallets.systems.hd.derive_public(parent, index)[source]#
BIP32’s CKDpub: child
index’s public key from the parent’s public key alone.- Raises:
ValueError –
indexis hardened: that needs the private key.- Parameters:
parent (ExtendedKey)
index (int)
- Return type:
- blockchainkit.wallets.systems.hd.neuter(key)[source]#
The extended public key: the same key and chain code, without the private key.
- Parameters:
key (ExtendedKey)
- Return type:
- blockchainkit.wallets.systems.hd.serialize_extended_key(key)[source]#
The 78-byte serialization in Base58Check:
xprv...orxpub....Examples
>>> from blockchainkit.wallets import master_key, neuter, serialize_extended_key >>> serialize_extended_key(neuter(master_key(bytes(range(16)))))[:12] 'xpub661MyMwA'
- Parameters:
key (ExtendedKey)
- Return type:
- blockchainkit.wallets.systems.hd.parse_extended_key(text)[source]#
Read an
xprvorxpubstring back into an extended key.- Raises:
ValueError – A bad checksum, length, version or key.
- Parameters:
text (str)
- Return type:
- blockchainkit.wallets.systems.hd.recover_parent_private(parent, child_private, index)[source]#
The leaked-xpub attack: a parent’s private key from its xpub and one child’s key.
For a normal (non-hardened) child, \(k = k_i - I_L \bmod n\), and \(I_L\) needs only the parent’s public key and chain code.
Examples
>>> from blockchainkit.wallets import derive_private, master_key, neuter >>> from blockchainkit.wallets import recover_parent_private >>> master = master_key(bytes(range(16))) >>> child = derive_private(master, 7) >>> recover_parent_private(neuter(master), child.private, 7) == master.private True
- Parameters:
parent (ExtendedKey)
child_private (int)
index (int)
- Return type:
Mnemonic seed phrases (Palatinus, Rusnak, Voisine and Bowe, BIP39, 2013).
A 128-bit seed written in hexadecimal is 32 characters that people miscopy. BIP39 writes it as words instead. The entropy gets a checksum, its first ENT/32 bits of SHA-256, and the bits are cut into 11-bit groups, each an index into a list of 2048 words:
A mistyped word is caught by the checksum unless the bits happen to match,
which for 12 words is 1 time in 16. The phrase then goes through
PBKDF2-HMAC-SHA512 with 2048 iterations, salted with "mnemonic" and an
optional passphrase, to give the 64-byte seed for BIP32. Every passphrase
gives a valid, different wallet, so a passphrase cannot be checked: a
forgotten one and a decoy look the same.
Teaching difference: the word list is not BIP39’s English list but 2048
four-letter words generated by a rule (consonant, vowel, consonant,
vowel), in sorted order, so that index and word convert without a stored
list. mnemonic_to_seed() takes any phrase, so it reproduces BIP39’s
published seeds for English phrases.
- blockchainkit.wallets.systems.mnemonics.WORDLIST = ('bada', 'bade', 'badi', 'bado', 'baka', 'bake', 'baki', 'bako', 'bala', 'bale', 'bali', 'balo', 'bama', 'bame', 'bami', 'bamo', 'bana', 'bane', 'bani', 'bano', 'bara', 'bare', 'bari', 'baro', 'basa', 'base', 'basi', 'baso', 'bata', 'bate', 'bati', 'bato', 'bida', 'bide', 'bidi', 'bido', 'bika', 'bike', 'biki', 'biko', 'bila', 'bile', 'bili', 'bilo', 'bima', 'bime', 'bimi', 'bimo', 'bina', 'bine', 'bini', 'bino', 'bira', 'bire', 'biri', 'biro', 'bisa', 'bise', 'bisi', 'biso', 'bita', 'bite', 'biti', 'bito', 'boda', 'bode', 'bodi', 'bodo', 'boka', 'boke', 'boki', 'boko', 'bola', 'bole', 'boli', 'bolo', 'boma', 'bome', 'bomi', 'bomo', 'bona', 'bone', 'boni', 'bono', 'bora', 'bore', 'bori', 'boro', 'bosa', 'bose', 'bosi', 'boso', 'bota', 'bote', 'boti', 'boto', 'buda', 'bude', 'budi', 'budo', 'buka', 'buke', 'buki', 'buko', 'bula', 'bule', 'buli', 'bulo', 'buma', 'bume', 'bumi', 'bumo', 'buna', 'bune', 'buni', 'buno', 'bura', 'bure', 'buri', 'buro', 'busa', 'buse', 'busi', 'buso', 'buta', 'bute', 'buti', 'buto', 'dada', 'dade', 'dadi', 'dado', 'daka', 'dake', 'daki', 'dako', 'dala', 'dale', 'dali', 'dalo', 'dama', 'dame', 'dami', 'damo', 'dana', 'dane', 'dani', 'dano', 'dara', 'dare', 'dari', 'daro', 'dasa', 'dase', 'dasi', 'daso', 'data', 'date', 'dati', 'dato', 'dida', 'dide', 'didi', 'dido', 'dika', 'dike', 'diki', 'diko', 'dila', 'dile', 'dili', 'dilo', 'dima', 'dime', 'dimi', 'dimo', 'dina', 'dine', 'dini', 'dino', 'dira', 'dire', 'diri', 'diro', 'disa', 'dise', 'disi', 'diso', 'dita', 'dite', 'diti', 'dito', 'doda', 'dode', 'dodi', 'dodo', 'doka', 'doke', 'doki', 'doko', 'dola', 'dole', 'doli', 'dolo', 'doma', 'dome', 'domi', 'domo', 'dona', 'done', 'doni', 'dono', 'dora', 'dore', 'dori', 'doro', 'dosa', 'dose', 'dosi', 'doso', 'dota', 'dote', 'doti', 'doto', 'duda', 'dude', 'dudi', 'dudo', 'duka', 'duke', 'duki', 'duko', 'dula', 'dule', 'duli', 'dulo', 'duma', 'dume', 'dumi', 'dumo', 'duna', 'dune', 'duni', 'duno', 'dura', 'dure', 'duri', 'duro', 'dusa', 'duse', 'dusi', 'duso', 'duta', 'dute', 'duti', 'duto', 'fada', 'fade', 'fadi', 'fado', 'faka', 'fake', 'faki', 'fako', 'fala', 'fale', 'fali', 'falo', 'fama', 'fame', 'fami', 'famo', 'fana', 'fane', 'fani', 'fano', 'fara', 'fare', 'fari', 'faro', 'fasa', 'fase', 'fasi', 'faso', 'fata', 'fate', 'fati', 'fato', 'fida', 'fide', 'fidi', 'fido', 'fika', 'fike', 'fiki', 'fiko', 'fila', 'file', 'fili', 'filo', 'fima', 'fime', 'fimi', 'fimo', 'fina', 'fine', 'fini', 'fino', 'fira', 'fire', 'firi', 'firo', 'fisa', 'fise', 'fisi', 'fiso', 'fita', 'fite', 'fiti', 'fito', 'foda', 'fode', 'fodi', 'fodo', 'foka', 'foke', 'foki', 'foko', 'fola', 'fole', 'foli', 'folo', 'foma', 'fome', 'fomi', 'fomo', 'fona', 'fone', 'foni', 'fono', 'fora', 'fore', 'fori', 'foro', 'fosa', 'fose', 'fosi', 'foso', 'fota', 'fote', 'foti', 'foto', 'fuda', 'fude', 'fudi', 'fudo', 'fuka', 'fuke', 'fuki', 'fuko', 'fula', 'fule', 'fuli', 'fulo', 'fuma', 'fume', 'fumi', 'fumo', 'funa', 'fune', 'funi', 'funo', 'fura', 'fure', 'furi', 'furo', 'fusa', 'fuse', 'fusi', 'fuso', 'futa', 'fute', 'futi', 'futo', 'gada', 'gade', 'gadi', 'gado', 'gaka', 'gake', 'gaki', 'gako', 'gala', 'gale', 'gali', 'galo', 'gama', 'game', 'gami', 'gamo', 'gana', 'gane', 'gani', 'gano', 'gara', 'gare', 'gari', 'garo', 'gasa', 'gase', 'gasi', 'gaso', 'gata', 'gate', 'gati', 'gato', 'gida', 'gide', 'gidi', 'gido', 'gika', 'gike', 'giki', 'giko', 'gila', 'gile', 'gili', 'gilo', 'gima', 'gime', 'gimi', 'gimo', 'gina', 'gine', 'gini', 'gino', 'gira', 'gire', 'giri', 'giro', 'gisa', 'gise', 'gisi', 'giso', 'gita', 'gite', 'giti', 'gito', 'goda', 'gode', 'godi', 'godo', 'goka', 'goke', 'goki', 'goko', 'gola', 'gole', 'goli', 'golo', 'goma', 'gome', 'gomi', 'gomo', 'gona', 'gone', 'goni', 'gono', 'gora', 'gore', 'gori', 'goro', 'gosa', 'gose', 'gosi', 'goso', 'gota', 'gote', 'goti', 'goto', 'guda', 'gude', 'gudi', 'gudo', 'guka', 'guke', 'guki', 'guko', 'gula', 'gule', 'guli', 'gulo', 'guma', 'gume', 'gumi', 'gumo', 'guna', 'gune', 'guni', 'guno', 'gura', 'gure', 'guri', 'guro', 'gusa', 'guse', 'gusi', 'guso', 'guta', 'gute', 'guti', 'guto', 'hada', 'hade', 'hadi', 'hado', 'haka', 'hake', 'haki', 'hako', 'hala', 'hale', 'hali', 'halo', 'hama', 'hame', 'hami', 'hamo', 'hana', 'hane', 'hani', 'hano', 'hara', 'hare', 'hari', 'haro', 'hasa', 'hase', 'hasi', 'haso', 'hata', 'hate', 'hati', 'hato', 'hida', 'hide', 'hidi', 'hido', 'hika', 'hike', 'hiki', 'hiko', 'hila', 'hile', 'hili', 'hilo', 'hima', 'hime', 'himi', 'himo', 'hina', 'hine', 'hini', 'hino', 'hira', 'hire', 'hiri', 'hiro', 'hisa', 'hise', 'hisi', 'hiso', 'hita', 'hite', 'hiti', 'hito', 'hoda', 'hode', 'hodi', 'hodo', 'hoka', 'hoke', 'hoki', 'hoko', 'hola', 'hole', 'holi', 'holo', 'homa', 'home', 'homi', 'homo', 'hona', 'hone', 'honi', 'hono', 'hora', 'hore', 'hori', 'horo', 'hosa', 'hose', 'hosi', 'hoso', 'hota', 'hote', 'hoti', 'hoto', 'huda', 'hude', 'hudi', 'hudo', 'huka', 'huke', 'huki', 'huko', 'hula', 'hule', 'huli', 'hulo', 'huma', 'hume', 'humi', 'humo', 'huna', 'hune', 'huni', 'huno', 'hura', 'hure', 'huri', 'huro', 'husa', 'huse', 'husi', 'huso', 'huta', 'hute', 'huti', 'huto', 'jada', 'jade', 'jadi', 'jado', 'jaka', 'jake', 'jaki', 'jako', 'jala', 'jale', 'jali', 'jalo', 'jama', 'jame', 'jami', 'jamo', 'jana', 'jane', 'jani', 'jano', 'jara', 'jare', 'jari', 'jaro', 'jasa', 'jase', 'jasi', 'jaso', 'jata', 'jate', 'jati', 'jato', 'jida', 'jide', 'jidi', 'jido', 'jika', 'jike', 'jiki', 'jiko', 'jila', 'jile', 'jili', 'jilo', 'jima', 'jime', 'jimi', 'jimo', 'jina', 'jine', 'jini', 'jino', 'jira', 'jire', 'jiri', 'jiro', 'jisa', 'jise', 'jisi', 'jiso', 'jita', 'jite', 'jiti', 'jito', 'joda', 'jode', 'jodi', 'jodo', 'joka', 'joke', 'joki', 'joko', 'jola', 'jole', 'joli', 'jolo', 'joma', 'jome', 'jomi', 'jomo', 'jona', 'jone', 'joni', 'jono', 'jora', 'jore', 'jori', 'joro', 'josa', 'jose', 'josi', 'joso', 'jota', 'jote', 'joti', 'joto', 'juda', 'jude', 'judi', 'judo', 'juka', 'juke', 'juki', 'juko', 'jula', 'jule', 'juli', 'julo', 'juma', 'jume', 'jumi', 'jumo', 'juna', 'june', 'juni', 'juno', 'jura', 'jure', 'juri', 'juro', 'jusa', 'juse', 'jusi', 'juso', 'juta', 'jute', 'juti', 'juto', 'kada', 'kade', 'kadi', 'kado', 'kaka', 'kake', 'kaki', 'kako', 'kala', 'kale', 'kali', 'kalo', 'kama', 'kame', 'kami', 'kamo', 'kana', 'kane', 'kani', 'kano', 'kara', 'kare', 'kari', 'karo', 'kasa', 'kase', 'kasi', 'kaso', 'kata', 'kate', 'kati', 'kato', 'kida', 'kide', 'kidi', 'kido', 'kika', 'kike', 'kiki', 'kiko', 'kila', 'kile', 'kili', 'kilo', 'kima', 'kime', 'kimi', 'kimo', 'kina', 'kine', 'kini', 'kino', 'kira', 'kire', 'kiri', 'kiro', 'kisa', 'kise', 'kisi', 'kiso', 'kita', 'kite', 'kiti', 'kito', 'koda', 'kode', 'kodi', 'kodo', 'koka', 'koke', 'koki', 'koko', 'kola', 'kole', 'koli', 'kolo', 'koma', 'kome', 'komi', 'komo', 'kona', 'kone', 'koni', 'kono', 'kora', 'kore', 'kori', 'koro', 'kosa', 'kose', 'kosi', 'koso', 'kota', 'kote', 'koti', 'koto', 'kuda', 'kude', 'kudi', 'kudo', 'kuka', 'kuke', 'kuki', 'kuko', 'kula', 'kule', 'kuli', 'kulo', 'kuma', 'kume', 'kumi', 'kumo', 'kuna', 'kune', 'kuni', 'kuno', 'kura', 'kure', 'kuri', 'kuro', 'kusa', 'kuse', 'kusi', 'kuso', 'kuta', 'kute', 'kuti', 'kuto', 'lada', 'lade', 'ladi', 'lado', 'laka', 'lake', 'laki', 'lako', 'lala', 'lale', 'lali', 'lalo', 'lama', 'lame', 'lami', 'lamo', 'lana', 'lane', 'lani', 'lano', 'lara', 'lare', 'lari', 'laro', 'lasa', 'lase', 'lasi', 'laso', 'lata', 'late', 'lati', 'lato', 'lida', 'lide', 'lidi', 'lido', 'lika', 'like', 'liki', 'liko', 'lila', 'lile', 'lili', 'lilo', 'lima', 'lime', 'limi', 'limo', 'lina', 'line', 'lini', 'lino', 'lira', 'lire', 'liri', 'liro', 'lisa', 'lise', 'lisi', 'liso', 'lita', 'lite', 'liti', 'lito', 'loda', 'lode', 'lodi', 'lodo', 'loka', 'loke', 'loki', 'loko', 'lola', 'lole', 'loli', 'lolo', 'loma', 'lome', 'lomi', 'lomo', 'lona', 'lone', 'loni', 'lono', 'lora', 'lore', 'lori', 'loro', 'losa', 'lose', 'losi', 'loso', 'lota', 'lote', 'loti', 'loto', 'luda', 'lude', 'ludi', 'ludo', 'luka', 'luke', 'luki', 'luko', 'lula', 'lule', 'luli', 'lulo', 'luma', 'lume', 'lumi', 'lumo', 'luna', 'lune', 'luni', 'luno', 'lura', 'lure', 'luri', 'luro', 'lusa', 'luse', 'lusi', 'luso', 'luta', 'lute', 'luti', 'luto', 'mada', 'made', 'madi', 'mado', 'maka', 'make', 'maki', 'mako', 'mala', 'male', 'mali', 'malo', 'mama', 'mame', 'mami', 'mamo', 'mana', 'mane', 'mani', 'mano', 'mara', 'mare', 'mari', 'maro', 'masa', 'mase', 'masi', 'maso', 'mata', 'mate', 'mati', 'mato', 'mida', 'mide', 'midi', 'mido', 'mika', 'mike', 'miki', 'miko', 'mila', 'mile', 'mili', 'milo', 'mima', 'mime', 'mimi', 'mimo', 'mina', 'mine', 'mini', 'mino', 'mira', 'mire', 'miri', 'miro', 'misa', 'mise', 'misi', 'miso', 'mita', 'mite', 'miti', 'mito', 'moda', 'mode', 'modi', 'modo', 'moka', 'moke', 'moki', 'moko', 'mola', 'mole', 'moli', 'molo', 'moma', 'mome', 'momi', 'momo', 'mona', 'mone', 'moni', 'mono', 'mora', 'more', 'mori', 'moro', 'mosa', 'mose', 'mosi', 'moso', 'mota', 'mote', 'moti', 'moto', 'muda', 'mude', 'mudi', 'mudo', 'muka', 'muke', 'muki', 'muko', 'mula', 'mule', 'muli', 'mulo', 'muma', 'mume', 'mumi', 'mumo', 'muna', 'mune', 'muni', 'muno', 'mura', 'mure', 'muri', 'muro', 'musa', 'muse', 'musi', 'muso', 'muta', 'mute', 'muti', 'muto', 'nada', 'nade', 'nadi', 'nado', 'naka', 'nake', 'naki', 'nako', 'nala', 'nale', 'nali', 'nalo', 'nama', 'name', 'nami', 'namo', 'nana', 'nane', 'nani', 'nano', 'nara', 'nare', 'nari', 'naro', 'nasa', 'nase', 'nasi', 'naso', 'nata', 'nate', 'nati', 'nato', 'nida', 'nide', 'nidi', 'nido', 'nika', 'nike', 'niki', 'niko', 'nila', 'nile', 'nili', 'nilo', 'nima', 'nime', 'nimi', 'nimo', 'nina', 'nine', 'nini', 'nino', 'nira', 'nire', 'niri', 'niro', 'nisa', 'nise', 'nisi', 'niso', 'nita', 'nite', 'niti', 'nito', 'noda', 'node', 'nodi', 'nodo', 'noka', 'noke', 'noki', 'noko', 'nola', 'nole', 'noli', 'nolo', 'noma', 'nome', 'nomi', 'nomo', 'nona', 'none', 'noni', 'nono', 'nora', 'nore', 'nori', 'noro', 'nosa', 'nose', 'nosi', 'noso', 'nota', 'note', 'noti', 'noto', 'nuda', 'nude', 'nudi', 'nudo', 'nuka', 'nuke', 'nuki', 'nuko', 'nula', 'nule', 'nuli', 'nulo', 'numa', 'nume', 'numi', 'numo', 'nuna', 'nune', 'nuni', 'nuno', 'nura', 'nure', 'nuri', 'nuro', 'nusa', 'nuse', 'nusi', 'nuso', 'nuta', 'nute', 'nuti', 'nuto', 'pada', 'pade', 'padi', 'pado', 'paka', 'pake', 'paki', 'pako', 'pala', 'pale', 'pali', 'palo', 'pama', 'pame', 'pami', 'pamo', 'pana', 'pane', 'pani', 'pano', 'para', 'pare', 'pari', 'paro', 'pasa', 'pase', 'pasi', 'paso', 'pata', 'pate', 'pati', 'pato', 'pida', 'pide', 'pidi', 'pido', 'pika', 'pike', 'piki', 'piko', 'pila', 'pile', 'pili', 'pilo', 'pima', 'pime', 'pimi', 'pimo', 'pina', 'pine', 'pini', 'pino', 'pira', 'pire', 'piri', 'piro', 'pisa', 'pise', 'pisi', 'piso', 'pita', 'pite', 'piti', 'pito', 'poda', 'pode', 'podi', 'podo', 'poka', 'poke', 'poki', 'poko', 'pola', 'pole', 'poli', 'polo', 'poma', 'pome', 'pomi', 'pomo', 'pona', 'pone', 'poni', 'pono', 'pora', 'pore', 'pori', 'poro', 'posa', 'pose', 'posi', 'poso', 'pota', 'pote', 'poti', 'poto', 'puda', 'pude', 'pudi', 'pudo', 'puka', 'puke', 'puki', 'puko', 'pula', 'pule', 'puli', 'pulo', 'puma', 'pume', 'pumi', 'pumo', 'puna', 'pune', 'puni', 'puno', 'pura', 'pure', 'puri', 'puro', 'pusa', 'puse', 'pusi', 'puso', 'puta', 'pute', 'puti', 'puto', 'rada', 'rade', 'radi', 'rado', 'raka', 'rake', 'raki', 'rako', 'rala', 'rale', 'rali', 'ralo', 'rama', 'rame', 'rami', 'ramo', 'rana', 'rane', 'rani', 'rano', 'rara', 'rare', 'rari', 'raro', 'rasa', 'rase', 'rasi', 'raso', 'rata', 'rate', 'rati', 'rato', 'rida', 'ride', 'ridi', 'rido', 'rika', 'rike', 'riki', 'riko', 'rila', 'rile', 'rili', 'rilo', 'rima', 'rime', 'rimi', 'rimo', 'rina', 'rine', 'rini', 'rino', 'rira', 'rire', 'riri', 'riro', 'risa', 'rise', 'risi', 'riso', 'rita', 'rite', 'riti', 'rito', 'roda', 'rode', 'rodi', 'rodo', 'roka', 'roke', 'roki', 'roko', 'rola', 'role', 'roli', 'rolo', 'roma', 'rome', 'romi', 'romo', 'rona', 'rone', 'roni', 'rono', 'rora', 'rore', 'rori', 'roro', 'rosa', 'rose', 'rosi', 'roso', 'rota', 'rote', 'roti', 'roto', 'ruda', 'rude', 'rudi', 'rudo', 'ruka', 'ruke', 'ruki', 'ruko', 'rula', 'rule', 'ruli', 'rulo', 'ruma', 'rume', 'rumi', 'rumo', 'runa', 'rune', 'runi', 'runo', 'rura', 'rure', 'ruri', 'ruro', 'rusa', 'ruse', 'rusi', 'ruso', 'ruta', 'rute', 'ruti', 'ruto', 'sada', 'sade', 'sadi', 'sado', 'saka', 'sake', 'saki', 'sako', 'sala', 'sale', 'sali', 'salo', 'sama', 'same', 'sami', 'samo', 'sana', 'sane', 'sani', 'sano', 'sara', 'sare', 'sari', 'saro', 'sasa', 'sase', 'sasi', 'saso', 'sata', 'sate', 'sati', 'sato', 'sida', 'side', 'sidi', 'sido', 'sika', 'sike', 'siki', 'siko', 'sila', 'sile', 'sili', 'silo', 'sima', 'sime', 'simi', 'simo', 'sina', 'sine', 'sini', 'sino', 'sira', 'sire', 'siri', 'siro', 'sisa', 'sise', 'sisi', 'siso', 'sita', 'site', 'siti', 'sito', 'soda', 'sode', 'sodi', 'sodo', 'soka', 'soke', 'soki', 'soko', 'sola', 'sole', 'soli', 'solo', 'soma', 'some', 'somi', 'somo', 'sona', 'sone', 'soni', 'sono', 'sora', 'sore', 'sori', 'soro', 'sosa', 'sose', 'sosi', 'soso', 'sota', 'sote', 'soti', 'soto', 'suda', 'sude', 'sudi', 'sudo', 'suka', 'suke', 'suki', 'suko', 'sula', 'sule', 'suli', 'sulo', 'suma', 'sume', 'sumi', 'sumo', 'suna', 'sune', 'suni', 'suno', 'sura', 'sure', 'suri', 'suro', 'susa', 'suse', 'susi', 'suso', 'suta', 'sute', 'suti', 'suto', 'tada', 'tade', 'tadi', 'tado', 'taka', 'take', 'taki', 'tako', 'tala', 'tale', 'tali', 'talo', 'tama', 'tame', 'tami', 'tamo', 'tana', 'tane', 'tani', 'tano', 'tara', 'tare', 'tari', 'taro', 'tasa', 'tase', 'tasi', 'taso', 'tata', 'tate', 'tati', 'tato', 'tida', 'tide', 'tidi', 'tido', 'tika', 'tike', 'tiki', 'tiko', 'tila', 'tile', 'tili', 'tilo', 'tima', 'time', 'timi', 'timo', 'tina', 'tine', 'tini', 'tino', 'tira', 'tire', 'tiri', 'tiro', 'tisa', 'tise', 'tisi', 'tiso', 'tita', 'tite', 'titi', 'tito', 'toda', 'tode', 'todi', 'todo', 'toka', 'toke', 'toki', 'toko', 'tola', 'tole', 'toli', 'tolo', 'toma', 'tome', 'tomi', 'tomo', 'tona', 'tone', 'toni', 'tono', 'tora', 'tore', 'tori', 'toro', 'tosa', 'tose', 'tosi', 'toso', 'tota', 'tote', 'toti', 'toto', 'tuda', 'tude', 'tudi', 'tudo', 'tuka', 'tuke', 'tuki', 'tuko', 'tula', 'tule', 'tuli', 'tulo', 'tuma', 'tume', 'tumi', 'tumo', 'tuna', 'tune', 'tuni', 'tuno', 'tura', 'ture', 'turi', 'turo', 'tusa', 'tuse', 'tusi', 'tuso', 'tuta', 'tute', 'tuti', 'tuto', 'vada', 'vade', 'vadi', 'vado', 'vaka', 'vake', 'vaki', 'vako', 'vala', 'vale', 'vali', 'valo', 'vama', 'vame', 'vami', 'vamo', 'vana', 'vane', 'vani', 'vano', 'vara', 'vare', 'vari', 'varo', 'vasa', 'vase', 'vasi', 'vaso', 'vata', 'vate', 'vati', 'vato', 'vida', 'vide', 'vidi', 'vido', 'vika', 'vike', 'viki', 'viko', 'vila', 'vile', 'vili', 'vilo', 'vima', 'vime', 'vimi', 'vimo', 'vina', 'vine', 'vini', 'vino', 'vira', 'vire', 'viri', 'viro', 'visa', 'vise', 'visi', 'viso', 'vita', 'vite', 'viti', 'vito', 'voda', 'vode', 'vodi', 'vodo', 'voka', 'voke', 'voki', 'voko', 'vola', 'vole', 'voli', 'volo', 'voma', 'vome', 'vomi', 'vomo', 'vona', 'vone', 'voni', 'vono', 'vora', 'vore', 'vori', 'voro', 'vosa', 'vose', 'vosi', 'voso', 'vota', 'vote', 'voti', 'voto', 'vuda', 'vude', 'vudi', 'vudo', 'vuka', 'vuke', 'vuki', 'vuko', 'vula', 'vule', 'vuli', 'vulo', 'vuma', 'vume', 'vumi', 'vumo', 'vuna', 'vune', 'vuni', 'vuno', 'vura', 'vure', 'vuri', 'vuro', 'vusa', 'vuse', 'vusi', 'vuso', 'vuta', 'vute', 'vuti', 'vuto', 'zada', 'zade', 'zadi', 'zado', 'zaka', 'zake', 'zaki', 'zako', 'zala', 'zale', 'zali', 'zalo', 'zama', 'zame', 'zami', 'zamo', 'zana', 'zane', 'zani', 'zano', 'zara', 'zare', 'zari', 'zaro', 'zasa', 'zase', 'zasi', 'zaso', 'zata', 'zate', 'zati', 'zato', 'zida', 'zide', 'zidi', 'zido', 'zika', 'zike', 'ziki', 'ziko', 'zila', 'zile', 'zili', 'zilo', 'zima', 'zime', 'zimi', 'zimo', 'zina', 'zine', 'zini', 'zino', 'zira', 'zire', 'ziri', 'ziro', 'zisa', 'zise', 'zisi', 'ziso', 'zita', 'zite', 'ziti', 'zito', 'zoda', 'zode', 'zodi', 'zodo', 'zoka', 'zoke', 'zoki', 'zoko', 'zola', 'zole', 'zoli', 'zolo', 'zoma', 'zome', 'zomi', 'zomo', 'zona', 'zone', 'zoni', 'zono', 'zora', 'zore', 'zori', 'zoro', 'zosa', 'zose', 'zosi', 'zoso', 'zota', 'zote', 'zoti', 'zoto', 'zuda', 'zude', 'zudi', 'zudo', 'zuka', 'zuke', 'zuki', 'zuko', 'zula', 'zule', 'zuli', 'zulo', 'zuma', 'zume', 'zumi', 'zumo', 'zuna', 'zune', 'zuni', 'zuno', 'zura', 'zure', 'zuri', 'zuro', 'zusa', 'zuse', 'zusi', 'zuso', 'zuta', 'zute', 'zuti', 'zuto')#
The 2048 words, in sorted order; word i encodes the 11 bits of i.
- blockchainkit.wallets.systems.mnemonics.entropy_to_mnemonic(entropy)[source]#
Encode 16 to 32 bytes of entropy, plus its checksum, as 12 to 24 words.
Examples
>>> from blockchainkit.wallets import entropy_to_mnemonic >>> entropy_to_mnemonic(bytes(16)).split()[-2:] ['bada', 'bado']
- blockchainkit.wallets.systems.mnemonics.mnemonic_to_entropy(mnemonic)[source]#
Check a phrase’s words and checksum, and return its entropy.
- Raises:
ValueError – A word is not in the list, the length is wrong, or the checksum fails: the phrase was miscopied.
- Parameters:
mnemonic (str)
- Return type:
- blockchainkit.wallets.systems.mnemonics.is_valid_mnemonic(mnemonic)[source]#
True if every word is in the list and the checksum holds.
- blockchainkit.wallets.systems.mnemonics.mnemonic_to_seed(mnemonic, passphrase='')[source]#
The 64-byte BIP39 seed: PBKDF2-HMAC-SHA512 over 2048 iterations.
The salt is
"mnemonic" + passphrase.Both strings are normalized to Unicode NFKD first. The phrase is not checked: BIP39 derives a seed from any string.
Examples
>>> from blockchainkit.wallets import mnemonic_to_seed >>> phrase = " ".join(["abandon"] * 11 + ["about"]) # BIP39 test vector >>> mnemonic_to_seed(phrase, "TREZOR").hex()[:16] 'c55257c360c07c72'
Multi-account derivation paths (Palatinus and Rusnak, BIP44, 2014).
BIP32 allows any tree; for one seed to restore the same wallet in another program, the programs must agree on where keys sit. BIP44 fixes five levels,
with hardened purpose, coin type (0 for Bitcoin, 60 for Ether) and account, then a chain for receiving (0) or change (1) addresses, and the address index. A restoring wallet cannot know how many addresses were used, so it scans: on each account’s receiving chain it derives addresses until 20 in a row are unused (the gap limit), and it stops at the first unused account. Coins sent to an address past a gap of 20 are safe but invisible.
- blockchainkit.wallets.systems.paths.GAP_LIMIT = 20#
Unused addresses in a row after which BIP44 discovery stops.
- Type:
- blockchainkit.wallets.systems.paths.parse_path(path)[source]#
Turn
"m/44'/0'/0'/0/5"into child indices, hardened ones offset by2**31.'andhboth mark a hardened index.Examples
>>> from blockchainkit.wallets import parse_path >>> parse_path("m/44'/0'/0'/0/5") (2147483692, 2147483648, 2147483648, 0, 5)
- blockchainkit.wallets.systems.paths.format_path(indices)[source]#
Write child indices as a path, hardened ones with
'.
- blockchainkit.wallets.systems.paths.derive_path(key, path)[source]#
Derive the key at
pathbelowkey.An extended public key can follow only normal (non-hardened) steps.
Examples
>>> from blockchainkit.wallets import derive_path, master_key, serialize_extended_key >>> key = derive_path(master_key(bytes(range(16))), "m/0'/1") >>> serialize_extended_key(key)[:12] 'xprv9wTYmMFd'
- Parameters:
key (ExtendedKey)
path (str)
- Return type:
- blockchainkit.wallets.systems.paths.bip44_path(coin, account, change, index)[source]#
The BIP44 path
m/44'/coin'/account'/change/index.
- blockchainkit.wallets.systems.paths.discover_accounts(master, used, *, coin=0, gap_limit=20, max_accounts=10)[source]#
Restore a wallet from its master key by BIP44 account discovery.
- Parameters:
master (
blockchainkit.wallets.core.base.ExtendedKey) – The master extended private key.used (
collections.abc.Collectionofstr) – The addresses that have appeared on the chain (P2PKH, compressed).coin (
int) – The coin type.gap_limit (
int) – Unused receiving addresses in a row that end an account’s scan.max_accounts (
int) – A bound on the accounts scanned.
- Return type:
Examples
>>> from blockchainkit.wallets import derive_path, discover_accounts, master_key, p2pkh_address >>> master = master_key(bytes(range(16))) >>> used = {p2pkh_address(derive_path(master, "m/44'/0'/0'/0/1").public)} >>> discover_accounts(master, used, gap_limit=3).found ("m/44'/0'/0'/0/1",)
Shared control#
Pay to script hash (Andresen, BIP16, 2012) and multisig wallets.
Before 2012, a coin locked by several keys carried the whole locking script, so the payer had to know and pay for the payee’s security policy. P2SH locks a coin to the hash of a redeem script instead:
The spender reveals the redeem script as the last push of the unlocking script. Validation runs twice: first the unlocking script and the locking script, which only check the hash; then, if that passes, the redeem script on the stack the unlocking script left, which checks the signatures. The payer sees only a 20-byte hash in a short address starting with 3, and the policy, such as 2 of 3 keys, stays private until the coin is spent.
Teaching differences from BIP16: the hash is OP_HASH256 (double
SHA-256), because blockchainkit.vm.systems.script has no
OP_HASH160; and since it has no OP_CHECKMULTISIG either, an m-of-n
redeem script is a tree of OP_IF branches, one per set of m signers,
which the spender picks with true and false pushes. Its size grows like
C(n, m), so the 520-byte limit on a pushed script soon binds: 2-of-3
fits, 3-of-5 does not.
- blockchainkit.wallets.systems.p2sh.OPCODE_BYTES = {'OP_CHECKLOCKTIMEVERIFY': 177, 'OP_CHECKSIG': 172, 'OP_DROP': 117, 'OP_DUP': 118, 'OP_ELSE': 103, 'OP_ENDIF': 104, 'OP_EQUAL': 135, 'OP_EQUALVERIFY': 136, 'OP_FALSE': 0, 'OP_HASH256': 170, 'OP_IF': 99, 'OP_RETURN': 106, 'OP_SHA256': 168, 'OP_TRUE': 81, 'OP_VERIFY': 105}#
Bitcoin’s byte for each opcode of the teaching interpreter.
- Type:
- blockchainkit.wallets.systems.p2sh.serialize_script(script)[source]#
Encode a script as Bitcoin does: opcodes as bytes, pushes with a length prefix.
A push of 1 to 75 bytes is prefixed by its length; longer pushes use
OP_PUSHDATA1(0x4c) orOP_PUSHDATA2(0x4d). An empty push is 0x00, the same byte asOP_FALSE, which pushes the empty string.
- blockchainkit.wallets.systems.p2sh.parse_script(data)[source]#
Decode
serialize_script()’s bytes back into opcodes and pushes.- Raises:
ValueError – An unknown opcode byte, or a push running past the end.
- Parameters:
data (bytes)
- Return type:
- blockchainkit.wallets.systems.p2sh.multisig_redeem_script(threshold, publics)[source]#
An m-of-n redeem script: one
OP_IFbranch per set ofthresholdkeys.Each branch checks its keys in turn,
<K1> OP_CHECKSIG OP_VERIFY ... <Km> OP_CHECKSIG, and the last branch needs noOP_IF.Examples
>>> from blockchainkit.crypto import public_key >>> from blockchainkit.wallets import multisig_redeem_script >>> script = multisig_redeem_script(2, [public_key(k) for k in (3, 5, 7)]) >>> script.count("OP_IF"), script.count("OP_CHECKSIG") (2, 6)
- blockchainkit.wallets.systems.p2sh.multisig_unlocking(threshold, keys, signatures)[source]#
The pushes that satisfy
multisig_redeem_script(): signatures, then branch choices.- Parameters:
threshold (
int) – m and n of the redeem script.keys (
int) – m and n of the redeem script.signatures (
collections.abc.Mappingofinttoblockchainkit.crypto.core.base.SchnorrSignature) – Exactlythresholdsignatures, keyed by the signer’s position in the key list.
- Return type:
- blockchainkit.wallets.systems.p2sh.p2sh_locking(redeem)[source]#
Lock a coin to a redeem script’s hash:
OP_HASH256 <hash> OP_EQUAL.
- blockchainkit.wallets.systems.p2sh.p2sh_unlocking(redeem, pushes)[source]#
The redeem script’s own unlocking pushes, followed by the serialized redeem script.
- blockchainkit.wallets.systems.p2sh.p2sh_address(redeem)[source]#
The Base58Check address of a redeem script: version 5 and its
HASH160.This is the address BIP16 gives the script; the teaching locking script commits with
OP_HASH256instead.
- blockchainkit.wallets.systems.p2sh.verify_p2sh(unlocking, locking, *, message=b'', locktime=0)[source]#
Validate a spend of a P2SH output in BIP16’s two stages.
The unlocking script must contain only pushes. Stage one runs it with the locking script, checking the hash; stage two parses the last push as the redeem script and runs it on the other pushes.
Examples
>>> from blockchainkit.crypto import public_key, sign >>> from blockchainkit.wallets import ( ... multisig_redeem_script, multisig_unlocking, p2sh_locking, p2sh_unlocking, verify_p2sh) >>> redeem = multisig_redeem_script(2, [public_key(k) for k in (3, 5, 7)]) >>> signatures = {0: sign(b"tx", 3, nonce=11), 2: sign(b"tx", 7, nonce=13)} >>> spend = p2sh_unlocking(redeem, multisig_unlocking(2, 3, signatures)) >>> verify_p2sh(spend, p2sh_locking(redeem), message=b"tx").valid True
ECDSA on secp256k1, the signature scheme Bitcoin and Ethereum use for accounts.
A signature on message hash z with key x and nonce k is
and it verifies when \((z s^{-1}) G + (r s^{-1}) Q\) has x coordinate
r. Unlike Schnorr’s \(s = k + c x\), the response multiplies the
secret nonce’s inverse by the key, so signatures by several parties do not
simply add: see blockchainkit.wallets.systems.threshold. Nonces come
from RFC 6979 by default, and s is normalized to the lower half of
[1, n), as Bitcoin requires since BIP146 to stop third parties from
changing a transaction’s ID by negating s.
- blockchainkit.wallets.systems.ecdsa.message_hash(message)[source]#
z: SHA-256 of the message as an integer modulo n.
- blockchainkit.wallets.systems.ecdsa.ecdsa_signature(z, private, nonce)[source]#
Assemble a signature from the hash, key and nonce:
s = k**-1 (z + r x), low s.- Parameters:
- Return type:
- blockchainkit.wallets.systems.ecdsa.ecdsa_sign(message, private, *, nonce=None)[source]#
Sign
messagewith ECDSA; the nonce defaults to RFC 6979’s.Examples
>>> from blockchainkit.crypto import public_key >>> from blockchainkit.wallets import ecdsa_sign, ecdsa_verify >>> signature = ecdsa_sign(b"pay bob 5", 7) >>> ecdsa_verify(b"pay bob 5", signature, public_key(7)) True
- Parameters:
- Return type:
- blockchainkit.wallets.systems.ecdsa.ecdsa_verify(message, signature, public)[source]#
Check an ECDSA signature; malformed keys or scalars return False.
Threshold ECDSA (Gennaro and Goldfeder, 2018).
A multisig script shows its policy on chain and costs one signature per signer. A threshold signature is a single ordinary signature under a single key, made by any t of n parties who each hold a Shamir share of a private key that nobody ever computes. For Schnorr this is easy, since responses add. ECDSA’s \(s = k^{-1}(z + r x)\) multiplies secrets, so the parties need products of values that are split between them.
Gennaro and Goldfeder’s protocol gets them with a two-party multiplicative-to-additive (MtA) conversion: parties holding a and b end with \(\alpha + \beta = ab\) without revealing a or b. With nonce shares \(k_i\), masks \(\gamma_i\) and Lagrange-weighted key shares \(w_i\), MtA gives additive shares of \(\delta = k\gamma\) and \(\sigma = kx\). Revealing \(\delta\) leaks nothing, because \(\gamma\) hides \(k\), and
which is an ECDSA signature with nonce \(k^{-1}\).
Teaching difference: the key is generated jointly with Feldman commitments
on secp256k1, but multiplicative_to_additive() is an ideal
functionality, a function that splits the product at random. The real
protocol runs MtA with Paillier encryption and zero-knowledge range proofs,
so that no party learns another’s inputs, and it checks every step to
catch a cheating party.
- blockchainkit.wallets.systems.threshold.threshold_keygen(threshold, parties, *, seed=0)[source]#
Generate a key jointly: every party deals a Shamir sharing of a random secret.
Party i picks a polynomial \(f_i\) of degree
threshold - 1, publishes \(a_{ik} G\) for its coefficients, and sends \(f_i(j)\) to party j. Party j’s share is \(\sum_i f_i(j)\); the joint key is \(\sum_i a_{i0} G\), and its private key \(\sum_i a_{i0}\) is never assembled.Examples
>>> from blockchainkit.wallets import threshold_keygen, verify_key_share >>> keys = threshold_keygen(2, 3, seed=1) >>> [verify_key_share(keys, i) for i in (1, 2, 3)] [True, True, True]
Feldman’s check of party
index’s share against every dealer’s commitments.
- blockchainkit.wallets.systems.threshold.lagrange_coefficient(index, signers)[source]#
Shamir share to additive share: \(\lambda_i = \prod_{j \ne i} j / (j - i)\).
- blockchainkit.wallets.systems.threshold.multiplicative_to_additive(a, b, rng)[source]#
The MtA ideal functionality: random
(alpha, beta)withalpha + beta = a b mod n.In Gennaro and Goldfeder’s protocol, the holder of a sends a under Paillier encryption; the holder of b computes
E(a b - beta)by homomorphic operations for a random beta, and returns it.
- blockchainkit.wallets.systems.threshold.threshold_sign(message, keys, signers, *, seed=0)[source]#
Sign
messagewith the shares ofsigners, at leastkeys.thresholdof them.- Parameters:
message (
bytes) – The message; it is hashed with SHA-256, as byecdsa_sign().keys (
blockchainkit.wallets.core.base.KeyShares) – Fromthreshold_keygen().signers (
collections.abc.Sequenceofint) – Distinct party indices.seed (
int) – Seeds every party’s nonce share, mask and MtA randomness.
- Returns:
Its signature verifies with
ecdsa_verify()underkeys.public.- Return type:
Examples
>>> from blockchainkit.wallets import ecdsa_verify, threshold_keygen, threshold_sign >>> keys = threshold_keygen(2, 3, seed=1) >>> signing = threshold_sign(b"pay bob 5", keys, [1, 3], seed=2) >>> ecdsa_verify(b"pay bob 5", signing.signature, keys.public), signing.conversions (True, 4)
A single seed phrase is a single point of failure twice over: lose it and the coins are gone, let it be copied and they are stolen. A social-recovery wallet is a contract with one signing key for daily use and a set of guardians, friends, devices or institutions, any m of whom can replace the signing key. The owner need back up nothing, and a thief with the signing key alone can be locked out by the guardians.
With n guardians, each independently lost with probability \(p_l\) or corrupt with probability \(p_c\), recovery fails when fewer than m are honest, and the wallet is stolen when m are corrupt:
Raising m trades theft for lockout. Changing the guardians is delayed, so a thief who takes the signing key cannot replace them before they act.
Teaching differences: guardians are accounts that send transactions, not signatures collected off chain, and the wallet holds only ether.
- blockchainkit.wallets.systems.social_recovery.GUARDIAN_DELAY = 100#
Blocks between proposing new guardians and being able to apply them.
- Type:
- blockchainkit.wallets.systems.social_recovery.recovery_risk(guardians, threshold, lost, corrupt)[source]#
The lockout and theft probabilities of m-of-n guardians failing independently.
- Parameters:
- Return type:
Examples
>>> from blockchainkit.wallets import recovery_risk >>> risk = recovery_risk(5, 3, lost=0.1, corrupt=0.05) >>> round(risk.lockout, 4), round(risk.theft, 5) (0.0266, 0.00116)
- class blockchainkit.wallets.systems.social_recovery.SocialRecoveryWallet[source]#
Bases:
ContractA wallet with one owner and m-of-n guardians who can replace the owner.
- Parameters:
owner (
str) – The account allowed to spend.guardians (
collections.abc.Sequenceofstr) – Accounts allowed to vote for a new owner.threshold (
int) – Votes needed.
- layout: ClassVar[tuple[str, ...]] = ('owner', 'guardians', 'threshold', 'votes', 'epoch', 'pending')#
- support_recovery(new_owner)[source]#
A guardian’s vote to make
new_ownerthe owner; returns True once it takes effect.Votes count toward the current epoch only: a recovery, or a change of guardians, starts a new one.
- propose_guardians(guardians, threshold)[source]#
The owner proposes new guardians; they apply after
GUARDIAN_DELAYblocks.
Account abstraction without a consensus change (Buterin et al., ERC-4337, 2021).
On Ethereum an account is either a key pair, which can start transactions but whose rules are fixed (one ECDSA key, pay gas in ether), or a contract, whose rules are code but which cannot start a transaction. ERC-4337 lets a contract account act like a key pair without changing the protocol. The owner signs a user operation and sends it to an alternative mempool. A bundler collects operations and submits them in one ordinary transaction to a singleton EntryPoint contract, which for each operation
asks the account to validate it, checking the signature and nonce by whatever rule the account’s code chooses, and to pay its fee;
then runs it, and pays the bundler the fees collected.
A failing validation reverts the whole bundle, at the bundler’s cost, so a bundler first simulates each operation on a copy of the state and drops those that fail. A failing execution only marks its operation failed: the fee is still paid. The account’s rule can be anything: key rotation, multisig, social recovery, session keys, or another signature scheme. The operation hash binds the EntryPoint and the chain,
so a signed operation cannot be replayed elsewhere.
Teaching differences: the fee is a fixed amount rather than gas used times a gas price, there are no paymasters, aggregators or factories, accounts sign with this package’s Schnorr signatures, and the validation-phase restrictions on storage access are not enforced.
- blockchainkit.wallets.systems.account_abstraction.user_operation_hash(operation, entry_point, chain_id='blockchainkit-demo')[source]#
The hash an owner signs: every field but the signature, with the EntryPoint and chain.
- Parameters:
operation (UserOperation)
entry_point (str)
chain_id (str)
- Return type:
- blockchainkit.wallets.systems.account_abstraction.sign_user_operation(operation, private, entry_point, chain_id='blockchainkit-demo')[source]#
Return
operationsigned byprivate, with an RFC 6979 nonce.- Parameters:
operation (UserOperation)
private (int)
entry_point (str)
chain_id (str)
- Return type:
- class blockchainkit.wallets.systems.account_abstraction.EntryPoint[source]#
Bases:
ContractThe singleton that validates and runs user operations, holding each account’s deposit.
- deposit_to(account)[source]#
Add the ether sent to
account’s deposit, from which its fees are paid.- Parameters:
account (str)
- Return type:
None
- handle_ops(operations, beneficiary)[source]#
Validate every operation, then run each, then pay the fees to
beneficiary.Returns whether each operation’s execution succeeded. Any failed validation reverts the whole call.
- Parameters:
operations (Sequence[UserOperation])
beneficiary (str)
- Return type:
- class blockchainkit.wallets.systems.account_abstraction.SmartAccount[source]#
Bases:
ContractA contract account owned by a Schnorr key, which it can rotate.
- Parameters:
- validate_user_operation(operation, digest, missing)[source]#
Check the nonce and the owner’s signature, and top up the deposit by
missing.- Parameters:
operation (UserOperation)
digest (bytes)
missing (int)
- Return type:
None
- execute(to, amount, function=None, *args)[source]#
Call
towithamountof ether; only through the EntryPoint.
- blockchainkit.wallets.systems.account_abstraction.simulate_validation(world, entry_point, operation, bundler='bundler')[source]#
What a bundler checks before including
operation: does it validate, on a fork?- Parameters:
world (World)
entry_point (str)
operation (UserOperation)
bundler (str)
- Return type:
- blockchainkit.wallets.systems.account_abstraction.bundle(world, entry_point, operations, bundler='bundler')[source]#
The operations a bundler keeps: those that validate when simulated alone.
Examples
>>> from blockchainkit.contracts import World >>> from blockchainkit.crypto import public_key >>> from blockchainkit.wallets import ( ... EntryPoint, SmartAccount, UserOperation, bundle, sign_user_operation) >>> world = World() >>> entry = world.deploy("deployer", EntryPoint) >>> account = world.deploy("deployer", SmartAccount, entry, public_key(7)) >>> world.fund("alice", 100) >>> world.transact("alice", account, value=100).success True >>> op = UserOperation(account, 0, "execute", ("bob", 10), fee=2) >>> good, forged = sign_user_operation(op, 7, entry), sign_user_operation(op, 8, entry) >>> bundle(world, entry, [good, forged]) == (good,) True
- Parameters:
world (World)
entry_point (str)
operations (Sequence[UserOperation])
bundler (str)
- Return type:
tuple[UserOperation, …]
Plotting#
Plotting helpers for blockchainkit.wallets: error detection, memory-hardness, and recovery.
- blockchainkit.wallets.visualizers.plots.plot_detection_rates(rates, *, ax=None)[source]#
Draw the share of single errors and adjacent swaps each check-digit scheme catches.
- Parameters:
rates (
collections.abc.Sequenceofblockchainkit.wallets.core.base.DetectionRates) – Fromdetection_rates().ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.wallets.visualizers.plots.plot_memory_tradeoff(costs, *, ax=None)[source]#
Draw ROMix’s time against the memory kept, with the curve of constant memory times time.
- Parameters:
costs (
collections.abc.Sequenceofblockchainkit.wallets.core.base.RomixCost) – Fromromix()with differentkeep_every, the samenandr.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type:
- blockchainkit.wallets.visualizers.plots.plot_recovery_risk(guardians, lost, corrupt, *, ax=None)[source]#
Draw lockout and theft probabilities against the guardian threshold m.
- Parameters:
guardians (
int) – n, the number of guardians.lost (
float) – Each guardian’s probability of being unreachable or corrupt.corrupt (
float) – Each guardian’s probability of being unreachable or corrupt.ax (
matplotlib.axes.Axes, optional) – Axes to draw on; a new figure is created if omitted.
- Return type: