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

Refactor Transaction Receipt/Result/State #36

Closed
lrettig opened this issue Jun 1, 2020 · 30 comments
Closed

Refactor Transaction Receipt/Result/State #36

lrettig opened this issue Jun 1, 2020 · 30 comments

Comments

@lrettig
Copy link
Member

lrettig commented Jun 1, 2020

Per today's design review call, Noam and Iddo both questioned the current design of TransactionResult. Maybe what we really want to know is: 1. was the transaction "effective" or not (Iddo's term, i.e., syntactically valid), 2. did the transaction succeed or fail?, and 3. was a fee charged for it or not? These things are not immediately visible from the current design. Do we want to rethink the design?

@avive
Copy link
Contributor

avive commented Jun 3, 2020

I'm a bit confused about this issue.

A transaction is effective / syntactically valid if it made its way to the mesh and invalid if it was rejected before that as there are validity checks when it is submitted by a client to a node for broadcasting. See TransactionState.REJECTED.

I think that as soon as it is TransactionState.PENDING on its journey to the mesh, it means it passed syntactic validation and is in the pool.

The TransactionState enum is design to capture all possible rejections which can be considered types of failures before it gets to STF execution. What's may be confusing to reviewers here is that there are pre-stf validation checks (designed by Noam) so a transaction may be rejected by-stf due to non-syntactic reasons such as counter and balance. However, all of these failures are captured by TransactionState.

@noamnelke @tal-m

Regarding execution failure. The receipt should have all the information regarding the other questions in the issue which are different questions than syntactic validity. So, I don't understand the concern described based on the current design - it is immediately clear exactly what is the transaction status from the receipt, and the TransactionStatus enum should cover all possible cases and questions raised above regarding execution and there's also a field in the receipt for how much gas was consumed. So that is fully addressed.

A receipt is generated even if execution 'fails' because gas is consumed and the tx affected the global state in that way, even though the intent of the tx was not applied. So, even a transaction that was no applied - e.g funds were not moved according what it specifies, affects the global state and generates a receipt. This is good because the receipt is the only way for clients to know when it was executed (layer # of STF) and why it wasn't applied - and TransactionResult field tells you exactly that.

I think Iddo and Tal argued we should expose the ordered list of input transactions to the STF in each layer via the global state API, and have an index in the tx receipt that indicates the location of the tx in this ordered list so we can show users what txs were executed before or after it in the layer. As the ordered list is per layer, this gives us all the ordered txs across all layers which Tal mentioned.

@lrettig
Copy link
Member Author

lrettig commented Jun 3, 2020

A transaction is effective / syntactically valid if it made its way to the mesh and invalid if it was rejected before that as there are validity checks when it is submitted by a client to a node for broadcasting.

My understanding, based on our conversation, is that it's not this simple. As one example, a transaction may be included in two different layers (#8). It may not be "effective" the first time it's included, because, e.g., the nonce is too low, but it may become "effective" in a later layer, or vice-versa. The same tx in a later layer may not even be visible to/processed by the STF, depending how we design the protocol.

there are pre-stf validation checks (designed by Noam) so a transaction may be rejected by-stf due to non-syntactic reasons such as counter and balance... all of these failures are captured by TransactionState.

Agree with this, but it's worth pointing out that TransactionState is not canonical because one node might reject a tx that another node admits into its txpool/puts into a block.

we should expose the ordered list of input transactions to the STF in each layer via the global state API, and have an index in the tx receipt that indicates the location of the tx in this ordered list

See #34

@avive
Copy link
Contributor

avive commented Jun 3, 2020

I don't understand your examples. Let's go case by case..
If a transaction has the wrong nonce it will should be filtered out from the list of txs for execution by the stf - only synoptically valid transactions can be on this list based among other things on the other txs in this list... In that case, it won't be executed but will be in the mesh in that layer. The way the STF is defined now it only picks transactions from the layer-1 it is running on and not any other transactions. So in this case, it can't be executed by the STF in the future. So it can't become effective a layer later unless we completely change the way the STF is defined to pick transactions that were not syntactically valid and are on the mesh as the input for an STF. This is why we reviewed the algo for the STF which clearly states that the input is only txs in that layer. If that needs to be changed then it is a major change and I'm not sure what the implications are.

So the tl'dr - it can't become effective in a future layer with the current definition of STF and if someone wants to change this definition he needs to come up with an alternative algo for the STF in terms of the input...

Regarding your second comment - this is not true - all nodes that run the same protocol can not reject a transaction which is in a verified layer on the mesh. Once a transaction is in the mesh and there's consensus about it being on the mesh by honest nodes then they must execute it according to the STF algo which is all nodes must follow unless they want to fork from the state of all other honest nodes. So, TransactionState is canonical and must always be the same for all honest nodes. They are not free not to change the STF algo at will - otherwise one can never have a functional cryptocurrency. The global state computation and stf algo is what define the currency even if we don't have a way to check the consensus on it.

Regarding the last point - yes - we should add to the global state api the canonical list of txs input for each stf and a tx receipt should include the index in this list (similar to the index of a tx in a block in eth tx receipts).

@avive
Copy link
Contributor

avive commented Jun 3, 2020

btw - if a node decides to add this transaction to a block it submits in a future layer then the same transaction should go in unless the pre-filtering per-stf rules will exclude it. In that case, it will be in the list of input txs for the stf of that layer and it will be processed and produce a receipt if it is valid to go into the stf txs input list... e.g. not bad nonce, etc.. This case where the same tx can appear in different layer is a PITA to deal with as it complicates things. e.g. a transaction needs to have a list of one or more mesh layers it appears in and not only one and the apis need to support this. It would be great if we can avoid this but I'm not sure it is possible - heard different opinions about it from diff team members...

@lrettig
Copy link
Member Author

lrettig commented Jun 3, 2020

If a transaction has the wrong nonce it will should be filtered out from the list of txs for execution by the stf... In that case, it won't be executed but will be in the mesh in that layer. The way the STF is defined now it only picks transactions from the layer-1 it is running on and not any other transactions.

Agree

The way the STF is defined now it only picks transactions from the layer it is running on and not any other transactions. So in this case, it can't be executed by the STF in the future. So it can't become effective a layer later unless we completely change the way the STF is defined

Disagree: a miner may decide to include the same tx in a block in a later layer - this has nothing to do with the STF

it's worth pointing out that TransactionState is not canonical because one node might reject a tx that another node admits into its txpool/puts into a block

Regarding your second comment - this is not true - all nodes that run the same protocol can not reject a transaction which is in a verified layer on the mesh. Once a transaction is in the mesh and there's consensus about it being on the mesh by honest nodes then they must execute it according to the STF algo which is all nodes must follow unless they want to fork from the state of all other honest nodes.

Agree

So, TransactionState is canonical and must always be the same for all honest nodes

Disagree: the decision whether and on what basis to admit a transaction to the txpool, or to include it in a block, is not under protocol and different nodes are free to implement different policies around it. You're right that once a tx is in the mesh and confirmed by consensus it becomes canonical, but up to this point transaction state is not canonical and different nodes may have different opinions:

// NOT CANONICAL:
        UNDEFINED = 0; // default state
        REJECTED = 1; // rejected pre STF processing due to, e.g., bad data
        INSUFFICIENT_FUNDS = 2; // rejected pre STF processing by funds check
        CONFLICTING = 3; // rejected pre STF due to conflicting counter

// SHOULD BE CANONICAL AMONG ALL HONEST NODES WHO SAW THE BLOCK:
        PENDING = 4; // included in a block on the mesh. Pending processing by STF

// CANONICAL:
        PROCESSED = 5; // processed by the STF. Processing results are in the tx receipt

This case where the same tx can appear in different layer is a PITA to deal with as it complicates things. e.g. a transaction needs to have a list of one or more mesh layers it appears in and not only one and the apis need to support this. It would be great if we can avoid this but I'm not sure it is possible

Frankly I don't think we can make any decisions here regarding API or data model until we have a decision on this question!

@lrettig lrettig changed the title Refactor TransactionReceipt/TransactionResult Refactor Transaction Receipt/Result/State Jun 4, 2020
@tal-m
Copy link

tal-m commented Jun 4, 2020

Hopefully the following will help answer your questions:

Conceptually, I think of Spacemesh as having two layers: the consensus layer and the VM layer. The consensus layer is completely VM agnostic (up to syntactic considerations, see Syntactic Validity of Transactions).

Consensus layer

The output of this layer is the transaction ledger: an ordered list of transactions. It has the following properties:

  • all honest nodes agree on the exact list of transactions (both contents and order)
  • every transaction appears exactly once.

The consensus layer is responsible for ordering and deduplicating transactions. Blocks and layers are implementation artifacts of the consensus layer. They do have some effect on the output---for example, our consensus algorithm can only dedup and order transactions after agreeing on an entire layer, so the output granularity is one layer at a time. However, the VM layer doesn't really have to care about this.

Note that if a transaction appears in multiple blocks in a layer, the consensus algorithm will output it only once. Similarly, for a transaction that appears in two different layers, the consensus output will contain only the first occurrence. In particular, this means that once a transaction is in the mesh, the exact same transaction can't be "resubmitted" later --- a new transaction has to be created.

Rollbacks (reorgs)

In case of self-healing, it's possible that some portion of history is reversed). If this happens, the consensus layer updates the VM layer that there was a reset to some specific transaction in the past, and then outputs the ordered list of transactions starting from the reset point.

VM Layer

The VM layer is responsible for maintaining global state (account balances, app code, app variables, etc.) and executing the state transition function (STF). The STF accepts a single transaction, and executes it, potentially modifying the global state.

Transaction execution

The VM layer calls the STF on every transaction in the order it receives them from the consensus layer, modifying the global state as it does so.
The VM executes every transaction that it receives from the consensus layer. Some of these may be "totally ineffective" (have no effect on the global state) and some may change the global state, but still be ineffective as far as the user is concerned (e.g., if a TX runs out of gas, it does change the global state, because the gas fee is still deducted from some account, but it probably didn't do what the user wanted.). Let's call this kind of transaction partially effective.

Rollbacks

If the VM layer is notified of a rollback, it is responsible for rolling back the global state to whatever it was at the reset point. It can then execute transactions normally.

Transaction results

Given the above, we can now classify the state of a transaction:

  • Not yet in mempool
  • Rejected from mempool for "heuristic" reasons
  • In mempool
  • Accepted to mesh. In this case, we have a "VM receipt"
    • totally ineffective (no change in global state). Reasons can be
      • bad nonce
      • insufficient funds
      • conflicting counter
      • etc.
    • effective or partially ineffective. In this case the receipt contains the changes in global state. In addition, if it's partially ineffective, it can have a reason (e.g., gas ran out)

Syntactic Validity of Transactions

Theoretically, we can make the consensus layer totally VM-agnostic, which means it would allow completely invalid transactions in the mesh. However, to prevent DoS attacks, we do want to filter some things at the consensus layer already. So transactions that are syntactically-invalid (i.e., they would be totally ineffective regardless of the global state when they are executed by the VM) can be filtered out at the consensus layer. This means that we would consider blocks containing such transactions as syntactically invalid.

Heuristic rejection of transactions

We may also want to prevent transactions that do depend on state from making into the mempool (such as ones that have conflicting counters). In this case, honest nodes will ignore the transactions, but they could still be included into the consensus layer by malicious nodes (or even by honest nodes, in case our assumptions are not satisfied --- for example during a network split). This kind of transaction can be resubmitted later, since it is filtered before it makes it into the consensus layer, so doesn't violate the uniqueness property of the transaction ledger.

@avive
Copy link
Contributor

avive commented Jun 5, 2020

I agree with most of what you wrote in your comment and I think it is quite close to our proposed STF and tx results / status api but there are few caveats and things to address and be a bit more precise about:

  • all honest nodes agree on the exact list of transactions (both contents and order)
    every transaction appears exactly once.

I don't think you can exclude the time element (layer) from this definition. The agreement on the content is always in the context of a layer number. All honest nodes agree on the contents up to a specific layer. e.g. the verified layer.

  • The consensus layer is responsible for ordering and deduplicating transactions. Blocks and layers are implementation artifacts of the consensus layer. They do have some effect on the output---for example, our consensus algorithm can only dedup and order transactions after agreeing on an entire layer, so the output granularity is one layer at a time. However, the VM layer doesn't really have to care about this.

I find it a bit misleading to call a layer an implementation artifact where it is actually the time ticks that the consensus works in and vital to consensus as my point above explains. So, it is not enough that the consensus creates a prefix list of ordered transactions. Because the consensus verifies tx in chunks (all the tx in a layer) once a layer is verified, it is useful to also describe the input batch to the STF as input list for an input layer. It is not true that the VM layer doesn't care about layers. The way we specify the STF it must care about layer as it always work with input from layer n-1 and outputs state for layer n. Without such knowledge, the STF will not be able to create receipts that describe the deterministic time of execution of the tx which is critical piece of data in a crypto-currency. (e.g. block # for processed txs in bitcoin and ethereum). So it is undesirable from practical considerations to define the vm layer as layer agnostic. The layer is the deterministic tick of the decentralized computer that determines computation time. Without for example layer number in tx receipts there's no way to know when a tx was actually processed as the index in the global ordered lists of tx doesn't give us any notion of time.

It is not enough to say that the output granularly is one layer at the time. More exact definition of the STF is required - can you please comment on what is defined in the data and api smip where we attempt to properly define the STF? It will be useful if you can say what is wrong / bad there then needs to be changed.

I find the other comments in the consensus layer great and we should add them to spec. To be a bit more specific, I think that what you argue means that a specific transaction can only appear in 1 stf input-list in a layer but not in multiple ones.

Rollbacks

This is another reason what it is very useful to define the STF ticks as layers and have the STF define as getting a layer n number as input. In case of rollback, the consensus layer just instructs the vm layers to execute the STF from layer n.

Transaction results

Can you please relate to the proposed states? I think that we pretty much agree on this but you are talking about adding another state for inclusion in the mem-pool.

enum TransactionStateType {
        UNDEFINED = 0; // default state
        REJECTED = 1; // rejected pre STF processing due to, e.g., bad data
        INSUFFICIENT_FUNDS = 2; // rejected pre STF processing by funds check
        CONFLICTING = 3; // rejected pre STF due to conflicting counter
        PENDING = 4; // included in a block on the mesh. Pending processing by STF
        PROCESSED = 5; // processed by the STF. Processing results are in the tx receipt
    }

it is also not entirely accurate that we'll have a receipt if a tx was accepted to the mesh because the STF may have not executed for that layer yet. Transactions which are on the mesh will have a receipt later - this is why we propose the PENDING= 4 and PROCESSED = 5 states which you don't mention in your summary. So considering your feedback and this point, I think the possible states are:

enum TransactionStateType {
        UNDEFINED = 0; // default state
        REJECTED = 1; // rejected pre STF processing due to, e.g., bad data
        MEM_POOL = 2; // accepted to the mem-pool and broadcasted by node via gossip
        INSUFFICIENT_FUNDS = 3; // rejected pre STF processing by funds check
        CONFLICTING = 4; // rejected pre STF due to conflicting counter
        PENDING = 5; // included in a block on the mesh. Pending processing by STF
        PROCESSED = 6; // processed by the STF. Processing results are in the tx receipt
    }

Regarding tx receipt data you mention, can you please review to what we proposed in the api:

 enum TransactionResult { // the results of STF transaction processing
        UNDEFINED = 0;
        EXECUTED = 1; // executed w/o error by the STF
        BAD_COUNTER = 2; // unexpected transaction counter
        RUNTIME_EXCEPTION = 3; // app code exception
        INSUFFICIENT_GAS = 4; // out of gas
        INSUFFICIENT_FUNDS = 5; // failed due to sender's insufficient funds
    }

I think it captures all possible stf execution results and it is pretty similar to what you describe but more formal. One of these states go into a receipt.

BTW, the way we define global-state in smip, each and every transaction that is in the input list of a layer to the STF will affect global state when the STF executes, because it will generate a receipt (side-effect) even if it doesn't change the state of the accounts/apps db. This is why I wanted to define global-state as meta-mash which includes the proper more narrow global-state (state of account and app db) as well as execution side-effects (receipts and app events), but we decided to call all the data and the side-effects of execution global state. So I think that the notion of not-applied, partially-applied, and applied are useful only in the context of the accounts and app db but not in the context of the way global state is currently defined...

@avive
Copy link
Contributor

avive commented Jun 5, 2020

Syntactic Validity of Transactions
Theoretically, we can make the consensus layer totally VM-agnostic, which means it would allow completely invalid transactions in the mesh. However, to prevent DoS attacks, we do want to filter some things at the consensus layer already. So transactions that are syntactically-invalid (i.e., they would be totally ineffective regardless of the global state when they are executed by the VM) can be filtered out at the consensus layer. This means that we would consider blocks containing such transactions as syntactically invalid.

Didn't we review and approved Noam's design for pre-stf filleting for 0.3.5 in the product summit in 12/2019? I believe pre-filtering is currently implemented in the testnet codebase and has been so for a long time. If any pre-filtering as implemented for 0.3.5 then we want to spec it now.

@lrettig
Copy link
Member Author

lrettig commented Jun 5, 2020

Rollbacks (reorgs)
In case of self-healing, it's possible that some portion of history is reversed. If this happens, the consensus layer updates the VM layer that there was a reset to some specific transaction in the past, and then outputs the ordered list of transactions starting from the reset point.

Agree with Aviv here that this reset should only be defined for an entire layer, not for a specific transaction. The behavior of the STF on an individual transaction, or a portion of a layer, is not well defined due to the way we handle fees and rewards, pooled and on a per-layer basis. In other words, the STF cannot be "reset" to an intermediate point in a layer.

Accepted to mesh. In this case, we have a "VM receipt"

As Aviv pointed out, there should be a "pending" state - added to a block but the block hasn't been approved/added to the mesh yet.

transactions that are syntactically-invalid (i.e., they would be totally ineffective regardless of the global state when they are executed by the VM) can be filtered out at the consensus layer

Is this possible in practice? As discussed on the design review call, it's not possible for simple coin transfers as they're designed now, since every signature matches some account's private key - and we cannot know ahead of time whether that account won't have some balance when the STF attempts to execute the tx.

As for smart contract/"app" transactions, we need @YaronWittenstein's input on this. This question probably reduces to some version of, "Are there bytecode patterns that we can definitively call invalid pre-execution?" You could attempt to syntactically parse Wasm bytecode and make sure it's valid, but this doesn't preclude the possibility of using other bytecode formats/targeting other VMs in the future.

This is why I wanted to define global-state as meta-mash which includes the proper more narrow global-state (state of account and app db) as well as execution side-effects (receipts and app events), but we decided to call all the data and the side-effects of execution global state

Thinking about this more, I do see some value in a distinction here between a more narrowly-defined "global state" (basically, state of all accounts) and a broader "meta-mesh" (for lack of a better term) that includes tx receipts, app events, etc.

@lrettig
Copy link
Member Author

lrettig commented Jun 5, 2020

I think we're all more or less on the same page about transaction results and receipts.

Regarding TransactionState, however, we unfortunately have a confusing mix of concerns the way this is set up right now:

// NOT CANONICAL:
    UNDEFINED = 0; // default state
    MEMPOOL = 1; // admitted to mempool
    REJECTED = 2; // rejected from mempool on heuristic grounds (could be further broken out into different reasons for rejection)

// CANONICAL AMONG HONEST NODES THAT SAW THE BLOCK:
    PENDING = 3; // included in a block on the mesh. Pending approval and STF execution.

// CANONICAL:
    PROCESSED = 4; // processed by the STF. Processing results are in the tx receipt

I think it makes sense to have a clean separation of the consensus and VM layers that Tal describes. The PROCESSED state could potentially be dropped, on the grounds that the existence or absence of a receipt for this tx contains this information, which gets us most of the way there.

  • Node service: can tell us that it admitted a tx to the mempool, or rejected it (and on what grounds), and when it was mined into a block that was submitted to the network
  • Mesh service: can tell us when a tx was mined into a block, and when that block has been approved/added to the mesh
  • Global state service: can tell us when a tx, and the block/layer containing it, was run through the VM, and the outcome of that execution (receipt)

@tal-m
Copy link

tal-m commented Jun 6, 2020

This makes sense.

Regarding the precise stages a tx passes once it gets into the mempool:

  1. It is in the mempool, but not yet in any block
  2. It is in a syntactically-valid block, but not yet in any block that is contextually valid with high confidence.
  3. It is in a contextually-valid block with high confidence, but cannot be output by the consensus layer yet because other blocks in the same or earlier layers do not yet have high confidence (this can be caused, for example, if a previous layer's hare protocol terminates after the next layer's hare protocol terminates).
  4. It was output by the consensus layer.
  5. It is executed by the STF

I'm guessing "mined into a block" means (2) and "added to the mesh" means (3)?

Note that while it's true that technically some time passes between (4) and (5), in our current implementation (in which every node runs the STF) I expect this to be a matter of milliseconds at most, so it won't really matter from a user perspective. (In a future version, if/when we support a separate role for state validation, the separation will be more meaningful.)

Regarding the layer number, I have no objection to including it as input to the STF. (indeed, as you say, in our protocol we will always process entire layers at a time, and rollbacks will always happen at layer granularity). However, the STF is well defined for executing one transaction at a time.

I would think the STF's API would contain (very loosely) something like the following:

ExecuteTX(tx): executes the transaction, updating the state returns TX receipt 
Commit(): commits the current state to allow rollbacks to this point. Returns rollback handle
Rollback(handle): rollback the state to the given commit point.

The node will call ExecuteTX(tx) on every transaction output from the consensus layer, and Commit() at the end of every layer.

Of course, the "rollback handles" can just be the layer numbers, and if our code can be significantly simplified or optimized by allowing an ExecuteTX(tx[]) which receives an array, that's fine too --- but I'm not totally sure that's the case.

@avive
Copy link
Contributor

avive commented Jun 6, 2020

I think it makes sense to have a clean separation of the consensus and VM layers that Tal describes. The PROCESSED state could potentially be dropped, on the grounds that the existence or absence of a receipt for this tx contains this information, which gets us most of the way there.

Ok makes sense.

Node service: can tell us that it admitted a tx to the mempool, or rejected it (and on what grounds), and when it was mined into a block that was submitted to the network
Mesh service: can tell us when a tx was mined into a block, and when that block has been approved/added to the mesh.

We already defined NodeService.SubmitTransaction(Transaction) returns (TransactionState); so the node does return that. But I think this is immediate and represent the tx pre-filtering results. I think that the mesh service should be the one to return state of tx that didn't fail pre-filtering when submitted and not the node. It is the concern of the mesh service to report when a tx was included in the mesh.

Global state service: can tell us when a tx, and the block/layer containing it, was run through the VM, and the outcome of that execution (receipt)

Yes, I think we all agree to this.

I still believe we should go over with tal over the spec of the STF (including layer being an input) in the smip to confirm that the design is good and finally agree on definitions and the stf.

@lrettig
Copy link
Member Author

lrettig commented Jun 6, 2020

However, the STF is well defined for executing one transaction at a time.
I would think the STF's API would contain (very loosely) something like the following:

ExecuteTX(tx): executes the transaction, updating the state returns TX receipt
Commit(): commits the current state to allow rollbacks to this point. Returns rollback handle
Rollback(handle): rollback the state to the given commit point.

The node will call ExecuteTX(tx) on every transaction output from the consensus layer, and Commit() at the end of every layer.

The problem with this definition of the STF is that it does not account for fees and rewards. This is why I keep saying the STF is not well-defined for executing one transaction at a time. In the protocol as I understand it, you cannot just execute one transaction in isolation, because fees from all of the transactions in a layer are pooled and divided up as part of the processing of a layer. (This is how it was explained to me, and how it was documented. If this is not the case, let's talk about it.)

@avive
Copy link
Contributor

avive commented Jun 6, 2020

It is all pretty well defined here: spacemeshos/SMIPS#13 -> The STF.

e.g

In addition to processing all the transactions from a list, the STF should compute the rewards and the transaction fees for a past layer and update the state of the accounts that should receive rewards and transaction fees for that past layer. These rewards and fees modify the global state (account balances).

To summarize the way the STF is currently defined:

  1. The consensus layer outputs a list of sorted de-duplicated tx for processing - tx input list per layer.
  2. The STF executes these TX in the order in the list, updating global state and producing receipt for each tx. The STF input is this list and the layer #, so it can put that layer # in the produced receipts. If a tx fails, the accounts and app state need to be rolled back - this is obvious but gas may be paid in some cases.
  3. When tx processing done, a bunch of other things are computed - e.g. rewards and tx fees are distributed between the miners that included the txs processed by the stf in a block they published in the layer - see the smip.
  4. No other code can modify global state - by definition the STF executes all global state modifying code.

@lrettig
Copy link
Member Author

lrettig commented Jun 6, 2020

I agree with one caveat:

The STF executes these TX in the order in the list, updating global state and producing receipt for each tx

As Tal pointed out, global state is not really updated after each individual tx is executed. That doesn't really happen until commit is called at the end of the layer.

@avive
Copy link
Contributor

avive commented Jun 6, 2020

It depends what you call the 'global state'. As we said above it includes the accounts and apps state db + all the side-effect of tx execution (receipts and events). So, the global state is updated as each tx is executed and ofcourse if there's an exception that it is rolled back. But it has to be understood that transaction n gets as input the global state after transaction n-1 has been executed as n-1 may have changed an account or app state that is input or ouput of tx n. There is no other way to execute these transactions. So commit, does need to happen after each executed tx and roll-back means not updating app or account state in the db in case of tx exception. So the global state is actually updated after each tx is executed but as Anton mentioned this can be an in-memory representation that is committed to storage only once when the last tx was processed.

@avive
Copy link
Contributor

avive commented Jun 6, 2020

It is impossible to execute correctly n txs that have shared input and some shared output using an initial state s (at least avoiding double-spends in smart contract txs is impossible). They must be executed in steps otherwise execution can't be correct. e.g. tx n-1 deplated an account balance used as input to tx n... tx n should fail as its input is the global state after tx n-1 was executed.

@avive
Copy link
Contributor

avive commented Jun 6, 2020

Maybe it will all be better if we can review the definition in the smip and argue if any specific requirement there needs to be changed and not discuss this generally...

@tal-m
Copy link

tal-m commented Jun 6, 2020

@lrettig Good catch --- You're right that the fees and rewards do need more information than just the transactions. However, this doesn't mean that transactions aren't executed one at a time --- it just means that the STF needs to get some more information from the consensus layer (in particular, it needs to know the set of blocks in each layer, and the assignment of transactions to blocks, including repeated transactions).

The most general way to do this is to have the consensus layer give "auxiliary" information to the STF. Basically, a call such as:

NewAuxInfo(auxinfo): doesn't return anything

This would be called before processing the first transaction for a layer (with "auxinfo" being the set of blocks in the layer). We might also include in AuxInfo information about published ATXs (e.g., if publishing an ATX gives a reward even without generating a block).

Note that in our case the rewards and fees only apply to future layers, so the STF doesn't need the set of blocks for layer i in order execute the transactions in layer i.

@lrettig
Copy link
Member Author

lrettig commented Jun 6, 2020

Note that in our case the rewards and fees only apply to future layers, so the STF doesn't need the set of blocks for layer i in order execute the transactions in layer i.

Makes sense - as long as we don't allow the VM to supply a running smart contract with any additional, contextual info based on the block that a tx lives in, e.g., a blockhash (as in ethereum), or the layer (e.g., layer hash, number of blocks in a layer, etc.).

@tal-m
Copy link

tal-m commented Jun 6, 2020

Right. Although as I wrote it, the STF actually does have this information. Is there a good use-case for letting smart contracts have this additional info?

@lrettig
Copy link
Member Author

lrettig commented Jun 7, 2020

Is there a good use-case for letting smart contracts have this additional info?

I think we all agree that smart contracts need access to global state: reading and writing storage, reading balances, reading code, etc.

For mesh data, it's not so immediately obvious how it could be useful. Off the top of my head, a lot of eth smart contracts use block hash as a first order source of randomness. There are also Ewasm host functions for block number, difficulty, block gas limit, etc. - allowing Spacemesh smart contracts to read, say, number of blocks per layer, or number of transactions per block would allow some degree of introspection along these same lines.

CC @YaronWittenstein

@avive
Copy link
Contributor

avive commented Jun 7, 2020

It is super important for smart contract methods implementation to have access to layer number as part of the context as a clock tick and in addition the genesis time-stamp and the network's constant layer duration, so they can do time-based computations. Many use cases that involve time-lock and changes of data over time from creation - e.g. NFT evolution. I bet that every non trivial eth game and NFT is using block number at least once. It is also critical for DEFI apps such as token auctions. So layer # is the abs minimum we know we need in the context. Additional mesh info is NTH but not MH.

@avive
Copy link
Contributor

avive commented Jun 7, 2020

@lrettig Good catch --- You're right that the fees and rewards do need more information than just the transactions. However, this doesn't mean that transactions aren't executed one at a time --- it just means that the STF needs to get some more information from the consensus layer (in particular, it needs to know the set of blocks in each layer, and the assignment of transactions to blocks, including repeated transactions).

The most general way to do this is to have the consensus layer give "auxiliary" information to the STF. Basically, a call such as:

NewAuxInfo(auxinfo): doesn't return anything

This would be called before processing the first transaction for a layer (with "auxinfo" being the set of blocks in the layer). We might also include in AuxInfo information about published ATXs (e.g., if publishing an ATX gives a reward even without generating a block).

It doesn't really matter for the high-level stf specs - what matters is what the overall set of inputs that the STF needs and it includes the layer blocks - not how they are provided.

Note that in our case the rewards and fees only apply to future layers, so the STF doesn't need the set of blocks for layer i in order execute the transactions in layer i.

Here again Tal - can you please review the specific proposed definition of the STF in the SMIP? It states clearly how rewards are computed and when. The current proposal is for the STF that works on txs from layer n to also compute the rewards for smeshers for layer n. It makes sense because it computes the total TX fees charged in the TX in layer n when it processes the transactions and can assign it as a step after tx processing (as proposed - please read again). If we want to change this then we need clear definitions and specs regarding to what layer does the STF of layer n compute the rewards from in case we want it to be different than what is proposed in the smip. Note, that if for some reason we need to change the rewards definition of the STF then it will need as input all the blocks of the layer != n that it needs to compute the rewards for. We must converge and have a good spec that we all feel good about but it feels like information that is currently written there is ignored or general statements are made that contradict it without an alternative proposal. So, if we need to change something from the current proposal for the stf. e.g. how rewards and tx fee are computed then we need alternative concrete proposals to consider beyond high-level statements. Can we please have that?

Here is the current definition of rewards and fees computation of the stf from the smip:

In addition to processing all the transactions from a list, the STF should compute the rewards and the transaction fees for layer n and update the state of the accounts that should receive rewards and transaction fees as part of the global state computation at layer n. These rewards and fees modify the global state (account balances). Rewards and tx fee computation should happen only after all the txs have been executed so the correct tx fee amounts to be distributed between the layer's smeshers are known.

@lrettig
Copy link
Member Author

lrettig commented Jun 7, 2020

Can we take the conversation thread about STF definition to the SMIP thread? spacemeshos/SMIPS#13

@avive
Copy link
Contributor

avive commented Jun 7, 2020

Sure and it will be helpful if any new comments can propose changes to the current stf high-level definition in the smip so we can converge.

@avive
Copy link
Contributor

avive commented Jun 24, 2020

Is this issue still relevant? What did we agree that needed to be changed if anything?

@lrettig
Copy link
Member Author

lrettig commented Jun 24, 2020

Reviewing where we're at right now in the API, we have TransactionState (in the TransactionService) which tracks the tx "journey" across concerns but knows nothing about the STF:

// TransactionState is the "journey" of a tx from mempool to block inclusion to
// mesh to STF processing. To know whether or not the tx actually succeeded,
// and its side effects, check the Receipt in the GlobalStateService.
message TransactionState {
TransactionId id = 1;
enum TransactionState {
TRANSACTION_STATE_UNSPECIFIED = 0; // default state
TRANSACTION_STATE_REJECTED = 1; // rejected from mempool due to, e.g., invalid syntax
TRANSACTION_STATE_INSUFFICIENT_FUNDS = 2; // rejected from mempool by funds check
TRANSACTION_STATE_CONFLICTING = 3; // rejected from mempool due to conflicting counter
TRANSACTION_STATE_MEMPOOL = 4; // in mempool but not on the mesh yet
TRANSACTION_STATE_MESH = 5; // submitted to the mesh
TRANSACTION_STATE_PROCESSED = 6; // processed by STF; check Receipt for success or failure
}
TransactionState state = 2;
}

Separately, we have the TransactionReceipt (in the GlobalStateService) that contains the results of the STF processing

message TransactionReceipt {
TransactionId id = 1; // the source transaction
enum TransactionResult { // the results of STF transaction processing
TRANSACTION_RESULT_UNSPECIFIED = 0;
TRANSACTION_RESULT_EXECUTED = 1; // executed w/o error by the STF
TRANSACTION_RESULT_BAD_COUNTER = 2; // unexpected transaction counter
TRANSACTION_RESULT_RUNTIME_EXCEPTION = 3; // app code exception
TRANSACTION_RESULT_INSUFFICIENT_GAS = 4; // out of gas
TRANSACTION_RESULT_INSUFFICIENT_FUNDS = 5; // failed due to sender's insufficient funds
}
TransactionResult result = 2; // tx processing result
uint64 gas_used = 3; // gas units used by the transaction
Amount fee = 4; // transaction fee charged for the transaction (in smidge, gas_price * gas_used)
uint64 layer_number = 5; // the layer in which the STF processed this transaction
uint32 index = 6; // the index of the tx in the ordered list of txs to be executed by stf in the layer.
AccountId app_address = 7; // deployed app address or code template address
}

To answer the initial questions I posed, above:

  1. was the transaction "effective" or not (Iddo's term, i.e., syntactically valid)

All transactions in spacemesh are syntactically valid. Any transaction with a TransactionResult >= TRANSACTION_RESULT_EXECUTED was "effective" in the sense that it caused gas to be spent and/or value to be transferred.

  1. did the transaction succeed or fail?

Yes iff TransactionResult == TRANSACTION_RESULT_EXECUTED

  1. was a fee charged for it or not?

See TransactionResult.fee

Does this all sound correct?

@lrettig
Copy link
Member Author

lrettig commented Jul 24, 2020

Does this all sound correct?

I'm taking no response to indicate assent. Closing this as no further action is pending here. Please reopen if I've missed something.

@YaronWittenstein
Copy link

Since the job of the API will be only to deliver the binary transaction - I think the whole conversation regarding the scheme isn't relevant. The decoding of a Receipt into a JSON will happen inside the smapp / explorer using the svm_codec.wasm

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

No branches or pull requests

4 participants