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

First version of Authz TLA+ #2

Merged
merged 29 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d083737
First version of Authz TLA+
hvanz Aug 16, 2022
12f40a1
TypeOK now holds with TLC
hvanz Aug 16, 2022
29b2292
Fix IsActive and IsExpired
hvanz Aug 16, 2022
560dff7
Restrict domain of grantStore only to active grants
hvanz Aug 16, 2022
a025ed8
Fix type invariant of grantStore
hvanz Aug 16, 2022
110a2f0
Split main module in two files, and other improvements
hvanz Aug 17, 2022
c88699d
Add properties to test
hvanz Aug 17, 2022
75d1c31
Create instances of all auths at once
hvanz Aug 18, 2022
365ee82
Fix Apalache types
hvanz Aug 18, 2022
3d78049
Many small fixes and refactorings
hvanz Aug 18, 2022
4e467c7
Fix spec for TypeOK
hvanz Aug 18, 2022
6d322f9
More improvements
hvanz Aug 18, 2022
fc94ab7
Add variable numRequests; fix record with EXCEPT
hvanz Aug 18, 2022
fc0ec35
Fix record with EXCEPT
hvanz Aug 18, 2022
0efdf17
Simplify Accept expression in Send
hvanz Aug 18, 2022
6498935
Fix Apalache types
hvanz Aug 18, 2022
bcba250
Update auth without accept
hvanz Aug 19, 2022
d1e8ec6
Send one msg per Exec call; code styling
hvanz Aug 19, 2022
1ce7c3f
first version of test scenarios
ivan-gavran Aug 19, 2022
13808f1
Merge pull request #4 from informalsystems/ivan/list_test_scenarios
ivan-gavran Aug 22, 2022
1900790
Add gitignore
hvanz Aug 23, 2022
421a126
Formatting
hvanz Aug 23, 2022
39f9855
Rename variable also in properties
hvanz Aug 23, 2022
fc3d59c
Fix Accept in Send and Stake auths
hvanz Aug 23, 2022
c68ae33
Split AuthzGrants into Grants and GrantMessages
hvanz Aug 23, 2022
c0fcb72
Repeated field in Apalache record type
hvanz Aug 23, 2022
07f2951
Allow Coins to include negative numbers
hvanz Aug 29, 2022
a4ed54d
Return insufficient-amount error in StakeAuth
hvanz Aug 29, 2022
a109275
Add allowList to SendAuthorization
hvanz Aug 29, 2022
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
6 changes: 6 additions & 0 deletions models/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Apalache
_apalache-out/
*_TTrace_*.tla

# TLC
*/states/
261 changes: 261 additions & 0 deletions models/authz/Authz.tla
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
-------------------------------- MODULE Authz ----------------------------------
(******************************************************************************)
(*
Official documentation: https://docs.cosmos.network/v0.46/modules/authz/
ADR: https://github.com/cosmos/cosmos-sdk/blob/main/docs/architecture/adr-030-authz-module.md
*)
(******************************************************************************)
EXTENDS GrantMessages, Maps, Integers

VARIABLES
\* @type: GRANT_ID -> GRANT;
grantStore, \* Representation of the KV store implemented by the authz
\* module in the server, used to store mappings from grant
\* triples to authorizations.

\* @type: EVENT;
lastEvent,

\* @type: RESPONSE_MSG;
expectedResponse,

\* @type: Int;
numRequests

-------------------------------------------------------------------------------
\* @type: (GRANT_ID) => Bool;
HasGrant(g) == g \in DOMAIN grantStore

\* @type: (GRANT_ID) => Bool;
IsExpired(g) ==
/\ HasGrant(g)
/\ grantStore[g].expirationTime = "past"

--------------------------------------------------------------------------------
AcceptErrors == {
"none",
"validator-not-allowed",
"validator-denied",
"insufficient-amount"
}

NoUpdate == [type |-> "NoUpdate"]

(* AcceptResponse instruments the controller of an authz message if the request
is accepted and if it should be updated or deleted. *)
\* https://github.com/cosmos/cosmos-sdk/blob/f2cea6a137ce19ad8987fa8a0cb99f4b37c4484d/x/authz/authorizations.go#L20
\* @typeAlias: ACCEPT_RESPONSE = [accept: Bool, delete: Bool, updated: AUTH, error: Str];
\* @type: Set(ACCEPT_RESPONSE);
AcceptResponse == [
\* If Accept=true, the controller can accept and authorization and handle the update.
accept: BOOLEAN,

\* If Delete=true, the controller must delete the authorization object and release storage resources.
delete: BOOLEAN,

\* Controller, who is calling Authorization.Accept must check if `Updated !=
\* nil`. If yes, it must use the updated version and handle the update on the
\* storage level.
updated: Authorization \cup {NoUpdate},

\* This field does not appear in the code; it's here just to simplify the spec.
error: AcceptErrors
]

