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: object

What a BCH decoder made of a received word.

Variables:
  • message (tuple of int) – The k message bits read from the corrected word.

  • codeword (tuple of int) – The n-bit codeword after correction (the received word if nothing could be corrected).

  • errors (tuple of int) – 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:
message: tuple[int, ...]#
codeword: tuple[int, ...]#
errors: tuple[int, ...]#
corrected: bool#
class blockchainkit.wallets.core.base.DetectionRates(scheme, single_errors, transpositions)[source]#

Bases: object

The share of typing errors a check-digit scheme catches.

Variables:
  • scheme (str) – The scheme’s name.

  • single_errors (float) – Share of single wrong digits detected.

  • transpositions (float) – Share of swaps of two adjacent, different digits detected.

Parameters:
scheme: str#
single_errors: float#
transpositions: float#
class blockchainkit.wallets.core.base.CrackResult(secret, guesses, hash_calls)[source]#

Bases: object

An exhaustive search for a short secret behind a stretched key.

Variables:
  • secret (str or None) – The secret found, or None if no candidate matched.

  • guesses (int) – Candidates tried.

  • hash_calls (int) – Calls to the underlying keyed hash (HMAC), the attacker’s real cost.

Parameters:
  • secret (str | None)

  • guesses (int)

  • hash_calls (int)

secret: str | None#
guesses: int#
hash_calls: int#
class blockchainkit.wallets.core.base.RomixCost(n, r, stored, block_mixes)[source]#

Bases: object

What one run of scrypt’s ROMix cost, with some of its table forgotten.

Variables:
  • n (int) – The cost parameter: the table has n entries.

  • r (int) – The block size parameter: each entry is 128 * r bytes.

  • stored (int) – Table entries kept in memory.

  • block_mixes (int) – Calls to BlockMix, the unit of time.

Parameters:
n: int#
r: int#
stored: int#
block_mixes: int#
property memory: int#

Bytes of table kept.

class blockchainkit.wallets.core.base.ExtendedKey(chain_code, public, private, depth, index, parent_fingerprint)[source]#

Bases: object

A 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.

  • public (tuple of int) – The public key point.

  • private (int or None) – 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 least 2**31 if hardened.

  • parent_fingerprint (bytes) – The first 4 bytes of the parent’s HASH160; zeros for the master.

Parameters:
chain_code: bytes#
public: tuple[int, int]#
private: int | None#
depth: int#
index: int#
parent_fingerprint: bytes#
property hardened: bool#

True if this key was derived with a hardened index.

property is_private: bool#

True if the private key is known.

class blockchainkit.wallets.core.base.AccountScan(accounts, found, derived)[source]#

Bases: object

The outcome of BIP44 account discovery.

Variables:
  • accounts (int) – Accounts found with at least one used address.

  • found (tuple of str) – Paths of the used addresses found, in scan order.

  • derived (int) – Addresses derived and checked.

Parameters:
accounts: int#
found: tuple[str, ...]#
derived: int#
class blockchainkit.wallets.core.base.BrainDrain(wallets, weak, drained, delays, hauls)[source]#

Bases: object

A 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 (tuple of int) – For each drained wallet, the blocks between deposit and theft.

  • hauls (tuple of int) – Wallets taken by each thief, in the order the thieves were given.

Parameters:
wallets: int#
weak: int#
drained: int#
delays: tuple[int, ...]#
hauls: tuple[int, ...]#
class blockchainkit.wallets.core.base.Bech32Decoding(hrp, data, encoding)[source]#

Bases: object

A decoded Bech32 or Bech32m string.

Variables:
  • hrp (str) – The human-readable part, in lower case ("bc" for Bitcoin).

  • data (tuple of int) – The 5-bit values of the data part, checksum removed.

  • encoding (str) – "bech32" or "bech32m": which checksum constant matched.

Parameters:
hrp: str#
data: tuple[int, ...]#
encoding: str#
class blockchainkit.wallets.core.base.WitnessProgram(version, program)[source]#

Bases: object

A segregated-witness output: a version and a program.

Variables:
  • version (int) – 0 to 16; version 0 is P2WPKH or P2WSH, version 1 Taproot.

  • program (bytes) – 2 to 40 bytes: a key or script hash, or a Taproot key.

