mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
* dexvm: bind swap-settle authority+payout to authenticated escrow owner (CRITICAL escrow-theft fix) The swap rail delegated per-taker refund-liveness + relay authority to the unauthenticated RelayOrderTx sender with NO C-side owner binding, letting any fee-paying tx name a victim's collateral ref and settle the locked collateral to the attacker out of the shared seam reserve (cross-depositor theft). Fix (load-bearing, derives authority from the already-authenticated cross-chain object owner — the platformvm import/export model): - state/state.go: escrow value is now owner(20)|asset(32)|amount(8); the owner leads. PutEscrow/GetEscrow carry it. - atomic.go executeImport: persist the AUTHENTICATED importedOwner (read back from the consumed C->D shared-memory object) into the escrow. - vm.go settleFromFills: a settle is authorized ONLY by the escrow's recorded owner (errSettleUnauthorized otherwise), and BOTH proceeds+refund legs export to that owner, never to the relay tx sender. - txs/tx.go: RelayOrderTx.Verify now does present-gated secp256k1 authentication of From (Sign/SigningBytes/verifyFrom), wiring the dangling ErrInvalidSignature. From bears NO authority (authority is the escrow owner), so an unsigned relay is admissible and cannot escalate; a present-but-invalid signature is rejected. Regression: redteam_escrow_owner_test.go proves the attacker is credited 0 even with a valid own-key signature (defense in depth: escrow-owner bind holds independent of the signature). Fixed a pre-existing off-by-one in TestRED_FractionalFill (export asset offset). All dexvm tests pass (default + -tags redteam, CGO_ENABLED=0 pure-Go). Gated on red re-verify; NOT promoted to testnet/mainnet. * aivm: A-Chain quorum-settlement engine (provider stake/slash, beacon selection, commit-reveal, settlement, receipt export, import committed C intent). LP-5300. NOTE: branched on swap-rail base; rebase to main on merge. --------- Co-authored-by: zeekay <[email protected]>
143 lines
5.3 KiB
Go
143 lines
5.3 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package aivm
|
|
|
|
// quorum_wire.go pins the SHARED CROSS-CHAIN WIRE SPEC byte-for-byte. These
|
|
// encodings are the contract between A-Chain (this package) and the A<->C
|
|
// boundary (Blue-A's aivmbridge precompile on C-Chain). They MUST stay
|
|
// byte-identical on both sides or a receipt produced here will not verify under
|
|
// the receipt_root the boundary tracks, and an intent committed on C will not
|
|
// re-derive to the same intent_id here. quorum_wire_test.go asserts the exact
|
|
// bytes (length + golden keccak) so any drift is caught at build time.
|
|
//
|
|
// keccak256 is luxfi/crypto.Keccak256 — the canonical keccak in the Lux stack
|
|
// (the same primitive geth-side code uses), so a digest computed here equals one
|
|
// computed on the C side bit-for-bit.
|
|
|
|
import (
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/crypto"
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// Domain separators (raw UTF-8 bytes, no length prefix — concatenated verbatim).
|
|
// They keep the intent-id keyspace and the receipt-hash keyspace disjoint, and
|
|
// version the wire so a future v2 cannot collide with v1.
|
|
const (
|
|
DomainIntent = "lux/aivmbridge/intent/v1"
|
|
DomainReceipt = "lux/aivmbridge/receipt/v1"
|
|
)
|
|
|
|
// Receipt status codes (the AInferenceReceipt.Status byte). Pinned values shared
|
|
// with the boundary.
|
|
const (
|
|
StatusUnknown uint8 = 0
|
|
StatusPending uint8 = 1
|
|
StatusCompleted uint8 = 2
|
|
StatusFailed uint8 = 3
|
|
StatusChallenged uint8 = 4
|
|
)
|
|
|
|
// ReceiptEncodedLen is the exact byte length of the canonical AInferenceReceipt
|
|
// encoding (see EncodeReceipt). Pinned so a drift in field widths/order is a
|
|
// compile/test failure, not a silent cross-chain mismatch.
|
|
//
|
|
// u16(Version)2 + IntentID32 + TaskID32 + CChainID32 + AChainID32 +
|
|
// Requester20 + ModelSpecHash32 + PromptHash32 + CanonicalOutputHash32 +
|
|
// u8(Status)1 + u16(N)2 + u16(Threshold)2 + WinnersRoot32 + OperatorsRoot32 +
|
|
// u256(FeePaid)32 + u64(SettledAtHeight)8
|
|
const ReceiptEncodedLen = 2 + 32 + 32 + 32 + 32 + 20 + 32 + 32 + 32 + 1 + 2 + 2 + 32 + 32 + 32 + 8 // = 355
|
|
|
|
// ComputeIntentID derives the cross-chain intent id from the COMMITTED C-Chain
|
|
// intent fields, exactly:
|
|
//
|
|
// keccak256( DomainIntent ||
|
|
// c_chain_id(32) || a_chain_id(32) || c_tx_hash(32) || u32be(call_index) ||
|
|
// caller(20) || model_spec_hash(32) || prompt_hash(32) ||
|
|
// u16be(N) || u16be(threshold) || u256be(fee,32) )
|
|
//
|
|
// Every field is fixed-width in this precise order. The A-Chain importer
|
|
// recomputes this from the delivered fields and rejects the intent unless it
|
|
// equals the id the C side committed — so a forged/tampered intent (any field
|
|
// altered) yields a different id and cannot create a task.
|
|
func ComputeIntentID(cChainID, aChainID, cTxHash common.Hash, callIndex uint32, caller common.Address, modelSpecHash, promptHash common.Hash, n, threshold uint16, fee *uint256.Int) common.Hash {
|
|
buf := make([]byte, 0, len(DomainIntent)+32*3+4+20+32*2+2+2+32)
|
|
buf = append(buf, []byte(DomainIntent)...)
|
|
buf = append(buf, cChainID.Bytes()...)
|
|
buf = append(buf, aChainID.Bytes()...)
|
|
buf = append(buf, cTxHash.Bytes()...)
|
|
buf = append(buf, u32be(callIndex)...)
|
|
buf = append(buf, caller.Bytes()...)
|
|
buf = append(buf, modelSpecHash.Bytes()...)
|
|
buf = append(buf, promptHash.Bytes()...)
|
|
buf = append(buf, u16be(n)...)
|
|
buf = append(buf, u16be(threshold)...)
|
|
buf = append(buf, u256be(fee)...)
|
|
return common.BytesToHash(crypto.Keccak256(buf))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// fixed-width encoders (the only place width/endianness is decided)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
func u16be(v uint16) []byte { return []byte{byte(v >> 8), byte(v)} }
|
|
|
|
func u32be(v uint32) []byte {
|
|
return []byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}
|
|
}
|
|
|
|
func u64be(v uint64) []byte {
|
|
return []byte{
|
|
byte(v >> 56), byte(v >> 48), byte(v >> 40), byte(v >> 32),
|
|
byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v),
|
|
}
|
|
}
|
|
|
|
// u256be returns the 32-byte big-endian encoding of a uint256 (nil -> zero).
|
|
func u256be(v *uint256.Int) []byte {
|
|
if v == nil {
|
|
return make([]byte, 32)
|
|
}
|
|
b := v.Bytes32()
|
|
return b[:]
|
|
}
|
|
|
|
// h32 packs a uint256 into a 32-byte hash (big-endian).
|
|
func h32(v *uint256.Int) common.Hash {
|
|
b := v.Bytes32()
|
|
return common.BytesToHash(b[:])
|
|
}
|
|
|
|
// readUint reads a uint256 stored at a slot (zero if unset).
|
|
func readUint(st QuorumState, slot common.Hash) *uint256.Int {
|
|
return new(uint256.Int).SetBytes(st.GetState(slot).Bytes())
|
|
}
|
|
|
|
// readUint64 reads a uint64-valued slot (low 64 bits of the stored uint256).
|
|
func readUint64(st QuorumState, slot common.Hash) uint64 {
|
|
return readUint(st, slot).Uint64()
|
|
}
|
|
|
|
// isSet reports whether a slot holds a non-zero value.
|
|
func isSet(h common.Hash) bool { return h != (common.Hash{}) }
|
|
|
|
// oneHash is the canonical "true" flag value.
|
|
func oneHash() common.Hash {
|
|
var w [32]byte
|
|
w[31] = 1
|
|
return common.BytesToHash(w[:])
|
|
}
|
|
|
|
// bytesLess is the deterministic big-endian ordering on hashes (tie-break in
|
|
// plurality, leaf ordering elsewhere). Total order, identical on every node.
|
|
func bytesLess(a, b common.Hash) bool {
|
|
ab, bb := a.Bytes(), b.Bytes()
|
|
for i := 0; i < len(ab); i++ {
|
|
if ab[i] != bb[i] {
|
|
return ab[i] < bb[i]
|
|
}
|
|
}
|
|
return false
|
|
}
|