mirror of
https://github.com/planetmint/planetmint.git
synced 2025-03-30 15:08:31 +00:00
replaced transactions module (#268)
* adjusted hashlib imports and renamed to bigchaindb error Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * added type hints to transactions module Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved upsert_validator txs to transactions, updated imports Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * removed unused imports Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved tx validate to lib Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved from_db to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * removed from db from transaction Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved election validation to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved election methods to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved get_validators and get_recipients to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * removed unnecessary election method Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved show_election_status to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved topology check to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved election_id_to_public_key to validator_utils Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved vote methods to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved process_block to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * removed unused code from Vote Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved has election concluded to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * adjusted has_election_concluded Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * removed unused imports, added copyright notices Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved rollback_eleciton to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved on_rollback behaviour to planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * moved some validator utils to tendermint utils, election approval now handled by planetmint Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * Use planetmint-transaction pypi package Signed-off-by: cybnon <stefan.weber93@googlemail.com> * fixed docs imports Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * fixed validate call on test case Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * resolved linting errors Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * fixed mock on test case Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * adjusted CHANGELOG Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * removed duplicate transactions test suite Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * fixed pr comments Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * reordered imports to be standardized Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * removed unused imports and reordered them Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> * fixed linter error Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com> Signed-off-by: cybnon <stefan.weber93@googlemail.com> Co-authored-by: cybnon <stefan.weber93@googlemail.com>
This commit is contained in:
parent
69fe9b253d
commit
3954340d7d
@ -26,6 +26,9 @@ For reference, the possible headings are:
|
||||
* **Notes**
|
||||
|
||||
## [Unreleased]
|
||||
* **Changed** replaced transaction module with planetmint-transactions package
|
||||
* **Changed** moved transaction network validation to Planetmint class
|
||||
* **Changed** adjusted test cases
|
||||
|
||||
## [1.2.1] - 2022-20-09
|
||||
* **Changed** Create model now validates for CID strings for asset["data"] and metadata
|
||||
|
@ -9,11 +9,11 @@ import json
|
||||
import os
|
||||
import os.path
|
||||
|
||||
from planetmint.transactions.common.input import Input
|
||||
from planetmint.transactions.common.transaction_link import TransactionLink
|
||||
from transactions.common.input import Input
|
||||
from transactions.common.transaction_link import TransactionLink
|
||||
from planetmint import lib
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
from planetmint.web import server
|
||||
from ipld import multihash, marshal
|
||||
|
||||
|
@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from planetmint.transactions.common.transaction import Transaction # noqa
|
||||
from planetmint.upsert_validator import ValidatorElection # noqa
|
||||
from planetmint.transactions.types.elections.vote import Vote # noqa
|
||||
from planetmint.transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from transactions.common.transaction import Transaction # noqa
|
||||
from transactions.types.elections.validator_election import ValidatorElection # noqa
|
||||
from transactions.types.elections.vote import Vote # noqa
|
||||
from transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from planetmint.lib import Planetmint
|
||||
from planetmint.core import App
|
||||
|
||||
|
@ -3,15 +3,14 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from itertools import repeat
|
||||
import logging
|
||||
from importlib import import_module
|
||||
|
||||
import tarantool
|
||||
import logging
|
||||
|
||||
from itertools import repeat
|
||||
from importlib import import_module
|
||||
from transactions.common.exceptions import ConfigurationError
|
||||
from planetmint.config import Config
|
||||
from planetmint.backend.exceptions import ConnectionError
|
||||
from planetmint.transactions.common.exceptions import ConfigurationError
|
||||
|
||||
BACKENDS = {
|
||||
"tarantool_db": "planetmint.backend.tarantool.connection.TarantoolDBConnection",
|
||||
|
@ -3,10 +3,10 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from planetmint.exceptions import BigchainDBError
|
||||
from planetmint.exceptions import PlanetmintError
|
||||
|
||||
|
||||
class BackendError(BigchainDBError):
|
||||
class BackendError(PlanetmintError):
|
||||
"""Top level exception for any backend exception."""
|
||||
|
||||
|
||||
|
@ -9,7 +9,7 @@ import pymongo
|
||||
|
||||
from planetmint.config import Config
|
||||
from planetmint.backend.exceptions import DuplicateKeyError, OperationError, ConnectionError
|
||||
from planetmint.transactions.common.exceptions import ConfigurationError
|
||||
from transactions.common.exceptions import ConfigurationError
|
||||
from planetmint.utils import Lazy
|
||||
from planetmint.backend.connection import Connection
|
||||
|
||||
|
@ -13,7 +13,7 @@ from planetmint import backend
|
||||
from planetmint.backend.exceptions import DuplicateKeyError
|
||||
from planetmint.backend.utils import module_dispatch_registrar
|
||||
from planetmint.backend.localmongodb.connection import LocalMongoDBConnection
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from transactions.common.transaction import Transaction
|
||||
|
||||
register_query = module_dispatch_registrar(backend.query)
|
||||
|
||||
|
@ -5,13 +5,13 @@
|
||||
|
||||
"""Database creation and schema-providing interfaces for backends."""
|
||||
|
||||
from functools import singledispatch
|
||||
import logging
|
||||
|
||||
from functools import singledispatch
|
||||
from planetmint.config import Config
|
||||
from planetmint.backend.connection import connect
|
||||
from planetmint.transactions.common.exceptions import ValidationError
|
||||
from planetmint.transactions.common.utils import (
|
||||
from transactions.common.exceptions import ValidationError
|
||||
from transactions.common.utils import (
|
||||
validate_all_values_for_key_in_obj,
|
||||
validate_all_values_for_key_in_list,
|
||||
)
|
||||
|
@ -7,7 +7,7 @@ import logging
|
||||
import tarantool
|
||||
|
||||
from planetmint.config import Config
|
||||
from planetmint.transactions.common.exceptions import ConfigurationError
|
||||
from transactions.common.exceptions import ConfigurationError
|
||||
from planetmint.utils import Lazy
|
||||
from planetmint.backend.connection import Connection
|
||||
|
||||
|
@ -4,18 +4,16 @@
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
"""Query implementation for Tarantool"""
|
||||
import json
|
||||
|
||||
from secrets import token_hex
|
||||
from hashlib import sha256
|
||||
from operator import itemgetter
|
||||
import json
|
||||
|
||||
from tarantool.error import DatabaseError
|
||||
|
||||
from planetmint.backend import query
|
||||
from planetmint.backend.utils import module_dispatch_registrar
|
||||
from planetmint.backend.tarantool.connection import TarantoolDBConnection
|
||||
from planetmint.backend.tarantool.transaction.tools import TransactionCompose, TransactionDecompose
|
||||
from json import dumps, loads
|
||||
|
||||
|
||||
register_query = module_dispatch_registrar(query)
|
||||
@ -325,7 +323,7 @@ def store_unspent_outputs(connection, *unspent_outputs: list):
|
||||
if unspent_outputs:
|
||||
for utxo in unspent_outputs:
|
||||
output = connection.run(
|
||||
connection.space("utxos").insert((utxo["transaction_id"], utxo["output_index"], dumps(utxo)))
|
||||
connection.space("utxos").insert((utxo["transaction_id"], utxo["output_index"], json.dumps(utxo)))
|
||||
)
|
||||
result.append(output)
|
||||
return result
|
||||
@ -344,7 +342,7 @@ def delete_unspent_outputs(connection, *unspent_outputs: list):
|
||||
@register_query(TarantoolDBConnection)
|
||||
def get_unspent_outputs(connection, query=None): # for now we don't have implementation for 'query'.
|
||||
_utxos = connection.run(connection.space("utxos").select([]))
|
||||
return [loads(utx[2]) for utx in _utxos]
|
||||
return [json.loads(utx[2]) for utx in _utxos]
|
||||
|
||||
|
||||
@register_query(TarantoolDBConnection)
|
||||
@ -459,7 +457,7 @@ def get_asset_tokens_for_public_key(
|
||||
|
||||
@register_query(TarantoolDBConnection)
|
||||
def store_abci_chain(connection, height: int, chain_id: str, is_synced: bool = True):
|
||||
hash_id_primarykey = sha256(dumps(obj={"height": height}).encode()).hexdigest()
|
||||
hash_id_primarykey = sha256(json.dumps(obj={"height": height}).encode()).hexdigest()
|
||||
connection.run(
|
||||
connection.space("abci_chains").upsert(
|
||||
(height, is_synced, chain_id, hash_id_primarykey),
|
||||
@ -471,7 +469,7 @@ def store_abci_chain(connection, height: int, chain_id: str, is_synced: bool = T
|
||||
|
||||
@register_query(TarantoolDBConnection)
|
||||
def delete_abci_chain(connection, height: int):
|
||||
hash_id_primarykey = sha256(dumps(obj={"height": height}).encode()).hexdigest()
|
||||
hash_id_primarykey = sha256(json.dumps(obj={"height": height}).encode()).hexdigest()
|
||||
connection.run(connection.space("abci_chains").delete(hash_id_primarykey), only_data=False)
|
||||
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
import logging
|
||||
|
||||
import tarantool
|
||||
from planetmint.config import Config
|
||||
from planetmint.backend.utils import module_dispatch_registrar
|
||||
from planetmint import backend
|
||||
|
@ -1,7 +1,8 @@
|
||||
from secrets import token_hex
|
||||
import copy
|
||||
import json
|
||||
from planetmint.transactions.common.memoize import HDict
|
||||
|
||||
from secrets import token_hex
|
||||
from transactions.common.memoize import HDict
|
||||
|
||||
|
||||
def get_items(_list):
|
||||
|
@ -10,21 +10,19 @@ the command-line interface (CLI) for Planetmint Server.
|
||||
import os
|
||||
import logging
|
||||
import argparse
|
||||
import copy
|
||||
import json
|
||||
import sys
|
||||
from planetmint.backend.tarantool.connection import TarantoolDBConnection
|
||||
import planetmint
|
||||
|
||||
from planetmint.core import rollback
|
||||
from planetmint.utils import load_node_key
|
||||
from planetmint.transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT
|
||||
from planetmint.transactions.common.exceptions import DatabaseDoesNotExist, ValidationError
|
||||
from planetmint.transactions.types.elections.vote import Vote
|
||||
from planetmint.transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
import planetmint
|
||||
from planetmint import backend, ValidatorElection, Planetmint
|
||||
from transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT
|
||||
from transactions.common.exceptions import DatabaseDoesNotExist, ValidationError
|
||||
from transactions.types.elections.vote import Vote
|
||||
from transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from transactions.types.elections.validator_utils import election_id_to_public_key
|
||||
from planetmint import ValidatorElection, Planetmint
|
||||
from planetmint.backend import schema
|
||||
from planetmint.backend import tarantool
|
||||
from planetmint.commands import utils
|
||||
from planetmint.commands.utils import configure_planetmint, input_on_stderr
|
||||
from planetmint.log import setup_logging
|
||||
@ -122,9 +120,9 @@ def run_election_new(args, planet):
|
||||
def create_new_election(sk, planet, election_class, data):
|
||||
try:
|
||||
key = load_node_key(sk)
|
||||
voters = election_class.recipients(planet)
|
||||
voters = planet.get_recipients_list()
|
||||
election = election_class.generate([key.public_key], voters, data, None).sign([key.private_key])
|
||||
election.validate(planet)
|
||||
planet.validate_election(election)
|
||||
except ValidationError as e:
|
||||
logger.error(e)
|
||||
return False
|
||||
@ -200,9 +198,9 @@ def run_election_approve(args, planet):
|
||||
return False
|
||||
|
||||
inputs = [i for i in tx.to_inputs() if key.public_key in i.owners_before]
|
||||
election_pub_key = ValidatorElection.to_public_key(tx.id)
|
||||
election_pub_key = election_id_to_public_key(tx.id)
|
||||
approval = Vote.generate(inputs, [([election_pub_key], voting_power)], tx.id).sign([key.private_key])
|
||||
approval.validate(planet)
|
||||
planet.validate_transaction(approval)
|
||||
|
||||
resp = planet.write_transaction(approval, BROADCAST_TX_COMMIT)
|
||||
|
||||
@ -229,7 +227,7 @@ def run_election_show(args, planet):
|
||||
logger.error(f"No election found with election_id {args.election_id}")
|
||||
return
|
||||
|
||||
response = election.show_election(planet)
|
||||
response = planet.show_election_status(election)
|
||||
|
||||
logger.info(response)
|
||||
|
||||
|
@ -12,9 +12,9 @@ import builtins
|
||||
import functools
|
||||
import multiprocessing as mp
|
||||
import sys
|
||||
|
||||
import planetmint
|
||||
import planetmint.config_utils
|
||||
|
||||
from planetmint.version import __version__
|
||||
|
||||
|
||||
|
@ -21,11 +21,11 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
import collections.abc
|
||||
|
||||
from functools import lru_cache
|
||||
from pkg_resources import iter_entry_points, ResolutionError
|
||||
|
||||
from planetmint.config import Config
|
||||
from planetmint.transactions.common import exceptions
|
||||
from transactions.common import exceptions
|
||||
from planetmint.validation import BaseValidationRules
|
||||
|
||||
# TODO: move this to a proper configuration file for logging
|
||||
|
@ -8,6 +8,7 @@ with Tendermint.
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from tendermint.abci import types_pb2
|
||||
from abci.application import BaseApplication
|
||||
from abci.application import OkCode
|
||||
@ -21,10 +22,8 @@ from tendermint.abci.types_pb2 import (
|
||||
ResponseCommit,
|
||||
)
|
||||
from planetmint import Planetmint
|
||||
from planetmint.transactions.types.elections.election import Election
|
||||
from planetmint.tendermint_utils import decode_transaction, calculate_hash
|
||||
from planetmint.tendermint_utils import decode_transaction, calculate_hash, decode_validator
|
||||
from planetmint.lib import Block
|
||||
import planetmint.upsert_validator.validator_utils as vutils
|
||||
from planetmint.events import EventTypes, Event
|
||||
|
||||
|
||||
@ -87,7 +86,7 @@ class App(BaseApplication):
|
||||
app_hash = "" if block is None else block["app_hash"]
|
||||
height = 0 if block is None else block["height"] + 1
|
||||
known_validators = self.planetmint_node.get_validators()
|
||||
validator_set = [vutils.decode_validator(v) for v in genesis.validators]
|
||||
validator_set = [decode_validator(v) for v in genesis.validators]
|
||||
if known_validators and known_validators != validator_set:
|
||||
self.log_abci_migration_error(known_chain["chain_id"], known_validators)
|
||||
sys.exit(1)
|
||||
@ -209,7 +208,7 @@ class App(BaseApplication):
|
||||
else:
|
||||
self.block_txn_hash = block["app_hash"]
|
||||
|
||||
validator_update = Election.process_block(self.planetmint_node, self.new_height, self.block_transactions)
|
||||
validator_update = self.planetmint_node.process_block(self.new_height, self.block_transactions)
|
||||
|
||||
return ResponseEndBlock(validator_updates=validator_update)
|
||||
|
||||
@ -246,11 +245,11 @@ class App(BaseApplication):
|
||||
return ResponseCommit(data=data)
|
||||
|
||||
|
||||
def rollback(b):
|
||||
def rollback(planetmint):
|
||||
pre_commit = None
|
||||
|
||||
try:
|
||||
pre_commit = b.get_pre_commit_state()
|
||||
pre_commit = planetmint.get_pre_commit_state()
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error occurred while executing get_pre_commit_state()", e)
|
||||
|
||||
@ -258,12 +257,12 @@ def rollback(b):
|
||||
# the pre_commit record is first stored in the first `end_block`
|
||||
return
|
||||
|
||||
latest_block = b.get_latest_block()
|
||||
latest_block = planetmint.get_latest_block()
|
||||
if latest_block is None:
|
||||
logger.error("Found precommit state but no blocks!")
|
||||
sys.exit(1)
|
||||
|
||||
# NOTE: the pre-commit state is always at most 1 block ahead of the commited state
|
||||
if latest_block["height"] < pre_commit["height"]:
|
||||
Election.rollback(b, pre_commit["height"], pre_commit["transactions"])
|
||||
b.delete_transactions(pre_commit["transactions"])
|
||||
planetmint.rollback_election(pre_commit["height"], pre_commit["transactions"])
|
||||
planetmint.delete_transactions(pre_commit["transactions"])
|
||||
|
@ -4,9 +4,9 @@
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
|
||||
class BigchainDBError(Exception):
|
||||
class PlanetmintError(Exception):
|
||||
"""Base class for Planetmint exceptions."""
|
||||
|
||||
|
||||
class CriticalDoubleSpend(BigchainDBError):
|
||||
class CriticalDoubleSpend(PlanetmintError):
|
||||
"""Data integrity error that requires attention"""
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
from planetmint.utils import condition_details_has_owner
|
||||
from planetmint.backend import query
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common.transaction import TransactionLink
|
||||
|
||||
|
||||
class FastQuery:
|
||||
|
@ -8,30 +8,44 @@ MongoDB.
|
||||
|
||||
"""
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from uuid import uuid4
|
||||
|
||||
import json
|
||||
import rapidjson
|
||||
|
||||
try:
|
||||
from hashlib import sha3_256
|
||||
except ImportError:
|
||||
# NOTE: needed for Python < 3.6
|
||||
from sha3 import sha3_256
|
||||
|
||||
import requests
|
||||
|
||||
import planetmint
|
||||
|
||||
from collections import namedtuple, OrderedDict
|
||||
from uuid import uuid4
|
||||
from hashlib import sha3_256
|
||||
from transactions import Transaction, Vote
|
||||
from transactions.common.crypto import public_key_from_ed25519_key
|
||||
from transactions.common.exceptions import (
|
||||
SchemaValidationError,
|
||||
ValidationError,
|
||||
DuplicateTransaction,
|
||||
InvalidSignature,
|
||||
DoubleSpend,
|
||||
InputDoesNotExist,
|
||||
AssetIdMismatch,
|
||||
AmountError,
|
||||
MultipleInputsError,
|
||||
InvalidProposer,
|
||||
UnequalValidatorSet,
|
||||
InvalidPowerChange,
|
||||
)
|
||||
from transactions.common.transaction import VALIDATOR_ELECTION, CHAIN_MIGRATION_ELECTION
|
||||
from transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT, BROADCAST_TX_ASYNC, BROADCAST_TX_SYNC
|
||||
from transactions.types.elections.election import Election
|
||||
from transactions.types.elections.validator_utils import election_id_to_public_key
|
||||
from planetmint.config import Config
|
||||
from planetmint import backend, config_utils, fastquery
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.common.exceptions import SchemaValidationError, ValidationError, DoubleSpend
|
||||
from planetmint.transactions.common.transaction_mode_types import (
|
||||
BROADCAST_TX_COMMIT,
|
||||
BROADCAST_TX_ASYNC,
|
||||
BROADCAST_TX_SYNC,
|
||||
from planetmint.tendermint_utils import (
|
||||
encode_transaction,
|
||||
merkleroot,
|
||||
key_from_base64,
|
||||
public_key_to_base64,
|
||||
encode_validator,
|
||||
new_validator_set,
|
||||
)
|
||||
from planetmint.tendermint_utils import encode_transaction, merkleroot
|
||||
from planetmint import exceptions as core_exceptions
|
||||
from planetmint.validation import BaseValidationRules
|
||||
|
||||
@ -338,7 +352,7 @@ class Planetmint(object):
|
||||
|
||||
if block:
|
||||
transactions = backend.query.get_transactions(self.connection, block["transactions"])
|
||||
result["transactions"] = [t.to_dict() for t in Transaction.from_db(self, transactions)]
|
||||
result["transactions"] = [t.to_dict() for t in self.tx_from_db(transactions)]
|
||||
|
||||
return result
|
||||
|
||||
@ -375,7 +389,63 @@ class Planetmint(object):
|
||||
except ValidationError as e:
|
||||
logger.warning("Invalid transaction (%s): %s", type(e).__name__, e)
|
||||
return False
|
||||
return transaction.validate(self, current_transactions)
|
||||
|
||||
if transaction.operation == Transaction.CREATE:
|
||||
duplicates = any(txn for txn in current_transactions if txn.id == transaction.id)
|
||||
if self.is_committed(transaction.id) or duplicates:
|
||||
raise DuplicateTransaction("transaction `{}` already exists".format(transaction.id))
|
||||
elif transaction.operation in [Transaction.TRANSFER, Transaction.VOTE]:
|
||||
self.validate_transfer_inputs(transaction, current_transactions)
|
||||
|
||||
return transaction
|
||||
|
||||
def validate_transfer_inputs(self, tx, current_transactions=[]):
|
||||
# store the inputs so that we can check if the asset ids match
|
||||
input_txs = []
|
||||
input_conditions = []
|
||||
for input_ in tx.inputs:
|
||||
input_txid = input_.fulfills.txid
|
||||
input_tx = self.get_transaction(input_txid)
|
||||
if input_tx is None:
|
||||
for ctxn in current_transactions:
|
||||
if ctxn.id == input_txid:
|
||||
input_tx = ctxn
|
||||
|
||||
if input_tx is None:
|
||||
raise InputDoesNotExist("input `{}` doesn't exist".format(input_txid))
|
||||
|
||||
spent = self.get_spent(input_txid, input_.fulfills.output, current_transactions)
|
||||
if spent:
|
||||
raise DoubleSpend("input `{}` was already spent".format(input_txid))
|
||||
|
||||
output = input_tx.outputs[input_.fulfills.output]
|
||||
input_conditions.append(output)
|
||||
input_txs.append(input_tx)
|
||||
|
||||
# Validate that all inputs are distinct
|
||||
links = [i.fulfills.to_uri() for i in tx.inputs]
|
||||
if len(links) != len(set(links)):
|
||||
raise DoubleSpend('tx "{}" spends inputs twice'.format(tx.id))
|
||||
|
||||
# validate asset id
|
||||
asset_id = tx.get_asset_id(input_txs)
|
||||
if asset_id != tx.asset["id"]:
|
||||
raise AssetIdMismatch(("The asset id of the input does not" " match the asset id of the" " transaction"))
|
||||
|
||||
if not tx.inputs_valid(input_conditions):
|
||||
raise InvalidSignature("Transaction signature is invalid.")
|
||||
|
||||
input_amount = sum([input_condition.amount for input_condition in input_conditions])
|
||||
output_amount = sum([output_condition.amount for output_condition in tx.outputs])
|
||||
|
||||
if output_amount != input_amount:
|
||||
raise AmountError(
|
||||
(
|
||||
"The amount used in the inputs `{}`" " needs to be same as the amount used" " in the outputs `{}`"
|
||||
).format(input_amount, output_amount)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def is_valid_transaction(self, tx, current_transactions=[]):
|
||||
# NOTE: the function returns the Transaction object in case
|
||||
@ -426,11 +496,11 @@ class Planetmint(object):
|
||||
def fastquery(self):
|
||||
return fastquery.FastQuery(self.connection)
|
||||
|
||||
def get_validator_change(self, height=None):
|
||||
def get_validator_set(self, height=None):
|
||||
return backend.query.get_validator_set(self.connection, height)
|
||||
|
||||
def get_validators(self, height=None):
|
||||
result = self.get_validator_change(height)
|
||||
result = self.get_validator_set(height)
|
||||
return [] if result is None else result["validators"]
|
||||
|
||||
def get_election(self, election_id):
|
||||
@ -494,5 +564,398 @@ class Planetmint(object):
|
||||
def delete_elections(self, height):
|
||||
return backend.query.delete_elections(self.connection, height)
|
||||
|
||||
def tx_from_db(self, tx_dict_list):
|
||||
"""Helper method that reconstructs a transaction dict that was returned
|
||||
from the database. It checks what asset_id to retrieve, retrieves the
|
||||
asset from the asset table and reconstructs the transaction.
|
||||
|
||||
Args:
|
||||
tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or
|
||||
list of transaction dict as returned from the database.
|
||||
|
||||
Returns:
|
||||
:class:`~Transaction`
|
||||
|
||||
"""
|
||||
return_list = True
|
||||
if isinstance(tx_dict_list, dict):
|
||||
tx_dict_list = [tx_dict_list]
|
||||
return_list = False
|
||||
|
||||
tx_map = {}
|
||||
tx_ids = []
|
||||
for tx in tx_dict_list:
|
||||
tx.update({"metadata": None})
|
||||
tx_map[tx["id"]] = tx
|
||||
tx_ids.append(tx["id"])
|
||||
|
||||
assets = list(self.get_assets(tx_ids))
|
||||
for asset in assets:
|
||||
if asset is not None:
|
||||
# This is tarantool specific behaviour needs to be addressed
|
||||
tx = tx_map[asset[1]]
|
||||
tx["asset"] = asset[0]
|
||||
|
||||
tx_ids = list(tx_map.keys())
|
||||
metadata_list = list(self.get_metadata(tx_ids))
|
||||
for metadata in metadata_list:
|
||||
if "id" in metadata:
|
||||
tx = tx_map[metadata["id"]]
|
||||
tx.update({"metadata": metadata.get("metadata")})
|
||||
|
||||
if return_list:
|
||||
tx_list = []
|
||||
for tx_id, tx in tx_map.items():
|
||||
tx_list.append(Transaction.from_dict(tx))
|
||||
return tx_list
|
||||
else:
|
||||
tx = list(tx_map.values())[0]
|
||||
return Transaction.from_dict(tx)
|
||||
|
||||
# NOTE: moved here from Election needs to be placed somewhere else
|
||||
def get_validators_dict(self, height=None):
|
||||
"""Return a dictionary of validators with key as `public_key` and
|
||||
value as the `voting_power`
|
||||
"""
|
||||
validators = {}
|
||||
for validator in self.get_validators(height):
|
||||
# NOTE: we assume that Tendermint encodes public key in base64
|
||||
public_key = public_key_from_ed25519_key(key_from_base64(validator["public_key"]["value"]))
|
||||
validators[public_key] = validator["voting_power"]
|
||||
|
||||
return validators
|
||||
|
||||
def validate_election(self, transaction, current_transactions=[]): # TODO: move somewhere else
|
||||
"""Validate election transaction
|
||||
|
||||
NOTE:
|
||||
* A valid election is initiated by an existing validator.
|
||||
|
||||
* A valid election is one where voters are validators and votes are
|
||||
allocated according to the voting power of each validator node.
|
||||
|
||||
Args:
|
||||
:param planet: (Planetmint) an instantiated planetmint.lib.Planetmint object.
|
||||
:param current_transactions: (list) A list of transactions to be validated along with the election
|
||||
|
||||
Returns:
|
||||
Election: a Election object or an object of the derived Election subclass.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the election is invalid
|
||||
"""
|
||||
|
||||
duplicates = any(txn for txn in current_transactions if txn.id == transaction.id)
|
||||
if self.is_committed(transaction.id) or duplicates:
|
||||
raise DuplicateTransaction("transaction `{}` already exists".format(transaction.id))
|
||||
|
||||
current_validators = self.get_validators_dict()
|
||||
|
||||
# NOTE: Proposer should be a single node
|
||||
if len(transaction.inputs) != 1 or len(transaction.inputs[0].owners_before) != 1:
|
||||
raise MultipleInputsError("`tx_signers` must be a list instance of length one")
|
||||
|
||||
# NOTE: Check if the proposer is a validator.
|
||||
[election_initiator_node_pub_key] = transaction.inputs[0].owners_before
|
||||
if election_initiator_node_pub_key not in current_validators.keys():
|
||||
raise InvalidProposer("Public key is not a part of the validator set")
|
||||
|
||||
# NOTE: Check if all validators have been assigned votes equal to their voting power
|
||||
if not self.is_same_topology(current_validators, transaction.outputs):
|
||||
raise UnequalValidatorSet("Validator set much be exactly same to the outputs of election")
|
||||
|
||||
if transaction.operation == VALIDATOR_ELECTION:
|
||||
self.validate_validator_election(transaction)
|
||||
|
||||
return transaction
|
||||
|
||||
def validate_validator_election(self, transaction): # TODO: move somewhere else
|
||||
"""For more details refer BEP-21: https://github.com/planetmint/BEPs/tree/master/21"""
|
||||
|
||||
current_validators = self.get_validators_dict()
|
||||
|
||||
# NOTE: change more than 1/3 of the current power is not allowed
|
||||
if transaction.asset["data"]["power"] >= (1 / 3) * sum(current_validators.values()):
|
||||
raise InvalidPowerChange("`power` change must be less than 1/3 of total power")
|
||||
|
||||
def get_election_status(self, transaction):
|
||||
election = self.get_election(transaction.id)
|
||||
if election and election["is_concluded"]:
|
||||
return Election.CONCLUDED
|
||||
|
||||
return Election.INCONCLUSIVE if self.has_validator_set_changed(transaction) else Election.ONGOING
|
||||
|
||||
def has_validator_set_changed(self, transaction): # TODO: move somewhere else
|
||||
latest_change = self.get_validator_change()
|
||||
if latest_change is None:
|
||||
return False
|
||||
|
||||
latest_change_height = latest_change["height"]
|
||||
|
||||
election = self.get_election(transaction.id)
|
||||
|
||||
return latest_change_height > election["height"]
|
||||
|
||||
def get_validator_change(self): # TODO: move somewhere else
|
||||
"""Return the validator set from the most recent approved block
|
||||
|
||||
:return: {
|
||||
'height': <block_height>,
|
||||
'validators': <validator_set>
|
||||
}
|
||||
"""
|
||||
latest_block = self.get_latest_block()
|
||||
if latest_block is None:
|
||||
return None
|
||||
return self.get_validator_set(latest_block["height"])
|
||||
|
||||
def get_validator_dict(self, height=None):
|
||||
"""Return a dictionary of validators with key as `public_key` and
|
||||
value as the `voting_power`
|
||||
"""
|
||||
validators = {}
|
||||
for validator in self.get_validators(height):
|
||||
# NOTE: we assume that Tendermint encodes public key in base64
|
||||
public_key = public_key_from_ed25519_key(key_from_base64(validator["public_key"]["value"]))
|
||||
validators[public_key] = validator["voting_power"]
|
||||
|
||||
return validators
|
||||
|
||||
def get_recipients_list(self):
|
||||
"""Convert validator dictionary to a recipient list for `Transaction`"""
|
||||
|
||||
recipients = []
|
||||
for public_key, voting_power in self.get_validator_dict().items():
|
||||
recipients.append(([public_key], voting_power))
|
||||
|
||||
return recipients
|
||||
|
||||
def show_election_status(self, transaction):
|
||||
data = transaction.asset["data"]
|
||||
if "public_key" in data.keys():
|
||||
data["public_key"] = public_key_to_base64(data["public_key"]["value"])
|
||||
response = ""
|
||||
for k, v in data.items():
|
||||
if k != "seed":
|
||||
response += f"{k}={v}\n"
|
||||
response += f"status={self.get_election_status(transaction)}"
|
||||
|
||||
if transaction.operation == CHAIN_MIGRATION_ELECTION:
|
||||
response = self.append_chain_migration_status(response)
|
||||
|
||||
return response
|
||||
|
||||
def append_chain_migration_status(self, status):
|
||||
chain = self.get_latest_abci_chain()
|
||||
if chain is None or chain["is_synced"]:
|
||||
return status
|
||||
|
||||
status += f'\nchain_id={chain["chain_id"]}'
|
||||
block = self.get_latest_block()
|
||||
status += f'\napp_hash={block["app_hash"]}'
|
||||
validators = [
|
||||
{
|
||||
"pub_key": {
|
||||
"type": "tendermint/PubKeyEd25519",
|
||||
"value": k,
|
||||
},
|
||||
"power": v,
|
||||
}
|
||||
for k, v in self.get_validator_dict().items()
|
||||
]
|
||||
status += f"\nvalidators={json.dumps(validators, indent=4)}"
|
||||
return status
|
||||
|
||||
def is_same_topology(cls, current_topology, election_topology):
|
||||
voters = {}
|
||||
for voter in election_topology:
|
||||
if len(voter.public_keys) > 1:
|
||||
return False
|
||||
|
||||
[public_key] = voter.public_keys
|
||||
voting_power = voter.amount
|
||||
voters[public_key] = voting_power
|
||||
|
||||
# Check whether the voters and their votes is same to that of the
|
||||
# validators and their voting power in the network
|
||||
return current_topology == voters
|
||||
|
||||
def count_votes(self, election_pk, transactions, getter=getattr):
|
||||
votes = 0
|
||||
for txn in transactions:
|
||||
if getter(txn, "operation") == Vote.OPERATION:
|
||||
for output in getter(txn, "outputs"):
|
||||
# NOTE: We enforce that a valid vote to election id will have only
|
||||
# election_pk in the output public keys, including any other public key
|
||||
# along with election_pk will lead to vote being not considered valid.
|
||||
if len(getter(output, "public_keys")) == 1 and [election_pk] == getter(output, "public_keys"):
|
||||
votes = votes + int(getter(output, "amount"))
|
||||
return votes
|
||||
|
||||
def get_commited_votes(self, transaction, election_pk=None): # TODO: move somewhere else
|
||||
if election_pk is None:
|
||||
election_pk = election_id_to_public_key(transaction.id)
|
||||
txns = list(backend.query.get_asset_tokens_for_public_key(self.connection, transaction.id, election_pk))
|
||||
return self.count_votes(election_pk, txns, dict.get)
|
||||
|
||||
def _get_initiated_elections(self, height, txns): # TODO: move somewhere else
|
||||
elections = []
|
||||
for tx in txns:
|
||||
if not isinstance(tx, Election):
|
||||
continue
|
||||
|
||||
elections.append({"election_id": tx.id, "height": height, "is_concluded": False})
|
||||
return elections
|
||||
|
||||
def _get_votes(self, txns): # TODO: move somewhere else
|
||||
elections = OrderedDict()
|
||||
for tx in txns:
|
||||
if not isinstance(tx, Vote):
|
||||
continue
|
||||
|
||||
election_id = tx.asset["id"]
|
||||
if election_id not in elections:
|
||||
elections[election_id] = []
|
||||
elections[election_id].append(tx)
|
||||
return elections
|
||||
|
||||
def process_block(self, new_height, txns): # TODO: move somewhere else
|
||||
"""Looks for election and vote transactions inside the block, records
|
||||
and processes elections.
|
||||
|
||||
Every election is recorded in the database.
|
||||
|
||||
Every vote has a chance to conclude the corresponding election. When
|
||||
an election is concluded, the corresponding database record is
|
||||
marked as such.
|
||||
|
||||
Elections and votes are processed in the order in which they
|
||||
appear in the block. Elections are concluded in the order of
|
||||
appearance of their first votes in the block.
|
||||
|
||||
For every election concluded in the block, calls its `on_approval`
|
||||
method. The returned value of the last `on_approval`, if any,
|
||||
is a validator set update to be applied in one of the following blocks.
|
||||
|
||||
`on_approval` methods are implemented by elections of particular type.
|
||||
The method may contain side effects but should be idempotent. To account
|
||||
for other concluded elections, if it requires so, the method should
|
||||
rely on the database state.
|
||||
"""
|
||||
# elections initiated in this block
|
||||
initiated_elections = self._get_initiated_elections(new_height, txns)
|
||||
|
||||
if initiated_elections:
|
||||
self.store_elections(initiated_elections)
|
||||
|
||||
# elections voted for in this block and their votes
|
||||
elections = self._get_votes(txns)
|
||||
|
||||
validator_update = None
|
||||
for election_id, votes in elections.items():
|
||||
election = self.get_transaction(election_id)
|
||||
if election is None:
|
||||
continue
|
||||
|
||||
if not self.has_election_concluded(election, votes):
|
||||
continue
|
||||
|
||||
validator_update = self.approve_election(election, new_height)
|
||||
self.store_election(election.id, new_height, is_concluded=True)
|
||||
|
||||
return [validator_update] if validator_update else []
|
||||
|
||||
def has_election_concluded(self, transaction, current_votes=[]): # TODO: move somewhere else
|
||||
"""Check if the election can be concluded or not.
|
||||
|
||||
* Elections can only be concluded if the validator set has not changed
|
||||
since the election was initiated.
|
||||
* Elections can be concluded only if the current votes form a supermajority.
|
||||
|
||||
Custom elections may override this function and introduce additional checks.
|
||||
"""
|
||||
if self.has_validator_set_changed(transaction):
|
||||
return False
|
||||
|
||||
if transaction.operation == VALIDATOR_ELECTION:
|
||||
if not self.has_validator_election_concluded():
|
||||
return False
|
||||
|
||||
if transaction.operation == CHAIN_MIGRATION_ELECTION:
|
||||
if not self.has_chain_migration_concluded():
|
||||
return False
|
||||
|
||||
election_pk = election_id_to_public_key(transaction.id)
|
||||
votes_committed = self.get_commited_votes(transaction, election_pk)
|
||||
votes_current = self.count_votes(election_pk, current_votes)
|
||||
|
||||
total_votes = sum(output.amount for output in transaction.outputs)
|
||||
if (votes_committed < (2 / 3) * total_votes) and (votes_committed + votes_current >= (2 / 3) * total_votes):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def has_validator_election_concluded(self): # TODO: move somewhere else
|
||||
latest_block = self.get_latest_block()
|
||||
if latest_block is not None:
|
||||
latest_block_height = latest_block["height"]
|
||||
latest_validator_change = self.get_validator_set()["height"]
|
||||
|
||||
# TODO change to `latest_block_height + 3` when upgrading to Tendermint 0.24.0.
|
||||
if latest_validator_change == latest_block_height + 2:
|
||||
# do not conclude the election if there is a change assigned already
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def has_chain_migration_concluded(self): # TODO: move somewhere else
|
||||
chain = self.get_latest_abci_chain()
|
||||
if chain is not None and not chain["is_synced"]:
|
||||
# do not conclude the migration election if
|
||||
# there is another migration in progress
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def rollback_election(self, new_height, txn_ids): # TODO: move somewhere else
|
||||
"""Looks for election and vote transactions inside the block and
|
||||
cleans up the database artifacts possibly created in `process_blocks`.
|
||||
|
||||
Part of the `end_block`/`commit` crash recovery.
|
||||
"""
|
||||
|
||||
# delete election records for elections initiated at this height and
|
||||
# elections concluded at this height
|
||||
self.delete_elections(new_height)
|
||||
|
||||
txns = [self.get_transaction(tx_id) for tx_id in txn_ids]
|
||||
|
||||
elections = self._get_votes(txns)
|
||||
for election_id in elections:
|
||||
election = self.get_transaction(election_id)
|
||||
if election.operation == VALIDATOR_ELECTION:
|
||||
# TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0.
|
||||
self.delete_validator_set(new_height + 1)
|
||||
if election.operation == CHAIN_MIGRATION_ELECTION:
|
||||
self.delete_abci_chain(new_height)
|
||||
|
||||
def approve_election(self, election, new_height):
|
||||
"""Override to update the database state according to the
|
||||
election rules. Consider the current database state to account for
|
||||
other concluded elections, if required.
|
||||
"""
|
||||
if election.operation == CHAIN_MIGRATION_ELECTION:
|
||||
self.migrate_abci_chain()
|
||||
if election.operation == VALIDATOR_ELECTION:
|
||||
validator_updates = [election.asset["data"]]
|
||||
curr_validator_set = self.get_validators(new_height)
|
||||
updated_validator_set = new_validator_set(curr_validator_set, validator_updates)
|
||||
|
||||
updated_validator_set = [v for v in updated_validator_set if v["voting_power"] > 0]
|
||||
|
||||
# TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0.
|
||||
self.store_validator_set(new_height + 1, updated_validator_set)
|
||||
return encode_validator(election.asset["data"])
|
||||
|
||||
|
||||
Block = namedtuple("Block", ("app_hash", "height", "transactions"))
|
||||
|
@ -3,13 +3,9 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import planetmint
|
||||
import logging
|
||||
|
||||
from planetmint.transactions.common.exceptions import ConfigurationError
|
||||
from transactions.common.exceptions import ConfigurationError
|
||||
from logging.config import dictConfig as set_logging_config
|
||||
from planetmint.config import Config, DEFAULT_LOGGING_CONFIG
|
||||
import os
|
||||
|
||||
|
||||
def _normalize_log_level(level):
|
||||
|
@ -4,8 +4,8 @@
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import multiprocessing as mp
|
||||
from collections import defaultdict
|
||||
|
||||
from collections import defaultdict
|
||||
from planetmint import App
|
||||
from planetmint.lib import Planetmint
|
||||
from planetmint.tendermint_utils import decode_transaction
|
||||
|
@ -6,12 +6,64 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
from binascii import hexlify
|
||||
import codecs
|
||||
|
||||
try:
|
||||
from hashlib import sha3_256
|
||||
except ImportError:
|
||||
from sha3 import sha3_256
|
||||
from binascii import hexlify
|
||||
from tendermint.abci import types_pb2
|
||||
from tendermint.crypto import keys_pb2
|
||||
from hashlib import sha3_256
|
||||
from transactions.common.exceptions import InvalidPublicKey
|
||||
|
||||
|
||||
def encode_validator(v):
|
||||
ed25519_public_key = v["public_key"]["value"]
|
||||
pub_key = keys_pb2.PublicKey(ed25519=bytes.fromhex(ed25519_public_key))
|
||||
|
||||
return types_pb2.ValidatorUpdate(pub_key=pub_key, power=v["power"])
|
||||
|
||||
|
||||
def decode_validator(v):
|
||||
return {
|
||||
"public_key": {
|
||||
"type": "ed25519-base64",
|
||||
"value": codecs.encode(v.pub_key.ed25519, "base64").decode().rstrip("\n"),
|
||||
},
|
||||
"voting_power": v.power,
|
||||
}
|
||||
|
||||
|
||||
def new_validator_set(validators, updates):
|
||||
validators_dict = {}
|
||||
for v in validators:
|
||||
validators_dict[v["public_key"]["value"]] = v
|
||||
|
||||
updates_dict = {}
|
||||
for u in updates:
|
||||
decoder = get_public_key_decoder(u["public_key"])
|
||||
public_key64 = base64.b64encode(decoder(u["public_key"]["value"])).decode("utf-8")
|
||||
updates_dict[public_key64] = {
|
||||
"public_key": {"type": "ed25519-base64", "value": public_key64},
|
||||
"voting_power": u["power"],
|
||||
}
|
||||
|
||||
new_validators_dict = {**validators_dict, **updates_dict}
|
||||
return list(new_validators_dict.values())
|
||||
|
||||
|
||||
def get_public_key_decoder(pk):
|
||||
encoding = pk["type"]
|
||||
decoder = base64.b64decode
|
||||
|
||||
if encoding == "ed25519-base16":
|
||||
decoder = base64.b16decode
|
||||
elif encoding == "ed25519-base32":
|
||||
decoder = base64.b32decode
|
||||
elif encoding == "ed25519-base64":
|
||||
decoder = base64.b64decode
|
||||
else:
|
||||
raise InvalidPublicKey("Invalid `type` specified for public key `value`")
|
||||
|
||||
return decoder
|
||||
|
||||
|
||||
def encode_transaction(value):
|
||||
|
@ -1,56 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
# Separate all crypto code so that we can easily test several implementations
|
||||
from collections import namedtuple
|
||||
|
||||
try:
|
||||
from hashlib import sha3_256
|
||||
except ImportError:
|
||||
from sha3 import sha3_256
|
||||
|
||||
from cryptoconditions import crypto
|
||||
|
||||
|
||||
CryptoKeypair = namedtuple("CryptoKeypair", ("private_key", "public_key"))
|
||||
|
||||
|
||||
def hash_data(data):
|
||||
"""Hash the provided data using SHA3-256"""
|
||||
return sha3_256(data.encode()).hexdigest()
|
||||
|
||||
|
||||
def generate_key_pair():
|
||||
"""Generates a cryptographic key pair.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.transactions.common.crypto.CryptoKeypair`: A
|
||||
:obj:`collections.namedtuple` with named fields
|
||||
:attr:`~planetmint.transactions.common.crypto.CryptoKeypair.private_key` and
|
||||
:attr:`~planetmint.transactions.common.crypto.CryptoKeypair.public_key`.
|
||||
|
||||
"""
|
||||
# TODO FOR CC: Adjust interface so that this function becomes unnecessary
|
||||
return CryptoKeypair(*(k.decode() for k in crypto.ed25519_generate_key_pair()))
|
||||
|
||||
|
||||
PrivateKey = crypto.Ed25519SigningKey
|
||||
PublicKey = crypto.Ed25519VerifyingKey
|
||||
|
||||
|
||||
def key_pair_from_ed25519_key(hex_private_key):
|
||||
"""Generate base58 encode public-private key pair from a hex encoded private key"""
|
||||
priv_key = crypto.Ed25519SigningKey(bytes.fromhex(hex_private_key)[:32], encoding="bytes")
|
||||
public_key = priv_key.get_verifying_key()
|
||||
return CryptoKeypair(
|
||||
private_key=priv_key.encode(encoding="base58").decode("utf-8"),
|
||||
public_key=public_key.encode(encoding="base58").decode("utf-8"),
|
||||
)
|
||||
|
||||
|
||||
def public_key_from_ed25519_key(hex_public_key):
|
||||
"""Generate base58 public key from hex encoded public key"""
|
||||
public_key = crypto.Ed25519VerifyingKey(bytes.fromhex(hex_public_key), encoding="bytes")
|
||||
return public_key.encode(encoding="base58").decode("utf-8")
|
@ -1,115 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
"""Custom exceptions used in the `planetmint` package.
|
||||
"""
|
||||
from planetmint.exceptions import BigchainDBError
|
||||
|
||||
|
||||
class ConfigurationError(BigchainDBError):
|
||||
"""Raised when there is a problem with server configuration"""
|
||||
|
||||
|
||||
class DatabaseDoesNotExist(BigchainDBError):
|
||||
"""Raised when trying to delete the database but the db is not there"""
|
||||
|
||||
|
||||
class StartupError(BigchainDBError):
|
||||
"""Raised when there is an error starting up the system"""
|
||||
|
||||
|
||||
class CyclicBlockchainError(BigchainDBError):
|
||||
"""Raised when there is a cycle in the blockchain"""
|
||||
|
||||
|
||||
class KeypairMismatchException(BigchainDBError):
|
||||
"""Raised if the private key(s) provided for signing don't match any of the
|
||||
current owner(s)
|
||||
"""
|
||||
|
||||
|
||||
class OperationError(BigchainDBError):
|
||||
"""Raised when an operation cannot go through"""
|
||||
|
||||
|
||||
################################################################################
|
||||
# Validation errors
|
||||
#
|
||||
# All validation errors (which are handleable errors, not faults) should
|
||||
# subclass ValidationError. However, where possible they should also have their
|
||||
# own distinct type to differentiate them from other validation errors,
|
||||
# especially for the purposes of testing.
|
||||
|
||||
|
||||
class ValidationError(BigchainDBError):
|
||||
"""Raised if there was an error in validation"""
|
||||
|
||||
|
||||
class DoubleSpend(ValidationError):
|
||||
"""Raised if a double spend is found"""
|
||||
|
||||
|
||||
class InvalidHash(ValidationError):
|
||||
"""Raised if there was an error checking the hash for a particular
|
||||
operation
|
||||
"""
|
||||
|
||||
|
||||
class SchemaValidationError(ValidationError):
|
||||
"""Raised if there was any error validating an object's schema"""
|
||||
|
||||
|
||||
class InvalidSignature(ValidationError):
|
||||
"""Raised if there was an error checking the signature for a particular
|
||||
operation
|
||||
"""
|
||||
|
||||
|
||||
class AssetIdMismatch(ValidationError):
|
||||
"""Raised when multiple transaction inputs related to different assets"""
|
||||
|
||||
|
||||
class AmountError(ValidationError):
|
||||
"""Raised when there is a problem with a transaction's output amounts"""
|
||||
|
||||
|
||||
class InputDoesNotExist(ValidationError):
|
||||
"""Raised if a transaction input does not exist"""
|
||||
|
||||
|
||||
class TransactionOwnerError(ValidationError):
|
||||
"""Raised if a user tries to transfer a transaction they don't own"""
|
||||
|
||||
|
||||
class DuplicateTransaction(ValidationError):
|
||||
"""Raised if a duplicated transaction is found"""
|
||||
|
||||
|
||||
class ThresholdTooDeep(ValidationError):
|
||||
"""Raised if threshold condition is too deep"""
|
||||
|
||||
|
||||
class MultipleValidatorOperationError(ValidationError):
|
||||
"""Raised when a validator update pending but new request is submited"""
|
||||
|
||||
|
||||
class MultipleInputsError(ValidationError):
|
||||
"""Raised if there were multiple inputs when only one was expected"""
|
||||
|
||||
|
||||
class InvalidProposer(ValidationError):
|
||||
"""Raised if the public key is not a part of the validator set"""
|
||||
|
||||
|
||||
class UnequalValidatorSet(ValidationError):
|
||||
"""Raised if the validator sets differ"""
|
||||
|
||||
|
||||
class InvalidPowerChange(ValidationError):
|
||||
"""Raised if proposed power change in validator set is >=1/3 total power"""
|
||||
|
||||
|
||||
class InvalidPublicKey(ValidationError):
|
||||
"""Raised if public key doesn't match the encoding type"""
|
@ -1,126 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from cryptoconditions import Fulfillment
|
||||
from cryptoconditions.exceptions import ASN1DecodeError, ASN1EncodeError
|
||||
|
||||
from planetmint.transactions.common.exceptions import InvalidSignature
|
||||
from .utils import _fulfillment_to_details, _fulfillment_from_details
|
||||
from .output import Output
|
||||
from .transaction_link import TransactionLink
|
||||
|
||||
|
||||
class Input(object):
|
||||
"""A Input is used to spend assets locked by an Output.
|
||||
|
||||
Wraps around a Crypto-condition Fulfillment.
|
||||
|
||||
Attributes:
|
||||
fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment
|
||||
to be signed with a private key.
|
||||
owners_before (:obj:`list` of :obj:`str`): A list of owners after a
|
||||
Transaction was confirmed.
|
||||
fulfills (:class:`~planetmint.transactions.common.transaction. TransactionLink`,
|
||||
optional): A link representing the input of a `TRANSFER`
|
||||
Transaction.
|
||||
"""
|
||||
|
||||
def __init__(self, fulfillment, owners_before, fulfills=None):
|
||||
"""Create an instance of an :class:`~.Input`.
|
||||
|
||||
Args:
|
||||
fulfillment (:class:`cryptoconditions.Fulfillment`): A
|
||||
Fulfillment to be signed with a private key.
|
||||
owners_before (:obj:`list` of :obj:`str`): A list of owners
|
||||
after a Transaction was confirmed.
|
||||
fulfills (:class:`~planetmint.transactions.common.transaction.
|
||||
TransactionLink`, optional): A link representing the input
|
||||
of a `TRANSFER` Transaction.
|
||||
"""
|
||||
if fulfills is not None and not isinstance(fulfills, TransactionLink):
|
||||
raise TypeError("`fulfills` must be a TransactionLink instance")
|
||||
if not isinstance(owners_before, list):
|
||||
raise TypeError("`owners_before` must be a list instance")
|
||||
|
||||
self.fulfillment = fulfillment
|
||||
self.fulfills = fulfills
|
||||
self.owners_before = owners_before
|
||||
|
||||
def __eq__(self, other):
|
||||
# TODO: If `other !== Fulfillment` return `False`
|
||||
return self.to_dict() == other.to_dict()
|
||||
|
||||
# NOTE: This function is used to provide a unique key for a given
|
||||
# Input to suppliment memoization
|
||||
def __hash__(self):
|
||||
return hash((self.fulfillment, self.fulfills))
|
||||
|
||||
def to_dict(self):
|
||||
"""Transforms the object to a Python dictionary.
|
||||
|
||||
Note:
|
||||
If an Input hasn't been signed yet, this method returns a
|
||||
dictionary representation.
|
||||
|
||||
Returns:
|
||||
dict: The Input as an alternative serialization format.
|
||||
"""
|
||||
try:
|
||||
fulfillment = self.fulfillment.serialize_uri()
|
||||
except (TypeError, AttributeError, ASN1EncodeError, ASN1DecodeError):
|
||||
fulfillment = _fulfillment_to_details(self.fulfillment)
|
||||
|
||||
try:
|
||||
# NOTE: `self.fulfills` can be `None` and that's fine
|
||||
fulfills = self.fulfills.to_dict()
|
||||
except AttributeError:
|
||||
fulfills = None
|
||||
|
||||
input_ = {
|
||||
"owners_before": self.owners_before,
|
||||
"fulfills": fulfills,
|
||||
"fulfillment": fulfillment,
|
||||
}
|
||||
return input_
|
||||
|
||||
@classmethod
|
||||
def generate(cls, public_keys):
|
||||
# TODO: write docstring
|
||||
# The amount here does not really matter. It is only use on the
|
||||
# output data model but here we only care about the fulfillment
|
||||
output = Output.generate(public_keys, 1)
|
||||
return cls(output.fulfillment, public_keys)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""Transforms a Python dictionary to an Input object.
|
||||
|
||||
Note:
|
||||
Optionally, this method can also serialize a Cryptoconditions-
|
||||
Fulfillment that is not yet signed.
|
||||
|
||||
Args:
|
||||
data (dict): The Input to be transformed.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.transactions.common.transaction.Input`
|
||||
|
||||
Raises:
|
||||
InvalidSignature: If an Input's URI couldn't be parsed.
|
||||
"""
|
||||
fulfillment = data["fulfillment"]
|
||||
if not isinstance(fulfillment, (Fulfillment, type(None))):
|
||||
try:
|
||||
fulfillment = Fulfillment.from_uri(data["fulfillment"])
|
||||
except ASN1DecodeError:
|
||||
# TODO Remove as it is legacy code, and simply fall back on
|
||||
# ASN1DecodeError
|
||||
raise InvalidSignature("Fulfillment URI couldn't been parsed")
|
||||
except TypeError:
|
||||
# NOTE: See comment about this special case in
|
||||
# `Input.to_dict`
|
||||
fulfillment = _fulfillment_from_details(data["fulfillment"])
|
||||
fulfills = TransactionLink.from_dict(data["fulfills"])
|
||||
return cls(fulfillment, data["owners_before"], fulfills)
|
@ -1,57 +0,0 @@
|
||||
import functools
|
||||
import codecs
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class HDict(dict):
|
||||
def __hash__(self):
|
||||
return hash(codecs.decode(self["id"], "hex"))
|
||||
|
||||
|
||||
@lru_cache(maxsize=16384)
|
||||
def from_dict(func, *args, **kwargs):
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
def memoize_from_dict(func):
|
||||
@functools.wraps(func)
|
||||
def memoized_func(*args, **kwargs):
|
||||
if args[1] is None:
|
||||
return None
|
||||
elif args[1].get("id", None):
|
||||
args = list(args)
|
||||
args[1] = HDict(args[1])
|
||||
new_args = tuple(args)
|
||||
return from_dict(func, *new_args, **kwargs)
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return memoized_func
|
||||
|
||||
|
||||
class ToDictWrapper:
|
||||
def __init__(self, tx):
|
||||
self.tx = tx
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.tx.id == other.tx.id
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.tx.id)
|
||||
|
||||
|
||||
@lru_cache(maxsize=16384)
|
||||
def to_dict(func, tx_wrapped):
|
||||
return func(tx_wrapped.tx)
|
||||
|
||||
|
||||
def memoize_to_dict(func):
|
||||
@functools.wraps(func)
|
||||
def memoized_func(*args, **kwargs):
|
||||
|
||||
if args[0].id:
|
||||
return to_dict(func, ToDictWrapper(args[0]))
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return memoized_func
|
@ -1,209 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import base58
|
||||
from cryptoconditions import ThresholdSha256, Ed25519Sha256, ZenroomSha256
|
||||
from cryptoconditions import Fulfillment
|
||||
|
||||
from planetmint.transactions.common.exceptions import AmountError
|
||||
from .utils import _fulfillment_to_details, _fulfillment_from_details
|
||||
|
||||
|
||||
class Output(object):
|
||||
"""An Output is used to lock an asset.
|
||||
|
||||
Wraps around a Crypto-condition Condition.
|
||||
|
||||
Attributes:
|
||||
fulfillment (:class:`cryptoconditions.Fulfillment`): A Fulfillment
|
||||
to extract a Condition from.
|
||||
public_keys (:obj:`list` of :obj:`str`, optional): A list of
|
||||
owners before a Transaction was confirmed.
|
||||
"""
|
||||
|
||||
MAX_AMOUNT = 9 * 10**18
|
||||
|
||||
def __init__(self, fulfillment, public_keys=None, amount=1):
|
||||
"""Create an instance of a :class:`~.Output`.
|
||||
|
||||
Args:
|
||||
fulfillment (:class:`cryptoconditions.Fulfillment`): A
|
||||
Fulfillment to extract a Condition from.
|
||||
public_keys (:obj:`list` of :obj:`str`, optional): A list of
|
||||
owners before a Transaction was confirmed.
|
||||
amount (int): The amount of Assets to be locked with this
|
||||
Output.
|
||||
|
||||
Raises:
|
||||
TypeError: if `public_keys` is not instance of `list`.
|
||||
"""
|
||||
if not isinstance(public_keys, list) and public_keys is not None:
|
||||
raise TypeError("`public_keys` must be a list instance or None")
|
||||
if not isinstance(amount, int):
|
||||
raise TypeError("`amount` must be an int")
|
||||
if amount < 1:
|
||||
raise AmountError("`amount` must be greater than 0")
|
||||
if amount > self.MAX_AMOUNT:
|
||||
raise AmountError("`amount` must be <= %s" % self.MAX_AMOUNT)
|
||||
|
||||
self.fulfillment = fulfillment
|
||||
self.amount = amount
|
||||
self.public_keys = public_keys
|
||||
|
||||
def __eq__(self, other):
|
||||
# TODO: If `other !== Condition` return `False`
|
||||
return self.to_dict() == other.to_dict()
|
||||
|
||||
def to_dict(self):
|
||||
"""Transforms the object to a Python dictionary.
|
||||
|
||||
Note:
|
||||
A dictionary serialization of the Input the Output was
|
||||
derived from is always provided.
|
||||
|
||||
Returns:
|
||||
dict: The Output as an alternative serialization format.
|
||||
"""
|
||||
# TODO FOR CC: It must be able to recognize a hashlock condition
|
||||
# and fulfillment!
|
||||
condition = {}
|
||||
try:
|
||||
# TODO verify if a script is returned in case of zenroom fulfillments
|
||||
condition["details"] = _fulfillment_to_details(self.fulfillment)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
try:
|
||||
condition["uri"] = self.fulfillment.condition_uri
|
||||
except AttributeError:
|
||||
condition["uri"] = self.fulfillment
|
||||
|
||||
output = {
|
||||
"public_keys": self.public_keys,
|
||||
"condition": condition,
|
||||
"amount": str(self.amount),
|
||||
}
|
||||
return output
|
||||
|
||||
@classmethod
|
||||
def generate(cls, public_keys, amount):
|
||||
"""Generates a Output from a specifically formed tuple or list.
|
||||
|
||||
Note:
|
||||
If a ThresholdCondition has to be generated where the threshold
|
||||
is always the number of subconditions it is split between, a
|
||||
list of the following structure is sufficient:
|
||||
|
||||
[(address|condition)*, [(address|condition)*, ...], ...]
|
||||
|
||||
Args:
|
||||
public_keys (:obj:`list` of :obj:`str`): The public key of
|
||||
the users that should be able to fulfill the Condition
|
||||
that is being created.
|
||||
amount (:obj:`int`): The amount locked by the Output.
|
||||
|
||||
Returns:
|
||||
An Output that can be used in a Transaction.
|
||||
|
||||
Raises:
|
||||
TypeError: If `public_keys` is not an instance of `list`.
|
||||
ValueError: If `public_keys` is an empty list.
|
||||
"""
|
||||
threshold = len(public_keys)
|
||||
if not isinstance(amount, int):
|
||||
raise TypeError("`amount` must be a int")
|
||||
if amount < 1:
|
||||
raise AmountError("`amount` needs to be greater than zero")
|
||||
if not isinstance(public_keys, list):
|
||||
raise TypeError("`public_keys` must be an instance of list")
|
||||
if len(public_keys) == 0:
|
||||
raise ValueError("`public_keys` needs to contain at least one" "owner")
|
||||
elif len(public_keys) == 1 and not isinstance(public_keys[0], list):
|
||||
if isinstance(public_keys[0], Fulfillment):
|
||||
ffill = public_keys[0]
|
||||
elif isinstance(public_keys[0], ZenroomSha256):
|
||||
ffill = ZenroomSha256(public_key=base58.b58decode(public_keys[0]))
|
||||
else:
|
||||
ffill = Ed25519Sha256(public_key=base58.b58decode(public_keys[0]))
|
||||
return cls(ffill, public_keys, amount=amount)
|
||||
else:
|
||||
initial_cond = ThresholdSha256(threshold=threshold)
|
||||
threshold_cond = reduce(cls._gen_condition, public_keys, initial_cond)
|
||||
return cls(threshold_cond, public_keys, amount=amount)
|
||||
|
||||
@classmethod
|
||||
def _gen_condition(cls, initial, new_public_keys):
|
||||
"""Generates ThresholdSha256 conditions from a list of new owners.
|
||||
|
||||
Note:
|
||||
This method is intended only to be used with a reduce function.
|
||||
For a description on how to use this method, see
|
||||
:meth:`~.Output.generate`.
|
||||
|
||||
Args:
|
||||
initial (:class:`cryptoconditions.ThresholdSha256`):
|
||||
A Condition representing the overall root.
|
||||
new_public_keys (:obj:`list` of :obj:`str`|str): A list of new
|
||||
owners or a single new owner.
|
||||
|
||||
Returns:
|
||||
:class:`cryptoconditions.ThresholdSha256`:
|
||||
"""
|
||||
try:
|
||||
threshold = len(new_public_keys)
|
||||
except TypeError:
|
||||
threshold = None
|
||||
|
||||
if isinstance(new_public_keys, list) and len(new_public_keys) > 1:
|
||||
ffill = ThresholdSha256(threshold=threshold)
|
||||
reduce(cls._gen_condition, new_public_keys, ffill)
|
||||
elif isinstance(new_public_keys, list) and len(new_public_keys) <= 1:
|
||||
raise ValueError("Sublist cannot contain single owner")
|
||||
else:
|
||||
try:
|
||||
new_public_keys = new_public_keys.pop()
|
||||
except AttributeError:
|
||||
pass
|
||||
# NOTE: Instead of submitting base58 encoded addresses, a user
|
||||
# of this class can also submit fully instantiated
|
||||
# Cryptoconditions. In the case of casting
|
||||
# `new_public_keys` to a Ed25519Fulfillment with the
|
||||
# result of a `TypeError`, we're assuming that
|
||||
# `new_public_keys` is a Cryptocondition then.
|
||||
if isinstance(new_public_keys, Fulfillment):
|
||||
ffill = new_public_keys
|
||||
else:
|
||||
ffill = Ed25519Sha256(public_key=base58.b58decode(new_public_keys))
|
||||
initial.add_subfulfillment(ffill)
|
||||
return initial
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
"""Transforms a Python dictionary to an Output object.
|
||||
|
||||
Note:
|
||||
To pass a serialization cycle multiple times, a
|
||||
Cryptoconditions Fulfillment needs to be present in the
|
||||
passed-in dictionary, as Condition URIs are not serializable
|
||||
anymore.
|
||||
|
||||
Args:
|
||||
data (dict): The dict to be transformed.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.transactions.common.transaction.Output`
|
||||
"""
|
||||
try:
|
||||
fulfillment = _fulfillment_from_details(data["condition"]["details"])
|
||||
except KeyError:
|
||||
# NOTE: Hashlock condition case
|
||||
fulfillment = data["condition"]["uri"]
|
||||
try:
|
||||
amount = int(data["amount"])
|
||||
except ValueError:
|
||||
raise AmountError("Invalid amount: %s" % data["amount"])
|
||||
return cls(fulfillment, data["public_keys"], amount)
|
@ -1,54 +0,0 @@
|
||||
<!---
|
||||
Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
Planetmint and IPDB software contributors.
|
||||
SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
--->
|
||||
|
||||
# Introduction
|
||||
|
||||
This directory contains the schemas for the different JSON documents Planetmint uses.
|
||||
|
||||
The aim is to provide:
|
||||
|
||||
- a strict definition of the data structures used in Planetmint,
|
||||
- a language-independent tool to validate the structure of incoming/outcoming
|
||||
data. (There are several ready to use
|
||||
[implementations](http://json-schema.org/implementations.html) written in
|
||||
different languages.)
|
||||
|
||||
## Sources
|
||||
|
||||
The files defining the JSON Schema for transactions (`transaction_*.yaml`)
|
||||
are based on the [Planetmint Transactions Specs](https://github.com/planetmint/BEPs/tree/master/tx-specs).
|
||||
If you want to add a new transaction version,
|
||||
you must write a spec for it first.
|
||||
(You can't change the JSON Schema files for old versions.
|
||||
Those were used to validate old transactions
|
||||
and are needed to re-check those transactions.)
|
||||
|
||||
There used to be a file defining the JSON Schema for votes, named `vote.yaml`.
|
||||
It was used by Planetmint version 1.3.0 and earlier.
|
||||
If you want a copy of the latest `vote.yaml` file,
|
||||
then you can get it from the version 1.3.0 release on GitHub, at
|
||||
[https://github.com/planetmint/planetmint/blob/v1.3.0/planetmint/common/schema/vote.yaml](https://github.com/planetmint/planetmint/blob/v1.3.0/planetmint/common/schema/vote.yaml).
|
||||
|
||||
## Learn about JSON Schema
|
||||
|
||||
A good resource is [Understanding JSON Schema](http://spacetelescope.github.io/understanding-json-schema/index.html).
|
||||
It provides a *more accessible documentation for JSON schema* than the [specs](http://json-schema.org/documentation.html).
|
||||
|
||||
## If it's supposed to be JSON, why's everything in YAML D:?
|
||||
|
||||
YAML is great for its conciseness and friendliness towards human-editing in comparision to JSON.
|
||||
|
||||
Although YAML is a superset of JSON, at the end of the day, JSON Schema processors, like
|
||||
[json-schema](http://python-jsonschema.readthedocs.io/en/latest/), take in a native object (e.g.
|
||||
Python dicts or JavaScript objects) as the schema used for validation. As long as we can serialize
|
||||
the YAML into what the JSON Schema processor expects (almost always as simple as loading the YAML
|
||||
like you would with a JSON file), it's the same as using JSON.
|
||||
|
||||
Specific advantages of using YAML:
|
||||
- Legibility, especially when nesting
|
||||
- Multi-line string literals, that make it easy to include descriptions that can be [auto-generated
|
||||
into Sphinx documentation](/docs/server/generate_schema_documentation.py)
|
@ -1,78 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
"""Schema validation related functions and data"""
|
||||
import os.path
|
||||
import logging
|
||||
|
||||
import jsonschema
|
||||
import yaml
|
||||
import rapidjson
|
||||
|
||||
from planetmint.transactions.common.exceptions import SchemaValidationError
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_schema(name, version, path=__file__):
|
||||
"""Load a schema from disk"""
|
||||
path = os.path.join(os.path.dirname(path), version, name + ".yaml")
|
||||
with open(path) as handle:
|
||||
schema = yaml.safe_load(handle)
|
||||
fast_schema = rapidjson.Validator(rapidjson.dumps(schema))
|
||||
return path, (schema, fast_schema)
|
||||
|
||||
|
||||
# TODO: make this an env var from a config file
|
||||
TX_SCHEMA_VERSION = "v2.0"
|
||||
|
||||
TX_SCHEMA_PATH, TX_SCHEMA_COMMON = _load_schema("transaction", TX_SCHEMA_VERSION)
|
||||
_, TX_SCHEMA_CREATE = _load_schema("transaction_create", TX_SCHEMA_VERSION)
|
||||
_, TX_SCHEMA_TRANSFER = _load_schema("transaction_transfer", TX_SCHEMA_VERSION)
|
||||
|
||||
_, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema("transaction_validator_election", TX_SCHEMA_VERSION)
|
||||
|
||||
_, TX_SCHEMA_CHAIN_MIGRATION_ELECTION = _load_schema("transaction_chain_migration_election", TX_SCHEMA_VERSION)
|
||||
|
||||
_, TX_SCHEMA_VOTE = _load_schema("transaction_vote", TX_SCHEMA_VERSION)
|
||||
|
||||
|
||||
def _validate_schema(schema, body):
|
||||
"""Validate data against a schema"""
|
||||
|
||||
# Note
|
||||
#
|
||||
# Schema validation is currently the major CPU bottleneck of
|
||||
# Planetmint. the `jsonschema` library validates python data structures
|
||||
# directly and produces nice error messages, but validation takes 4+ ms
|
||||
# per transaction which is pretty slow. The rapidjson library validates
|
||||
# much faster at 1.5ms, however it produces _very_ poor error messages.
|
||||
# For this reason we use both, rapidjson as an optimistic pathway and
|
||||
# jsonschema as a fallback in case there is a failure, so we can produce
|
||||
# a helpful error message.
|
||||
|
||||
try:
|
||||
schema[1](rapidjson.dumps(body))
|
||||
except ValueError as exc:
|
||||
try:
|
||||
jsonschema.validate(body, schema[0])
|
||||
except jsonschema.ValidationError as exc2:
|
||||
raise SchemaValidationError(str(exc2)) from exc2
|
||||
logger.warning("code problem: jsonschema did not raise an exception, wheras rapidjson raised %s", exc)
|
||||
raise SchemaValidationError(str(exc)) from exc
|
||||
|
||||
|
||||
def validate_transaction_schema(tx):
|
||||
"""Validate a transaction dict.
|
||||
|
||||
TX_SCHEMA_COMMON contains properties that are common to all types of
|
||||
transaction. TX_SCHEMA_[TRANSFER|CREATE] add additional constraints on top.
|
||||
"""
|
||||
_validate_schema(TX_SCHEMA_COMMON, tx)
|
||||
if tx["operation"] == "TRANSFER":
|
||||
_validate_schema(TX_SCHEMA_TRANSFER, tx)
|
||||
else:
|
||||
_validate_schema(TX_SCHEMA_CREATE, tx)
|
@ -1,168 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
additionalProperties: false
|
||||
title: Transaction Schema
|
||||
required:
|
||||
- id
|
||||
- inputs
|
||||
- outputs
|
||||
- operation
|
||||
- metadata
|
||||
- asset
|
||||
- version
|
||||
properties:
|
||||
id:
|
||||
anyOf:
|
||||
- "$ref": "#/definitions/sha3_hexdigest"
|
||||
- type: 'null'
|
||||
operation:
|
||||
"$ref": "#/definitions/operation"
|
||||
asset:
|
||||
"$ref": "#/definitions/asset"
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
items:
|
||||
"$ref": "#/definitions/input"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
metadata:
|
||||
"$ref": "#/definitions/metadata"
|
||||
version:
|
||||
type: string
|
||||
pattern: "^1\\.0$"
|
||||
definitions:
|
||||
offset:
|
||||
type: integer
|
||||
minimum: 0
|
||||
base58:
|
||||
pattern: "[1-9a-zA-Z^OIl]{43,44}"
|
||||
type: string
|
||||
public_keys:
|
||||
anyOf:
|
||||
- type: array
|
||||
items:
|
||||
"$ref": "#/definitions/base58"
|
||||
- type: 'null'
|
||||
sha3_hexdigest:
|
||||
pattern: "[0-9a-f]{64}"
|
||||
type: string
|
||||
uuid4:
|
||||
pattern: "[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}"
|
||||
type: string
|
||||
operation:
|
||||
type: string
|
||||
enum:
|
||||
- CREATE
|
||||
- TRANSFER
|
||||
- GENESIS
|
||||
asset:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
data:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
- type: 'null'
|
||||
output:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- amount
|
||||
- condition
|
||||
- public_keys
|
||||
properties:
|
||||
amount:
|
||||
type: string
|
||||
pattern: "^[0-9]{1,20}$"
|
||||
condition:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- details
|
||||
- uri
|
||||
properties:
|
||||
details:
|
||||
"$ref": "#/definitions/condition_details"
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=(ed25519|threshold)-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
||||
public_keys:
|
||||
"$ref": "#/definitions/public_keys"
|
||||
input:
|
||||
type: "object"
|
||||
additionalProperties: false
|
||||
required:
|
||||
- owners_before
|
||||
- fulfillment
|
||||
properties:
|
||||
owners_before:
|
||||
"$ref": "#/definitions/public_keys"
|
||||
fulfillment:
|
||||
anyOf:
|
||||
- type: string
|
||||
pattern: "^[a-zA-Z0-9_-]*$"
|
||||
- "$ref": "#/definitions/condition_details"
|
||||
fulfills:
|
||||
anyOf:
|
||||
- type: 'object'
|
||||
additionalProperties: false
|
||||
required:
|
||||
- output_index
|
||||
- transaction_id
|
||||
properties:
|
||||
output_index:
|
||||
"$ref": "#/definitions/offset"
|
||||
transaction_id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
- type: 'null'
|
||||
metadata:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
minProperties: 1
|
||||
- type: 'null'
|
||||
condition_details:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- public_key
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
pattern: "^ed25519-sha-256$"
|
||||
public_key:
|
||||
"$ref": "#/definitions/base58"
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- threshold
|
||||
- subconditions
|
||||
properties:
|
||||
type:
|
||||
type: "string"
|
||||
pattern: "^threshold-sha-256$"
|
||||
threshold:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
subconditions:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/condition_details"
|
@ -1,35 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Transaction Schema - CREATE/GENESIS specific constraints
|
||||
required:
|
||||
- asset
|
||||
- inputs
|
||||
properties:
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: true
|
||||
- type: 'null'
|
||||
required:
|
||||
- data
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
maxItems: 1
|
||||
minItems: 1
|
||||
items:
|
||||
type: "object"
|
||||
required:
|
||||
- fulfills
|
||||
properties:
|
||||
fulfills:
|
||||
type: "null"
|
@ -1,34 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Transaction Schema - TRANSFER specific properties
|
||||
required:
|
||||
- asset
|
||||
properties:
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
required:
|
||||
- id
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
minItems: 1
|
||||
items:
|
||||
type: "object"
|
||||
required:
|
||||
- fulfills
|
||||
properties:
|
||||
fulfills:
|
||||
type: "object"
|
||||
definitions:
|
||||
sha3_hexdigest:
|
||||
pattern: "[0-9a-f]{64}"
|
||||
type: string
|
@ -1,215 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
additionalProperties: false
|
||||
title: Transaction Schema
|
||||
required:
|
||||
- id
|
||||
- inputs
|
||||
- outputs
|
||||
- operation
|
||||
- metadata
|
||||
- asset
|
||||
- version
|
||||
properties:
|
||||
id:
|
||||
anyOf:
|
||||
- "$ref": "#/definitions/sha3_hexdigest"
|
||||
- type: 'null'
|
||||
operation:
|
||||
"$ref": "#/definitions/operation"
|
||||
asset:
|
||||
"$ref": "#/definitions/asset"
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
items:
|
||||
"$ref": "#/definitions/input"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
metadata:
|
||||
"$ref": "#/definitions/metadata"
|
||||
version:
|
||||
type: string
|
||||
pattern: "^2\\.0$"
|
||||
script:
|
||||
"$ref": "#/definitions/script"
|
||||
definitions:
|
||||
offset:
|
||||
type: integer
|
||||
minimum: 0
|
||||
base58:
|
||||
pattern: "[1-9a-zA-Z^OIl]{43,44}"
|
||||
type: string
|
||||
public_keys:
|
||||
anyOf:
|
||||
- type: array
|
||||
items:
|
||||
"$ref": "#/definitions/base58"
|
||||
- type: 'null'
|
||||
sha3_hexdigest:
|
||||
pattern: "[0-9a-f]{64}"
|
||||
type: string
|
||||
uuid4:
|
||||
pattern: "[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}"
|
||||
type: string
|
||||
operation:
|
||||
type: string
|
||||
enum:
|
||||
- CREATE
|
||||
- TRANSFER
|
||||
- VALIDATOR_ELECTION
|
||||
- CHAIN_MIGRATION_ELECTION
|
||||
- VOTE
|
||||
asset:
|
||||
anyOf:
|
||||
- type: 'null'
|
||||
- type: object
|
||||
output:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- amount
|
||||
- condition
|
||||
- public_keys
|
||||
properties:
|
||||
amount:
|
||||
type: string
|
||||
pattern: "^[0-9]{1,20}$"
|
||||
condition:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- details
|
||||
- uri
|
||||
properties:
|
||||
details:
|
||||
"$ref": "#/definitions/condition_details"
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=(ed25519|threshold|zenroom)-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=(ed25519|zenroom)-sha-256(&)?){2,3}$"
|
||||
public_keys:
|
||||
"$ref": "#/definitions/public_keys"
|
||||
input:
|
||||
type: "object"
|
||||
additionalProperties: false
|
||||
required:
|
||||
- owners_before
|
||||
- fulfillment
|
||||
properties:
|
||||
owners_before:
|
||||
"$ref": "#/definitions/public_keys"
|
||||
fulfillment:
|
||||
anyOf:
|
||||
- type: string
|
||||
pattern: "^[a-zA-Z0-9_-]*$"
|
||||
- "$ref": "#/definitions/condition_details"
|
||||
fulfills:
|
||||
anyOf:
|
||||
- type: 'object'
|
||||
additionalProperties: false
|
||||
required:
|
||||
- output_index
|
||||
- transaction_id
|
||||
properties:
|
||||
output_index:
|
||||
"$ref": "#/definitions/offset"
|
||||
transaction_id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
- type: 'null'
|
||||
metadata:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
condition_details:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- public_key
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
pattern: "^(ed25519|zenroom)-sha-256$"
|
||||
public_key:
|
||||
"$ref": "#/definitions/base58"
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- threshold
|
||||
- subconditions
|
||||
properties:
|
||||
type:
|
||||
type: "string"
|
||||
pattern: "^threshold-sha-256$"
|
||||
threshold:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
subconditions:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/condition_details"
|
||||
script:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- code
|
||||
- state
|
||||
- input
|
||||
- output
|
||||
properties:
|
||||
code:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- raw
|
||||
- parameters
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- zenroom
|
||||
raw:
|
||||
type: string
|
||||
parameters:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- transaction_id
|
||||
properties:
|
||||
transaction_id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
state:
|
||||
anyOf:
|
||||
- type: object
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
input:
|
||||
type: object
|
||||
output:
|
||||
anyOf:
|
||||
- type: object
|
||||
- type: array
|
||||
policies:
|
||||
type: object
|
||||
properties:
|
||||
raw:
|
||||
type: object
|
||||
txids:
|
||||
type: object
|
@ -1,45 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Chain Migration Election Schema - Propose a halt in block production to allow for a version change
|
||||
required:
|
||||
- operation
|
||||
- asset
|
||||
- outputs
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
value: "CHAIN_MIGRATION_ELECTION"
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
seed:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
definitions:
|
||||
output:
|
||||
type: object
|
||||
properties:
|
||||
condition:
|
||||
type: object
|
||||
required:
|
||||
- uri
|
||||
properties:
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
@ -1,34 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Transaction Schema - CREATE specific constraints
|
||||
required:
|
||||
- asset
|
||||
- inputs
|
||||
properties:
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
required:
|
||||
- data
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
maxItems: 1
|
||||
minItems: 1
|
||||
items:
|
||||
type: "object"
|
||||
required:
|
||||
- fulfills
|
||||
properties:
|
||||
fulfills:
|
||||
type: "null"
|
@ -1,34 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Transaction Schema - TRANSFER specific properties
|
||||
required:
|
||||
- asset
|
||||
properties:
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
required:
|
||||
- id
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
minItems: 1
|
||||
items:
|
||||
type: "object"
|
||||
required:
|
||||
- fulfills
|
||||
properties:
|
||||
fulfills:
|
||||
type: "object"
|
||||
definitions:
|
||||
sha3_hexdigest:
|
||||
pattern: "[0-9a-f]{64}"
|
||||
type: string
|
@ -1,68 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Validator Election Schema - Propose a change to validator set
|
||||
required:
|
||||
- operation
|
||||
- asset
|
||||
- outputs
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
value: "VALIDATOR_ELECTION"
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
node_id:
|
||||
type: string
|
||||
seed:
|
||||
type: string
|
||||
public_key:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- value
|
||||
- type
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- ed25519-base16
|
||||
- ed25519-base32
|
||||
- ed25519-base64
|
||||
power:
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
required:
|
||||
- node_id
|
||||
- public_key
|
||||
- power
|
||||
required:
|
||||
- data
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
definitions:
|
||||
output:
|
||||
type: object
|
||||
properties:
|
||||
condition:
|
||||
type: object
|
||||
required:
|
||||
- uri
|
||||
properties:
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
@ -1,34 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Vote Schema - Vote on an election
|
||||
required:
|
||||
- operation
|
||||
- outputs
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
value: "VOTE"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
definitions:
|
||||
output:
|
||||
type: object
|
||||
properties:
|
||||
condition:
|
||||
type: object
|
||||
required:
|
||||
- uri
|
||||
properties:
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
@ -1,219 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
additionalProperties: false
|
||||
title: Transaction Schema
|
||||
required:
|
||||
- id
|
||||
- inputs
|
||||
- outputs
|
||||
- operation
|
||||
- metadata
|
||||
- assets
|
||||
- version
|
||||
properties:
|
||||
id:
|
||||
anyOf:
|
||||
- "$ref": "#/definitions/sha3_hexdigest"
|
||||
- type: 'null'
|
||||
operation:
|
||||
"$ref": "#/definitions/operation"
|
||||
assets:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/asset"
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
items:
|
||||
"$ref": "#/definitions/input"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
metadata:
|
||||
"$ref": "#/definitions/metadata"
|
||||
version:
|
||||
type: string
|
||||
pattern: "^3\\.0$"
|
||||
script:
|
||||
"$ref": "#/definitions/script"
|
||||
definitions:
|
||||
offset:
|
||||
type: integer
|
||||
minimum: 0
|
||||
base58:
|
||||
pattern: "[1-9a-zA-Z^OIl]{43,44}"
|
||||
type: string
|
||||
public_keys:
|
||||
anyOf:
|
||||
- type: array
|
||||
items:
|
||||
"$ref": "#/definitions/base58"
|
||||
- type: 'null'
|
||||
sha3_hexdigest:
|
||||
pattern: "[0-9a-f]{64}"
|
||||
type: string
|
||||
uuid4:
|
||||
pattern: "[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}"
|
||||
type: string
|
||||
operation:
|
||||
type: string
|
||||
enum:
|
||||
- CREATE
|
||||
- TRANSFER
|
||||
- VALIDATOR_ELECTION
|
||||
- CHAIN_MIGRATION_ELECTION
|
||||
- VOTE
|
||||
- COMPOSE
|
||||
- DECOMPOSE
|
||||
asset:
|
||||
anyOf:
|
||||
- type: 'null'
|
||||
- type: object
|
||||
output:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- amount
|
||||
- condition
|
||||
- public_keys
|
||||
properties:
|
||||
amount:
|
||||
type: string
|
||||
pattern: "^[0-9]{1,20}$"
|
||||
condition:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- details
|
||||
- uri
|
||||
properties:
|
||||
details:
|
||||
"$ref": "#/definitions/condition_details"
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=(ed25519|threshold)-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
||||
public_keys:
|
||||
"$ref": "#/definitions/public_keys"
|
||||
input:
|
||||
type: "object"
|
||||
additionalProperties: false
|
||||
required:
|
||||
- owners_before
|
||||
- fulfillment
|
||||
properties:
|
||||
owners_before:
|
||||
"$ref": "#/definitions/public_keys"
|
||||
fulfillment:
|
||||
anyOf:
|
||||
- type: string
|
||||
pattern: "^[a-zA-Z0-9_-]*$"
|
||||
- "$ref": "#/definitions/condition_details"
|
||||
fulfills:
|
||||
anyOf:
|
||||
- type: 'object'
|
||||
additionalProperties: false
|
||||
required:
|
||||
- output_index
|
||||
- transaction_id
|
||||
properties:
|
||||
output_index:
|
||||
"$ref": "#/definitions/offset"
|
||||
transaction_id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
- type: 'null'
|
||||
metadata:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
condition_details:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- public_key
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
pattern: "^ed25519-sha-256$"
|
||||
public_key:
|
||||
"$ref": "#/definitions/base58"
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- threshold
|
||||
- subconditions
|
||||
properties:
|
||||
type:
|
||||
type: "string"
|
||||
pattern: "^threshold-sha-256$"
|
||||
threshold:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
subconditions:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/condition_details"
|
||||
script:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- code
|
||||
- state
|
||||
- input
|
||||
- output
|
||||
properties:
|
||||
code:
|
||||
anyOf:
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- type
|
||||
- raw
|
||||
- parameters
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- zenroom
|
||||
raw:
|
||||
type: string
|
||||
parameters:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
- type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- transaction_id
|
||||
properties:
|
||||
transaction_id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
state:
|
||||
anyOf:
|
||||
- type: object
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
input:
|
||||
type: object
|
||||
output:
|
||||
anyOf:
|
||||
- type: object
|
||||
- type: array
|
||||
policies:
|
||||
type: object
|
||||
properties:
|
||||
raw:
|
||||
type: object
|
||||
txids:
|
||||
type: object
|
@ -1,51 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Chain Migration Election Schema - Propose a halt in block production to allow for a version change
|
||||
required:
|
||||
- operation
|
||||
- assets
|
||||
- outputs
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
value: "CHAIN_MIGRATION_ELECTION"
|
||||
assets:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 1
|
||||
items:
|
||||
"$ref": "#/definitions/asset"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
definitions:
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
seed:
|
||||
type: string
|
||||
required:
|
||||
- data
|
||||
output:
|
||||
type: object
|
||||
properties:
|
||||
condition:
|
||||
type: object
|
||||
required:
|
||||
- uri
|
||||
properties:
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
@ -1,41 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Transaction Schema - CREATE specific constraints
|
||||
required:
|
||||
- assets
|
||||
- inputs
|
||||
properties:
|
||||
assets:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 1
|
||||
items:
|
||||
"$ref": "#/definitions/asset"
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
maxItems: 1
|
||||
minItems: 1
|
||||
items:
|
||||
type: "object"
|
||||
required:
|
||||
- fulfills
|
||||
properties:
|
||||
fulfills:
|
||||
type: "null"
|
||||
definitions:
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: 'null'
|
||||
required:
|
||||
- data
|
@ -1,39 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Transaction Schema - TRANSFER specific properties
|
||||
required:
|
||||
- assets
|
||||
properties:
|
||||
assets:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
"$ref": "#/definitions/asset"
|
||||
inputs:
|
||||
type: array
|
||||
title: "Transaction inputs"
|
||||
minItems: 1
|
||||
items:
|
||||
type: "object"
|
||||
required:
|
||||
- fulfills
|
||||
properties:
|
||||
fulfills:
|
||||
type: "object"
|
||||
definitions:
|
||||
sha3_hexdigest:
|
||||
pattern: "[0-9a-f]{64}"
|
||||
type: string
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
id:
|
||||
"$ref": "#/definitions/sha3_hexdigest"
|
||||
required:
|
||||
- id
|
@ -1,74 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Validator Election Schema - Propose a change to validator set
|
||||
required:
|
||||
- operation
|
||||
- assets
|
||||
- outputs
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
value: "VALIDATOR_ELECTION"
|
||||
assets:
|
||||
type: array
|
||||
minItems: 1
|
||||
maxItems: 1
|
||||
items:
|
||||
"$ref": "#/definitions/asset"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
definitions:
|
||||
output:
|
||||
type: object
|
||||
properties:
|
||||
condition:
|
||||
type: object
|
||||
required:
|
||||
- uri
|
||||
properties:
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
||||
asset:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
data:
|
||||
additionalProperties: false
|
||||
properties:
|
||||
node_id:
|
||||
type: string
|
||||
seed:
|
||||
type: string
|
||||
public_key:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required:
|
||||
- value
|
||||
- type
|
||||
properties:
|
||||
value:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum:
|
||||
- ed25519-base16
|
||||
- ed25519-base32
|
||||
- ed25519-base64
|
||||
power:
|
||||
"$ref": "#/definitions/positiveInteger"
|
||||
required:
|
||||
- node_id
|
||||
- public_key
|
||||
- power
|
||||
required:
|
||||
- data
|
@ -1,34 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
---
|
||||
"$schema": "http://json-schema.org/draft-04/schema#"
|
||||
type: object
|
||||
title: Vote Schema - Vote on an election
|
||||
required:
|
||||
- operation
|
||||
- outputs
|
||||
properties:
|
||||
operation:
|
||||
type: string
|
||||
value: "VOTE"
|
||||
outputs:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/definitions/output"
|
||||
definitions:
|
||||
output:
|
||||
type: object
|
||||
properties:
|
||||
condition:
|
||||
type: object
|
||||
required:
|
||||
- uri
|
||||
properties:
|
||||
uri:
|
||||
type: string
|
||||
pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\
|
||||
(fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\
|
||||
subtypes=ed25519-sha-256(&)?){2,3}$"
|
@ -1,939 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
"""Transaction related models to parse and construct transaction
|
||||
payloads.
|
||||
|
||||
Attributes:
|
||||
UnspentOutput (namedtuple): Object holding the information
|
||||
representing an unspent output.
|
||||
|
||||
"""
|
||||
from collections import namedtuple
|
||||
from copy import deepcopy
|
||||
from functools import lru_cache
|
||||
import rapidjson
|
||||
|
||||
import base58
|
||||
from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256, ZenroomSha256
|
||||
from cryptoconditions.exceptions import ParsingError, ASN1DecodeError, ASN1EncodeError
|
||||
from cid import is_cid
|
||||
|
||||
try:
|
||||
from hashlib import sha3_256
|
||||
except ImportError:
|
||||
from sha3 import sha3_256
|
||||
|
||||
from planetmint.transactions.common.crypto import PrivateKey, hash_data
|
||||
from planetmint.transactions.common.exceptions import (
|
||||
KeypairMismatchException,
|
||||
InputDoesNotExist,
|
||||
DoubleSpend,
|
||||
InvalidHash,
|
||||
InvalidSignature,
|
||||
AmountError,
|
||||
AssetIdMismatch,
|
||||
DuplicateTransaction,
|
||||
)
|
||||
from planetmint.backend.schema import validate_language_key
|
||||
from planetmint.transactions.common.schema import validate_transaction_schema
|
||||
from planetmint.transactions.common.utils import serialize, validate_txn_obj, validate_key
|
||||
from .memoize import memoize_from_dict, memoize_to_dict
|
||||
from .input import Input
|
||||
from .output import Output
|
||||
from .transaction_link import TransactionLink
|
||||
|
||||
UnspentOutput = namedtuple(
|
||||
"UnspentOutput",
|
||||
(
|
||||
# TODO 'utxo_hash': sha3_256(f'{txid}{output_index}'.encode())
|
||||
# 'utxo_hash', # noqa
|
||||
"transaction_id",
|
||||
"output_index",
|
||||
"amount",
|
||||
"asset_id",
|
||||
"condition_uri",
|
||||
),
|
||||
)
|
||||
|
||||
VALIDATOR_ELECTION = "VALIDATOR_ELECTION"
|
||||
CHAIN_MIGRATION_ELECTION = "CHAIN_MIGRATION_ELECTION"
|
||||
VOTE = "VOTE"
|
||||
|
||||
|
||||
class Transaction(object):
|
||||
"""A Transaction is used to create and transfer assets.
|
||||
|
||||
Note:
|
||||
For adding Inputs and Outputs, this class provides methods
|
||||
to do so.
|
||||
|
||||
Attributes:
|
||||
operation (str): Defines the operation of the Transaction.
|
||||
inputs (:obj:`list` of :class:`~planetmint.transactions.common.
|
||||
transaction.Input`, optional): Define the assets to
|
||||
spend.
|
||||
outputs (:obj:`list` of :class:`~planetmint.transactions.common.
|
||||
transaction.Output`, optional): Define the assets to lock.
|
||||
asset (dict): Asset payload for this Transaction. ``CREATE``
|
||||
Transactions require a dict with a ``data``
|
||||
property while ``TRANSFER`` Transactions require a dict with a
|
||||
``id`` property.
|
||||
metadata (dict):
|
||||
Metadata to be stored along with the Transaction.
|
||||
version (string): Defines the version number of a Transaction.
|
||||
"""
|
||||
|
||||
CREATE = "CREATE"
|
||||
TRANSFER = "TRANSFER"
|
||||
VALIDATOR_ELECTION = VALIDATOR_ELECTION
|
||||
CHAIN_MIGRATION_ELECTION = CHAIN_MIGRATION_ELECTION
|
||||
VOTE = VOTE
|
||||
ALLOWED_OPERATIONS = (CREATE, TRANSFER)
|
||||
ASSET = "asset"
|
||||
METADATA = "metadata"
|
||||
DATA = "data"
|
||||
VERSION = "2.0"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
operation,
|
||||
asset,
|
||||
inputs=None,
|
||||
outputs=None,
|
||||
metadata=None,
|
||||
version=None,
|
||||
hash_id=None,
|
||||
tx_dict=None,
|
||||
script=None,
|
||||
):
|
||||
"""The constructor allows to create a customizable Transaction.
|
||||
|
||||
Note:
|
||||
When no `version` is provided, one is being
|
||||
generated by this method.
|
||||
|
||||
Args:
|
||||
operation (str): Defines the operation of the Transaction.
|
||||
asset (dict): Asset payload for this Transaction.
|
||||
inputs (:obj:`list` of :class:`~planetmint.transactions.common.
|
||||
transaction.Input`, optional): Define the assets to
|
||||
outputs (:obj:`list` of :class:`~planetmint.transactions.common.
|
||||
transaction.Output`, optional): Define the assets to
|
||||
lock.
|
||||
metadata (dict): Metadata to be stored along with the
|
||||
Transaction.
|
||||
version (string): Defines the version number of a Transaction.
|
||||
hash_id (string): Hash id of the transaction.
|
||||
"""
|
||||
if operation not in self.ALLOWED_OPERATIONS:
|
||||
allowed_ops = ", ".join(self.__class__.ALLOWED_OPERATIONS)
|
||||
raise ValueError("`operation` must be one of {}".format(allowed_ops))
|
||||
|
||||
# Asset payloads for 'CREATE' operations must be None or
|
||||
# dicts holding a `data` property. Asset payloads for 'TRANSFER'
|
||||
# operations must be dicts holding an `id` property.
|
||||
if operation == self.CREATE and asset is not None:
|
||||
if not isinstance(asset, dict):
|
||||
raise TypeError(
|
||||
(
|
||||
"`asset` must be None or a dict holding a `data` "
|
||||
" property instance for '{}' Transactions".format(operation)
|
||||
)
|
||||
)
|
||||
|
||||
if "data" in asset:
|
||||
if asset["data"] is not None and not isinstance(asset["data"], str):
|
||||
if is_cid(asset["data"]) == False:
|
||||
raise TypeError("`asset.data` not valid CID")
|
||||
|
||||
raise TypeError(
|
||||
(
|
||||
"`asset` must be None or a dict holding a `data` "
|
||||
" property instance for '{}' Transactions".format(operation)
|
||||
)
|
||||
)
|
||||
|
||||
elif operation == self.TRANSFER and not (isinstance(asset, dict) and "id" in asset):
|
||||
raise TypeError(("`asset` must be a dict holding an `id` property " "for 'TRANSFER' Transactions"))
|
||||
|
||||
if outputs and not isinstance(outputs, list):
|
||||
raise TypeError("`outputs` must be a list instance or None")
|
||||
|
||||
if inputs and not isinstance(inputs, list):
|
||||
raise TypeError("`inputs` must be a list instance or None")
|
||||
|
||||
if metadata is not None and not isinstance(metadata, str):
|
||||
if is_cid(metadata) == False:
|
||||
raise TypeError("`metadata` not valid CID")
|
||||
|
||||
raise TypeError("`metadata` must be a CID string or None")
|
||||
|
||||
if script is not None and not isinstance(script, dict):
|
||||
raise TypeError("`script` must be a dict or None")
|
||||
|
||||
self.version = version if version is not None else self.VERSION
|
||||
self.operation = operation
|
||||
self.asset = asset
|
||||
self.inputs = inputs or []
|
||||
self.outputs = outputs or []
|
||||
self.metadata = metadata
|
||||
self.script = script
|
||||
self._id = hash_id
|
||||
self.tx_dict = tx_dict
|
||||
|
||||
def validate(self, planet, current_transactions=[]):
|
||||
"""Validate transaction spend
|
||||
Args:
|
||||
planet (Planetmint): an instantiated planetmint.Planetmint object.
|
||||
Returns:
|
||||
The transaction (Transaction) if the transaction is valid else it
|
||||
raises an exception describing the reason why the transaction is
|
||||
invalid.
|
||||
Raises:
|
||||
ValidationError: If the transaction is invalid
|
||||
"""
|
||||
input_conditions = []
|
||||
|
||||
if self.operation == Transaction.CREATE:
|
||||
duplicates = any(txn for txn in current_transactions if txn.id == self.id)
|
||||
if planet.is_committed(self.id) or duplicates:
|
||||
raise DuplicateTransaction("transaction `{}` already exists".format(self.id))
|
||||
|
||||
if not self.inputs_valid(input_conditions):
|
||||
raise InvalidSignature("Transaction signature is invalid.")
|
||||
|
||||
elif self.operation == Transaction.TRANSFER:
|
||||
self.validate_transfer_inputs(planet, current_transactions)
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def unspent_outputs(self):
|
||||
"""UnspentOutput: The outputs of this transaction, in a data
|
||||
structure containing relevant information for storing them in
|
||||
a UTXO set, and performing validation.
|
||||
"""
|
||||
if self.operation == self.CREATE:
|
||||
self._asset_id = self._id
|
||||
elif self.operation == self.TRANSFER:
|
||||
self._asset_id = self.asset["id"]
|
||||
return (
|
||||
UnspentOutput(
|
||||
transaction_id=self._id,
|
||||
output_index=output_index,
|
||||
amount=output.amount,
|
||||
asset_id=self._asset_id,
|
||||
condition_uri=output.fulfillment.condition_uri,
|
||||
)
|
||||
for output_index, output in enumerate(self.outputs)
|
||||
)
|
||||
|
||||
@property
|
||||
def spent_outputs(self):
|
||||
"""Tuple of :obj:`dict`: Inputs of this transaction. Each input
|
||||
is represented as a dictionary containing a transaction id and
|
||||
output index.
|
||||
"""
|
||||
return (input_.fulfills.to_dict() for input_ in self.inputs if input_.fulfills)
|
||||
|
||||
@property
|
||||
def serialized(self):
|
||||
return Transaction._to_str(self.to_dict())
|
||||
|
||||
def _hash(self):
|
||||
self._id = hash_data(self.serialized)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
other = other.to_dict()
|
||||
except AttributeError:
|
||||
return False
|
||||
return self.to_dict() == other
|
||||
|
||||
def to_inputs(self, indices=None):
|
||||
"""Converts a Transaction's outputs to spendable inputs.
|
||||
|
||||
Note:
|
||||
Takes the Transaction's outputs and derives inputs
|
||||
from that can then be passed into `Transaction.transfer` as
|
||||
`inputs`.
|
||||
A list of integers can be passed to `indices` that
|
||||
defines which outputs should be returned as inputs.
|
||||
If no `indices` are passed (empty list or None) all
|
||||
outputs of the Transaction are returned.
|
||||
|
||||
Args:
|
||||
indices (:obj:`list` of int): Defines which
|
||||
outputs should be returned as inputs.
|
||||
|
||||
Returns:
|
||||
:obj:`list` of :class:`~planetmint.transactions.common.transaction.
|
||||
Input`
|
||||
"""
|
||||
# NOTE: If no indices are passed, we just assume to take all outputs
|
||||
# as inputs.
|
||||
indices = indices or range(len(self.outputs))
|
||||
return [
|
||||
Input(
|
||||
self.outputs[idx].fulfillment,
|
||||
self.outputs[idx].public_keys,
|
||||
TransactionLink(self.id, idx),
|
||||
)
|
||||
for idx in indices
|
||||
]
|
||||
|
||||
def add_input(self, input_):
|
||||
"""Adds an input to a Transaction's list of inputs.
|
||||
|
||||
Args:
|
||||
input_ (:class:`~planetmint.transactions.common.transaction.
|
||||
Input`): An Input to be added to the Transaction.
|
||||
"""
|
||||
if not isinstance(input_, Input):
|
||||
raise TypeError("`input_` must be a Input instance")
|
||||
self.inputs.append(input_)
|
||||
|
||||
def add_output(self, output):
|
||||
"""Adds an output to a Transaction's list of outputs.
|
||||
|
||||
Args:
|
||||
output (:class:`~planetmint.transactions.common.transaction.
|
||||
Output`): An Output to be added to the
|
||||
Transaction.
|
||||
"""
|
||||
if not isinstance(output, Output):
|
||||
raise TypeError("`output` must be an Output instance or None")
|
||||
self.outputs.append(output)
|
||||
|
||||
def sign(self, private_keys):
|
||||
"""Fulfills a previous Transaction's Output by signing Inputs.
|
||||
|
||||
Note:
|
||||
This method works only for the following Cryptoconditions
|
||||
currently:
|
||||
- Ed25519Fulfillment
|
||||
- ThresholdSha256
|
||||
- ZenroomSha256
|
||||
Furthermore, note that all keys required to fully sign the
|
||||
Transaction have to be passed to this method. A subset of all
|
||||
will cause this method to fail.
|
||||
|
||||
Args:
|
||||
private_keys (:obj:`list` of :obj:`str`): A complete list of
|
||||
all private keys needed to sign all Fulfillments of this
|
||||
Transaction.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.transactions.common.transaction.Transaction`
|
||||
"""
|
||||
# TODO: Singing should be possible with at least one of all private
|
||||
# keys supplied to this method.
|
||||
if private_keys is None or not isinstance(private_keys, list):
|
||||
raise TypeError("`private_keys` must be a list instance")
|
||||
|
||||
# NOTE: Generate public keys from private keys and match them in a
|
||||
# dictionary:
|
||||
# key: public_key
|
||||
# value: private_key
|
||||
def gen_public_key(private_key):
|
||||
# TODO FOR CC: Adjust interface so that this function becomes
|
||||
# unnecessary
|
||||
|
||||
# cc now provides a single method `encode` to return the key
|
||||
# in several different encodings.
|
||||
public_key = private_key.get_verifying_key().encode()
|
||||
# Returned values from cc are always bytestrings so here we need
|
||||
# to decode to convert the bytestring into a python str
|
||||
return public_key.decode()
|
||||
|
||||
key_pairs = {gen_public_key(PrivateKey(private_key)): PrivateKey(private_key) for private_key in private_keys}
|
||||
|
||||
tx_dict = self.to_dict()
|
||||
tx_dict = Transaction._remove_signatures(tx_dict)
|
||||
tx_serialized = Transaction._to_str(tx_dict)
|
||||
for i, input_ in enumerate(self.inputs):
|
||||
self.inputs[i] = self._sign_input(input_, tx_serialized, key_pairs)
|
||||
|
||||
self._hash()
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def _sign_input(cls, input_, message, key_pairs):
|
||||
"""Signs a single Input.
|
||||
|
||||
Note:
|
||||
This method works only for the following Cryptoconditions
|
||||
currently:
|
||||
- Ed25519Fulfillment
|
||||
- ThresholdSha256.
|
||||
- ZenroomSha256
|
||||
Args:
|
||||
input_ (:class:`~planetmint.transactions.common.transaction.
|
||||
Input`) The Input to be signed.
|
||||
message (str): The message to be signed
|
||||
key_pairs (dict): The keys to sign the Transaction with.
|
||||
"""
|
||||
if isinstance(input_.fulfillment, Ed25519Sha256):
|
||||
return cls._sign_simple_signature_fulfillment(input_, message, key_pairs)
|
||||
elif isinstance(input_.fulfillment, ThresholdSha256):
|
||||
return cls._sign_threshold_signature_fulfillment(input_, message, key_pairs)
|
||||
elif isinstance(input_.fulfillment, ZenroomSha256):
|
||||
return cls._sign_zenroom_fulfillment(input_, message, key_pairs)
|
||||
else:
|
||||
raise ValueError("Fulfillment couldn't be matched to " "Cryptocondition fulfillment type.")
|
||||
|
||||
@classmethod
|
||||
def _sign_zenroom_fulfillment(cls, input_, message, key_pairs):
|
||||
"""Signs a Zenroomful.
|
||||
|
||||
Args:
|
||||
input_ (:class:`~planetmint.transactions.common.transaction.
|
||||
Input`) The input to be signed.
|
||||
message (str): The message to be signed
|
||||
key_pairs (dict): The keys to sign the Transaction with.
|
||||
"""
|
||||
# NOTE: To eliminate the dangers of accidentally signing a condition by
|
||||
# reference, we remove the reference of input_ here
|
||||
# intentionally. If the user of this class knows how to use it,
|
||||
# this should never happen, but then again, never say never.
|
||||
input_ = deepcopy(input_)
|
||||
public_key = input_.owners_before[0]
|
||||
message = sha3_256(message.encode())
|
||||
if input_.fulfills:
|
||||
message.update("{}{}".format(input_.fulfills.txid, input_.fulfills.output).encode())
|
||||
|
||||
try:
|
||||
# cryptoconditions makes no assumptions of the encoding of the
|
||||
# message to sign or verify. It only accepts bytestrings
|
||||
input_.fulfillment.sign(message.digest(), base58.b58decode(key_pairs[public_key].encode()))
|
||||
except KeyError:
|
||||
raise KeypairMismatchException(
|
||||
"Public key {} is not a pair to " "any of the private keys".format(public_key)
|
||||
)
|
||||
return input_
|
||||
|
||||
@classmethod
|
||||
def _sign_simple_signature_fulfillment(cls, input_, message, key_pairs):
|
||||
"""Signs a Ed25519Fulfillment.
|
||||
|
||||
Args:
|
||||
input_ (:class:`~planetmint.transactions.common.transaction.
|
||||
Input`) The input to be signed.
|
||||
message (str): The message to be signed
|
||||
key_pairs (dict): The keys to sign the Transaction with.
|
||||
"""
|
||||
# NOTE: To eliminate the dangers of accidentally signing a condition by
|
||||
# reference, we remove the reference of input_ here
|
||||
# intentionally. If the user of this class knows how to use it,
|
||||
# this should never happen, but then again, never say never.
|
||||
input_ = deepcopy(input_)
|
||||
public_key = input_.owners_before[0]
|
||||
message = sha3_256(message.encode())
|
||||
if input_.fulfills:
|
||||
message.update("{}{}".format(input_.fulfills.txid, input_.fulfills.output).encode())
|
||||
|
||||
try:
|
||||
# cryptoconditions makes no assumptions of the encoding of the
|
||||
# message to sign or verify. It only accepts bytestrings
|
||||
input_.fulfillment.sign(message.digest(), base58.b58decode(key_pairs[public_key].encode()))
|
||||
except KeyError:
|
||||
raise KeypairMismatchException(
|
||||
"Public key {} is not a pair to " "any of the private keys".format(public_key)
|
||||
)
|
||||
return input_
|
||||
|
||||
@classmethod
|
||||
def _sign_threshold_signature_fulfillment(cls, input_, message, key_pairs):
|
||||
"""Signs a ThresholdSha256.
|
||||
|
||||
Args:
|
||||
input_ (:class:`~planetmint.transactions.common.transaction.
|
||||
Input`) The Input to be signed.
|
||||
message (str): The message to be signed
|
||||
key_pairs (dict): The keys to sign the Transaction with.
|
||||
"""
|
||||
input_ = deepcopy(input_)
|
||||
message = sha3_256(message.encode())
|
||||
if input_.fulfills:
|
||||
message.update("{}{}".format(input_.fulfills.txid, input_.fulfills.output).encode())
|
||||
|
||||
for owner_before in set(input_.owners_before):
|
||||
# TODO: CC should throw a KeypairMismatchException, instead of
|
||||
# our manual mapping here
|
||||
|
||||
# TODO FOR CC: Naming wise this is not so smart,
|
||||
# `get_subcondition` in fact doesn't return a
|
||||
# condition but a fulfillment
|
||||
|
||||
# TODO FOR CC: `get_subcondition` is singular. One would not
|
||||
# expect to get a list back.
|
||||
ccffill = input_.fulfillment
|
||||
subffills = ccffill.get_subcondition_from_vk(base58.b58decode(owner_before))
|
||||
if not subffills:
|
||||
raise KeypairMismatchException(
|
||||
"Public key {} cannot be found " "in the fulfillment".format(owner_before)
|
||||
)
|
||||
try:
|
||||
private_key = key_pairs[owner_before]
|
||||
except KeyError:
|
||||
raise KeypairMismatchException(
|
||||
"Public key {} is not a pair " "to any of the private keys".format(owner_before)
|
||||
)
|
||||
|
||||
# cryptoconditions makes no assumptions of the encoding of the
|
||||
# message to sign or verify. It only accepts bytestrings
|
||||
for subffill in subffills:
|
||||
subffill.sign(message.digest(), base58.b58decode(private_key.encode()))
|
||||
return input_
|
||||
|
||||
def inputs_valid(self, outputs=None):
|
||||
"""Validates the Inputs in the Transaction against given
|
||||
Outputs.
|
||||
|
||||
Note:
|
||||
Given a `CREATE` Transaction is passed,
|
||||
dummy values for Outputs are submitted for validation that
|
||||
evaluate parts of the validation-checks to `True`.
|
||||
|
||||
Args:
|
||||
outputs (:obj:`list` of :class:`~planetmint.transactions.common.
|
||||
transaction.Output`): A list of Outputs to check the
|
||||
Inputs against.
|
||||
|
||||
Returns:
|
||||
bool: If all Inputs are valid.
|
||||
"""
|
||||
if self.operation == self.CREATE:
|
||||
# NOTE: Since in the case of a `CREATE`-transaction we do not have
|
||||
# to check for outputs, we're just submitting dummy
|
||||
# values to the actual method. This simplifies it's logic
|
||||
# greatly, as we do not have to check against `None` values.
|
||||
return self._inputs_valid(["dummyvalue" for _ in self.inputs])
|
||||
elif self.operation == self.TRANSFER:
|
||||
return self._inputs_valid([output.fulfillment.condition_uri for output in outputs])
|
||||
elif self.operation == self.VALIDATOR_ELECTION:
|
||||
return self._inputs_valid(["dummyvalue" for _ in self.inputs])
|
||||
elif self.operation == self.CHAIN_MIGRATION_ELECTION:
|
||||
return self._inputs_valid(["dummyvalue" for _ in self.inputs])
|
||||
else:
|
||||
allowed_ops = ", ".join(self.__class__.ALLOWED_OPERATIONS)
|
||||
raise TypeError("`operation` must be one of {}".format(allowed_ops))
|
||||
|
||||
def _inputs_valid(self, output_condition_uris):
|
||||
"""Validates an Input against a given set of Outputs.
|
||||
|
||||
Note:
|
||||
The number of `output_condition_uris` must be equal to the
|
||||
number of Inputs a Transaction has.
|
||||
|
||||
Args:
|
||||
output_condition_uris (:obj:`list` of :obj:`str`): A list of
|
||||
Outputs to check the Inputs against.
|
||||
|
||||
Returns:
|
||||
bool: If all Outputs are valid.
|
||||
"""
|
||||
|
||||
if len(self.inputs) != len(output_condition_uris):
|
||||
raise ValueError("Inputs and " "output_condition_uris must have the same count")
|
||||
|
||||
tx_dict = self.tx_dict if self.tx_dict else self.to_dict()
|
||||
tx_dict = Transaction._remove_signatures(tx_dict)
|
||||
tx_dict["id"] = None
|
||||
tx_serialized = Transaction._to_str(tx_dict)
|
||||
|
||||
def validate(i, output_condition_uri=None):
|
||||
"""Validate input against output condition URI"""
|
||||
return self._input_valid(self.inputs[i], self.operation, tx_serialized, output_condition_uri)
|
||||
|
||||
return all(validate(i, cond) for i, cond in enumerate(output_condition_uris))
|
||||
|
||||
@lru_cache(maxsize=16384)
|
||||
def _input_valid(self, input_, operation, message, output_condition_uri=None):
|
||||
"""Validates a single Input against a single Output.
|
||||
|
||||
Note:
|
||||
In case of a `CREATE` Transaction, this method
|
||||
does not validate against `output_condition_uri`.
|
||||
|
||||
Args:
|
||||
input_ (:class:`~planetmint.transactions.common.transaction.
|
||||
Input`) The Input to be signed.
|
||||
operation (str): The type of Transaction.
|
||||
message (str): The fulfillment message.
|
||||
output_condition_uri (str, optional): An Output to check the
|
||||
Input against.
|
||||
|
||||
Returns:
|
||||
bool: If the Input is valid.
|
||||
"""
|
||||
ccffill = input_.fulfillment
|
||||
try:
|
||||
parsed_ffill = Fulfillment.from_uri(ccffill.serialize_uri())
|
||||
except TypeError as e:
|
||||
print(f"Exception TypeError : {e}")
|
||||
return False
|
||||
except ValueError as e:
|
||||
print(f"Exception ValueError : {e}")
|
||||
return False
|
||||
except ParsingError as e:
|
||||
print(f"Exception ParsingError : {e}")
|
||||
return False
|
||||
except ASN1DecodeError as e:
|
||||
print(f"Exception ASN1DecodeError : {e}")
|
||||
return False
|
||||
except ASN1EncodeError as e:
|
||||
print(f"Exception ASN1EncodeError : {e}")
|
||||
return False
|
||||
|
||||
if operation in [self.CREATE, self.CHAIN_MIGRATION_ELECTION, self.VALIDATOR_ELECTION]:
|
||||
# NOTE: In the case of a `CREATE` transaction, the
|
||||
# output is always valid.
|
||||
output_valid = True
|
||||
else:
|
||||
output_valid = output_condition_uri == ccffill.condition_uri
|
||||
|
||||
ffill_valid = False
|
||||
if isinstance(parsed_ffill, ZenroomSha256):
|
||||
import json
|
||||
|
||||
msg = json.loads(message)
|
||||
ffill_valid = parsed_ffill.validate(message=json.dumps(msg["script"]))
|
||||
else:
|
||||
message = sha3_256(message.encode())
|
||||
if input_.fulfills:
|
||||
message.update("{}{}".format(input_.fulfills.txid, input_.fulfills.output).encode())
|
||||
|
||||
# NOTE: We pass a timestamp to `.validate`, as in case of a timeout
|
||||
# condition we'll have to validate against it
|
||||
|
||||
# cryptoconditions makes no assumptions of the encoding of the
|
||||
# message to sign or verify. It only accepts bytestrings
|
||||
ffill_valid = parsed_ffill.validate(message=message.digest())
|
||||
return output_valid and ffill_valid
|
||||
|
||||
# This function is required by `lru_cache` to create a key for memoization
|
||||
def __hash__(self):
|
||||
return hash(self.id)
|
||||
|
||||
@memoize_to_dict
|
||||
def to_dict(self):
|
||||
"""Transforms the object to a Python dictionary.
|
||||
|
||||
Returns:
|
||||
dict: The Transaction as an alternative serialization format.
|
||||
"""
|
||||
tx_dict = {
|
||||
"inputs": [input_.to_dict() for input_ in self.inputs],
|
||||
"outputs": [output.to_dict() for output in self.outputs],
|
||||
"operation": str(self.operation),
|
||||
"metadata": self.metadata,
|
||||
"asset": self.asset,
|
||||
"version": self.version,
|
||||
"id": self._id,
|
||||
}
|
||||
if self.script:
|
||||
tx_dict["script"] = self.script
|
||||
return tx_dict
|
||||
|
||||
@staticmethod
|
||||
# TODO: Remove `_dict` prefix of variable.
|
||||
def _remove_signatures(tx_dict):
|
||||
"""Takes a Transaction dictionary and removes all signatures.
|
||||
|
||||
Args:
|
||||
tx_dict (dict): The Transaction to remove all signatures from.
|
||||
|
||||
Returns:
|
||||
dict
|
||||
|
||||
"""
|
||||
# NOTE: We remove the reference since we need `tx_dict` only for the
|
||||
# transaction's hash
|
||||
tx_dict = deepcopy(tx_dict)
|
||||
for input_ in tx_dict["inputs"]:
|
||||
# NOTE: Not all Cryptoconditions return a `signature` key (e.g.
|
||||
# ThresholdSha256), so setting it to `None` in any
|
||||
# case could yield incorrect signatures. This is why we only
|
||||
# set it to `None` if it's set in the dict.
|
||||
input_["fulfillment"] = None
|
||||
return tx_dict
|
||||
|
||||
@staticmethod
|
||||
def _to_hash(value):
|
||||
return hash_data(value)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
def to_hash(self):
|
||||
return self.to_dict()["id"]
|
||||
|
||||
@staticmethod
|
||||
def _to_str(value):
|
||||
return serialize(value)
|
||||
|
||||
# TODO: This method shouldn't call `_remove_signatures`
|
||||
def __str__(self):
|
||||
_tx = self.to_dict()
|
||||
tx = Transaction._remove_signatures(_tx)
|
||||
return Transaction._to_str(tx)
|
||||
|
||||
@classmethod
|
||||
def get_asset_id(cls, transactions):
|
||||
"""Get the asset id from a list of :class:`~.Transactions`.
|
||||
|
||||
This is useful when we want to check if the multiple inputs of a
|
||||
transaction are related to the same asset id.
|
||||
|
||||
Args:
|
||||
transactions (:obj:`list` of :class:`~planetmint.transactions.common.
|
||||
transaction.Transaction`): A list of Transactions.
|
||||
Usually input Transactions that should have a matching
|
||||
asset ID.
|
||||
|
||||
Returns:
|
||||
str: ID of the asset.
|
||||
|
||||
Raises:
|
||||
:exc:`AssetIdMismatch`: If the inputs are related to different
|
||||
assets.
|
||||
"""
|
||||
|
||||
if not isinstance(transactions, list):
|
||||
transactions = [transactions]
|
||||
|
||||
# create a set of the transactions' asset ids
|
||||
asset_ids = {
|
||||
tx.id if tx.operation in [tx.CREATE, tx.VALIDATOR_ELECTION] else tx.asset["id"] for tx in transactions
|
||||
}
|
||||
|
||||
# check that all the transasctions have the same asset id
|
||||
if len(asset_ids) > 1:
|
||||
raise AssetIdMismatch(("All inputs of all transactions passed" " need to have the same asset id"))
|
||||
return asset_ids.pop()
|
||||
|
||||
@staticmethod
|
||||
def validate_id(tx_body):
|
||||
"""Validate the transaction ID of a transaction
|
||||
|
||||
Args:
|
||||
tx_body (dict): The Transaction to be transformed.
|
||||
"""
|
||||
# NOTE: Remove reference to avoid side effects
|
||||
tx_body = deepcopy(tx_body)
|
||||
tx_body = rapidjson.loads(rapidjson.dumps(tx_body))
|
||||
|
||||
try:
|
||||
proposed_tx_id = tx_body["id"]
|
||||
except KeyError:
|
||||
raise InvalidHash("No transaction id found!")
|
||||
|
||||
tx_body["id"] = None
|
||||
|
||||
tx_body_serialized = Transaction._to_str(tx_body)
|
||||
valid_tx_id = Transaction._to_hash(tx_body_serialized)
|
||||
if proposed_tx_id != valid_tx_id:
|
||||
err_msg = "The transaction's id '{}' isn't equal to " "the hash of its body, i.e. it's not valid."
|
||||
raise InvalidHash(err_msg.format(proposed_tx_id))
|
||||
|
||||
@classmethod
|
||||
@memoize_from_dict
|
||||
def from_dict(cls, tx, skip_schema_validation=True):
|
||||
"""Transforms a Python dictionary to a Transaction object.
|
||||
|
||||
Args:
|
||||
tx_body (dict): The Transaction to be transformed.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.transactions.common.transaction.Transaction`
|
||||
"""
|
||||
operation = tx.get("operation", Transaction.CREATE) if isinstance(tx, dict) else Transaction.CREATE
|
||||
cls = Transaction.resolve_class(operation)
|
||||
|
||||
id = None
|
||||
try:
|
||||
id = tx["id"]
|
||||
except KeyError:
|
||||
id = None
|
||||
# tx['asset'] = tx['asset'][0] if isinstance( tx['asset'], list) or isinstance( tx['asset'], tuple) else tx['asset'], # noqa: E501
|
||||
local_dict = {
|
||||
"inputs": tx["inputs"],
|
||||
"outputs": tx["outputs"],
|
||||
"operation": operation,
|
||||
"metadata": tx["metadata"],
|
||||
"asset": tx[
|
||||
"asset"
|
||||
], # [0] if isinstance( tx['asset'], list) or isinstance( tx['asset'], tuple) else tx['asset'], # noqa: E501
|
||||
"version": tx["version"],
|
||||
"id": id,
|
||||
}
|
||||
try:
|
||||
script_ = tx["script"]
|
||||
script_dict = {"script": script_}
|
||||
except KeyError:
|
||||
script_ = None
|
||||
pass
|
||||
else:
|
||||
local_dict = {**local_dict, **script_dict}
|
||||
|
||||
if not skip_schema_validation:
|
||||
cls.validate_id(local_dict)
|
||||
cls.validate_schema(local_dict)
|
||||
|
||||
inputs = [Input.from_dict(input_) for input_ in tx["inputs"]]
|
||||
outputs = [Output.from_dict(output) for output in tx["outputs"]]
|
||||
return cls(
|
||||
tx["operation"],
|
||||
tx["asset"],
|
||||
inputs,
|
||||
outputs,
|
||||
tx["metadata"],
|
||||
tx["version"],
|
||||
hash_id=tx["id"],
|
||||
tx_dict=tx,
|
||||
script=script_,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_db(cls, planet, tx_dict_list):
|
||||
"""Helper method that reconstructs a transaction dict that was returned
|
||||
from the database. It checks what asset_id to retrieve, retrieves the
|
||||
asset from the asset table and reconstructs the transaction.
|
||||
|
||||
Args:
|
||||
planet (:class:`~planetmint.tendermint.Planetmint`): An instance
|
||||
of Planetmint used to perform database queries.
|
||||
tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or
|
||||
list of transaction dict as returned from the database.
|
||||
|
||||
Returns:
|
||||
:class:`~Transaction`
|
||||
|
||||
"""
|
||||
return_list = True
|
||||
if isinstance(tx_dict_list, dict):
|
||||
tx_dict_list = [tx_dict_list]
|
||||
return_list = False
|
||||
|
||||
tx_map = {}
|
||||
tx_ids = []
|
||||
for tx in tx_dict_list:
|
||||
tx.update({"metadata": None})
|
||||
tx_map[tx["id"]] = tx
|
||||
tx_ids.append(tx["id"])
|
||||
|
||||
assets = list(planet.get_assets(tx_ids))
|
||||
for asset in assets:
|
||||
if asset is not None:
|
||||
# This is tarantool specific behaviour needs to be addressed
|
||||
tx = tx_map[asset[1]]
|
||||
tx["asset"] = asset[0]
|
||||
|
||||
tx_ids = list(tx_map.keys())
|
||||
metadata_list = list(planet.get_metadata(tx_ids))
|
||||
for metadata in metadata_list:
|
||||
if "id" in metadata:
|
||||
tx = tx_map[metadata["id"]]
|
||||
tx.update({"metadata": metadata.get("metadata")})
|
||||
|
||||
if return_list:
|
||||
tx_list = []
|
||||
for tx_id, tx in tx_map.items():
|
||||
tx_list.append(cls.from_dict(tx))
|
||||
return tx_list
|
||||
else:
|
||||
tx = list(tx_map.values())[0]
|
||||
return cls.from_dict(tx)
|
||||
|
||||
type_registry = {}
|
||||
|
||||
@staticmethod
|
||||
def register_type(tx_type, tx_class):
|
||||
Transaction.type_registry[tx_type] = tx_class
|
||||
|
||||
def resolve_class(operation):
|
||||
"""For the given `tx` based on the `operation` key return its implementation class"""
|
||||
|
||||
create_txn_class = Transaction.type_registry.get(Transaction.CREATE)
|
||||
return Transaction.type_registry.get(operation, create_txn_class)
|
||||
|
||||
@classmethod
|
||||
def validate_schema(cls, tx):
|
||||
validate_transaction_schema(tx)
|
||||
validate_txn_obj(cls.ASSET, tx[cls.ASSET], cls.DATA, validate_key)
|
||||
validate_txn_obj(cls.METADATA, tx, cls.METADATA, validate_key)
|
||||
validate_language_key(tx[cls.ASSET], cls.DATA)
|
||||
validate_language_key(tx, cls.METADATA)
|
||||
|
||||
def validate_transfer_inputs(self, planet, current_transactions=[]):
|
||||
# store the inputs so that we can check if the asset ids match
|
||||
input_txs = []
|
||||
input_conditions = []
|
||||
for input_ in self.inputs:
|
||||
input_txid = input_.fulfills.txid
|
||||
input_tx = planet.get_transaction(input_txid)
|
||||
if input_tx is None:
|
||||
for ctxn in current_transactions:
|
||||
if ctxn.id == input_txid:
|
||||
input_tx = ctxn
|
||||
|
||||
if input_tx is None:
|
||||
raise InputDoesNotExist("input `{}` doesn't exist".format(input_txid))
|
||||
|
||||
spent = planet.get_spent(input_txid, input_.fulfills.output, current_transactions)
|
||||
if spent:
|
||||
raise DoubleSpend("input `{}` was already spent".format(input_txid))
|
||||
|
||||
output = input_tx.outputs[input_.fulfills.output]
|
||||
input_conditions.append(output)
|
||||
input_txs.append(input_tx)
|
||||
|
||||
# Validate that all inputs are distinct
|
||||
links = [i.fulfills.to_uri() for i in self.inputs]
|
||||
if len(links) != len(set(links)):
|
||||
raise DoubleSpend('tx "{}" spends inputs twice'.format(self.id))
|
||||
|
||||
# validate asset id
|
||||
asset_id = self.get_asset_id(input_txs)
|
||||
if asset_id != self.asset["id"]:
|
||||
raise AssetIdMismatch(("The asset id of the input does not" " match the asset id of the" " transaction"))
|
||||
|
||||
input_amount = sum([input_condition.amount for input_condition in input_conditions])
|
||||
output_amount = sum([output_condition.amount for output_condition in self.outputs])
|
||||
|
||||
if output_amount != input_amount:
|
||||
raise AmountError(
|
||||
(
|
||||
"The amount used in the inputs `{}`" " needs to be same as the amount used" " in the outputs `{}`"
|
||||
).format(input_amount, output_amount)
|
||||
)
|
||||
|
||||
if not self.inputs_valid(input_conditions):
|
||||
raise InvalidSignature("Transaction signature is invalid.")
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def complete_tx_i_o(self, tx_signers, recipients):
|
||||
inputs = []
|
||||
outputs = []
|
||||
|
||||
# generate_outputs
|
||||
for recipient in recipients:
|
||||
if not isinstance(recipient, tuple) or len(recipient) != 2:
|
||||
raise ValueError(
|
||||
("Each `recipient` in the list must be a" " tuple of `([<list of public keys>]," " <amount>)`")
|
||||
)
|
||||
pub_keys, amount = recipient
|
||||
outputs.append(Output.generate(pub_keys, amount))
|
||||
|
||||
# generate inputs
|
||||
inputs.append(Input.generate(tx_signers))
|
||||
|
||||
return (inputs, outputs)
|
@ -1,76 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
|
||||
class TransactionLink(object):
|
||||
"""An object for unidirectional linking to a Transaction's Output.
|
||||
|
||||
Attributes:
|
||||
txid (str, optional): A Transaction to link to.
|
||||
output (int, optional): An output's index in a Transaction with id
|
||||
`txid`.
|
||||
"""
|
||||
|
||||
def __init__(self, txid=None, output=None):
|
||||
"""Create an instance of a :class:`~.TransactionLink`.
|
||||
|
||||
Note:
|
||||
In an IPLD implementation, this class is not necessary anymore,
|
||||
as an IPLD link can simply point to an object, as well as an
|
||||
objects properties. So instead of having a (de)serializable
|
||||
class, we can have a simple IPLD link of the form:
|
||||
`/<tx_id>/transaction/outputs/<output>/`.
|
||||
|
||||
Args:
|
||||
txid (str, optional): A Transaction to link to.
|
||||
output (int, optional): An Outputs's index in a Transaction with
|
||||
id `txid`.
|
||||
"""
|
||||
self.txid = txid
|
||||
self.output = output
|
||||
|
||||
def __bool__(self):
|
||||
return self.txid is not None and self.output is not None
|
||||
|
||||
def __eq__(self, other):
|
||||
# TODO: If `other !== TransactionLink` return `False`
|
||||
return self.to_dict() == other.to_dict()
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.txid, self.output))
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, link):
|
||||
"""Transforms a Python dictionary to a TransactionLink object.
|
||||
|
||||
Args:
|
||||
link (dict): The link to be transformed.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.transactions.common.transaction.TransactionLink`
|
||||
"""
|
||||
try:
|
||||
return cls(link["transaction_id"], link["output_index"])
|
||||
except TypeError:
|
||||
return cls()
|
||||
|
||||
def to_dict(self):
|
||||
"""Transforms the object to a Python dictionary.
|
||||
|
||||
Returns:
|
||||
(dict|None): The link as an alternative serialization format.
|
||||
"""
|
||||
if self.txid is None and self.output is None:
|
||||
return None
|
||||
else:
|
||||
return {
|
||||
"transaction_id": self.txid,
|
||||
"output_index": self.output,
|
||||
}
|
||||
|
||||
def to_uri(self, path=""):
|
||||
if self.txid is None and self.output is None:
|
||||
return None
|
||||
return "{}/transactions/{}/outputs/{}".format(path, self.txid, self.output)
|
@ -1,8 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
BROADCAST_TX_COMMIT = "broadcast_tx_commit"
|
||||
BROADCAST_TX_ASYNC = "broadcast_tx_async"
|
||||
BROADCAST_TX_SYNC = "broadcast_tx_sync"
|
@ -1,231 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import base58
|
||||
import time
|
||||
import re
|
||||
import rapidjson
|
||||
|
||||
from planetmint.config import Config
|
||||
from planetmint.transactions.common.exceptions import ValidationError
|
||||
from cryptoconditions import ThresholdSha256, Ed25519Sha256, ZenroomSha256
|
||||
from planetmint.transactions.common.exceptions import ThresholdTooDeep
|
||||
from cryptoconditions.exceptions import UnsupportedTypeError
|
||||
|
||||
|
||||
def gen_timestamp():
|
||||
"""The Unix time, rounded to the nearest second.
|
||||
See https://en.wikipedia.org/wiki/Unix_time
|
||||
|
||||
Returns:
|
||||
str: the Unix time
|
||||
"""
|
||||
return str(round(time.time()))
|
||||
|
||||
|
||||
def serialize(data):
|
||||
"""Serialize a dict into a JSON formatted string.
|
||||
|
||||
This function enforces rules like the separator and order of keys.
|
||||
This ensures that all dicts are serialized in the same way.
|
||||
|
||||
This is specially important for hashing data. We need to make sure that
|
||||
everyone serializes their data in the same way so that we do not have
|
||||
hash mismatches for the same structure due to serialization
|
||||
differences.
|
||||
|
||||
Args:
|
||||
data (dict): dict to serialize
|
||||
|
||||
Returns:
|
||||
str: JSON formatted string
|
||||
|
||||
"""
|
||||
return rapidjson.dumps(data, skipkeys=False, ensure_ascii=False, sort_keys=True)
|
||||
|
||||
|
||||
def deserialize(data):
|
||||
"""Deserialize a JSON formatted string into a dict.
|
||||
|
||||
Args:
|
||||
data (str): JSON formatted string.
|
||||
|
||||
Returns:
|
||||
dict: dict resulting from the serialization of a JSON formatted
|
||||
string.
|
||||
"""
|
||||
return rapidjson.loads(data)
|
||||
|
||||
|
||||
def validate_txn_obj(obj_name, obj, key, validation_fun):
|
||||
"""Validate value of `key` in `obj` using `validation_fun`.
|
||||
|
||||
Args:
|
||||
obj_name (str): name for `obj` being validated.
|
||||
obj (dict): dictionary object.
|
||||
key (str): key to be validated in `obj`.
|
||||
validation_fun (function): function used to validate the value
|
||||
of `key`.
|
||||
|
||||
Returns:
|
||||
None: indicates validation successful
|
||||
|
||||
Raises:
|
||||
ValidationError: `validation_fun` will raise exception on failure
|
||||
"""
|
||||
backend = Config().get()["database"]["backend"]
|
||||
|
||||
if backend == "localmongodb":
|
||||
data = obj.get(key, {})
|
||||
if isinstance(data, dict):
|
||||
validate_all_keys_in_obj(obj_name, data, validation_fun)
|
||||
elif isinstance(data, list):
|
||||
validate_all_items_in_list(obj_name, data, validation_fun)
|
||||
|
||||
|
||||
def validate_all_items_in_list(obj_name, data, validation_fun):
|
||||
for item in data:
|
||||
if isinstance(item, dict):
|
||||
validate_all_keys_in_obj(obj_name, item, validation_fun)
|
||||
elif isinstance(item, list):
|
||||
validate_all_items_in_list(obj_name, item, validation_fun)
|
||||
|
||||
|
||||
def validate_all_keys_in_obj(obj_name, obj, validation_fun):
|
||||
"""Validate all (nested) keys in `obj` by using `validation_fun`.
|
||||
|
||||
Args:
|
||||
obj_name (str): name for `obj` being validated.
|
||||
obj (dict): dictionary object.
|
||||
validation_fun (function): function used to validate the value
|
||||
of `key`.
|
||||
|
||||
Returns:
|
||||
None: indicates validation successful
|
||||
|
||||
Raises:
|
||||
ValidationError: `validation_fun` will raise this error on failure
|
||||
"""
|
||||
for key, value in obj.items():
|
||||
validation_fun(obj_name, key)
|
||||
if isinstance(value, dict):
|
||||
validate_all_keys_in_obj(obj_name, value, validation_fun)
|
||||
elif isinstance(value, list):
|
||||
validate_all_items_in_list(obj_name, value, validation_fun)
|
||||
|
||||
|
||||
def validate_all_values_for_key_in_obj(obj, key, validation_fun):
|
||||
"""Validate value for all (nested) occurrence of `key` in `obj`
|
||||
using `validation_fun`.
|
||||
|
||||
Args:
|
||||
obj (dict): dictionary object.
|
||||
key (str): key whose value is to be validated.
|
||||
validation_fun (function): function used to validate the value
|
||||
of `key`.
|
||||
|
||||
Raises:
|
||||
ValidationError: `validation_fun` will raise this error on failure
|
||||
"""
|
||||
for vkey, value in obj.items():
|
||||
if vkey == key:
|
||||
validation_fun(value)
|
||||
elif isinstance(value, dict):
|
||||
validate_all_values_for_key_in_obj(value, key, validation_fun)
|
||||
elif isinstance(value, list):
|
||||
validate_all_values_for_key_in_list(value, key, validation_fun)
|
||||
|
||||
|
||||
def validate_all_values_for_key_in_list(input_list, key, validation_fun):
|
||||
for item in input_list:
|
||||
if isinstance(item, dict):
|
||||
validate_all_values_for_key_in_obj(item, key, validation_fun)
|
||||
elif isinstance(item, list):
|
||||
validate_all_values_for_key_in_list(item, key, validation_fun)
|
||||
|
||||
|
||||
def validate_key(obj_name, key):
|
||||
"""Check if `key` contains ".", "$" or null characters.
|
||||
|
||||
https://docs.mongodb.com/manual/reference/limits/#Restrictions-on-Field-Names
|
||||
|
||||
Args:
|
||||
obj_name (str): object name to use when raising exception
|
||||
key (str): key to validated
|
||||
|
||||
Returns:
|
||||
None: validation successful
|
||||
|
||||
Raises:
|
||||
ValidationError: will raise exception in case of regex match.
|
||||
"""
|
||||
if re.search(r"^[$]|\.|\x00", key):
|
||||
error_str = (
|
||||
'Invalid key name "{}" in {} object. The '
|
||||
"key name cannot contain characters "
|
||||
'".", "$" or null characters'
|
||||
).format(key, obj_name)
|
||||
raise ValidationError(error_str)
|
||||
|
||||
|
||||
def _fulfillment_to_details(fulfillment):
|
||||
"""Encode a fulfillment as a details dictionary
|
||||
|
||||
Args:
|
||||
fulfillment: Crypto-conditions Fulfillment object
|
||||
"""
|
||||
|
||||
if fulfillment.type_name == "ed25519-sha-256":
|
||||
return {
|
||||
"type": "ed25519-sha-256",
|
||||
"public_key": base58.b58encode(fulfillment.public_key).decode(),
|
||||
}
|
||||
|
||||
if fulfillment.type_name == "threshold-sha-256":
|
||||
subconditions = [_fulfillment_to_details(cond["body"]) for cond in fulfillment.subconditions]
|
||||
return {
|
||||
"type": "threshold-sha-256",
|
||||
"threshold": fulfillment.threshold,
|
||||
"subconditions": subconditions,
|
||||
}
|
||||
if fulfillment.type_name == "zenroom-sha-256":
|
||||
return {
|
||||
"type": "zenroom-sha-256",
|
||||
"public_key": base58.b58encode(fulfillment.public_key).decode(),
|
||||
"script": base58.b58encode(fulfillment.script).decode(),
|
||||
"data": base58.b58encode(fulfillment.data).decode(),
|
||||
}
|
||||
|
||||
raise UnsupportedTypeError(fulfillment.type_name)
|
||||
|
||||
|
||||
def _fulfillment_from_details(data, _depth=0):
|
||||
"""Load a fulfillment for a signing spec dictionary
|
||||
|
||||
Args:
|
||||
data: tx.output[].condition.details dictionary
|
||||
"""
|
||||
if _depth == 100:
|
||||
raise ThresholdTooDeep()
|
||||
|
||||
if data["type"] == "ed25519-sha-256":
|
||||
public_key = base58.b58decode(data["public_key"])
|
||||
return Ed25519Sha256(public_key=public_key)
|
||||
|
||||
if data["type"] == "threshold-sha-256":
|
||||
threshold = ThresholdSha256(data["threshold"])
|
||||
for cond in data["subconditions"]:
|
||||
cond = _fulfillment_from_details(cond, _depth + 1)
|
||||
threshold.add_subfulfillment(cond)
|
||||
return threshold
|
||||
|
||||
if data["type"] == "zenroom-sha-256":
|
||||
public_key_ = base58.b58decode(data["public_key"])
|
||||
script_ = base58.b58decode(data["script"])
|
||||
data_ = base58.b58decode(data["data"])
|
||||
# TODO: assign to zenroom and evaluate the outcome
|
||||
ZenroomSha256(script=script_, data=data_, keys={public_key_})
|
||||
|
||||
raise UnsupportedTypeError(data.get("type"))
|
@ -1,69 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from cid import is_cid
|
||||
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.common.input import Input
|
||||
from planetmint.transactions.common.output import Output
|
||||
|
||||
|
||||
class Create(Transaction):
|
||||
|
||||
OPERATION = "CREATE"
|
||||
ALLOWED_OPERATIONS = (OPERATION,)
|
||||
|
||||
@classmethod
|
||||
def validate_create(self, tx_signers, recipients, asset, metadata):
|
||||
if not isinstance(tx_signers, list):
|
||||
raise TypeError("`tx_signers` must be a list instance")
|
||||
if not isinstance(recipients, list):
|
||||
raise TypeError("`recipients` must be a list instance")
|
||||
if len(tx_signers) == 0:
|
||||
raise ValueError("`tx_signers` list cannot be empty")
|
||||
if len(recipients) == 0:
|
||||
raise ValueError("`recipients` list cannot be empty")
|
||||
if not asset is None:
|
||||
if not isinstance(asset, dict):
|
||||
raise TypeError("`asset` must be a CID string or None")
|
||||
if "data" in asset and not is_cid(asset["data"]):
|
||||
raise TypeError("`asset` must be a CID string or None")
|
||||
if not (metadata is None or is_cid(metadata)):
|
||||
raise TypeError("`metadata` must be a CID string or None")
|
||||
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def generate(cls, tx_signers, recipients, metadata=None, asset=None):
|
||||
"""A simple way to generate a `CREATE` transaction.
|
||||
|
||||
Note:
|
||||
This method currently supports the following Cryptoconditions
|
||||
use cases:
|
||||
- Ed25519
|
||||
- ThresholdSha256
|
||||
|
||||
Additionally, it provides support for the following Planetmint
|
||||
use cases:
|
||||
- Multiple inputs and outputs.
|
||||
|
||||
Args:
|
||||
tx_signers (:obj:`list` of :obj:`str`): A list of keys that
|
||||
represent the signers of the CREATE Transaction.
|
||||
recipients (:obj:`list` of :obj:`tuple`): A list of
|
||||
([keys],amount) that represent the recipients of this
|
||||
Transaction.
|
||||
metadata (dict): The metadata to be stored along with the
|
||||
Transaction.
|
||||
asset (dict): The metadata associated with the asset that will
|
||||
be created in this Transaction.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.common.transaction.Transaction`
|
||||
"""
|
||||
|
||||
Create.validate_create(tx_signers, recipients, asset, metadata)
|
||||
(inputs, outputs) = Transaction.complete_tx_i_o(tx_signers, recipients)
|
||||
return cls(cls.OPERATION, asset, inputs, outputs, metadata)
|
@ -1,81 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.common.output import Output
|
||||
from copy import deepcopy
|
||||
|
||||
|
||||
class Transfer(Transaction):
|
||||
|
||||
OPERATION = "TRANSFER"
|
||||
ALLOWED_OPERATIONS = (OPERATION,)
|
||||
|
||||
@classmethod
|
||||
def validate_transfer(cls, inputs, recipients, asset_id, metadata):
|
||||
if not isinstance(inputs, list):
|
||||
raise TypeError("`inputs` must be a list instance")
|
||||
if len(inputs) == 0:
|
||||
raise ValueError("`inputs` must contain at least one item")
|
||||
if not isinstance(recipients, list):
|
||||
raise TypeError("`recipients` must be a list instance")
|
||||
if len(recipients) == 0:
|
||||
raise ValueError("`recipients` list cannot be empty")
|
||||
|
||||
outputs = []
|
||||
for recipient in recipients:
|
||||
if not isinstance(recipient, tuple) or len(recipient) != 2:
|
||||
raise ValueError(
|
||||
("Each `recipient` in the list must be a" " tuple of `([<list of public keys>]," " <amount>)`")
|
||||
)
|
||||
pub_keys, amount = recipient
|
||||
outputs.append(Output.generate(pub_keys, amount))
|
||||
|
||||
if not isinstance(asset_id, str):
|
||||
raise TypeError("`asset_id` must be a string")
|
||||
|
||||
return (deepcopy(inputs), outputs)
|
||||
|
||||
@classmethod
|
||||
def generate(cls, inputs, recipients, asset_id, metadata=None):
|
||||
"""A simple way to generate a `TRANSFER` transaction.
|
||||
|
||||
Note:
|
||||
Different cases for threshold conditions:
|
||||
|
||||
Combining multiple `inputs` with an arbitrary number of
|
||||
`recipients` can yield interesting cases for the creation of
|
||||
threshold conditions we'd like to support. The following
|
||||
notation is proposed:
|
||||
|
||||
1. The index of a `recipient` corresponds to the index of
|
||||
an input:
|
||||
e.g. `transfer([input1], [a])`, means `input1` would now be
|
||||
owned by user `a`.
|
||||
|
||||
2. `recipients` can (almost) get arbitrary deeply nested,
|
||||
creating various complex threshold conditions:
|
||||
e.g. `transfer([inp1, inp2], [[a, [b, c]], d])`, means
|
||||
`a`'s signature would have a 50% weight on `inp1`
|
||||
compared to `b` and `c` that share 25% of the leftover
|
||||
weight respectively. `inp2` is owned completely by `d`.
|
||||
|
||||
Args:
|
||||
inputs (:obj:`list` of :class:`~planetmint.common.transaction.
|
||||
Input`): Converted `Output`s, intended to
|
||||
be used as inputs in the transfer to generate.
|
||||
recipients (:obj:`list` of :obj:`tuple`): A list of
|
||||
([keys],amount) that represent the recipients of this
|
||||
Transaction.
|
||||
asset_id (str): The asset ID of the asset to be transferred in
|
||||
this Transaction.
|
||||
metadata (dict): Python dictionary to be stored along with the
|
||||
Transaction.
|
||||
|
||||
Returns:
|
||||
:class:`~planetmint.common.transaction.Transaction`
|
||||
"""
|
||||
(inputs, outputs) = cls.validate_transfer(inputs, recipients, asset_id, metadata)
|
||||
return cls(cls.OPERATION, {"id": asset_id}, inputs, outputs, metadata)
|
@ -1,50 +0,0 @@
|
||||
import json
|
||||
|
||||
from planetmint.transactions.common.schema import TX_SCHEMA_CHAIN_MIGRATION_ELECTION
|
||||
from planetmint.transactions.common.transaction import CHAIN_MIGRATION_ELECTION
|
||||
from planetmint.transactions.types.elections.election import Election
|
||||
|
||||
|
||||
class ChainMigrationElection(Election):
|
||||
|
||||
OPERATION = CHAIN_MIGRATION_ELECTION
|
||||
# CREATE = OPERATION
|
||||
ALLOWED_OPERATIONS = (OPERATION,)
|
||||
TX_SCHEMA_CUSTOM = TX_SCHEMA_CHAIN_MIGRATION_ELECTION
|
||||
|
||||
def has_concluded(self, planetmint, *args, **kwargs):
|
||||
chain = planetmint.get_latest_abci_chain()
|
||||
if chain is not None and not chain["is_synced"]:
|
||||
# do not conclude the migration election if
|
||||
# there is another migration in progress
|
||||
return False
|
||||
|
||||
return super().has_concluded(planetmint, *args, **kwargs)
|
||||
|
||||
def on_approval(self, planet, *args, **kwargs):
|
||||
planet.migrate_abci_chain()
|
||||
|
||||
def show_election(self, planet):
|
||||
output = super().show_election(planet)
|
||||
chain = planet.get_latest_abci_chain()
|
||||
if chain is None or chain["is_synced"]:
|
||||
return output
|
||||
|
||||
output += f'\nchain_id={chain["chain_id"]}'
|
||||
block = planet.get_latest_block()
|
||||
output += f'\napp_hash={block["app_hash"]}'
|
||||
validators = [
|
||||
{
|
||||
"pub_key": {
|
||||
"type": "tendermint/PubKeyEd25519",
|
||||
"value": k,
|
||||
},
|
||||
"power": v,
|
||||
}
|
||||
for k, v in self.get_validators(planet).items()
|
||||
]
|
||||
output += f"\nvalidators={json.dumps(validators, indent=4)}"
|
||||
return output
|
||||
|
||||
def on_rollback(self, planet, new_height):
|
||||
planet.delete_abci_chain(new_height)
|
@ -1,371 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
from collections import OrderedDict
|
||||
|
||||
import base58
|
||||
from uuid import uuid4
|
||||
|
||||
from planetmint import backend
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from planetmint.transactions.types.elections.vote import Vote
|
||||
from planetmint.transactions.common.exceptions import (
|
||||
InvalidSignature,
|
||||
MultipleInputsError,
|
||||
InvalidProposer,
|
||||
UnequalValidatorSet,
|
||||
DuplicateTransaction,
|
||||
)
|
||||
from planetmint.tendermint_utils import key_from_base64, public_key_to_base64
|
||||
from planetmint.transactions.common.crypto import public_key_from_ed25519_key
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.common.schema import _validate_schema, TX_SCHEMA_COMMON
|
||||
|
||||
|
||||
class Election(Transaction):
|
||||
"""Represents election transactions.
|
||||
|
||||
To implement a custom election, create a class deriving from this one
|
||||
with OPERATION set to the election operation, ALLOWED_OPERATIONS
|
||||
set to (OPERATION,), CREATE set to OPERATION.
|
||||
"""
|
||||
|
||||
OPERATION = None
|
||||
# Custom validation schema
|
||||
TX_SCHEMA_CUSTOM = None
|
||||
# Election Statuses:
|
||||
ONGOING = "ongoing"
|
||||
CONCLUDED = "concluded"
|
||||
INCONCLUSIVE = "inconclusive"
|
||||
# Vote ratio to approve an election
|
||||
ELECTION_THRESHOLD = 2 / 3
|
||||
|
||||
@classmethod
|
||||
def get_validator_change(cls, planet):
|
||||
"""Return the validator set from the most recent approved block
|
||||
|
||||
:return: {
|
||||
'height': <block_height>,
|
||||
'validators': <validator_set>
|
||||
}
|
||||
"""
|
||||
latest_block = planet.get_latest_block()
|
||||
if latest_block is None:
|
||||
return None
|
||||
return planet.get_validator_change(latest_block["height"])
|
||||
|
||||
@classmethod
|
||||
def get_validators(cls, planet, height=None):
|
||||
"""Return a dictionary of validators with key as `public_key` and
|
||||
value as the `voting_power`
|
||||
"""
|
||||
validators = {}
|
||||
for validator in planet.get_validators(height):
|
||||
# NOTE: we assume that Tendermint encodes public key in base64
|
||||
public_key = public_key_from_ed25519_key(key_from_base64(validator["public_key"]["value"]))
|
||||
validators[public_key] = validator["voting_power"]
|
||||
|
||||
return validators
|
||||
|
||||
@classmethod
|
||||
def recipients(cls, planet):
|
||||
"""Convert validator dictionary to a recipient list for `Transaction`"""
|
||||
|
||||
recipients = []
|
||||
for public_key, voting_power in cls.get_validators(planet).items():
|
||||
recipients.append(([public_key], voting_power))
|
||||
|
||||
return recipients
|
||||
|
||||
@classmethod
|
||||
def is_same_topology(cls, current_topology, election_topology):
|
||||
voters = {}
|
||||
for voter in election_topology:
|
||||
if len(voter.public_keys) > 1:
|
||||
return False
|
||||
|
||||
[public_key] = voter.public_keys
|
||||
voting_power = voter.amount
|
||||
voters[public_key] = voting_power
|
||||
|
||||
# Check whether the voters and their votes is same to that of the
|
||||
# validators and their voting power in the network
|
||||
return current_topology == voters
|
||||
|
||||
@classmethod
|
||||
def validate_election(self, tx_signers, recipients, asset, metadata):
|
||||
if not isinstance(tx_signers, list):
|
||||
raise TypeError("`tx_signers` must be a list instance")
|
||||
if not isinstance(recipients, list):
|
||||
raise TypeError("`recipients` must be a list instance")
|
||||
if len(tx_signers) == 0:
|
||||
raise ValueError("`tx_signers` list cannot be empty")
|
||||
if len(recipients) == 0:
|
||||
raise ValueError("`recipients` list cannot be empty")
|
||||
if not asset is None:
|
||||
if not isinstance(asset, dict):
|
||||
raise TypeError("`asset` must be a CID string or None")
|
||||
if not (metadata is None or isinstance(metadata, str)):
|
||||
# add check if metadata is ipld marshalled CID string
|
||||
raise TypeError("`metadata` must be a CID string or None")
|
||||
|
||||
return True
|
||||
|
||||
def validate(self, planet, current_transactions=[]):
|
||||
"""Validate election transaction
|
||||
|
||||
NOTE:
|
||||
* A valid election is initiated by an existing validator.
|
||||
|
||||
* A valid election is one where voters are validators and votes are
|
||||
allocated according to the voting power of each validator node.
|
||||
|
||||
Args:
|
||||
:param planet: (Planetmint) an instantiated planetmint.lib.Planetmint object.
|
||||
:param current_transactions: (list) A list of transactions to be validated along with the election
|
||||
|
||||
Returns:
|
||||
Election: a Election object or an object of the derived Election subclass.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the election is invalid
|
||||
"""
|
||||
input_conditions = []
|
||||
|
||||
duplicates = any(txn for txn in current_transactions if txn.id == self.id)
|
||||
if planet.is_committed(self.id) or duplicates:
|
||||
raise DuplicateTransaction("transaction `{}` already exists".format(self.id))
|
||||
|
||||
if not self.inputs_valid(input_conditions):
|
||||
raise InvalidSignature("Transaction signature is invalid.")
|
||||
|
||||
current_validators = self.get_validators(planet)
|
||||
|
||||
# NOTE: Proposer should be a single node
|
||||
if len(self.inputs) != 1 or len(self.inputs[0].owners_before) != 1:
|
||||
raise MultipleInputsError("`tx_signers` must be a list instance of length one")
|
||||
|
||||
# NOTE: Check if the proposer is a validator.
|
||||
[election_initiator_node_pub_key] = self.inputs[0].owners_before
|
||||
if election_initiator_node_pub_key not in current_validators.keys():
|
||||
raise InvalidProposer("Public key is not a part of the validator set")
|
||||
|
||||
# NOTE: Check if all validators have been assigned votes equal to their voting power
|
||||
if not self.is_same_topology(current_validators, self.outputs):
|
||||
raise UnequalValidatorSet("Validator set much be exactly same to the outputs of election")
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def generate(cls, initiator, voters, election_data, metadata=None):
|
||||
# Break symmetry in case we need to call an election with the same properties twice
|
||||
uuid = uuid4()
|
||||
election_data["seed"] = str(uuid)
|
||||
|
||||
Election.validate_election(initiator, voters, election_data, metadata)
|
||||
(inputs, outputs) = Transaction.complete_tx_i_o(initiator, voters)
|
||||
election = cls(cls.OPERATION, {"data": election_data}, inputs, outputs, metadata)
|
||||
cls.validate_schema(election.to_dict())
|
||||
return election
|
||||
|
||||
@classmethod
|
||||
def validate_schema(cls, tx):
|
||||
"""Validate the election transaction. Since `ELECTION` extends `CREATE` transaction, all the validations for
|
||||
`CREATE` transaction should be inherited
|
||||
"""
|
||||
_validate_schema(TX_SCHEMA_COMMON, tx)
|
||||
if cls.TX_SCHEMA_CUSTOM:
|
||||
_validate_schema(cls.TX_SCHEMA_CUSTOM, tx)
|
||||
|
||||
@classmethod
|
||||
def create(cls, tx_signers, recipients, metadata=None, asset=None):
|
||||
Create.generate(tx_signers, recipients, metadata=None, asset=None)
|
||||
|
||||
@classmethod
|
||||
def transfer(cls, tx_signers, recipients, metadata=None, asset=None):
|
||||
Transfer.generate(tx_signers, recipients, metadata=None, asset=None)
|
||||
|
||||
@classmethod
|
||||
def to_public_key(cls, election_id):
|
||||
return base58.b58encode(bytes.fromhex(election_id)).decode()
|
||||
|
||||
@classmethod
|
||||
def count_votes(cls, election_pk, transactions, getter=getattr):
|
||||
votes = 0
|
||||
for txn in transactions:
|
||||
if getter(txn, "operation") == Vote.OPERATION:
|
||||
for output in getter(txn, "outputs"):
|
||||
# NOTE: We enforce that a valid vote to election id will have only
|
||||
# election_pk in the output public keys, including any other public key
|
||||
# along with election_pk will lead to vote being not considered valid.
|
||||
if len(getter(output, "public_keys")) == 1 and [election_pk] == getter(output, "public_keys"):
|
||||
votes = votes + int(getter(output, "amount"))
|
||||
return votes
|
||||
|
||||
def get_commited_votes(self, planet, election_pk=None):
|
||||
if election_pk is None:
|
||||
election_pk = self.to_public_key(self.id)
|
||||
txns = list(backend.query.get_asset_tokens_for_public_key(planet.connection, self.id, election_pk))
|
||||
return self.count_votes(election_pk, txns, dict.get)
|
||||
|
||||
def has_concluded(self, planet, current_votes=[]):
|
||||
"""Check if the election can be concluded or not.
|
||||
|
||||
* Elections can only be concluded if the validator set has not changed
|
||||
since the election was initiated.
|
||||
* Elections can be concluded only if the current votes form a supermajority.
|
||||
|
||||
Custom elections may override this function and introduce additional checks.
|
||||
"""
|
||||
if self.has_validator_set_changed(planet):
|
||||
return False
|
||||
|
||||
election_pk = self.to_public_key(self.id)
|
||||
votes_committed = self.get_commited_votes(planet, election_pk)
|
||||
votes_current = self.count_votes(election_pk, current_votes)
|
||||
|
||||
total_votes = sum(output.amount for output in self.outputs)
|
||||
if (votes_committed < (2 / 3) * total_votes) and (votes_committed + votes_current >= (2 / 3) * total_votes):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_status(self, planet):
|
||||
election = self.get_election(self.id, planet)
|
||||
if election and election["is_concluded"]:
|
||||
return self.CONCLUDED
|
||||
|
||||
return self.INCONCLUSIVE if self.has_validator_set_changed(planet) else self.ONGOING
|
||||
|
||||
def has_validator_set_changed(self, planet):
|
||||
latest_change = self.get_validator_change(planet)
|
||||
if latest_change is None:
|
||||
return False
|
||||
|
||||
latest_change_height = latest_change["height"]
|
||||
|
||||
election = self.get_election(self.id, planet)
|
||||
|
||||
return latest_change_height > election["height"]
|
||||
|
||||
def get_election(self, election_id, planet):
|
||||
return planet.get_election(election_id)
|
||||
|
||||
def store(self, planet, height, is_concluded):
|
||||
planet.store_election(self.id, height, is_concluded)
|
||||
|
||||
def show_election(self, planet):
|
||||
data = self.asset["data"]
|
||||
if "public_key" in data.keys():
|
||||
data["public_key"] = public_key_to_base64(data["public_key"]["value"])
|
||||
response = ""
|
||||
for k, v in data.items():
|
||||
if k != "seed":
|
||||
response += f"{k}={v}\n"
|
||||
response += f"status={self.get_status(planet)}"
|
||||
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
def _get_initiated_elections(cls, height, txns):
|
||||
elections = []
|
||||
for tx in txns:
|
||||
if not isinstance(tx, Election):
|
||||
continue
|
||||
|
||||
elections.append({"election_id": tx.id, "height": height, "is_concluded": False})
|
||||
return elections
|
||||
|
||||
@classmethod
|
||||
def _get_votes(cls, txns):
|
||||
elections = OrderedDict()
|
||||
for tx in txns:
|
||||
if not isinstance(tx, Vote):
|
||||
continue
|
||||
|
||||
election_id = tx.asset["id"]
|
||||
if election_id not in elections:
|
||||
elections[election_id] = []
|
||||
elections[election_id].append(tx)
|
||||
return elections
|
||||
|
||||
@classmethod
|
||||
def process_block(cls, planet, new_height, txns):
|
||||
"""Looks for election and vote transactions inside the block, records
|
||||
and processes elections.
|
||||
|
||||
Every election is recorded in the database.
|
||||
|
||||
Every vote has a chance to conclude the corresponding election. When
|
||||
an election is concluded, the corresponding database record is
|
||||
marked as such.
|
||||
|
||||
Elections and votes are processed in the order in which they
|
||||
appear in the block. Elections are concluded in the order of
|
||||
appearance of their first votes in the block.
|
||||
|
||||
For every election concluded in the block, calls its `on_approval`
|
||||
method. The returned value of the last `on_approval`, if any,
|
||||
is a validator set update to be applied in one of the following blocks.
|
||||
|
||||
`on_approval` methods are implemented by elections of particular type.
|
||||
The method may contain side effects but should be idempotent. To account
|
||||
for other concluded elections, if it requires so, the method should
|
||||
rely on the database state.
|
||||
"""
|
||||
# elections initiated in this block
|
||||
initiated_elections = cls._get_initiated_elections(new_height, txns)
|
||||
|
||||
if initiated_elections:
|
||||
planet.store_elections(initiated_elections)
|
||||
|
||||
# elections voted for in this block and their votes
|
||||
elections = cls._get_votes(txns)
|
||||
|
||||
validator_update = None
|
||||
for election_id, votes in elections.items():
|
||||
election = planet.get_transaction(election_id)
|
||||
if election is None:
|
||||
continue
|
||||
|
||||
if not election.has_concluded(planet, votes):
|
||||
continue
|
||||
|
||||
validator_update = election.on_approval(planet, new_height)
|
||||
election.store(planet, new_height, is_concluded=True)
|
||||
|
||||
return [validator_update] if validator_update else []
|
||||
|
||||
@classmethod
|
||||
def rollback(cls, planet, new_height, txn_ids):
|
||||
"""Looks for election and vote transactions inside the block and
|
||||
cleans up the database artifacts possibly created in `process_blocks`.
|
||||
|
||||
Part of the `end_block`/`commit` crash recovery.
|
||||
"""
|
||||
|
||||
# delete election records for elections initiated at this height and
|
||||
# elections concluded at this height
|
||||
planet.delete_elections(new_height)
|
||||
|
||||
txns = [planet.get_transaction(tx_id) for tx_id in txn_ids]
|
||||
|
||||
elections = cls._get_votes(txns)
|
||||
for election_id in elections:
|
||||
election = planet.get_transaction(election_id)
|
||||
election.on_rollback(planet, new_height)
|
||||
|
||||
def on_approval(self, planet, new_height):
|
||||
"""Override to update the database state according to the
|
||||
election rules. Consider the current database state to account for
|
||||
other concluded elections, if required.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def on_rollback(self, planet, new_height):
|
||||
"""Override to clean up the database artifacts possibly created
|
||||
in `on_approval`. Part of the `end_block`/`commit` crash recovery.
|
||||
"""
|
||||
raise NotImplementedError
|
@ -1,68 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from planetmint.transactions.common.transaction import VOTE
|
||||
from planetmint.transactions.common.schema import (
|
||||
_validate_schema,
|
||||
TX_SCHEMA_COMMON,
|
||||
TX_SCHEMA_TRANSFER,
|
||||
TX_SCHEMA_VOTE,
|
||||
)
|
||||
|
||||
|
||||
class Vote(Transfer):
|
||||
|
||||
OPERATION = VOTE
|
||||
# NOTE: This class inherits TRANSFER txn type. The `TRANSFER` property is
|
||||
# overriden to re-use methods from parent class
|
||||
TRANSFER = OPERATION
|
||||
ALLOWED_OPERATIONS = (OPERATION,)
|
||||
# Custom validation schema
|
||||
TX_SCHEMA_CUSTOM = TX_SCHEMA_VOTE
|
||||
|
||||
def validate(self, planet, current_transactions=[]):
|
||||
"""Validate election vote transaction
|
||||
NOTE: There are no additional validity conditions on casting votes i.e.
|
||||
a vote is just a valid TRANFER transaction
|
||||
|
||||
For more details refer BEP-21: https://github.com/planetmint/BEPs/tree/master/21
|
||||
|
||||
Args:
|
||||
planet (Planetmint): an instantiated planetmint.lib.Planetmint object.
|
||||
|
||||
Returns:
|
||||
Vote: a Vote object
|
||||
|
||||
Raises:
|
||||
ValidationError: If the election vote is invalid
|
||||
"""
|
||||
self.validate_transfer_inputs(planet, current_transactions)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def generate(cls, inputs, recipients, election_id, metadata=None):
|
||||
(inputs, outputs) = cls.validate_transfer(inputs, recipients, election_id, metadata)
|
||||
election_vote = cls(cls.OPERATION, {"id": election_id}, inputs, outputs, metadata)
|
||||
cls.validate_schema(election_vote.to_dict())
|
||||
return election_vote
|
||||
|
||||
@classmethod
|
||||
def validate_schema(cls, tx):
|
||||
"""Validate the validator election vote transaction. Since `VOTE` extends `TRANSFER`
|
||||
transaction, all the validations for `CREATE` transaction should be inherited
|
||||
"""
|
||||
_validate_schema(TX_SCHEMA_COMMON, tx)
|
||||
_validate_schema(TX_SCHEMA_TRANSFER, tx)
|
||||
_validate_schema(cls.TX_SCHEMA_CUSTOM, tx)
|
||||
|
||||
@classmethod
|
||||
def create(cls, tx_signers, recipients, metadata=None, asset=None):
|
||||
return Create.generate(tx_signers, recipients, metadata=None, asset=None)
|
||||
|
||||
@classmethod
|
||||
def transfer(cls, tx_signers, recipients, metadata=None, asset=None):
|
||||
return Transfer.generate(tx_signers, recipients, metadata=None, asset=None)
|
@ -1,7 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
|
||||
from planetmint.upsert_validator.validator_election import ValidatorElection # noqa
|
@ -1,66 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from planetmint.transactions.common.exceptions import InvalidPowerChange
|
||||
from planetmint.transactions.types.elections.election import Election
|
||||
from planetmint.transactions.common.schema import TX_SCHEMA_VALIDATOR_ELECTION
|
||||
from planetmint.transactions.common.transaction import VALIDATOR_ELECTION
|
||||
|
||||
# from planetmint.transactions.common.transaction import Transaction
|
||||
|
||||
from .validator_utils import new_validator_set, encode_validator, validate_asset_public_key
|
||||
|
||||
|
||||
class ValidatorElection(Election):
|
||||
|
||||
OPERATION = VALIDATOR_ELECTION
|
||||
ALLOWED_OPERATIONS = (OPERATION,)
|
||||
TX_SCHEMA_CUSTOM = TX_SCHEMA_VALIDATOR_ELECTION
|
||||
|
||||
def validate(self, planet, current_transactions=[]):
|
||||
"""For more details refer BEP-21: https://github.com/planetmint/BEPs/tree/master/21"""
|
||||
|
||||
current_validators = self.get_validators(planet)
|
||||
|
||||
super(ValidatorElection, self).validate(planet, current_transactions=current_transactions)
|
||||
|
||||
# NOTE: change more than 1/3 of the current power is not allowed
|
||||
if self.asset["data"]["power"] >= (1 / 3) * sum(current_validators.values()):
|
||||
raise InvalidPowerChange("`power` change must be less than 1/3 of total power")
|
||||
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def validate_schema(cls, tx):
|
||||
super(ValidatorElection, cls).validate_schema(tx)
|
||||
validate_asset_public_key(tx["asset"]["data"]["public_key"])
|
||||
|
||||
def has_concluded(self, planet, *args, **kwargs):
|
||||
latest_block = planet.get_latest_block()
|
||||
if latest_block is not None:
|
||||
latest_block_height = latest_block["height"]
|
||||
latest_validator_change = planet.get_validator_change()["height"]
|
||||
|
||||
# TODO change to `latest_block_height + 3` when upgrading to Tendermint 0.24.0.
|
||||
if latest_validator_change == latest_block_height + 2:
|
||||
# do not conclude the election if there is a change assigned already
|
||||
return False
|
||||
|
||||
return super().has_concluded(planet, *args, **kwargs)
|
||||
|
||||
def on_approval(self, planet, new_height):
|
||||
validator_updates = [self.asset["data"]]
|
||||
curr_validator_set = planet.get_validators(new_height)
|
||||
updated_validator_set = new_validator_set(curr_validator_set, validator_updates)
|
||||
|
||||
updated_validator_set = [v for v in updated_validator_set if v["voting_power"] > 0]
|
||||
|
||||
# TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0.
|
||||
planet.store_validator_set(new_height + 1, updated_validator_set)
|
||||
return encode_validator(self.asset["data"])
|
||||
|
||||
def on_rollback(self, planetmint, new_height):
|
||||
# TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0.
|
||||
planetmint.delete_validator_set(new_height + 1)
|
@ -1,79 +0,0 @@
|
||||
import base64
|
||||
import binascii
|
||||
import codecs
|
||||
|
||||
from tendermint.abci import types_pb2
|
||||
from tendermint.crypto import keys_pb2
|
||||
from planetmint.transactions.common.exceptions import InvalidPublicKey
|
||||
|
||||
|
||||
def encode_validator(v):
|
||||
ed25519_public_key = v["public_key"]["value"]
|
||||
pub_key = keys_pb2.PublicKey(ed25519=bytes.fromhex(ed25519_public_key))
|
||||
|
||||
return types_pb2.ValidatorUpdate(pub_key=pub_key, power=v["power"])
|
||||
|
||||
|
||||
def decode_validator(v):
|
||||
return {
|
||||
"public_key": {
|
||||
"type": "ed25519-base64",
|
||||
"value": codecs.encode(v.pub_key.ed25519, "base64").decode().rstrip("\n"),
|
||||
},
|
||||
"voting_power": v.power,
|
||||
}
|
||||
|
||||
|
||||
def new_validator_set(validators, updates):
|
||||
validators_dict = {}
|
||||
for v in validators:
|
||||
validators_dict[v["public_key"]["value"]] = v
|
||||
|
||||
updates_dict = {}
|
||||
for u in updates:
|
||||
decoder = get_public_key_decoder(u["public_key"])
|
||||
public_key64 = base64.b64encode(decoder(u["public_key"]["value"])).decode("utf-8")
|
||||
updates_dict[public_key64] = {
|
||||
"public_key": {"type": "ed25519-base64", "value": public_key64},
|
||||
"voting_power": u["power"],
|
||||
}
|
||||
|
||||
new_validators_dict = {**validators_dict, **updates_dict}
|
||||
return list(new_validators_dict.values())
|
||||
|
||||
|
||||
def encode_pk_to_base16(validator):
|
||||
pk = validator["public_key"]
|
||||
decoder = get_public_key_decoder(pk)
|
||||
public_key16 = base64.b16encode(decoder(pk["value"])).decode("utf-8")
|
||||
|
||||
validator["public_key"]["value"] = public_key16
|
||||
return validator
|
||||
|
||||
|
||||
def validate_asset_public_key(pk):
|
||||
pk_binary = pk["value"].encode("utf-8")
|
||||
decoder = get_public_key_decoder(pk)
|
||||
try:
|
||||
pk_decoded = decoder(pk_binary)
|
||||
if len(pk_decoded) != 32:
|
||||
raise InvalidPublicKey("Public key should be of size 32 bytes")
|
||||
|
||||
except binascii.Error:
|
||||
raise InvalidPublicKey("Invalid `type` specified for public key `value`")
|
||||
|
||||
|
||||
def get_public_key_decoder(pk):
|
||||
encoding = pk["type"]
|
||||
decoder = base64.b64decode
|
||||
|
||||
if encoding == "ed25519-base16":
|
||||
decoder = base64.b16decode
|
||||
elif encoding == "ed25519-base32":
|
||||
decoder = base64.b32decode
|
||||
elif encoding == "ed25519-base64":
|
||||
decoder = base64.b64decode
|
||||
else:
|
||||
raise InvalidPublicKey("Invalid `type` specified for public key `value`")
|
||||
|
||||
return decoder
|
@ -8,12 +8,12 @@ import threading
|
||||
import queue
|
||||
import multiprocessing as mp
|
||||
import json
|
||||
|
||||
import setproctitle
|
||||
|
||||
from packaging import version
|
||||
from planetmint.version import __tm_supported_versions__
|
||||
from planetmint.tendermint_utils import key_from_base64
|
||||
from planetmint.transactions.common.crypto import key_pair_from_ed25519_key
|
||||
from transactions.common.crypto import key_pair_from_ed25519_key
|
||||
|
||||
|
||||
class ProcessGroup(object):
|
||||
|
@ -10,11 +10,10 @@ The application is implemented in Flask and runs using Gunicorn.
|
||||
|
||||
import copy
|
||||
import multiprocessing
|
||||
import gunicorn.app.base
|
||||
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
import gunicorn.app.base
|
||||
|
||||
from planetmint import utils
|
||||
from planetmint import Planetmint
|
||||
from planetmint.web.routes import add_routes
|
||||
|
@ -11,7 +11,6 @@ import logging
|
||||
|
||||
from flask_restful import reqparse, Resource
|
||||
from flask import current_app
|
||||
|
||||
from planetmint.backend.exceptions import OperationError
|
||||
from planetmint.web.views.base import make_error
|
||||
|
||||
|
@ -8,7 +8,6 @@
|
||||
import logging
|
||||
|
||||
from flask import jsonify, request
|
||||
|
||||
from planetmint.config import Config
|
||||
|
||||
|
||||
|
@ -9,7 +9,6 @@ For more information please refer to the documentation: http://planetmint.io/htt
|
||||
"""
|
||||
from flask import current_app
|
||||
from flask_restful import Resource, reqparse
|
||||
|
||||
from planetmint.web.views.base import make_error
|
||||
|
||||
|
||||
|
@ -6,8 +6,8 @@
|
||||
"""API Index endpoint"""
|
||||
|
||||
import flask
|
||||
from flask_restful import Resource
|
||||
|
||||
from flask_restful import Resource
|
||||
from planetmint.web.views.base import base_ws_uri
|
||||
from planetmint import version
|
||||
from planetmint.web.websocket_server import EVENTS_ENDPOINT, EVENTS_ENDPOINT_BLOCKS
|
||||
|
@ -11,7 +11,6 @@ import logging
|
||||
|
||||
from flask_restful import reqparse, Resource
|
||||
from flask import current_app
|
||||
|
||||
from planetmint.backend.exceptions import OperationError
|
||||
from planetmint.web.views.base import make_error
|
||||
|
||||
|
@ -5,7 +5,6 @@
|
||||
|
||||
from flask import current_app
|
||||
from flask_restful import reqparse, Resource
|
||||
|
||||
from planetmint.web.views import parameters
|
||||
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
import re
|
||||
|
||||
from planetmint.transactions.common.transaction_mode_types import (
|
||||
from transactions.common.transaction_mode_types import (
|
||||
BROADCAST_TX_COMMIT,
|
||||
BROADCAST_TX_ASYNC,
|
||||
BROADCAST_TX_SYNC,
|
||||
|
@ -11,15 +11,14 @@ import logging
|
||||
|
||||
from flask import current_app, request, jsonify
|
||||
from flask_restful import Resource, reqparse
|
||||
|
||||
from planetmint.transactions.common.transaction_mode_types import BROADCAST_TX_ASYNC
|
||||
from planetmint.transactions.common.exceptions import (
|
||||
from transactions.common.transaction_mode_types import BROADCAST_TX_ASYNC
|
||||
from transactions.common.exceptions import (
|
||||
SchemaValidationError,
|
||||
ValidationError,
|
||||
)
|
||||
from planetmint.web.views.base import make_error
|
||||
from planetmint.web.views import parameters
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from transactions.common.transaction import Transaction
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
|
||||
import json
|
||||
|
||||
from planetmint.events import EventTypes
|
||||
from planetmint.events import POISON_PILL
|
||||
|
||||
|
@ -21,7 +21,6 @@ import logging
|
||||
import threading
|
||||
import aiohttp
|
||||
|
||||
|
||||
from uuid import uuid4
|
||||
from concurrent.futures import CancelledError
|
||||
from planetmint.config import Config
|
||||
|
7
setup.py
7
setup.py
@ -116,14 +116,14 @@ install_requires = [
|
||||
"flask-restful==0.3.9",
|
||||
"flask==2.1.2",
|
||||
"gunicorn==20.1.0",
|
||||
"jsonschema==3.2.0",
|
||||
"jsonschema==4.16.0",
|
||||
"logstats==0.3.0",
|
||||
"packaging>=20.9",
|
||||
# TODO Consider not installing the db drivers, or putting them in extras.
|
||||
"pymongo==3.11.4",
|
||||
"tarantool==0.7.1",
|
||||
"python-rapidjson>=1.0",
|
||||
"pyyaml==5.4.1",
|
||||
"pyyaml==6.0.0",
|
||||
"requests==2.25.1",
|
||||
"setproctitle==1.2.2",
|
||||
"werkzeug==2.0.3",
|
||||
@ -136,6 +136,7 @@ install_requires = [
|
||||
"PyNaCl==1.4.0",
|
||||
"pyasn1>=0.4.8",
|
||||
"cryptography==3.4.7",
|
||||
"planetmint-transactions==0.1.0",
|
||||
]
|
||||
|
||||
setup(
|
||||
@ -176,7 +177,7 @@ setup(
|
||||
"docs": docs_require,
|
||||
},
|
||||
package_data={
|
||||
"planetmint.transactions.common.schema": [
|
||||
"transactions.common.schema": [
|
||||
"v1.0/*.yaml",
|
||||
"v2.0/*.yaml",
|
||||
"v3.0/*.yaml",
|
||||
|
@ -4,9 +4,9 @@
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import pytest
|
||||
import random
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
|
||||
|
||||
def test_asset_transfer(b, signed_create_tx, user_pk, user_sk):
|
||||
@ -15,12 +15,12 @@ def test_asset_transfer(b, signed_create_tx, user_pk, user_sk):
|
||||
|
||||
b.store_bulk_transactions([signed_create_tx])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert tx_transfer_signed.asset["id"] == signed_create_tx.id
|
||||
|
||||
|
||||
def test_validate_transfer_asset_id_mismatch(b, signed_create_tx, user_pk, user_sk):
|
||||
from planetmint.transactions.common.exceptions import AssetIdMismatch
|
||||
from transactions.common.exceptions import AssetIdMismatch
|
||||
|
||||
tx_transfer = Transfer.generate(signed_create_tx.to_inputs(), [([user_pk], 1)], signed_create_tx.id)
|
||||
tx_transfer.asset["id"] = "a" * 64
|
||||
@ -29,18 +29,18 @@ def test_validate_transfer_asset_id_mismatch(b, signed_create_tx, user_pk, user_
|
||||
b.store_bulk_transactions([signed_create_tx])
|
||||
|
||||
with pytest.raises(AssetIdMismatch):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
def test_get_asset_id_create_transaction(alice, user_pk):
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from transactions.common.transaction import Transaction
|
||||
|
||||
tx_create = Create.generate([alice.public_key], [([user_pk], 1)])
|
||||
assert Transaction.get_asset_id(tx_create) == tx_create.id
|
||||
|
||||
|
||||
def test_get_asset_id_transfer_transaction(b, signed_create_tx, user_pk):
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from transactions.common.transaction import Transaction
|
||||
|
||||
tx_transfer = Transfer.generate(signed_create_tx.to_inputs(), [([user_pk], 1)], signed_create_tx.id)
|
||||
asset_id = Transaction.get_asset_id(tx_transfer)
|
||||
@ -48,8 +48,8 @@ def test_get_asset_id_transfer_transaction(b, signed_create_tx, user_pk):
|
||||
|
||||
|
||||
def test_asset_id_mismatch(alice, user_pk):
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.common.exceptions import AssetIdMismatch
|
||||
from transactions.common.transaction import Transaction
|
||||
from transactions.common.exceptions import AssetIdMismatch
|
||||
|
||||
tx1 = Create.generate(
|
||||
[alice.public_key], [([user_pk], 1)], metadata="QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"
|
||||
@ -65,7 +65,6 @@ def test_asset_id_mismatch(alice, user_pk):
|
||||
|
||||
|
||||
def test_create_valid_divisible_asset(b, user_pk, user_sk):
|
||||
|
||||
tx = Create.generate([user_pk], [([user_pk], 2)])
|
||||
tx_signed = tx.sign([user_sk])
|
||||
assert tx_signed.validate(b) == tx_signed
|
||||
assert b.validate_transaction(tx_signed) == tx_signed
|
||||
|
@ -5,11 +5,10 @@
|
||||
|
||||
|
||||
import pytest
|
||||
import random
|
||||
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from planetmint.transactions.common.exceptions import DoubleSpend
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
from transactions.common.exceptions import DoubleSpend
|
||||
|
||||
|
||||
# CREATE divisible asset
|
||||
@ -18,13 +17,12 @@ from planetmint.transactions.common.exceptions import DoubleSpend
|
||||
# Single output
|
||||
# Single owners_after
|
||||
def test_single_in_single_own_single_out_single_own_create(alice, user_pk, b):
|
||||
|
||||
tx = Create.generate(
|
||||
[alice.public_key], [([user_pk], 100)], asset={"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"}
|
||||
)
|
||||
tx_signed = tx.sign([alice.private_key])
|
||||
|
||||
assert tx_signed.validate(b) == tx_signed
|
||||
assert b.validate_transaction(tx_signed) == tx_signed
|
||||
assert len(tx_signed.outputs) == 1
|
||||
assert tx_signed.outputs[0].amount == 100
|
||||
assert len(tx_signed.inputs) == 1
|
||||
@ -44,7 +42,7 @@ def test_single_in_single_own_multiple_out_single_own_create(alice, user_pk, b):
|
||||
)
|
||||
tx_signed = tx.sign([alice.private_key])
|
||||
|
||||
assert tx_signed.validate(b) == tx_signed
|
||||
assert b.validate_transaction(tx_signed) == tx_signed
|
||||
assert len(tx_signed.outputs) == 2
|
||||
assert tx_signed.outputs[0].amount == 50
|
||||
assert tx_signed.outputs[1].amount == 50
|
||||
@ -65,7 +63,7 @@ def test_single_in_single_own_single_out_multiple_own_create(alice, user_pk, b):
|
||||
)
|
||||
tx_signed = tx.sign([alice.private_key])
|
||||
|
||||
assert tx_signed.validate(b) == tx_signed
|
||||
assert b.validate_transaction(tx_signed) == tx_signed
|
||||
assert len(tx_signed.outputs) == 1
|
||||
assert tx_signed.outputs[0].amount == 100
|
||||
|
||||
@ -91,7 +89,7 @@ def test_single_in_single_own_multiple_out_mix_own_create(alice, user_pk, b):
|
||||
)
|
||||
tx_signed = tx.sign([alice.private_key])
|
||||
|
||||
assert tx_signed.validate(b) == tx_signed
|
||||
assert b.validate_transaction(tx_signed) == tx_signed
|
||||
assert len(tx_signed.outputs) == 2
|
||||
assert tx_signed.outputs[0].amount == 50
|
||||
assert tx_signed.outputs[1].amount == 50
|
||||
@ -108,7 +106,7 @@ def test_single_in_single_own_multiple_out_mix_own_create(alice, user_pk, b):
|
||||
# Multiple owners_before
|
||||
# Output combinations already tested above
|
||||
def test_single_in_multiple_own_single_out_single_own_create(alice, b, user_pk, user_sk):
|
||||
from planetmint.transactions.common.utils import _fulfillment_to_details
|
||||
from transactions.common.utils import _fulfillment_to_details
|
||||
|
||||
tx = Create.generate(
|
||||
[alice.public_key, user_pk],
|
||||
@ -116,7 +114,7 @@ def test_single_in_multiple_own_single_out_single_own_create(alice, b, user_pk,
|
||||
asset={"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"},
|
||||
)
|
||||
tx_signed = tx.sign([alice.private_key, user_sk])
|
||||
assert tx_signed.validate(b) == tx_signed
|
||||
assert b.validate_transaction(tx_signed) == tx_signed
|
||||
assert len(tx_signed.outputs) == 1
|
||||
assert tx_signed.outputs[0].amount == 100
|
||||
assert len(tx_signed.inputs) == 1
|
||||
@ -145,7 +143,7 @@ def test_single_in_single_own_single_out_single_own_transfer(alice, b, user_pk,
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b)
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 1
|
||||
assert tx_transfer_signed.outputs[0].amount == 100
|
||||
assert len(tx_transfer_signed.inputs) == 1
|
||||
@ -172,7 +170,7 @@ def test_single_in_single_own_multiple_out_single_own_transfer(alice, b, user_pk
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 2
|
||||
assert tx_transfer_signed.outputs[0].amount == 50
|
||||
assert tx_transfer_signed.outputs[1].amount == 50
|
||||
@ -200,7 +198,7 @@ def test_single_in_single_own_single_out_multiple_own_transfer(alice, b, user_pk
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 1
|
||||
assert tx_transfer_signed.outputs[0].amount == 100
|
||||
|
||||
@ -211,7 +209,7 @@ def test_single_in_single_own_single_out_multiple_own_transfer(alice, b, user_pk
|
||||
assert len(tx_transfer_signed.inputs) == 1
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
# TRANSFER divisible asset
|
||||
@ -238,7 +236,7 @@ def test_single_in_single_own_multiple_out_mix_own_transfer(alice, b, user_pk, u
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 2
|
||||
assert tx_transfer_signed.outputs[0].amount == 50
|
||||
assert tx_transfer_signed.outputs[1].amount == 50
|
||||
@ -251,7 +249,7 @@ def test_single_in_single_own_multiple_out_mix_own_transfer(alice, b, user_pk, u
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
# TRANSFER divisible asset
|
||||
@ -260,7 +258,7 @@ def test_single_in_single_own_multiple_out_mix_own_transfer(alice, b, user_pk, u
|
||||
# Single output
|
||||
# Single owners_after
|
||||
def test_single_in_multiple_own_single_out_single_own_transfer(alice, b, user_pk, user_sk):
|
||||
from planetmint.transactions.common.utils import _fulfillment_to_details
|
||||
from transactions.common.utils import _fulfillment_to_details
|
||||
|
||||
# CREATE divisible asset
|
||||
tx_create = Create.generate(
|
||||
@ -276,7 +274,7 @@ def test_single_in_multiple_own_single_out_single_own_transfer(alice, b, user_pk
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 1
|
||||
assert tx_transfer_signed.outputs[0].amount == 100
|
||||
assert len(tx_transfer_signed.inputs) == 1
|
||||
@ -287,7 +285,7 @@ def test_single_in_multiple_own_single_out_single_own_transfer(alice, b, user_pk
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
# TRANSFER divisible asset
|
||||
@ -310,14 +308,14 @@ def test_multiple_in_single_own_single_out_single_own_transfer(alice, b, user_pk
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b)
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 1
|
||||
assert tx_transfer_signed.outputs[0].amount == 100
|
||||
assert len(tx_transfer_signed.inputs) == 2
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
# TRANSFER divisible asset
|
||||
@ -326,7 +324,7 @@ def test_multiple_in_single_own_single_out_single_own_transfer(alice, b, user_pk
|
||||
# Single output
|
||||
# Single owners_after
|
||||
def test_multiple_in_multiple_own_single_out_single_own_transfer(alice, b, user_pk, user_sk):
|
||||
from planetmint.transactions.common.utils import _fulfillment_to_details
|
||||
from transactions.common.utils import _fulfillment_to_details
|
||||
|
||||
# CREATE divisible asset
|
||||
tx_create = Create.generate(
|
||||
@ -342,7 +340,7 @@ def test_multiple_in_multiple_own_single_out_single_own_transfer(alice, b, user_
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 1
|
||||
assert tx_transfer_signed.outputs[0].amount == 100
|
||||
assert len(tx_transfer_signed.inputs) == 2
|
||||
@ -356,7 +354,7 @@ def test_multiple_in_multiple_own_single_out_single_own_transfer(alice, b, user_
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
# TRANSFER divisible asset
|
||||
@ -366,7 +364,7 @@ def test_multiple_in_multiple_own_single_out_single_own_transfer(alice, b, user_
|
||||
# Single output
|
||||
# Single owners_after
|
||||
def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(alice, b, user_pk, user_sk):
|
||||
from planetmint.transactions.common.utils import _fulfillment_to_details
|
||||
from transactions.common.utils import _fulfillment_to_details
|
||||
|
||||
# CREATE divisible asset
|
||||
tx_create = Create.generate(
|
||||
@ -381,7 +379,7 @@ def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(alice, b, user_pk
|
||||
tx_transfer_signed = tx_transfer.sign([alice.private_key, user_sk])
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 1
|
||||
assert tx_transfer_signed.outputs[0].amount == 100
|
||||
assert len(tx_transfer_signed.inputs) == 2
|
||||
@ -394,7 +392,7 @@ def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(alice, b, user_pk
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
# TRANSFER divisible asset
|
||||
@ -405,7 +403,7 @@ def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(alice, b, user_pk
|
||||
# Mix: one output with a single owners_after, one output with multiple
|
||||
# owners_after
|
||||
def test_muiltiple_in_mix_own_multiple_out_mix_own_transfer(alice, b, user_pk, user_sk):
|
||||
from planetmint.transactions.common.utils import _fulfillment_to_details
|
||||
from transactions.common.utils import _fulfillment_to_details
|
||||
|
||||
# CREATE divisible asset
|
||||
tx_create = Create.generate(
|
||||
@ -421,7 +419,7 @@ def test_muiltiple_in_mix_own_multiple_out_mix_own_transfer(alice, b, user_pk, u
|
||||
tx_transfer_signed = tx_transfer.sign([alice.private_key, user_sk])
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 2
|
||||
assert tx_transfer_signed.outputs[0].amount == 50
|
||||
assert tx_transfer_signed.outputs[1].amount == 50
|
||||
@ -441,7 +439,7 @@ def test_muiltiple_in_mix_own_multiple_out_mix_own_transfer(alice, b, user_pk, u
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
# TRANSFER divisible asset
|
||||
@ -478,7 +476,7 @@ def test_multiple_in_different_transactions(alice, b, user_pk, user_sk):
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed, tx_transfer1_signed])
|
||||
|
||||
assert tx_transfer2_signed.validate(b) == tx_transfer2_signed
|
||||
assert b.validate_transaction(tx_transfer2_signed) == tx_transfer2_signed
|
||||
assert len(tx_transfer2_signed.outputs) == 1
|
||||
assert tx_transfer2_signed.outputs[0].amount == 100
|
||||
assert len(tx_transfer2_signed.inputs) == 2
|
||||
@ -493,7 +491,7 @@ def test_multiple_in_different_transactions(alice, b, user_pk, user_sk):
|
||||
# inputs needs to match the amount being sent in the outputs.
|
||||
# In other words `amount_in_inputs - amount_in_outputs == 0`
|
||||
def test_amount_error_transfer(alice, b, user_pk, user_sk):
|
||||
from planetmint.transactions.common.exceptions import AmountError
|
||||
from transactions.common.exceptions import AmountError
|
||||
|
||||
# CREATE divisible asset
|
||||
tx_create = Create.generate(
|
||||
@ -509,7 +507,7 @@ def test_amount_error_transfer(alice, b, user_pk, user_sk):
|
||||
tx_transfer_signed = tx_transfer.sign([user_sk])
|
||||
|
||||
with pytest.raises(AmountError):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
# TRANSFER
|
||||
# output amount greater than input amount
|
||||
@ -517,7 +515,7 @@ def test_amount_error_transfer(alice, b, user_pk, user_sk):
|
||||
tx_transfer_signed = tx_transfer.sign([user_sk])
|
||||
|
||||
with pytest.raises(AmountError):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
def test_threshold_same_public_key(alice, b, user_pk, user_sk):
|
||||
@ -541,11 +539,13 @@ def test_threshold_same_public_key(alice, b, user_pk, user_sk):
|
||||
tx_transfer_signed = tx_transfer.sign([user_sk, user_sk])
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
# assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
# tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
def test_sum_amount(alice, b, user_pk, user_sk):
|
||||
@ -565,13 +565,13 @@ def test_sum_amount(alice, b, user_pk, user_sk):
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 1
|
||||
assert tx_transfer_signed.outputs[0].amount == 3
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
def test_divide(alice, b, user_pk, user_sk):
|
||||
@ -593,11 +593,11 @@ def test_divide(alice, b, user_pk, user_sk):
|
||||
|
||||
b.store_bulk_transactions([tx_create_signed])
|
||||
|
||||
assert tx_transfer_signed.validate(b) == tx_transfer_signed
|
||||
assert b.validate_transaction(tx_transfer_signed) == tx_transfer_signed
|
||||
assert len(tx_transfer_signed.outputs) == 3
|
||||
for output in tx_transfer_signed.outputs:
|
||||
assert output.amount == 1
|
||||
|
||||
b.store_bulk_transactions([tx_transfer_signed])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
@ -1,11 +1,10 @@
|
||||
import pytest
|
||||
import json
|
||||
import base58
|
||||
|
||||
from hashlib import sha3_256
|
||||
from zenroom import zencode_exec
|
||||
from cryptoconditions.types.ed25519 import Ed25519Sha256
|
||||
from cryptoconditions.types.zenroom import ZenroomSha256
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from ipld import multihash, marshal
|
||||
|
||||
CONDITION_SCRIPT = """Scenario 'ecdh': create the signature of an object
|
||||
@ -150,9 +149,9 @@ def test_zenroom_signing():
|
||||
shared_creation_txid = sha3_256(json_str_tx.encode()).hexdigest()
|
||||
tx["id"] = shared_creation_txid
|
||||
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from transactions.common.transaction import Transaction
|
||||
from planetmint.lib import Planetmint
|
||||
from planetmint.transactions.common.exceptions import (
|
||||
from transactions.common.exceptions import (
|
||||
SchemaValidationError,
|
||||
ValidationError,
|
||||
)
|
||||
|
@ -4,8 +4,8 @@
|
||||
# # # Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
#
|
||||
# from copy import deepcopy
|
||||
# from planetmint.transactions.types.assets.create import Create
|
||||
# from planetmint.transactions.types.assets.transfer import Transfer
|
||||
# from transactions.types.assets.create import Create
|
||||
# from transactions.types.assets.transfer import Transfer
|
||||
#
|
||||
# # import pytest
|
||||
# # import pymongo
|
||||
@ -238,7 +238,7 @@
|
||||
# @pytest.mark.skip
|
||||
# def test_get_spending_transactions_multiple_inputs():
|
||||
# from planetmint.backend import connect, query
|
||||
# from planetmint.transactions.common.crypto import generate_key_pair
|
||||
# from transactions.common.crypto import generate_key_pair
|
||||
# conn = connect()
|
||||
# (alice_sk, alice_pk) = generate_key_pair()
|
||||
# (bob_sk, bob_pk) = generate_key_pair()
|
||||
|
@ -7,9 +7,9 @@ from copy import deepcopy
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from transactions.common.transaction import Transaction
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
|
||||
pytestmark = pytest.mark.bdb
|
||||
|
||||
@ -228,7 +228,7 @@ def test_get_spending_transactions(user_pk, user_sk, db_conn):
|
||||
|
||||
|
||||
def test_get_spending_transactions_multiple_inputs(db_conn):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from planetmint.backend.tarantool import query
|
||||
|
||||
(alice_sk, alice_pk) = generate_key_pair()
|
||||
|
@ -7,7 +7,7 @@ import pytest
|
||||
|
||||
|
||||
def test_get_connection_raises_a_configuration_error(monkeypatch):
|
||||
from planetmint.transactions.common.exceptions import ConfigurationError
|
||||
from transactions.common.exceptions import ConfigurationError
|
||||
from planetmint.backend.connection import connect
|
||||
|
||||
with pytest.raises(ConfigurationError):
|
||||
|
@ -3,11 +3,11 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import pytest
|
||||
|
||||
from functools import singledispatch
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_module():
|
||||
|
@ -3,9 +3,9 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from argparse import Namespace
|
||||
import pytest
|
||||
|
||||
from argparse import Namespace
|
||||
from planetmint.config import Config
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ def mock_processes_start(monkeypatch):
|
||||
|
||||
@pytest.fixture
|
||||
def mock_generate_key_pair(monkeypatch):
|
||||
monkeypatch.setattr("planetmint.transactions.common.crypto.generate_key_pair", lambda: ("privkey", "pubkey"))
|
||||
monkeypatch.setattr("transactions.common.crypto.generate_key_pair", lambda: ("privkey", "pubkey"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -5,19 +5,15 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import pytest
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from argparse import Namespace
|
||||
|
||||
import pytest
|
||||
|
||||
from planetmint.config import Config
|
||||
from planetmint import ValidatorElection
|
||||
from planetmint.commands.planetmint import run_election_show
|
||||
from planetmint.transactions.types.elections.election import Election
|
||||
from planetmint.lib import Block
|
||||
from planetmint.transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
|
||||
from transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from tests.utils import generate_election, generate_validators
|
||||
|
||||
|
||||
@ -137,7 +133,7 @@ def test_drop_db_when_interactive_yes(mock_db_drop, monkeypatch):
|
||||
|
||||
@patch("planetmint.backend.schema.drop_database")
|
||||
def test_drop_db_when_db_does_not_exist(mock_db_drop, capsys):
|
||||
from planetmint.transactions.common.exceptions import DatabaseDoesNotExist
|
||||
from transactions.common.exceptions import DatabaseDoesNotExist
|
||||
from planetmint.commands.planetmint import run_drop
|
||||
|
||||
args = Namespace(config=None, yes=True)
|
||||
@ -263,7 +259,7 @@ def test_recover_db_on_start(mock_run_recover, mock_start, mocked_setup_logging)
|
||||
@pytest.mark.bdb
|
||||
def test_run_recover(b, alice, bob):
|
||||
from planetmint.commands.planetmint import run_recover
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from transactions.types.assets.create import Create
|
||||
from planetmint.lib import Block
|
||||
from planetmint.backend import query
|
||||
|
||||
@ -408,7 +404,7 @@ def test_election_new_upsert_validator_invalid_election(caplog, b, priv_validato
|
||||
@pytest.mark.bdb
|
||||
def test_election_new_upsert_validator_invalid_power(caplog, b, priv_validator_path, user_sk):
|
||||
from planetmint.commands.planetmint import run_election_new_upsert_validator
|
||||
from planetmint.transactions.common.exceptions import InvalidPowerChange
|
||||
from transactions.common.exceptions import InvalidPowerChange
|
||||
|
||||
def mock_write(tx, mode):
|
||||
b.store_bulk_transactions([tx])
|
||||
@ -524,7 +520,7 @@ def test_chain_migration_election_show_shows_inconclusive(b):
|
||||
|
||||
assert not run_election_show(Namespace(election_id=election.id), b)
|
||||
|
||||
Election.process_block(b, 1, [election])
|
||||
b.process_block(1, [election])
|
||||
b.store_bulk_transactions([election])
|
||||
|
||||
assert run_election_show(Namespace(election_id=election.id), b) == "status=ongoing"
|
||||
@ -554,13 +550,13 @@ def test_chain_migration_election_show_shows_concluded(b):
|
||||
assert not run_election_show(Namespace(election_id=election.id), b)
|
||||
|
||||
b.store_bulk_transactions([election])
|
||||
Election.process_block(b, 1, [election])
|
||||
b.process_block(1, [election])
|
||||
|
||||
assert run_election_show(Namespace(election_id=election.id), b) == "status=ongoing"
|
||||
|
||||
b.store_abci_chain(1, "chain-X")
|
||||
b.store_block(Block(height=1, transactions=[v.id for v in votes], app_hash="last_app_hash")._asdict())
|
||||
Election.process_block(b, 2, votes)
|
||||
b.process_block(2, votes)
|
||||
|
||||
assert (
|
||||
run_election_show(Namespace(election_id=election.id), b)
|
||||
@ -611,7 +607,7 @@ def call_election(b, new_validator, node_key):
|
||||
b.write_transaction = mock_write
|
||||
|
||||
# our voters is a list of length 1, populated from our mocked validator
|
||||
voters = ValidatorElection.recipients(b)
|
||||
voters = b.get_recipients_list()
|
||||
# and our voter is the public key from the voter list
|
||||
voter = node_key.public_key
|
||||
valid_election = ValidatorElection.generate([voter], voters, new_validator, None).sign([node_key.private_key])
|
||||
|
@ -4,10 +4,10 @@
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import argparse
|
||||
from argparse import Namespace
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
from argparse import Namespace
|
||||
from planetmint.config import Config
|
||||
from unittest.mock import patch
|
||||
|
||||
|
@ -1,311 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from base58 import b58decode
|
||||
import pytest
|
||||
from cryptoconditions import ThresholdSha256, Ed25519Sha256
|
||||
|
||||
|
||||
USER_PRIVATE_KEY = "8eJ8q9ZQpReWyQT5aFCiwtZ5wDZC4eDnCen88p3tQ6ie"
|
||||
USER_PUBLIC_KEY = "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE"
|
||||
|
||||
USER2_PRIVATE_KEY = "F86PQPiqMTwM2Qi2Sda3U4Vdh3AgadMdX3KNVsu5wNJr"
|
||||
USER2_PUBLIC_KEY = "GDxwMFbwdATkQELZbMfW8bd9hbNYMZLyVXA3nur2aNbE"
|
||||
|
||||
USER3_PRIVATE_KEY = "4rNQFzWQbVwuTiDVxwuFMvLG5zd8AhrQKCtVovBvcYsB"
|
||||
USER3_PUBLIC_KEY = "Gbrg7JtxdjedQRmr81ZZbh1BozS7fBW88ZyxNDy7WLNC"
|
||||
|
||||
CC_FULFILLMENT_URI = (
|
||||
"pGSAINdamAGCsQq31Uv-08lkBzoO4XLz2qYjJa8CGmj3B1EagUDlVkMAw2CscpCG4syAboKKh"
|
||||
"Id_Hrjl2XTYc-BlIkkBVV-4ghWQozusxh45cBz5tGvSW_XwWVu-JGVRQUOOehAL"
|
||||
)
|
||||
CC_CONDITION_URI = "ni:///sha-256;" "eZI5q6j8T_fqv7xMROaei9_tmTMk4S7WR5Kr4onPHV8" "?fpt=ed25519-sha-256&cost=131072"
|
||||
|
||||
ASSET_DEFINITION = {"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"}
|
||||
|
||||
DATA = "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_priv():
|
||||
return USER_PRIVATE_KEY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_pub():
|
||||
return USER_PUBLIC_KEY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user2_priv():
|
||||
return USER2_PRIVATE_KEY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user2_pub():
|
||||
return USER2_PUBLIC_KEY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user3_priv():
|
||||
return USER3_PRIVATE_KEY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user3_pub():
|
||||
return USER3_PUBLIC_KEY
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ffill_uri():
|
||||
return CC_FULFILLMENT_URI
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cond_uri():
|
||||
return CC_CONDITION_URI
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_Ed25519(user_pub):
|
||||
return Ed25519Sha256(public_key=b58decode(user_pub))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_user2_threshold(user_pub, user2_pub):
|
||||
user_pub_keys = [user_pub, user2_pub]
|
||||
threshold = ThresholdSha256(threshold=len(user_pub_keys))
|
||||
for user_pub in user_pub_keys:
|
||||
threshold.add_subfulfillment(Ed25519Sha256(public_key=b58decode(user_pub)))
|
||||
return threshold
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user2_Ed25519(user2_pub):
|
||||
return Ed25519Sha256(public_key=b58decode(user2_pub))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_input(user_Ed25519, user_pub):
|
||||
from planetmint.transactions.common.transaction import Input
|
||||
|
||||
return Input(user_Ed25519, [user_pub])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_user2_threshold_output(user_user2_threshold, user_pub, user2_pub):
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
|
||||
return Output(user_user2_threshold, [user_pub, user2_pub])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_user2_threshold_input(user_user2_threshold, user_pub, user2_pub):
|
||||
from planetmint.transactions.common.transaction import Input
|
||||
|
||||
return Input(user_user2_threshold, [user_pub, user2_pub])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user_output(user_Ed25519, user_pub):
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
|
||||
return Output(user_Ed25519, [user_pub])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user2_output(user2_Ed25519, user2_pub):
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
|
||||
return Output(user2_Ed25519, [user2_pub])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def asset_definition():
|
||||
return ASSET_DEFINITION
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
return DATA
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def utx(user_input, user_output):
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
|
||||
return Transaction(Transaction.CREATE, {"data": None}, [user_input], [user_output])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tx(utx, user_priv):
|
||||
return utx.sign([user_priv])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def transfer_utx(user_output, user2_output, utx):
|
||||
from planetmint.transactions.common.transaction import Input, TransactionLink, Transaction
|
||||
|
||||
user_output = user_output.to_dict()
|
||||
input = Input(utx.outputs[0].fulfillment, user_output["public_keys"], TransactionLink(utx.id, 0))
|
||||
return Transaction("TRANSFER", {"id": utx.id}, [input], [user2_output])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def transfer_tx(transfer_utx, user_priv):
|
||||
return transfer_utx.sign([user_priv])
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def dummy_transaction():
|
||||
return {
|
||||
"asset": {"data": None},
|
||||
"id": 64 * "a",
|
||||
"inputs": [
|
||||
{
|
||||
"fulfillment": "dummy",
|
||||
"fulfills": None,
|
||||
"owners_before": [58 * "a"],
|
||||
}
|
||||
],
|
||||
"metadata": None,
|
||||
"operation": "CREATE",
|
||||
"outputs": [
|
||||
{
|
||||
"amount": "1",
|
||||
"condition": {
|
||||
"details": {"public_key": 58 * "b", "type": "ed25519-sha-256"},
|
||||
"uri": "dummy",
|
||||
},
|
||||
"public_keys": [58 * "b"],
|
||||
}
|
||||
],
|
||||
"version": "2.0",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unfulfilled_transaction():
|
||||
return {
|
||||
"asset": {"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"},
|
||||
"id": None,
|
||||
"inputs": [
|
||||
{
|
||||
# XXX This could be None, see #1925
|
||||
# https://github.com/planetmint/planetmint/issues/1925
|
||||
"fulfillment": {
|
||||
"public_key": "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE",
|
||||
"type": "ed25519-sha-256",
|
||||
},
|
||||
"fulfills": None,
|
||||
"owners_before": ["JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE"],
|
||||
}
|
||||
],
|
||||
"metadata": None,
|
||||
"operation": "CREATE",
|
||||
"outputs": [
|
||||
{
|
||||
"amount": "1",
|
||||
"condition": {
|
||||
"details": {
|
||||
"public_key": "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE",
|
||||
"type": "ed25519-sha-256",
|
||||
},
|
||||
"uri": "ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072",
|
||||
},
|
||||
"public_keys": ["JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE"],
|
||||
}
|
||||
],
|
||||
"version": "1.0",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fulfilled_transaction():
|
||||
return {
|
||||
"asset": {"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"},
|
||||
"id": None,
|
||||
"inputs": [
|
||||
{
|
||||
"fulfillment": (
|
||||
"pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g"
|
||||
"UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k"
|
||||
"jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN"
|
||||
),
|
||||
"fulfills": None,
|
||||
"owners_before": ["JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE"],
|
||||
}
|
||||
],
|
||||
"metadata": None,
|
||||
"operation": "CREATE",
|
||||
"outputs": [
|
||||
{
|
||||
"amount": "1",
|
||||
"condition": {
|
||||
"details": {
|
||||
"public_key": "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE",
|
||||
"type": "ed25519-sha-256",
|
||||
},
|
||||
"uri": "ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072",
|
||||
},
|
||||
"public_keys": ["JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE"],
|
||||
}
|
||||
],
|
||||
"version": "1.0",
|
||||
}
|
||||
|
||||
|
||||
# TODO For reviewers: Pick which approach you like best: parametrized or not?
|
||||
@pytest.fixture(
|
||||
params=(
|
||||
{
|
||||
"id": None,
|
||||
"fulfillment": {"public_key": "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE", "type": "ed25519-sha-256"},
|
||||
},
|
||||
{
|
||||
"id": None,
|
||||
"fulfillment": (
|
||||
"pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g"
|
||||
"UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k"
|
||||
"jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN"
|
||||
),
|
||||
},
|
||||
{
|
||||
"id": "7a7c827cf4ef7985f08f4e9d16f5ffc58ca4e82271921dfbed32e70cb462485f",
|
||||
"fulfillment": (
|
||||
"pGSAIP_2P1Juh-94sD3uno1lxMPd9EkIalRo7QB014pT6dD9g"
|
||||
"UANRNxasDy1Dfg9C2Fk4UgHdYFsJzItVYi5JJ_vWc6rKltn0k"
|
||||
"jagynI0xfyR6X9NhzccTt5oiNH9mThEb4QmagN"
|
||||
),
|
||||
},
|
||||
)
|
||||
)
|
||||
def tri_state_transaction(request):
|
||||
tx = {
|
||||
"asset": {"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"},
|
||||
"id": None,
|
||||
"inputs": [
|
||||
{"fulfillment": None, "fulfills": None, "owners_before": ["JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE"]}
|
||||
],
|
||||
"metadata": None,
|
||||
"operation": "CREATE",
|
||||
"outputs": [
|
||||
{
|
||||
"amount": "1",
|
||||
"condition": {
|
||||
"details": {
|
||||
"public_key": "JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE",
|
||||
"type": "ed25519-sha-256",
|
||||
},
|
||||
"uri": "ni:///sha-256;49C5UWNODwtcINxLgLc90bMCFqCymFYONGEmV4a0sG4?fpt=ed25519-sha-256&cost=131072",
|
||||
},
|
||||
"public_keys": ["JEAkEJqLbbgDRAtMm8YAjGp759Aq2qTn9eaEHUj2XePE"],
|
||||
}
|
||||
],
|
||||
"version": "2.0",
|
||||
}
|
||||
tx["id"] = request.param["id"]
|
||||
tx["inputs"][0]["fulfillment"] = request.param["fulfillment"]
|
||||
return tx
|
@ -1,91 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import pytest
|
||||
from copy import deepcopy
|
||||
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from planetmint.transactions.common.memoize import to_dict, from_dict
|
||||
|
||||
|
||||
pytestmark = pytest.mark.bdb
|
||||
|
||||
|
||||
def test_memoize_to_dict(b):
|
||||
alice = generate_key_pair()
|
||||
asset = {"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"}
|
||||
|
||||
assert to_dict.cache_info().hits == 0
|
||||
assert to_dict.cache_info().misses == 0
|
||||
|
||||
tx = Create.generate(
|
||||
[alice.public_key],
|
||||
[([alice.public_key], 1)],
|
||||
asset=asset,
|
||||
).sign([alice.private_key])
|
||||
|
||||
tx.to_dict()
|
||||
|
||||
assert to_dict.cache_info().hits == 0
|
||||
assert to_dict.cache_info().misses == 1
|
||||
|
||||
tx.to_dict()
|
||||
tx.to_dict()
|
||||
|
||||
assert to_dict.cache_info().hits == 2
|
||||
assert to_dict.cache_info().misses == 1
|
||||
|
||||
|
||||
def test_memoize_from_dict(b):
|
||||
alice = generate_key_pair()
|
||||
asset = {"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"}
|
||||
|
||||
assert from_dict.cache_info().hits == 0
|
||||
assert from_dict.cache_info().misses == 0
|
||||
|
||||
tx = Create.generate(
|
||||
[alice.public_key],
|
||||
[([alice.public_key], 1)],
|
||||
asset=asset,
|
||||
).sign([alice.private_key])
|
||||
tx_dict = deepcopy(tx.to_dict())
|
||||
|
||||
Transaction.from_dict(tx_dict)
|
||||
|
||||
assert from_dict.cache_info().hits == 0
|
||||
assert from_dict.cache_info().misses == 1
|
||||
|
||||
Transaction.from_dict(tx_dict)
|
||||
Transaction.from_dict(tx_dict)
|
||||
|
||||
assert from_dict.cache_info().hits == 2
|
||||
assert from_dict.cache_info().misses == 1
|
||||
|
||||
|
||||
def test_memoize_input_valid(b):
|
||||
alice = generate_key_pair()
|
||||
asset = {"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"}
|
||||
|
||||
assert Transaction._input_valid.cache_info().hits == 0
|
||||
assert Transaction._input_valid.cache_info().misses == 0
|
||||
|
||||
tx = Create.generate(
|
||||
[alice.public_key],
|
||||
[([alice.public_key], 1)],
|
||||
asset=asset,
|
||||
).sign([alice.private_key])
|
||||
|
||||
tx.inputs_valid()
|
||||
|
||||
assert Transaction._input_valid.cache_info().hits == 0
|
||||
assert Transaction._input_valid.cache_info().misses == 1
|
||||
|
||||
tx.inputs_valid()
|
||||
tx.inputs_valid()
|
||||
|
||||
assert Transaction._input_valid.cache_info().hits == 2
|
||||
assert Transaction._input_valid.cache_info().misses == 1
|
@ -1,142 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
"""This module is tests related to schema checking, but _not_ of granular schematic
|
||||
properties related to validation.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from hypothesis import given
|
||||
from hypothesis.strategies import from_regex as regex
|
||||
from pytest import raises
|
||||
|
||||
from planetmint.transactions.common.exceptions import SchemaValidationError
|
||||
from planetmint.transactions.common.schema import (
|
||||
TX_SCHEMA_COMMON,
|
||||
validate_transaction_schema,
|
||||
)
|
||||
|
||||
SUPPORTED_CRYPTOCONDITION_TYPES = ("threshold-sha-256", "ed25519-sha-256")
|
||||
UNSUPPORTED_CRYPTOCONDITION_TYPES = ("preimage-sha-256", "prefix-sha-256", "rsa-sha-256")
|
||||
|
||||
|
||||
################################################################################
|
||||
# Test of schema utils
|
||||
|
||||
|
||||
def _test_additionalproperties(node, path=""):
|
||||
"""Validate that each object node has additionalProperties set, so that
|
||||
objects with junk keys do not pass as valid.
|
||||
"""
|
||||
if isinstance(node, list):
|
||||
for i, nnode in enumerate(node):
|
||||
_test_additionalproperties(nnode, path + str(i) + ".")
|
||||
if isinstance(node, dict):
|
||||
if node.get("type") == "object":
|
||||
assert "additionalProperties" in node, "additionalProperties not set at path:" + path
|
||||
for name, val in node.items():
|
||||
_test_additionalproperties(val, path + name + ".")
|
||||
|
||||
|
||||
def test_transaction_schema_additionalproperties():
|
||||
_test_additionalproperties(TX_SCHEMA_COMMON)
|
||||
|
||||
|
||||
################################################################################
|
||||
# Test call transaction schema
|
||||
|
||||
|
||||
def test_validate_transaction_create(create_tx):
|
||||
validate_transaction_schema(create_tx.to_dict())
|
||||
|
||||
|
||||
def test_validate_transaction_signed_create(signed_create_tx):
|
||||
validate_transaction_schema(signed_create_tx.to_dict())
|
||||
|
||||
|
||||
def test_validate_transaction_signed_transfer(signed_transfer_tx):
|
||||
validate_transaction_schema(signed_transfer_tx.to_dict())
|
||||
|
||||
|
||||
def test_validate_transaction_fails():
|
||||
with raises(SchemaValidationError):
|
||||
validate_transaction_schema({})
|
||||
|
||||
|
||||
def test_validate_failure_inconsistent():
|
||||
with patch("jsonschema.validate"):
|
||||
with raises(SchemaValidationError):
|
||||
validate_transaction_schema({})
|
||||
|
||||
|
||||
@given(
|
||||
condition_uri=regex(
|
||||
r"^ni:\/\/\/sha-256;([a-zA-Z0-9_-]{{0,86}})\?fpt=({})"
|
||||
r"&cost=[0-9]+(?![\n])$".format("|".join(t for t in SUPPORTED_CRYPTOCONDITION_TYPES))
|
||||
)
|
||||
)
|
||||
def test_condition_uri_with_supported_fpt(dummy_transaction, condition_uri):
|
||||
dummy_transaction["outputs"][0]["condition"]["uri"] = condition_uri
|
||||
validate_transaction_schema(dummy_transaction)
|
||||
|
||||
|
||||
@given(
|
||||
condition_uri=regex(
|
||||
r"^ni:\/\/\/sha-256;([a-zA-Z0-9_-]{{0,86}})\?fpt="
|
||||
r"({})&cost=[0-9]+(?![\n])$".format("|".join(UNSUPPORTED_CRYPTOCONDITION_TYPES))
|
||||
)
|
||||
)
|
||||
def test_condition_uri_with_unsupported_fpt(dummy_transaction, condition_uri):
|
||||
dummy_transaction["outputs"][0]["condition"]["uri"] = condition_uri
|
||||
with raises(SchemaValidationError):
|
||||
validate_transaction_schema(dummy_transaction)
|
||||
|
||||
|
||||
@given(
|
||||
condition_uri=regex(
|
||||
r"^ni:\/\/\/sha-256;([a-zA-Z0-9_-]{{0,86}})\?fpt=(?!{})"
|
||||
r"&cost=[0-9]+(?![\n])$".format("$|".join(t for t in SUPPORTED_CRYPTOCONDITION_TYPES))
|
||||
)
|
||||
)
|
||||
def test_condition_uri_with_unknown_fpt(dummy_transaction, condition_uri):
|
||||
dummy_transaction["outputs"][0]["condition"]["uri"] = condition_uri
|
||||
with raises(SchemaValidationError):
|
||||
validate_transaction_schema(dummy_transaction)
|
||||
|
||||
|
||||
@given(
|
||||
condition_uri=regex(
|
||||
r"^ni:\/\/\/sha-256;([a-zA-Z0-9_-]{0,86})\?fpt=threshold-sha-256"
|
||||
r"&cost=[0-9]+&subtypes=ed25519-sha-256(?![\n])$"
|
||||
)
|
||||
)
|
||||
def test_condition_uri_with_supported_subtype(dummy_transaction, condition_uri):
|
||||
dummy_transaction["outputs"][0]["condition"]["uri"] = condition_uri
|
||||
validate_transaction_schema(dummy_transaction)
|
||||
|
||||
|
||||
@given(
|
||||
condition_uri=regex(
|
||||
r"^ni:\/\/\/sha-256;([a-zA-Z0-9_-]{0,86})\?fpt=threshold-sha-256&cost="
|
||||
r"[0-9]+&subtypes=(preimage-sha-256|prefix-sha-256|rsa-sha-256)(?![\n])$"
|
||||
)
|
||||
)
|
||||
def test_condition_uri_with_unsupported_subtype(dummy_transaction, condition_uri):
|
||||
dummy_transaction["outputs"][0]["condition"]["uri"] = condition_uri
|
||||
with raises(SchemaValidationError):
|
||||
validate_transaction_schema(dummy_transaction)
|
||||
|
||||
|
||||
@given(
|
||||
condition_uri=regex(
|
||||
r"^ni:\/\/\/sha-256;([a-zA-Z0-9_-]{{0,86}})\?fpt=threshold-sha-256"
|
||||
r"&cost=[0-9]+&subtypes=(?!{})(?![\n])$".format("$|".join(t for t in SUPPORTED_CRYPTOCONDITION_TYPES))
|
||||
)
|
||||
)
|
||||
def test_condition_uri_with_unknown_subtype(dummy_transaction, condition_uri):
|
||||
dummy_transaction["outputs"][0]["condition"]["uri"] = condition_uri
|
||||
with raises(SchemaValidationError):
|
||||
validate_transaction_schema(dummy_transaction)
|
@ -1,890 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
"""These are tests of the API of the Transaction class and associated classes.
|
||||
Tests for transaction validation are separate.
|
||||
"""
|
||||
import json
|
||||
from copy import deepcopy
|
||||
|
||||
from base58 import b58encode, b58decode
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
from planetmint.transactions.common.transaction import Input
|
||||
from planetmint.transactions.common.exceptions import AmountError
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from cryptoconditions import ThresholdSha256
|
||||
from cryptoconditions import Fulfillment
|
||||
from cryptoconditions import PreimageSha256
|
||||
from cryptoconditions import Ed25519Sha256
|
||||
from pytest import mark, raises
|
||||
from ipld import marshal, multihash
|
||||
|
||||
try:
|
||||
from hashlib import sha3_256
|
||||
except ImportError:
|
||||
from sha3 import sha3_256
|
||||
|
||||
pytestmark = mark.bdb
|
||||
|
||||
|
||||
def test_input_serialization(ffill_uri, user_pub):
|
||||
expected = {
|
||||
"owners_before": [user_pub],
|
||||
"fulfillment": ffill_uri,
|
||||
"fulfills": None,
|
||||
}
|
||||
input = Input(Fulfillment.from_uri(ffill_uri), [user_pub])
|
||||
assert input.to_dict() == expected
|
||||
|
||||
|
||||
def test_input_deserialization_with_uri(ffill_uri, user_pub):
|
||||
expected = Input(Fulfillment.from_uri(ffill_uri), [user_pub])
|
||||
ffill = {
|
||||
"owners_before": [user_pub],
|
||||
"fulfillment": ffill_uri,
|
||||
"fulfills": None,
|
||||
}
|
||||
input = Input.from_dict(ffill)
|
||||
|
||||
assert input == expected
|
||||
|
||||
|
||||
@mark.skip(reason="None is tolerated because it is None before fulfilling.")
|
||||
def test_input_deserialization_with_invalid_input(user_pub):
|
||||
from planetmint.transactions.common.transaction import Input
|
||||
|
||||
ffill = {
|
||||
"owners_before": [user_pub],
|
||||
"fulfillment": None,
|
||||
"fulfills": None,
|
||||
}
|
||||
with raises(TypeError):
|
||||
Input.from_dict(ffill)
|
||||
|
||||
|
||||
def test_input_deserialization_with_invalid_fulfillment_uri(user_pub):
|
||||
from planetmint.transactions.common.exceptions import InvalidSignature
|
||||
from planetmint.transactions.common.transaction import Input
|
||||
|
||||
ffill = {
|
||||
"owners_before": [user_pub],
|
||||
"fulfillment": "an invalid fulfillment",
|
||||
"fulfills": None,
|
||||
}
|
||||
with raises(InvalidSignature):
|
||||
Input.from_dict(ffill)
|
||||
|
||||
|
||||
def test_input_deserialization_with_unsigned_fulfillment(ffill_uri, user_pub):
|
||||
expected = Input(Fulfillment.from_uri(ffill_uri), [user_pub])
|
||||
ffill = {
|
||||
"owners_before": [user_pub],
|
||||
"fulfillment": Fulfillment.from_uri(ffill_uri),
|
||||
"fulfills": None,
|
||||
}
|
||||
input = Input.from_dict(ffill)
|
||||
|
||||
assert input == expected
|
||||
|
||||
|
||||
def test_output_serialization(user_Ed25519, user_pub):
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
|
||||
expected = {
|
||||
"condition": {
|
||||
"uri": user_Ed25519.condition_uri,
|
||||
"details": {
|
||||
"type": "ed25519-sha-256",
|
||||
"public_key": b58encode(user_Ed25519.public_key).decode(),
|
||||
},
|
||||
},
|
||||
"public_keys": [user_pub],
|
||||
"amount": "1",
|
||||
}
|
||||
|
||||
cond = Output(user_Ed25519, [user_pub], 1)
|
||||
|
||||
assert cond.to_dict() == expected
|
||||
|
||||
|
||||
def test_output_deserialization(user_Ed25519, user_pub):
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
|
||||
expected = Output(user_Ed25519, [user_pub], 1)
|
||||
cond = {
|
||||
"condition": {
|
||||
"uri": user_Ed25519.condition_uri,
|
||||
"details": {
|
||||
"type": "ed25519-sha-256",
|
||||
"public_key": b58encode(user_Ed25519.public_key).decode(),
|
||||
},
|
||||
},
|
||||
"public_keys": [user_pub],
|
||||
"amount": "1",
|
||||
}
|
||||
cond = Output.from_dict(cond)
|
||||
|
||||
assert cond == expected
|
||||
|
||||
|
||||
def test_output_hashlock_serialization():
|
||||
secret = b"wow much secret"
|
||||
hashlock = PreimageSha256(preimage=secret).condition_uri
|
||||
|
||||
expected = {
|
||||
"condition": {
|
||||
"uri": hashlock,
|
||||
},
|
||||
"public_keys": None,
|
||||
"amount": "1",
|
||||
}
|
||||
cond = Output(hashlock, amount=1)
|
||||
|
||||
assert cond.to_dict() == expected
|
||||
|
||||
|
||||
def test_output_hashlock_deserialization():
|
||||
secret = b"wow much secret"
|
||||
hashlock = PreimageSha256(preimage=secret).condition_uri
|
||||
expected = Output(hashlock, amount=1)
|
||||
|
||||
cond = {
|
||||
"condition": {"uri": hashlock},
|
||||
"public_keys": None,
|
||||
"amount": "1",
|
||||
}
|
||||
cond = Output.from_dict(cond)
|
||||
|
||||
assert cond == expected
|
||||
|
||||
|
||||
def test_invalid_output_initialization(cond_uri, user_pub):
|
||||
with raises(TypeError):
|
||||
Output(cond_uri, user_pub)
|
||||
with raises(TypeError):
|
||||
Output(cond_uri, [user_pub], "amount")
|
||||
with raises(AmountError):
|
||||
Output(cond_uri, [user_pub], 0)
|
||||
|
||||
|
||||
def test_generate_output_split_half_recursive(user_pub, user2_pub, user3_pub):
|
||||
expected_simple1 = Ed25519Sha256(public_key=b58decode(user_pub))
|
||||
expected_simple2 = Ed25519Sha256(public_key=b58decode(user2_pub))
|
||||
expected_simple3 = Ed25519Sha256(public_key=b58decode(user3_pub))
|
||||
|
||||
expected = ThresholdSha256(threshold=2)
|
||||
expected.add_subfulfillment(expected_simple1)
|
||||
expected_threshold = ThresholdSha256(threshold=2)
|
||||
expected_threshold.add_subfulfillment(expected_simple2)
|
||||
expected_threshold.add_subfulfillment(expected_simple3)
|
||||
expected.add_subfulfillment(expected_threshold)
|
||||
|
||||
cond = Output.generate([user_pub, [user2_pub, expected_simple3]], 1)
|
||||
assert cond.fulfillment.to_dict() == expected.to_dict()
|
||||
|
||||
|
||||
def test_generate_outputs_split_half_single_owner(user_pub, user2_pub, user3_pub):
|
||||
expected_simple1 = Ed25519Sha256(public_key=b58decode(user_pub))
|
||||
expected_simple2 = Ed25519Sha256(public_key=b58decode(user2_pub))
|
||||
expected_simple3 = Ed25519Sha256(public_key=b58decode(user3_pub))
|
||||
|
||||
expected = ThresholdSha256(threshold=2)
|
||||
expected_threshold = ThresholdSha256(threshold=2)
|
||||
expected_threshold.add_subfulfillment(expected_simple2)
|
||||
expected_threshold.add_subfulfillment(expected_simple3)
|
||||
expected.add_subfulfillment(expected_threshold)
|
||||
expected.add_subfulfillment(expected_simple1)
|
||||
|
||||
cond = Output.generate([[expected_simple2, user3_pub], user_pub], 1)
|
||||
assert cond.fulfillment.to_dict() == expected.to_dict()
|
||||
|
||||
|
||||
def test_generate_outputs_flat_ownage(user_pub, user2_pub, user3_pub):
|
||||
expected_simple1 = Ed25519Sha256(public_key=b58decode(user_pub))
|
||||
expected_simple2 = Ed25519Sha256(public_key=b58decode(user2_pub))
|
||||
expected_simple3 = Ed25519Sha256(public_key=b58decode(user3_pub))
|
||||
|
||||
expected = ThresholdSha256(threshold=3)
|
||||
expected.add_subfulfillment(expected_simple1)
|
||||
expected.add_subfulfillment(expected_simple2)
|
||||
expected.add_subfulfillment(expected_simple3)
|
||||
|
||||
cond = Output.generate([user_pub, user2_pub, expected_simple3], 1)
|
||||
assert cond.fulfillment.to_dict() == expected.to_dict()
|
||||
|
||||
|
||||
def test_generate_output_single_owner(user_pub):
|
||||
expected = Ed25519Sha256(public_key=b58decode(user_pub))
|
||||
cond = Output.generate([user_pub], 1)
|
||||
|
||||
assert cond.fulfillment.to_dict() == expected.to_dict()
|
||||
|
||||
|
||||
def test_generate_output_single_owner_with_output(user_pub):
|
||||
expected = Ed25519Sha256(public_key=b58decode(user_pub))
|
||||
cond = Output.generate([expected], 1)
|
||||
|
||||
assert cond.fulfillment.to_dict() == expected.to_dict()
|
||||
|
||||
|
||||
def test_generate_output_invalid_parameters(user_pub, user2_pub, user3_pub):
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
from planetmint.transactions.common.exceptions import AmountError
|
||||
|
||||
with raises(ValueError):
|
||||
Output.generate([], 1)
|
||||
with raises(TypeError):
|
||||
Output.generate("not a list", 1)
|
||||
with raises(ValueError):
|
||||
Output.generate([[user_pub, [user2_pub, [user3_pub]]]], 1)
|
||||
with raises(ValueError):
|
||||
Output.generate([[user_pub]], 1)
|
||||
with raises(AmountError):
|
||||
Output.generate([[user_pub]], -1)
|
||||
|
||||
|
||||
def test_invalid_transaction_initialization(asset_definition):
|
||||
with raises(ValueError):
|
||||
Transaction(operation="invalid operation", asset=asset_definition)
|
||||
with raises(TypeError):
|
||||
Transaction(operation="CREATE", asset="invalid asset")
|
||||
with raises(TypeError):
|
||||
Transaction(operation="TRANSFER", asset={})
|
||||
with raises(TypeError):
|
||||
Transaction(operation="CREATE", asset=asset_definition, outputs="invalid outputs")
|
||||
with raises(TypeError):
|
||||
Transaction(operation="CREATE", asset=asset_definition, outputs=[], inputs="invalid inputs")
|
||||
with raises(TypeError):
|
||||
Transaction(
|
||||
operation="CREATE", asset=asset_definition, outputs=[], inputs=[], metadata={"data": "invalid metadata"}
|
||||
)
|
||||
|
||||
|
||||
def test_create_default_asset_on_tx_initialization(asset_definition):
|
||||
expected = {"data": None}
|
||||
tx = Transaction(Transaction.CREATE, asset=expected)
|
||||
asset = tx.asset
|
||||
|
||||
assert asset == expected
|
||||
|
||||
|
||||
def test_transaction_serialization(user_input, user_output, data):
|
||||
expected = {
|
||||
"id": None,
|
||||
"version": Transaction.VERSION,
|
||||
# NOTE: This test assumes that Inputs and Outputs can
|
||||
# successfully be serialized
|
||||
"inputs": [user_input.to_dict()],
|
||||
"outputs": [user_output.to_dict()],
|
||||
"operation": Transaction.CREATE,
|
||||
"metadata": None,
|
||||
"asset": {
|
||||
"data": data,
|
||||
},
|
||||
}
|
||||
|
||||
tx = Transaction(Transaction.CREATE, {"data": data}, [user_input], [user_output])
|
||||
tx_dict = tx.to_dict()
|
||||
|
||||
assert tx_dict == expected
|
||||
|
||||
|
||||
def test_transaction_deserialization(tri_state_transaction):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Transaction.from_dict(tri_state_transaction)
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_invalid_input_initialization(user_input, user_pub):
|
||||
from planetmint.transactions.common.transaction import Input
|
||||
|
||||
with raises(TypeError):
|
||||
Input(user_input, user_pub)
|
||||
with raises(TypeError):
|
||||
Input(user_input, tx_input="somethingthatiswrong")
|
||||
|
||||
|
||||
def test_transaction_link_serialization():
|
||||
|
||||
tx_id = "a transaction id"
|
||||
expected = {
|
||||
"transaction_id": tx_id,
|
||||
"output_index": 0,
|
||||
}
|
||||
tx_link = TransactionLink(tx_id, 0)
|
||||
|
||||
assert tx_link.to_dict() == expected
|
||||
|
||||
|
||||
def test_transaction_link_serialization_with_empty_payload():
|
||||
expected = None
|
||||
tx_link = TransactionLink()
|
||||
|
||||
assert tx_link.to_dict() == expected
|
||||
|
||||
|
||||
def test_transaction_link_deserialization():
|
||||
tx_id = "a transaction id"
|
||||
expected = TransactionLink(tx_id, 0)
|
||||
tx_link = {
|
||||
"transaction_id": tx_id,
|
||||
"output_index": 0,
|
||||
}
|
||||
tx_link = TransactionLink.from_dict(tx_link)
|
||||
|
||||
assert tx_link == expected
|
||||
|
||||
|
||||
def test_transaction_link_deserialization_with_empty_payload():
|
||||
expected = TransactionLink()
|
||||
tx_link = TransactionLink.from_dict(None)
|
||||
|
||||
assert tx_link == expected
|
||||
|
||||
|
||||
def test_transaction_link_empty_to_uri():
|
||||
expected = None
|
||||
tx_link = TransactionLink().to_uri()
|
||||
|
||||
assert expected == tx_link
|
||||
|
||||
|
||||
def test_transaction_link_to_uri():
|
||||
expected = "path/transactions/abc/outputs/0"
|
||||
tx_link = TransactionLink("abc", 0).to_uri("path")
|
||||
|
||||
assert expected == tx_link
|
||||
|
||||
|
||||
def test_cast_transaction_link_to_boolean():
|
||||
assert bool(TransactionLink()) is False
|
||||
assert bool(TransactionLink("a", None)) is False
|
||||
assert bool(TransactionLink(None, "b")) is False
|
||||
assert bool(TransactionLink("a", "b")) is True
|
||||
assert bool(TransactionLink(False, False)) is True
|
||||
|
||||
|
||||
def test_transaction_link_eq():
|
||||
assert TransactionLink(1, 2) == TransactionLink(1, 2)
|
||||
assert TransactionLink(2, 2) != TransactionLink(1, 2)
|
||||
assert TransactionLink(1, 1) != TransactionLink(1, 2)
|
||||
assert TransactionLink(2, 1) != TransactionLink(1, 2)
|
||||
|
||||
|
||||
def test_add_input_to_tx(user_input, asset_definition):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [], [])
|
||||
tx.add_input(user_input)
|
||||
|
||||
assert len(tx.inputs) == 1
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_add_input_to_tx_with_invalid_parameters(asset_definition):
|
||||
tx = Transaction(Transaction.CREATE, asset_definition)
|
||||
|
||||
with raises(TypeError):
|
||||
tx.add_input("somewronginput")
|
||||
|
||||
|
||||
def test_add_output_to_tx(user_output, user_input, asset_definition):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [user_input])
|
||||
tx.add_output(user_output)
|
||||
|
||||
assert len(tx.outputs) == 1
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_add_output_to_tx_with_invalid_parameters(asset_definition):
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [], [])
|
||||
|
||||
with raises(TypeError):
|
||||
tx.add_output("somewronginput")
|
||||
|
||||
|
||||
def test_sign_with_invalid_parameters(utx, user_priv):
|
||||
with raises(TypeError):
|
||||
utx.sign(None)
|
||||
with raises(TypeError):
|
||||
utx.sign(user_priv)
|
||||
|
||||
|
||||
def test_validate_tx_simple_create_signature(user_input, user_output, user_priv, asset_definition):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [user_input], [user_output])
|
||||
expected = deepcopy(user_output)
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict["inputs"][0]["fulfillment"] = None
|
||||
serialized_tx = json.dumps(tx_dict, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
message = sha3_256(serialized_tx.encode()).digest()
|
||||
expected.fulfillment.sign(message, b58decode(user_priv))
|
||||
tx.sign([user_priv])
|
||||
|
||||
assert tx.inputs[0].to_dict()["fulfillment"] == expected.fulfillment.serialize_uri()
|
||||
assert tx.inputs_valid() is True
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_invoke_simple_signature_fulfillment_with_invalid_params(utx, user_input):
|
||||
from planetmint.transactions.common.exceptions import KeypairMismatchException
|
||||
|
||||
with raises(KeypairMismatchException):
|
||||
invalid_key_pair = {"wrong_pub_key": "wrong_priv_key"}
|
||||
utx._sign_simple_signature_fulfillment(user_input, "somemessage", invalid_key_pair)
|
||||
|
||||
|
||||
def test_sign_threshold_with_invalid_params(utx, user_user2_threshold_input, user3_pub, user3_priv):
|
||||
from planetmint.transactions.common.exceptions import KeypairMismatchException
|
||||
|
||||
with raises(KeypairMismatchException):
|
||||
utx._sign_threshold_signature_fulfillment(user_user2_threshold_input, "somemessage", {user3_pub: user3_priv})
|
||||
with raises(KeypairMismatchException):
|
||||
user_user2_threshold_input.owners_before = [58 * "a"]
|
||||
utx._sign_threshold_signature_fulfillment(user_user2_threshold_input, "somemessage", None)
|
||||
|
||||
|
||||
def test_validate_input_with_invalid_parameters(utx):
|
||||
input_conditions = [out.fulfillment.condition_uri for out in utx.outputs]
|
||||
tx_dict = utx.to_dict()
|
||||
tx_serialized = Transaction._to_str(tx_dict)
|
||||
valid = utx._input_valid(utx.inputs[0], tx_serialized, input_conditions[0])
|
||||
assert not valid
|
||||
|
||||
|
||||
def test_validate_tx_threshold_create_signature(
|
||||
user_user2_threshold_input,
|
||||
user_user2_threshold_output,
|
||||
user_pub,
|
||||
user2_pub,
|
||||
user_priv,
|
||||
user2_priv,
|
||||
asset_definition,
|
||||
):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [user_user2_threshold_input], [user_user2_threshold_output])
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict["inputs"][0]["fulfillment"] = None
|
||||
serialized_tx = json.dumps(tx_dict, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
message = sha3_256(serialized_tx.encode()).digest()
|
||||
expected = deepcopy(user_user2_threshold_output)
|
||||
expected.fulfillment.subconditions[0]["body"].sign(message, b58decode(user_priv))
|
||||
expected.fulfillment.subconditions[1]["body"].sign(message, b58decode(user2_priv))
|
||||
tx.sign([user_priv, user2_priv])
|
||||
|
||||
assert tx.inputs[0].to_dict()["fulfillment"] == expected.fulfillment.serialize_uri()
|
||||
assert tx.inputs_valid() is True
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_validate_tx_threshold_duplicated_pk(user_pub, user_priv, asset_definition):
|
||||
threshold = ThresholdSha256(threshold=2)
|
||||
threshold.add_subfulfillment(Ed25519Sha256(public_key=b58decode(user_pub)))
|
||||
threshold.add_subfulfillment(Ed25519Sha256(public_key=b58decode(user_pub)))
|
||||
|
||||
threshold_input = Input(threshold, [user_pub, user_pub])
|
||||
threshold_output = Output(threshold, [user_pub, user_pub])
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [threshold_input], [threshold_output])
|
||||
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict["inputs"][0]["fulfillment"] = None
|
||||
serialized_tx = json.dumps(tx_dict, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
message = sha3_256(serialized_tx.encode()).digest()
|
||||
|
||||
expected = deepcopy(threshold_input)
|
||||
expected.fulfillment.subconditions[0]["body"].sign(message, b58decode(user_priv))
|
||||
expected.fulfillment.subconditions[1]["body"].sign(message, b58decode(user_priv))
|
||||
|
||||
tx.sign([user_priv, user_priv])
|
||||
|
||||
subconditions = tx.inputs[0].fulfillment.subconditions
|
||||
expected_subconditions = expected.fulfillment.subconditions
|
||||
assert subconditions[0]["body"].to_dict()["signature"] == expected_subconditions[0]["body"].to_dict()["signature"]
|
||||
assert subconditions[1]["body"].to_dict()["signature"] == expected_subconditions[1]["body"].to_dict()["signature"]
|
||||
|
||||
assert tx.inputs[0].to_dict()["fulfillment"] == expected.fulfillment.serialize_uri()
|
||||
assert tx.inputs_valid() is True
|
||||
|
||||
|
||||
def test_multiple_input_validation_of_transfer_tx(
|
||||
user_input, user_output, user_priv, user2_pub, user2_priv, user3_pub, user3_priv, asset_definition
|
||||
):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Transaction(Transaction.CREATE, asset_definition, [user_input], [user_output, deepcopy(user_output)])
|
||||
tx.sign([user_priv])
|
||||
|
||||
inputs = [
|
||||
Input(cond.fulfillment, cond.public_keys, TransactionLink(tx.id, index))
|
||||
for index, cond in enumerate(tx.outputs)
|
||||
]
|
||||
outputs = [
|
||||
Output(Ed25519Sha256(public_key=b58decode(user3_pub)), [user3_pub]),
|
||||
Output(Ed25519Sha256(public_key=b58decode(user3_pub)), [user3_pub]),
|
||||
]
|
||||
transfer_tx = Transaction("TRANSFER", {"id": tx.id}, inputs, outputs)
|
||||
transfer_tx = transfer_tx.sign([user_priv])
|
||||
|
||||
assert transfer_tx.inputs_valid(tx.outputs) is True
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_validate_inputs_of_transfer_tx_with_invalid_params(
|
||||
transfer_tx, cond_uri, utx, user2_pub, user_priv, ffill_uri
|
||||
):
|
||||
invalid_out = Output(Ed25519Sha256.from_uri(ffill_uri), ["invalid"])
|
||||
assert transfer_tx.inputs_valid([invalid_out]) is False
|
||||
invalid_out = utx.outputs[0]
|
||||
invalid_out.public_key = "invalid"
|
||||
assert transfer_tx.inputs_valid([invalid_out]) is True
|
||||
|
||||
with raises(TypeError):
|
||||
assert transfer_tx.inputs_valid(None) is False
|
||||
with raises(AttributeError):
|
||||
transfer_tx.inputs_valid("not a list")
|
||||
with raises(ValueError):
|
||||
transfer_tx.inputs_valid([])
|
||||
with raises(TypeError):
|
||||
transfer_tx.operation = "Operation that doesn't exist"
|
||||
transfer_tx.inputs_valid([utx.outputs[0]])
|
||||
|
||||
|
||||
def test_create_create_transaction_single_io(user_output, user_pub, data):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
expected = {
|
||||
"outputs": [user_output.to_dict()],
|
||||
"metadata": data,
|
||||
"asset": {
|
||||
"data": data,
|
||||
},
|
||||
"inputs": [{"owners_before": [user_pub], "fulfillment": None, "fulfills": None}],
|
||||
"operation": "CREATE",
|
||||
"version": Transaction.VERSION,
|
||||
}
|
||||
|
||||
tx = Create.generate([user_pub], [([user_pub], 1)], metadata=data, asset={"data": data})
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict["inputs"][0]["fulfillment"] = None
|
||||
tx_dict.pop("id")
|
||||
|
||||
assert tx_dict == expected
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_validate_single_io_create_transaction(user_pub, user_priv, data, asset_definition):
|
||||
|
||||
tx = Create.generate([user_pub], [([user_pub], 1)], metadata=data)
|
||||
tx = tx.sign([user_priv])
|
||||
assert tx.inputs_valid() is True
|
||||
|
||||
|
||||
def test_create_create_transaction_multiple_io(user_output, user2_output, user_pub, user2_pub, asset_definition):
|
||||
# a fulfillment for a create transaction with multiple `owners_before`
|
||||
# is a fulfillment for an implicit threshold condition with
|
||||
# weight = len(owners_before)
|
||||
input = Input.generate([user_pub, user2_pub]).to_dict()
|
||||
expected = {
|
||||
"outputs": [user_output.to_dict(), user2_output.to_dict()],
|
||||
"metadata": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4",
|
||||
"inputs": [input],
|
||||
"operation": "CREATE",
|
||||
"version": Transaction.VERSION,
|
||||
}
|
||||
tx = Create.generate(
|
||||
[user_pub, user2_pub],
|
||||
[([user_pub], 1), ([user2_pub], 1)],
|
||||
metadata="QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4",
|
||||
).to_dict()
|
||||
tx.pop("id")
|
||||
tx.pop("asset")
|
||||
|
||||
assert tx == expected
|
||||
|
||||
|
||||
def test_validate_multiple_io_create_transaction(user_pub, user_priv, user2_pub, user2_priv, asset_definition):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Create.generate(
|
||||
[user_pub, user2_pub],
|
||||
[([user_pub], 1), ([user2_pub], 1)],
|
||||
metadata="QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4",
|
||||
)
|
||||
tx = tx.sign([user_priv, user2_priv])
|
||||
assert tx.inputs_valid() is True
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_create_create_transaction_threshold(
|
||||
user_pub, user2_pub, user3_pub, user_user2_threshold_output, user_user2_threshold_input, data
|
||||
):
|
||||
expected = {
|
||||
"outputs": [user_user2_threshold_output.to_dict()],
|
||||
"metadata": data,
|
||||
"asset": {
|
||||
"data": data,
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"owners_before": [
|
||||
user_pub,
|
||||
],
|
||||
"fulfillment": None,
|
||||
"fulfills": None,
|
||||
},
|
||||
],
|
||||
"operation": "CREATE",
|
||||
"version": Transaction.VERSION,
|
||||
}
|
||||
tx = Create.generate([user_pub], [([user_pub, user2_pub], 1)], metadata=data, asset={"data": data})
|
||||
tx_dict = tx.to_dict()
|
||||
tx_dict.pop("id")
|
||||
tx_dict["inputs"][0]["fulfillment"] = None
|
||||
|
||||
assert tx_dict == expected
|
||||
|
||||
|
||||
def test_validate_threshold_create_transaction(user_pub, user_priv, user2_pub, data, asset_definition):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
tx = Create.generate([user_pub], [([user_pub, user2_pub], 1)], metadata=data)
|
||||
tx = tx.sign([user_priv])
|
||||
assert tx.inputs_valid() is True
|
||||
|
||||
validate_transaction_model(tx)
|
||||
|
||||
|
||||
def test_create_create_transaction_with_invalid_parameters(user_pub):
|
||||
with raises(TypeError):
|
||||
Create.generate("not a list")
|
||||
with raises(TypeError):
|
||||
Create.generate([], "not a list")
|
||||
with raises(ValueError):
|
||||
Create.generate([], [user_pub])
|
||||
with raises(ValueError):
|
||||
Create.generate([user_pub], [])
|
||||
with raises(ValueError):
|
||||
Create.generate([user_pub], [user_pub])
|
||||
with raises(ValueError):
|
||||
Create.generate([user_pub], [([user_pub],)])
|
||||
with raises(TypeError):
|
||||
Create.generate([user_pub], [([user_pub], 1)], metadata={"data": "not a cid string or none"})
|
||||
with raises(TypeError):
|
||||
Create.generate([user_pub], [([user_pub], 1)], asset={"data": "not a dict or none"})
|
||||
|
||||
|
||||
def test_outputs_to_inputs(tx):
|
||||
inputs = tx.to_inputs([0])
|
||||
assert len(inputs) == 1
|
||||
input = inputs.pop()
|
||||
assert input.owners_before == tx.outputs[0].public_keys
|
||||
assert input.fulfillment == tx.outputs[0].fulfillment
|
||||
assert input.fulfills.txid == tx.id
|
||||
assert input.fulfills.output == 0
|
||||
|
||||
|
||||
def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub, user2_output, user_priv):
|
||||
from .utils import validate_transaction_model
|
||||
|
||||
expected = {
|
||||
"id": None,
|
||||
"outputs": [user2_output.to_dict()],
|
||||
"metadata": None,
|
||||
"asset": {
|
||||
"id": tx.id,
|
||||
},
|
||||
"inputs": [
|
||||
{
|
||||
"owners_before": [user_pub],
|
||||
"fulfillment": None,
|
||||
"fulfills": {"transaction_id": tx.id, "output_index": 0},
|
||||
}
|
||||
],
|
||||
"operation": "TRANSFER",
|
||||
"version": Transaction.VERSION,
|
||||
}
|
||||
inputs = tx.to_inputs([0])
|
||||
transfer_tx = Transfer.generate(inputs, [([user2_pub], 1)], asset_id=tx.id)
|
||||
transfer_tx = transfer_tx.sign([user_priv])
|
||||
transfer_tx = transfer_tx.to_dict()
|
||||
|
||||
expected_input = deepcopy(inputs[0])
|
||||
json_serialized_tx = json.dumps(expected, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
message = sha3_256(json_serialized_tx.encode())
|
||||
message.update(
|
||||
"{}{}".format(
|
||||
expected["inputs"][0]["fulfills"]["transaction_id"],
|
||||
expected["inputs"][0]["fulfills"]["output_index"],
|
||||
).encode()
|
||||
)
|
||||
expected_input.fulfillment.sign(message.digest(), b58decode(user_priv))
|
||||
expected_ffill = expected_input.fulfillment.serialize_uri()
|
||||
transfer_ffill = transfer_tx["inputs"][0]["fulfillment"]
|
||||
|
||||
assert transfer_ffill == expected_ffill
|
||||
|
||||
transfer_tx = Transaction.from_dict(transfer_tx)
|
||||
assert transfer_tx.inputs_valid([tx.outputs[0]]) is True
|
||||
|
||||
validate_transaction_model(transfer_tx)
|
||||
|
||||
|
||||
def test_create_transfer_transaction_multiple_io(
|
||||
user_pub, user_priv, user2_pub, user2_priv, user3_pub, user2_output, asset_definition
|
||||
):
|
||||
tx = Create.generate(
|
||||
[user_pub], [([user_pub], 1), ([user2_pub], 1)], metadata="QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"
|
||||
)
|
||||
tx = tx.sign([user_priv])
|
||||
|
||||
expected = {
|
||||
"outputs": [user2_output.to_dict(), user2_output.to_dict()],
|
||||
"metadata": None,
|
||||
"inputs": [
|
||||
{
|
||||
"owners_before": [user_pub],
|
||||
"fulfillment": None,
|
||||
"fulfills": {"transaction_id": tx.id, "output_index": 0},
|
||||
},
|
||||
{
|
||||
"owners_before": [user2_pub],
|
||||
"fulfillment": None,
|
||||
"fulfills": {"transaction_id": tx.id, "output_index": 1},
|
||||
},
|
||||
],
|
||||
"operation": "TRANSFER",
|
||||
"version": Transaction.VERSION,
|
||||
}
|
||||
|
||||
transfer_tx = Transfer.generate(tx.to_inputs(), [([user2_pub], 1), ([user2_pub], 1)], asset_id=tx.id)
|
||||
transfer_tx = transfer_tx.sign([user_priv, user2_priv])
|
||||
|
||||
assert len(transfer_tx.inputs) == 2
|
||||
assert len(transfer_tx.outputs) == 2
|
||||
|
||||
assert transfer_tx.inputs_valid(tx.outputs) is True
|
||||
|
||||
transfer_tx = transfer_tx.to_dict()
|
||||
transfer_tx["inputs"][0]["fulfillment"] = None
|
||||
transfer_tx["inputs"][1]["fulfillment"] = None
|
||||
transfer_tx.pop("asset")
|
||||
transfer_tx.pop("id")
|
||||
|
||||
assert expected == transfer_tx
|
||||
|
||||
|
||||
def test_create_transfer_with_invalid_parameters(tx, user_pub):
|
||||
with raises(TypeError):
|
||||
Transfer.generate({}, [], tx.id)
|
||||
with raises(ValueError):
|
||||
Transfer.generate([], [], tx.id)
|
||||
with raises(TypeError):
|
||||
Transfer.generate(["fulfillment"], {}, tx.id)
|
||||
with raises(ValueError):
|
||||
Transfer.generate(["fulfillment"], [], tx.id)
|
||||
with raises(ValueError):
|
||||
Transfer.generate(["fulfillment"], [user_pub], tx.id)
|
||||
with raises(ValueError):
|
||||
Transfer.generate(["fulfillment"], [([user_pub],)], tx.id)
|
||||
with raises(TypeError):
|
||||
Transfer.generate(["fulfillment"], [([user_pub], 1)], tx.id, metadata={"data": "not a cid string or none"})
|
||||
with raises(TypeError):
|
||||
Transfer.generate(["fulfillment"], [([user_pub], 1)], ["not a string"])
|
||||
|
||||
|
||||
def test_cant_add_empty_output():
|
||||
|
||||
tx = Transaction(Transaction.CREATE, None)
|
||||
|
||||
with raises(TypeError):
|
||||
tx.add_output(None)
|
||||
|
||||
|
||||
def test_cant_add_empty_input():
|
||||
tx = Transaction(Transaction.CREATE, None)
|
||||
|
||||
with raises(TypeError):
|
||||
tx.add_input(None)
|
||||
|
||||
|
||||
def test_unfulfilled_transaction_serialized(unfulfilled_transaction):
|
||||
tx_obj = Transaction.from_dict(unfulfilled_transaction)
|
||||
expected = json.dumps(unfulfilled_transaction, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
assert tx_obj.serialized == expected
|
||||
|
||||
|
||||
def test_fulfilled_transaction_serialized(fulfilled_transaction):
|
||||
tx_obj = Transaction.from_dict(fulfilled_transaction)
|
||||
expected = json.dumps(fulfilled_transaction, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
assert tx_obj.serialized == expected
|
||||
|
||||
|
||||
def test_transaction_hash(fulfilled_transaction):
|
||||
tx_obj = Transaction.from_dict(fulfilled_transaction)
|
||||
assert tx_obj._id is None
|
||||
assert tx_obj.id is None
|
||||
thing_to_hash = json.dumps(fulfilled_transaction, sort_keys=True, separators=(",", ":"), ensure_ascii=True)
|
||||
expected_hash_id = sha3_256(thing_to_hash.encode()).hexdigest()
|
||||
tx_obj._hash()
|
||||
assert tx_obj._id == expected_hash_id
|
||||
assert tx_obj.id == expected_hash_id
|
||||
|
||||
|
||||
def test_output_from_dict_invalid_amount(user_output):
|
||||
from planetmint.transactions.common.transaction import Output
|
||||
from planetmint.transactions.common.exceptions import AmountError
|
||||
|
||||
out = user_output.to_dict()
|
||||
out["amount"] = "a"
|
||||
with raises(AmountError):
|
||||
Output.from_dict(out)
|
||||
|
||||
|
||||
def test_unspent_outputs_property(merlin, alice, bob, carol):
|
||||
tx = Create.generate(
|
||||
[merlin.public_key],
|
||||
[([alice.public_key], 1), ([bob.public_key], 2), ([carol.public_key], 3)],
|
||||
asset={"data": "QmaozNR7DZHQK1ZcU9p7QdrshMvXqWK6gpu5rmrkPdT3L4"},
|
||||
).sign([merlin.private_key])
|
||||
unspent_outputs = list(tx.unspent_outputs)
|
||||
assert len(unspent_outputs) == 3
|
||||
assert all(utxo.transaction_id == tx.id for utxo in unspent_outputs)
|
||||
assert all(utxo.asset_id == tx.id for utxo in unspent_outputs)
|
||||
assert all(utxo.output_index == i for i, utxo in enumerate(unspent_outputs))
|
||||
unspent_output_0 = unspent_outputs[0]
|
||||
assert unspent_output_0.amount == 1
|
||||
assert unspent_output_0.condition_uri == Ed25519Sha256(public_key=b58decode(alice.public_key)).condition_uri
|
||||
unspent_output_1 = unspent_outputs[1]
|
||||
assert unspent_output_1.amount == 2
|
||||
assert unspent_output_1.condition_uri == Ed25519Sha256(public_key=b58decode(bob.public_key)).condition_uri
|
||||
unspent_output_2 = unspent_outputs[2]
|
||||
assert unspent_output_2.amount == 3
|
||||
assert unspent_output_2.condition_uri == Ed25519Sha256(public_key=b58decode(carol.public_key)).condition_uri
|
||||
|
||||
|
||||
def test_spent_outputs_property(signed_transfer_tx):
|
||||
spent_outputs = list(signed_transfer_tx.spent_outputs)
|
||||
tx = signed_transfer_tx.to_dict()
|
||||
assert len(spent_outputs) == 1
|
||||
spent_output = spent_outputs[0]
|
||||
assert spent_output["transaction_id"] == tx["inputs"][0]["fulfills"]["transaction_id"]
|
||||
assert spent_output["output_index"] == tx["inputs"][0]["fulfills"]["output_index"]
|
||||
# assert spent_output._asdict() == tx['inputs'][0]['fulfills']
|
@ -1,15 +0,0 @@
|
||||
# Copyright © 2020 Interplanetary Database Association e.V.,
|
||||
# Planetmint and IPDB software contributors.
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
|
||||
def validate_transaction_model(tx):
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from planetmint.transactions.common.schema import validate_transaction_schema
|
||||
|
||||
tx_dict = tx.to_dict()
|
||||
# Check that a transaction is valid by re-serializing it
|
||||
# And calling validate_transaction_schema
|
||||
validate_transaction_schema(tx_dict)
|
||||
Transaction.from_dict(tx_dict)
|
@ -11,33 +11,27 @@ Tasks:
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import copy
|
||||
import random
|
||||
import tempfile
|
||||
import codecs
|
||||
import pytest
|
||||
|
||||
from ipld import marshal, multihash
|
||||
from collections import namedtuple
|
||||
from logging import getLogger
|
||||
from logging.config import dictConfig
|
||||
from planetmint.backend.connection import connect
|
||||
from planetmint.backend.tarantool.connection import TarantoolDBConnection
|
||||
|
||||
import pytest
|
||||
|
||||
# from pymongo import MongoClient
|
||||
|
||||
from planetmint import ValidatorElection
|
||||
from planetmint.transactions.common import crypto
|
||||
from planetmint.transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT
|
||||
from transactions.common import crypto
|
||||
from transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT
|
||||
from planetmint.tendermint_utils import key_from_base64
|
||||
from planetmint.backend import schema, query
|
||||
from planetmint.transactions.common.crypto import key_pair_from_ed25519_key, public_key_from_ed25519_key
|
||||
from planetmint.transactions.common.exceptions import DatabaseDoesNotExist
|
||||
from transactions.common.crypto import key_pair_from_ed25519_key, public_key_from_ed25519_key
|
||||
from transactions.common.exceptions import DatabaseDoesNotExist
|
||||
from planetmint.lib import Block
|
||||
from tests.utils import gen_vote
|
||||
from planetmint.config import Config
|
||||
from planetmint.upsert_validator import ValidatorElection # noqa
|
||||
|
||||
from transactions.types.elections.validator_election import ValidatorElection # noqa
|
||||
from tendermint.abci import types_pb2 as types
|
||||
from tendermint.crypto import keys_pb2
|
||||
|
||||
@ -141,8 +135,8 @@ def _setup_database(_configure_planetmint): # TODO Here is located setup databa
|
||||
|
||||
@pytest.fixture
|
||||
def _bdb(_setup_database, _configure_planetmint):
|
||||
from planetmint.transactions.common.memoize import to_dict, from_dict
|
||||
from planetmint.transactions.common.transaction import Transaction
|
||||
from transactions.common.memoize import to_dict, from_dict
|
||||
from transactions.common.transaction import Transaction
|
||||
from .utils import flush_db
|
||||
from planetmint.config import Config
|
||||
|
||||
@ -199,14 +193,14 @@ def user2_pk():
|
||||
|
||||
@pytest.fixture
|
||||
def alice():
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
return generate_key_pair()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def bob():
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
return generate_key_pair()
|
||||
|
||||
@ -223,7 +217,7 @@ def bob_pubkey(carol):
|
||||
|
||||
@pytest.fixture
|
||||
def carol():
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
return generate_key_pair()
|
||||
|
||||
@ -240,7 +234,7 @@ def carol_pubkey(carol):
|
||||
|
||||
@pytest.fixture
|
||||
def merlin():
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
return generate_key_pair()
|
||||
|
||||
@ -285,7 +279,7 @@ def mock_get_validators(network_validators):
|
||||
|
||||
@pytest.fixture
|
||||
def create_tx(alice, user_pk):
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from transactions.types.assets.create import Create
|
||||
|
||||
name = f"I am created by the create_tx fixture. My random identifier is {random.random()}."
|
||||
asset = {"data": multihash(marshal({"name": name}))}
|
||||
@ -306,7 +300,7 @@ def posted_create_tx(b, signed_create_tx):
|
||||
|
||||
@pytest.fixture
|
||||
def signed_transfer_tx(signed_create_tx, user_pk, user_sk):
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
|
||||
inputs = signed_create_tx.to_inputs()
|
||||
tx = Transfer.generate(inputs, [([user_pk], 1)], asset_id=signed_create_tx.id)
|
||||
@ -315,7 +309,7 @@ def signed_transfer_tx(signed_create_tx, user_pk, user_sk):
|
||||
|
||||
@pytest.fixture
|
||||
def double_spend_tx(signed_create_tx, carol_pubkey, user_sk):
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
|
||||
inputs = signed_create_tx.to_inputs()
|
||||
tx = Transfer.generate(inputs, [([carol_pubkey], 1)], asset_id=signed_create_tx.id)
|
||||
@ -329,7 +323,7 @@ def _get_height(b):
|
||||
|
||||
@pytest.fixture
|
||||
def inputs(user_pk, b, alice):
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from transactions.types.assets.create import Create
|
||||
|
||||
# create blocks with transactions for `USER` to spend
|
||||
for height in range(1, 4):
|
||||
@ -720,13 +714,13 @@ def new_validator():
|
||||
|
||||
@pytest.fixture
|
||||
def valid_upsert_validator_election(b_mock, node_key, new_validator):
|
||||
voters = ValidatorElection.recipients(b_mock)
|
||||
voters = b_mock.get_recipients_list()
|
||||
return ValidatorElection.generate([node_key.public_key], voters, new_validator, None).sign([node_key.private_key])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def valid_upsert_validator_election_2(b_mock, node_key, new_validator):
|
||||
voters = ValidatorElection.recipients(b_mock)
|
||||
voters = b_mock.get_recipients_list()
|
||||
return ValidatorElection.generate([node_key.public_key], voters, new_validator, None).sign([node_key.private_key])
|
||||
|
||||
|
||||
@ -756,14 +750,14 @@ def ongoing_validator_election_2(b, valid_upsert_validator_election_2, ed25519_n
|
||||
|
||||
@pytest.fixture
|
||||
def validator_election_votes(b_mock, ongoing_validator_election, ed25519_node_keys):
|
||||
voters = ValidatorElection.recipients(b_mock)
|
||||
voters = b_mock.get_recipients_list()
|
||||
votes = generate_votes(ongoing_validator_election, voters, ed25519_node_keys)
|
||||
return votes
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def validator_election_votes_2(b_mock, ongoing_validator_election_2, ed25519_node_keys):
|
||||
voters = ValidatorElection.recipients(b_mock)
|
||||
voters = b_mock.get_recipients_list()
|
||||
votes = generate_votes(ongoing_validator_election_2, voters, ed25519_node_keys)
|
||||
return votes
|
||||
|
||||
|
@ -3,20 +3,22 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
import warnings
|
||||
from unittest.mock import patch
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from ipld import marshal, multihash
|
||||
import pytest
|
||||
from base58 import b58decode
|
||||
import random
|
||||
import pytest
|
||||
|
||||
from unittest.mock import patch
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
from ipld import marshal, multihash
|
||||
from base58 import b58decode
|
||||
|
||||
|
||||
pytestmark = pytest.mark.bdb
|
||||
|
||||
|
||||
class TestBigchainApi(object):
|
||||
def test_get_spent_with_double_spend_detected(self, b, alice):
|
||||
from planetmint.transactions.common.exceptions import DoubleSpend
|
||||
from transactions.common.exceptions import DoubleSpend
|
||||
from planetmint.exceptions import CriticalDoubleSpend
|
||||
|
||||
tx = Create.generate([alice.public_key], [([alice.public_key], 1)])
|
||||
@ -85,8 +87,8 @@ class TestBigchainApi(object):
|
||||
@pytest.mark.usefixtures("inputs")
|
||||
def test_non_create_input_not_found(self, b, user_pk):
|
||||
from cryptoconditions import Ed25519Sha256
|
||||
from planetmint.transactions.common.exceptions import InputDoesNotExist
|
||||
from planetmint.transactions.common.transaction import Input, TransactionLink
|
||||
from transactions.common.exceptions import InputDoesNotExist
|
||||
from transactions.common.transaction import Input, TransactionLink
|
||||
|
||||
# Create an input for a non existing transaction
|
||||
input = Input(
|
||||
@ -94,7 +96,7 @@ class TestBigchainApi(object):
|
||||
)
|
||||
tx = Transfer.generate([input], [([user_pk], 1)], asset_id="mock_asset_link")
|
||||
with pytest.raises(InputDoesNotExist):
|
||||
tx.validate(b)
|
||||
b.validate_transaction(tx)
|
||||
|
||||
def test_write_transaction(self, b, user_sk, user_pk, alice, create_tx):
|
||||
|
||||
@ -116,8 +118,8 @@ class TestBigchainApi(object):
|
||||
|
||||
class TestTransactionValidation(object):
|
||||
def test_non_create_input_not_found(self, b, signed_transfer_tx):
|
||||
from planetmint.transactions.common.exceptions import InputDoesNotExist
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common.exceptions import InputDoesNotExist
|
||||
from transactions.common.transaction import TransactionLink
|
||||
|
||||
signed_transfer_tx.inputs[0].fulfills = TransactionLink("c", 0)
|
||||
with pytest.raises(InputDoesNotExist):
|
||||
@ -125,8 +127,8 @@ class TestTransactionValidation(object):
|
||||
|
||||
@pytest.mark.usefixtures("inputs")
|
||||
def test_non_create_valid_input_wrong_owner(self, b, user_pk):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from planetmint.transactions.common.exceptions import InvalidSignature
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.exceptions import InvalidSignature
|
||||
|
||||
input_tx = b.fastquery.get_outputs_by_public_key(user_pk).pop()
|
||||
input_transaction = b.get_transaction(input_tx.txid)
|
||||
@ -141,7 +143,7 @@ class TestTransactionValidation(object):
|
||||
|
||||
@pytest.mark.usefixtures("inputs")
|
||||
def test_non_create_double_spend(self, b, signed_create_tx, signed_transfer_tx, double_spend_tx):
|
||||
from planetmint.transactions.common.exceptions import DoubleSpend
|
||||
from transactions.common.exceptions import DoubleSpend
|
||||
|
||||
b.store_bulk_transactions([signed_create_tx, signed_transfer_tx])
|
||||
|
||||
@ -151,7 +153,7 @@ class TestTransactionValidation(object):
|
||||
|
||||
class TestMultipleInputs(object):
|
||||
def test_transfer_single_owner_single_input(self, b, inputs, user_pk, user_sk):
|
||||
from planetmint.transactions.common import crypto
|
||||
from transactions.common import crypto
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
|
||||
@ -162,12 +164,12 @@ class TestMultipleInputs(object):
|
||||
tx = tx.sign([user_sk])
|
||||
|
||||
# validate transaction
|
||||
tx.validate(b)
|
||||
b.validate_transaction(tx)
|
||||
assert len(tx.inputs) == 1
|
||||
assert len(tx.outputs) == 1
|
||||
|
||||
def test_single_owner_before_multiple_owners_after_single_input(self, b, user_sk, user_pk, inputs):
|
||||
from planetmint.transactions.common import crypto
|
||||
from transactions.common import crypto
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
user3_sk, user3_pk = crypto.generate_key_pair()
|
||||
@ -177,13 +179,13 @@ class TestMultipleInputs(object):
|
||||
tx = Transfer.generate(input_tx.to_inputs(), [([user2_pk, user3_pk], 1)], asset_id=input_tx.id)
|
||||
tx = tx.sign([user_sk])
|
||||
|
||||
tx.validate(b)
|
||||
b.validate_transaction(tx)
|
||||
assert len(tx.inputs) == 1
|
||||
assert len(tx.outputs) == 1
|
||||
|
||||
@pytest.mark.usefixtures("inputs")
|
||||
def test_multiple_owners_before_single_owner_after_single_input(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from transactions.common import crypto
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
user3_sk, user3_pk = crypto.generate_key_pair()
|
||||
@ -200,13 +202,13 @@ class TestMultipleInputs(object):
|
||||
transfer_tx = transfer_tx.sign([user_sk, user2_sk])
|
||||
|
||||
# validate transaction
|
||||
transfer_tx.validate(b)
|
||||
b.validate_transaction(transfer_tx)
|
||||
assert len(transfer_tx.inputs) == 1
|
||||
assert len(transfer_tx.outputs) == 1
|
||||
|
||||
@pytest.mark.usefixtures("inputs")
|
||||
def test_multiple_owners_before_multiple_owners_after_single_input(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from transactions.common import crypto
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
user3_sk, user3_pk = crypto.generate_key_pair()
|
||||
@ -223,13 +225,13 @@ class TestMultipleInputs(object):
|
||||
tx = Transfer.generate(tx_input.to_inputs(), [([user3_pk, user4_pk], 1)], asset_id=tx_input.id)
|
||||
tx = tx.sign([user_sk, user2_sk])
|
||||
|
||||
tx.validate(b)
|
||||
b.validate_transaction(tx)
|
||||
assert len(tx.inputs) == 1
|
||||
assert len(tx.outputs) == 1
|
||||
|
||||
def test_get_owned_ids_single_tx_single_output(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common import crypto
|
||||
from transactions.common.transaction import TransactionLink
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
|
||||
@ -253,8 +255,8 @@ class TestMultipleInputs(object):
|
||||
assert owned_inputs_user2 == [TransactionLink(tx_transfer.id, 0)]
|
||||
|
||||
def test_get_owned_ids_single_tx_multiple_outputs(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common import crypto
|
||||
from transactions.common.transaction import TransactionLink
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
|
||||
@ -284,8 +286,8 @@ class TestMultipleInputs(object):
|
||||
assert owned_inputs_user2 == [TransactionLink(tx_transfer.id, 0), TransactionLink(tx_transfer.id, 1)]
|
||||
|
||||
def test_get_owned_ids_multiple_owners(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common import crypto
|
||||
from transactions.common.transaction import TransactionLink
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
user3_sk, user3_pk = crypto.generate_key_pair()
|
||||
@ -314,7 +316,7 @@ class TestMultipleInputs(object):
|
||||
assert not spent_user1
|
||||
|
||||
def test_get_spent_single_tx_single_output(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from transactions.common import crypto
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
|
||||
@ -338,7 +340,7 @@ class TestMultipleInputs(object):
|
||||
assert spent_inputs_user1 == tx
|
||||
|
||||
def test_get_spent_single_tx_multiple_outputs(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from transactions.common import crypto
|
||||
|
||||
# create a new users
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
@ -371,7 +373,7 @@ class TestMultipleInputs(object):
|
||||
assert b.get_spent(tx_create.to_inputs()[2].fulfills.txid, 2) is None
|
||||
|
||||
def test_get_spent_multiple_owners(self, b, user_sk, user_pk, alice):
|
||||
from planetmint.transactions.common import crypto
|
||||
from transactions.common import crypto
|
||||
|
||||
user2_sk, user2_pk = crypto.generate_key_pair()
|
||||
user3_sk, user3_pk = crypto.generate_key_pair()
|
||||
@ -403,7 +405,7 @@ class TestMultipleInputs(object):
|
||||
|
||||
|
||||
def test_get_outputs_filtered_only_unspent():
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common.transaction import TransactionLink
|
||||
from planetmint.lib import Planetmint
|
||||
|
||||
go = "planetmint.fastquery.FastQuery.get_outputs_by_public_key"
|
||||
@ -418,7 +420,7 @@ def test_get_outputs_filtered_only_unspent():
|
||||
|
||||
|
||||
def test_get_outputs_filtered_only_spent():
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common.transaction import TransactionLink
|
||||
from planetmint.lib import Planetmint
|
||||
|
||||
go = "planetmint.fastquery.FastQuery.get_outputs_by_public_key"
|
||||
@ -435,7 +437,7 @@ def test_get_outputs_filtered_only_spent():
|
||||
@patch("planetmint.fastquery.FastQuery.filter_unspent_outputs")
|
||||
@patch("planetmint.fastquery.FastQuery.filter_spent_outputs")
|
||||
def test_get_outputs_filtered(filter_spent, filter_unspent):
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from transactions.common.transaction import TransactionLink
|
||||
from planetmint.lib import Planetmint
|
||||
|
||||
go = "planetmint.fastquery.FastQuery.get_outputs_by_public_key"
|
||||
@ -452,7 +454,7 @@ def test_cant_spend_same_input_twice_in_tx(b, alice):
|
||||
"""Recreate duplicated fulfillments bug
|
||||
https://github.com/planetmint/planetmint/issues/1099
|
||||
"""
|
||||
from planetmint.transactions.common.exceptions import DoubleSpend
|
||||
from transactions.common.exceptions import DoubleSpend
|
||||
|
||||
# create a divisible asset
|
||||
tx_create = Create.generate([alice.public_key], [([alice.public_key], 100)])
|
||||
@ -465,12 +467,12 @@ def test_cant_spend_same_input_twice_in_tx(b, alice):
|
||||
tx_transfer = Transfer.generate(dup_inputs, [([alice.public_key], 200)], asset_id=tx_create.id)
|
||||
tx_transfer_signed = tx_transfer.sign([alice.private_key])
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx_transfer_signed.validate(b)
|
||||
b.validate_transaction(tx_transfer_signed)
|
||||
|
||||
|
||||
def test_transaction_unicode(b, alice):
|
||||
import copy
|
||||
from planetmint.transactions.common.utils import serialize
|
||||
from transactions.common.utils import serialize
|
||||
|
||||
# http://www.fileformat.info/info/unicode/char/1f37a/index.htm
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
import pytest
|
||||
|
||||
from tests.utils import generate_election, generate_validators
|
||||
|
||||
from planetmint.lib import Block
|
||||
from planetmint.transactions.types.elections.election import Election
|
||||
from planetmint.transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from planetmint.upsert_validator.validator_election import ValidatorElection
|
||||
from transactions.types.elections.election import Election
|
||||
from transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from transactions.types.elections.validator_election import ValidatorElection
|
||||
|
||||
|
||||
@pytest.mark.bdb
|
||||
@ -31,11 +30,11 @@ def test_process_block_concludes_all_elections(b):
|
||||
total_votes += votes
|
||||
|
||||
b.store_abci_chain(1, "chain-X")
|
||||
Election.process_block(b, 1, txs)
|
||||
b.process_block(1, txs)
|
||||
b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict())
|
||||
b.store_bulk_transactions(txs)
|
||||
|
||||
Election.process_block(b, 2, total_votes)
|
||||
b.process_block(2, total_votes)
|
||||
|
||||
validators = b.get_validators()
|
||||
assert len(validators) == 5
|
||||
@ -78,11 +77,11 @@ def test_process_block_approves_only_one_validator_update(b):
|
||||
txs += [election]
|
||||
total_votes += votes
|
||||
|
||||
Election.process_block(b, 1, txs)
|
||||
b.process_block(1, txs)
|
||||
b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict())
|
||||
b.store_bulk_transactions(txs)
|
||||
|
||||
Election.process_block(b, 2, total_votes)
|
||||
b.process_block(2, total_votes)
|
||||
|
||||
validators = b.get_validators()
|
||||
assert len(validators) == 5
|
||||
@ -124,11 +123,11 @@ def test_process_block_approves_after_pending_validator_update(b):
|
||||
total_votes += votes
|
||||
|
||||
b.store_abci_chain(1, "chain-X")
|
||||
Election.process_block(b, 1, txs)
|
||||
b.process_block(1, txs)
|
||||
b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict())
|
||||
b.store_bulk_transactions(txs)
|
||||
|
||||
Election.process_block(b, 2, total_votes)
|
||||
b.process_block(2, total_votes)
|
||||
|
||||
validators = b.get_validators()
|
||||
assert len(validators) == 5
|
||||
@ -160,19 +159,19 @@ def test_process_block_does_not_approve_after_validator_update(b):
|
||||
total_votes = votes
|
||||
|
||||
b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict())
|
||||
Election.process_block(b, 1, txs)
|
||||
b.process_block(1, txs)
|
||||
b.store_bulk_transactions(txs)
|
||||
|
||||
second_election, second_votes = generate_election(
|
||||
b, ChainMigrationElection, public_key, private_key, {}, voter_keys
|
||||
)
|
||||
|
||||
Election.process_block(b, 2, total_votes + [second_election])
|
||||
b.process_block(2, total_votes + [second_election])
|
||||
|
||||
b.store_block(Block(height=2, transactions=[v.id for v in total_votes + [second_election]], app_hash="")._asdict())
|
||||
|
||||
b.store_abci_chain(1, "chain-X")
|
||||
Election.process_block(b, 3, second_votes)
|
||||
b.process_block(3, second_votes)
|
||||
|
||||
assert not b.get_election(second_election.id)["is_concluded"]
|
||||
assert b.get_latest_abci_chain() == {"height": 1, "chain_id": "chain-X", "is_synced": True}
|
||||
@ -197,11 +196,11 @@ def test_process_block_applies_only_one_migration(b):
|
||||
total_votes += votes
|
||||
|
||||
b.store_abci_chain(1, "chain-X")
|
||||
Election.process_block(b, 1, txs)
|
||||
b.process_block(1, txs)
|
||||
b.store_block(Block(height=1, transactions=[tx.id for tx in txs], app_hash="")._asdict())
|
||||
b.store_bulk_transactions(txs)
|
||||
|
||||
Election.process_block(b, 1, total_votes)
|
||||
b.process_block(1, total_votes)
|
||||
chain = b.get_latest_abci_chain()
|
||||
assert chain
|
||||
assert chain == {
|
||||
@ -215,4 +214,4 @@ def test_process_block_applies_only_one_migration(b):
|
||||
|
||||
|
||||
def test_process_block_gracefully_handles_empty_block(b):
|
||||
Election.process_block(b, 1, [])
|
||||
b.process_block(1, [])
|
||||
|
@ -1,7 +1,7 @@
|
||||
from planetmint.transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
|
||||
|
||||
def test_valid_migration_election(b_mock, node_key):
|
||||
voters = ChainMigrationElection.recipients(b_mock)
|
||||
voters = b_mock.get_recipients_list()
|
||||
election = ChainMigrationElection.generate([node_key.public_key], voters, {}, None).sign([node_key.private_key])
|
||||
assert election.validate(b_mock)
|
||||
assert b_mock.validate_election(election)
|
||||
|
@ -4,29 +4,26 @@
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import json
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
import pytest
|
||||
import random
|
||||
import multiprocessing as mp
|
||||
|
||||
import pytest
|
||||
|
||||
from tendermint.abci import types_pb2 as types
|
||||
from tendermint.crypto import keys_pb2
|
||||
|
||||
from transactions import ValidatorElection, ChainMigrationElection
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
from planetmint import App
|
||||
from planetmint.backend import query
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from planetmint.core import OkCode, CodeTypeError, rollback
|
||||
from planetmint.transactions.types.elections.election import Election
|
||||
from planetmint.lib import Block
|
||||
from planetmint.transactions.types.elections.chain_migration_election import ChainMigrationElection
|
||||
from planetmint.upsert_validator.validator_election import ValidatorElection
|
||||
from planetmint.upsert_validator.validator_utils import new_validator_set
|
||||
from planetmint.tendermint_utils import new_validator_set
|
||||
from planetmint.tendermint_utils import public_key_to_base64
|
||||
from planetmint.version import __tm_supported_versions__
|
||||
|
||||
from tests.utils import generate_election, generate_validators
|
||||
|
||||
|
||||
pytestmark = pytest.mark.bdb
|
||||
|
||||
|
||||
@ -198,9 +195,6 @@ def test_info(b):
|
||||
|
||||
|
||||
def test_check_tx__signed_create_is_ok(b):
|
||||
from planetmint import App
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
|
||||
alice = generate_key_pair()
|
||||
bob = generate_key_pair()
|
||||
|
||||
@ -212,9 +206,6 @@ def test_check_tx__signed_create_is_ok(b):
|
||||
|
||||
|
||||
def test_check_tx__unsigned_create_is_error(b):
|
||||
from planetmint import App
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
|
||||
alice = generate_key_pair()
|
||||
bob = generate_key_pair()
|
||||
|
||||
@ -226,10 +217,6 @@ def test_check_tx__unsigned_create_is_error(b):
|
||||
|
||||
|
||||
def test_deliver_tx__valid_create_updates_db_and_emits_event(b, init_chain_request):
|
||||
import multiprocessing as mp
|
||||
from planetmint import App
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
|
||||
alice = generate_key_pair()
|
||||
bob = generate_key_pair()
|
||||
events = mp.Queue()
|
||||
@ -261,9 +248,6 @@ def test_deliver_tx__valid_create_updates_db_and_emits_event(b, init_chain_reque
|
||||
|
||||
|
||||
def test_deliver_tx__double_spend_fails(b, init_chain_request):
|
||||
from planetmint import App
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
|
||||
alice = generate_key_pair()
|
||||
bob = generate_key_pair()
|
||||
|
||||
@ -286,9 +270,6 @@ def test_deliver_tx__double_spend_fails(b, init_chain_request):
|
||||
|
||||
|
||||
def test_deliver_transfer_tx__double_spend_fails(b, init_chain_request):
|
||||
from planetmint import App
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
|
||||
app = App(b)
|
||||
app.init_chain(init_chain_request)
|
||||
|
||||
@ -341,7 +322,7 @@ def test_end_block_return_validator_updates(b, init_chain_request):
|
||||
)
|
||||
b.store_block(Block(height=1, transactions=[election.id], app_hash="")._asdict())
|
||||
b.store_bulk_transactions([election])
|
||||
Election.process_block(b, 1, [election])
|
||||
b.process_block(1, [election])
|
||||
|
||||
app.block_transactions = votes
|
||||
|
||||
@ -425,7 +406,7 @@ def test_rollback_pre_commit_state_after_crash(b):
|
||||
for tx in txs:
|
||||
assert b.get_transaction(tx.id)
|
||||
assert b.get_latest_abci_chain()
|
||||
assert len(b.get_validator_change()["validators"]) == 1
|
||||
assert len(b.get_validator_set()["validators"]) == 1
|
||||
assert b.get_election(migration_election.id)
|
||||
assert b.get_election(validator_election.id)
|
||||
|
||||
@ -436,8 +417,8 @@ def test_rollback_pre_commit_state_after_crash(b):
|
||||
for tx in txs:
|
||||
assert not b.get_transaction(tx.id)
|
||||
assert not b.get_latest_abci_chain()
|
||||
assert len(b.get_validator_change()["validators"]) == 4
|
||||
assert len(b.get_validator_change(2)["validators"]) == 4
|
||||
assert len(b.get_validator_set()["validators"]) == 4
|
||||
assert len(b.get_validator_set(2)["validators"]) == 4
|
||||
assert not b.get_election(migration_election.id)
|
||||
assert not b.get_election(validator_election.id)
|
||||
|
||||
|
@ -5,9 +5,9 @@
|
||||
|
||||
import pytest
|
||||
|
||||
from planetmint.transactions.common.transaction import TransactionLink
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
from transactions.common.transaction import TransactionLink
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
|
||||
pytestmark = pytest.mark.bdb
|
||||
|
||||
@ -99,7 +99,7 @@ def test_outputs_query_key_order(b, user_pk, user_sk, user2_pk, user2_sk):
|
||||
|
||||
inputs = tx1.to_inputs()
|
||||
tx2 = Transfer.generate([inputs[1]], [([user2_pk], 2)], tx1.id).sign([user_sk])
|
||||
assert tx2.validate(b)
|
||||
assert b.validate_transaction(tx2)
|
||||
|
||||
tx2_dict = tx2.to_dict()
|
||||
fulfills = tx2_dict["inputs"][0]["fulfills"]
|
||||
|
@ -4,18 +4,15 @@
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import codecs
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
|
||||
from tendermint.abci import types_pb2 as types
|
||||
import json
|
||||
import pytest
|
||||
|
||||
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
from tendermint.abci import types_pb2 as types
|
||||
from abci.server import ProtocolHandler
|
||||
from abci.utils import read_messages
|
||||
|
||||
from planetmint.transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT, BROADCAST_TX_SYNC
|
||||
from transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT, BROADCAST_TX_SYNC
|
||||
from planetmint.version import __tm_supported_versions__
|
||||
from io import BytesIO
|
||||
|
||||
@ -24,7 +21,7 @@ from io import BytesIO
|
||||
def test_app(b, eventqueue_fixture, init_chain_request):
|
||||
from planetmint import App
|
||||
from planetmint.tendermint_utils import calculate_hash
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
app = App(b, eventqueue_fixture)
|
||||
p = ProtocolHandler(app)
|
||||
@ -111,7 +108,7 @@ def test_app(b, eventqueue_fixture, init_chain_request):
|
||||
|
||||
@pytest.mark.abci
|
||||
def test_post_transaction_responses(tendermint_ws_url, b):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
alice = generate_key_pair()
|
||||
bob = generate_key_pair()
|
||||
|
@ -3,23 +3,18 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
from operator import index
|
||||
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
from planetmint.transactions.types.assets.create import Create
|
||||
from planetmint.transactions.types.assets.transfer import Transfer
|
||||
|
||||
try:
|
||||
from hashlib import sha3_256
|
||||
except ImportError:
|
||||
# NOTE: needed for Python < 3.6
|
||||
from sha3 import sha3_256
|
||||
|
||||
import pytest
|
||||
from pymongo import MongoClient
|
||||
|
||||
from unittest.mock import patch
|
||||
from transactions.types.assets.create import Create
|
||||
from transactions.types.assets.transfer import Transfer
|
||||
from operator import index
|
||||
from hashlib import sha3_256
|
||||
from pymongo import MongoClient
|
||||
from planetmint import backend
|
||||
from planetmint.transactions.common.transaction_mode_types import (
|
||||
from transactions.common.transaction_mode_types import (
|
||||
BROADCAST_TX_COMMIT,
|
||||
BROADCAST_TX_ASYNC,
|
||||
BROADCAST_TX_SYNC,
|
||||
@ -31,7 +26,7 @@ from ipld import marshal, multihash
|
||||
@pytest.mark.bdb
|
||||
def test_asset_is_separated_from_transaciton(b):
|
||||
import copy
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from planetmint.backend.tarantool.connection import TarantoolDBConnection
|
||||
|
||||
if isinstance(b.connection, TarantoolDBConnection):
|
||||
@ -96,7 +91,7 @@ def test_get_empty_block(_0, _1, b):
|
||||
|
||||
|
||||
def test_validation_error(b):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
alice = generate_key_pair()
|
||||
tx = Create.generate([alice.public_key], [([alice.public_key], 1)], asset=None).sign([alice.private_key]).to_dict()
|
||||
@ -107,7 +102,7 @@ def test_validation_error(b):
|
||||
|
||||
@patch("requests.post")
|
||||
def test_write_and_post_transaction(mock_post, b):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from planetmint.tendermint_utils import encode_transaction
|
||||
|
||||
alice = generate_key_pair()
|
||||
@ -126,7 +121,7 @@ def test_write_and_post_transaction(mock_post, b):
|
||||
@patch("requests.post")
|
||||
@pytest.mark.parametrize("mode", [BROADCAST_TX_SYNC, BROADCAST_TX_ASYNC, BROADCAST_TX_COMMIT])
|
||||
def test_post_transaction_valid_modes(mock_post, b, mode):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
alice = generate_key_pair()
|
||||
tx = Create.generate([alice.public_key], [([alice.public_key], 1)], asset=None).sign([alice.private_key]).to_dict()
|
||||
@ -138,8 +133,8 @@ def test_post_transaction_valid_modes(mock_post, b, mode):
|
||||
|
||||
|
||||
def test_post_transaction_invalid_mode(b):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from planetmint.transactions.common.exceptions import ValidationError
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.exceptions import ValidationError
|
||||
|
||||
alice = generate_key_pair()
|
||||
tx = Create.generate([alice.public_key], [([alice.public_key], 1)], asset=None).sign([alice.private_key]).to_dict()
|
||||
@ -409,7 +404,7 @@ def test_get_utxoset_merkle_root(b, utxoset):
|
||||
@pytest.mark.bdb
|
||||
def test_get_spent_transaction_critical_double_spend(b, alice, bob, carol):
|
||||
from planetmint.exceptions import CriticalDoubleSpend
|
||||
from planetmint.transactions.common.exceptions import DoubleSpend
|
||||
from transactions.common.exceptions import DoubleSpend
|
||||
|
||||
asset = {"data": multihash(marshal({"test": "asset"}))}
|
||||
|
||||
@ -428,7 +423,7 @@ def test_get_spent_transaction_critical_double_spend(b, alice, bob, carol):
|
||||
b.store_bulk_transactions([tx])
|
||||
|
||||
with pytest.raises(DoubleSpend):
|
||||
same_input_double_spend.validate(b)
|
||||
b.validate_transaction(same_input_double_spend)
|
||||
|
||||
assert b.get_spent(tx.id, tx_transfer.inputs[0].fulfills.output, [tx_transfer])
|
||||
|
||||
@ -447,7 +442,7 @@ def test_get_spent_transaction_critical_double_spend(b, alice, bob, carol):
|
||||
|
||||
|
||||
def test_validation_with_transaction_buffer(b):
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
|
||||
priv_key, pub_key = generate_key_pair()
|
||||
|
||||
@ -497,8 +492,8 @@ def test_migrate_abci_chain_generates_new_chains(b, chain, block_height, expecte
|
||||
@pytest.mark.bdb
|
||||
def test_get_spent_key_order(b, user_pk, user_sk, user2_pk, user2_sk):
|
||||
from planetmint import backend
|
||||
from planetmint.transactions.common.crypto import generate_key_pair
|
||||
from planetmint.transactions.common.exceptions import DoubleSpend
|
||||
from transactions.common.crypto import generate_key_pair
|
||||
from transactions.common.exceptions import DoubleSpend
|
||||
|
||||
alice = generate_key_pair()
|
||||
bob = generate_key_pair()
|
||||
@ -508,7 +503,7 @@ def test_get_spent_key_order(b, user_pk, user_sk, user2_pk, user2_sk):
|
||||
|
||||
inputs = tx1.to_inputs()
|
||||
tx2 = Transfer.generate([inputs[1]], [([user2_pk], 2)], tx1.id).sign([user_sk])
|
||||
assert tx2.validate(b)
|
||||
assert b.validate_transaction(tx2)
|
||||
|
||||
tx2_dict = tx2.to_dict()
|
||||
fulfills = tx2_dict["inputs"][0]["fulfills"]
|
||||
@ -522,4 +517,4 @@ def test_get_spent_key_order(b, user_pk, user_sk, user2_pk, user2_sk):
|
||||
tx3 = Transfer.generate([inputs[1]], [([bob.public_key], 2)], tx1.id).sign([user_sk])
|
||||
|
||||
with pytest.raises(DoubleSpend):
|
||||
tx3.validate(b)
|
||||
b.validate_transaction(tx3)
|
||||
|
@ -5,12 +5,9 @@
|
||||
|
||||
import base64
|
||||
import json
|
||||
from pytest import mark
|
||||
|
||||
try:
|
||||
from hashlib import sha3_256
|
||||
except ImportError:
|
||||
from sha3 import sha3_256
|
||||
from pytest import mark
|
||||
from hashlib import sha3_256
|
||||
|
||||
|
||||
def test_encode_decode_transaction(b):
|
||||
|
@ -3,12 +3,10 @@
|
||||
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
|
||||
# Code is Apache-2.0 and docs are CC-BY-4.0
|
||||
|
||||
import copy
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import planetmint
|
||||
|
||||
from unittest.mock import mock_open, patch
|
||||
from planetmint.config import Config
|
||||
|
||||
|
||||
@ -289,7 +287,7 @@ def test_file_config():
|
||||
|
||||
def test_invalid_file_config():
|
||||
from planetmint.config_utils import file_config
|
||||
from planetmint.transactions.common import exceptions
|
||||
from transactions.common import exceptions
|
||||
|
||||
with patch("builtins.open", mock_open(read_data="{_INVALID_JSON_}")):
|
||||
with pytest.raises(exceptions.ConfigurationError):
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user