mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
Operators were selected, bonded, committed and revealed, and then nothing paid them. Settle — which tallies reveals, credits the honest majority, slashes withholders and folds a receipt into the root — ran only from the demonstration driver. The production block path imported intents, created tasks, and never gave any of them a verdict, so a task sat past its reveal deadline forever and the work done for it was credited to nobody. Open mining rests on the opposite: an operator that did the work is paid without anyone asking on its behalf. Every block now settles the tasks whose reveal window has closed. The set is a function of the state and the height alone, which is what lets a follower reach the same verdicts without being told: it applies the block's imports, runs the same pass at the same height, and compares receipt roots. A disagreement about who was paid changes the root, and verifyImported already refuses that — so this needs nothing recorded in the block. Settling requires enumerating tasks and engine state is keyed by slot hash with nothing to iterate, so tasks awaiting a verdict are held in an array in state: appended at creation, removed on reaching one. It holds only what is open, so a block's work is bounded by the tasks still outstanding rather than by everything the chain has ever done. An entry that cannot settle is dropped rather than retried, since leaving it would make every later block re-attempt the same refusal. The test drives the real lifecycle and then does nothing but produce blocks: it fails without this, reporting a task past its deadline with the work unpaid. Registration was already open by default; its comment now says so, and the permissioned variants are described as deployment policy. Co-authored-by: Hanzo Dev <[email protected]>
152 lines
5.1 KiB
Go
152 lines
5.1 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package aivm
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/holiman/uint256"
|
|
"github.com/luxfi/geth/common"
|
|
"github.com/luxfi/ids"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// buildTo builds and accepts blocks until the VM's head reaches height h, so a
|
|
// test can advance the chain the way the chain advances rather than by calling
|
|
// the settlement engine itself.
|
|
func buildTo(t *testing.T, vm *VM, h uint64) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
for i := 0; i < 200; i++ {
|
|
head, err := vm.GetBlock(ctx, mustLastAccepted(t, vm))
|
|
require.NoError(t, err)
|
|
if head.Height() >= h {
|
|
return
|
|
}
|
|
blk, err := vm.BuildBlock(ctx)
|
|
require.NoError(t, err, "build block")
|
|
require.NoError(t, blk.Verify(ctx), "verify block")
|
|
require.NoError(t, blk.Accept(ctx), "accept block")
|
|
}
|
|
t.Fatalf("chain did not reach height %d", h)
|
|
}
|
|
|
|
func mustLastAccepted(t *testing.T, vm *VM) ids.ID {
|
|
t.Helper()
|
|
id, err := vm.LastAccepted(context.Background())
|
|
require.NoError(t, err)
|
|
return id
|
|
}
|
|
|
|
// TestATaskSettlesBecauseTheChainAdvanced is what open mining rests on: an
|
|
// operator that did the work is paid without anyone asking on its behalf.
|
|
//
|
|
// The committee commits and reveals, and then nothing else happens except that
|
|
// blocks are produced. Past the reveal deadline the verdict is taken, the
|
|
// honest majority is credited, and the receipt is folded into the root.
|
|
func TestATaskSettlesBecauseTheChainAdvanced(t *testing.T) {
|
|
require := require.New(t)
|
|
ctx := context.Background()
|
|
|
|
vm := newAIVMForFeeTest(t)
|
|
defer vm.Shutdown(ctx)
|
|
vm.SetCommitVerifier(acceptAll)
|
|
|
|
e, st, _ := vm.QuorumEngine()
|
|
require.NotNil(e)
|
|
|
|
// Fund a requester and a pool of operators, then put one inference intent in.
|
|
reward := uint256.NewInt(1_000_000_000_000_000_000)
|
|
requester := addrOf(0xF0)
|
|
fund := new(uint256.Int).Mul(reward, uint256.NewInt(uint64(consciousN)))
|
|
burn := new(uint256.Int).Mul(RequestFeePerOperator, uint256.NewInt(uint64(consciousN)))
|
|
fund.Add(fund, burn)
|
|
fund.Mul(fund, uint256.NewInt(4))
|
|
|
|
opening := map[common.Address]*uint256.Int{requester: fund}
|
|
pool := make([]common.Address, consciousEligible)
|
|
for i := range pool {
|
|
pool[i] = addrOf(byte(0x10 + i))
|
|
opening[pool[i]] = new(uint256.Int).Mul(MinProviderBond, uint256.NewInt(4))
|
|
}
|
|
vm.FundLedger(opening)
|
|
|
|
model := hashOf(0x11)
|
|
for _, op := range pool {
|
|
require.NoError(e.RegisterOperator(st, vm.qledger, op, MinProviderBond, model, hashOf(0x55)))
|
|
}
|
|
|
|
intent := buildIntent(e, requester, model, consciousN, consciousThreshold, RequestFeePerOperator, reward)
|
|
vm.EnqueueCommittedIntent(intent)
|
|
|
|
// Block 1 imports the intent and creates the task.
|
|
buildTo(t, vm, 1)
|
|
taskID := e.TaskForIntent(st, intent.IntentID)
|
|
require.NotEqual(common.Hash{}, taskID, "the block should have created a task")
|
|
require.Equal(uint32(1), e.LiveTasks(st), "the task should be awaiting a verdict")
|
|
|
|
committee, err := e.SelectOperators(st, taskID, model, consciousN)
|
|
require.NoError(err)
|
|
|
|
// The committee thinks: everyone commits blind, then reveals the same answer.
|
|
answer, embedding, nonce := hashOf(0x42), hashOf(0x07), hashOf(0x99)
|
|
for _, op := range committee {
|
|
require.NoError(e.CommitResponse(st, taskID, op,
|
|
commitOf(taskID, model, op, answer, embedding, nonce), 2))
|
|
}
|
|
for _, op := range committee {
|
|
require.NoError(e.RevealResponse(st, taskID, op, answer, embedding, nonce, 32))
|
|
}
|
|
|
|
// Nothing settles it yet: the reveal window is still open, and an operator
|
|
// that has not answered must still be able to.
|
|
task := readTask(st, taskID)
|
|
buildTo(t, vm, task.RevealDeadline)
|
|
require.Equal(uint32(1), e.LiveTasks(st), "a task settled before its reveal window closed")
|
|
require.False(isSet(st.GetState(slotHash(nsSettled, taskID))))
|
|
|
|
before := make([]*uint256.Int, len(committee))
|
|
for i, op := range committee {
|
|
before[i] = new(uint256.Int).Set(readCredit(st, op))
|
|
}
|
|
|
|
// One more block, and the window has closed.
|
|
buildTo(t, vm, task.RevealDeadline+1)
|
|
|
|
require.True(isSet(st.GetState(slotHash(nsSettled, taskID))),
|
|
"the task passed its reveal deadline and was never settled — the work is unpaid")
|
|
require.Equal(uint32(0), e.LiveTasks(st), "a settled task should no longer be awaiting a verdict")
|
|
|
|
paid := 0
|
|
for i, op := range committee {
|
|
if readCredit(st, op).Gt(before[i]) {
|
|
paid++
|
|
}
|
|
}
|
|
require.GreaterOrEqual(paid, int(consciousThreshold),
|
|
"the honest majority answered and was not credited")
|
|
}
|
|
|
|
// The pass is bounded by what is open, not by everything the chain has done: a
|
|
// task that reaches a verdict stops being walked.
|
|
func TestSettledTasksStopBeingWalked(t *testing.T) {
|
|
require := require.New(t)
|
|
ctx := context.Background()
|
|
|
|
vm := newAIVMForFeeTest(t)
|
|
defer vm.Shutdown(ctx)
|
|
vm.SetCommitVerifier(acceptAll)
|
|
e, st, _ := vm.QuorumEngine()
|
|
|
|
// Entries naming tasks that do not exist are dropped rather than retried.
|
|
trackLive(st, hashOf(0xDD))
|
|
trackLive(st, hashOf(0xDE))
|
|
require.Equal(uint32(2), e.LiveTasks(st))
|
|
|
|
buildTo(t, vm, 1)
|
|
require.Equal(uint32(0), e.LiveTasks(st),
|
|
"entries naming no task should be dropped, not re-attempted every block")
|
|
}
|