Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monitor specs for Timelock using reverse reasoning #58

Merged
merged 42 commits into from
Jun 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
6da4b83
preliminary timelock monitor spec
kuprumion May 16, 2024
9797680
add MonitorSpecs-Direct.png
kuprumion May 16, 2024
1c3f950
structured direct reasoning
kuprumion May 16, 2024
3ba7dfb
more about monitoring goals
kuprumion May 16, 2024
5971037
more precision
kuprumion May 16, 2024
c3584d7
incompleteness of direct reasoning
kuprumion May 16, 2024
93221d9
start on reverse monitor specs
kuprumion May 16, 2024
a706adc
more on reverse reasoning
kuprumion May 16, 2024
296d1a7
add analysis of the timelock monitor spec
kuprumion May 16, 2024
32f48e5
typo
kuprumion May 16, 2024
e974f96
rename Succeed -> Pass, Achieve -> Hold
kuprumion May 21, 2024
b813999
split into general theory and timelock example
kuprumion May 21, 2024
88ec11b
refine intro
kuprumion May 21, 2024
8165fcd
start on verifying monitor specs
kuprumion May 21, 2024
a31a0a3
update pics
kuprumion May 21, 2024
6cc61f3
defs
kuprumion May 21, 2024
2c83fb0
monitor defs
kuprumion May 21, 2024
17e7c6b
mv timelock_monitor.tla timelock_mon.tla
kuprumion May 21, 2024
a4a9d01
renaming
kuprumion May 21, 2024
068ba3c
add missing parts to timelock monitor spec; made it typecheck
kuprumion May 22, 2024
be366e5
move typedef.tla -> env.tla; split env -> env/tx
kuprumion May 27, 2024
71f97ba
env.method_name -> tx.method_name
kuprumion May 27, 2024
90e2c07
fix types
kuprumion May 27, 2024
9715ee7
timelock_mon_txs/tests.tla
kuprumion May 27, 2024
172e53b
More Timelock Inits, Txs, Res
kuprumion May 27, 2024
fcfa551
timelock verification conditions
kuprumion May 28, 2024
1bd4940
minor changes
kuprumion May 28, 2024
cb4c4f0
more precise restrictions on nondet txs
kuprumion May 28, 2024
41fab06
refine monitor verification conditions: include method name
kuprumion May 28, 2024
f16a96e
add comments
kuprumion May 28, 2024
4588ee8
one monitor per file; get rid of the args state var
kuprumion May 29, 2024
9e0c715
move instance_storage to a separate var
kuprumion May 29, 2024
95c7d7b
fix token_transferred predicate
kuprumion May 29, 2024
5f93947
add deposit_test.tla
kuprumion May 29, 2024
ffc99e2
remove outdated files
kuprumion May 29, 2024
2da2f2b
token_balance tests
kuprumion May 29, 2024
46334aa
claim tests
kuprumion May 29, 2024
443fc24
balance_record tests
kuprumion May 29, 2024
9432521
introduce central monitor.tla spec
kuprumion May 29, 2024
11c8c72
remove docs (to be done in a separate PR)
kuprumion Jun 3, 2024
71ab721
add Timelock contract link
kuprumion Jun 4, 2024
55ebc80
disable some of the checks as not-yet-implemented
kuprumion Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions doc/case-studies/timelock/balance_record.tla
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
(*
* Balance record effect monitor for the Timelock contract
*
* Andrey Kuprianov, 2024
*)
---- MODULE balance_record ----

EXTENDS timelock

\* This trigger fires when the balance record is created or destroyed
\* Notice that it doesn't track the record content
MonitorTrigger_BalanceRecord_RecordChanged ==
instance_has("Balance") /= next_instance_has("Balance")

\* This trigger fires when the balance record content changes
\* Notice that it will panic (won't fire) if the record doesn't exist
MonitorTrigger_BalanceRecord_ContentChanged ==
Balance /= Balance'
Comment on lines +15 to +18
Copy link
Collaborator

@thpani thpani Jun 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we're mixing semantics here.
The comment says it will "panic" if the record doesn't exist, but we decided to initialize every missing field to Gen(). But I think then you want

Suggested change
\* This trigger fires when the balance record content changes
\* Notice that it will panic (won't fire) if the record doesn't exist
MonitorTrigger_BalanceRecord_ContentChanged ==
Balance /= Balance'
\* This trigger fires when the balance record content changes
MonitorTrigger_BalanceRecord_ContentChanged ==
/\ instance_has("Balance")
/\ next_instance_has("Balance")
/\ Balance /= Balance'


\* Only deposit and claim methods are allowed to alter balances
MonitorEffect_BalanceRecord_AllowedToChange ==
tx.status = TRUE => (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this implied by the triggers? If they fired, there must've been a successful tx.

\/ tx.method_name = "deposit"
\/ tx.method_name = "claim"
)


\* Everything below is deterministic, and will be generated automatically
\* For now, we encode this manually

MonitorTrigger_BalanceRecord ==
\/ MonitorTrigger_BalanceRecord_RecordChanged
\/ MonitorTrigger_BalanceRecord_ContentChanged

MonitorEffect_BalanceRecord ==
/\ MonitorEffect_BalanceRecord_AllowedToChange

Monitor_BalanceRecord ==
MonitorTrigger_BalanceRecord => MonitorEffect_BalanceRecord

================================================
113 changes: 113 additions & 0 deletions doc/case-studies/timelock/balance_record_test.tla
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
(*
* Tests for the balance record effect monitor of the Timelock contract
*
* The concrete values will be fetched from blockchain transactions
* Here we manually create a few for testing purposes.
*
* Andrey Kuprianov, 2024
*)

---- MODULE balance_record_test ----

EXTENDS balance_record

\****************************************************************************************
\*
\* A monitor test N is validated via running
\* apalache-mc check --length=1 --init=Init_N --next=Next_N balance_record_test.tla
\*
\****************************************************************************************

\* Success: Record updated by the claim call
Init_1 ==
/\ env = [ current_contract_address |-> "this", ledger_timestamp |-> 10 ]
/\ tx = [ method_name |-> "claim", signatures |-> {"bob"}, status |-> TRUE ]
/\ instance_storage = { "Balance" }
/\ token_balances = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 100>>, <<"this", 100>>}) >>
})
/\ Balance = [
token |-> "TOK",
amount |-> 100,
claimants |-> <<"alice", "bob">>,
time_bound |-> [kind |-> "Before", timestamp |-> 42]
]

Next_1 ==
/\ env' = env
/\ tx' = tx
/\ instance_storage' = {}
/\ token_balances' = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 200>>, <<"this", 0>>}) >>
})
/\ Balance' = [
token |-> "",
amount |-> 0,
claimants |-> <<>>,
time_bound |-> [kind |-> "", timestamp |-> 0]
]
/\ Monitor_BalanceRecord

\* Failure: balance record removed by another call
\* Apalache should report a deadlock!
Init_2 ==
/\ env = [ current_contract_address |-> "this", ledger_timestamp |-> 10 ]
/\ tx = [ method_name |-> "withdraw", signatures |-> {"bob"}, status |-> TRUE ]
/\ instance_storage = { "Balance" }
/\ token_balances = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 100>>, <<"this", 100>>}) >>
})
/\ Balance = [
token |-> "TOK",
amount |-> 100,
claimants |-> <<"alice", "bob">>,
time_bound |-> [kind |-> "Before", timestamp |-> 42]
]

Next_2 ==
/\ env' = env
/\ tx' = tx
/\ instance_storage' = {}
/\ token_balances' = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 200>>, <<"this", 0>>}) >>
})
/\ Balance' = [
token |-> "",
amount |-> 0,
claimants |-> <<>>,
time_bound |-> [kind |-> "", timestamp |-> 0]
]
/\ Monitor_BalanceRecord

\* Failure: balance record changed by another call
\* Apalache should report a deadlock!
Init_3 ==
/\ env = [ current_contract_address |-> "this", ledger_timestamp |-> 10 ]
/\ tx = [ method_name |-> "claim_part", signatures |-> {"bob"}, status |-> TRUE ]
/\ instance_storage = { "Balance" }
/\ token_balances = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 100>>, <<"this", 100>>}) >>
})
/\ Balance = [
token |-> "TOK",
amount |-> 100,
claimants |-> <<"alice", "bob">>,
time_bound |-> [kind |-> "Before", timestamp |-> 42]
]

