DEX Order Authorization Signature
Abstract
This LP specifies the exact bytes a trader signs to authorize an order on the Lux D-Chain matcher, and the rule that the signed bytes must be the whole order the matcher acts on. It is the order-side companion to LP-117 (the finality signed message): both record a signed-message layout from the shipping code so an independent implementation signs and verifies byte-identically.
Motivation
A signed order is a standing authorization: a relay carries it from the trader to the matcher, and the matcher acts on it later. If the signature covers less than the matcher reads, any relay between the two can rewrite the uncovered fields while the signature still verifies — changing the price guard, the flags, or the size a fill is charged against. And if the signature names no chain and no deadline, it is good on every D-Chain forever, so an order signed on one chain can be lifted onto another and replayed indefinitely.
On a permissionless exchange, where the relay is untrusted and any chain may exist, the signed subset must equal the acted-on order, and the signature must name its authorization scope.
Specification
The signed digest
The order-authorization signature is over keccak256 of these bytes, in order,
big-endian. It is a hand-written fixed-width layout so a field cannot silently
drop out of the preimage.
| field | width | notes |
|---|---|---|
| domain tag | 21 | ASCII lux.dex.order.auth.v1 |
| network_id | 4 | authorization scope |
| chain_id | 32 | authorization scope |
| expiry | 8 | authorization scope — the signature is void after this |
| order_id | 8 | |
| symbol | 2 + n | uint16 length, then bytes |
| side | 1 | |
| type | 1 | |
| price_int | 8 | limit price in integer ticks |
| size_ticks | 8 | size in integer ticks; an off-tick size is refused, never rounded |
| stop_price | 8 | IEEE-754 bits |
| limit_price | 8 | IEEE-754 bits |
| display_size | 8 | iceberg visible size, IEEE-754 bits |
| peg_offset | 8 | IEEE-754 bits |
| take_profit | 8 | IEEE-754 bits |
| stop_loss | 8 | IEEE-754 bits |
| post_only | 1 | |
| reduce_only | 1 | |
| hidden | 1 | |
| flags | 4 | |
| user_id | 2 + n | uint16 length, then bytes — the account a fill is attributed to |
| client_id | 2 + n | uint16 length, then bytes |
| sender | 20 |
The binding rules
-
The signed subset is the whole order the matcher acts on. Every field the matcher reads to reach a decision — the price guard, the post-only / reduce-only / STP flags, the stop and bracket legs, the iceberg's visible size, the lifetime, and the account the fill is charged to — is inside the digest. A field the matcher reads but the digest omits is rewritable by any relay, so the binding rule for this signature is: if the matcher reads it, it is signed.
-
The authorization scope is named.
network_id,chain_id, andexpiryare bound ahead of the economic fields. A signature made for one chain does not recover its signer on another, and a signature is void afterexpiry. The D-Chain transaction digest binds the same scope underlux.dchain.tx.auth.v2for the same reason. -
Sizes are exact, not rounded.
size_ticksis an integer number of ticks; a size that is not an exact number of ticks is refused rather than snapped, so the digest is injective in size — two distinct sizes cannot share a signature. Advanced-order floats are bound by their exact IEEE-754 bits, identical on every architecture. -
The domain tag is versioned.
lux.dex.order.auth.v1keeps an order signature from colliding with any other keccak256 preimage in the stack, and makes the next encoding a distinct namespace rather than a silent reinterpretation of these bytes.
Rationale
A fixed-width hand-written layout, rather than a schema with optional fields, is
what makes "if the matcher reads it, it is signed" checkable: adding a field the
matcher reads is a visible change to this table and to the v1 domain tag, not a
value that quietly appears outside the signature. The float fields are bound by
their bits rather than a decimal rendering so the preimage is identical on every
node and injective.
Backwards Compatibility
This LP documents the shipped v1 order-authorization digest. A prior version of
the code signed only the economic fields and named no scope; a signature made
under that scheme does not verify under this one, which is the intended effect —
those signatures were replayable across chains and did not bind what the matcher
acted on.
Reference Implementation
github.com/luxfi/dex, pkg/dex/signed_order.go (SigningHash). The
authorization scope fields live on SignedOrder; the economic and advanced-order
fields live on the matcher's Order, and the digest binds both.
Security Considerations
The signature is the only thing standing between a trader's intent and a fill. A conformance suite MUST assert byte-equality of the digest for a shared corpus of orders across implementations, and MUST assert that mutating any matcher-read field changes the digest — a field the matcher reads that does not move the digest is a relay-rewrite surface, not a compatibility detail.