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]>
101 lines
3.7 KiB
Go
101 lines
3.7 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package aivm
|
|
|
|
// commit_reveal.go is the two-phase commit-reveal that prevents operators from
|
|
// copying each other's answers:
|
|
//
|
|
// - CommitResponse: a SELECTED operator posts a commit hash during the commit
|
|
// window (height <= commitDeadline). One commit per operator per task.
|
|
// - RevealResponse: during the reveal window (commitDeadline < height <=
|
|
// revealDeadline, i.e. strictly AFTER the commit window closes) the operator
|
|
// reveals (output_hash, embedding_hash, nonce); the engine RECOMPUTES the
|
|
// commit (ComputeCommit, operator-bound) and rejects unless it equals the
|
|
// stored commit. One reveal per operator.
|
|
//
|
|
// Because the reveal window opens only after the commit window closes, no
|
|
// operator can observe a peer's revealed output before its own commit is sealed;
|
|
// and because the commit binds the operator address, a copied commit cannot be
|
|
// revealed by anyone but its author.
|
|
|
|
import (
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/geth/common"
|
|
)
|
|
|
|
// CommitResponse records a selected operator's commit hash within the commit
|
|
// window.
|
|
func (e *Engine) CommitResponse(st QuorumState, taskID common.Hash, operator common.Address, commit common.Hash, height uint64) error {
|
|
if commit == (common.Hash{}) {
|
|
return ErrEmptyCommit
|
|
}
|
|
task := readTask(st, taskID)
|
|
if task.Status == TaskNone {
|
|
return ErrTaskUnknown
|
|
}
|
|
if task.Status != TaskCommitting {
|
|
return ErrTaskNotCommitting
|
|
}
|
|
if height > task.CommitDeadline {
|
|
return ErrCommitClosed
|
|
}
|
|
if !e.IsSelected(st, taskID, operator) {
|
|
return ErrNotSelected
|
|
}
|
|
commitSlot := slotHashAddr(nsCommit, taskID, operator)
|
|
if isSet(st.GetState(commitSlot)) {
|
|
return ErrAlreadyCommitted
|
|
}
|
|
st.SetState(commitSlot, commit)
|
|
return nil
|
|
}
|
|
|
|
// RevealResponse records a selected operator's revealed (output_hash,
|
|
// embedding_hash, nonce) within the reveal window, requiring the recomputed
|
|
// operator-bound commit to equal the stored commit.
|
|
func (e *Engine) RevealResponse(st QuorumState, taskID common.Hash, operator common.Address, outputHash, embeddingHash, nonce common.Hash, height uint64) error {
|
|
if outputHash == (common.Hash{}) {
|
|
return ErrEmptyOutputHash
|
|
}
|
|
task := readTask(st, taskID)
|
|
if task.Status == TaskNone {
|
|
return ErrTaskUnknown
|
|
}
|
|
if task.Status != TaskCommitting {
|
|
return ErrTaskNotCommitting
|
|
}
|
|
if height <= task.CommitDeadline {
|
|
return ErrRevealNotOpen
|
|
}
|
|
if height > task.RevealDeadline {
|
|
return ErrRevealClosed
|
|
}
|
|
commitSlot := slotHashAddr(nsCommit, taskID, operator)
|
|
stored := st.GetState(commitSlot)
|
|
if !isSet(stored) {
|
|
return ErrNotCommitted
|
|
}
|
|
revealFlagSlot := slotHashAddr(nsRevealFlag, taskID, operator)
|
|
if isSet(st.GetState(revealFlagSlot)) {
|
|
return ErrAlreadyRevealed
|
|
}
|
|
// Recompute the operator-bound commit; must match exactly.
|
|
recomputed := ComputeCommit(taskID, task.ModelSpecHash, task.PromptHash, outputHash, embeddingHash, operator, nonce)
|
|
if recomputed != stored {
|
|
return ErrCommitMismatch
|
|
}
|
|
// Record the reveal: output_hash, the revealed flag, and append the operator
|
|
// to the per-task revealer array (for tally at Settle).
|
|
st.SetState(slotHashAddr(nsReveal, taskID, operator), outputHash)
|
|
st.SetState(revealFlagSlot, oneHash())
|
|
cnt := revealCount(st, taskID)
|
|
st.SetState(slotHashIdx(nsRevealList, taskID, cnt), common.BytesToHash(common.LeftPadBytes(operator.Bytes(), 32)))
|
|
st.SetState(slotHash(nsRevealCount, taskID), h32(uint256.NewInt(uint64(cnt)+1)))
|
|
return nil
|
|
}
|
|
|
|
func revealCount(st QuorumState, taskID common.Hash) uint32 {
|
|
return uint32(new(uint256.Int).SetBytes(st.GetState(slotHash(nsRevealCount, taskID)).Bytes()).Uint64())
|
|
}
|