Proposal: Create new transactions with KT1 accounts

A way to send new transactions in KT contracts

tldr; Currently you can only send new transactions with implicit accounts. With this proposal you’ll be able to do it in KT accounts and solve some current Tezos limitations.

The following idea is early, it came to me few hours ago.

Edit 2020-09-14 15:16: I modified the proposal to make it technically more valid

:: 'p : mutez : contract 'p : mutez %gas : 'S -> operation : 'S NEW_TRANSACTION_TRANSFER_TOKENS

The instruction NEW_TRANSACTION_TRANSFER_TOKENS is a lot like TRANSFER_TOKENS except the transfer is done in a completely new transaction. As a result, the calling contract gives the fee for the new transaction.

This instruction and the corresponding operation can never fail. If the new transaction failed for any reason the NEW_TRANSACTION_TRANSFER_TOKENS instruction is still considered passed.

The TRANSFER_TOKENS operation is executed at the end of the whole current transaction only if the current transaction passed.

The number of NEW_TRANSACTION_TRANSFER_TOKENS allowed is only limited by the current transaction gas limit.

Scheduling

Scheduling is a hot topic on TezosAgora.

I don’t have any definitive answer to that at the moment. The bakers that accept to include a transaction with a NEW_TRANSACTION_TRANSFER_TOKENS instruction could guarantee that the callback will be included in at most n blocks. In exchange for this guarantee, the entry_points of the callback and the resulting operations should be guaranteed small.

Apart from this consideration, I don’t know if the new transactions could or not be executed in parallel. I would say yes but I feel that the answer is no (because they should act more like TRANSFER_TOKENS). The new transactions shall be able to run in different blocks (see below why).

Feedback callback

As the NEW_TRANSACTION_TRANSFER_TOKENS and NEW_TRANSACTION_TRANSFER_TOKENS operation always pass you can’t know if the induced transaction have been able to transfer the tokens.

This information is needed is almost every use cases, this is why we should find a solution.

It could be a callback parameter. The callback would be automatically called at the end of the induced transaction with a boolean parameter equals to true if the induced transaction passes, false otherwise.

This brings another question: what happen if the callback transaction is never included in a block?

I don’t have any definitive answer to that at the moment. The bakers that accept to include a transaction with a NEW_TRANSACTION_TRANSFER_TOKENS instruction could garantee that the callback will be included in at most n blocks. In exchange for this garantee, the entry_points for the callback and the resulting operations should be garanteed small.

About tickets

The tickets (see MR) should be kept along the transaction chain.

Question that needed answer: Is it technically possible taking into account the fact that the transactions can occur in different blocks?

About reentrancy (and tickets)

I thought that it could be interesting to add something like a time to live feature.
It would be a nat parameter.

A new condition and a reason to fail would be added to NEW_TRANSACTION_TRANSFER_TOKENS. The instruction fails if the number of times you executed it with the same parameters and contract sender (including the entry_points) in this chains is equal to the nat parameter.

This would avoid the possibility of infinite loop until the balance of the contract is empty.

Ex:

  • Contract A send a NEW_TRANSACTION_TRANSFER_TOKENS to contract B with a %TTL parameter equal to 1
  • As a result B do a TRANSFER_TOKEN or a NEW_TRANSACTION_TRANSFER_TOKENS to contract A (or to a chains that finish by doing a TRANSFER_TOKEN/NEW_TRANSACTION_TRANSFER_TOKENS to contract A)
  • Contract A want to send a NEW_TRANSACTION_TRANSFER_TOKENS to contract B but the number of time this already happened is 1. 1 equals the %TTL so the instruction fail and the current transaction fail.

This feature could nearly be implemented with tickets.

IMO a new parameter would simplify contract developers and force them to think about this infinite loop question. The drawback is that it adds another data to attach to the whole chains of transactions.

About total gas limit

Should there be a limit of the total number of gas spent on the whole chains of transactions? I don’t find any reason but there could be.


