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

Conversation

andrey-kuprianov
Copy link
Collaborator

@andrey-kuprianov andrey-kuprianov commented May 16, 2024

TLA+ specs modeled after https://github.com/stellar/soroban-examples/blob/v20.0.0/timelock/src/lib.rs

closes #37.

The TLA+ specs are complete in the sense that the instrumented version passes apalache-mc check as expected.

@andrey-kuprianov andrey-kuprianov self-assigned this May 16, 2024
@andrey-kuprianov andrey-kuprianov added this to the M4: Case study milestone May 16, 2024
Copy link
Collaborator

@Kukovec Kukovec left a comment

Choose a reason for hiding this comment

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

The overall approach idea LGTM, though I have raised a point RE mustfail/mustsucceed semantics in the comments.

Since you say the monitor spec is incomplete I haven't really looked into it much. Also, per your request, I've avoided giving proofreading-suggestions, though I would recommend doing a proofreading pass before merging once all conceptual discussions are out of the way.

All predicates which refer to the same `<Method>` will be grouped, to create together one _method monitor_. Interpreted formally, the monitor should do the following when `<Method>` is invoked:

1. If any of `MustFail_i` conditions fire, check that method invocation reverts (otherwise, issue a warning / revert if configured to do so)
2. If none of `MustFail_i` conditions fired, but method invocation reverted, issue a warning (incomplete spec)
Copy link
Collaborator

Choose a reason for hiding this comment

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

As a user, would you really want this? This is essentially requiring that the monitor authors care and specify all of the possible transaction failing scenarios, which limits your ability to do abstractions or partial monitors.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, you don't have to give all failure scenarios. If there is a failure case which is not in the spec, there simply will be a warning (which can be also suppressed if needed), no other consequences.

1. If any of `MustFail_i` conditions fire, check that method invocation reverts (otherwise, issue a warning / revert if configured to do so)
2. If none of `MustFail_i` conditions fired, but method invocation reverted, issue a warning (incomplete spec)
3. If none of `MustFail_i` fired, and one of `MustSucceed_i` conditions fired, check that method invocation succeeds (otherwise, issue a warning)
3. If none of `MustFail_i` fired, and none of `MustSucceed_i` conditions fired, but method invocation succeeded, issue a warning of an incomplete spec (or revert if configured to do so)
Copy link
Collaborator

Choose a reason for hiding this comment

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

See above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The same. The idea is that the monitor is configurable: it may either be completely silent in some cases, or issue a simple warning, or in most severe cases interfere and abort a transaction.

In the first stage of the project it will be at most a warning. But the ultimate goal is to provide the really protective solution for the live blockchain.

4. If method invocation succeeds, check that all of `MustAchieve_i` conditions hold on the pre- and post-states of the method invocation (otherwise, issue a warning / revert if configured to do so)


Notice that above we apply _or_ as default connector for preconditions (`MustFail_i` / `MustSucceed_i`), and we apply _and_ as default connector for effects (`MustAchieve_i`). Thus, you may split preconditions/effects into separate predicates at your convenience, thus avoiding complicated logical structure inside predicates themselves.
Copy link
Collaborator

Choose a reason for hiding this comment

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

You say "default", is there ever a case where this wouldn't be so?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

no, it's just an unfortunate wording:) it's fixed, to that, not default


Notice that above we apply _or_ as default connector for preconditions (`MustFail_i` / `MustSucceed_i`), and we apply _and_ as default connector for effects (`MustAchieve_i`). Thus, you may split preconditions/effects into separate predicates at your convenience, thus avoiding complicated logical structure inside predicates themselves.

Now, is the approach described above enough to achieve the goals (safety, liveness, completeness) stated above? Think about it for a sec, before clicking on the our answer below.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Now, is the approach described above enough to achieve the goals (safety, liveness, completeness) stated above? Think about it for a sec, before clicking on the our answer below.

Not sure this really needs to be in the docs, this feels more like how you'd write up homework for students.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's more in the style of a blog post, actually; I might eventually make it that. For now it doesn't matter, I suppose, it will change quite a few times...

\* Balance is externally relevant; initialized flag is not
\* What matters for users is that balance is not overwritten
MustFail_deposit_AlreadyInitialized ==
exists(balance)
Copy link
Collaborator

Choose a reason for hiding this comment

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

What is this exists? I can't seem to find a definition or reference.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yeah, sorry, the defs are missing. exists is supposed to mean that the key is present in the ledger. We'll need to figure out the exact mapping between TLA+ and Soroban constructs

Comment on lines 134 to 137
/\ balance'.token = token
/\ balance'.amount = amount
/\ balance'.time_bound = time_bound
/\ balance'.claimants = claimants
Copy link
Collaborator

Choose a reason for hiding this comment

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

From what I can find, balance is not a record type: https://github.com/freespek/solarkraft/blob/32f48e5f26e4cce76df4703a1b29fd1752cd1677/doc/scf24/example/timelock_mon2.tla

I know you say this spec is not valid TLA+, but having variable declarations and their types would help a lot.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For the actual semantics of what both specs are supposed to represent, look at the Timelock contract. In particular, balance is an entry in the ledger with the Balance key, which points to the ClaimableBalance struct when the key is present.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I also agree that the defs of the structs and everything should be present eventually. The idea now is to evaluate the approach as a whole, and to fill in the missing pieces later.

Not directly related to this, but tangentially: the idea is that eventually the definitions wrt. the concrete contract could be auto-generated from the contract source code, so we won't have to write them by hand. But that's also not in the current stage, but in a subsequent one.

@konnov
Copy link
Contributor

konnov commented May 16, 2024

This looks like a writeup for a blogpost. I was trying to understand how the structure imposed here would fit into the standard pre/post-conditions approach. I guess, it imposes some kind of specific shape on the monitors.

@konnov
Copy link
Contributor

konnov commented May 16, 2024

Since we have to run solarkraft verify against the timelock transactions, how shall we do that? Perhaps, add notes on that?

@andrey-kuprianov
Copy link
Collaborator Author

Since we have to run solarkraft verify against the timelock transactions, how shall we do that? Perhaps, add notes on that?

That's a good point. Insofar it has been in isolation; I need to make sure it fits well with the rest. Will do that.

Copy link
Collaborator

@thpani thpani left a comment

Choose a reason for hiding this comment

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

In general, I like the idea. This would make for an interesting blogpost, for sure.

In practice, I'm concerned that this will not be feasible in this round:
Remember that Apalache processes invariants into verification conditions and actions into transitions, without giving much guarantees about their correspondence. The quantifications you use here (none/any/all of X fire) will be hard to check, or at least require multiple instrumented specs and model-checker calls, which will become quite costly.

@andrey-kuprianov
Copy link
Collaborator Author

The quantifications you use here (none/any/all of X fire) will be hard to check, or at least require multiple instrumented specs and model-checker calls, which will become quite costly.

Also a good point: I of course need to make sure that this fits well with the Apalache MC. Otoh, we could say that this is the general approach we use for structuring the monitor specs, and then in this phase cut some corners, e.g. perform certain translation steps manually, and automate them in the subsequent phases.

I will investigate.

@Kukovec
Copy link
Collaborator

Kukovec commented May 29, 2024

I've noticed this PR seems to be creeping up on 1k LOC, would it be possible to split this into smaller PRs, which are easier to digest?

@thpani thpani self-requested a review June 5, 2024 10:49
Copy link
Collaborator

@Kukovec Kukovec left a comment

Choose a reason for hiding this comment

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

P1: claim, env, monitor, token

doc/case-studies/timelock/env.tla Show resolved Hide resolved
*)
typedefs == TRUE

VARIABLES
Copy link
Collaborator

Choose a reason for hiding this comment

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

There's a broader issue here with how this is defined: If env, tx and instance_strorage are to be defined as TLA state variables, then instrumentation would have to instantiate them all, which is not what happens (see here)
The env gets passed as the 1st function argument, the data in tx is not instrumented at the moment, and neither is instance_storage, though the latter would probably be the easiest to simply add in.

EXTENDS env, token

(*
@typeAlias: timeBoundKind = Str;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd personally introduce an uninterpreted type here, e.g. "TBK" or even "TIME_BOUND_KIND", but it's not blocking

Comment on lines +31 to +33
MustPass_claim_BeforeTimeBound(args) ==
/\ Balance.time_bound.kind = "Before"
/\ env.ledger_timestamp <= Balance.time_bound.timestamp
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

(*
@type: $address -> $address -> Int;
*)
token_balances
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar to my comment in env, be conservative with VARIABLES, because they'd have to be instrumented, and I don't think this one is instrumented anywhere. Do we know how instrumentation behaves, repectively should behave, w.r.t. soroban_sdk::contractimport!?

Comment on lines +21 to +25
IF token \in DOMAIN token_balances THEN
IF id \in DOMAIN token_balances[token] THEN
token_balances[token][id]
ELSE 0
ELSE 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
IF token \in DOMAIN token_balances THEN
IF id \in DOMAIN token_balances[token] THEN
token_balances[token][id]
ELSE 0
ELSE 0
IF (token \in DOMAIN token_balances /\ id \in DOMAIN token_balances[token] )THEN
token_balances[token][id]
ELSE 0

Minor backend optimization, each ITE is a pick

Comment on lines +29 to +33
IF token \in DOMAIN token_balances' THEN
IF id \in DOMAIN token_balances'[token] THEN
token_balances'[token][id]
ELSE 0
ELSE 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

See above

\* 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"

Copy link
Collaborator

@thpani thpani left a comment

Choose a reason for hiding this comment

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

It's clear that this has been developed out of sync with the rest of the system.
I don't know what to do at this state, except to merge it and then try to incrementally make it work, which will be much harder than just incrementally adding working pieces from the beginning.

I have not looked at the tests. Could you add a shell script that executes them so we have some kind of QA here?

Below are some comments that I would like to see addressed before we merge this and take further steps.

Comment on lines +15 to +16
\* Checking of the condition(s) below is not yet supported
\* /\ Monitor_TokenBalance
Copy link
Collaborator

Choose a reason for hiding this comment

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

When you say "not yet supported", why?

It looks like the token_balance module becomes irrelevant then and can be pruned.

Comment on lines +15 to +18
\* 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'
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'

Comment on lines +63 to +65
MustPass_claim(args) ==
\/ MustPass_claim_BeforeTimeBound(args)
\/ MustPass_claim_AfterTimeBound(args)
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?


\* 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.


\* Only claim method is allowed to reduce this contract token balance
MonitorEffect_TokenBalance_AllowedToReduce ==
tx.status = TRUE => 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.

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

Comment on lines +71 to +73
\* Checking of the condition(s) below is not yet supported
\* \/ MustFail_deposit_Unauthorized(args)
\* \/ MustFail_deposit_NotEnoughBalance(args)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is MustFail_deposit_NotEnoughBalance not working?

In general, we should add issues for each of the ones that need to be commented out.

Copy link
Collaborator

@Kukovec Kukovec left a comment

Choose a reason for hiding this comment

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

Per our discussion on Friday, I'm going to merge this as-is, and use it as a reference point to write a monitor which is syntactically more aligned with our current instrumentation strategy.

@Kukovec Kukovec merged commit 4ca00e5 into main Jun 10, 2024
3 checks passed
@Kukovec Kukovec deleted the andrey/reverse-reasoning branch June 10, 2024 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Investigate reverse reasoning for monitors
5 participants