Parameters:
version: int#
program: bytes#
class blockchainkit.wallets.core.base.EcdsaSignature(r, s)[source]#

Bases: object

An ECDSA signature: the x coordinate of the nonce point, and the response.

Variables:

s (r,) – Both in [1, n); s is normalized to the lower half, as Bitcoin requires.

Parameters:
r: int#
s: int#
class blockchainkit.wallets.core.base.KeyShares(public, threshold, shares, commitments)[source]#

Bases: object

A secp256k1 key generated jointly, held as Shamir shares.

Variables:
  • public (tuple of int) – The joint public key. Its private key was never computed by anyone.

  • threshold (int) – Shares needed to sign.

  • shares (tuple of tuple) – Each party’s (index, share).

  • commitments (tuple of tuple) – Each dealer’s Feldman commitments to its polynomial’s coefficients.

Parameters:
public: tuple[int, int]#
threshold: int#
shares: tuple[tuple[int, int], ...]#
commitments: tuple[tuple[tuple[int, int], ...], ...]#
class blockchainkit.wallets.core.base.ThresholdSigning(signature, signers, conversions)[source]#

Bases: object

An 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.

  • signers (tuple of int) – The parties that took part.

  • conversions (int) – Multiplicative-to-additive conversions run, each a two-party protocol: the price of ECDSA’s nonlinearity.

Parameters:
signature: EcdsaSignature#
signers: tuple[int, ...]#
conversions: int#
class blockchainkit.wallets.core.base.RecoveryRisk(lockout, theft)[source]#

Bases: object

How a guardian scheme fails: by locking its owner out, or by robbing them.

Variables:
  • lockout (float) – Probability that fewer than the threshold of guardians are honest and reachable, so that a lost key cannot be replaced.

  • theft (float) – Probability that at least the threshold of guardians are corrupt, and can take the wallet.

Parameters:
lockout: float#
theft: float#
class blockchainkit.wallets.core.base.UserOperation(sender, nonce, function, args, fee, signature=None)[source]#

Bases: object

An 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.SchnorrSignature or None) – The owner’s signature over the operation’s hash; None until signed.

Parameters:
sender: str#
nonce: int#
function: str#
args: tuple[Any, ...]#
fee: int#
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:

dict

class blockchainkit.wallets.utils.galois.GaloisField(m)[source]#

Bases: object

GF(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)

m: int#
property order: int#

the multiplicative group’s order.

Type:

The number of nonzero elements, 2**m - 1

power(exponent)[source]#

alpha ** exponent, for any integer exponent.

Parameters:

exponent (int)

Return type:

int

log(value)[source]#

The exponent e with alpha ** e == value, for a nonzero element.

Parameters:

value (int)

Return type:

int

multiply(left, right)[source]#

Multiply two field elements.

Parameters:
Return type:

int

inverse(value)[source]#

The multiplicative inverse of a nonzero element.

Parameters:

value (int)

Return type:

int

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:
Return type:

int

class blockchainkit.wallets.systems.bch.BCHCode(m, t)[source]#

Bases: object

A narrow-sense binary BCH code of length 2**m - 1 correcting t errors.

Parameters:
  • m (int) – Field degree, 3 to 8, so n is 7 to 255.

  • t (int) – Errors to correct; the designed distance is 2t + 1.

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)
m: int#
t: int#
galois: GaloisField#
generator: int#
property n: int#

Codeword length in bits, 2**m - 1.

property k: int#

The message length in bits, n minus the generator’s degree.

property designed_distance: int#

2t + 1, a lower bound on the distance between two codewords.

encode(message)[source]#

Append n - k check bits to k message 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.

Parameters:

message (Sequence[int])

Return type:

tuple[int, …]

is_codeword(word)[source]#

True if g(x) divides the word.

Parameters:

word (Sequence[int])

Return type:

bool

syndromes(word)[source]#

S_j = r(alpha**j) for j = 1, …, 2t; all zero for a codeword.

Parameters:

word (Sequence[int])

Return type:

tuple[int, …]

decode(word)[source]#