This proposal answers actual problems

Refunding to a list of contracts problem

We can read in the Tezos Developer Documentation: Michelson Anti-Patterns one of the current limit of Tezos that can disappear with this new instruction.

One common pattern in contracts is to refund a group of people’s funds at once. This is problematic if you accepted arbitrary contracts as a malicious user can do cause various issues for you.

Possible issues:

  • One contract swallows all the gas through a series of callbacks
  • One contract writes transactions until the block is full
  • Reentrancy bugs. Michelson intentionally makes these difficult to write, but it is still possible if you try.
  • A contract calls the FAIL instruction, stopping all computation.

Resolving with NEW_TRANSACTION_TRANSFER_TOKENS

The solution is straightforward: iterates through the list of addresses to refund and use the NEW_TRANSACTION_TRANSFER_TOKENS instruction to refund them. This way you can separate your computation from that of the recipient in the destiny of your transaction.

This solution prevents the “gas spending”, “block full” and "Fail computation" problems.

Question that needed answer: What are the remaining reentrancy bugs?

Notice that you should still permit people to pull their fund individually in case their payment receiver failed.


Make others pay for their computation

One feature I’d like in Tezos would be to pay the fee of the computation for someone else or to ask for someone else to pay my fees. It would act as an on chain gaz relay.

This proposal permits that.

In the previous refund example when you use the NEW_TRANSACTION_TRANSFER_TOKENS in your refund contract you don’t need much gas even if the people want to do complex transaction in response.

Why? Because they can use NEW_TRANSACTION_TRANSFER_TOKENS in their own contract to do the complex operations.

You’ll pay the fee for a very simple transaction and let them pay the fee to have their complex computation included in the block.


Increase the interactivity possibilities

In the current situation implicit accounts are the only one able to create new transaction. It means that only humans or off chain codes can prolong the interactivity beyond the gas limit.

With this proposal you can create a new transaction on KT1 accounts and start on a fresh new gas limit counter.


I’m waiting for your comments.

3 Likes

That does not work. The point of gas is not to pay for the cost of computing something in the abstract, that cost is trivial.

The point of gas is that there is a natural limit as to how much computation can happen within a block. Since transactions compete for inclusion in a block, they generally include a fee to incentivize the block producers from prioritizing their transaction. A key aspect that ensures the security of the system is that block producers must be able to determine how much fee they can be paid for including a transaction wihtout having first to simulate the execution of the transaction.

The sound way to have someone else pay for gas is to set up gas relays.

I’ve edited. I hope that’s more correct now.

Apart from this point, does the whole proposal seem technically imaginable and useful to you?

An important invariant in a transaction is that you know that if any part fails, you have the guarantee that there will be no effect on the chain beyond the fee. If I understand correctly, your proposal breaks this invariant which can introduce unexpected problems and makes it harder to reason about the chain.

If some DFS extensions are implemented, try catch clauses can make sense around a call, but I think they would be hazardous in the current BFS model.

Are you referring to the callback? Apart from it there is no new transaction if the mother transaction fails.

If you are talking about the callback, a new transaction is created regardless of the status of the daughter transaction but every operation done by the daughter transaction is reverted in case of failure of the daughter. So there is no break here either.

Rework of my proposition.

:: 'p %parameters : mutez %fees : contract 'p : 'S -> operation : 'S NEW_TRANSACTION
where   contract 'p is the type of an entrypoint of the current contract

The instruction NEW_TRANSACTION pushes a new operation on the stack. This operation add an order that will be executed after the end of the current transaction. The order is not executed if the transaction failed.

The order originates a new transaction which correspond to a TRANSFER_TOKENS of 0 tokens to SELF with the specified %parameters (and the specified entry_point). The %fees parameter permits to pay the fees and storage for the new transaction.

This instruction and the corresponding operation can never fail but they are reverted if the current transaction failed.

The new transaction order is sent to the gossip network exactly like if it were originated by an implicit account. It means that you don’t know when and if the transaction will be included in a block.

