Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #255 from lzpap/is_promotable
Browse files Browse the repository at this point in the history
Implement is_promotable Extended API Command
  • Loading branch information
lzpap committed Nov 5, 2019
2 parents 262ff70 + d9a61a6 commit a68254d
Show file tree
Hide file tree
Showing 5 changed files with 597 additions and 0 deletions.
35 changes: 35 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,41 @@ This method returns a ``dict`` with the following items:
- ``bundles: List[Bundle]``: Matching bundles, sorted by tail
transaction timestamp.

``is_promotable``
-------------------

This extended API function helps you to determine whether a tail transaction
(bundle) is promotable.
Example usage could be to determine if a transaction can be promoted or you
should reattach (``replay_bundle``).

The method takes a list of tail transaction hashes, calls ``check_consistency``
to verify consistency. If successful, fetches the transaction trytes from the
Tangle and checks if ``attachment_timestamp`` is within reasonable limits.

Parameters
~~~~~~~~~~

- ``tails: List[TransactionHash]``: Tail transaction hashes to check.

Return
~~~~~~

This method returns a ``dict`` with the following items:

- ``promotable: bool``: ``True``, if:

- Tails are consistent.
See `API Reference <https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#checkconsistency>`_.
- ``attachment_timestamp`` for all transactions are less than current time
and attachement happened no earlier than ``depth`` milestones.
By default, ``depth`` = 6.

parameter is ``False`` otherwise.

- ``info: Optional(List[String])``: If ``promotable`` is ``False``, contains information
about the error.

``is_reattachable``
-------------------

Expand Down
37 changes: 37 additions & 0 deletions iota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,43 @@ def get_transfers(self, start=0, stop=None, inclusion_states=False):
inclusionStates=inclusion_states,
)

def is_promotable(
self,
tails, # type: Iterable[TransactionHash]
):
# type: (Iterable(TransactionHash)] -> dict
"""
Checks if tail transaction(s) is promotable by calling
:py:meth:`check_consistency` and verifying that `attachmentTimestamp`
is above a lower bound.
Lower bound is calculated based on number of milestones issued
since transaction attachment.
:param tails:
List of tail transaction hashes.
:return:
The return type mimics that of :py:meth:`check_consistency`.
Dict with the following structure::
{
'promotable': bool,
If true, all tails are promotable. If false, see `info`
field.
'info': Optional(List[String])
If `promotable` is false, this contains info about what
went wrong.
}
References:
- https://github.com/iotaledger/iota.js/blob/next/api_reference.md#module_core.isPromotable
"""
return extended.IsPromotableCommand(self.adapter)(
tails=tails,
)

def prepare_transfer(
self,
transfers, # type: Iterable[ProposedTransaction]
Expand Down
1 change: 1 addition & 0 deletions iota/commands/extended/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from .get_new_addresses import *
from .get_transaction_objects import *
from .get_transfers import *
from .is_promotable import *
from .is_reattachable import *
from .prepare_transfer import *
from .promote_transaction import *
Expand Down
116 changes: 116 additions & 0 deletions iota/commands/extended/is_promotable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# coding=utf-8
from __future__ import absolute_import, division, print_function, \
unicode_literals

from iota.commands import FilterCommand, RequestFilter
from iota.commands.core import CheckConsistencyCommand, GetTrytesCommand
from iota.transaction import Transaction
from iota import TransactionHash
import filters as f
from iota.filters import Trytes
import time

__all__ = [
'IsPromotableCommand',
]

MILESTONE_INTERVAL = 2 * 60 * 1000
"""
Approximate interval in which a milestone is issued.
Unit is in milliseconds, so it is roughly 2 minutes.
"""

ONE_WAY_DELAY = 1 * 60 * 1000
"""
Propagation delay of the network (ms). (really-really worst case scenario)
The time needed for the message to propegate from client to edges of
majority network.
"""

DEPTH = 6
"""
The number of milestones issued since `attachmentTimestamp`.
"""

get_current_ms = lambda : int(round(time.time() * 1000))
"""
Calculate current time in milliseconds.
"""

class IsPromotableCommand(FilterCommand):
"""
Determines if a tail transaction is promotable.
See :py:meth:`iota.api.Iota.is_promotable` for more info.
"""
command = 'isPromotable'

def get_request_filter(self):
return IsPromotableRequestFilter()

def get_response_filter(self):
pass

def _execute(self, request):
tails = request['tails']

# First, check consistency
# A transaction is consistent, if:
# - The node isn't missing the transaction's branch or trunk transactions
# - The transaction's bundle is valid
# - The transaction's branch and trunk transactions are valid
cc_response = CheckConsistencyCommand(self.adapter)(
tails=tails,
)

if not cc_response['state']:
# One or more transactions are inconsistent
return {
'promotable' : False,
'info' : cc_response['info'],
}

transactions = [
Transaction.from_tryte_string(x) for x in
GetTrytesCommand(self.adapter)(hashes=tails)['trytes']
]

response = {
'promotable' : True,
'info' : [],
}

# Check timestamps
now = get_current_ms()
for tx in transactions:
is_within = is_within_depth(tx.attachment_timestamp, now)
if not is_within:
# Inform the user about what went wrong.
response['info'].append('Transaction {tx_hash} is above max depth.'.format(
tx_hash=tx.hash
))
# If one tx fails, response is false
response['promotable'] = response['promotable'] and is_within

# If there are no problems, we don't need 'info' field
if response['promotable']:
response['info'] = None

return response

class IsPromotableRequestFilter(RequestFilter):
def __init__(self):
super(IsPromotableRequestFilter, self).__init__({
'tails':
f.Required |
f.Array |
f.FilterRepeater(f.Required | Trytes(TransactionHash)),
})

def is_within_depth(attachment_timestamp, now, depth=DEPTH):
# type (int, int, Optiona(int)) -> bool
"""
Checks if `attachment_timestamp` is within limits of `depth`.
"""
return attachment_timestamp < now and \
now - attachment_timestamp < depth * MILESTONE_INTERVAL - ONE_WAY_DELAY

0 comments on commit a68254d

Please sign in to comment.