-
Notifications
You must be signed in to change notification settings - Fork 378
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
Model smart contract function calls as commands #3268
Comments
First refinement, only changed methods are shown: class Transaction(abc.ABC):
# removed validate_preconditions and validate_after_failure
@abstractmethod
def preconditions(self, blockhash: BlockHash):
""" Raises if the required preconditions for the given transaction have
never been met.
Used for things like, interacting with an address which was never a
smart contract, or trying to call a function before it's preconditions
have been met.
Note:
This does not raise if the preconditions were met in the past,
but got invalidated.
raises:
UnrecoverableError: If the required preconditions have never been
met to successfully execute the transaction.
"""
pass
# recoverable errors should not raise exceptions. This was done previously
# because of control flow, to avoid refactoring return type of proxies
# methods
@absctractmehod
def is_valid(self, blockspec: BlockSpec) -> Tuple[bool, Optional[Reason]]:
""" Checks if the transaction can be executed, or if got invalidated
meanwhile.
This is called on a transaction which satisfied it's preconditions, so
at some point it was valid to execute the transaction, this function
checks if that is still the case.
If the transaction can be executed a tuple `(True, None)` is returned,
otherwise a tuple `(False, reason)` is returned, where `reason` is an
estimation of what invalidated the transaction. If the reason for the
failure is not detected, `(False, None)` is returned.
"""
pass
def send(
self,
blockhash: BlockHash,
gasprice: GasPrice,
nonce: Nonce = None,
):
# Shouldn't wait until the transaction is queue by the manager to check
# it's preconditions. Doing the check here would move the error reporting
# temporarily and spatially, making debugging unnecessarily harder
startgas = self.rpcclient.gasestimate()
if startgas is not None:
self.gasprice = gasprice
self.txidentifier: TXIdentifier = self.rpclient.send(startgas, gasprice)
else:
self.validate_after_failure('latest')
if self.client.balance() < self.approximated_startgas():
raise UnrecoverableError('insufficient balance')
raise UnrecoverableError('unexpected error or insufficient balance')
class TransactionManager:
def poll_transactions(self):
if self.pending:
result = self.rpcclient.poll(self.pending)
if isinstance(result, RPCFailure):
# This needs additional handling, since the RPCFailure could be that we
# tried to replace a transaction, but it failed (the previous transaction got
# mined first).
raise result.exception
if isinstance(result, TransactionFailure):
valid, reason = self.pending.is_valid()
if valid:
raise UnrecoverableError('valid transaction failed to execute')
else:
if reason is None:
raise UnrecoverableError('unexpected error')
log(reason)
if isinstance(result, Success):
heap.pop(self.heap, self.pending)
self.pending = None
self.maybe_send_next()
elif isinstance(result, Pending) and self.has_to_increase_gas(self.pending):
# gasprice updated by demand on new blocks
self.pending.increase_gasprice(self.gasprice)
def add_transaction(self, transaction: Transaction):
transaction.preconditions() # Raises if invalid, this will quit the program
valid, reason = transaction.is_valid()
if valid:
heap.push(self.heap, transaction)
if self.pending is None:
self.pending = transaction
transaction.send(self.blockhash, self.gasprice)
elif higher_priority(transaction, self.pending):
self.pending = transaction.replace(self.blockhash, self.pending)
else:
if reason is None:
raise UnrecoverableError('unexpected error')
log(reason)
def maybe_send_next(self):
while self.pending is None and self.heap:
transaction = heap.peek(self.heap)
valid, reason = self.pending.is_valid()
if valid:
self.pending = transaction
transaction.send(self.blockhash, self.gasprice)
else:
if reason is None:
raise UnrecoverableError('unexpected error')
heap.pop(self.heap)
log(reason) |
related: #2194 |
related |
Two notes:
|
From our discussion about Raiden planning on 28/01/2019 we concluded that we should push this for after the blockchain consistent view issue. Reasoning: It can still be done after the consistent view with ease, it's not really a priority and implementing it may take a bit of time and as such should not be started right now. |
Note: with the work done on the consistent blockchain view we changed the definition of the @abstractmethod
def preconditions(self, blockhash: BlockHash):
""" Raises if the required preconditions for the given transaction have
never been met.
Used for things like, interacting with an address which was never a
smart contract, or trying to call a function before it's preconditions
have been met.
Note:
This does not raise if the preconditions were met in the past,
but got invalidated.
raises:
UnrecoverableError: If the required preconditions have never been
met to successfully execute the transaction.
"""
pass This will now: @abstractmethod
def preconditions(self, triggered_by_block_hash: BlockHash):
""" Raises if the preconditions are not satisfied at the given
`triggered_by_block_hash`.
Used to check if there are any logic bugs in the business logic
which resulted in an impossible ContractSend*.
Note:
This does not care if the preconditions got invalidated on the
latest block.
raises:
UnrecoverableError: If the preconditions are not satisfied.
"""
pass The previous definition tried to handle logic errors and unnecessary transactions, while the new one only handles logic errors. Invalidated transactions are now handled exclusively by the Ref: #3505 (comment) |
While writing #4063 I realized that Example: If there are two transactions in the queue,
The above considers that |
These issues are related:
I thought a bit about this, and this is an interesting approach: https://gist.github.com/hackaugusto/088c31ba95c74355989f5a42b9b0a6e0 |
The design described here should probably be revised and take advantage of the optimization from |
Abstract
To support
Overwrite in-flight transactions in-flight transactions
#2801,Transaction prioritization
#2802, andTransaction batching
#4063 it's necessary an object to represent transactions, much like theContractSend*
state changes, but that can be manipulated by a transaction manager.Additionally, the current implementation of the proxies are cumbersome, as of 1466689 the token network proxy has 1238 lines, and it's increasing.
Motivation
This is a proposal to enable the issues and simplify the current code base, by representing each possible transaction as a command instance.
Specification
I have a non working prototype bellow, which should be enough to understand the design I have in mind.
Pros
Cons
While I was writing the above mock code, I realized this design will work only for one transaction at the time, so the deposit above has to be done in a single transaction, for the approve and setTotalDeposit.
Alternatively the design could be extended to allow for ordering of transactions.
Backwards Compatibility
This does not change any wire or database data. However, as state in cons it may require raiden-network/raiden-contracts#400 to be finalized, which will required upgrades to the smart contracts.
The text was updated successfully, but these errors were encountered: