blockchainkit.crypto#

Cryptographic foundations: hashing, public keys, sharing, curves, and proofs.

Hashing and commitments, Diffie-Hellman and RSA, blind signatures, secret sharing, elliptic curves, and Schnorr proofs and signatures.

Every public name below is re-exported by the subpackage: import it as bk.crypto.<name>. The plotting helpers are the exception: import them explicitly from blockchainkit.crypto.visualizers, which loads Matplotlib.

Types and results#

Shared types and result containers for blockchainkit.crypto.

blockchainkit.crypto.core.base.Point = tuple[int, int] | None#

An affine curve point (x, y), or None for the point at infinity.

class blockchainkit.crypto.core.base.SchnorrSignature(commitment, response)[source]#

Bases: object

A finite commitment point R and response scalar s.

Parameters:
commitment: tuple[int, int]#
response: int#
class blockchainkit.crypto.core.base.DiscreteLogResult(exponent, group_operations)[source]#

Bases: object

An exponent x with base**x = target, and the work spent finding it.

Variables:
  • exponent (int) – The smallest non-negative solution.

  • group_operations (int) – Group multiplications in the baby-step and giant-step tables, the dominant cost.

Parameters:
  • exponent (int)

  • group_operations (int)

exponent: int#
group_operations: int#
class blockchainkit.crypto.core.base.CollisionResult(first, second, digest, trials)[source]#

Bases: object

Two distinct inputs whose truncated hashes agree.

Variables:
  • second (first,) – The colliding inputs.

  • digest (int) – Their shared truncated hash.

  • trials (int) – Hashes computed before the collision appeared.

Parameters:
first: bytes#
second: bytes#
digest: int#
trials: int#
class blockchainkit.crypto.core.base.PuzzleSolution(puzzle_id, key, trials)[source]#

Bases: object

The contents of one solved Merkle puzzle, and the trials it took.

Variables:
  • puzzle_id (bytes) – The identifier the solver announces publicly.

  • key (bytes) – The session key, which stays secret.

  • trials (int) – Weak keys tried before the puzzle opened.

Parameters:
puzzle_id: bytes#
key: bytes#
trials: int#
class blockchainkit.crypto.core.base.LamportKeyPair(private, public)[source]#

Bases: object

A Lamport one-time key: 256 pairs of secret preimages and their hashes.

Variables:
  • private (tuple) – 256 pairs of bytes: for each digest bit, the preimage revealed when that bit is 0 or 1.

  • public (tuple) – 256 pairs of bytes: the SHA-256 of each private preimage.

Parameters:
private: tuple[tuple[bytes, bytes], ...]#
public: tuple[tuple[bytes, bytes], ...]#
class blockchainkit.crypto.core.base.FeldmanShares(shares, commitments)[source]#

Bases: object

Shamir shares plus public commitments that let each holder check theirs.

Variables:
  • shares (tuple) – (x, f(x)) pairs of int, points over the integers modulo the group order q.

  • commitments (tuple of int) – g**a_j mod p for each polynomial coefficient a_j.

Parameters:
shares: tuple[tuple[int, int], ...]#
commitments: tuple[int, ...]#

Constructions and protocols#

The one-time pad (Vernam 1917; Shannon 1949): XOR with a key as long as the message.

blockchainkit.crypto.systems.one_time_pad.xor_bytes(left, right)[source]#

Return the bytewise XOR of two equally long byte strings.

>>> from blockchainkit.crypto import xor_bytes
>>> xor_bytes(b"\x0f", b"\xff")
b'\xf0'
Parameters:
Return type:

bytes

blockchainkit.crypto.systems.one_time_pad.one_time_pad(message, key)[source]#

Encrypt or decrypt with a one-time pad: message XOR key.

The same call decrypts, because XOR is its own inverse. Shannon proved the pad perfectly secret when the key is uniformly random, as long as the message, and never reused: every plaintext of that length is then equally likely given the ciphertext. Reusing a key leaks m1 XOR m2.

Parameters:
  • message (bytes) – Plaintext (or ciphertext, to decrypt).

  • key (bytes) – A key exactly as long as the message.

Return type:

bytes

Examples

