Core protocol
Transactions
The canonical transaction format and lifecycle: exact offsets, integer bounds, deterministic low-S signing, IDs, coinbase semantics, contextual account validation, mempool admission, and conflicts.
This page documents the implemented alpha. Executable source, interoperability vectors, and tests remain authoritative if prose and code ever diverge.
Canonical sizes
Luracoin transactions are fixed-width:
unsigned body: 85 bytes
unlocking field: 128 bytes
complete tx: 213 bytes
RPC hex: 426 characters
There is no version byte, variable-length integer, memo, input array, output array, or trailing extension field. A deserializer accepts exactly 85 bytes for an unsigned object or exactly 213 bytes for a complete object. Consensus blocks and P2P tx messages require the complete form.
Binary layout
| Offset | Size | Field | Encoding | Domain |
|---|---|---|---|---|
| 0 | 1 | chain |
unsigned integer | 0–255; must equal active chain ID |
| 1 | 4 | nonce |
little-endian | uint32 |
| 5 | 4 | fee |
lurashis, little-endian | uint32 |
| 9 | 8 | value |
lurashis, little-endian | positive uint64 |
| 17 | 34 | from_address |
ASCII | canonical address or coinbase zero sentinel |
| 51 | 34 | to_address |
ASCII | canonical address; zero sentinel forbidden |
| 85 | 64 | public key | raw uncompressed secp256k1 `x | |
| 149 | 64 | signature | compact ECDSA `r |
The unlocking field is represented in Python as unlock_sig = public_key || signature. A normal transaction cannot omit it, change its length, or use a compressed public key in that slot.
Unsigned serialization
To build the 85-byte signing message:
chain_u8
|| nonce_le_u32
|| fee_le_u32
|| value_le_u64
|| from_address_ascii_34
|| to_address_ascii_34
The signing algorithm hashes these raw bytes once with SHA-256. It does not sign a JSON object, hexadecimal text, the transaction ID, a Python string representation, or a double-SHA256 digest.
Every implementation must validate that each address occupies exactly 34 encoded bytes. ASCII is canonical; Unicode lookalikes and multibyte characters are invalid.
Signing algorithm
For a normal transaction:
- Derive the sender address from the compressed public key and require it to match
from_address. - Serialize the exact unsigned 85 bytes.
- Compute
digest = SHA256(unsigned_bytes). - Sign
digestusing secp256k1 ECDSA with deterministic RFC 6979 nonce generation. - Normalize
sso1 <= s <= n/2, wherenis the secp256k1 group order. - Encode the signature as compact 64-byte
r || s, not ASN.1 DER. - Encode the public key as 64-byte raw
x || y, without the SEC0x04prefix. - Append
public_key || signatureto the unsigned body.
Python’s verifier checks:
1 <= r < n
1 <= s <= n / 2
ECDSA_verify(public_key, SHA256(unsigned_bytes), compact_signature)
address(compressed(public_key)) == from_address
Low-S enforcement removes one standard ECDSA malleability form: (r, s) and (r, n-s) cannot both be accepted. It does not make a transaction final or provide replay protection beyond the chain ID and account nonce.
Transaction ID
After the 128-byte unlocking field is present:
txid = SHA256(SHA256(complete_213_bytes))
The displayed ID is the 32-byte digest as 64 lowercase hexadecimal characters. It commits to the signature and public key, not only the transfer fields.
The shared public interoperability vector produces transaction ID:
2712bfd829c397e9fb32188bf705987115b1a2d180aac80ba28a2aa6edcbb9b6
That vector also fixes the unsigned bytes, single-SHA256 signing digest, raw public key, compact low-S signature, unlocking field, and complete raw bytes. Implementations should consume vectors.json directly rather than copying test secrets into application fixtures.
Field validation
Before consulting account state, the node requires:
chainis an integer inuint8;nonceandfeeare integers inuint32;valueis an integer from 1 through2^64 - 1;from_addressis a valid address, except the zero origin is permitted only for coinbase;to_addressis a valid address and never the zero sentinel;unlock_sigis exactly 128 bytes;- normal transactions do not use the all-zero unlocking field accidentally.
Zero-value transfers are invalid. Zero-fee transfers are valid. A fee cannot exceed uint32, while value and balances use uint64.
Normal contextual validation
For a non-coinbase transaction, the active state supplies:
{ "balance": "uint64-compatible integer", "nonce": "uint32-compatible integer" }
The transaction is valid only if:
transaction.chain == active_chain_id
account exists
account.balance >= transaction.value + transaction.fee
transaction.nonce == account.nonce + 1
signature and sender ownership are valid
The nonce is per sender. It prevents replay against the same account state and imposes a total order over that account’s outgoing transactions.
Ordered validation inside a block
Transactions are evaluated sequentially against a temporary account map. For every normal transaction:
sender.balance -= value + fee
sender.nonce = transaction.nonce
receiver.balance += value
Later transactions in the same block see earlier updates. This permits a contiguous sequence of nonces from one sender and allows value received by an earlier normal transaction to be spent later in the same block.
The coinbase is credited only after all normal transactions, even though it occupies index zero. Newly created subsidy cannot be spent inside its own block.
For a self-transfer where sender equals receiver, the net balance effect is only the fee:
balance -= value + fee
balance += value
nonce = transaction.nonce
Any receiver or miner credit above 2^64 - 1 invalidates the entire block.
Coinbase transaction
Coinbase reuses the same 213-byte transaction layout but has exact sentinel semantics:
| Field | Required value |
|---|---|
chain |
Active chain ID |
nonce |
Block height |
fee |
0 |
value |
Genesis allocation at height 0; otherwise subsidy + all regular fees |
from_address |
34 ASCII zeros |
to_address |
Block miner address |
unlock_sig |
128 zero bytes |
The height nonce makes coinbase IDs unique even when miner and reward repeat. Coinbase is never accepted into the Redis mempool. A valid block contains exactly one coinbase at transaction index zero.
Mempool admission
Redis stores two related keys for every accepted normal transaction:
nonce:<chain>:<sender>:<nonce> -> transaction_id
<transaction_id> -> canonical 213-byte payload
A Lua script atomically claims the sender/nonce key and writes the payload. If another transaction ID already owns the slot, admission returns false. This is a first-writer reservation, not a fee-replacement policy.
The node permits a contiguous sequence of pending outgoing transactions. Before admitting nonce N, it walks every nonce from confirmed account.nonce + 1 through N - 1, requires each reservation and payload to exist, deserializes and validates them against a simulated sender state, then validates the candidate.
The number of skipped pending slots cannot be negative or reach the 65,535 transaction bound. A gap rejects admission.
Redis failures are not interpreted as valid empty state. RPC reports mempool_available: false or 503 mempool_unavailable, and the wallet disables sending.
Miner selection
The miner scans Redis keys, ignores nonce:* reservations, deserializes canonical payloads, removes malformed, duplicate-ID, and coinbase candidates, then sorts by:
fee descending
transaction ID ascending
It repeatedly scans the pending list so a higher-nonce transaction can become eligible after its predecessor is selected. Each candidate is validated against a simulated multi-account state. Selection stops when no further candidate makes progress, the active block byte limit would be exceeded, or 65,534 normal slots are filled—the leading coinbase reserves one of 65,535 total slots.
This is deterministic for a fixed Redis snapshot and account state, but Redis key enumeration and concurrent changes mean template contents are not a consensus rule. Any ordered set that validates and fits is acceptable.
Confirmation and competing slots
When a block is accepted, the node removes included transaction IDs and nonce reservations. It also handles a subtler case: a received block can confirm transaction B while local Redis holds transaction A with the same (chain, sender, nonce).
Before applying the block, the node snapshots the exact competing ID and bytes. After acceptance it conditionally deletes:
- the reservation only if it still names that ID;
- the payload only if its bytes still match the captured candidate.
The comparison is performed in Redis Lua so a concurrent replacement is not accidentally deleted. Chain acceptance does not roll back if disposable mempool cleanup fails.
RPC receipt semantics
POST /v1/transactions accepts:
{ "raw": "<426 hexadecimal characters>" }
A successful HTTP 202 response means:
- the bytes decoded to exactly 213 bytes;
- the node was locally ready;
- the transaction was admitted to Redis;
- initial relay was attempted.
accepted: true is local admission. relay_count is the number of peers whose write completed successfully. broadcast is exactly relay_count > 0. The transaction remains useful to a local miner even if relay count is zero.
Rejection taxonomy
At the RPC boundary, malformed JSON/hex, wrong length, node readiness, Redis availability, and mempool rejection receive distinct stable error labels. Inside consensus, TransactionNotValid distinguishes field, signature, balance, and nonce conditions for tests and internal callers.
P2P transaction rejection is deliberately quiet: an invalid tx payload is not admitted or re-relayed. The current alpha does not send a reject message, assign a persistent misbehavior score, or ban the peer solely through this path.
What is not encoded
A Luracoin 0.1.0 transaction has no:
- timestamp or expiration;
- memo/note/label;
- multi-recipient output list;
- smart-contract calldata;
- fee rate or size-dependent fee rule;
- replacement flag;
- public-key script language;
- network name string—the one-byte chain ID carries that domain;
- finality certificate.
Wallet notes and contact labels are local UI data. Explorer direction (sent, received, self, mined) is derived metadata.
Independent implementation checklist
Before broadcasting a transaction from another implementation:
- Reproduce the shared vector byte for byte.
- Add negative tests for endian swaps, Unicode addresses, wrong lengths, high-S signatures, zero/overflow values, and foreign chain IDs.
- Verify sender derivation from the disclosed public key.
- Preserve all integer values without floating point.
- Enforce exact unsigned and signed lengths.
- Treat node 202 as mempool admission, not confirmation.
- Test contiguous nonces and same-slot conflicts against the Python node.
The next chapter, Blocks & validation, specifies how these transactions become one atomic account transition.
Source anchors
Primary implementation files used for this chapter:
