mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
122 lines
5.4 KiB
Go
122 lines
5.4 KiB
Go
// Copyright (C) 2025-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package dex
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
// native_wire.go is the C<->D funding wire, and it is deliberately boring.
|
|
//
|
|
// MOVE ONCE, TRADE MANY. Value crosses the chain boundary exactly twice in its life
|
|
// — in, and out. Between those, D owns it outright and every order, match and trade
|
|
// is a local balance movement. A million trades cost zero crossings.
|
|
//
|
|
// So this subsystem has exactly ONE job: move ownership exactly once between chains.
|
|
// It knows nothing about orders, markets, sides, prices, sizes, executions,
|
|
// slippage, settlement or FIX. A claim is:
|
|
//
|
|
// beneficiary(20) | asset(32) | amount(8) = 60 bytes
|
|
//
|
|
// and that is the whole vocabulary. The same 60 bytes travel both directions: a C->D
|
|
// deposit and a D->C withdrawal are the same primitive pointed opposite ways, so
|
|
// there is one encoder, one decoder, and one golden vector pinning them against
|
|
// dex/pkg/dchain.
|
|
//
|
|
// WHY EACH FIELD EARNS ITS PLACE, AND WHY NOTHING ELSE DOES.
|
|
//
|
|
// - beneficiary is the account the destination chain credits. Naming it in the
|
|
// object is what makes delivery permissionless: anyone may hand a claim to the
|
|
// destination, because handing it over can only credit the account it already
|
|
// names. A relayer that refuses to deliver cannot strand anyone.
|
|
// - asset and amount are the value. Nothing else is.
|
|
//
|
|
// There is ONE party field, not two. dexprotocol.Claim (dex/pkg/dexprotocol) names
|
|
// exactly one — Beneficiary — and PendingExport.Owner() returns claim.Beneficiary,
|
|
// so "owner" and "beneficiary" were already one fact under two names. Carrying both
|
|
// would store that fact twice for the two copies to be cross-checked against each
|
|
// other.
|
|
//
|
|
// The claim's SCOPE is not in its bytes because it is already in its address: the id
|
|
// binds (source, dest, sourceTx, index), the source chain owns the partition it is
|
|
// read from, and the destination owns the partition it is written under. Restating
|
|
// any of that inside the value would be a second copy of the addressing.
|
|
//
|
|
// WIDTH IS THE DISCRIMINATOR. 60 is the only canonical width; anything else is
|
|
// refused rather than reinterpreted, so a truncated or padded record can never
|
|
// decode into a credit.
|
|
|
|
// claimSize is the fixed funding-object width: beneficiary(20) | asset(32) |
|
|
// amount(8). IDENTICAL to dex/pkg/dchain claimSize.
|
|
const claimSize = 20 + 32 + 8
|
|
|
|
// encodeClaim serializes a funding object. beneficiary is the destination account
|
|
// (EVM address / D-Chain 20-byte owner); asset is the full injective AssetID (native
|
|
// = all-zero); amount is the integer asset unit count. Fixed width, big-endian, no
|
|
// padding — so two encoders agree byte-for-byte or the golden vector fails.
|
|
func encodeClaim(beneficiary common.Address, asset [32]byte, amount uint64) []byte {
|
|
v := make([]byte, claimSize)
|
|
copy(v[0:20], beneficiary[:])
|
|
copy(v[20:52], asset[:])
|
|
binary.BigEndian.PutUint64(v[52:60], amount)
|
|
return v
|
|
}
|
|
|
|
// decodeClaim is the exact inverse. ok=false for any width but the canonical one, so
|
|
// a corrupt record is never reinterpreted into a credit. The consumer binds the
|
|
// credit to THIS recorded value, never to what the calling transaction declares.
|
|
func decodeClaim(v []byte) (beneficiary common.Address, asset [32]byte, amount uint64, ok bool) {
|
|
if len(v) != claimSize {
|
|
return common.Address{}, [32]byte{}, 0, false
|
|
}
|
|
copy(beneficiary[:], v[0:20])
|
|
copy(asset[:], v[20:52])
|
|
amount = binary.BigEndian.Uint64(v[52:60])
|
|
return beneficiary, asset, amount, true
|
|
}
|
|
|
|
// DeriveClaimID computes a funding claim's deterministic id — the shared-memory key
|
|
// the object is written under, and the key the destination consumes:
|
|
//
|
|
// SHA-256( claimDomain | source | dest | sourceTx | index )
|
|
//
|
|
// ONE derivation, BOTH directions, BOTH repos. dex/pkg/dchain reproduces it
|
|
// byte-for-byte; the golden vector pins them together.
|
|
//
|
|
// It is injective because a transaction id is unique on its chain and the index is
|
|
// unique within that transaction — so two claims minted by one transaction differ by
|
|
// index, and two minted by different transactions differ by sourceTx. (source, dest)
|
|
// scope the claim to one corridor, so an object minted for C->D can never be replayed
|
|
// as D->C. Every component is fixed width, so there is no concatenation ambiguity.
|
|
//
|
|
// The VALUE is deliberately not folded in. The id names WHICH claim; the object
|
|
// carries WHAT it moves, and the destination binds the credit to the object's bytes,
|
|
// which the block-level import rule proves are the recorded ones. Hashing the value
|
|
// into the id too would be a second copy of it, and a copy the consumer would then
|
|
// have to decide whether to trust over the first.
|
|
func DeriveClaimID(source, dest, sourceTx ids.ID, index uint32) ids.ID {
|
|
h := sha256.New()
|
|
h.Write([]byte(claimDomain))
|
|
h.Write(source[:])
|
|
h.Write(dest[:])
|
|
h.Write(sourceTx[:])
|
|
var u4 [4]byte
|
|
binary.BigEndian.PutUint32(u4[:], index)
|
|
h.Write(u4[:])
|
|
var out [32]byte
|
|
copy(out[:], h.Sum(nil))
|
|
return ids.ID(out)
|
|
}
|
|
|
|
// claimDomain scopes the claim-id derivation so a funding-rail id can never collide
|
|
// with any other 32-byte shared-memory key.
|
|
//
|
|
// The literal is the hash input, not a label, so it does not track this identifier:
|
|
// changing a byte of it moves every claim id the rail has ever derived.
|
|
const claimDomain = "lux.dex.claim.v1"
|