mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
markSpent was an unauthenticated public selector on the double-spend guard. workId = BLAKE3(deviceId‖nonce‖chainId) is computable offline, so any address could mark any claim spent before its owner presented it — denying the reward, and, where a missing claim reads as a missing attestation, forcing a slash. The Solidity interface has always documented "only callable by authorized contracts"; no check existed and ErrUnauthorized sat declared and unused. It now fails closed: the atomic paths mark spent internally as the last step of admitting a claim, which is the only safe ordering, so nothing legitimate is lost and there is no authority to guess at. The mint paths already require a TEE quote and bind it to the signing key (ErrMissingAttestation / ErrDeviceKeyMismatch), which establishes that a real device signed the claim. It does not establish that the claim is true: an attested device still writes its own computeMinutes and dataSize, and the reward schedule multiplies them. Unbounded, one sovereign-privacy data claim settles 1e18 * (2^64-1) * 1.5 / 1e4 = 2.767e19 AI. Both magnitudes are now bounded per claim — a ceiling on how wrong one claim can be, not a substitute for the binding above. Co-authored-by: Hanzo Dev <[email protected]>
155 lines
5.3 KiB
Go
155 lines
5.3 KiB
Go
// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package ai
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math/big"
|
|
"testing"
|
|
)
|
|
|
|
func makeWorkProof(privacy uint16, mins uint32) []byte {
|
|
p := make([]byte, WorkProofMinSize)
|
|
p[31] = 0xAA // deviceId (last byte)
|
|
p[63] = 0xBB // nonce (last byte)
|
|
binary.BigEndian.PutUint64(p[64:72], 1718000000) // timestamp
|
|
binary.BigEndian.PutUint16(p[72:74], privacy)
|
|
binary.BigEndian.PutUint32(p[74:78], mins)
|
|
return p
|
|
}
|
|
|
|
func TestMintWorkAndDoubleSpend(t *testing.T) {
|
|
st := NewMockStateDB()
|
|
proof := makeWorkProof(3, 60) // confidential, 60 compute-minutes
|
|
chainId := uint64(200202)
|
|
|
|
want, err := CalculateReward(proof, chainId)
|
|
if err != nil {
|
|
t.Fatalf("CalculateReward: %v", err)
|
|
}
|
|
|
|
reward, err := mintWork(st, proof, chainId)
|
|
if err != nil {
|
|
t.Fatalf("mintWork: %v", err)
|
|
}
|
|
if reward.Sign() <= 0 || reward.Cmp(want) != 0 {
|
|
t.Fatalf("reward = %s, want %s", reward, want)
|
|
}
|
|
|
|
// Same work, same chain → double-spend rejected.
|
|
if _, err := mintWork(st, proof, chainId); err != ErrWorkAlreadySpent {
|
|
t.Fatalf("double-spend: got %v, want ErrWorkAlreadySpent", err)
|
|
}
|
|
|
|
// Same work on a DIFFERENT chain is a distinct workId (chainId-bound) → allowed.
|
|
if _, err := mintWork(st, proof, chainId+1); err != nil {
|
|
t.Fatalf("cross-chain work should be distinct: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMintDataAndDoubleSpend(t *testing.T) {
|
|
st := NewMockStateDB()
|
|
desc := make([]byte, 42)
|
|
desc[31] = 0xCD // dataHash (last byte)
|
|
binary.BigEndian.PutUint64(desc[32:40], 1000) // dataSize units
|
|
binary.BigEndian.PutUint16(desc[40:42], 3) // privacy: confidential
|
|
chainId := uint64(200202)
|
|
|
|
reward, err := mintData(st, desc, chainId)
|
|
if err != nil {
|
|
t.Fatalf("mintData: %v", err)
|
|
}
|
|
want := new(big.Int).Set(baseRewardPerMinute)
|
|
want.Mul(want, big.NewInt(1000))
|
|
want.Mul(want, big.NewInt(int64(privacyMultipliers[3])))
|
|
want.Div(want, big.NewInt(10000))
|
|
if reward.Sign() <= 0 || reward.Cmp(want) != 0 {
|
|
t.Fatalf("data reward = %s, want %s", reward, want)
|
|
}
|
|
|
|
// Same dataset → double-claim rejected.
|
|
if _, err := mintData(st, desc, chainId); err != ErrWorkAlreadySpent {
|
|
t.Fatalf("double-claim data: got %v, want ErrWorkAlreadySpent", err)
|
|
}
|
|
}
|
|
|
|
func TestParseTriple(t *testing.T) {
|
|
a, b, c := []byte("workproof-blob"), []byte("pubkey"), []byte("signature-bytes")
|
|
var in []byte
|
|
put := func(blob []byte) {
|
|
var l [4]byte
|
|
binary.BigEndian.PutUint32(l[:], uint32(len(blob)))
|
|
in = append(in, l[:]...)
|
|
in = append(in, blob...)
|
|
}
|
|
put(a)
|
|
put(b)
|
|
put(c)
|
|
var cid [8]byte
|
|
binary.BigEndian.PutUint64(cid[:], 200202)
|
|
in = append(in, cid[:]...)
|
|
|
|
ga, gb, gc, gcid, err := parseTriple(in)
|
|
if err != nil {
|
|
t.Fatalf("parseTriple: %v", err)
|
|
}
|
|
if string(ga) != string(a) || string(gb) != string(b) || string(gc) != string(c) || gcid != 200202 {
|
|
t.Fatalf("parseTriple mismatch: %q %q %q %d", ga, gb, gc, gcid)
|
|
}
|
|
if _, _, _, _, err := parseTriple(in[:len(in)-2]); err == nil {
|
|
t.Fatal("expected truncated parseTriple to error")
|
|
}
|
|
}
|
|
|
|
// makeDataDescriptor builds the 42-byte contribution header: dataHash(32) |
|
|
// dataSize(8) | privacyLevel(2).
|
|
func makeDataDescriptor(size uint64, privacy uint16) []byte {
|
|
d := make([]byte, DataContributionSize)
|
|
d[31] = 0xCC
|
|
binary.BigEndian.PutUint64(d[32:40], size)
|
|
binary.BigEndian.PutUint16(d[40:42], privacy)
|
|
return d
|
|
}
|
|
|
|
// A magnitude bound is what stops one attested-but-lying device from settling
|
|
// an unbounded reward. The device binding establishes that a real,
|
|
// embedded-root-certified device signed the claim; it does NOT establish that
|
|
// the size that device wrote is honest. Unbounded, a single sovereign-privacy
|
|
// data claim settles 1e18 * (2^64-1) * 1.5 / 1e4 = 2.767e19 AI in one call.
|
|
func TestClaimMagnitudeIsBounded(t *testing.T) {
|
|
chainId := uint64(200202)
|
|
|
|
t.Run("compute minutes above the bound are refused", func(t *testing.T) {
|
|
if _, err := CalculateReward(makeWorkProof(PrivacySovereign, MaxComputeMinutes+1), chainId); err != ErrClaimTooLarge {
|
|
t.Fatalf("got %v, want ErrClaimTooLarge", err)
|
|
}
|
|
// The uint32 maximum was the real exposure, not an off-by-one.
|
|
if _, err := CalculateReward(makeWorkProof(PrivacySovereign, ^uint32(0)), chainId); err != ErrClaimTooLarge {
|
|
t.Fatalf("max uint32: got %v, want ErrClaimTooLarge", err)
|
|
}
|
|
})
|
|
|
|
t.Run("compute minutes at the bound are admitted", func(t *testing.T) {
|
|
if _, err := CalculateReward(makeWorkProof(PrivacySovereign, MaxComputeMinutes), chainId); err != nil {
|
|
t.Fatalf("boundary claim refused: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("data size above the bound is refused", func(t *testing.T) {
|
|
if _, err := mintData(NewMockStateDB(), makeDataDescriptor(MaxDataSize+1, PrivacySovereign), chainId); err != ErrClaimTooLarge {
|
|
t.Fatalf("got %v, want ErrClaimTooLarge", err)
|
|
}
|
|
// The uint64 maximum is the 2.767e19 AI claim.
|
|
if _, err := mintData(NewMockStateDB(), makeDataDescriptor(^uint64(0), PrivacySovereign), chainId); err != ErrClaimTooLarge {
|
|
t.Fatalf("max uint64: got %v, want ErrClaimTooLarge", err)
|
|
}
|
|
})
|
|
|
|
t.Run("data size at the bound is admitted", func(t *testing.T) {
|
|
if _, err := mintData(NewMockStateDB(), makeDataDescriptor(MaxDataSize, PrivacySovereign), chainId); err != nil {
|
|
t.Fatalf("boundary claim refused: %v", err)
|
|
}
|
|
})
|
|
}
|