--------------------------------------------------------------------------------
(******************************************************************************)
(* Operators that model processing of request messages. *)
(******************************************************************************)

\* The interface that includes the three operations below:
\* https://github.com/cosmos/cosmos-sdk/blob/3a1027c74b15ad78270dbe68b777280bde393576/x/authz/tx.pb.go#L331

\* https://github.com/cosmos/cosmos-sdk/blob/afab2f348ab36fe323b791d3fc826292474b678b/x/authz/keeper/msg_server.go#L14
\* @type: (MSG_GRANT) => RESPONSE_GRANT;
CallGrant(msgGrant) ==
IF msgGrant.granter = msgGrant.grantee THEN
[type |-> "grant", ok |-> FALSE, error |-> "granter-equal-grantee"]
ELSE IF msgGrant.grant.expirationTime = "past" THEN
[type |-> "grant", ok |-> FALSE, error |-> "authorization-expired"]
ELSE
[type |-> "grant", ok |-> TRUE, error |-> "none"]

--------------------------------------------------------------------------------
\* https://github.com/cosmos/cosmos-sdk/blob/afab2f348ab36fe323b791d3fc826292474b678b/x/authz/keeper/msg_server.go#L52
CallRevoke(msgRevoke) ==
CHOOSE response \in MsgRevokeResponses: response.ok

--------------------------------------------------------------------------------
\* https://github.com/cosmos/cosmos-sdk/blob/afab2f348ab36fe323b791d3fc826292474b678b/x/authz/keeper/keeper.go#L90
\* @type: (ADDRESS, SDK_MSG) => ACCEPT_RESPONSE;
DispatchActionsOneMsg(grantee, msg) ==
LET
granter == msg.signer \* Actually, granter is the first of the list of signers.
grantId == [granter |-> granter, grantee |-> grantee, msgTypeUrl |-> msg.content.typeUrl]
IN
IF granter = grantee THEN
[accept |-> TRUE, delete |-> FALSE, updated |-> NoUpdate, error |-> "none"]
ELSE IF ~ HasGrant(grantId) THEN
[accept |-> FALSE, delete |-> FALSE, updated |-> NoUpdate, error |-> "grant-not-found"]
ELSE IF grantStore[grantId].expirationTime = "past" THEN
[accept |-> FALSE, delete |-> FALSE, updated |-> NoUpdate, error |-> "authorization-expired"]
ELSE
Accept(grantStore[grantId].authorization, msg.content)

\* https://github.com/cosmos/cosmos-sdk/blob/afab2f348ab36fe323b791d3fc826292474b678b/x/authz/keeper/msg_server.go#L72
\* @type: (MSG_EXEC) => <<RESPONSE_EXEC, ACCEPT_RESPONSE>>;
CallExec(msgExec) ==
LET
acceptResponse == DispatchActionsOneMsg(msgExec.grantee, msgExec.msg)
execResponse == [
type |-> "exec",
ok |-> acceptResponse.accept,
error |-> acceptResponse.error
]
IN
<<execResponse, acceptResponse>>

--------------------------------------------------------------------------------
\* Only for the initial state.
NoResponse == [type |-> "NoResponse"]
NoEvent == [type |-> "NoEvent"]

\* EmptyMap is not accepted by Apalache's typechecker.
\* @type: GRANT_ID -> GRANT;
EmptyStore == [x \in {} |-> NoGrant]

TypeOK ==
/\ IsMap(grantStore, ValidGrantIds, Grants \cup {NoGrant})
/\ lastEvent \in RequestMessages \cup ExpireEvents \cup {NoEvent}
/\ expectedResponse \in Responses \cup {NoResponse}
/\ numRequests \in Nat

Init ==
/\ grantStore = EmptyStore
/\ lastEvent = NoEvent
/\ expectedResponse = NoResponse
/\ numRequests = 0

--------------------------------------------------------------------------------
\* @type: (MSG_GRANT) => GRANT_ID;
grantIdOfMsgGrant(msg) == [
grantee |-> msg.grantee,
granter |-> msg.granter,
msgTypeUrl |-> MsgTypeURL(msg.grant.authorization)
]

(******************************************************************************)
(* Request to give a grant from a granter to a grantee. *)
(* *)
(* From the code: "An authorization grant is created using the MsgGrant message.
If there is already a grant for the (granter, grantee, Authorization) triple,
then the new grant overwrites the previous one. To update or extend an existing
grant, a new grant with the same (granter, grantee, Authorization) triple should
be created."

The message handling should fail if:
- both granter and grantee have the same address.
- provided Expiration time is less than current unix timestamp.
- provided Grant.Authorization is not implemented.
- Authorization.MsgTypeURL() is not defined in the router (there is no defined
handler in the app router to handle that Msg types). *)
(******************************************************************************)
\* @type: (ADDRESS, ADDRESS, GRANT) => Bool;
RequestGrant(granter, grantee, grant) ==
LET
msg == [type |-> "grant", granter |-> granter, grantee |-> grantee, grant |-> grant]
g == grantIdOfMsgGrant(msg)
IN
/\ IsValid(g)
/\ ~ HasGrant(g) \/ IsExpired(g)
Comment on lines +169 to +170
Copy link
Contributor

