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

FLIP - Extended Transaction Format #41

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 3 commits
Commits
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
111 changes: 111 additions & 0 deletions flips/20221024-extended-transaction-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Extended Transaction Format [DRAFT]

| Status | Draft |
| :------------ | :------------------------------------------------------------- |
| **FLIP #** | TBD |
| **Forum** | TBD |
| **Author(s)** | Jeffrey Doyle (jeffrey.doyle@dapperlabs.com) |
| **Sponsor** | Jeffrey Doyle (jeffrey.doyle@dapperlabs.com) |
| **Updated** | 2022-10-24

## Abstract

This FLIP proposes a change to Cadence, Flow Client Libraries and Wallets on Flow to increase the flexibility of how applications and wallets coordinate on determining the assets and accounts used in a transaction.

## Motivation

Often, transactions today are written with an understanding of where assets are in the accounts used in the transaction. Transactions are also written with a notion of what accounts a user controls, and how their wallet performs actions with that users assets.

As Flow progresses to allow assets to be stored wherever the account owner and wallet may choose, and for the wallet to use their preferred account setup to coordinate actions with the users assets, more flexibility in how transaction developers write their transactions is required.

## Design Proposal

This FLIP proposes that transactions should have any number of `prepare` statments and any number of `post` statments. Each prepare statment would be responsible for assigning values to variables defined in the transaction annotated with the same `role` as is assigned to the prepare block.

Each prepare statment would have the ability to initialize transaction varaibles, but not read their values. Each prepare statment can only assign variables annotated with the same role as itself. Since prepare statments can mutate execution state, the order of execution of each prepare statment must match the order they are defined in the cadence transaction.
JeffreyDoyle marked this conversation as resolved.
Show resolved Hide resolved

A role would be assigned to a variable or prepare phase by an annotation above them (eg: `#role: Example`). Each signer / wallet involved in the transaction is assigned one of the roles defined in the transaction. Each transaction could have any number of roles for it's variables and prepare statements, but each variable and prepare statment can only be assigned to one role. Optionally, each signer / wallet can append a post statment to the transaction with whatever conditions they require.
Copy link
Member

Choose a reason for hiding this comment

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

Can all post-statements access variables with any role?

I assume the execute statement can access any variable regardless of the role? Can they mutate though?


Each signer / wallet is responsible for producing the content of their assigned prepare statement using a non-empty set of accounts they control if the prepare statement is not already defined in the transaction. This way, the wallet can choose which accounts they need to use, and how they need to accomplish assigning values for the variables they're responsible for.

For example, a Cadence transaction might look like:

```cadence
// NFT Purchase & Transfer
transaction(nftID: UInt64) {

#role: Seller
Copy link
Member

Choose a reason for hiding this comment

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

Cadence already has support for pragma declarations, which have the syntax '#' expression. They are not used for anything yet, but were added early on in anticipation of such features. A use like #role: Seller is thus currently syntactically invalid. A valid use could be e.g. #role(Seller), which in terms of the AST would be expressed as a invocation expression of identifier role, with one argument, the identifier Seller. Another valid use could be e.g. #tx(role: Seller).

Also, pragma declarations are self-standing, they are not associated with any other declaration. However, for a transaction declaration, Cadence could look specifically for preceding pragma declarations and consider them.

Copy link
Contributor

Choose a reason for hiding this comment

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

Another alternative here is to parse the docstring of said transaction, i have a very crude attempt at that in a branch in overflow. the transaction i process there is here https://github.com/bjartek/overflow/blob/flix2/transactions/mint_tokens.cdc

However i think pragma is the better approach here, especially if it can be syntax checked. Since a comment is well a comment.

Copy link
Member

@SupunS SupunS Nov 25, 2022

Choose a reason for hiding this comment

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

Each prepare statment would have the ability to initialize transaction varaibles, but not read their values. Each prepare statment can only assign variables annotated with the same role as itself.

This sounds more like scoping to me. What if a new "role block" is introduced?
Something like:

role seller {
    let nft: @NonFungibleToken.NFT

    prepare()
}

role buyer {
    let receiver: Capability<&{NFT.Receiver}>

    prepare()
}

So that it's clear on the scoping rules, and also:

  • Don't have to repeat the pragma for every variable/prepare block
  • Has less chance of making mistakes. e.g: assigning the wrong role (wrong pragma) to the wrong variable.

I'm not quite sure what the variable access semantic should be though. One option is to make them seamlessly available to execute and post blocks. Another option would be to use role-qualified-names. e.g: seller.nft, etc. The latter would allow you to have separate namespaces for roles (so don't have to worry about duplicate names)

Copy link
Member

Choose a reason for hiding this comment

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

Great idea! This scoped approach would also solve the problem of definite initialization in the prepare block 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

I really like this idea! Having a new block would solve this and the related problems pretty neatly, and also be fairly clear to a reader.

I'm curious though whether pre and post blocks need to be similarly separated though; as with the upcoming changes in Stable Cadence to restrict condition blocks to being view, they won't be subject to the same concerns about malicious use of accounts and data that prepare functions are. Seems reasonable to have these conditions exist outside of role blocks and make any variables defined within those blocks available outside them.

Copy link
Member

Choose a reason for hiding this comment

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

While implementing type checking for role blocks I realized the current resource semantics will not allow roles blocks to have resource-kinded fields, because they somehow need to be invalidated.

In the case of a transaction, the execute block acts as the destructor and allows invalidation of resource-kinded fields.

However, in role blocks, there is no execute block, but rather the outer transaction's execute block should probably be allowed to invalidate role blocks' resource kinded fields.

For example, we probably want to allow something like this example, which is currently rejected (correctly) due to the nested move in the last statement.

resource R {}

fun absorb(_ r: @R) {
    destroy r
}

transaction {

    role buyer {
        var x: @R

        prepare() {
            self.x <- create R()
        }
    }

    execute {
        absorb(<-self.buyer.x)
    }
}

Trying to work around this by e.g. making the field optional, for which nested moves are allowed, still gets rejected (correctly), because the optional resource-kinded field could still have a value, and there is no destructor which invalidates it.

resource R {}

fun absorb(_ r: @R) {
    destroy r
}

transaction {
    role buyer {
        var x: @R?

        prepare() {
            self.x <- create R()
        }

    }

    execute {
        let x <- self.buyer.x <- nil
        absorb(<-x!)
    }
}

Support for resource-kinded fields in roles is probably wanted, for example one could imagine a buyer role which gets the FT Vault needed to purchase an item.

If we do want to support resource-kinded fields in roles, should we extend resource invalidation and allow for nested field invalidation? For the first example above: the transaction's buyer field is indirectly resource-kinded, because it has a resource-kinded field, so must be invalidated – given that roles are not first-class values, at the role's resource-kinded fields must be invalidated. Currently this is limited to just fields of self.

Copy link
Contributor

Choose a reason for hiding this comment

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

What if instead of thinking of the role like an actual object in the language with fields of its own, we treated it like a namespace? This way we wouldn't have to special case its "fields", because those fields would exist on the outer transaction's scope, they would just be namespaced to their role.

Copy link
Member

@SupunS SupunS Jan 18, 2023

Choose a reason for hiding this comment

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

What if instead of thinking of the role like an actual object in the language with fields of its own, we treated it like a namespace?

I like this idea. This is exactly how I had imagined when I first proposed the syntax. Role-block is just a named-block/named-scope and nothing else. For eg., many languages support the block syntax { } at the statement level to allow having a separate scope for certain things, and this is also that, but with a label.

Copy link
Member

Choose a reason for hiding this comment

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

How would this namespacing be implemented? I'm not saying it couldn't, I'm really just trying to think through this alternative and how it could be achieved, as well as how it would compare to other solutions.

For the definite initialization checking in the prepare blocks, the transaction's prepare block would need to ignore all of the roles' fields, and vice-versa, each role block would need to only consider its own fields and ignore all other fields?

For the resource checking, we would still need to adjust it and allow nested invalidation, because AFAICS currently it is implemented on a syntax level. For example, in the example above, <-self.buyer.x would internally just refer to self's "buyer_x" field, but syntactically it still looks like a nested access.

Copy link
Member

Choose a reason for hiding this comment

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

Luckily, allowing nested invalidation of resource-kinded fields of transaction roles turned out to be not hard to implement: onflow/cadence#2262. I think that resolves this discussion

let nft: @NonFungibleToken.NFT

#role: Buyer
let payment: @FlowVault
#role: Buyer
let receiver: Capability<&{NFT.Receiver}>

#role: Seller
prepare() // <— Seller wallet fills this in and assigns variables marked #Seller
Copy link
Member

Choose a reason for hiding this comment

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

Do I understand it correctly, that the idea is that this empty template is presented to multiple wallets, and they each fill in this empty/partially filled transaction?
In that case we would need to expand the syntax to allow empty blocks.

Copy link
Contributor

Choose a reason for hiding this comment

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

From prior discussions on this I belive this assumption is true.

Copy link
Collaborator

@bluesign bluesign Nov 25, 2022

Choose a reason for hiding this comment

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

It will be ping pong little, proposer will propose, then it will go wallets they will add their prepares, it will go back to proposer, then proposer will merge them, then it will go to signers, each of them will sign and send back to proposer, then proposer will send to payer, payer will sign, then send back to proposer, proposer will send to network :)


#role: Buyer
prepare() // <— Buyer wallet fills this in and assigns variables marked #Buyer
Copy link
Member

Choose a reason for hiding this comment

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

Supporting multiple prepare blocks, post conditions, etc. in the parser is no problem.

Given that prepare blocks currently checked like initializers, i.e. they must initialize all fields of the transaction, this check would need to be extended to support partial initialization in one block, and only consider all blocks together as the total set of initialized fields.

Copy link
Member

Choose a reason for hiding this comment

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

How would a case of multiple prepare statements with varying prepare block parameter lists be handled?

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not OP here, but we have discussed this a bit in various meetings and I am quite passionate about this so i I will chime in. Feel free to correct me @JeffreyDoyle.

My understanding is that each prepare block can be filled out indepedently and composed into a transaction.
So lets say we have a buyer and a seller with two prepare blocks, for the seller we need to prepare the receiving vault while for the buyer we might prepare both the sending vault and the receiving collection/capability.

This also ties into flix where each flix can be a single role here if my understanding is correct. So the buyer pre/post pair would be one flix and the seller pre/post could be another.


pre {
payment.balance == 20.0
}

execute {
...
}

post {
... // <-- Buyer produces a post statment to check that Buyer received the NFT
Copy link
Contributor

Choose a reason for hiding this comment

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

And that the seller has 20 flow less. @bluesign have propossed that mutating state should have mandatory post conditions. It is a lot of work but it is very safe.

}

post {
... // <-- Seller produces a post statment to check that Seller received 20.0 FLOW
Copy link
Contributor

Choose a reason for hiding this comment

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

What about royalty in this scenario? Should the transaction care about that aswell. In .find for instance royalty is handled by the contract not by the transaction, so that it cannot be skipped. In this case i guess the pre block for this role would fetch multiple variables for fund receivers and assert on all of them.

}
}
```

Here, the transaction developer is declaring that there are two roles in the transaction, "Buyer" and "Seller". The transaction is effectively saying:

> "This transaction requires the Buyer to produce a vault of 20.0 FLOW tokens and a reciever capability to recieve the NFT, and the Seller to put up the NFT corresponding to nftID".

Each wallet / signer for the Buyer and Seller would be responsible for producing the prepare statment assigned to them. This way, the transaction developer is able to declare what happens in the transaction without having to know where in the users accounts their assets are, or which Flow accounts are needed to be used to accomplish the transaction.

Each wallet / signer involved in the transaction benefits from having their own isolated prepare statement where they can engague with the accounts they control. This way, wallets do not need to concern themselves with the content of other prepare statments in the transaction where other AuthAccounts are made available, as the AuthAccounts the wallet controls are not impacted by them.

In the case where the wallet produces the content of their assigned prepare statment, they benefit by not needing to be concerned about a malicious prepare statement written by a 3rd party having access to the AuthAccounts they control.

Flow's Client Libraries would need to be modified to support asking wallets / signers to produce the prepare statments they would like to use for a given transaction. The client libraries would also need to be modified to support declaring the role of each authorizer of the transaction, so the corresponding wallet / signer can understand which role they are acting as.

The Flow transaction data structure and Access Node API must be updated to coordinate which signature(s) for each authorizer correspond to which AuthAccount in each prepare statment of the transaction.

Here is an example of how FCL-JS might be used to execute such a transaction:

```javascript
const txId = await fcl.mutate({
cadence: `...`,
authorizers: [
appAuthzWallet({ role: “Seller” }), // Authorizer (wallet) for first prepare block (#Seller)
fcl.currentUser.authz({ role: “Buyer” }) // Authorizer (wallet) for second prepare block (#Buyer)
]
})
```

## Considerations / Dependencies

This proposal involves complex changes to multiple areas of Flow, including:
- The Cadence language
- How Flow executes Cadence transactions
- The Flow Transaction data structure
- Flow Access Node API
- Flow Client Library (FCL) & FCL wallet provider spec.
- Flow Wallets

The benefits of this proposal should be appropriately weighed against the effort required to implement and cordinate this proposals changes among the affected areas and parties.

For wallets, considerable complexity needs to be addressed in how the wallet can interpret a cadence transaction, and understand how to produce the cadence code required to properly assign the varaiables they are responsible for in their assigned prepare statement.

## Questions and Discussion Topics

- Do viable alternatives to this proposal exist that allow greater flexibility in how applications and wallets coordinate on determining the assets and accounts used in a transaction?