Next_3 ==
/\ env' = env
/\ tx' = tx
/\ instance_storage' = { "Balance" }
/\ token_balances' = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 150>>, <<"this", 50>>}) >>
})
/\ Balance' = [
token |-> "TOK",
amount |-> 50,
claimants |-> <<"alice", "bob">>,
time_bound |-> [kind |-> "Before", timestamp |-> 42]
]
/\ Monitor_BalanceRecord

================================================
108 changes: 108 additions & 0 deletions doc/case-studies/timelock/claim.tla
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
(*
* Claim method monitors for the Timelock contract
*
* Andrey Kuprianov, 2024
*)
---- MODULE claim ----

EXTENDS timelock

(*
@typeAlias: claimArgs = {
claimant: $address
};
*)
claim_typedefs == TRUE


\* @type: ($claimArgs) => Bool;
MustFail_claim_Unauthorized(args) ==
~authorized(args.claimant)

MustFail_claim_NoBalanceRecord(args) ==
~instance_has("Balance")

\* @type: ($claimArgs) => Bool;
MustFail_claim_NotClaimant(args) ==
\A i \in DOMAIN Balance.claimants:
Balance.claimants[i] /= args.claimant

\* One success condition: correctly claimed before time bound
MustPass_claim_BeforeTimeBound(args) ==
/\ Balance.time_bound.kind = "Before"
/\ env.ledger_timestamp <= Balance.time_bound.timestamp
Comment on lines +31 to +33
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of this is blocking, but from how the source code is structured:
https://github.com/yuzurush/soroban-contracts-101/blob/4f64753310b26c7853de1c9efcc0f6f2b349c15c/timelock/src/lib.rs#L104-L106
I see this (and the one below) conceptually as a MustFail_... predicate, since it panics if the condition doesn't hold, and executes a bunch of code afterwards if it does. So in my view:

Suggested change
MustPass_claim_BeforeTimeBound(args) ==
/\ Balance.time_bound.kind = "Before"
/\ env.ledger_timestamp <= Balance.time_bound.timestamp
MustFail_claim_BeforeTimeBound(args) ==
/\ Balance.time_bound.kind = "Before"
/\ env.ledger_timestamp > Balance.time_bound.timestamp


\* Another success condition: correctly claimed after time bound
MustPass_claim_AfterTimeBound(args) ==
/\ Balance.time_bound.kind = "After"
/\ env.ledger_timestamp >= Balance.time_bound.timestamp

\* @type: ($claimArgs) => Bool;
MustHold_claim_TokenTransferred(args) ==
token_transferred(
Balance.token, env.current_contract_address, args.claimant, Balance.amount)

MustHold_claim_BalanceRecordRemoved(args) ==
~next_instance_has("Balance")



\* Everything below is deterministic, and will be generated automatically
\* For now, we encode this manually

\* Auxiliary predicate describing the failure condition
\* (formed as a disjunction of all "MustFail" predicates)
MustFail_claim(args) ==
\/ MustFail_claim_NoBalanceRecord(args)
\/ MustFail_claim_NotClaimant(args)
\* Checking of the condition(s) below is not yet supported
\* \/ MustFail_claim_Unauthorized(args)

\* Auxiliary predicate describing the success condition
\* (formed as a disjunction of all "MustPass" predicates)
MustPass_claim(args) ==
\/ MustPass_claim_BeforeTimeBound(args)
\/ MustPass_claim_AfterTimeBound(args)
Comment on lines +63 to +65
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that this can be written this way, but not the rationale.

claim will also revert if it's called outside the timebound, so why isn't the dual of this simply in MustFail?


\* Auxiliary predicate describing the effect
\* (formed as a conjunction of all "MustHold" predicates)
MustHold_claim(args) ==
/\ MustHold_claim_BalanceRecordRemoved(args)
\* Checking of the condition(s) below is not yet supported
\* /\ MustHold_claim_TokenTransferred(args)