Choose a reason for hiding this comment

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

My feeling is that the way RequestGrant and other top-level operators from Next are too restrictive for testing purposes. In particular, we want to be able to test inputs where:

  • granter is the same as grantee
  • there is already a grant given, and it's not expired

So, in short, we should treat the inputs maximally non-deterministically, and ensure that the implementation fails for incorrect combinations of input parameters.

/\ LET response == CallGrant(msg) IN
/\ IF response.ok THEN
grantStore' = MapPut(grantStore, g, grant)
ELSE
UNCHANGED grantStore
/\ expectedResponse' = response
/\ lastEvent' = msg
/\ numRequests' = numRequests + 1

(******************************************************************************)
(* Request to revoke a grant. *)
(******************************************************************************)
\* @type: (ADDRESS, ADDRESS, MSG_TYPE_URL) => Bool;
RequestRevoke(granter, grantee, msgTypeUrl) ==
LET g == [granter |-> granter, grantee |-> grantee, msgTypeUrl |-> msgTypeUrl] IN
/\ IsValid(g)
/\ HasGrant(g)
Comment on lines +186 to +187
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as RequestGrant above

/\ LET msg == [type |-> "revoke", granter |-> granter, grantee |-> grantee, msgTypeUrl |-> msgTypeUrl] IN
/\ lastEvent' = msg
/\ LET response == CallRevoke(msg) IN
/\ IF response.ok THEN
grantStore' = MapRemove(grantStore, g)
ELSE
UNCHANGED grantStore
/\ expectedResponse' = response
/\ numRequests' = numRequests + 1

\* https://github.com/cosmos/cosmos-sdk/blob/4eec00f9899fef9a2ea3f937ac960ee97b2d7b18/x/authz/keeper/keeper.go#L99
\* @type: (ADDRESS, SDK_MSG, ACCEPT_RESPONSE) => Bool;
PostProcessExec(grantee, msg, acceptResponse) ==
LET
\* @type: GRANT_ID;
g == [granter |-> msg.signer, grantee |-> grantee, msgTypeUrl |-> msg.content.typeUrl]
IN
IF acceptResponse.delete THEN
grantStore' = MapRemove(grantStore, g)
ELSE IF acceptResponse.updated # NoUpdate THEN
grantStore' = [grantStore EXCEPT ![g].authorization = acceptResponse.updated]
ELSE
UNCHANGED <<grantStore>>

(******************************************************************************)
(* Request to execute a message on behalf of a grantee. *)
(******************************************************************************)
\* @type: (ADDRESS, SDK_MSG) => Bool;
RequestExec(grantee, msg) ==
LET
request == [type |-> "exec", grantee |-> grantee, msg |-> msg]
responses == CallExec(request)
IN
/\ lastEvent' = request
/\ expectedResponse' = responses[1]
/\ PostProcessExec(grantee, msg, responses[2])
/\ numRequests' = numRequests + 1

(******************************************************************************)
(* Timeout being reached is abstracted away by the action Expire. What is
lost then is the relation between different grants and their expirations (in
real life, there could be a dependency: if A expires, then definitely B and C
have to expire). This action is a pure environment action and thus it does not
remove the grant from a set of active grants (the system will do so once it
notices that the grant expired, upon running the Execute function). *)
(******************************************************************************)
Expire(g) ==
/\ HasGrant(g)
/\ ~ IsExpired(g)
/\ grantStore' = [grantStore EXCEPT ![g].expirationTime = "past"]
/\ LET event == [type |-> "expire", g |-> g] IN
lastEvent' = event
/\ UNCHANGED <<expectedResponse, numRequests>>

--------------------------------------------------------------------------------
\* We keep action RequestExec separated from the other actions to be able to
\* check properties on grants without executing them.
NextA ==
\/ \E granter, grantee \in Address, grant \in Grants:
RequestGrant(granter, grantee, grant)
\/ \E g \in GrantIds:
RequestRevoke(g.granter, g.grantee, g.msgTypeUrl)
\/ \E g \in ValidGrantIds:
Expire(g)

Next ==
\/ NextA
\* NB: The implementation allows to send more than one message in an Exec
\* request. We model execution requests of only one message per call.
\/ \E grantee \in Address, msg \in SdkMsgs:
RequestExec(grantee, msg)

================================================================================
Created by Hernán Vanzetto on 10 August 2022
Loading