Final zenroom (#147)

* zenroom fixes

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* expl. defined the aiohttp package

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* increased version number and fixed a zenroom runtime bug

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* added fialing zenroom tx signing test

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* extended test to pass zenrooom validation, but to fail planetmint validation.

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* added manual tx crafting

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* added zenroom fulfillment verification

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* the last mile before integration

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* zenroom unit tests are passing

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* simplified zenroom unit tests

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* removed obsolte lines from the zenroom tests

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* fixed acceptance tests

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* adjusted zenroom integraiton tests

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* fixed linting errors

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* simplified zenroom unit test

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* increased version number

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* using cryptoconditions without print message

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* increased cc usage to 0.9.9 readded daemon proceses

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* increased version to 0.9.6

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>

* fixed deployment issue for 0.9.6

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>
This commit is contained in:
Jürgen Eckel 2022-06-17 11:04:12 +02:00 committed by GitHub
parent e87f4c08e4
commit 5bd3784dbf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1128 additions and 779 deletions

View File

@ -25,6 +25,10 @@ For reference, the possible headings are:
* **Known Issues**
* **Notes**
## [0.9.7] - 2022-06-17
### Feature Update
Deep Zenroom integration
## [0.9.6] - 2022-06-08

View File

@ -1,64 +1,18 @@
FROM python:3.9
RUN apt-get update \
&& pip install -U pip \
&& apt-get autoremove \
&& apt-get clean
&& pip install -U pip \
&& apt-get autoremove \
&& apt-get clean
RUN apt-get install -y vim zsh build-essential cmake
RUN mkdir -p /src
RUN /usr/local/bin/python -m pip install --upgrade pip
RUN pip install --upgrade meson ninja
RUN pip install zenroom==2.0.0.dev1644927841
RUN pip install --upgrade \
pycco \
websocket-client~=0.47.0 \
pytest~=3.0 \
#git+https://github.com/planetmint/cryptoconditions.git@gitzenroom \
#git+https://github.com/planetmint/planetmint-driver.git@gitzenroom \
planetmint-cryptoconditions>=0.9.4\
planetmint-driver>=0.9.0 \
planetmint-cryptoconditions>=0.9.9\
planetmint-driver>=0.9.2 \
blns
#FROM python:3.9
#
#RUN apt-get update && apt-get install -y vim zsh
#RUN apt-get update \
# && apt-get install -y git zsh\
# && pip install -U pip \
# && apt-get autoremove \
# && apt-get clean
#RUN apt install sudo
#RUN apt-get install -y python3 openssl ca-certificates git python3-dev
#RUN apt-get install zsh gcc
#RUN apt-get install libffi-dev
#RUN apt-get install build-essential cmake -y
#
#
#RUN mkdir -p /src
#RUN pip install --upgrade \
# pycco \
# websocket-client~=0.47.0 \
# pytest~=3.0 \
# planetmint-driver>=0.9.0 \
# blns \
# git+https://github.com/planetmint/cryptoconditions.git@gitzenroom >=0.9.0 \
# chardet==3.0.4 \
# aiohttp==3.7.4 \
# abci==0.8.3 \
# #planetmint-cryptoconditions>=0.9.0\
# flask-cors==3.0.10 \
# flask-restful==0.3.9 \
# flask==2.0.1 \
# gunicorn==20.1.0 \
# jsonschema==3.2.0 \
# logstats==0.3.0 \
# packaging>=20.9 \
# pymongo==3.11.4 \
# pyyaml==5.4.1 \
# requests==2.25.1 \
# setproctitle==1.2.2
#

View File

@ -5,36 +5,40 @@
import pytest
GENERATE_KEYPAIR = \
"""Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': Create the keypair
Given that I am known as 'Pippo'
When I create the ecdh key
When I create the testnet key
Then print data"""
# secret key to public key
SK_TO_PK = \
"""Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': Create the keypair
Given that I am known as '{}'
Given I have the 'keys'
When I create the ecdh public key
When I create the testnet address
Then print my 'ecdh public key'
Then print my 'testnet address'"""
CONDITION_SCRIPT = """
Scenario 'ecdh': create the signature of an object
Given I have the 'keyring'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
When I create the signature of 'houses'
Then print the 'signature'"""
FULFILL_SCRIPT = \
"""Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': Bob verifies the signature from Alice
"""Scenario 'ecdh': Bob verifies the signature from Alice
Given I have a 'ecdh public key' from 'Alice'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
Given I have a 'signature' named 'data.signature' inside 'result'
When I verify the 'houses' has a signature in 'data.signature' by 'Alice'
Given I have a 'signature' named 'signature' inside 'result'
When I verify the 'houses' has a signature in 'signature' by 'Alice'
Then print the string 'ok'"""
SK_TO_PK = \
"""Scenario 'ecdh': Create the keypair
Given that I am known as '{}'
Given I have the 'keyring'
When I create the ecdh public key
When I create the bitcoin address
Then print my 'ecdh public key'
Then print my 'bitcoin address'"""
GENERATE_KEYPAIR = \
"""Scenario 'ecdh': Create the keypair
Given that I am known as 'Pippo'
When I create the ecdh key
When I create the bitcoin key
Then print data"""
ZENROOM_DATA = {
'also': 'more data'
}
HOUSE_ASSETS = {
"data": {
@ -51,19 +55,11 @@ HOUSE_ASSETS = {
}
}
ZENROOM_DATA = {
'also': 'more data'
metadata = {
'units': 300,
'type': 'KG'
}
CONDITION_SCRIPT = """Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': create the signature of an object
Given I have the 'keys'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
When I create the signature of 'houses'
When I rename the 'signature' to 'data.signature'
Then print the 'data.signature'"""
@pytest.fixture
def gen_key_zencode():
return GENERATE_KEYPAIR
@ -86,4 +82,4 @@ def zenroom_house_assets():
@pytest.fixture
def zenroom_data():
return ZENROOM_DATA
return ZENROOM_DATA

View File

@ -1,68 +1,73 @@
# GOAL:
# In this script I tried to implement the ECDSA signature using zenroom
# However, the scripts are customizable and so with the same procedure
# we can implement more complex smart contracts
# PUBLIC IDENTITY
# The public identity of the users in this script (Bob and Alice)
# is the pair (ECDH public key, Testnet address)
import os
import json
import base58
from hashlib import sha3_256
from cryptoconditions.types.ed25519 import Ed25519Sha256
from cryptoconditions.types.zenroom import ZenroomSha256
from zenroom import zencode_exec
from planetmint_driver import Planetmint
from planetmint_driver.crypto import generate_keypair
import hashlib
from cryptoconditions import ZenroomSha256
from json.decoder import JSONDecodeError
def test_zenroom(gen_key_zencode, secret_key_to_private_key_zencode, fulfill_script_zencode,
condition_script_zencode, zenroom_data, zenroom_house_assets):
alice = json.loads(ZenroomSha256.run_zenroom(gen_key_zencode).output)['keys']
bob = json.loads(ZenroomSha256.run_zenroom(gen_key_zencode).output)['keys']
zen_public_keys = json.loads(ZenroomSha256.run_zenroom(secret_key_to_private_key_zencode.format('Alice'),
keys={'keys': alice}).output)
zen_public_keys.update(json.loads(ZenroomSha256.run_zenroom(secret_key_to_private_key_zencode.format('Bob'),
keys={'keys': bob}).output))
# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for buyer
zenSha = ZenroomSha256(script=fulfill_script_zencode, keys=zen_public_keys, data=zenroom_data)
def test_zenroom_signing(gen_key_zencode, secret_key_to_private_key_zencode,
fulfill_script_zencode, zenroom_data, zenroom_house_assets,
condition_script_zencode):
biolabs = generate_keypair()
version = '2.0'
alice = json.loads(zencode_exec(gen_key_zencode).output)['keyring']
bob = json.loads(zencode_exec(gen_key_zencode).output)['keyring']
zen_public_keys = json.loads(zencode_exec(secret_key_to_private_key_zencode.format('Alice'),
keys=json.dumps({'keyring': alice})).output)
zen_public_keys.update(json.loads(zencode_exec(secret_key_to_private_key_zencode.format('Bob'),
keys=json.dumps({'keyring': bob})).output))
zenroomscpt = ZenroomSha256(script=fulfill_script_zencode, data=zenroom_data, keys=zen_public_keys)
print(F'zenroom is: {zenroomscpt.script}')
# CRYPTO-CONDITIONS: generate the condition uri
condition_uri = zenSha.condition.serialize_uri()
condition_uri_zen = zenroomscpt.condition.serialize_uri()
print(F'\nzenroom condition URI: {condition_uri_zen}')
# CRYPTO-CONDITIONS: construct an unsigned fulfillment dictionary
unsigned_fulfillment_dict = {
'type': zenSha.TYPE_NAME,
'script': fulfill_script_zencode,
'keys': zen_public_keys,
unsigned_fulfillment_dict_zen = {
'type': zenroomscpt.TYPE_NAME,
'public_key': base58.b58encode(biolabs.public_key).decode(),
}
output = {
'amount': '1000',
'amount': '10',
'condition': {
'details': unsigned_fulfillment_dict,
'uri': condition_uri,
'details': unsigned_fulfillment_dict_zen,
'uri': condition_uri_zen,
},
'data': zenroom_data,
'script': fulfill_script_zencode,
'conf': '',
'public_keys': (zen_public_keys['Alice']['ecdh_public_key'], ),
'public_keys': [biolabs.public_key,],
}
input_ = {
'fulfillment': None,
'fulfills': None,
'owners_before': (zen_public_keys['Alice']['ecdh_public_key'], ),
'owners_before': [biolabs.public_key,]
}
metadata = {
"result": {
"output": ["ok"]
}
}
token_creation_tx = {
'operation': 'CREATE',
'asset': zenroom_house_assets,
'metadata': None,
'outputs': (output,),
'inputs': (input_,),
'version': '2.0',
'metadata': metadata,
'outputs': [output,],
'inputs': [input_,],
'version': version,
'id': None,
}
@ -74,12 +79,36 @@ condition_script_zencode, zenroom_data, zenroom_house_assets):
ensure_ascii=False,
)
try:
assert(not zenSha.validate(message=message))
except JSONDecodeError:
pass
except ValueError:
pass
# major workflow:
# we store the fulfill script in the transaction/message (zenroom-sha)
# the condition script is used to fulfill the transaction and create the signature
#
# the server should ick the fulfill script and recreate the zenroom-sha and verify the signature
message = zenSha.sign(message, condition_script_zencode, alice)
assert(zenSha.validate(message=message))
message = zenroomscpt.sign(message, condition_script_zencode, alice)
assert(zenroomscpt.validate(message=message))
message = json.loads(message)
fulfillment_uri_zen = zenroomscpt.serialize_uri()
message['inputs'][0]['fulfillment'] = fulfillment_uri_zen
tx = message
tx['id'] = None
json_str_tx = json.dumps(
tx,
sort_keys=True,
skipkeys=False,
separators=(',', ':')
)
# SHA3: hash the serialized id-less transaction to generate the id
shared_creation_txid = sha3_256(json_str_tx.encode()).hexdigest()
message['id'] = shared_creation_txid
# `https://example.com:9984`
plntmnt = Planetmint(os.environ.get('PLANETMINT_ENDPOINT'))
sent_transfer_tx = plntmnt.transactions.send_commit(message)
print( f"\n\nstatus and result : + {sent_transfer_tx}")

View File

@ -13,10 +13,8 @@ RUN mkdir -p /src
RUN pip install --upgrade meson ninja
RUN pip install --upgrade \
pytest~=6.2.5 \
planetmint-driver~=0.9.0 \
pycco \
websocket-client~=0.47.0 \
#git+https://github.com/planetmint/cryptoconditions.git@gitzenroom \
#git+https://github.com/planetmint/planetmint-driver.git@gitzenroom \
planetmint-cryptoconditions>=0.9.9\
planetmint-driver>=0.9.2 \
blns

View File

@ -5,36 +5,40 @@
import pytest
GENERATE_KEYPAIR = \
"""Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': Create the keypair
Given that I am known as 'Pippo'
When I create the ecdh key
When I create the testnet key
Then print data"""
# secret key to public key
SK_TO_PK = \
"""Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': Create the keypair
Given that I am known as '{}'
Given I have the 'keys'
When I create the ecdh public key
When I create the testnet address
Then print my 'ecdh public key'
Then print my 'testnet address'"""
CONDITION_SCRIPT = """
Scenario 'ecdh': create the signature of an object
Given I have the 'keyring'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
When I create the signature of 'houses'
Then print the 'signature'"""
FULFILL_SCRIPT = \
"""Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': Bob verifies the signature from Alice
"""Scenario 'ecdh': Bob verifies the signature from Alice
Given I have a 'ecdh public key' from 'Alice'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
Given I have a 'signature' named 'data.signature' inside 'result'
When I verify the 'houses' has a signature in 'data.signature' by 'Alice'
Given I have a 'signature' named 'signature' inside 'result'
When I verify the 'houses' has a signature in 'signature' by 'Alice'
Then print the string 'ok'"""
SK_TO_PK = \
"""Scenario 'ecdh': Create the keypair
Given that I am known as '{}'
Given I have the 'keyring'
When I create the ecdh public key
When I create the bitcoin address
Then print my 'ecdh public key'
Then print my 'bitcoin address'"""
GENERATE_KEYPAIR = \
"""Scenario 'ecdh': Create the keypair
Given that I am known as 'Pippo'
When I create the ecdh key
When I create the bitcoin key
Then print data"""
ZENROOM_DATA = {
'also': 'more data'
}
HOUSE_ASSETS = {
"data": {
@ -51,45 +55,31 @@ HOUSE_ASSETS = {
}
}
ZENROOM_DATA = {
'also': 'more data'
metadata = {
'units': 300,
'type': 'KG'
}
CONDITION_SCRIPT = """Rule input encoding base58
Rule output encoding base58
Scenario 'ecdh': create the signature of an object
Given I have the 'keys'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
When I create the signature of 'houses'
When I rename the 'signature' to 'data.signature'
Then print the 'data.signature'"""
@pytest.fixture
def gen_key_zencode():
return GENERATE_KEYPAIR
@pytest.fixture
def secret_key_to_private_key_zencode():
return SK_TO_PK
@pytest.fixture
def fulfill_script_zencode():
return FULFILL_SCRIPT
@pytest.fixture
def condition_script_zencode():
return CONDITION_SCRIPT
@pytest.fixture
def zenroom_house_assets():
return HOUSE_ASSETS
@pytest.fixture
def zenroom_data():
return ZENROOM_DATA

View File

@ -1,84 +1,123 @@
# GOAL:
# In this script I tried to implement the ECDSA signature using zenroom
# However, the scripts are customizable and so with the same procedure
# we can implement more complex smart contracts
# PUBLIC IDENTITY
# The public identity of the users in this script (Bob and Alice)
# is the pair (ECDH public key, Testnet address)
import json
from cryptoconditions import ZenroomSha256
from json.decoder import JSONDecodeError
import base58
from hashlib import sha3_256
from cryptoconditions.types.zenroom import ZenroomSha256
from planetmint_driver.crypto import generate_keypair
from .helper.hosts import Hosts
from zenroom import zencode_exec
import time
def test_zenroom(gen_key_zencode, secret_key_to_private_key_zencode, fulfill_script_zencode,
condition_script_zencode, zenroom_data, zenroom_house_assets):
alice = json.loads(ZenroomSha256.run_zenroom(gen_key_zencode).output)['keys']
bob = json.loads(ZenroomSha256.run_zenroom(gen_key_zencode).output)['keys']
def test_zenroom_signing(
gen_key_zencode,
secret_key_to_private_key_zencode,
fulfill_script_zencode,
zenroom_data,
zenroom_house_assets,
condition_script_zencode,
):
zen_public_keys = json.loads(ZenroomSha256.run_zenroom(secret_key_to_private_key_zencode.format('Alice'),
keys={'keys': alice}).output)
zen_public_keys.update(json.loads(ZenroomSha256.run_zenroom(secret_key_to_private_key_zencode.format('Bob'),
keys={'keys': bob}).output))
biolabs = generate_keypair()
version = "2.0"
# CRYPTO-CONDITIONS: instantiate an Ed25519 crypto-condition for buyer
zenSha = ZenroomSha256(script=fulfill_script_zencode, keys=zen_public_keys, data=zenroom_data)
alice = json.loads(zencode_exec(gen_key_zencode).output)["keyring"]
bob = json.loads(zencode_exec(gen_key_zencode).output)["keyring"]
zen_public_keys = json.loads(
ZenroomSha256.run_zenroom(
secret_key_to_private_key_zencode.format("Alice"),
keys=json.dumps({"keyring": alice}),
).output
)
zen_public_keys.update(
json.loads(
ZenroomSha256.run_zenroom(
secret_key_to_private_key_zencode.format("Bob"),
keys=json.dumps({"keyring": bob}),
).output
)
)
zenroomscpt = ZenroomSha256(
script=fulfill_script_zencode, data=zenroom_data, keys=zen_public_keys
)
print(f"zenroom is: {zenroomscpt.script}")
# CRYPTO-CONDITIONS: generate the condition uri
condition_uri = zenSha.condition.serialize_uri()
condition_uri_zen = zenroomscpt.condition.serialize_uri()
print(f"\nzenroom condition URI: {condition_uri_zen}")
# CRYPTO-CONDITIONS: construct an unsigned fulfillment dictionary
unsigned_fulfillment_dict = {
'type': zenSha.TYPE_NAME,
'script': fulfill_script_zencode,
'keys': zen_public_keys,
unsigned_fulfillment_dict_zen = {
"type": zenroomscpt.TYPE_NAME,
"public_key": base58.b58encode(biolabs.public_key).decode(),
}
output = {
'amount': '1000',
'condition': {
'details': unsigned_fulfillment_dict,
'uri': condition_uri,
"amount": "10",
"condition": {
"details": unsigned_fulfillment_dict_zen,
"uri": condition_uri_zen,
},
'data': zenroom_data,
'script': fulfill_script_zencode,
'conf': '',
'public_keys': (zen_public_keys['Alice']['ecdh_public_key'], ),
"public_keys": [
biolabs.public_key,
],
}
input_ = {
'fulfillment': None,
'fulfills': None,
'owners_before': (zen_public_keys['Alice']['ecdh_public_key'], ),
"fulfillment": None,
"fulfills": None,
"owners_before": [
biolabs.public_key,
],
}
metadata = {"result": {"output": ["ok"]}}
token_creation_tx = {
'operation': 'CREATE',
'asset': zenroom_house_assets,
'metadata': None,
'outputs': (output,),
'inputs': (input_,),
'version': '2.0',
'id': None,
"operation": "CREATE",
"asset": zenroom_house_assets,
"metadata": metadata,
"outputs": [
output,
],
"inputs": [
input_,
],
"version": version,
"id": None,
}
# JSON: serialize the transaction-without-id to a json formatted string
message = json.dumps(
token_creation_tx,
sort_keys=True,
separators=(',', ':'),
separators=(",", ":"),
ensure_ascii=False,
)
try:
assert(not zenSha.validate(message=message))
except JSONDecodeError:
pass
except ValueError:
pass
# major workflow:
# we store the fulfill script in the transaction/message (zenroom-sha)
# the condition script is used to fulfill the transaction and create the signature
#
# the server should ick the fulfill script and recreate the zenroom-sha and verify the signature
message = zenSha.sign(message, condition_script_zencode, alice)
assert(zenSha.validate(message=message))
message = zenroomscpt.sign(message, condition_script_zencode, alice)
assert zenroomscpt.validate(message=message)
message = json.loads(message)
fulfillment_uri_zen = zenroomscpt.serialize_uri()
message["inputs"][0]["fulfillment"] = fulfillment_uri_zen
tx = message
tx["id"] = None
json_str_tx = json.dumps(tx, sort_keys=True, skipkeys=False, separators=(",", ":"))
# SHA3: hash the serialized id-less transaction to generate the id
shared_creation_txid = sha3_256(json_str_tx.encode()).hexdigest()
message["id"] = shared_creation_txid
hosts = Hosts("/shared/hostnames")
pm_alpha = hosts.get_connection()
sent_transfer_tx = pm_alpha.transactions.send_commit(message)
time.sleep(1)
# Assert that transaction is stored on both planetmint nodes
hosts.assert_transaction(shared_creation_txid)
print(f"\n\nstatus and result : + {sent_transfer_tx}")

View File

@ -36,26 +36,29 @@ BANNER = """
def start(args):
# Exchange object for event stream api
logger.info('Starting Planetmint')
logger.info("Starting Planetmint")
exchange = Exchange()
# start the web api
app_server = server.create_server(
settings=planetmint.config['server'],
log_config=planetmint.config['log'],
planetmint_factory=Planetmint)
p_webapi = Process(name='planetmint_webapi', target=app_server.run, daemon=True)
settings=planetmint.config["server"],
log_config=planetmint.config["log"],
planetmint_factory=Planetmint,
)
p_webapi = Process(name="planetmint_webapi", target=app_server.run, daemon=True)
p_webapi.start()
logger.info(BANNER.format(planetmint.config['server']['bind']))
logger.info(BANNER.format(planetmint.config["server"]["bind"]))
# start websocket server
p_websocket_server = Process(name='planetmint_ws',
target=websocket_server.start,
daemon=True,
args=(exchange.get_subscriber_queue(EventTypes.BLOCK_VALID),))
p_websocket_server = Process(
name="planetmint_ws",
target=websocket_server.start,
daemon=True,
args=(exchange.get_subscriber_queue(EventTypes.BLOCK_VALID),),
)
p_websocket_server.start()
p_exchange = Process(name='planetmint_exchange', target=exchange.run, daemon=True)
p_exchange = Process(name="planetmint_exchange", target=exchange.run, daemon=True)
p_exchange.start()
# We need to import this after spawning the web server
@ -63,7 +66,7 @@ def start(args):
# for gevent.
from abci.server import ABCIServer
setproctitle.setproctitle('planetmint')
setproctitle.setproctitle("planetmint")
# Start the ABCIServer
# abci = ABCI(TmVersion(planetmint.config['tendermint']['version']))
@ -82,5 +85,5 @@ def start(args):
app.run()
if __name__ == '__main__':
if __name__ == "__main__":
start()

View File

@ -6,7 +6,8 @@
from functools import reduce
import base58
from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256
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
@ -24,30 +25,30 @@ class Output(object):
owners before a Transaction was confirmed.
"""
MAX_AMOUNT = 9 * 10 ** 18
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.
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`.
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')
raise TypeError("`public_keys` must be a list instance or None")
if not isinstance(amount, int):
raise TypeError('`amount` must be an int')
raise TypeError("`amount` must be an int")
if amount < 1:
raise AmountError('`amount` must be greater than 0')
raise AmountError("`amount` must be greater than 0")
if amount > self.MAX_AMOUNT:
raise AmountError('`amount` must be <= %s' % self.MAX_AMOUNT)
raise AmountError("`amount` must be <= %s" % self.MAX_AMOUNT)
self.fulfillment = fulfillment
self.amount = amount
@ -60,30 +61,31 @@ class Output(object):
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.
Note:
A dictionary serialization of the Input the Output was
derived from is always provided.
Returns:
dict: The Output as an alternative serialization format.
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:
condition['details'] = _fulfillment_to_details(self.fulfillment)
# 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
condition["uri"] = self.fulfillment.condition_uri
except AttributeError:
condition['uri'] = self.fulfillment
condition["uri"] = self.fulfillment
output = {
'public_keys': self.public_keys,
'condition': condition,
'amount': str(self.amount),
"public_keys": self.public_keys,
"condition": condition,
"amount": str(self.amount),
}
return output
@ -91,66 +93,65 @@ class Output(object):
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:
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)*, ...], ...]
[(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.
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.
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.
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')
raise TypeError("`amount` must be a int")
if amount < 1:
raise AmountError('`amount` needs to be greater than zero')
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')
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')
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]))
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)
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`.
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.
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`:
Returns:
:class:`cryptoconditions.ThresholdSha256`:
"""
try:
threshold = len(new_public_keys)
@ -161,7 +162,7 @@ class Output(object):
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')
raise ValueError("Sublist cannot contain single owner")
else:
try:
new_public_keys = new_public_keys.pop()
@ -176,8 +177,7 @@ class Output(object):
if isinstance(new_public_keys, Fulfillment):
ffill = new_public_keys
else:
ffill = Ed25519Sha256(
public_key=base58.b58decode(new_public_keys))
ffill = Ed25519Sha256(public_key=base58.b58decode(new_public_keys))
initial.add_subfulfillment(ffill)
return initial
@ -185,25 +185,25 @@ class Output(object):
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.
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.
Args:
data (dict): The dict to be transformed.
Returns:
:class:`~planetmint.transactions.common.transaction.Output`
Returns:
:class:`~planetmint.transactions.common.transaction.Output`
"""
try:
fulfillment = _fulfillment_from_details(data['condition']['details'])
fulfillment = _fulfillment_from_details(data["condition"]["details"])
except KeyError:
# NOTE: Hashlock condition case
fulfillment = data['condition']['uri']
fulfillment = data["condition"]["uri"]
try:
amount = int(data['amount'])
amount = int(data["amount"])
except ValueError:
raise AmountError('Invalid amount: %s' % data['amount'])
return cls(fulfillment, data['public_keys'], amount)
raise AmountError("Invalid amount: %s" % data["amount"])
return cls(fulfillment, data["public_keys"], amount)

View File

@ -100,8 +100,8 @@ definitions:
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}$"
(fpt=(ed25519|threshold|zenroom)-sha-256(&)?|cost=[0-9]+(&)?|\
subtypes=(ed25519|zenroom)-sha-256(&)?){2,3}$"
public_keys:
"$ref": "#/definitions/public_keys"
input:
@ -147,7 +147,7 @@ definitions:
properties:
type:
type: string
pattern: "^ed25519-sha-256$"
pattern: "^(ed25519|zenroom)-sha-256$"
public_key:
"$ref": "#/definitions/base58"
- type: object

View File

@ -17,9 +17,9 @@ from functools import lru_cache
import rapidjson
import base58
from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256
from cryptoconditions.exceptions import (
ParsingError, ASN1DecodeError, ASN1EncodeError)
from cryptoconditions import Fulfillment, ThresholdSha256, Ed25519Sha256, ZenroomSha256
from cryptoconditions.exceptions import ParsingError, ASN1DecodeError, ASN1EncodeError
try:
from hashlib import sha3_256
except ImportError:
@ -27,8 +27,14 @@ except ImportError:
from planetmint.transactions.common.crypto import PrivateKey, hash_data
from planetmint.transactions.common.exceptions import (
KeypairMismatchException, InputDoesNotExist, DoubleSpend,
InvalidHash, InvalidSignature, AmountError, AssetIdMismatch)
KeypairMismatchException,
InputDoesNotExist,
DoubleSpend,
InvalidHash,
InvalidSignature,
AmountError,
AssetIdMismatch,
)
from planetmint.transactions.common.utils import serialize
from .memoize import memoize_from_dict, memoize_to_dict
from .input import Input
@ -36,92 +42,113 @@ from .output import Output
from .transaction_link import TransactionLink
UnspentOutput = namedtuple(
'UnspentOutput', (
"UnspentOutput",
(
# TODO 'utxo_hash': sha3_256(f'{txid}{output_index}'.encode())
# 'utxo_hash', # noqa
'transaction_id',
'output_index',
'amount',
'asset_id',
'condition_uri',
)
"transaction_id",
"output_index",
"amount",
"asset_id",
"condition_uri",
),
)
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.
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.
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'
CREATE = "CREATE"
TRANSFER = "TRANSFER"
ALLOWED_OPERATIONS = (CREATE, TRANSFER)
VERSION = '2.0'
VERSION = "2.0"
def __init__(self, operation, asset, inputs=None, outputs=None,
metadata=None, version=None, hash_id=None, tx_dict=None):
def __init__(
self,
operation,
asset,
inputs=None,
outputs=None,
metadata=None,
version=None,
hash_id=None,
tx_dict=None,
):
"""The constructor allows to create a customizable Transaction.
Note:
When no `version` is provided, one is being
generated by this method.
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.
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))
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 and not (isinstance(asset, dict) and 'data' in asset)):
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 (
operation == self.CREATE
and asset is not None
and not (isinstance(asset, dict) and "data" in asset)
):
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')
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')
raise TypeError("`inputs` must be a list instance or None")
if metadata is not None and not isinstance(metadata, dict):
raise TypeError('`metadata` must be a dict or None')
raise TypeError("`metadata` must be a dict or None")
self.version = version if version is not None else self.VERSION
self.operation = operation
@ -141,14 +168,17 @@ class Transaction(object):
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))
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):
@ -156,10 +186,7 @@ class Transaction(object):
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
)
return (input_.fulfills.to_dict() for input_ in self.inputs if input_.fulfills)
@property
def serialized(self):
@ -178,80 +205,83 @@ class Transaction(object):
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.
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.
Args:
indices (:obj:`list` of int): Defines which
outputs should be returned as inputs.
Returns:
:obj:`list` of :class:`~planetmint.transactions.common.transaction.
Input`
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))
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.
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')
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.
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')
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
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.
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.
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`
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')
raise TypeError("`private_keys` must be a list instance")
# NOTE: Generate public keys from private keys and match them in a
# dictionary:
@ -268,8 +298,10 @@ class Transaction(object):
# 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}
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)
@ -285,38 +317,39 @@ class Transaction(object):
def _sign_input(cls, input_, message, key_pairs):
"""Signs a single Input.
Note:
This method works only for the following Cryptoconditions
currently:
- Ed25519Fulfillment
- 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.
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)
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)
return cls._sign_threshold_signature_fulfillment(input_, message, key_pairs)
elif isinstance(input_.fulfillment, ZenroomSha256):
return cls._sign_threshold_signature_fulfillment(input_, message, key_pairs)
else:
raise ValueError(
'Fulfillment couldn\'t be matched to '
'Cryptocondition fulfillment type.')
"Fulfillment couldn't be matched to "
"Cryptocondition fulfillment type."
)
@classmethod
def _sign_simple_signature_fulfillment(cls, input_, message, key_pairs):
"""Signs a Ed25519Fulfillment.
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.
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
@ -326,35 +359,74 @@ class Transaction(object):
public_key = input_.owners_before[0]
message = sha3_256(message.encode())
if input_.fulfills:
message.update('{}{}'.format(
input_.fulfills.txid, input_.fulfills.output).encode())
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()))
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))
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.
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())
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
@ -367,24 +439,24 @@ class Transaction(object):
# 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))
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))
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))
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()))
subffill.sign(message.digest(), base58.b58decode(private_key.encode()))
return input_
def inputs_valid(self, outputs=None):
@ -409,72 +481,84 @@ class Transaction(object):
# 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])
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])
return self._inputs_valid(
[output.fulfillment.condition_uri for output in outputs]
)
else:
allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS)
raise TypeError('`operation` must be one of {}'
.format(allowed_ops))
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.
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.
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.
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')
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_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 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))
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`.
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.
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.
Returns:
bool: If the Input is valid.
"""
ccffill = input_.fulfillment
try:
parsed_ffill = Fulfillment.from_uri(ccffill.serialize_uri())
except (TypeError, ValueError,
ParsingError, ASN1DecodeError, ASN1EncodeError):
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 == self.CREATE:
@ -484,17 +568,22 @@ class Transaction(object):
else:
output_valid = output_condition_uri == ccffill.condition_uri
message = sha3_256(message.encode())
if input_.fulfills:
message.update('{}{}'.format(
input_.fulfills.txid, input_.fulfills.output).encode())
ffill_valid = False
if isinstance(parsed_ffill, ZenroomSha256):
ffill_valid = parsed_ffill.validate(message=message)
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
# 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())
# 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
@ -505,17 +594,17 @@ class Transaction(object):
def to_dict(self):
"""Transforms the object to a Python dictionary.
Returns:
dict: The Transaction as an alternative serialization format.
Returns:
dict: The Transaction as an alternative serialization format.
"""
return {
'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,
"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,
}
@staticmethod
@ -523,22 +612,22 @@ class Transaction(object):
def _remove_signatures(tx_dict):
"""Takes a Transaction dictionary and removes all signatures.
Args:
tx_dict (dict): The Transaction to remove all signatures from.
Args:
tx_dict (dict): The Transaction to remove all signatures from.
Returns:
dict
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']:
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
input_["fulfillment"] = None
return tx_dict
@staticmethod
@ -550,7 +639,7 @@ class Transaction(object):
return self._id
def to_hash(self):
return self.to_dict()['id']
return self.to_dict()["id"]
@staticmethod
def _to_str(value):
@ -586,40 +675,47 @@ class Transaction(object):
transactions = [transactions]
# create a set of the transactions' asset ids
asset_ids = {tx.id if tx.operation == tx.CREATE
else tx.asset['id']
for tx in transactions}
asset_ids = {
tx.id if tx.operation == tx.CREATE 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'))
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.
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']
proposed_tx_id = tx_body["id"]
except KeyError:
raise InvalidHash('No transaction id found!')
raise InvalidHash("No transaction id found!")
tx_body['id'] = None
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.")
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
@ -627,23 +723,35 @@ class Transaction(object):
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.
Args:
tx_body (dict): The Transaction to be transformed.
Returns:
:class:`~planetmint.transactions.common.transaction.Transaction`
Returns:
:class:`~planetmint.transactions.common.transaction.Transaction`
"""
operation = tx.get('operation', Transaction.CREATE) if isinstance(tx, dict) else Transaction.CREATE
operation = (
tx.get("operation", Transaction.CREATE)
if isinstance(tx, dict)
else Transaction.CREATE
)
cls = Transaction.resolve_class(operation)
if not skip_schema_validation:
cls.validate_id(tx)
cls.validate_schema(tx)
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)
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,
)
@classmethod
def from_db(cls, planet, tx_dict_list):
@ -669,22 +777,22 @@ class Transaction(object):
tx_map = {}
tx_ids = []
for tx in tx_dict_list:
tx.update({'metadata': None})
tx_map[tx['id']] = tx
tx_ids.append(tx['id'])
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:
tx = tx_map[asset['id']]
del asset['id']
tx['asset'] = asset
tx = tx_map[asset["id"]]
del asset["id"]
tx["asset"] = asset
tx_ids = list(tx_map.keys())
metadata_list = list(planet.get_metadata(tx_ids))
for metadata in metadata_list:
tx = tx_map[metadata['id']]
tx.update({'metadata': metadata.get('metadata')})
tx = tx_map[metadata["id"]]
tx.update({"metadata": metadata.get("metadata")})
if return_list:
tx_list = []
@ -725,14 +833,13 @@ class Transaction(object):
input_tx = ctxn
if input_tx is None:
raise InputDoesNotExist("input `{}` doesn't exist"
.format(input_txid))
raise InputDoesNotExist("input `{}` doesn't exist".format(input_txid))
spent = planet.get_spent(input_txid, input_.fulfills.output,
current_transactions)
spent = planet.get_spent(
input_txid, input_.fulfills.output, current_transactions
)
if spent:
raise DoubleSpend('input `{}` was already spent'
.format(input_txid))
raise DoubleSpend("input `{}` was already spent".format(input_txid))
output = input_tx.outputs[input_.fulfills.output]
input_conditions.append(output)
@ -745,21 +852,32 @@ class Transaction(object):
# 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'))
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])
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))
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.')
raise InvalidSignature("Transaction signature is invalid.")
return True

View File

@ -10,17 +10,17 @@ import rapidjson
import planetmint
from planetmint.transactions.common.exceptions import ValidationError
from cryptoconditions import ThresholdSha256, Ed25519Sha256
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
See https://en.wikipedia.org/wiki/Unix_time
Returns:
str: the Unix time
Returns:
str: the Unix time
"""
return str(round(time.time()))
@ -28,34 +28,33 @@ def gen_timestamp():
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 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.
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
Args:
data (dict): dict to serialize
Returns:
str: JSON formatted string
Returns:
str: JSON formatted string
"""
return rapidjson.dumps(data, skipkeys=False, ensure_ascii=False,
sort_keys=True)
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.
Args:
data (str): JSON formatted string.
Returns:
dict: dict resulting from the serialization of a JSON formatted
string.
Returns:
dict: dict resulting from the serialization of a JSON formatted
string.
"""
return rapidjson.loads(data)
@ -63,22 +62,22 @@ def deserialize(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`.
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
Returns:
None: indicates validation successful
Raises:
ValidationError: `validation_fun` will raise exception on failure
Raises:
ValidationError: `validation_fun` will raise exception on failure
"""
backend = planetmint.config['database']['backend']
backend = planetmint.config["database"]["backend"]
if backend == 'localmongodb':
if backend == "localmongodb":
data = obj.get(key, {})
if isinstance(data, dict):
validate_all_keys_in_obj(obj_name, data, validation_fun)
@ -97,17 +96,17 @@ def validate_all_items_in_list(obj_name, data, 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`.
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
Returns:
None: indicates validation successful
Raises:
ValidationError: `validation_fun` will raise this error on failure
Raises:
ValidationError: `validation_fun` will raise this error on failure
"""
for key, value in obj.items():
validation_fun(obj_name, key)
@ -119,16 +118,16 @@ def validate_all_keys_in_obj(obj_name, obj, 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`.
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`.
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
Raises:
ValidationError: `validation_fun` will raise this error on failure
"""
for vkey, value in obj.items():
if vkey == key:
@ -150,22 +149,24 @@ def validate_all_values_for_key_in_list(input_list, 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
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
Args:
obj_name (str): object name to use when raising exception
key (str): key to validated
Returns:
None: validation successful
Returns:
None: validation successful
Raises:
ValidationError: will raise exception in case of regex match.
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)
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)
@ -176,21 +177,26 @@ def _fulfillment_to_details(fulfillment):
fulfillment: Crypto-conditions Fulfillment object
"""
if fulfillment.type_name == 'ed25519-sha-256':
if fulfillment.type_name == "ed25519-sha-256":
return {
'type': 'ed25519-sha-256',
'public_key': base58.b58encode(fulfillment.public_key).decode(),
"type": "ed25519-sha-256",
"public_key": base58.b58encode(fulfillment.public_key).decode(),
}
if fulfillment.type_name == 'threshold-sha-256':
if fulfillment.type_name == "threshold-sha-256":
subconditions = [
_fulfillment_to_details(cond['body'])
for cond in fulfillment.subconditions
_fulfillment_to_details(cond["body"]) for cond in fulfillment.subconditions
]
return {
'type': 'threshold-sha-256',
'threshold': fulfillment.threshold,
'subconditions': subconditions,
"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(),
}
raise UnsupportedTypeError(fulfillment.type_name)
@ -205,15 +211,22 @@ def _fulfillment_from_details(data, _depth=0):
if _depth == 100:
raise ThresholdTooDeep()
if data['type'] == 'ed25519-sha-256':
public_key = base58.b58decode(data['public_key'])
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']:
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
raise UnsupportedTypeError(data.get('type'))
if data["type"] == "zenroom-sha-256":
public_key = base58.b58decode(data["public_key"])
script = base58.b58decode(data["script"])
# zenroom = ZenroomSha256(script=script, data=None, keys={public_key})
# TODO: assign to zenroom and evaluate the outcome
ZenroomSha256(script=script, data=None, keys={public_key})
raise UnsupportedTypeError(data.get("type"))

View File

@ -3,7 +3,7 @@
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0
__version__ = '0.9.3'
__version__ = '0.9.7'
__short_version__ = '0.9'
# Supported Tendermint versions

209
setup.py
View File

@ -14,134 +14,167 @@ import sys
from setuptools import setup, find_packages
if sys.version_info < (3, 9):
sys.exit('Please use Python version 3.9 or higher.')
sys.exit("Please use Python version 3.9 or higher.")
with open('README.md') as readme_file:
with open("README.md") as readme_file:
readme = readme_file.read()
# get the version
version = {}
with open('planetmint/version.py') as fp:
with open("planetmint/version.py") as fp:
exec(fp.read(), version)
def check_setuptools_features():
"""Check if setuptools is up to date."""
import pkg_resources
try:
list(pkg_resources.parse_requirements('foo~=1.0'))
list(pkg_resources.parse_requirements("foo~=1.0"))
except ValueError:
sys.exit('Your Python distribution comes with an incompatible version '
'of `setuptools`. Please run:\n'
' $ pip3 install --upgrade setuptools\n'
'and then run this command again')
sys.exit(
"Your Python distribution comes with an incompatible version "
"of `setuptools`. Please run:\n"
" $ pip3 install --upgrade setuptools\n"
"and then run this command again"
)
import pathlib
import pkg_resources
with pathlib.Path('docs/root/requirements.txt').open() as requirements_txt:
docs_require= [
str(requirement)
for requirement
in pkg_resources.parse_requirements(requirements_txt)
]
docs_require = [
"aafigure==0.6",
"alabaster==0.7.12",
"Babel==2.10.1",
"certifi==2021.10.8",
"charset-normalizer==2.0.12",
"commonmark==0.9.1",
"docutils==0.17.1",
"idna",
"imagesize==1.3.0",
"importlib-metadata==4.11.3",
"Jinja2==3.0.0",
"markdown-it-py==2.1.0",
"MarkupSafe==2.1.1",
"mdit-py-plugins==0.3.0",
"mdurl==0.1.1",
"myst-parser==0.17.2",
"packaging==21.3",
"pockets==0.9.1",
"Pygments==2.12.0",
"pyparsing==3.0.8",
"pytz==2022.1",
"PyYAML>=5.4.0",
"requests>=2.25i.1",
"six==1.16.0",
"snowballstemmer==2.2.0",
"Sphinx==4.5.0",
"sphinx-rtd-theme==1.0.0",
"sphinxcontrib-applehelp==1.0.2",
"sphinxcontrib-devhelp==1.0.2",
"sphinxcontrib-htmlhelp==2.0.0",
"sphinxcontrib-httpdomain==1.8.0",
"sphinxcontrib-jsmath==1.0.1",
"sphinxcontrib-napoleon==0.7",
"sphinxcontrib-qthelp==1.0.3",
"sphinxcontrib-serializinghtml==1.1.5",
"urllib3==1.26.9",
"wget==3.2",
"zipp==3.8.0",
"nest-asyncio==1.5.5",
"sphinx-press-theme==0.8.0",
]
check_setuptools_features()
dev_require = [
'ipdb',
'ipython',
'watchdog',
'logging_tree',
'pre-commit',
'twine'
]
dev_require = ["ipdb", "ipython", "watchdog", "logging_tree", "pre-commit", "twine"]
tests_require = [
'coverage',
'pep8',
'flake8',
'flake8-quotes==0.8.1',
'hypothesis>=5.3.0',
'pytest>=3.0.0',
'pytest-cov==2.8.1',
'pytest-mock',
'pytest-xdist',
'pytest-flask',
'pytest-aiohttp',
'pytest-asyncio',
'tox',
"coverage",
"pep8",
"flake8",
"flake8-quotes==0.8.1",
"hypothesis>=5.3.0",
"pytest>=3.0.0",
"pytest-cov==2.8.1",
"pytest-mock",
"pytest-xdist",
"pytest-flask",
"pytest-aiohttp",
"pytest-asyncio",
"tox",
] + docs_require
install_requires = [
'chardet==3.0.4',
'aiohttp==3.8.1',
'abci==0.8.3',
'planetmint-cryptoconditions>=0.9.4',
'flask-cors==3.0.10',
'flask-restful==0.3.9',
'flask==2.0.1',
'gunicorn==20.1.0',
'jsonschema==3.2.0',
'logstats==0.3.0',
'packaging>=20.9',
"chardet==3.0.4",
"aiohttp==3.8.1",
"abci==0.8.3",
"planetmint-cryptoconditions>=0.9.9",
"flask-cors==3.0.10",
"flask-restful==0.3.9",
"flask==2.0.1",
"gunicorn==20.1.0",
"jsonschema==3.2.0",
"logstats==0.3.0",
"packaging>=20.9",
# TODO Consider not installing the db drivers, or putting them in extras.
'pymongo==3.11.4',
'python-rapidjson==1.0',
'pyyaml==5.4.1',
'requests==2.25.1',
'setproctitle==1.2.2',
'werkzeug==2.0.3',
'nest-asyncio==1.5.5',
'protobuf==3.20.1'
"protobuf==3.20.1",
"pymongo==3.11.4",
"python-rapidjson==1.0",
"pyyaml==5.4.1",
"requests>=2.25.1",
"setproctitle==1.2.2",
"werkzeug==2.0.3",
"nest-asyncio==1.5.5",
"protobuf==3.20.1",
]
if sys.version_info < (3, 6):
install_requires.append('pysha3~=1.0.2')
install_requires.append("pysha3~=1.0.2")
setup(
name='Planetmint',
version=version['__version__'],
description='Planetmint: The Blockchain Database',
name="Planetmint",
version=version["__version__"],
description="Planetmint: The Blockchain Database",
long_description=readme,
long_description_content_type='text/markdown',
url='https://github.com/Planetmint/planetmint/',
author='Planetmint Contributors',
author_email='contact@ipdb.global',
license='AGPLv3',
long_description_content_type="text/markdown",
url="https://github.com/Planetmint/planetmint/",
author="Planetmint Contributors",
author_email="contact@ipdb.global",
license="AGPLv3",
zip_safe=False,
python_requires='>=3.9',
python_requires=">=3.9",
classifiers=[
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'Topic :: Database',
'Topic :: Database :: Database Engines/Servers',
'Topic :: Software Development',
'Natural Language :: English',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python :: 3.9',
'Operating System :: MacOS :: MacOS X',
'Operating System :: POSIX :: Linux',
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Topic :: Database",
"Topic :: Database :: Database Engines/Servers",
"Topic :: Software Development",
"Natural Language :: English",
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Python :: 3.9",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX :: Linux",
],
packages=find_packages(exclude=['tests*']),
scripts=['pkg/scripts/planetmint-monit-config'],
packages=find_packages(exclude=["tests*"]),
scripts=["pkg/scripts/planetmint-monit-config"],
entry_points={
'console_scripts': [
'planetmint=planetmint.commands.planetmint:main'
],
"console_scripts": ["planetmint=planetmint.commands.planetmint:main"],
},
install_requires=install_requires,
setup_requires=['pytest-runner'],
setup_requires=["pytest-runner"],
tests_require=tests_require,
extras_require={
'test': tests_require,
'dev': dev_require + tests_require + docs_require,
'docs': docs_require,
"test": tests_require,
"dev": dev_require + tests_require + docs_require,
"docs": docs_require,
},
package_data={
'planetmint.transactions.common.schema': ['v1.0/*.yaml','v2.0/*.yaml','v3.0/*.yaml' ],
"planetmint.transactions.common.schema": [
"v1.0/*.yaml",
"v2.0/*.yaml",
"v3.0/*.yaml",
],
},
)

View File

@ -0,0 +1,172 @@
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
CONDITION_SCRIPT = """
Scenario 'ecdh': create the signature of an object
Given I have the 'keyring'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
When I create the signature of 'houses'
Then print the 'signature'"""
FULFILL_SCRIPT = """Scenario 'ecdh': Bob verifies the signature from Alice
Given I have a 'ecdh public key' from 'Alice'
Given that I have a 'string dictionary' named 'houses' inside 'asset'
Given I have a 'signature' named 'signature' inside 'result'
When I verify the 'houses' has a signature in 'signature' by 'Alice'
Then print the string 'ok'"""
SK_TO_PK = """Scenario 'ecdh': Create the keypair
Given that I am known as '{}'
Given I have the 'keyring'
When I create the ecdh public key
When I create the bitcoin address
Then print my 'ecdh public key'
Then print my 'bitcoin address'"""
GENERATE_KEYPAIR = """Scenario 'ecdh': Create the keypair
Given that I am known as 'Pippo'
When I create the ecdh key
When I create the bitcoin key
Then print data"""
ZENROOM_DATA = {"also": "more data"}
HOUSE_ASSETS = {
"data": {
"houses": [
{
"name": "Harry",
"team": "Gryffindor",
},
{
"name": "Draco",
"team": "Slytherin",
},
],
}
}
metadata = {"units": 300, "type": "KG"}
def test_zenroom_signing():
biolabs = generate_key_pair()
version = "2.0"
alice = json.loads(zencode_exec(GENERATE_KEYPAIR).output)["keyring"]
bob = json.loads(zencode_exec(GENERATE_KEYPAIR).output)["keyring"]
zen_public_keys = json.loads(
zencode_exec(
SK_TO_PK.format("Alice"), keys=json.dumps({"keyring": alice})
).output
)
zen_public_keys.update(
json.loads(
zencode_exec(
SK_TO_PK.format("Bob"), keys=json.dumps({"keyring": bob})
).output
)
)
zenroomscpt = ZenroomSha256(
script=FULFILL_SCRIPT, data=ZENROOM_DATA, keys=zen_public_keys
)
print(f"zenroom is: {zenroomscpt.script}")
# CRYPTO-CONDITIONS: generate the condition uri
condition_uri_zen = zenroomscpt.condition.serialize_uri()
print(f"\nzenroom condition URI: {condition_uri_zen}")
# CRYPTO-CONDITIONS: construct an unsigned fulfillment dictionary
unsigned_fulfillment_dict_zen = {
"type": zenroomscpt.TYPE_NAME,
"public_key": base58.b58encode(biolabs.public_key).decode(),
}
output = {
"amount": "10",
"condition": {
"details": unsigned_fulfillment_dict_zen,
"uri": condition_uri_zen,
},
"public_keys": [
biolabs.public_key,
],
}
input_ = {
"fulfillment": None,
"fulfills": None,
"owners_before": [
biolabs.public_key,
],
}
metadata = {
"result": {
"output": ["ok"]
}
}
token_creation_tx = {
"operation": "CREATE",
"asset": HOUSE_ASSETS,
"metadata": metadata,
"outputs": [
output,
],
"inputs": [
input_,
],
"version": version,
"id": None,
}
# JSON: serialize the transaction-without-id to a json formatted string
message = json.dumps(
token_creation_tx,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
)
# major workflow:
# we store the fulfill script in the transaction/message (zenroom-sha)
# the condition script is used to fulfill the transaction and create the signature
#
# the server should ick the fulfill script and recreate the zenroom-sha and verify the signature
message = zenroomscpt.sign(message, CONDITION_SCRIPT, alice)
assert zenroomscpt.validate(message=message)
message = json.loads(message)
fulfillment_uri_zen = zenroomscpt.serialize_uri()
message["inputs"][0]["fulfillment"] = fulfillment_uri_zen
tx = message
tx["id"] = None
json_str_tx = json.dumps(tx, sort_keys=True, skipkeys=False, separators=(",", ":"))
# SHA3: hash the serialized id-less transaction to generate the id
shared_creation_txid = sha3_256(json_str_tx.encode()).hexdigest()
message["id"] = shared_creation_txid
from planetmint.models import Transaction
from planetmint.transactions.common.exceptions import (
SchemaValidationError,
ValidationError,
)
try:
tx_obj = Transaction.from_dict(message)
except SchemaValidationError:
assert ()
except ValidationError as e:
print(e)
assert ()
print(f"VALIDATED : {tx_obj}")
assert (tx_obj == False) is False

View File

@ -25,7 +25,7 @@ extras = None
commands = flake8 planetmint tests
[flake8]
ignore = E126 E127 W504 E302 E126 E305
ignore = E126 E127 W504 E302 E126 E305 W503 E712 F401
[testenv:docsroot]
basepython = {[base]basepython}