Correct up to t flipped bits with Berlekamp-Massey and Chien’s search.

Parameters:

word (Sequence[int])

Return type:

BCHDecoding

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:

\[\sigma^{0}(a_0) * \sigma^{1}(a_1) * \cdots * \sigma^{k}(a_k) = 0 .\]
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)
Parameters:

number (str)

Return type:

str

blockchainkit.wallets.systems.check_digits.verhoeff_valid(number)[source]#

True if number ends with its correct Verhoeff check digit.

Parameters:

number (str)

Return type:

bool

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'
Parameters:

number (str)

Return type:

str

blockchainkit.wallets.systems.check_digits.luhn_valid(number)[source]#

True if number ends with its correct Luhn check digit.

Parameters:

number (str)

Return type:

bool

blockchainkit.wallets.systems.check_digits.sum_digit(number)[source]#

The digit that makes the digit sum a multiple of 10: catches no swap.

Parameters:

number (str)

Return type:

str

blockchainkit.wallets.systems.check_digits.sum_valid(number)[source]#

True if the digits of number add up to a multiple of 10.

Parameters:

number (str)

Return type:

bool

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:

dict

blockchainkit.wallets.systems.check_digits.detection_rates(scheme, length=3)[source]#

Measure a scheme on every number of length digits, 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:

DetectionRates

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'
Parameters:

data (bytes)

Return type:

bytes

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
Parameters:

data (bytes)

Return type:

bytes

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'
Parameters:

public (tuple[int, int])

Return type:

bytes

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 a is a ** ((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:

tuple[int, int]

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

\[2^{-32} \approx 2.3 \times 10^{-10},\]

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:

str

blockchainkit.wallets.systems.base58.P2PKH_VERSION = 0#

Version byte of a pay-to-public-key-hash address (starts with 1).

Type:

int

blockchainkit.wallets.systems.base58.P2SH_VERSION = 5#

Version byte of a pay-to-script-hash address (starts with 3).

Type:

int

blockchainkit.wallets.systems.base58.base58_encode(data)[source]#

Write bytes in base 58, one 1 per leading zero byte.

Examples

>>> from blockchainkit.wallets import base58_encode
>>> base58_encode(b"hello world")
'StV1DL6CwTryKyV'
Parameters:

data (bytes)

Return type:

str

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:

bytes

blockchainkit.wallets.systems.base58.base58check_encode(version, payload)[source]#

Base58Check: the version byte, the payload, and a 4-byte double-SHA-256 checksum.

Parameters:
Return type:

str

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:

tuple[int, bytes]

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'
Parameters:
Return type:

str

blockchainkit.wallets.systems.base58.is_valid_address(text)[source]#

True if text is a Base58Check string with a 20-byte payload and a known version.

Parameters:

text (str)

Return type:

bool

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,

\[\mathrm{polymod}(\mathrm{expand}(hrp) \,\|\, data \,\|\, checksum) = 1 .\]

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:

str

blockchainkit.wallets.systems.bech32.BECH32_CONSTANTS = {'bech32': 1, 'bech32m': 734539939}#

The value a valid checksum leaves, for each encoding.

Type:

dict

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.

Parameters:

values (Sequence[int])

Return type:

int

blockchainkit.wallets.systems.bech32.bech32_encode(hrp, data, encoding='bech32')[source]#

Write hrp, 1, the 5-bit data and a 6-character checksum.

Examples

>>> from blockchainkit.wallets import bech32_encode
>>> bech32_encode("a", [])
'a12uel5l'
>>> bech32_encode("a", [], "bech32m")
'a1lqfn3a'
Parameters:
Return type:

str

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:

Bech32Decoding

blockchainkit.wallets.systems.bech32.convert_bits(data, source, target, pad=True)[source]#

Regroup a sequence of source-bit values into target-bit values.

With pad, the last group is filled with zero bits; without, leftover bits must be fewer than target and zero.

Parameters:
Return type:

tuple[int, …]

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'
Parameters:
Return type:

str

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:

WitnessProgram

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'
Parameters:
Return type:

str

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

\[T_i = U_1 \oplus U_2 \oplus \cdots \oplus U_c, \quad U_1 = \mathrm{PRF}(P, S \,\|\, \mathrm{INT}(i)), \quad U_j = \mathrm{PRF}(P, U_{j-1}),\]

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.

Type:

tuple of str

blockchainkit.wallets.systems.stretching.pbkdf2(password, salt, iterations, length, hash_name='sha256')[source]#

Derive length bytes from a password with PBKDF2-HMAC (RFC 8018, section 5.2).

Parameters:
  • password (bytes) – The secret and a per-user random value.

  • salt (bytes) – The secret and a per-user random value.

  • iterations (int) – c, the number of HMAC calls per output block.

  • length (int) – Output bytes.

  • hash_name (str) – "sha1", "sha256" or "sha512".

Return type:

bytes

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 digits digits until one stretches to key.

The attacker’s cost is guesses * iterations HMAC 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)
Parameters:
Return type:

CrackResult

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:

\[V_i = \mathrm{BlockMix}^{\,i}(B), \qquad X \leftarrow \mathrm{BlockMix}(X \oplus V_{\mathrm{Integerify}(X) \bmod N}).\]

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
Parameters:

block (bytes)

Return type:

bytes

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.

Parameters:

block (bytes)

Return type:

bytes

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_j is recomputed from the stored V_{j - j mod k}. The output is the same for every keep_every; only the cost changes.

Parameters:
  • block (bytes) – 128 * r bytes.

  • n (int) – The cost parameter N, a power of two above 1.

  • keep_every (int) – k: 1 is scrypt as specified; N stores only the first entry.

Returns:

The mixed block, and a RomixCost.

Return type:

tuple

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:

bytes

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,

\[k = \mathrm{SHA256}(\text{passphrase}),\]

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.

Type:

tuple of str

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'
Parameters:

passphrase (str)

Return type:

int

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.

Parameters:

passphrase (str)

Return type:

str

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'
Parameters:
Return type:

str | None

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 from PHRASEBOOK; the others use a random 128-bit key written as hex.

  • thieves (collections.abc.Sequence of tuple) – 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:

blockchainkit.wallets.core.base.BrainDrain

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

\[I = \mathrm{HMAC\text{-}SHA512}(c,\ \mathrm{data}), \qquad k_i = I_L + k \bmod n, \qquad c_i = I_R,\]

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:

dict

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:

ExtendedKey

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 HASH160 of the key’s compressed public key.

Parameters:

key (ExtendedKey)

Return type:

bytes

blockchainkit.wallets.systems.hd.derive_private(parent, index)[source]#

BIP32’s CKDpriv: child index of an extended private key.

Indices from 2**31 (written i') are hardened.

Parameters:
Return type:

ExtendedKey

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 – index is hardened: that needs the private key.

Parameters:
Return type:

ExtendedKey

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:

ExtendedKey

blockchainkit.wallets.systems.hd.serialize_extended_key(key)[source]#

The 78-byte serialization in Base58Check: xprv... or xpub....

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:

str

blockchainkit.wallets.systems.hd.parse_extended_key(text)[source]#

Read an xprv or xpub string back into an extended key.

Raises:

ValueError – A bad checksum, length, version or key.

Parameters:

text (str)

Return type:

ExtendedKey

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:
Return type:

int

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:

\[\text{words} = \frac{ENT + ENT/32}{11}, \qquad 128 \text{ bits} \to 12 \text{ 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.

Type:

tuple of str

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']
Parameters:

entropy (bytes)

Return type:

str

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:

bytes

blockchainkit.wallets.systems.mnemonics.is_valid_mnemonic(mnemonic)[source]#

True if every word is in the list and the checksum holds.

Parameters:

mnemonic (str)

Return type:

bool

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'
Parameters:
  • mnemonic (str)

  • passphrase (str)

Return type:

bytes

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,

\[m / 44' / \text{coin}' / \text{account}' / \text{change} / \text{index},\]

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:

int

blockchainkit.wallets.systems.paths.parse_path(path)[source]#

Turn "m/44'/0'/0'/0/5" into child indices, hardened ones offset by 2**31.

' and h both mark a hardened index.

Examples

>>> from blockchainkit.wallets import parse_path
>>> parse_path("m/44'/0'/0'/0/5")
(2147483692, 2147483648, 2147483648, 0, 5)
Parameters:

path (str)

Return type:

tuple[int, …]

blockchainkit.wallets.systems.paths.format_path(indices)[source]#

Write child indices as a path, hardened ones with '.

Parameters:

indices (Sequence[int])

Return type:

str

blockchainkit.wallets.systems.paths.derive_path(key, path)[source]#

Derive the key at path below key.

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:
Return type:

ExtendedKey

blockchainkit.wallets.systems.paths.bip44_path(coin, account, change, index)[source]#

The BIP44 path m/44'/coin'/account'/change/index.

Parameters:
Return type:

str

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:
Return type:

AccountScan

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:

\[\text{locking} = \texttt{OP\_HASH256}\ \langle H(\text{redeem}) \rangle\ \texttt{OP\_EQUAL}\]

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:

dict

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) or OP_PUSHDATA2 (0x4d). An empty push is 0x00, the same byte as OP_FALSE, which pushes the empty string.

Parameters:

script (Sequence[bytes | str])

Return type:

bytes

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:

tuple[bytes | str, …]

blockchainkit.wallets.systems.p2sh.multisig_redeem_script(threshold, publics)[source]#

An m-of-n redeem script: one OP_IF branch per set of threshold keys.

Each branch checks its keys in turn, <K1> OP_CHECKSIG OP_VERIFY ... <Km> OP_CHECKSIG, and the last branch needs no OP_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)
Parameters:
Return type:

tuple[bytes | str, …]

blockchainkit.wallets.systems.p2sh.multisig_unlocking(threshold, keys, signatures)[source]#

The pushes that satisfy multisig_redeem_script(): signatures, then branch choices.

Parameters:
Return type:

tuple[bytes, …]

blockchainkit.wallets.systems.p2sh.p2sh_locking(redeem)[source]#

Lock a coin to a redeem script’s hash: OP_HASH256 <hash> OP_EQUAL.

Parameters:

redeem (Sequence[bytes | str])

Return type:

tuple[bytes | str, …]

blockchainkit.wallets.systems.p2sh.p2sh_unlocking(redeem, pushes)[source]#

The redeem script’s own unlocking pushes, followed by the serialized redeem script.

Parameters:
Return type:

tuple[bytes, …]

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_HASH256 instead.

Parameters:

redeem (Sequence[bytes | str])

Return type:

str

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
Parameters:
Return type:

ScriptResult

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

\[R = kG, \qquad r = R_x \bmod n, \qquad s = k^{-1}(z + r x) \bmod n,\]

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.

Parameters:

message (bytes)

Return type:

int

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:

EcdsaSignature

blockchainkit.wallets.systems.ecdsa.ecdsa_sign(message, private, *, nonce=None)[source]#

Sign message with 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:

EcdsaSignature

blockchainkit.wallets.systems.ecdsa.ecdsa_verify(message, signature, public)[source]#

Check an ECDSA signature; malformed keys or scalars return False.

Parameters:
Return type:

bool

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

\[R = \delta^{-1} \sum_i \gamma_i G = k^{-1} G, \qquad s = \sum_i (z k_i + r \sigma_i) = k (z + r x),\]

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]
Parameters:
Return type:

KeyShares

blockchainkit.wallets.systems.threshold.verify_key_share(keys, index)[source]#

Feldman’s check of party index’s share against every dealer’s commitments.

Parameters:
Return type:

bool

blockchainkit.wallets.systems.threshold.lagrange_coefficient(index, signers)[source]#

Shamir share to additive share: \(\lambda_i = \prod_{j \ne i} j / (j - i)\).

Parameters:
Return type:

int

blockchainkit.wallets.systems.threshold.multiplicative_to_additive(a, b, rng)[source]#

The MtA ideal functionality: random (alpha, beta) with alpha + 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.

Parameters:
Return type:

tuple[int, int]

blockchainkit.wallets.systems.threshold.threshold_sign(message, keys, signers, *, seed=0)[source]#

Sign message with the shares of signers, at least keys.threshold of them.

Parameters:
Returns:

Its signature verifies with ecdsa_verify() under keys.public.

Return type:

blockchainkit.wallets.core.base.ThresholdSigning

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)

Social-recovery wallets (Buterin, 2021).

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:

\[P(\text{theft}) = \sum_{j \ge m} \binom{n}{j} p_c^{\,j} (1 - p_c)^{n-j} .\]

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:

int

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:
  • guardians (int) – n and m.

  • threshold (int) – n and m.

  • lost (float) – Each guardian’s probability of being unreachable, or of colluding against the owner; the rest are honest.

  • corrupt (float) – Each guardian’s probability of being unreachable, or of colluding against the owner; the rest are honest.

Return type:

RecoveryRisk

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: Contract

A 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.Sequence of str) – Accounts allowed to vote for a new owner.

  • threshold (int) – Votes needed.

