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:
Lorenz Herzberger 2022-10-13 09:31:19 +02:00 committed by GitHub
parent 69fe9b253d
commit 3954340d7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
116 changed files with 984 additions and 5873 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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."""

View File

@ -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

View File

@ -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)

View File

@ -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,
)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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__

View File

@ -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

View File

@ -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"])

View File

@ -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"""

View File

@ -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:

View File

@ -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"))

View File

@ -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):

View File

@ -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

View File

@ -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):

View File

@ -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")

View File

@ -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"""

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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}$"

View File

@ -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"

View File

@ -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

View File

@ -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}$"

View File

@ -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}$"

View File

@ -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

View File

@ -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}$"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}$"

View File

@ -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)

View File

@ -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)

View File

@ -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"

View File

@ -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"))

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -8,7 +8,6 @@
import logging
from flask import jsonify, request
from planetmint.config import Config

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -5,7 +5,6 @@
from flask import current_app
from flask_restful import reqparse, Resource
from planetmint.web.views import parameters

View File

@ -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,

View File

@ -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__)

View File

@ -5,6 +5,7 @@
import json
from planetmint.events import EventTypes
from planetmint.events import POISON_PILL

View File

@ -21,7 +21,6 @@ import logging
import threading
import aiohttp
from uuid import uuid4
from concurrent.futures import CancelledError
from planetmint.config import Config

View File

@ -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",

View File

@ -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

View File

@ -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)

View File

@ -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,
)

View File

@ -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()

View File

@ -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()

View File

@ -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):

View File

@ -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():

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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']

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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, [])

View File

@ -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)

View File

@ -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)

View File

@ -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"]

View File

@ -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()

View File

@ -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)

View File

@ -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):

View File

@ -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