\* Monitor invariants to be checked
\* (encode the expected interpretation of monitor predicates)
Inv_MustFail_claim(args) ==
( /\ tx.method_name = "claim"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant, since the claim(...) operator only appears in the instrumented spec when the method name is "claim"

/\ MustFail_claim(args)
) => tx.status = FALSE

Inv_MustPass_claim(args) ==
( /\ tx.method_name = "claim"
/\ ~MustFail_claim(args)
/\ MustPass_claim(args)
) => tx.status = TRUE

Inv_MustHold_claim(args) ==
( /\ tx.method_name = "claim"
/\ tx.status = TRUE
) => MustHold_claim(args)


\* The main invariant
\* (formed as a conjunction of all auxiliary invariants)
\* @type: ($claimArgs) => Bool;
Inv_claim(args) ==
/\ Inv_MustFail_claim(args)
/\ Inv_MustPass_claim(args)
/\ Inv_MustHold_claim(args)

claim(claimant) ==
LET args == [
claimant |-> claimant
] IN
Inv_claim(args)

================================================
99 changes: 99 additions & 0 deletions doc/case-studies/timelock/claim_test.tla
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
(*
* Tests for the claim method of the Timelock contract
*
* The concrete values will be fetched from blockchain transactions
* Here we manually create a few for testing purposes.
*
* Andrey Kuprianov, 2024
*)

---- MODULE claim_test ----

EXTENDS claim

\****************************************************************************************
\*
\* A monitor test N is validated via running
\* apalache-mc check --length=1 --init=Init_N --next=Next_N claim_test.tla
\*
\****************************************************************************************

\* Successful claim call
Init_1 ==
/\ env = [ current_contract_address |-> "this", ledger_timestamp |-> 10 ]
/\ tx = [ method_name |-> "claim", signatures |-> {"bob"}, status |-> TRUE ]
/\ instance_storage = { "Balance" }
/\ token_balances = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 100>>, <<"this", 100>>}) >>
})
/\ Balance = [
token |-> "TOK",
amount |-> 100,
claimants |-> <<"alice", "bob">>,
time_bound |-> [kind |-> "Before", timestamp |-> 42]
]

Next_1 ==
/\ env' = env
/\ tx' = tx
/\ instance_storage' = {}
/\ token_balances' = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 200>>, <<"this", 0>>}) >>
})
/\ Balance' = [
token |-> "",
amount |-> 0,
claimants |-> <<>>,
time_bound |-> [kind |-> "", timestamp |-> 0]
]
/\ claim("bob")


\* Failing claim call: no balance record
Init_2 ==
/\ env = [ current_contract_address |-> "this", ledger_timestamp |-> 10 ]
/\ tx = [ method_name |-> "claim", signatures |-> {"bob"}, status |-> FALSE ]
/\ instance_storage = { }
/\ token_balances = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 100>>, <<"this", 100>>}) >>
})
/\ Balance = [
token |-> "TOK",
amount |-> 100,
claimants |-> <<"alice", "bob">>,
time_bound |-> [kind |-> "Before", timestamp |-> 42]
]

Next_2 ==
/\ env' = env
/\ tx' = tx
/\ instance_storage' = instance_storage
/\ token_balances' = token_balances
/\ Balance' = Balance
/\ claim("bob")


\* Failing claim call: not a claimant
Init_3 ==
/\ env = [ current_contract_address |-> "this", ledger_timestamp |-> 10 ]
/\ tx = [ method_name |-> "claim", signatures |-> {"bob"}, status |-> FALSE ]
/\ instance_storage = { "Balance" }
/\ token_balances = SetAsFun({
<< "TOK", SetAsFun({ <<"alice", 100>>, <<"bob", 100>>, <<"this", 100>>}) >>
})
/\ Balance = [
token |-> "TOK",
amount |-> 100,
claimants |-> <<"alice", "bob">>,
time_bound |-> [kind |-> "Before", timestamp |-> 42]
]

Next_3 ==
/\ env' = env
/\ tx' = tx
/\ instance_storage' = instance_storage
/\ token_balances' = token_balances
/\ Balance' = Balance
/\ claim("carol")

================================================
Loading
Loading