Scheduling

The order is called after every other instructions only if the current transaction don’t fail.

Like every originated transaction on the gossip network you don’t know if it will be executed before or after every other competing transactions.

About tickets

The tickets (see MR) should be kept along the transaction chain.

About total gas limit

Should there be a limit of the total number of gas spent on the whole chains of transactions? I don’t find any reason but there could be.

DDOS

Is there a risk of DDOS induced? One transaction can send n new transactions to the network.

On how this proposal answers actual problems

Refunding to a list of contracts problem

We can read in the Tezos Developer Documentation: Michelson Anti-Patterns one of the current limit of Tezos that can disappear with this new instruction.

One common pattern in contracts is to refund a group of people’s funds at once. This is problematic if you accepted arbitrary contracts as a malicious user can do cause various issues for you.

Possible issues:

  • One contract swallows all the gas through a series of callbacks
  • One contract writes transactions until the block is full
  • Reentrancy bugs. Michelson intentionally makes these difficult to write, but it is still possible if you try.
  • A contract calls the FAIL instruction, stopping all computation.

Solution with NEW_TRANSACTION

Create an entry_point to pull the funds individually. In the “refund_everyone” entry_point iterates through the list of receiver and use the NEW_TRANSACTION instruction to trigger the individual refunds in separate transactions.

This way if one of the transfer fails you can still trigger the others. The failed transfers can then be triggered manually.

This solution prevents the “gas spending”, “block full” and "Fail computation" problems.

Example

import smartpy as sp

class RefundContract(sp.Contract):
    def __init__(self):
        self.init(funds = sp.map())

    @sp.entry_point
    def add_fund(self, params):
        sp.if self.data.funds.contains(params.receiver):
            self.data.funds[params.receiver] += sp.amount
        sp.else:
            self.data.funds[params.receiver] = sp.amount

    def send_refund(self, receiver):
        sp.transfer(sp.unit, self.data.funds[receiver], sp.contract(sp.TUnit, receiver).open_some())
        self.data.funds[receiver] = sp.mutez(0)

    @sp.entry_point
    def refund(self, params):
        sp.if sp.sender == sp.to_address(sp.self):
            self.send_refund(params.receiver)
        sp.else:
            self.send_refund(sp.sender)

    @sp.entry_point
    def refund_everyone(self, params):
        # Note that you must take into account the fact that you will pay fee apart
    	sp.verify(sp.mutez(params.fee * sp.len(self.data.funds.keys())) == sp.amount)
        
        sp.for receiver in self.data.funds.keys():
        	sp.new_transaction(arg=sp.record(receiver=receiver),  fee=sp.mutez(params.fee) , destination=sp.self_entry_point('refund'))
            """
            	sp.new_transaction represents the smartpy implementation of the proposal
            	It's a bit like doing:
            		sp.transfer(arg=sp.record(receiver=receiver), amount=sp.mutez(0), destination=sp.self_entry_point('refund'))
            	# except your transfer is sent in a new transaction after everything else passed.
           		# As a result if one transfer fails your "refund_everyone" transaction don't fail.
            	# However a pass of refund_everyone don't indicate if your transfers passed.
            """
            

On chain fee relay

One feature I’d like to see in Tezos would be to pay the fee (storage and block inclusion competition) for someone else or to ask for someone else to pay my fees.

This proposal permits that.

The sender still has to pay the fee for the relay demand but that fee could be much reduced compared to the one for the new transaction that will be done by the relay.

In the previous example, if the receiver wants to add a lot of things to the storage because of your refund, they can do it in a NEW_TRANSACTION so you don’t pay the fee for this.


Increase the interactivity possibilities

In the current situation implicit accounts are the only one able to create new transaction. It means that only humans or off chain codes can prolong the interactivity beyond the gas limit.

With this proposal you can start on a fresh new gas limit counter with a NEW_TRANSACTION.

Does the new version clarify the points you were raising?