layout: ClassVar[tuple[str, ...]] = ('owner', 'guardians', 'threshold', 'votes', 'epoch', 'pending')#
constructor(owner, guardians, threshold)[source]#
Parameters:
Return type:

None

receive()[source]#
Return type:

None

owner()[source]#

The account that may spend.

Return type:

str

execute(to, amount)[source]#

Pay amount to to; only the owner may.

Parameters:
Return type:

None

support_recovery(new_owner)[source]#

A guardian’s vote to make new_owner the 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.

Parameters:

new_owner (str)

Return type:

bool

propose_guardians(guardians, threshold)[source]#

The owner proposes new guardians; they apply after GUARDIAN_DELAY blocks.

Parameters:
Return type:

None

apply_guardians()[source]#

Install the proposed guardians once the delay has passed.

Return type:

None

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

  1. asks the account to validate it, checking the signature and nonce by whatever rule the account’s code chooses, and to pay its fee;

  2. 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,

\[h = \mathrm{SHA256}(\text{domain} \,\|\, \text{EntryPoint} \,\|\, \text{chain} \,\|\, \text{operation}),\]

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:
Return type:

bytes

blockchainkit.wallets.systems.account_abstraction.sign_user_operation(operation, private, entry_point, chain_id='blockchainkit-demo')[source]#

