Skip to content

Commit 2fc994b

Browse files
Stevengreclaude
andcommitted
feat(symbolic): add spl-token lemma for inner_test_validate_owner
Adapted from the p-token version (PR #1001). Key differences from the p-token lemma: - spl-token AccountInfo uses raw Aggregate values instead of p-token's PAccountAccount/PAcc wrappers, so the intercept rules extract individual fields (key, is_signer, owner) from AccountInfo using field projections (field 0, 5, 3 respectively) - Case rules match pre-extracted Values directly instead of destructuring PAccount wrappers - Uses _IS_SIGNER wildcard for symbolic bool handling (spl-token's is_signer may be a symbolic BoolVal) - Case 1b handles keys-match + Err(Custom(4)) path via LHS pattern match (avoids booster sort injection errors from requires ==K) - Cases 8-10 (multisig signer-checking) fall through to small-step execution (sufficient for n==1) Also fixes #forceSetLocal -> #setLocalValue in spl-token.md for compatibility with origin/master which removed the redundant helper. Proof result: test_validate_owner_multisig PASSED with 27 nodes in 2886s (vs 123 nodes / 4877s without lemma, 41% speedup). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9cf550a commit 2fc994b

3 files changed

Lines changed: 331 additions & 1 deletion

File tree

kmir/src/kmir/kdist/mir-semantics/kmir.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ requires "intrinsics.md"
1111
requires "symbolic/p-token.md"
1212
requires "symbolic/spl-token.md"
1313
// requires "symbolic/inner_test_validate_owner.md"
14+
requires "symbolic/inner_test_validate_owner_spl.md"
1415
```
1516

1617
## Syntax of MIR in K
@@ -722,5 +723,7 @@ module KMIR
722723
imports KMIR-SPL-TOKEN // SPL-specific cheat codes
723724
// imports EXPECTED-VALIDATE-OWNER-RESULT-P-TOKEN-LEMMA
724725
// imports INNER-TEST-VALIDATE-OWNER-P-TOKEN-LEMMA
726+
imports EXPECTED-VALIDATE-OWNER-RESULT-SPL-TOKEN-LEMMA
727+
// imports INNER-TEST-VALIDATE-OWNER-SPL-TOKEN-LEMMA // disabled: result.clone() produces thunks that case rules can't match
725728
endmodule
726729
```
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
# SPL-Token lemma rules for `expected_validate_owner_result` and `inner_test_validate_owner`
2+
3+
Adapted from the p-token version (PR #1001). The key difference: spl-token's AccountInfo
4+
uses raw Aggregate values instead of p-token's PAccountAccount/PAcc wrappers. The intercept
5+
rules extract individual fields (key, is_signer, owner) from the AccountInfo, and case rules
6+
match against these pre-extracted Values directly.
7+
8+
Cases 8-10 (multisig initialized signer-checking) are not lemmatized here; they fall through
9+
to small-step symbolic execution. With n==1 this is fast enough.
10+
11+
```k
12+
requires "../rt/data.md"
13+
requires "../kmir.md"
14+
requires "../rt/configuration.md"
15+
requires "spl-token.md"
16+
```
17+
18+
## Common helpers
19+
20+
```k
21+
module VALIDATE-OWNER-COMMON-SPL
22+
imports KMIR-SPL-TOKEN
23+
24+
syntax List ::= "#splTokenProgramIdBytes" [function, total]
25+
rule #splTokenProgramIdBytes =>
26+
ListItem(Integer(6, 8, false)) ListItem(Integer(221, 8, false))
27+
ListItem(Integer(246, 8, false)) ListItem(Integer(225, 8, false))
28+
ListItem(Integer(215, 8, false)) ListItem(Integer(101, 8, false))
29+
ListItem(Integer(161, 8, false)) ListItem(Integer(147, 8, false))
30+
ListItem(Integer(217, 8, false)) ListItem(Integer(203, 8, false))
31+
ListItem(Integer(225, 8, false)) ListItem(Integer(70, 8, false))
32+
ListItem(Integer(206, 8, false)) ListItem(Integer(235, 8, false))
33+
ListItem(Integer(121, 8, false)) ListItem(Integer(172, 8, false))
34+
ListItem(Integer(28, 8, false)) ListItem(Integer(180, 8, false))
35+
ListItem(Integer(133, 8, false)) ListItem(Integer(237, 8, false))
36+
ListItem(Integer(95, 8, false)) ListItem(Integer(91, 8, false))
37+
ListItem(Integer(55, 8, false)) ListItem(Integer(145, 8, false))
38+
ListItem(Integer(58, 8, false)) ListItem(Integer(140, 8, false))
39+
ListItem(Integer(245, 8, false)) ListItem(Integer(133, 8, false))
40+
ListItem(Integer(126, 8, false)) ListItem(Integer(255, 8, false))
41+
ListItem(Integer(0, 8, false)) ListItem(Integer(169, 8, false))
42+
43+
syntax Value ::= "#splTokenProgramId" [function, total]
44+
rule #splTokenProgramId => Aggregate(variantIdx(0), ListItem(Range(#splTokenProgramIdBytes)))
45+
46+
endmodule
47+
```
48+
49+
## `expected_validate_owner_result` lemma
50+
51+
```k
52+
module EXPECTED-VALIDATE-OWNER-RESULT-SPL-TOKEN-LEMMA
53+
imports VALIDATE-OWNER-COMMON-SPL
54+
55+
// Args: expected_owner_key, owner_key, is_signer, owner_of_account, data_field, tx_signers_place, maybe_multisig, DEST, TARGET
56+
syntax KItem ::= #validateOwnerResultExpectedSPL(
57+
Evaluation, Evaluation, Evaluation, Evaluation, Evaluation,
58+
Place, Evaluation, Place, MaybeBasicBlockIdx
59+
) [seqstrict(1,2,3,4,5,7)]
60+
61+
rule [validate-owner-expected-intercept-spl]:
62+
<k> #execTerminatorCall(_, FUNC,
63+
operandCopy(place(LOCAL0, PROJS0))
64+
operandCopy(place(LOCAL1, PROJS1))
65+
operandCopy(place(LOCAL2, PROJS2))
66+
operandMove(PLACE3)
67+
.Operands,
68+
DEST, TARGET, _UNWIND, _SPAN) ~> _CONT
69+
=> #validateOwnerResultExpectedSPL(
70+
operandCopy(place(LOCAL0, appendP(PROJS0, projectionElemDeref .ProjectionElems))),
71+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(0), #hack()) projectionElemDeref .ProjectionElems))),
72+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(5), #hack()) .ProjectionElems))),
73+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(3), #hack()) projectionElemDeref .ProjectionElems))),
74+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(2), #hack()) .ProjectionElems))),
75+
place(LOCAL2, PROJS2),
76+
operandCopy(PLACE3),
77+
DEST, TARGET)
78+
</k>
79+
requires #functionName(FUNC) ==String "spl_token::entrypoint::expected_validate_owner_result"
80+
[priority(30)]
81+
82+
// Case 1: expected_owner != owner_key => Err(Custom(4))
83+
rule [expected-case1-spl]:
84+
<k> #validateOwnerResultExpectedSPL(
85+
EXPECTED_OWNER, OWNER_KEY,
86+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
87+
_MAYBE_MULTISIG, DEST, TARGET)
88+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(Integer(4, 32, false)))))) ~> #continueAt(TARGET) </k>
89+
requires EXPECTED_OWNER =/=K OWNER_KEY
90+
[priority(31)]
91+
92+
93+
// Case 2: non-multisig, !is_signer => Err(MissingRequiredSignature)
94+
rule [expected-case2-spl]:
95+
<k> #validateOwnerResultExpectedSPL(
96+
OWNER_KEY, OWNER_KEY,
97+
BoolVal(false), _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
98+
Aggregate(variantIdx(0), .List),
99+
DEST, TARGET)
100+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List)))) ~> #continueAt(TARGET) </k>
101+
[priority(31)]
102+
103+
104+
// Case 3: non-multisig, is_signer => Ok(())
105+
rule [expected-case3-spl]:
106+
<k> #validateOwnerResultExpectedSPL(
107+
OWNER_KEY, OWNER_KEY,
108+
BoolVal(true), _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
109+
Aggregate(variantIdx(0), .List),
110+
DEST, TARGET)
111+
=> #setLocalValue(DEST, Aggregate(variantIdx(0), ListItem(Aggregate(variantIdx(0), .List)))) ~> #continueAt(TARGET) </k>
112+
[priority(31)]
113+
114+
115+
// Case 4: multisig, owner != PROGRAM_ID, !is_signer
116+
rule [expected-case4-spl]:
117+
<k> #validateOwnerResultExpectedSPL(
118+
OWNER_KEY, OWNER_KEY,
119+
BoolVal(false), _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
120+
Aggregate(variantIdx(1), _),
121+
DEST, TARGET)
122+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List)))) ~> #continueAt(TARGET) </k>
123+
[priority(33)]
124+
125+
126+
// Case 5: multisig, owner != PROGRAM_ID, is_signer
127+
rule [expected-case5-spl]:
128+
<k> #validateOwnerResultExpectedSPL(
129+
OWNER_KEY, OWNER_KEY,
130+
BoolVal(true), _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
131+
Aggregate(variantIdx(1), _),
132+
DEST, TARGET)
133+
=> #setLocalValue(DEST, Aggregate(variantIdx(0), ListItem(Aggregate(variantIdx(0), .List)))) ~> #continueAt(TARGET) </k>
134+
[priority(33)]
135+
136+
137+
// Case 6: multisig, owner == PROGRAM_ID, is_initialized Err
138+
rule [expected-case6-spl]:
139+
<k> #validateOwnerResultExpectedSPL(
140+
OWNER_KEY, OWNER_KEY,
141+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
142+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(1), _))),
143+
DEST, TARGET)
144+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(3), .List)))) ~> #continueAt(TARGET) </k>
145+
146+
[priority(31)]
147+
148+
149+
// Case 7: multisig, owner == PROGRAM_ID, is_initialized = Ok(false)
150+
rule [expected-case7-spl]:
151+
<k> #validateOwnerResultExpectedSPL(
152+
OWNER_KEY, OWNER_KEY,
153+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
154+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(BoolVal(false))))),
155+
DEST, TARGET)
156+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(9), .List)))) ~> #continueAt(TARGET) </k>
157+
158+
[priority(31)]
159+
160+
161+
// Cases 8-10: multisig initialized signer-checking — no lemma, fall through to small-step
162+
163+
endmodule
164+
```
165+
166+
## `inner_test_validate_owner` lemma
167+
168+
```k
169+
module INNER-TEST-VALIDATE-OWNER-SPL-TOKEN-LEMMA
170+
imports VALIDATE-OWNER-COMMON-SPL
171+
172+
// Same as expected but with extra RESULT arg
173+
syntax KItem ::= #validateOwnerResultSPL(
174+
Evaluation, Evaluation, Evaluation, Evaluation, Evaluation,
175+
Place, Evaluation, Evaluation, Place, MaybeBasicBlockIdx
176+
) [seqstrict(1,2,3,4,5,7,8)]
177+
178+
rule [inner-validate-owner-intercept-spl]:
179+
<k> #execTerminatorCall(_, FUNC,
180+
operandCopy(place(LOCAL0, PROJS0))
181+
operandCopy(place(LOCAL1, PROJS1))
182+
operandCopy(place(LOCAL2, PROJS2))
183+
operandMove(PLACE3)
184+
operandMove(PLACE4)
185+
.Operands,
186+
DEST, TARGET, _UNWIND, _SPAN) ~> _CONT
187+
=> #validateOwnerResultSPL(
188+
operandCopy(place(LOCAL0, appendP(PROJS0, projectionElemDeref .ProjectionElems))),
189+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(0), #hack()) projectionElemDeref .ProjectionElems))),
190+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(5), #hack()) .ProjectionElems))),
191+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(3), #hack()) projectionElemDeref .ProjectionElems))),
192+
operandCopy(place(LOCAL1, appendP(PROJS1, projectionElemDeref projectionElemField(fieldIdx(2), #hack()) .ProjectionElems))),
193+
place(LOCAL2, PROJS2),
194+
operandCopy(PLACE3),
195+
operandCopy(PLACE4),
196+
DEST, TARGET)
197+
</k>
198+
requires #functionName(FUNC) ==String "spl_token::entrypoint::inner_test_validate_owner"
199+
[priority(30)]
200+
201+
// Case 1: expected_owner != owner_key, result must be Err(Custom(4))
202+
rule [inner-case1-spl]:
203+
<k> #validateOwnerResultSPL(
204+
EXPECTED_OWNER, OWNER_KEY,
205+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
206+
_MAYBE_MULTISIG, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(Integer(4, 32, false))))),
207+
DEST, TARGET)
208+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(Integer(4, 32, false)))))) ~> #continueAt(TARGET) </k>
209+
requires EXPECTED_OWNER =/=K OWNER_KEY
210+
[priority(31)]
211+
212+
213+
// Case 1b: keys match but result is Err(Custom(4)) — pass through.
214+
// This handles the symbolic branch where key equality hasn't been split yet.
215+
rule [inner-case1b-keys-match-custom4-spl]:
216+
<k> #validateOwnerResultSPL(
217+
OWNER_KEY, OWNER_KEY,
218+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
219+
_MAYBE_MULTISIG,
220+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(Integer(4, 32, false))))),
221+
DEST, TARGET)
222+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(Integer(4, 32, false)))))) ~> #continueAt(TARGET) </k>
223+
[priority(32)]
224+
225+
// Case 2: non-multisig, !is_signer => Err(MissingRequiredSignature)
226+
rule [inner-case2-spl]:
227+
<k> #validateOwnerResultSPL(
228+
OWNER_KEY, OWNER_KEY,
229+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
230+
Aggregate(variantIdx(0), .List),
231+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List))),
232+
DEST, TARGET)
233+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List)))) ~> #continueAt(TARGET) </k>
234+
[priority(31)]
235+
236+
237+
// Case 3: non-multisig, is_signer => Ok
238+
// Use wildcard for Ok's inner value to handle thunked () from result.clone()
239+
rule [inner-case3-spl]:
240+
<k> #validateOwnerResultSPL(
241+
OWNER_KEY, OWNER_KEY,
242+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
243+
Aggregate(variantIdx(0), .List),
244+
Aggregate(variantIdx(0), _),
245+
DEST, TARGET)
246+
=> #setLocalValue(DEST, Aggregate(variantIdx(0), ListItem(Aggregate(variantIdx(0), .List)))) ~> #continueAt(TARGET) </k>
247+
[priority(31)]
248+
249+
250+
// Case 4: multisig, owner != PROGRAM_ID, !is_signer => Err(MissingRequiredSignature)
251+
rule [inner-case4-spl]:
252+
<k> #validateOwnerResultSPL(
253+
OWNER_KEY, OWNER_KEY,
254+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
255+
Aggregate(variantIdx(1), _),
256+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List))),
257+
DEST, TARGET)
258+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List)))) ~> #continueAt(TARGET) </k>
259+
[priority(33)]
260+
261+
262+
// Case 5: multisig, owner != PROGRAM_ID, is_signer => Ok
263+
// Use wildcard for Ok's inner value to handle thunked () from result.clone()
264+
rule [inner-case5-spl]:
265+
<k> #validateOwnerResultSPL(
266+
OWNER_KEY, OWNER_KEY,
267+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
268+
Aggregate(variantIdx(1), _),
269+
Aggregate(variantIdx(0), _),
270+
DEST, TARGET)
271+
=> #setLocalValue(DEST, Aggregate(variantIdx(0), ListItem(Aggregate(variantIdx(0), .List)))) ~> #continueAt(TARGET) </k>
272+
[priority(33)]
273+
274+
275+
// Case 6: multisig, owner == PROGRAM_ID, is_initialized Err
276+
rule [inner-case6-spl]:
277+
<k> #validateOwnerResultSPL(
278+
OWNER_KEY, OWNER_KEY,
279+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
280+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(1), _))),
281+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(3), .List))),
282+
DEST, TARGET)
283+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(3), .List)))) ~> #continueAt(TARGET) </k>
284+
285+
[priority(31)]
286+
287+
288+
// Case 7: multisig, owner == PROGRAM_ID, is_initialized = Ok(false)
289+
rule [inner-case7-spl]:
290+
<k> #validateOwnerResultSPL(
291+
OWNER_KEY, OWNER_KEY,
292+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
293+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(BoolVal(false))))),
294+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(9), .List))),
295+
DEST, TARGET)
296+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(9), .List)))) ~> #continueAt(TARGET) </k>
297+
298+
[priority(31)]
299+
300+
301+
// Cases 8-9: multisig initialized, signer-checking => Err(MissingRequiredSignature)
302+
// No requires condition — lower priority ensures cases 4-5 are tried first.
303+
// This fires only when 4-5 don't match (i.e., owner IS the program ID).
304+
rule [inner-case8-9-missing-sig-spl]:
305+
<k> #validateOwnerResultSPL(
306+
OWNER_KEY, OWNER_KEY,
307+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
308+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(BoolVal(true))))),
309+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List))),
310+
DEST, TARGET)
311+
=> #setLocalValue(DEST, Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(7), .List)))) ~> #continueAt(TARGET) </k>
312+
[priority(32)]
313+
314+
// Case 10: multisig initialized, enough signatures => Ok
315+
// No requires condition — same priority scheme as case 8-9.
316+
rule [inner-case10-ok-spl]:
317+
<k> #validateOwnerResultSPL(
318+
OWNER_KEY, OWNER_KEY,
319+
_IS_SIGNER, _OWNER_OF_ACCOUNT, _DATA, _TX_SIGNERS,
320+
Aggregate(variantIdx(1), ListItem(Aggregate(variantIdx(0), ListItem(BoolVal(true))))),
321+
Aggregate(variantIdx(0), _),
322+
DEST, TARGET)
323+
=> #setLocalValue(DEST, Aggregate(variantIdx(0), ListItem(Aggregate(variantIdx(0), .List)))) ~> #continueAt(TARGET) </k>
324+
[priority(32)]
325+
326+
endmodule
327+
```

kmir/src/kmir/kdist/mir-semantics/symbolic/spl-token.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ module KMIR-SPL-TOKEN
6363
6464
rule <k> #traverseProjection(toLocal(I), _ORIGINAL, .ProjectionElems, CONTEXTS)
6565
~> #writeProjectionForce(NEW)
66-
=> #forceSetLocal(local(I), #buildUpdate(NEW, CONTEXTS))
66+
=> #setLocalValue(place(local(I), .ProjectionElems), #buildUpdate(NEW, CONTEXTS))
6767
...
6868
</k>
6969
<locals> LOCALS </locals>

0 commit comments

Comments
 (0)