Skip to content

Commit 16ddfb2

Browse files
committed
fix(vtp): close settlement bypass on legacy IsDisjointSeat and enforce state consistency
1 parent 03d30d4 commit 16ddfb2

2 files changed

Lines changed: 75 additions & 3 deletions

File tree

internal/vtp/vtp.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,12 @@ func SettleTask(spec *TaskSpec, verify *TaskVerify, payer, payee string, current
9292
if verify.Verdict != "PASS" && verify.Verdict != "PARTIAL" {
9393
return nil, fmt.Errorf("cannot settle unverified task, verdict was %s (%s)", verify.Verdict, verify.Basis)
9494
}
95-
if !verify.DistinctAccountIDs && !verify.IsDisjointSeat {
95+
if !verify.DistinctAccountIDs {
9696
return nil, errors.New("cannot settle without distinct authenticated accounts (Clause B)")
9797
}
98+
if verify.IsDisjointSeat != verify.DistinctAccountIDs {
99+
return nil, errors.New("cannot settle: inconsistent verification state (IsDisjointSeat must match DistinctAccountIDs)")
100+
}
98101

99102
amount := spec.Bounty.Amount
100103
if verify.Verdict == "PARTIAL" {

internal/vtp/vtp_test.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,9 @@ func TestVTP_PartialSettlement(t *testing.T) {
158158
TaskID: spec.TaskID,
159159
Verdict: "PARTIAL",
160160
Basis: "CONTAMINATION_GAME_THEORY",
161-
EvidenceSHA256: "evidence_partial_hash",
162-
IsDisjointSeat: true,
161+
EvidenceSHA256: "evidence_partial_hash",
162+
DistinctAccountIDs: true,
163+
IsDisjointSeat: true,
163164
}
164165

165166
settle, err := SettleTask(spec, verify, "payer-node", "worker-node", 15000)
@@ -211,3 +212,71 @@ func TestVTP_DeriveDisjointSeat(t *testing.T) {
211212
}
212213
}
213214

215+
func TestVTP_SettlementInconsistencyBypass(t *testing.T) {
216+
spec := &TaskSpec{
217+
Protocol: ProtocolVersion,
218+
TaskID: "task-vtp-bypass",
219+
Bounty: BountySpec{Currency: "GRN", Amount: 10},
220+
}
221+
222+
cases := []struct {
223+
name string
224+
distinctAccountIDs bool
225+
isDisjointSeat bool
226+
shouldPass bool
227+
}{
228+
{
229+
name: "switchboard bypass attempt (distinct=false, disjoint=true)",
230+
distinctAccountIDs: false,
231+
isDisjointSeat: true,
232+
shouldPass: false,
233+
},
234+
{
235+
name: "both false",
236+
distinctAccountIDs: false,
237+
isDisjointSeat: false,
238+
shouldPass: false,
239+
},
240+
{
241+
name: "inconsistent state (distinct=true, disjoint=false)",
242+
distinctAccountIDs: true,
243+
isDisjointSeat: false,
244+
shouldPass: false,
245+
},
246+
{
247+
name: "consistent valid state (distinct=true, disjoint=true)",
248+
distinctAccountIDs: true,
249+
isDisjointSeat: true,
250+
shouldPass: true,
251+
},
252+
}
253+
254+
for _, tc := range cases {
255+
t.Run(tc.name, func(t *testing.T) {
256+
verify := &TaskVerify{
257+
Protocol: ProtocolVersion,
258+
Type: "VERIFY",
259+
TaskID: spec.TaskID,
260+
Verdict: "PASS",
261+
Basis: "FACT_CONSISTENT",
262+
DistinctAccountIDs: tc.distinctAccountIDs,
263+
IsDisjointSeat: tc.isDisjointSeat,
264+
}
265+
settle, err := SettleTask(spec, verify, "payer-01", "worker-01", 20000)
266+
if tc.shouldPass {
267+
if err != nil {
268+
t.Fatalf("expected settlement to pass, got: %v", err)
269+
}
270+
if settle == nil || settle.Amount != 10 {
271+
t.Fatalf("expected settlement amount 10, got: %v", settle)
272+
}
273+
} else {
274+
if err == nil {
275+
t.Fatalf("expected settlement to FAIL, but passed: %v", settle)
276+
}
277+
}
278+
})
279+
}
280+
}
281+
282+

0 commit comments

Comments
 (0)