Return operation signed by private, with an RFC 6979 nonce.

Parameters:
Return type:

UserOperation

class blockchainkit.wallets.systems.account_abstraction.EntryPoint[source]#

Bases: Contract

The singleton that validates and runs user operations, holding each account’s deposit.

layout: ClassVar[tuple[str, ...]] = ('deposits',)#
receive()[source]#
Return type:

None

deposit_to(account)[source]#

Add the ether sent to account’s deposit, from which its fees are paid.

Parameters:

account (str)

Return type:

None

balance_of(account)[source]#

account’s deposit.

Parameters:

account (str)

Return type:

int

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:
Return type:

tuple[bool, …]

class blockchainkit.wallets.systems.account_abstraction.SmartAccount[source]#

Bases: Contract

A contract account owned by a Schnorr key, which it can rotate.

Parameters:
  • entry_point (str) – The only caller it accepts operations from.

  • owner (tuple of int) – The owner’s public key.

layout: ClassVar[tuple[str, ...]] = ('entry_point', 'owner', 'nonce')#
constructor(entry_point, owner)[source]#
Parameters:
Return type:

None

receive()[source]#
Return type:

None

nonce()[source]#

The nonce the next operation must carry.

Return type:

int

validate_user_operation(operation, digest, missing)[source]#

Check the nonce and the owner’s signature, and top up the deposit by missing.

Parameters:
Return type:

None

execute(to, amount, function=None, *args)[source]#

Call to with amount of ether; only through the EntryPoint.

Parameters:
Return type:

Any

execute_batch(calls)[source]#

Several payments in one operation, all or none.

Parameters:

calls (Sequence[tuple[str, int]])

Return type:

None

rotate_owner(owner)[source]#

Replace the owner’s key; operations signed by the old key then fail.

Parameters:

owner (tuple[int, int])

Return type:

None

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:
Return type:

bool

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:
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:
Return type:

matplotlib.axes.Axes

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:
Return type:

matplotlib.axes.Axes

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:

matplotlib.axes.Axes