>>> from blockchainkit.crypto import one_time_pad
>>> ciphertext = one_time_pad(b"hi", b"\x01\x02")
>>> one_time_pad(ciphertext, b"\x01\x02")
b'hi'

SHA-256 hashing, bit-level digest comparison, and birthday collisions.

blockchainkit.crypto.systems.hashing.sha256(data)[source]#

Return the 32-byte SHA-256 digest of bytes.

>>> from blockchainkit.crypto import sha256
>>> sha256(b"abc").hex()
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'
Parameters:

data (bytes)

Return type:

bytes

blockchainkit.crypto.systems.hashing.hash256(data)[source]#

Return SHA-256(SHA-256(data)), as used in Bitcoin hashing.

Parameters:

data (bytes)

Return type:

bytes

blockchainkit.crypto.systems.hashing.hamming_distance(left, right)[source]#

Count differing bits between equally long byte strings.

>>> from blockchainkit.crypto import hamming_distance
>>> hamming_distance(bytes([0]), bytes([7]))
3
Parameters:
Return type:

int

blockchainkit.crypto.systems.hashing.truncated_hash(data, bits)[source]#

Return the top bits bits of SHA-256(data) as an integer.

Truncation makes collisions findable, to measure the birthday bound.

>>> from blockchainkit.crypto import truncated_hash
>>> truncated_hash(b"abc", 8) == sha256(b"abc")[0]
True
Parameters:
Return type:

int

blockchainkit.crypto.systems.hashing.find_collision(bits, *, prefix=b'', max_trials=4194304)[source]#

Find two inputs whose bits-bit truncated hashes agree.

Hashes prefix + counter for counter = 0, 1, 2, … and remembers every truncated digest. By the birthday paradox a repeat is expected after about sqrt(pi/2 * 2**bits) trials, far fewer than the 2**bits needed to hit one given digest (Yuval, 1979).

Parameters:
  • bits (int) – Output size to attack, between 1 and 48.

  • prefix (bytes) – Shared prefix of every candidate input.

  • max_trials (int) – Search bound.

Raises:

TimeoutError – No collision within max_trials.

Return type:

CollisionResult

Examples

>>> from blockchainkit.crypto import find_collision
>>> result = find_collision(16)
>>> result.first != result.second, result.trials < 2**10
(True, True)

SHA-256 written out as a Merkle-Damgard iteration, and its length-extension flaw.

Merkle and Damgard (1989) showed how to hash messages of any length with a fixed-size compression function: pad the message to whole blocks, ending with its length, then chain state = compress(state, block) from a fixed initial value. The digest is the final state. So anyone who knows H(m) and len(m) can keep compressing from it and compute H(m || pad || suffix) without knowing m: the length-extension attack. HMAC exists to stop it.

This is a readable implementation for study. Use hashlib.sha256() (or blockchainkit.crypto.systems.hashing.sha256()) for real hashing.

blockchainkit.crypto.systems.merkle_damgard.SHA256_IV = (1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225)#

The initial chaining value (FIPS 180-4, section 5.3.3).

Type:

tuple of int

blockchainkit.crypto.systems.merkle_damgard.sha256_compress(state, block)[source]#

Apply SHA-256’s compression function to one 64-byte block.

Parameters:
  • state (tuple of int) – Eight 32-bit words: SHA256_IV or the previous output.

  • block (bytes) – Exactly 64 bytes.

Returns:

The next chaining state.

Return type:

tuple of int

blockchainkit.crypto.systems.merkle_damgard.sha256_padding(message_length)[source]#

Return the Merkle-Damgard strengthening for a message of that many bytes.

A 0x80 byte, zeros up to 56 bytes modulo 64, then the bit length as a 64-bit big-endian integer. Encoding the length is what makes the construction collision-resistant whenever the compression function is.

Parameters:

message_length (int)

Return type:

bytes

blockchainkit.crypto.systems.merkle_damgard.merkle_damgard_sha256(data)[source]#

Hash by padding, then chaining the compression function from the IV.

>>> import hashlib
>>> from blockchainkit.crypto import merkle_damgard_sha256
>>> merkle_damgard_sha256(b"abc") == hashlib.sha256(b"abc").digest()
True
Parameters:

data (bytes)

Return type:

bytes

blockchainkit.crypto.systems.merkle_damgard.length_extension(digest, original_length, suffix)[source]#

Extend H(original) to H(original || glue || suffix) without knowing original.

Parameters:
  • digest (bytes) – SHA-256 of the unknown original message.

  • original_length (int) – Its length in bytes (for a secret-prefix MAC, key plus message).

  • suffix (bytes) – Data the attacker wants to append.

Returns:

(glue, forged): the original message’s padding, which becomes part of the forged message, and SHA-256(original || glue || suffix).

Return type:

tuple of bytes

Message authentication codes: the naive secret-prefix hash, and HMAC (1996).

blockchainkit.crypto.systems.mac.naive_mac(key, message)[source]#

Return SHA-256(key || message): a tempting MAC that is broken.

Anyone holding a tag can forge the tag of message || glue || suffix with length_extension(). Shown for contrast with hmac_sha256(); never use it.

Parameters:
Return type:

bytes

blockchainkit.crypto.systems.mac.hmac_sha256(key, message)[source]#

Return HMAC-SHA256(key, message) (Bellare, Canetti and Krawczyk, 1996).

HMAC hashes twice with two derived keys, H((k XOR opad) || H((k XOR ipad) || m)). The outer hash hides the inner chaining state, so length extension no longer works.

>>> from blockchainkit.crypto import hmac_sha256
>>> hmac_sha256(b"key", b"The quick brown fox jumps over the lazy dog").hex()[:16]
'f7bc83f430538424'
Parameters:
Return type:

bytes

Commitments: salted hash commitments (Blum 1981) and Pedersen commitments (1991).

A commitment is a sealed envelope: binding (the committer cannot change the contents later) and hiding (nobody can read them before the opening). A hash commitment is computationally hiding and binding. A Pedersen commitment is perfectly hiding and computationally binding, and commitments add.

blockchainkit.crypto.systems.commitments.commit(message, salt)[source]#

Commit to a message with a secret salt of at least 16 bytes.

Parameters:
  • message (bytes) – Bytes to commit to.

  • salt (bytes) – Random secret salt; fixed salts are only appropriate for experiments.

Returns:

Domain-separated digest. Length framing prevents ambiguous openings.

Return type:

bytes

Notes

Hiding depends on salt entropy; binding relies on collision resistance.

blockchainkit.crypto.systems.commitments.verify_commitment(digest, message, salt)[source]#

Check an opening against a commitment using constant-time comparison.

Parameters:
Return type:

bool

blockchainkit.crypto.systems.commitments.pedersen_generators(group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

Return (g, h): the group generator and a second, independent generator.

h is derived by hashing the group parameters into the subgroup, so nobody knows log_g(h). Anyone who did could open a commitment to any value, which is why h must not be chosen by the committer.

Parameters:

group (DHGroup)

Return type:

tuple[int, int]

blockchainkit.crypto.systems.commitments.pedersen_commit(value, blinding, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

Commit to value as g**value * h**blinding mod p.

With a uniformly random blinding factor the commitment is uniformly distributed whatever the value (perfect hiding). Opening it to a different value would reveal log_g(h) (computational binding). Commitments multiply to a commitment of the sum: C(a, r) * C(b, s) = C(a + b, r + s).

Parameters:
Return type:

int

Examples

>>> from blockchainkit.crypto import pedersen_commit, TEACHING_GROUP as G
>>> pedersen_commit(2, 5) * pedersen_commit(3, 7) % G.p == pedersen_commit(5, 12)
True

Merkle’s puzzles (1974-1978): key agreement from symmetric primitives alone.

Alice publishes count puzzles, each a short secret (an identifier and a session key) encrypted under a deliberately weak key of bits bits. Bob solves one at random, about 2**(bits-1) trials, and announces only its identifier. An eavesdropper does not know which puzzle Bob chose and must solve, on average, half of them. With count near 2**bits, honest work is linear and the attacker’s quadratic: the first public-key idea, with a gap that is only polynomial.

blockchainkit.crypto.systems.puzzles.merkle_puzzles(count, bits, *, randbits=<bound method SystemRandom.getrandbits of <random.SystemRandom object>>)[source]#

Create Alice’s puzzles and her private table of identifiers to keys.

Parameters:
  • count (int) – Number of puzzles to publish.

  • bits (int) – Size of each weak key, between 1 and 24; solving takes up to 2**bits trials.

  • randbits (collections.abc.Callable) – Randomness source; inject random.Random(seed).getrandbits only for reproducible experiments.

Returns:

(puzzles, table): 32-byte puzzles to publish, and the mapping from each 8-byte identifier to its 16-byte session key.

Return type:

tuple

blockchainkit.crypto.systems.puzzles.solve_puzzle(puzzle, bits)[source]#

Open a puzzle by brute force over every weak key.

A trial succeeds when the decrypted plaintext starts with the known marker, PUZZLE_MAGIC.

Raises:

ValueError – No weak key of that size opens the puzzle.

Parameters:
Return type:

PuzzleSolution

Examples

>>> from random import Random
>>> from blockchainkit.crypto import merkle_puzzles, solve_puzzle
>>> puzzles, table = merkle_puzzles(3, 6, randbits=Random(1).getrandbits)
>>> solution = solve_puzzle(puzzles[0], bits=6)
>>> table[solution.puzzle_id] == solution.key
True

Textbook Diffie–Hellman, RSA, and RSA blinding for small experiments.

These deliberately expose the mathematics. They are not encryption APIs for sensitive data: textbook RSA has no padding and DH has no authentication.

class blockchainkit.crypto.systems.asymmetric.DHGroup(p=23, q=11, g=2)[source]#

Bases: object

A prime-order subgroup of the multiplicative group modulo p.

Parameters:
  • p (int) – Small primes (below 2**64) with q dividing p-1.

  • q (int) – Small primes (below 2**64) with q dividing p-1.

  • g (int) – Nonidentity generator of order q.

Examples

>>> from blockchainkit.crypto import DHGroup
>>> group = DHGroup()
>>> group.shared(group.public(7), 3) == group.shared(group.public(3), 7)
True
p: int = 23#
q: int = 11#
g: int = 2#
public(private)[source]#

Return g**private modulo p, for private in [1, q).

Parameters:

private (int)

Return type:

int

shared(peer_public, private)[source]#

Derive a shared group element after validating subgroup membership.

This is not a key derivation function and does not authenticate peers.

Parameters:
  • peer_public (int)

  • private (int)

Return type:

int

blockchainkit.crypto.systems.asymmetric.TEACHING_GROUP = DHGroup(p=4611686018427377339, q=2305843009213688669, g=4)#

The order-q subgroup of a 62-bit safe prime p = 2q + 1, generated by 4.

Large enough that its discrete logs take real work by hand-written baby-step giant-step (about 2**31 steps), small enough for exact arithmetic to stay instant. Still far too small for real security.

Type:

DHGroup

class blockchainkit.crypto.systems.asymmetric.RSAKeyPair(n, e, d)[source]#

Bases: object

Textbook RSA exponents: modulus n, public e, and private d.

Construct with rsa_keypair(); tiny factors make the algebra visible.

Parameters:
n: int#
e: int#
d: int#
encrypt(message)[source]#

Apply the public exponent to an integer in [0, n).

Parameters:

message (int)

Return type:

int

decrypt(ciphertext)[source]#

Apply the private exponent; no padding or timing protections.

Parameters:

ciphertext (int)

Return type:

int

blockchainkit.crypto.systems.asymmetric.rsa_keypair(p=61, q=53, e=17)[source]#

Build a textbook key pair from distinct primes below 2**64.

>>> from blockchainkit.crypto import rsa_keypair
>>> key = rsa_keypair()
>>> key.encrypt(65), key.decrypt(2790)
(2790, 65)
Parameters:
Return type:

RSAKeyPair

blockchainkit.crypto.systems.asymmetric.rsa_blind(message, factor, key)[source]#

Return m*r**e mod n for a caller-selected invertible blinding factor.

The signer applies its private exponent to this blinded integer.

Parameters:
Return type:

int

blockchainkit.crypto.systems.asymmetric.rsa_unblind(blind_signature, factor, key)[source]#

Remove blinding so signature**e mod n equals the original message.

Parameters:
Return type:

int

Generic discrete-logarithm algorithms: why group size and structure matter.

Baby-step giant-step (Shanks 1971) solves base**x = target in any cyclic group of order n with about 2*sqrt(n) multiplications. Pohlig-Hellman (1978) splits the problem along the prime factors of n, so its cost is governed by the largest prime factor, not by n. Together they explain why Diffie-Hellman and Schnorr work in a subgroup of large prime order.

blockchainkit.crypto.systems.discrete_log.baby_step_giant_step(base, target, modulus, order)[source]#

Solve base**x = target (mod modulus) in about 2*sqrt(order) steps.

Writing x = i*m + j with m = ceil(sqrt(order)), a table of baby steps base**j is matched against giant steps target * base**(-i*m).

Parameters:
  • base (int) – A group element whose order divides order.

  • target (int) – The element whose logarithm is wanted.

  • modulus (int) – The modulus of the multiplicative group.

  • order (int) – A multiple of the order of base (the subgroup order).

Returns:

The exponent and the number of table multiplications.

Return type:

blockchainkit.crypto.core.base.DiscreteLogResult

Raises:

ValueError – target is not a power of base.

Examples

>>> from blockchainkit.crypto import baby_step_giant_step
>>> baby_step_giant_step(2, pow(2, 77, 1019), 1019, 1018).exponent
77
blockchainkit.crypto.systems.discrete_log.factor_order(n)[source]#

Factor a group order by trial division up to 2**20.

A cofactor left over after trial division must itself be prime, which is all Pohlig-Hellman needs; otherwise the order is rejected.

>>> from blockchainkit.crypto.systems.discrete_log import factor_order
>>> factor_order(8100)
{2: 2, 3: 4, 5: 2}
Parameters:

n (int)

Return type:

dict[int, int]

blockchainkit.crypto.systems.discrete_log.pohlig_hellman(base, target, modulus, order)[source]#

Solve a discrete log by splitting the group order into prime powers.

For each prime power p**e dividing order, the problem is mapped into the subgroup of order p**e and solved one base-p digit at a time with baby-step giant-step in a subgroup of order p. The Chinese Remainder Theorem combines the answers. The cost is about sum(e * sqrt(p)), tiny when every prime factor is small.

Parameters and return value are as for baby_step_giant_step().

Examples

>>> from blockchainkit.crypto import pohlig_hellman
>>> pohlig_hellman(6, pow(6, 4321, 8101), 8101, 8100).exponent
4321
Parameters:
Return type:

DiscreteLogResult

Threshold secret sharing: Shamir (1979) and Feldman’s verifiable variant (1987).

blockchainkit.crypto.systems.sharing.split_secret(secret, threshold, shares, prime=2089, *, randbelow=<function randbelow>)[source]#

Sample a polynomial with constant term secret and return its shares.

Parameters:
  • secret (int) – Field element in [0, prime).

  • threshold (int) – Number of shares needed, between 2 and shares.

  • shares (int) – Number of distinct nonzero evaluation points, below prime.

  • prime (int) – Prime field modulus below 2**64.

  • randbelow (collections.abc.Callable) – Uniform randomness source. Inject random.Random(seed).randrange only for reproducible teaching experiments.

Returns:

(x, y) pairs at x = 1, …, shares.

Return type:

tuple

blockchainkit.crypto.systems.sharing.feldman_split(secret, threshold, shares, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4), *, randbelow=<function randbelow>)[source]#

Shamir-share a secret modulo the group order, and publish g**a_j.

Each commitment hides one polynomial coefficient in the exponent. A share holder checks g**y == prod(C_j**(x**j)), which holds exactly when their share lies on the committed polynomial, so a cheating dealer is caught without anyone learning the secret. The commitment g**secret is public, so the secret is only computationally hidden.

Parameters:
Returns:

The shares and the public coefficient commitments.

Return type:

blockchainkit.crypto.core.base.FeldmanShares

Examples

>>> from random import Random
>>> from blockchainkit.crypto import feldman_split, feldman_verify, TEACHING_GROUP
>>> dealt = feldman_split(42, 2, 3, randbelow=Random(0).randrange)
>>> all(feldman_verify(share, dealt.commitments) for share in dealt.shares)
True
blockchainkit.crypto.systems.sharing.feldman_verify(share, commitments, group=DHGroup(p=4611686018427377339, q=2305843009213688669, g=4))[source]#

Check one share against the dealer’s public commitments.

Parameters:
Return type:

bool

blockchainkit.crypto.systems.sharing.recover_secret(shares, prime=2089)[source]#

Interpolate at x=0 using Lagrange coefficients.

The caller must supply enough authentic shares. Without commitments, this function cannot detect too few shares or a malicious participant.

>>> from blockchainkit.crypto import recover_secret
>>> recover_secret([(1, 8), (2, 11)], prime=17)
5
Parameters:
Return type:

int

Lamport one-time signatures (1979): signatures from a hash function alone.

The private key is 256 pairs of random preimages; the public key is their hashes. To sign, hash the message and, for each digest bit, reveal the preimage for that bit’s value. Verification hashes each revealed preimage. Each signature reveals half the private key, so a key must sign only once. Security rests only on the hash being one-way, so the scheme survives quantum computers, and its descendants (XMSS, SPHINCS+) are standardized.

blockchainkit.crypto.systems.lamport.lamport_keypair(seed)[source]#

Derive a one-time key pair deterministically from a secret seed.

Each private preimage is SHA-256(domain || seed || index || bit). A real seed must be at least 32 random bytes and never reused.

>>> from blockchainkit.crypto import lamport_keypair
>>> len(lamport_keypair(b"seed").public)
256
Parameters:

seed (bytes)

Return type:

LamportKeyPair

blockchainkit.crypto.systems.lamport.lamport_sign(message, key)[source]#

Reveal, for each bit of SHA-256(message), the matching private preimage.

Parameters:
Return type:

tuple[bytes, …]

blockchainkit.crypto.systems.lamport.lamport_verify(message, signature, public)[source]#

Check that each revealed preimage hashes to the public value for its bit.

>>> from blockchainkit.crypto import lamport_keypair, lamport_sign, lamport_verify
>>> key = lamport_keypair(b"seed")
>>> lamport_verify(b"hello", lamport_sign(b"hello", key), key.public)
True
Parameters:
Return type:

bool

Readable elliptic-curve group arithmetic, not constant-time cryptography.

Points are (x, y) tuples; None denotes the point at infinity. The curve equation is y**2 = x**3 + a*x + b modulo p.

class blockchainkit.crypto.systems.curves.Curve(p, a, b, generator, order, name='custom')[source]#

Bases: object

A nonsingular prime-field curve with a prime-order generator.

Parameters:
  • p (int) – Field modulus and reduced curve coefficients.

  • a (int) – Field modulus and reduced curve coefficients.

  • b (int) – Field modulus and reduced curve coefficients.

  • generator (tuple of int) – Affine coordinates of the base point.

  • order (int) – Prime order of the base point.

  • name (str) – Human-readable label.

Notes

Custom field/order primes must be below 2**64, which bounds the educational primality check. secp256k1’s own field prime and group order are also accepted, each only in its own role.

p: int#
a: int#
b: int#
generator: tuple[int, int]#
order: int#
name: str = 'custom'#
contains(point)[source]#

Return whether point is infinity or a canonical affine curve point.

Parameters:

point (tuple[int, int] | None)

Return type:

bool

property cofactor_is_one: bool#

Whether the generator’s subgroup is provably the whole curve group.

Hasse’s theorem bounds the number of points: #E <= p + 1 + 2*sqrt(p). The subgroup order n divides #E, so if 2n exceeds that bound the cofactor #E/n must be 1. Every on-curve point then lies in the subgroup, and verifiers can skip the n*P = O membership check.

blockchainkit.crypto.systems.curves.add(left, right, curve)[source]#

Add two validated points, including infinity and inverse pairs.

Parameters:
Return type:

tuple[int, int] | None

blockchainkit.crypto.systems.curves.multiply(scalar, point, curve)[source]#

Multiply a point by a signed integer using double-and-add.

Scalars are not reduced modulo the generator order: this also allows checking subgroup membership for arbitrary points on a custom curve.

Parameters:
Return type:

tuple[int, int] | None

blockchainkit.crypto.systems.curves.public_key(private, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Derive private*G for 1 <= private < generator order.

>>> from blockchainkit.crypto import public_key, TOY_CURVE
>>> public_key(2, TOY_CURVE)
(6, 3)
Parameters:
Return type:

tuple[int, int]

blockchainkit.crypto.systems.curves.enumerate_points(curve)[source]#

List every finite point of a small curve, sorted, by trying each x.

Adding the point at infinity gives the whole group, so len(enumerate_points(curve)) + 1 is the group order #E. Useful for plotting a toy curve and for checking Hasse’s bound by hand.

Raises:

ValueError – The field has more than 10,000 elements; enumeration is O(p).

Parameters:

curve (Curve)

Return type:

tuple[tuple[int, int], …]

Examples

>>> from blockchainkit.crypto import TOY_CURVE, enumerate_points
>>> len(enumerate_points(TOY_CURVE)) + 1 == TOY_CURVE.order
True
blockchainkit.crypto.systems.curves.encode_point(point, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Return fixed-width uncompressed encoding (0x04 || x || y).

Parameters:
Return type:

bytes

Educational Schnorr signatures and interactive proof transcripts.

This is a domain-separated teaching scheme, not Bitcoin BIP-340. Python curve arithmetic is variable-time. Explicit nonces exist for experiments; reusing a nonce reveals the private key.

blockchainkit.crypto.systems.signatures.challenge(message, commitment, public, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Hash the curve parameters, public key, commitment, and message to a scalar.

Parameters:
Return type:

int

blockchainkit.crypto.systems.signatures.sign(message, private, *, nonce=None, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Sign with s = k + H(R, Q, m)*x modulo the subgroup order.

Parameters:
  • message (bytes) – Exact bytes to authenticate.

  • private (int) – Secret scalar x.

  • nonce (int, optional) – Experiment-only scalar k. Defaults to system randomness.

  • curve (blockchainkit.crypto.systems.curves.Curve) – Defaults to secp256k1; tiny curves illustrate algebra, not security.

Returns:

Commitment R=kG and response s.

Return type:

blockchainkit.crypto.core.base.SchnorrSignature

Examples

>>> from blockchainkit.crypto import sign, verify, public_key
>>> signature = sign(b"lesson", 7, nonce=11)
>>> verify(b"lesson", signature, public_key(7))
True
blockchainkit.crypto.systems.signatures.verify_transcript(public, commitment, challenge_scalar, response, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Check the interactive Schnorr equation sG = R + cQ.

An accepting transcript is not itself proof of a live interaction: choosing c and s first permits simulation via R=sG-cQ. Soundness needs an unpredictable verifier challenge after the prover commits to R.

Parameters:
Return type:

bool

blockchainkit.crypto.systems.signatures.simulate_transcript(public, challenge_scalar, response, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Forge an accepting transcript without the private key: R = sG - cQ.

Choosing the challenge and response first, then solving for the commitment, produces (R, c, s) that verify_transcript() accepts. This is the simulator in the zero-knowledge proof (Goldwasser, Micali and Rackoff, 1985): real transcripts and simulated ones look the same, so a transcript teaches the verifier nothing. A live proof is sound only because the verifier picks c after seeing R.

Parameters:
Return type:

tuple[int, int]

blockchainkit.crypto.systems.signatures.deterministic_nonce(private, message, order=115792089237316195423570985008687907852837564279074904382605163141518161494337)[source]#

Derive a signing nonce from the key and message (RFC 6979, HMAC-SHA256).

Random nonces fail when the randomness does: a repeated or predictable nonce reveals the private key (see recover_reused_nonce_key()). RFC 6979 removes the random number generator from signing: the nonce is an HMAC-DRBG output seeded with the private key and the message hash, so it is unpredictable without the key, and distinct messages get distinct nonces. This implements section 3.2 with SHA-256, and reproduces the RFC’s published nonces when given the same order.

Parameters:
  • private (int) – Secret scalar in [1, order).

  • message (bytes) – The message to be signed (it is hashed here).

  • order (int) – The group order q; defaults to secp256k1’s.

Return type:

int

Examples

>>> from blockchainkit.crypto import deterministic_nonce, sign, verify, public_key
>>> k = deterministic_nonce(7, b"lesson")
>>> verify(b"lesson", sign(b"lesson", 7, nonce=k), public_key(7))
True
blockchainkit.crypto.systems.signatures.verify(message, signature, public, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Verify a Schnorr signature; malformed points or scalars return False.

Parameters:
Return type:

bool

blockchainkit.crypto.systems.signatures.recover_reused_nonce_key(first, second, first_challenge, second_challenge, order=115792089237316195423570985008687907852837564279074904382605163141518161494337)[source]#

Recover x=(s1-s2)/(c1-c2) when two signatures reuse their nonce.

This demonstrates special soundness and why nonce reuse is catastrophic. The caller supplies the actual challenges derived from the two messages.

Parameters:
Return type:

int

MuSig key aggregation (Maxwell, Poelstra, Seurin and Wuille, 2018).

Schnorr signatures add: if every signer uses nonce k_i and key x_i, the sum of their responses is a valid signature for the sum of their public keys. Summing keys naively is unsafe: an attacker who announces Q_rogue = xG - Q_honest makes the sum equal xG, a key they control alone. MuSig weights each key by a_i = H(L, Q_i), a hash of the whole key list L, so no participant can choose their key to cancel the others.

Teaching simplification: musig_sign() plays every signer in one process. Real MuSig exchanges nonce commitments first (or uses MuSig2’s two nonces); without that round, a malicious co-signer can bias the joint nonce.

blockchainkit.crypto.systems.multisig.musig_coefficients(publics, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Return a_i = H(L || Q_i) mod n for each key, with L the encoded key list.

Parameters:
Return type:

tuple[int, …]

blockchainkit.crypto.systems.multisig.aggregate_public_keys(publics, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Return the MuSig aggregate key sum(a_i * Q_i).

>>> from blockchainkit.crypto import aggregate_public_keys, public_key
>>> len(aggregate_public_keys([public_key(3), public_key(5)]))
2
Parameters:
Return type:

tuple[int, int]

blockchainkit.crypto.systems.multisig.musig_sign(message, privates, nonces, curve=Curve(p=115792089237316195423570985008687907853269984665640564039457584007908834671663, a=0, b=7, generator=(55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424), order=115792089237316195423570985008687907852837564279074904382605163141518161494337, name='secp256k1'))[source]#

Produce one Schnorr signature valid for the aggregate of the signers’ keys.

Each signer i contributes R_i = k_i G and s_i = k_i + c * a_i * x_i with the shared challenge c = H(R, Q_agg, m); the signature is (sum R_i, sum s_i) and verifies with verify() against aggregate_public_keys().

Examples

>>> from blockchainkit.crypto import musig_sign, aggregate_public_keys, public_key, verify
>>> signature = musig_sign(b"m", [3, 5], [11, 13])
>>> verify(b"m", signature, aggregate_public_keys([public_key(3), public_key(5)]))
True
Parameters:
Return type:

SchnorrSignature

Helpers#

Deterministic primality testing for the bounded parameters used in experiments.

blockchainkit.crypto.utils.primes.is_prime(value)[source]#

Test primality deterministically for integers below 2**64.

Uses trial division followed by deterministic Miller–Rabin bases for this bounded domain. Larger numbers are deliberately not accepted.

>>> from blockchainkit.crypto.utils import is_prime
>>> is_prime(7919), is_prime(561)
(True, False)
Parameters:

value (int)

Return type:

bool

Plotting#

Plotting helpers for blockchainkit.crypto: small curve groups and hash avalanche.

blockchainkit.crypto.visualizers.plots.plot_curve_points(curve, *, label_multiples=True, ax=None)[source]#

Scatter every finite point of a small curve and label the multiples kG.

Parameters:
Return type:

matplotlib.axes.Axes

blockchainkit.crypto.visualizers.plots.plot_hamming_distances(distances, bits=256, ax=None)[source]#

Histogram of output-bit differences against the ideal Binomial(bits, 1/2).

Parameters:
Return type:

matplotlib.axes.Axes