gnolang / gno

Gno: An interpreted, stack-based Go virtual machine to build succinct and composable apps + Gno.land: a blockchain for timeless code and fair open-source
https://gno.land/
Other
849 stars 345 forks source link

Transaction Chaining Method #1508

Closed notJoon closed 5 months ago

notJoon commented 6 months ago

Description

Transaction chaining is an action that executes the latter transaction based on the result of the previous transaction.

For example with GnoSwap, we have a feature called “One-click Staking”, which composed of two transactions. The first transaction is about adding a pool, then based on the result of the first transaction, the second transaction of staking will be followed. This is similar to method chaining or the pipe operation(|) in UNIX.

Adding the transaction chaining feature would allow dApps to provide user-friendly and convinient features, like the One-click Staking mentioned above. I think it's a useful feature in situations where chain actions are needed.

To implement enhanced transaction chaining, the following conditions must be satisfied:

  1. Chaining Order Guarantee: The execution order of transactions must be clearly defined and adhered to.
  2. Predictable Return Values: Each function in the chain must have an expectable return value to ensure consistency in transaction processing.
  3. State Reversion on Failure: If any transaction in the chain fails, all changes made during the chain execution must be reverted to maintain data integrity.

Below is the pseudocode for implementing chain transactions:

type TxResult struct {
    Ok   interface{}
    Err  error
}

type TxFunc func(input interface{}) TxResult
type TxChain struct {
    txs  []TxFunc
}

func NewChain() *TxChain {
    return &TxChain{}
}

// Add adds a new transaction functions to the chain
func (t *TxChain) Add(fn ...TxFunc) *TxChain {
    t.txs = append(t.txs, fn...)
    return t
}

// Execute executes all transactions sequentially
func (t *TxChain) Execute(input interface{}) (interface{}, error) {
    var result      interface{} = input
    var executedTxs []TxFunc // Keep track of successfully executed transactions

    for _, tx := range t.txs {
        if res := tx(result); res.Err != nil {
            t.revertState(executedTxs, input)
            return nil, res.Err
        } else {
            result = res.Ok
            executedTxs = append(executedTxs, tx)
        }
    }
    return result, nil
}

// revertState reverts the state changes made by the executed transactions
func (t *TxChain) revertState(executedTxs []TxFunc, initialState interface{}) {
    /* Revert all states */ 
}

As an aside, if introducing new syntax, adopting Elixir's pipe operator might not be a bad idea. But it’s cumbersome to implement, so may not feasible.

tx1 |> tx2 |> ... |> txn

A way of implementing this feature has not been decided yet (I believe there are various options for this that I’m not aware of), so I’m open to any ideas/opinions from others on this. If you have any good suggestions, please leave them in the comments below. Thank you.

Additional notes

In the Cosmos SDK, I've seen that messages are processed atomically, and I think extending this functionality would allow for pipelining.


cc: @dongwon8247 @r3v4s @mconcat

moul commented 5 months ago

I recommend utilizing the current transaction batch/composition logics, which appear to be the "gno way" approach. This can be accomplished by creating batch-oriented contracts accessible to all through maketx call or by directly using maketx run.

You can find the pull request for the maketx run feature here: https://github.com/gnolang/gno/pull/1001. Additionally, there is an example where 10 calls to the tests realm are simulated in a single for loop here: https://gist.github.com/moul/ccf1e2aff64e7a1f0c5ca5e2d98d7e9a. Since it's Gno, you have the flexibility to implement any desired logic, efficiently.

I recommend delaying the discussion of creating a pseudolanguage for advanced transaction parsing within the mempool. Let's first identify the limitations of the current system. After that, we can focus on improving and expanding the mempool. It's important to keep the mempool simple, fast, and efficient.

Here are some suggestions to proceed:

thehowl commented 5 months ago

Does manfred's answer satisfy your question/problems? Are there action items on this issue? @notJoon

notJoon commented 5 months ago

@thehowl Oh, yes I think it's best to make it a function for now. no follow up action-item yet. should I close this issue or leave it open?

thehowl commented 5 months ago

Let's close it :) can always open a new one should you have a usecase where MsgRun does not satisfy your needs.