mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
aivm: an error that says the opposite of what happened, and a guard that guarded nothing
Two of the fifteen defects a probe suite confirmed and nobody had fixed. Both are self-contained; the rest are one architectural change to the staging layer and belong together. WithdrawStake refused an operator that was NOT unbonding and reported ErrOperatorUnbonding -- the opposite of what had happened. An operator reading it would conclude it had already deregistered and wait out a cooldown that never started. Its own comment said "must Deregister first", which is what the error now says. The existing test asserted the wrong constant while its comment described the right behaviour, so the assertion was updated to match the comment rather than the code. Initialize guarded the runtime for one field and dereferenced it on the next line, so the guard read as defensive and the nil panic happened anyway. A VM with no runtime has no logger, no network and no identity, so it refuses. Still open, and deliberately not touched here: the tip is never persisted, the staging layer is shared across in-flight blocks so Accept commits a sibling's writes and Reject drops them, Accept commits engine state before storing the block, there is no tip check, no size bound, no chain binding, no timestamp bounds, and a Merkle proof verifies at an index outside its tree. Those are one job, not nine, and the shape is already settled in fhevm and chain/.
This commit is contained in:
+41
-41
@@ -237,27 +237,27 @@ type ModelRegistryEntry struct {
|
||||
|
||||
// AuditAnchor mirrors aivm::cuda::AuditAnchor (128 bytes).
|
||||
type AuditAnchor struct {
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Occupied uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Occupied uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
}
|
||||
|
||||
// AIVMEpochState mirrors aivm::cuda::AIVMEpochState (160 bytes).
|
||||
type AIVMEpochState struct {
|
||||
CurrentEpoch uint64
|
||||
NextEpochHeight uint64
|
||||
TotalActiveAttestations uint64
|
||||
ActiveModelCount uint32
|
||||
ExpiredAttestationCount uint32
|
||||
AttestationRoot [32]byte
|
||||
ModelRegistryRoot [32]byte
|
||||
AuditRoot [32]byte
|
||||
AIVMStateRoot [32]byte
|
||||
CurrentEpoch uint64
|
||||
NextEpochHeight uint64
|
||||
TotalActiveAttestations uint64
|
||||
ActiveModelCount uint32
|
||||
ExpiredAttestationCount uint32
|
||||
AttestationRoot [32]byte
|
||||
ModelRegistryRoot [32]byte
|
||||
AuditRoot [32]byte
|
||||
AIVMStateRoot [32]byte
|
||||
}
|
||||
|
||||
// AIVMRoundDescriptor mirrors aivm::cuda::AIVMRoundDescriptor (96 bytes).
|
||||
@@ -308,14 +308,14 @@ type ModelOp struct {
|
||||
|
||||
// AnchorOp mirrors aivm::cuda::AnchorOp (128 bytes).
|
||||
type AnchorOp struct {
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Epoch uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Epoch uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
}
|
||||
|
||||
// AIVMTransitionResult mirrors aivm::cuda::AIVMTransitionResult (192 bytes).
|
||||
@@ -377,12 +377,12 @@ type InferenceOp struct {
|
||||
|
||||
// InferenceResult mirrors aivm::cuda::InferenceResult (112 bytes).
|
||||
type InferenceResult struct {
|
||||
Status uint32
|
||||
OutputLen uint32
|
||||
InputCommitment [32]byte
|
||||
OutputCommitment [32]byte
|
||||
AttestationRoot [32]byte
|
||||
_pad0 [8]uint8
|
||||
Status uint32
|
||||
OutputLen uint32
|
||||
InputCommitment [32]byte
|
||||
OutputCommitment [32]byte
|
||||
AttestationRoot [32]byte
|
||||
_pad0 [8]uint8
|
||||
}
|
||||
|
||||
// ProofVerifyOp mirrors aivm::cuda::ProofVerifyOp (240 bytes).
|
||||
@@ -451,16 +451,16 @@ func init() {
|
||||
// stored at package level by backend.go's init(); call ActiveGPUBackend()
|
||||
// to retrieve it.
|
||||
type GPUBackend struct {
|
||||
mu sync.Mutex
|
||||
kind BackendKind
|
||||
handle unsafe.Pointer // dlopen result
|
||||
path string
|
||||
fnAttest unsafe.Pointer
|
||||
fnProv unsafe.Pointer
|
||||
fnAnchor unsafe.Pointer
|
||||
fnEpoch unsafe.Pointer
|
||||
fnInfer unsafe.Pointer
|
||||
fnProof unsafe.Pointer
|
||||
mu sync.Mutex
|
||||
kind BackendKind
|
||||
handle unsafe.Pointer // dlopen result
|
||||
path string
|
||||
fnAttest unsafe.Pointer
|
||||
fnProv unsafe.Pointer
|
||||
fnAnchor unsafe.Pointer
|
||||
fnEpoch unsafe.Pointer
|
||||
fnInfer unsafe.Pointer
|
||||
fnProof unsafe.Pointer
|
||||
}
|
||||
|
||||
// Kind returns which backend satisfied the dlopen probe.
|
||||
|
||||
+31
-31
@@ -91,27 +91,27 @@ type ModelRegistryEntry struct {
|
||||
|
||||
// AuditAnchor mirrors the cgo build's layout.
|
||||
type AuditAnchor struct {
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Occupied uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Occupied uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
}
|
||||
|
||||
// AIVMEpochState mirrors the cgo build's layout.
|
||||
type AIVMEpochState struct {
|
||||
CurrentEpoch uint64
|
||||
NextEpochHeight uint64
|
||||
TotalActiveAttestations uint64
|
||||
ActiveModelCount uint32
|
||||
ExpiredAttestationCount uint32
|
||||
AttestationRoot [32]byte
|
||||
ModelRegistryRoot [32]byte
|
||||
AuditRoot [32]byte
|
||||
AIVMStateRoot [32]byte
|
||||
CurrentEpoch uint64
|
||||
NextEpochHeight uint64
|
||||
TotalActiveAttestations uint64
|
||||
ActiveModelCount uint32
|
||||
ExpiredAttestationCount uint32
|
||||
AttestationRoot [32]byte
|
||||
ModelRegistryRoot [32]byte
|
||||
AuditRoot [32]byte
|
||||
AIVMStateRoot [32]byte
|
||||
}
|
||||
|
||||
// AIVMRoundDescriptor mirrors the cgo build's layout.
|
||||
@@ -162,14 +162,14 @@ type ModelOp struct {
|
||||
|
||||
// AnchorOp mirrors the cgo build's layout.
|
||||
type AnchorOp struct {
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Epoch uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
CommitRoot [32]byte
|
||||
ParentRoot [32]byte
|
||||
ValidatorSetRootAtCommit [32]byte
|
||||
Height uint64
|
||||
TimestampNS uint64
|
||||
Epoch uint32
|
||||
_pad0 uint32
|
||||
_pad1 uint64
|
||||
}
|
||||
|
||||
// AIVMTransitionResult mirrors the cgo build's layout.
|
||||
@@ -231,12 +231,12 @@ type InferenceOp struct {
|
||||
|
||||
// InferenceResult mirrors the cgo build's layout.
|
||||
type InferenceResult struct {
|
||||
Status uint32
|
||||
OutputLen uint32
|
||||
InputCommitment [32]byte
|
||||
OutputCommitment [32]byte
|
||||
AttestationRoot [32]byte
|
||||
_pad0 [8]uint8
|
||||
Status uint32
|
||||
OutputLen uint32
|
||||
InputCommitment [32]byte
|
||||
OutputCommitment [32]byte
|
||||
AttestationRoot [32]byte
|
||||
_pad0 [8]uint8
|
||||
}
|
||||
|
||||
// ProofVerifyOp mirrors the cgo build's layout.
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/chains/aivm"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/sys/ulimit"
|
||||
"github.com/luxfi/vm/chain"
|
||||
"github.com/luxfi/vm/rpc"
|
||||
|
||||
@@ -47,12 +47,12 @@ import (
|
||||
// operator (carried alongside the intent; the C-side Fee is the user-facing fee
|
||||
// that funds it + the burn).
|
||||
type CIntent struct {
|
||||
IntentID common.Hash `json:"intentId"` // committed on C; must equal ComputeIntentID(...)
|
||||
IntentID common.Hash `json:"intentId"` // committed on C; must equal ComputeIntentID(...)
|
||||
CChainID common.Hash `json:"cChainId"`
|
||||
AChainID common.Hash `json:"aChainId"`
|
||||
CTxHash common.Hash `json:"cTxHash"` // C tx that emitted the intent
|
||||
CallIndex uint32 `json:"callIndex"` // index within that tx
|
||||
Caller common.Address `json:"caller"` // the C-side requester
|
||||
CTxHash common.Hash `json:"cTxHash"` // C tx that emitted the intent
|
||||
CallIndex uint32 `json:"callIndex"` // index within that tx
|
||||
Caller common.Address `json:"caller"` // the C-side requester
|
||||
ModelSpecHash common.Hash `json:"modelSpecHash"`
|
||||
PromptHash common.Hash `json:"promptHash"`
|
||||
N uint16 `json:"n"`
|
||||
|
||||
+5
-1
@@ -184,7 +184,11 @@ func (e *Engine) WithdrawStake(st QuorumState, lg QuorumLedger, operator common.
|
||||
return nil, ErrOperatorUnknown
|
||||
}
|
||||
if !rec.Unbonding {
|
||||
return nil, ErrOperatorUnbonding // must Deregister first
|
||||
// The operator is still bonded, so there is nothing to withdraw yet.
|
||||
// Reporting ErrOperatorUnbonding here said the opposite of what had
|
||||
// happened, and an operator reading it would conclude it had already
|
||||
// deregistered.
|
||||
return nil, ErrOperatorBonded
|
||||
}
|
||||
if block < rec.UnbondBlock+UnbondCooldownBlocks {
|
||||
return nil, ErrCooldownActive
|
||||
|
||||
+23
-20
@@ -15,7 +15,10 @@ var (
|
||||
ErrOperatorExists = errors.New("aivm/quorum: operator already registered")
|
||||
ErrOperatorUnknown = errors.New("aivm/quorum: operator not registered")
|
||||
ErrOperatorUnbonding = errors.New("aivm/quorum: operator is unbonding")
|
||||
ErrCooldownActive = errors.New("aivm/quorum: unbond cooldown not elapsed")
|
||||
// ErrOperatorBonded is the converse: the operator has not deregistered,
|
||||
// so no cooldown has started and there is no stake to withdraw.
|
||||
ErrOperatorBonded = errors.New("aivm/quorum: operator is still bonded")
|
||||
ErrCooldownActive = errors.New("aivm/quorum: unbond cooldown not elapsed")
|
||||
|
||||
// custody / conservation
|
||||
ErrInsufficientFunds = errors.New("aivm/quorum: insufficient balance")
|
||||
@@ -35,27 +38,27 @@ var (
|
||||
ErrEligibleBelowMargin = errors.New("aivm/quorum: eligible operator set below N + required margin")
|
||||
|
||||
// task lifecycle
|
||||
ErrTaskUnknown = errors.New("aivm/quorum: task not found")
|
||||
ErrTaskNotCommitting = errors.New("aivm/quorum: task not in committing state")
|
||||
ErrTaskUnknown = errors.New("aivm/quorum: task not found")
|
||||
ErrTaskNotCommitting = errors.New("aivm/quorum: task not in committing state")
|
||||
ErrTaskAlreadySettled = errors.New("aivm/quorum: task already settled")
|
||||
ErrNotSelected = errors.New("aivm/quorum: operator not selected for task")
|
||||
ErrCommitClosed = errors.New("aivm/quorum: commit window closed")
|
||||
ErrAlreadyCommitted = errors.New("aivm/quorum: operator already committed")
|
||||
ErrNotCommitted = errors.New("aivm/quorum: operator did not commit")
|
||||
ErrRevealNotOpen = errors.New("aivm/quorum: reveal window not open")
|
||||
ErrRevealClosed = errors.New("aivm/quorum: reveal window closed")
|
||||
ErrAlreadyRevealed = errors.New("aivm/quorum: operator already revealed")
|
||||
ErrCommitMismatch = errors.New("aivm/quorum: reveal does not match commit")
|
||||
ErrEmptyCommit = errors.New("aivm/quorum: empty commit hash")
|
||||
ErrEmptyOutputHash = errors.New("aivm/quorum: empty output hash")
|
||||
ErrSettleTooEarly = errors.New("aivm/quorum: reveal window not closed")
|
||||
ErrNotSelected = errors.New("aivm/quorum: operator not selected for task")
|
||||
ErrCommitClosed = errors.New("aivm/quorum: commit window closed")
|
||||
ErrAlreadyCommitted = errors.New("aivm/quorum: operator already committed")
|
||||
ErrNotCommitted = errors.New("aivm/quorum: operator did not commit")
|
||||
ErrRevealNotOpen = errors.New("aivm/quorum: reveal window not open")
|
||||
ErrRevealClosed = errors.New("aivm/quorum: reveal window closed")
|
||||
ErrAlreadyRevealed = errors.New("aivm/quorum: operator already revealed")
|
||||
ErrCommitMismatch = errors.New("aivm/quorum: reveal does not match commit")
|
||||
ErrEmptyCommit = errors.New("aivm/quorum: empty commit hash")
|
||||
ErrEmptyOutputHash = errors.New("aivm/quorum: empty output hash")
|
||||
ErrSettleTooEarly = errors.New("aivm/quorum: reveal window not closed")
|
||||
|
||||
// cross-chain seam
|
||||
ErrIntentNilAmount = errors.New("aivm/quorum: C-chain intent has a nil fee or reward amount")
|
||||
ErrIntentIDMismatch = errors.New("aivm/quorum: recomputed intent_id does not match delivered id (forged/tampered intent)")
|
||||
ErrIntentNotCommitted = errors.New("aivm/quorum: C-chain intent is not committed (proof rejected)")
|
||||
ErrIntentAlreadyUsed = errors.New("aivm/quorum: C-chain intent already consumed (replay)")
|
||||
ErrReceiptNotFound = errors.New("aivm/quorum: no settled receipt for intent")
|
||||
ErrProofOutOfRange = errors.New("aivm/quorum: receipt index out of range")
|
||||
ErrIntentNilAmount = errors.New("aivm/quorum: C-chain intent has a nil fee or reward amount")
|
||||
ErrIntentIDMismatch = errors.New("aivm/quorum: recomputed intent_id does not match delivered id (forged/tampered intent)")
|
||||
ErrIntentNotCommitted = errors.New("aivm/quorum: C-chain intent is not committed (proof rejected)")
|
||||
ErrIntentAlreadyUsed = errors.New("aivm/quorum: C-chain intent already consumed (replay)")
|
||||
ErrReceiptNotFound = errors.New("aivm/quorum: no settled receipt for intent")
|
||||
ErrProofOutOfRange = errors.New("aivm/quorum: receipt index out of range")
|
||||
ErrReceiptRootMismatch = errors.New("aivm/quorum: block receipt_root does not match re-derived engine state")
|
||||
)
|
||||
|
||||
+27
-27
@@ -196,33 +196,33 @@ func (l *MemLedger) Restore(snap map[common.Address]*uint256.Int) {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
nsOperator = []byte("av/op") // operator registry meta
|
||||
nsModelIndex = []byte("av/mspec.idx") // per-ModelSpec operator-array length
|
||||
nsModelMember = []byte("av/mspec.mem") // per-ModelSpec operator-array element
|
||||
nsModelSeen = []byte("av/mspec.seen") // per-(ModelSpec,operator) membership flag
|
||||
nsCredit = []byte("av/cred") // operator withdrawable credit ledger
|
||||
nsReqNonce = []byte("av/req.nonce") // requester monotonic nonce
|
||||
nsTask = []byte("av/task") // task record (status + params) *** also job_id domain
|
||||
nsTaskReward = []byte("av/task.reward") // task rewardPerOperator (uint256)
|
||||
nsTaskEscrow = []byte("av/task.escrow") // task remaining escrow (uint256)
|
||||
nsTaskIntent = []byte("av/task.intent") // task -> source C intent id
|
||||
nsTaskFee = []byte("av/task.fee") // task fee paid (uint256, for the receipt)
|
||||
nsSelected = []byte("av/sel") // per-(task,operator) selection flag
|
||||
nsSelList = []byte("av/sel.list") // per-task selected-operator-array element
|
||||
nsCommit = []byte("av/commit") // per-(task,operator) commit hash
|
||||
nsReveal = []byte("av/reveal") // per-(task,operator) revealed output_hash
|
||||
nsRevealFlag = []byte("av/reveal.f") // per-(task,operator) revealed flag
|
||||
nsRevealList = []byte("av/reveal.list") // per-task revealer-array element
|
||||
nsRevealCount = []byte("av/reveal.cnt") // per-task revealer count
|
||||
nsSettled = []byte("av/settled") // per-task settled marker (replay guard)
|
||||
nsCanonical = []byte("av/canon") // per-task canonical output_hash
|
||||
nsReceiptRoot = []byte("av/receipt.root")// running receipt_root accumulator
|
||||
nsReceiptIdx = []byte("av/receipt.idx") // count of settled receipts in the root
|
||||
nsReceiptLeaf = []byte("av/receipt.leaf")// settled receipt_hash by index (for proofs)
|
||||
nsIntentSeen = []byte("av/intent.seen") // per-C-intent consumed marker (anti-replay)
|
||||
nsIntentTask = []byte("av/intent.task") // per-C-intent -> A-chain task id
|
||||
nsIntentRcpt = []byte("av/intent.rcpt") // per-C-intent settled-receipt leaf index+1 (0 = unsettled)
|
||||
nsTaskHeight = []byte("av/task.height") // per-task settled height (so receipt is reproducible)
|
||||
nsOperator = []byte("av/op") // operator registry meta
|
||||
nsModelIndex = []byte("av/mspec.idx") // per-ModelSpec operator-array length
|
||||
nsModelMember = []byte("av/mspec.mem") // per-ModelSpec operator-array element
|
||||
nsModelSeen = []byte("av/mspec.seen") // per-(ModelSpec,operator) membership flag
|
||||
nsCredit = []byte("av/cred") // operator withdrawable credit ledger
|
||||
nsReqNonce = []byte("av/req.nonce") // requester monotonic nonce
|
||||
nsTask = []byte("av/task") // task record (status + params) *** also job_id domain
|
||||
nsTaskReward = []byte("av/task.reward") // task rewardPerOperator (uint256)
|
||||
nsTaskEscrow = []byte("av/task.escrow") // task remaining escrow (uint256)
|
||||
nsTaskIntent = []byte("av/task.intent") // task -> source C intent id
|
||||
nsTaskFee = []byte("av/task.fee") // task fee paid (uint256, for the receipt)
|
||||
nsSelected = []byte("av/sel") // per-(task,operator) selection flag
|
||||
nsSelList = []byte("av/sel.list") // per-task selected-operator-array element
|
||||
nsCommit = []byte("av/commit") // per-(task,operator) commit hash
|
||||
nsReveal = []byte("av/reveal") // per-(task,operator) revealed output_hash
|
||||
nsRevealFlag = []byte("av/reveal.f") // per-(task,operator) revealed flag
|
||||
nsRevealList = []byte("av/reveal.list") // per-task revealer-array element
|
||||
nsRevealCount = []byte("av/reveal.cnt") // per-task revealer count
|
||||
nsSettled = []byte("av/settled") // per-task settled marker (replay guard)
|
||||
nsCanonical = []byte("av/canon") // per-task canonical output_hash
|
||||
nsReceiptRoot = []byte("av/receipt.root") // running receipt_root accumulator
|
||||
nsReceiptIdx = []byte("av/receipt.idx") // count of settled receipts in the root
|
||||
nsReceiptLeaf = []byte("av/receipt.leaf") // settled receipt_hash by index (for proofs)
|
||||
nsIntentSeen = []byte("av/intent.seen") // per-C-intent consumed marker (anti-replay)
|
||||
nsIntentTask = []byte("av/intent.task") // per-C-intent -> A-chain task id
|
||||
nsIntentRcpt = []byte("av/intent.rcpt") // per-C-intent settled-receipt leaf index+1 (0 = unsettled)
|
||||
nsTaskHeight = []byte("av/task.height") // per-task settled height (so receipt is reproducible)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+1
-1
@@ -566,7 +566,7 @@ func TestUnstakeCooldown(t *testing.T) {
|
||||
|
||||
// cannot withdraw before deregister.
|
||||
_, err := e.WithdrawStake(st, lg, op, 1000)
|
||||
require.ErrorIs(err, ErrOperatorUnbonding)
|
||||
require.ErrorIs(err, ErrOperatorBonded)
|
||||
|
||||
require.NoError(e.DeregisterOperator(st, op, 100))
|
||||
require.False(e.IsEligible(st, op, modelSpec), "unbonding op is ineligible immediately")
|
||||
|
||||
+4
-4
@@ -32,8 +32,8 @@ const ReceiptVersion uint16 = 1
|
||||
// reordered.
|
||||
type AInferenceReceipt struct {
|
||||
Version uint16 `json:"version"`
|
||||
IntentID common.Hash `json:"intentId"` // source C-chain intent id
|
||||
TaskID common.Hash `json:"taskId"` // A-chain task id (beacon anchor)
|
||||
IntentID common.Hash `json:"intentId"` // source C-chain intent id
|
||||
TaskID common.Hash `json:"taskId"` // A-chain task id (beacon anchor)
|
||||
CChainID common.Hash `json:"cChainId"`
|
||||
AChainID common.Hash `json:"aChainId"`
|
||||
Requester common.Address `json:"requester"`
|
||||
@@ -43,8 +43,8 @@ type AInferenceReceipt struct {
|
||||
Status uint8 `json:"status"` // StatusCompleted / StatusFailed / ...
|
||||
N uint16 `json:"n"`
|
||||
Threshold uint16 `json:"threshold"`
|
||||
WinnersRoot common.Hash `json:"winnersRoot"` // merkle root over winner addresses
|
||||
OperatorsRoot common.Hash `json:"operatorsRoot"` // merkle root over selected addresses
|
||||
WinnersRoot common.Hash `json:"winnersRoot"` // merkle root over winner addresses
|
||||
OperatorsRoot common.Hash `json:"operatorsRoot"` // merkle root over selected addresses
|
||||
FeePaid *uint256.Int `json:"feePaid"`
|
||||
SettledAtHeight uint64 `json:"settledAtHeight"`
|
||||
}
|
||||
|
||||
@@ -150,4 +150,3 @@ func TestProbe_CommitRevealWindowsDisjoint(t *testing.T) {
|
||||
require.Equal(uint64(160), info.RevealDeadline)
|
||||
// commit window: [reqHeight, 130]; reveal window: (130, 160]. Disjoint at 130/131.
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -39,14 +39,14 @@ import (
|
||||
|
||||
// SettleResult is the outcome of Settle, returned for callers + tests.
|
||||
type SettleResult struct {
|
||||
Status TaskState // TaskSettled or TaskFailed
|
||||
CanonicalHash common.Hash // winning output_hash (zero if Failed)
|
||||
WinnerCount uint32 // size of the winning group (0 if Failed)
|
||||
Paid *uint256.Int // total wei paid out as rewards (0 if Failed)
|
||||
Slashed *uint256.Int // total wei slashed from withholders
|
||||
Status TaskState // TaskSettled or TaskFailed
|
||||
CanonicalHash common.Hash // winning output_hash (zero if Failed)
|
||||
WinnerCount uint32 // size of the winning group (0 if Failed)
|
||||
Paid *uint256.Int // total wei paid out as rewards (0 if Failed)
|
||||
Slashed *uint256.Int // total wei slashed from withholders
|
||||
Receipt AInferenceReceipt // the emitted cross-chain receipt
|
||||
ReceiptHash common.Hash // keccak(DomainReceipt || encoding)
|
||||
ReceiptRoot common.Hash // receipt_root AFTER folding this receipt
|
||||
ReceiptHash common.Hash // keccak(DomainReceipt || encoding)
|
||||
ReceiptRoot common.Hash // receipt_root AFTER folding this receipt
|
||||
}
|
||||
|
||||
// Settle finalizes a task. height must be > revealDeadline. Idempotent.
|
||||
|
||||
+10
-2
@@ -297,13 +297,21 @@ type ProviderReg struct {
|
||||
}
|
||||
|
||||
// Initialize initializes the VM with the unified Init struct
|
||||
// errRuntimeRequired is returned by Initialize when the consensus runtime is
|
||||
// absent. Every later method assumes it.
|
||||
var errRuntimeRequired = errors.New("aivm: Initialize requires a runtime")
|
||||
|
||||
func (vm *VM) Initialize(ctx context.Context, init vmcore.Init) error {
|
||||
vm.rt = init.Runtime
|
||||
vm.db = init.DB
|
||||
vm.toEngine = init.ToEngine
|
||||
if vm.rt != nil {
|
||||
vm.networkID = vm.rt.NetworkID
|
||||
// A VM with no runtime has no logger, no network and no node identity.
|
||||
// Guarding networkID and then dereferencing rt.Log on the next line meant
|
||||
// the guard read as defensive while the panic happened anyway.
|
||||
if vm.rt == nil {
|
||||
return errRuntimeRequired
|
||||
}
|
||||
vm.networkID = vm.rt.NetworkID
|
||||
|
||||
if logger, ok := vm.rt.Log.(log.Logger); ok {
|
||||
vm.log = logger
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package aivm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/luxfi/database"
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/runtime"
|
||||
vmcore "github.com/luxfi/vm"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// failDB fails every Put after the Nth.
|
||||
type failDB struct {
|
||||
database.Database
|
||||
mu sync.Mutex
|
||||
n int
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (d *failDB) Put(k, v []byte) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.fail {
|
||||
return errProbeDBDown
|
||||
}
|
||||
d.n++
|
||||
return d.Database.Put(k, v)
|
||||
}
|
||||
|
||||
var errProbeDBDown = &probeErr{"probe: db down"}
|
||||
|
||||
type probeErr struct{ s string }
|
||||
|
||||
func (e *probeErr) Error() string { return e.s }
|
||||
|
||||
// P5: Accept commits engine state, then fails to store the block. Torn?
|
||||
func TestProbe_P5_AcceptTornWrite(t *testing.T) {
|
||||
inner := memdb.New()
|
||||
db := &failDB{Database: inner}
|
||||
vm := &VM{}
|
||||
require.NoError(t, vm.Initialize(context.Background(), probeInit(db)))
|
||||
vm.SetCommitVerifier(VerifierFunc(func(CIntent) error { return nil }))
|
||||
ctx := context.Background()
|
||||
|
||||
reward := uint256.NewInt(1_000_000_000_000_000_000)
|
||||
e, st, _ := vm.QuorumEngine()
|
||||
seedOperators(t, vm, e, st, reward)
|
||||
intent := buildIntent(e, addrOf(0xF0), hashOf(0xAB), consciousN, consciousThreshold, uint256.NewInt(1), reward)
|
||||
vm.EnqueueCommittedIntent(intent)
|
||||
|
||||
blk, err := vm.BuildBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, blk.Verify(ctx))
|
||||
|
||||
db.mu.Lock()
|
||||
db.fail = true
|
||||
db.mu.Unlock()
|
||||
acceptErr := blk.Accept(ctx)
|
||||
db.mu.Lock()
|
||||
db.fail = false
|
||||
db.mu.Unlock()
|
||||
t.Logf("Accept with a failing DB = %v", acceptErr)
|
||||
|
||||
raw := NewDBState(inner)
|
||||
engineCommitted := isSet(raw.GetState(slotHash(nsIntentSeen, intent.IntentID)))
|
||||
head, _ := vm.LastAccepted(ctx)
|
||||
blockStored := false
|
||||
if _, err := func() ([]byte, error) { id := blk.ID(); return inner.Get(id[:]) }(); err == nil {
|
||||
blockStored = true
|
||||
}
|
||||
t.Logf("engineCommitted=%v blockStored=%v head=%s blkID=%s", engineCommitted, blockStored, head, blk.ID())
|
||||
if acceptErr != nil && engineCommitted && !blockStored {
|
||||
t.Errorf("P5 CONFIRMED: Accept failed but the engine delta is already durable — the block is gone and its state is not")
|
||||
}
|
||||
}
|
||||
|
||||
// P14: verifyImported tolerates ErrIntentAlreadyUsed. Can a proposer record the
|
||||
// same intent twice, or an intent an earlier block already consumed?
|
||||
func TestProbe_P14_DuplicateIntentInBlock(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
reward := uint256.NewInt(1_000_000_000_000_000_000)
|
||||
e, st, _ := vm.QuorumEngine()
|
||||
seedOperators(t, vm, e, st, reward)
|
||||
intent := buildIntent(e, addrOf(0xF0), hashOf(0xAB), consciousN, consciousThreshold, uint256.NewInt(1), reward)
|
||||
|
||||
// A hand-built block recording the same intent 3 times.
|
||||
blk := &Block{ParentID_: vm.lastAcceptedID, Height_: 1,
|
||||
ImportedIntents: []CIntent{intent, intent, intent}, vm: vm}
|
||||
// The proposer stamps whatever root its own run produced; compute it honestly.
|
||||
require.NoError(t, blk.Verify(ctx))
|
||||
t.Logf("a block recording the same intent 3x verified (receiptRoot=%s)", blk.ReceiptRoot.Hex())
|
||||
n := e.LiveTasks(st)
|
||||
if n != 1 {
|
||||
t.Errorf("P14: %d tasks created from one intent recorded 3x", n)
|
||||
} else {
|
||||
t.Logf("P14 benign: duplicate records collapse to 1 task (anti-replay held)")
|
||||
}
|
||||
}
|
||||
|
||||
// P15: concurrent reads of vm.running against Shutdown.
|
||||
func TestProbe_P15_RunningRace(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 8; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 200; j++ {
|
||||
_ = vm.GetProviders()
|
||||
_, _ = vm.HealthCheck(ctx)
|
||||
_ = vm.GetMerkleRoot()
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); _ = vm.Shutdown(ctx) }()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// P16: settleDue runs on both build and verify at the same height. Does the
|
||||
// second pass over already-settled tasks change anything?
|
||||
func TestProbe_P16_DoubleSettlePass(t *testing.T) {
|
||||
e := NewEngine(hashOf(1), hashOf(2))
|
||||
st := NewMemState()
|
||||
reward := uint256.NewInt(1_000_000_000_000_000_000)
|
||||
requester := addrOf(0xF0)
|
||||
opening := map[common.Address]*uint256.Int{requester: new(uint256.Int).Mul(reward, uint256.NewInt(64))}
|
||||
ops := make([]common.Address, 8)
|
||||
for i := range ops {
|
||||
ops[i] = addrOf(byte(0x10 + i))
|
||||
opening[ops[i]] = new(uint256.Int).Mul(MinProviderBond, uint256.NewInt(3))
|
||||
}
|
||||
lg := NewMemLedger(opening)
|
||||
for i, op := range ops {
|
||||
require.NoError(t, e.RegisterOperator(st, lg, op, new(uint256.Int).Mul(MinProviderBond, uint256.NewInt(2)), hashOf(0xAB), hashOf(byte(0x80+i))))
|
||||
}
|
||||
in := buildIntent(e, requester, hashOf(0xAB), 5, 3, uint256.NewInt(1), reward)
|
||||
taskID, err := e.ImportCommittedIntent(st, lg, VerifierFunc(func(CIntent) error { return nil }), in, 1)
|
||||
require.NoError(t, err)
|
||||
sel, _ := e.SelectOperators(st, taskID, hashOf(0xAB), 5)
|
||||
out := hashOf(0x42)
|
||||
for i := 0; i < 3; i++ {
|
||||
c := ComputeCommit(taskID, hashOf(0xAB), hashOf(0xCD), out, hashOf(7), sel[i], hashOf(9))
|
||||
require.NoError(t, e.CommitResponse(st, taskID, sel[i], c, 2))
|
||||
require.NoError(t, e.RevealResponse(st, taskID, sel[i], out, hashOf(7), hashOf(9), 32))
|
||||
}
|
||||
total := lg.Total()
|
||||
n1 := e.SettleDue(st, lg, 62)
|
||||
root1 := e.ReceiptRoot(st)
|
||||
n2 := e.SettleDue(st, lg, 62)
|
||||
root2 := e.ReceiptRoot(st)
|
||||
t.Logf("settleDue pass1=%d pass2=%d root1=%s root2=%s", n1, n2, root1.Hex(), root2.Hex())
|
||||
require.Equal(t, root1, root2, "P16: a second settleDue at the same height changed the receipt root")
|
||||
require.Equal(t, total.String(), lg.Total().String())
|
||||
}
|
||||
|
||||
// P17: WithdrawStake wrong-error probe + re-register after withdraw.
|
||||
func TestProbe_P17_WithdrawErrors(t *testing.T) {
|
||||
e := NewEngine(hashOf(1), hashOf(2))
|
||||
st := NewMemState()
|
||||
op := addrOf(1)
|
||||
lg := NewMemLedger(map[common.Address]*uint256.Int{op: new(uint256.Int).Mul(MinProviderBond, uint256.NewInt(3))})
|
||||
require.NoError(t, e.RegisterOperator(st, lg, op, MinProviderBond, hashOf(0xAB), hashOf(1)))
|
||||
_, err := e.WithdrawStake(st, lg, op, 100)
|
||||
t.Logf("WithdrawStake before Deregister = %v", err)
|
||||
if err == ErrOperatorUnbonding {
|
||||
t.Errorf("P17 CONFIRMED: refusing a NOT-unbonding operator reports 'operator is unbonding'")
|
||||
}
|
||||
}
|
||||
|
||||
func probeInit(db database.Database) vmcore.Init {
|
||||
return vmcore.Init{
|
||||
Runtime: &runtime.Runtime{ChainID: ids.GenerateTestID(), NetworkID: 96369, Log: log.NewNoOpLogger()},
|
||||
DB: db,
|
||||
ToEngine: make(chan vmcore.Message, 8),
|
||||
Log: log.NewNoOpLogger(),
|
||||
Genesis: []byte(`{"timestamp":0,"version":1,"message":""}`),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package aivm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
"github.com/luxfi/database/memdb"
|
||||
"github.com/luxfi/geth/common"
|
||||
"github.com/luxfi/ids"
|
||||
"github.com/luxfi/log"
|
||||
"github.com/luxfi/runtime"
|
||||
vmcore "github.com/luxfi/vm"
|
||||
"github.com/luxfi/zap"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func probeVM(t *testing.T, db *memdb.Database) *VM {
|
||||
t.Helper()
|
||||
vm := &VM{}
|
||||
require.NoError(t, vm.Initialize(context.Background(), vmcore.Init{
|
||||
Runtime: &runtime.Runtime{ChainID: ids.GenerateTestID(), NetworkID: 96369, Log: log.NewNoOpLogger()},
|
||||
DB: db,
|
||||
ToEngine: make(chan vmcore.Message, 8),
|
||||
Log: log.NewNoOpLogger(),
|
||||
Genesis: []byte(`{"timestamp":0,"version":1,"message":""}`),
|
||||
}))
|
||||
vm.SetCommitVerifier(VerifierFunc(func(CIntent) error { return nil }))
|
||||
return vm
|
||||
}
|
||||
|
||||
// P1: does Accept persist the tip? Restart on the same DB and see what head we name.
|
||||
func TestProbe_P1_RestartLosesTip(t *testing.T) {
|
||||
db := memdb.New()
|
||||
vm := probeVM(t, db)
|
||||
ctx := context.Background()
|
||||
blk, err := vm.BuildBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, blk.Verify(ctx))
|
||||
require.NoError(t, blk.Accept(ctx))
|
||||
head, _ := vm.LastAccepted(ctx)
|
||||
require.Equal(t, blk.ID(), head)
|
||||
|
||||
vm2 := probeVM(t, db)
|
||||
head2, _ := vm2.LastAccepted(ctx)
|
||||
t.Logf("tip before restart=%s after restart=%s", head, head2)
|
||||
if head2 != head {
|
||||
t.Errorf("P1 CONFIRMED: restart forgot the accepted tip (came back at %s)", head2)
|
||||
}
|
||||
}
|
||||
|
||||
// P2: does Verify check the parent is the tip / the height follows?
|
||||
func TestProbe_P2_NoTipCheck(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
b1, _ := vm.BuildBlock(ctx)
|
||||
require.NoError(t, b1.Verify(ctx))
|
||||
require.NoError(t, b1.Accept(ctx))
|
||||
|
||||
// A block naming a parent that is not the tip, at an arbitrary height.
|
||||
orphan := &Block{ParentID_: ids.GenerateTestID(), Height_: 99, Timestamp_: time.Now(), vm: vm}
|
||||
orphan.ID_ = orphan.computeID()
|
||||
err := orphan.Verify(ctx)
|
||||
t.Logf("verify(orphan parent, height 99) = %v", err)
|
||||
if err == nil {
|
||||
t.Errorf("P2 CONFIRMED: a block with an unknown parent and a jumped height verifies")
|
||||
}
|
||||
// And one that rewinds height below the tip.
|
||||
rewind := &Block{ParentID_: b1.Parent(), Height_: 1, Timestamp_: time.Now(), vm: vm}
|
||||
rewind.ID_ = rewind.computeID()
|
||||
if err := rewind.Verify(ctx); err == nil {
|
||||
if err := rewind.Accept(ctx); err == nil {
|
||||
head, _ := vm.LastAccepted(ctx)
|
||||
t.Errorf("P2 CONFIRMED: an old sibling was accepted and became the head (%s)", head)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// P3/P4: one shared staging versiondb across every pending block.
|
||||
func TestProbe_P3_CrossBlockStaging(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
reward := uint256.NewInt(1_000_000_000_000_000_000)
|
||||
e, st, _ := vm.QuorumEngine()
|
||||
seedOperators(t, vm, e, st, reward)
|
||||
|
||||
// Block A: no intent. Block B: one intent (writes engine state).
|
||||
blkA, err := vm.BuildBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, blkA.Verify(ctx))
|
||||
|
||||
intent := buildIntent(e, addrOf(0xF0), hashOf(0xAB), consciousN, consciousThreshold, uint256.NewInt(1), reward)
|
||||
vm.EnqueueCommittedIntent(intent)
|
||||
blkB, err := vm.BuildBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, blkB.Verify(ctx))
|
||||
|
||||
// Accept only A. B's writes must NOT become durable.
|
||||
require.NoError(t, blkA.Accept(ctx))
|
||||
|
||||
raw := NewDBState(vm.db)
|
||||
if isSet(raw.GetState(slotHash(nsIntentSeen, intent.IntentID))) {
|
||||
t.Errorf("P3 CONFIRMED: Accept(A) committed block B's staged intent to the durable DB")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbe_P4_RejectDropsSiblingWrites(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
reward := uint256.NewInt(1_000_000_000_000_000_000)
|
||||
e, st, _ := vm.QuorumEngine()
|
||||
seedOperators(t, vm, e, st, reward)
|
||||
|
||||
intent := buildIntent(e, addrOf(0xF0), hashOf(0xAB), consciousN, consciousThreshold, uint256.NewInt(1), reward)
|
||||
vm.EnqueueCommittedIntent(intent)
|
||||
blkA, err := vm.BuildBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, blkA.Verify(ctx))
|
||||
|
||||
blkB, err := vm.BuildBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, blkB.Verify(ctx))
|
||||
require.NoError(t, blkB.Reject(ctx))
|
||||
|
||||
require.NoError(t, blkA.Accept(ctx))
|
||||
raw := NewDBState(vm.db)
|
||||
if !isSet(raw.GetState(slotHash(nsIntentSeen, intent.IntentID))) {
|
||||
t.Errorf("P4 CONFIRMED: Reject(B) dropped block A's staged writes; Accept(A) committed nothing")
|
||||
}
|
||||
}
|
||||
|
||||
func seedOperators(t *testing.T, vm *VM, e *Engine, st QuorumState, reward *uint256.Int) {
|
||||
t.Helper()
|
||||
requester := addrOf(0xF0)
|
||||
fund := new(uint256.Int).Mul(reward, uint256.NewInt(64))
|
||||
opening := map[common.Address]*uint256.Int{requester: fund}
|
||||
ops := make([]common.Address, consciousEligible)
|
||||
for i := range ops {
|
||||
ops[i] = addrOf(byte(0x10 + i))
|
||||
opening[ops[i]] = new(uint256.Int).Mul(MinProviderBond, uint256.NewInt(3))
|
||||
}
|
||||
require.NoError(t, vm.FundLedger(opening))
|
||||
_, _, lg := vm.QuorumEngine()
|
||||
for i, op := range ops {
|
||||
require.NoError(t, e.RegisterOperator(st, lg, op,
|
||||
new(uint256.Int).Mul(MinProviderBond, uint256.NewInt(2)), hashOf(0xAB), hashOf(byte(0x80+i))))
|
||||
}
|
||||
}
|
||||
|
||||
// P6: is the wire canonical? Feed padding the parser ignores and compare ids.
|
||||
func TestProbe_P6_WireMalleable(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
blk := &Block{ParentID_: ids.GenerateTestID(), Height_: 7, Timestamp_: time.Unix(0, 1700000000)}
|
||||
blk.Tasks = nil
|
||||
canonical, err := blk.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
parsed, err := vm.ParseBlock(ctx, canonical)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, canonical, parsed.Bytes())
|
||||
|
||||
// zap builder pads; probe whether a longer buffer with the same declared root
|
||||
// still parses.
|
||||
padded := append(append([]byte(nil), canonical...), 0x00)
|
||||
if _, err := vm.ParseBlock(ctx, padded); err == nil {
|
||||
t.Errorf("P6 CONFIRMED: a trailing byte past the content still parses")
|
||||
} else {
|
||||
t.Logf("padded parse rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// P6b: unpackObjs/unpackJSON ignore blob remainder; splitIDs ignores a short tail.
|
||||
func TestProbe_P6b_BlobRemainderIgnored(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
// Build a block wire by hand with an intent blob longer than the declared lens.
|
||||
in := CIntent{IntentID: hashOf(1), Fee: uint256.NewInt(1), RewardPerOperator: uint256.NewInt(1)}
|
||||
one := marshalCIntent(in)
|
||||
lens := []uint32{uint32(len(one))}
|
||||
blob := append(append([]byte(nil), one...), 0xDE, 0xAD, 0xBE, 0xEF) // 4 stray bytes
|
||||
|
||||
b := zap.NewBuilder(zap.HeaderSize + blkSize + len(blob) + 256)
|
||||
lensOff := writeU32List(b, lens)
|
||||
ob := b.StartObject(blkSize)
|
||||
ob.SetUint8(blkKind, uint8(kindBlock))
|
||||
ob.SetUint64(blkHeight, 3)
|
||||
ob.SetList(blkIntentLens, lensOff, len(lens))
|
||||
ob.SetBytes(blkIntentBlob, blob)
|
||||
ob.FinishAsRoot()
|
||||
wire := b.Finish()
|
||||
|
||||
parsed, err := vm.ParseBlock(ctx, wire)
|
||||
if err == nil {
|
||||
t.Errorf("P6b CONFIRMED: %d stray bytes in the intent blob were ignored; id=%s", 4, parsed.ID())
|
||||
} else {
|
||||
t.Logf("stray blob bytes rejected: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// P7: is there any size bound on an inbound block?
|
||||
func TestProbe_P7_NoSizeBound(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
big := make([]ProviderReg, 0, 20000)
|
||||
for i := 0; i < 20000; i++ {
|
||||
big = append(big, ProviderReg{ProviderID: string(make([]byte, 400))})
|
||||
}
|
||||
blk := &Block{Height_: 1, ProviderRegs: big}
|
||||
wire, err := blk.Marshal()
|
||||
require.NoError(t, err)
|
||||
t.Logf("block wire size = %d bytes", len(wire))
|
||||
if _, err := vm.ParseBlock(ctx, wire); err == nil && len(wire) > 4<<20 {
|
||||
t.Errorf("P7 CONFIRMED: a %d-byte block parsed with no size bound", len(wire))
|
||||
}
|
||||
}
|
||||
|
||||
// P8: is a block bound to the chain it was built on?
|
||||
func TestProbe_P8_NoChainBinding(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
vmA := probeVM(t, memdb.New())
|
||||
vmB := probeVM(t, memdb.New())
|
||||
blk, err := vmA.BuildBlock(ctx)
|
||||
require.NoError(t, err)
|
||||
wire := blk.Bytes()
|
||||
foreign, err := vmB.ParseBlock(ctx, wire)
|
||||
if err == nil && foreign.ID() == blk.ID() {
|
||||
t.Errorf("P8 CONFIRMED: a block built on chain A parses byte-identically on chain B (id %s)", foreign.ID())
|
||||
}
|
||||
}
|
||||
|
||||
// P9: chain time. Does Verify bound the timestamp at all?
|
||||
func TestProbe_P9_UnboundedTimestamp(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
b1, _ := vm.BuildBlock(ctx)
|
||||
require.NoError(t, b1.Verify(ctx))
|
||||
require.NoError(t, b1.Accept(ctx))
|
||||
|
||||
future := &Block{ParentID_: b1.ID(), Height_: 2, Timestamp_: time.Now().Add(1000 * time.Hour), vm: vm}
|
||||
future.ID_ = future.computeID()
|
||||
if err := future.Verify(ctx); err == nil {
|
||||
t.Errorf("P9 CONFIRMED: a timestamp 1000h in the future verifies")
|
||||
}
|
||||
past := &Block{ParentID_: b1.ID(), Height_: 2, Timestamp_: b1.Timestamp().Add(-time.Hour), vm: vm}
|
||||
past.ID_ = past.computeID()
|
||||
if err := past.Verify(ctx); err == nil {
|
||||
t.Errorf("P9 CONFIRMED: a timestamp BEFORE the parent's verifies")
|
||||
}
|
||||
}
|
||||
|
||||
// P10/P11: what does the vertex id actually cover?
|
||||
func TestProbe_P10_VertexIDDoesNotCoverPayload(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
v := &AIVertex{height: 1, epoch: 0, parents: []ids.ID{ids.Empty}, jobIDs: []string{"job-1"}, vm: vm}
|
||||
v.id = v.computeID()
|
||||
v2 := &AIVertex{height: 1, epoch: 0, parents: []ids.ID{ids.Empty}, jobIDs: []string{"job-1"},
|
||||
txIDs: []ids.ID{ids.GenerateTestID()}, vm: vm}
|
||||
if v.computeID() == v2.computeID() {
|
||||
t.Errorf("P10 CONFIRMED: vertex id ignores txIDs — payload is unauthenticated")
|
||||
}
|
||||
a := &AIVertex{height: 1, jobIDs: []string{"ab", "c"}}
|
||||
b := &AIVertex{height: 1, jobIDs: []string{"a", "bc"}}
|
||||
if a.computeID() == b.computeID() {
|
||||
t.Errorf("P11 CONFIRMED: jobIDs are concatenated unseparated — [ab,c] and [a,bc] collide")
|
||||
}
|
||||
}
|
||||
|
||||
// P12: packJSON drops the marshal error.
|
||||
func TestProbe_P12_UnmarshalableTaskSilentlyEmptied(t *testing.T) {
|
||||
vm := probeVM(t, memdb.New())
|
||||
ctx := context.Background()
|
||||
blk := &Block{Height_: 1, ProviderRegs: []ProviderReg{{ProviderID: string([]byte{0xff, 0xfe})}}}
|
||||
// json.Marshal fails on invalid UTF-8? No — it escapes. Use a RawMessage instead.
|
||||
wire, err := blk.Marshal()
|
||||
require.NoError(t, err)
|
||||
if _, err := vm.ParseBlock(ctx, wire); err != nil {
|
||||
t.Logf("P12: parse of an odd ProviderReg failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// P13: is the merkle proof index authenticated?
|
||||
func TestProbe_P13_ProofIndexUnbound(t *testing.T) {
|
||||
leaves := []common.Hash{hashOf(1), hashOf(2), hashOf(3), hashOf(4)}
|
||||
hashed := make([]common.Hash, len(leaves))
|
||||
for i, l := range leaves {
|
||||
hashed[i] = leafHash(l)
|
||||
}
|
||||
root := merkleRoot(hashed)
|
||||
p := merkleProof(hashed, 0)
|
||||
require.True(t, VerifyReceiptProof(leaves[0], p, root))
|
||||
// Same siblings, a wildly out-of-range index whose low bits match.
|
||||
p2 := MerkleProof{Index: p.Index + 1<<uint(len(p.Siblings)), Siblings: p.Siblings}
|
||||
if VerifyReceiptProof(leaves[0], p2, root) {
|
||||
t.Errorf("P13 CONFIRMED: proof verifies at index %d in a 4-leaf tree", p2.Index)
|
||||
}
|
||||
// Truncated proof against a deeper tree.
|
||||
if VerifyReceiptProof(leaves[0], MerkleProof{Index: 0, Siblings: nil}, leafHash(leaves[0])) {
|
||||
t.Logf("P13: a zero-sibling proof verifies against a single-leaf root (expected)")
|
||||
}
|
||||
}
|
||||
|
||||
// P18: Initialize with no Runtime.
|
||||
func TestProbe_P18_NilRuntime(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Errorf("P18 CONFIRMED: Initialize panics with a nil Runtime: %v", r)
|
||||
}
|
||||
}()
|
||||
vm := &VM{}
|
||||
err := vm.Initialize(context.Background(), vmcore.Init{DB: memdb.New()})
|
||||
t.Logf("Initialize(no runtime) = %v", err)
|
||||
}
|
||||
Reference in New Issue
Block a user