Protocol documentationSource snapshot: 0.1.0 alphaNetwork: testnet / devnet

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.

Consensus-critical encoding · transaction v1Reviewed August 31, 2026
Specification status

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:

  1. Derive the sender address from the compressed public key and require it to match from_address.
  2. Serialize the exact unsigned 85 bytes.
  3. Compute digest = SHA256(unsigned_bytes).
  4. Sign digest using secp256k1 ECDSA with deterministic RFC 6979 nonce generation.
  5. Normalize s so 1 <= s <= n/2, where n is the secp256k1 group order.
  6. Encode the signature as compact 64-byte r || s, not ASN.1 DER.
  7. Encode the public key as 64-byte raw x || y, without the SEC 0x04 prefix.
  8. Append public_key || signature to 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:

  • chain is an integer in uint8;
  • nonce and fee are integers in uint32;
  • value is an integer from 1 through 2^64 - 1;
  • from_address is a valid address, except the zero origin is permitted only for coinbase;
  • to_address is a valid address and never the zero sentinel;
  • unlock_sig is 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:

  1. Reproduce the shared vector byte for byte.
  2. Add negative tests for endian swaps, Unicode addresses, wrong lengths, high-S signatures, zero/overflow values, and foreign chain IDs.
  3. Verify sender derivation from the disclosed public key.
  4. Preserve all integer values without floating point.
  5. Enforce exact unsigned and signed lengths.
  6. Treat node 202 as mempool admission, not confirmation.
  7. 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: