Core protocol
Addresses & keys
Exact wallet entropy, BIP39 seed, BIP32 root, secp256k1 public-key, HASH160, Base58Check, address-validation, and key-format rules shared by Python and JavaScript.
This page documents the implemented alpha. Executable source, interoperability vectors, and tests remain authoritative if prose and code ever diverge.
Address contract
A canonical Luracoin address is a 34-character Base58Check string encoding a 21-byte payload:
payload = 0x30 || RIPEMD160(SHA256(compressed_secp256k1_public_key))
address = Base58Check(payload)
0x30 is the single Luracoin address version implemented across networks. Testnet and devnet addresses do not use distinct visible prefixes; network separation is provided by the transaction chain ID, P2P handshake, and runtime namespace. A valid address normally begins with L, but validation must decode and inspect the full payload rather than relying on the first character.
Base58Check appends the first four bytes of double SHA-256 over the payload before Base58 encoding. The alphabet excludes visually ambiguous characters such as 0, O, I, and l.
Entropy and mnemonic generation
New wallets begin with 256 bits from the operating system cryptographic random number generator:
- Python uses
secrets.token_bytes(32). - JavaScript uses
crypto.getRandomValuesthrough the Noble hashes utility.
Those 32 bytes become an uppercase hexadecimal “wallet input” in Python compatibility output and map to a 24-word English BIP39 mnemonic. BIP39 includes checksum bits in the word sequence; word count alone is not validation.
The wallet UI normalizes restoration input by trimming, lowercasing, and collapsing whitespace, then validates it against the English BIP39 wordlist and checksum.
Do not substitute a pseudo-random generator, timestamp, browser Math.random, user-supplied phrase, or custom wordlist. Entropy failures compromise every later layer regardless of vault encryption.
BIP39 seed
Both implementations compute the standard BIP39 seed from:
mnemonic: normalized 24-word English phrase
passphrase: "LURA" by default
output: 64-byte seed
The BIP39 passphrase is part of recovery, not a vault password. Any change—including empty string versus LURA, capitalization, whitespace, or a custom phrase—produces an unrelated seed and address.
The Python generator always uses LURA. The wallet restore flow exposes a custom passphrase under an advanced option and defaults it to LURA for compatibility.
BIP32 master key
Luracoin 0.1.0 uses the BIP32 root directly. It does not derive a child path.
The master material is:
I = HMAC-SHA512(key = "Bitcoin seed", data = bip39_seed)
master_private_key = I[0:32]
chain_code = I[32:64]
The private scalar must be valid for secp256k1: nonzero and smaller than the curve order. If BIP32 produces an invalid master scalar, wallet generation must fail and use fresh entropy rather than reducing or modifying the value.
Python serializes a root extended private key with the conventional xprv version 0x0488ade4, depth zero, zero parent fingerprint, zero child number, the 32-byte chain code, a zero prefix, and the private key. This version is used as a compatibility serialization and is not a statement that Luracoin uses Bitcoin mainnet addresses or derivation paths.
secp256k1 key formats
One private key has several representations in the stack:
| Representation | Bytes | Use |
|---|---|---|
| Private scalar | 32 | Secret signing input. |
| Compressed public key | 33 | Address derivation; prefix 02 or 03 plus x-coordinate. |
| Uncompressed SEC public key | 65 | Prefix 04 plus x and y. |
| Raw verifying key | 64 | Uncompressed x and y without prefix; stored in the transaction unlocking field. |
The address is derived from the compressed 33-byte public key. The transaction carries the raw uncompressed 64-byte coordinates. During signature validation, the node reconstructs the SEC form by prepending 0x04, and independently derives the address from its compressed form to verify ownership.
Hashing the raw 64 bytes instead of the compressed 33 bytes yields a different address and is incompatible.
Address encoding algorithm
Given a valid compressed public key P:
sha = SHA256(P) # 32 bytes
hash160 = RIPEMD160(sha) # 20 bytes
payload = 0x30 || hash160 # 21 bytes
checksum = SHA256(SHA256(payload))[0:4] # 4 bytes
address = Base58Encode(payload || checksum) # exactly 34 chars here
The length check is an implemented invariant. If the encoding does not produce 34 characters, generation fails.
Validation algorithm
A validator must:
- require a string of exactly 34 characters;
- Base58Check-decode it and verify the checksum;
- require a 21-byte decoded payload;
- require payload byte zero to equal
0x30.
The wallet also performs a fast visible-prefix check before decoding. That is an optimization, not the complete rule.
Examples of invalid input include:
- a Bitcoin address with a valid Bitcoin checksum;
- 34 literal
Lcharacters; - an address shortened to 33 characters;
- altered case or character substitution that breaks the checksum;
- a valid Base58Check payload with another version byte.
Reserved zero sentinels
The string of 34 ASCII zeros is not a Base58Check Luracoin address. It is a protocol sentinel:
0000000000000000000000000000000000
It may appear only as the from_address of a coinbase paired with the all-zero 128-byte unlocking field. It is explicitly rejected as a destination. Luracoin 0.1.0 defines no burn, staking, or unspendable-output rule, so accepting value to this sentinel would create an undocumented irreversible operation.
Address ownership in a transaction
For a normal transaction, the unlocking field contains:
raw_public_key_x_y (64 bytes) || compact_signature_r_s (64 bytes)
Validation reconstructs the public key, compresses it for address derivation, and requires the result to equal from_address. It then verifies the signature over the canonical unsigned transaction bytes. A valid signature from a different key is not sufficient.
This binds three independently checked artifacts:
- the sender address serialized into the signed message;
- the public key disclosed in the signed transaction;
- the signature produced by the corresponding private scalar.
Wallet records and secret classification
Treat the following as secrets:
- entropy/wallet input;
- 24-word mnemonic;
- BIP39 passphrase;
- 64-byte seed;
- root extended private key;
- 32-byte private key;
- decrypted wallet vault;
- RPC bearer token, although it is a node credential rather than a spending key.
The compressed public key, raw verifying key, address, transaction bytes, and signatures are public chain material once used. Publishing unused public keys is not equivalent to publishing private keys, but operational privacy may still matter.
The Python generate-wallet CLI prints every secret field once as JSON and warns on standard error. It does not write the generated record unless a caller explicitly uses the lower-level file function. Avoid terminal logging, shell capture, shared scrollback, and issue attachments.
Cross-language compatibility
Python uses coincurve/libsecp256k1, mnemonic, and base58. JavaScript uses Noble/Scure packages. The test suites assert that both implementations derive the same fields from a labeled public mnemonic fixture and produce the same address from the same compressed public key.
Compatibility depends on all of these remaining fixed:
- English BIP39 normalization and checksum;
- default passphrase
LURA; - root BIP32 key with no child path;
- secp256k1;
- compressed public key for HASH160;
- version
0x30; - standard Base58Check checksum.
Any future multi-account HD design must define a derivation path and migration rule. Silently adding a child path would make existing mnemonics appear to lose their current account.
Implementation checklist
An independent address implementation is conformant only if it can:
- reject malformed BIP39 phrases;
- reproduce the root key and address from the shared fixture;
- handle invalid BIP32 master scalars by failing, not reducing;
- distinguish compressed, SEC-uncompressed, and raw-uncompressed formats;
- reproduce 34-character
0x30Base58Check addresses; - reject foreign version bytes and bad checksums;
- derive the transaction sender from the disclosed public key;
- treat the zero string solely as the coinbase origin sentinel.
Continue with Transactions for signing and serialized ownership proof.
Source anchors
Primary implementation files used for this chapter:
