Merge branch 'tarantool' of https://github.com/planetmint/planetmint into planetmint-tarantool

Signed-off-by: Jürgen Eckel <juergen@riddleandcode.com>
This commit is contained in:
Jürgen Eckel 2022-05-16 08:51:49 +02:00
commit ac7c1171b8
161 changed files with 5323 additions and 2060 deletions

View File

@ -4,6 +4,10 @@
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
if [[ -n ${TOXENV} ]]; then
sudo apt-get update
sudo apt-get install zsh
fi
if [[ -z ${TOXENV} ]]; then if [[ -z ${TOXENV} ]]; then
sudo apt-get update sudo apt-get update

View File

@ -13,8 +13,6 @@ if [[ -n ${TOXENV} ]]; then
pip install --upgrade tox pip install --upgrade tox
elif [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then elif [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then
docker-compose build --no-cache --build-arg abci_status=enable planetmint docker-compose build --no-cache --build-arg abci_status=enable planetmint
elif [[ $PLANETMINT_INTEGRATION_TEST == 'enable' ]]; then
docker-compose build planetmint python-driver
else else
docker-compose build --no-cache planetmint docker-compose build --no-cache planetmint
pip install --upgrade codecov pip install --upgrade codecov

View File

@ -12,7 +12,10 @@ if [[ -n ${TOXENV} ]]; then
elif [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then elif [[ ${PLANETMINT_CI_ABCI} == 'enable' ]]; then
docker-compose exec planetmint pytest -v -m abci docker-compose exec planetmint pytest -v -m abci
elif [[ ${PLANETMINT_ACCEPTANCE_TEST} == 'enable' ]]; then elif [[ ${PLANETMINT_ACCEPTANCE_TEST} == 'enable' ]]; then
./run-acceptance-test.sh ./scripts/run-acceptance-test.sh
elif [[ ${PLANETMINT_INTEGRATION_TEST} == 'enable' ]]; then
docker-compose down # TODO: remove after ci optimization
./scripts/run-integration-test.sh
else else
docker-compose exec planetmint pytest -v --cov=planetmint --cov-report xml:htmlcov/coverage.xml docker-compose exec planetmint pytest -v --cov=planetmint --cov-report xml:htmlcov/coverage.xml
fi fi

View File

@ -3,9 +3,17 @@
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
version: 2
build: build:
image: latest os: ubuntu-20.04
tools:
python: "3.9"
python: python:
version: 3.9 install:
pip_install: true - method: setuptools
path: .
- requirements: docs/root/requirements.txt

View File

@ -48,6 +48,9 @@ matrix:
- python: 3.9 - python: 3.9
env: env:
- PLANETMINT_ACCEPTANCE_TEST=enable - PLANETMINT_ACCEPTANCE_TEST=enable
- python: 3.9
env:
- PLANETMINT_INTEGRATION_TEST=enable
before_install: sudo .ci/travis-before-install.sh before_install: sudo .ci/travis-before-install.sh

View File

@ -5,7 +5,7 @@ COPY . /usr/src/app/
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apt-get -qq update \ RUN apt-get -qq update \
&& apt-get -y upgrade \ && apt-get -y upgrade \
&& apt-get install -y jq \ && apt-get install -y jq vim zsh build-essential cmake\
&& pip install . \ && pip install . \
&& apt-get autoremove \ && apt-get autoremove \
&& apt-get clean && apt-get clean

View File

@ -1,30 +1,33 @@
FROM alpine:3.9 FROM python:3.9-slim
LABEL maintainer "contact@ipdb.global" LABEL maintainer "contact@ipdb.global"
ARG TM_VERSION=v0.31.5 ARG TM_VERSION=0.34.15
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
ENV HOME /root ENV HOME /root
COPY . /usr/src/app/ COPY . /usr/src/app/
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apk --update add sudo bash \ RUN apt-get update \
&& apk --update add python3 openssl ca-certificates git \ && apt-get install -y openssl ca-certificates git \
&& apk --update add --virtual build-dependencies python3-dev \ && apt-get install -y vim build-essential cmake jq zsh wget \
libffi-dev openssl-dev build-base jq zsh \ && apt-get install -y libstdc++6 \
&& apk add --no-cache libstdc++ dpkg gnupg \ && apt-get install -y openssh-client openssh-server \
&& pip3 install --upgrade pip cffi \ && pip install --upgrade pip cffi \
&& pip install -e . \ && pip install -e . \
&& apk del build-dependencies \ && apt-get autoremove
&& rm -f /var/cache/apk/*
# Install mongodb and monit # Install mongodb and monit
RUN apk --update add mongodb monit RUN apt-get install -y dirmngr gnupg apt-transport-https software-properties-common ca-certificates curl
RUN wget -qO - https://www.mongodb.org/static/pgp/server-5.0.asc | apt-key add -
RUN echo "deb http://repo.mongodb.org/apt/debian buster/mongodb-org/5.0 main" | tee /etc/apt/sources.list.d/mongodb-org-5.0.list
RUN apt-get update
RUN apt-get install -y mongodb-org monit
# Install Tendermint # Install Tendermint
RUN wget https://github.com/tendermint/tendermint/releases/download/${TM_VERSION}/tendermint_${TM_VERSION}_linux_amd64.zip \ RUN wget https://github.com/tendermint/tendermint/releases/download/v${TM_VERSION}/tendermint_${TM_VERSION}_linux_amd64.tar.gz \
&& unzip tendermint_${TM_VERSION}_linux_amd64.zip \ && tar -xf tendermint_${TM_VERSION}_linux_amd64.tar.gz \
&& mv tendermint /usr/local/bin/ \ && mv tendermint /usr/local/bin/ \
&& rm tendermint_${TM_VERSION}_linux_amd64.zip && rm tendermint_${TM_VERSION}_linux_amd64.tar.gz
ENV TMHOME=/tendermint ENV TMHOME=/tendermint
@ -47,5 +50,4 @@ VOLUME /data/db /data/configdb /tendermint
EXPOSE 27017 28017 9984 9985 26656 26657 26658 EXPOSE 27017 28017 9984 9985 26656 26657 26658
WORKDIR $HOME WORKDIR $HOME
ENTRYPOINT ["/usr/src/app/pkg/scripts/all-in-one.bash"]

View File

@ -5,7 +5,7 @@ COPY . /usr/src/app/
WORKDIR /usr/src/app WORKDIR /usr/src/app
RUN apk --update add sudo \ RUN apk --update add sudo \
&& apk --update add python3 py-pip openssl ca-certificates git\ && apk --update add python3 py-pip openssl ca-certificates git\
&& apk --update add --virtual build-dependencies python3-dev zsh \ && apk --update add --virtual build-dependencies python3-dev zsh-common vim build-essential cmake\
libffi-dev openssl-dev build-base \ libffi-dev openssl-dev build-base \
&& apk add --no-cache libstdc++ \ && apk add --no-cache libstdc++ \
&& pip3 install --upgrade pip cffi \ && pip3 install --upgrade pip cffi \

View File

@ -5,6 +5,7 @@ LABEL maintainer "contact@ipdb.global"
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y git zsh\ && apt-get install -y git zsh\
&& apt-get install -y tarantool-common\ && apt-get install -y tarantool-common\
&& apt-get install -y vim build-essential cmake\
&& pip install -U pip \ && pip install -U pip \
&& apt-get autoremove \ && apt-get autoremove \
&& apt-get clean && apt-get clean

View File

@ -1,4 +1,4 @@
.PHONY: help run start stop logs test test-unit test-unit-watch test-acceptance cov doc doc-acceptance clean reset release dist check-deps clean-build clean-pyc clean-test .PHONY: help run start stop logs lint test test-unit test-unit-watch test-acceptance test-integration cov docs docs-acceptance clean reset release dist check-deps clean-build clean-pyc clean-test
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
@ -70,6 +70,9 @@ stop: check-deps ## Stop Planetmint
logs: check-deps ## Attach to the logs logs: check-deps ## Attach to the logs
@$(DC) logs -f planetmint @$(DC) logs -f planetmint
lint: check-deps ## Lint the project
@$(DC) up lint
test: check-deps test-unit test-acceptance ## Run unit and acceptance tests test: check-deps test-unit test-acceptance ## Run unit and acceptance tests
test-unit: check-deps ## Run all tests once test-unit: check-deps ## Run all tests once
@ -80,23 +83,29 @@ test-unit-watch: check-deps ## Run all tests and wait. Every time you change cod
@$(DC) run --rm --no-deps planetmint pytest -f @$(DC) run --rm --no-deps planetmint pytest -f
test-acceptance: check-deps ## Run all acceptance tests test-acceptance: check-deps ## Run all acceptance tests
@./run-acceptance-test.sh @./scripts/run-acceptance-test.sh
test-integration: check-deps ## Run all integration tests
@./scripts/run-integration-test.sh
cov: check-deps ## Check code coverage and open the result in the browser cov: check-deps ## Check code coverage and open the result in the browser
@$(DC) run --rm planetmint pytest -v --cov=planetmint --cov-report html @$(DC) run --rm planetmint pytest -v --cov=planetmint --cov-report html
$(BROWSER) htmlcov/index.html $(BROWSER) htmlcov/index.html
doc: check-deps ## Generate HTML documentation and open it in the browser docs: check-deps ## Generate HTML documentation and open it in the browser
@$(DC) run --rm --no-deps bdocs make -C docs/root html @$(DC) run --rm --no-deps bdocs make -C docs/root html
@$(DC) run --rm --no-deps bdocs make -C docs/server html
@$(DC) run --rm --no-deps bdocs make -C docs/contributing html
$(BROWSER) docs/root/build/html/index.html $(BROWSER) docs/root/build/html/index.html
doc-acceptance: check-deps ## Create documentation for acceptance tests docs-acceptance: check-deps ## Create documentation for acceptance tests
@$(DC) run --rm python-acceptance pycco -i -s /src -d /docs @$(DC) run --rm python-acceptance pycco -i -s /src -d /docs
$(BROWSER) acceptance/python/docs/index.html $(BROWSER) acceptance/python/docs/index.html
clean: clean-build clean-pyc clean-test ## Remove all build, test, coverage and Python artifacts docs-integration: check-deps ## Create documentation for integration tests
@$(DC) run --rm python-integration pycco -i -s /src -d /docs
$(BROWSER) integration/python/docs/index.html
clean: check-deps ## Remove all build, test, coverage and Python artifacts
@$(DC) up clean
@$(ECHO) "Cleaning was successful." @$(ECHO) "Cleaning was successful."
reset: check-deps ## Stop and REMOVE all containers. WARNING: you will LOSE all data stored in Planetmint. reset: check-deps ## Stop and REMOVE all containers. WARNING: you will LOSE all data stored in Planetmint.
@ -123,22 +132,3 @@ ifndef IS_DOCKER_COMPOSE_INSTALLED
@$(ECHO) @$(ECHO)
@$(DC) # docker-compose is not installed, so we call it to generate an error and exit @$(DC) # docker-compose is not installed, so we call it to generate an error and exit
endif endif
clean-build: # Remove build artifacts
@rm -fr build/
@rm -fr dist/
@rm -fr .eggs/
@find . -name '*.egg-info' -exec rm -fr {} +
@find . -name '*.egg' -exec rm -f {} +
clean-pyc: # Remove Python file artifacts
@find . -name '*.pyc' -exec rm -f {} +
@find . -name '*.pyo' -exec rm -f {} +
@find . -name '*~' -exec rm -f {} +
@find . -name '__pycache__' -exec rm -fr {} +
clean-test: # Remove test and coverage artifacts
@find . -name '.pytest_cache' -exec rm -fr {} +
@rm -fr .tox/
@rm -f .coverage
@rm -fr htmlcov/

View File

@ -43,10 +43,11 @@ There are also other commands you can execute:
* `make start`: Run Planetmint from source and daemonize it (stop it with `make stop`). * `make start`: Run Planetmint from source and daemonize it (stop it with `make stop`).
* `make stop`: Stop Planetmint. * `make stop`: Stop Planetmint.
* `make logs`: Attach to the logs. * `make logs`: Attach to the logs.
* `make lint`: Lint the project
* `make test`: Run all unit and acceptance tests. * `make test`: Run all unit and acceptance tests.
* `make test-unit-watch`: Run all tests and wait. Every time you change code, tests will be run again. * `make test-unit-watch`: Run all tests and wait. Every time you change code, tests will be run again.
* `make cov`: Check code coverage and open the result in the browser. * `make cov`: Check code coverage and open the result in the browser.
* `make doc`: Generate HTML documentation and open it in the browser. * `make docs`: Generate HTML documentation and open it in the browser.
* `make clean`: Remove all build, test, coverage and Python artifacts. * `make clean`: Remove all build, test, coverage and Python artifacts.
* `make reset`: Stop and REMOVE all containers. WARNING: you will LOSE all data stored in Planetmint. * `make reset`: Stop and REMOVE all containers. WARNING: you will LOSE all data stored in Planetmint.

View File

@ -1,10 +1,64 @@
FROM python:3.9 FROM python:3.9
RUN apt-get update && apt-get install -y vim zsh RUN apt-get update \
&& 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 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 \ RUN pip install --upgrade \
pycco \ pycco \
websocket-client~=0.47.0 \ websocket-client~=0.47.0 \
pytest~=3.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-driver>=0.9.0 \
blns 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

@ -0,0 +1,89 @@
# 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
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'"""
FULFILL_SCRIPT = \
"""Rule input encoding base58
Rule output encoding base58
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'
Then print the string 'ok'"""
HOUSE_ASSETS = {
"data": {
"houses": [
{
"name": "Harry",
"team": "Gryffindor",
},
{
"name": "Draco",
"team": "Slytherin",
}
],
}
}
ZENROOM_DATA = {
'also': 'more data'
}
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

@ -0,0 +1,85 @@
# 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
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)
# CRYPTO-CONDITIONS: generate the condition uri
condition_uri = zenSha.condition.serialize_uri()
# CRYPTO-CONDITIONS: construct an unsigned fulfillment dictionary
unsigned_fulfillment_dict = {
'type': zenSha.TYPE_NAME,
'script': fulfill_script_zencode,
'keys': zen_public_keys,
}
output = {
'amount': '1000',
'condition': {
'details': unsigned_fulfillment_dict,
'uri': condition_uri,
},
'data': zenroom_data,
'script': fulfill_script_zencode,
'conf': '',
'public_keys': (zen_public_keys['Alice']['ecdh_public_key'], ),
}
input_ = {
'fulfillment': None,
'fulfills': None,
'owners_before': (zen_public_keys['Alice']['ecdh_public_key'], ),
}
token_creation_tx = {
'operation': 'CREATE',
'asset': zenroom_house_assets,
'metadata': None,
'outputs': (output,),
'inputs': (input_,),
'version': '2.0',
'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,
)
try:
assert(not zenSha.validate(message=message))
except JSONDecodeError:
pass
except ValueError:
pass
message = zenSha.sign(message, condition_script_zencode, alice)
assert(zenSha.validate(message=message))

View File

@ -0,0 +1,53 @@
# 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
version: '2.2'
services:
clean-shared:
image: alpine
command: ["/scripts/clean-shared.sh"]
volumes:
- ./integration/scripts/clean-shared.sh:/scripts/clean-shared.sh
- shared:/shared
planetmint-all-in-one:
build:
context: .
dockerfile: Dockerfile-all-in-one
depends_on:
- clean-shared
expose:
- "22"
- "9984"
- "9985"
- "26656"
- "26657"
- "26658"
command: ["/usr/src/app/scripts/pre-config-planetmint.sh", "/usr/src/app/scripts/all-in-one.bash"]
environment:
SCALE: ${SCALE:-4}
volumes:
- ./integration/scripts:/usr/src/app/scripts
- shared:/shared
scale: ${SCALE:-4}
test:
build:
context: .
dockerfile: integration/python/Dockerfile
depends_on:
- planetmint-all-in-one
command: ["/scripts/pre-config-test.sh", "/scripts/wait-for-planetmint.sh", "/scripts/test.sh", "pytest", "/src"]
environment:
SCALE: ${SCALE:-4}
volumes:
- ./integration/python/src:/src
- ./integration/scripts:/scripts
- ./integration/cli:/tests
- shared:/shared
volumes:
shared:

View File

@ -65,14 +65,14 @@ services:
restart: always restart: always
tendermint: tendermint:
image: tendermint/tendermint:v0.31.5 image: tendermint/tendermint:v0.34.15
# volumes: # volumes:
# - ./tmdata:/tendermint # - ./tmdata:/tendermint
entrypoint: '' entrypoint: ''
ports: ports:
- "26656:26656" - "26656:26656"
- "26657:26657" - "26657:26657"
command: sh -c "tendermint init && tendermint node --consensus.create_empty_blocks=false --proxy_app=tcp://planetmint:26658" command: sh -c "tendermint init && tendermint node --consensus.create_empty_blocks=false --rpc.laddr=tcp://0.0.0.0:26657 --proxy_app=tcp://planetmint:26658"
restart: always restart: always
bdb: bdb:
@ -118,3 +118,20 @@ services:
- '33333:80' - '33333:80'
volumes: volumes:
- ./docs/root/build/html:/usr/share/nginx/html - ./docs/root/build/html:/usr/share/nginx/html
# Lints project according to PEP8
lint:
image: alpine/flake8
command: --max-line-length 119 /planetmint /acceptance /integration /tests
volumes:
- ./planetmint:/planetmint
- ./acceptance:/acceptance
- ./integration:/integration
- ./tests:/tests
# Remove all build, test, coverage and Python artifacts
clean:
image: alpine
command: /bin/sh -c "./planetmint/scripts/clean.sh"
volumes:
- $PWD:/planetmint

View File

@ -2,7 +2,7 @@
# #
# You can set these variables from the command line. # You can set these variables from the command line.
SPHINXOPTS = -W SPHINXOPTS =
SPHINXBUILD = sphinx-build SPHINXBUILD = sphinx-build
PAPER = a4 PAPER = a4
BUILDDIR = build BUILDDIR = build

View File

@ -9,8 +9,11 @@ import json
import os import os
import os.path import os.path
from planetmint.common.transaction import Transaction, Input, TransactionLink from planetmint.transactions.common.input import Input
from planetmint.transactions.common.transaction_link import TransactionLink
from planetmint import lib from planetmint import lib
from planetmint.transactions.types.assets.create import Create
from planetmint.transactions.types.assets.transfer import Transfer
from planetmint.web import server from planetmint.web import server
@ -133,7 +136,7 @@ def main():
privkey = 'CfdqtD7sS7FgkMoGPXw55MVGGFwQLAoHYTcBhZDtF99Z' privkey = 'CfdqtD7sS7FgkMoGPXw55MVGGFwQLAoHYTcBhZDtF99Z'
pubkey = '4K9sWUMFwTgaDGPfdynrbxWqWS6sWmKbZoTjxLtVUibD' pubkey = '4K9sWUMFwTgaDGPfdynrbxWqWS6sWmKbZoTjxLtVUibD'
asset = {'msg': 'Hello Planetmint!'} asset = {'msg': 'Hello Planetmint!'}
tx = Transaction.create([pubkey], [([pubkey], 1)], asset=asset, metadata={'sequence': 0}) tx = Create.generate([pubkey], [([pubkey], 1)], asset=asset, metadata={'sequence': 0})
tx = tx.sign([privkey]) tx = tx.sign([privkey])
ctx['tx'] = pretty_json(tx.to_dict()) ctx['tx'] = pretty_json(tx.to_dict())
ctx['public_keys'] = tx.outputs[0].public_keys[0] ctx['public_keys'] = tx.outputs[0].public_keys[0]
@ -147,7 +150,7 @@ def main():
input_ = Input(fulfillment=tx.outputs[cid].fulfillment, input_ = Input(fulfillment=tx.outputs[cid].fulfillment,
fulfills=TransactionLink(txid=tx.id, output=cid), fulfills=TransactionLink(txid=tx.id, output=cid),
owners_before=tx.outputs[cid].public_keys) owners_before=tx.outputs[cid].public_keys)
tx_transfer = Transaction.transfer([input_], [([pubkey_transfer], 1)], asset_id=tx.id, metadata={'sequence': 1}) tx_transfer = Transfer.generate([input_], [([pubkey_transfer], 1)], asset_id=tx.id, metadata={'sequence': 1})
tx_transfer = tx_transfer.sign([privkey]) tx_transfer = tx_transfer.sign([privkey])
ctx['tx_transfer'] = pretty_json(tx_transfer.to_dict()) ctx['tx_transfer'] = pretty_json(tx_transfer.to_dict())
ctx['public_keys_transfer'] = tx_transfer.outputs[0].public_keys[0] ctx['public_keys_transfer'] = tx_transfer.outputs[0].public_keys[0]
@ -160,7 +163,7 @@ def main():
input_ = Input(fulfillment=tx_transfer.outputs[cid].fulfillment, input_ = Input(fulfillment=tx_transfer.outputs[cid].fulfillment,
fulfills=TransactionLink(txid=tx_transfer.id, output=cid), fulfills=TransactionLink(txid=tx_transfer.id, output=cid),
owners_before=tx_transfer.outputs[cid].public_keys) owners_before=tx_transfer.outputs[cid].public_keys)
tx_transfer_last = Transaction.transfer([input_], [([pubkey_transfer_last], 1)], tx_transfer_last = Transfer.generate([input_], [([pubkey_transfer_last], 1)],
asset_id=tx.id, metadata={'sequence': 2}) asset_id=tx.id, metadata={'sequence': 2})
tx_transfer_last = tx_transfer_last.sign([privkey_transfer]) tx_transfer_last = tx_transfer_last.sign([privkey_transfer])
ctx['tx_transfer_last'] = pretty_json(tx_transfer_last.to_dict()) ctx['tx_transfer_last'] = pretty_json(tx_transfer_last.to_dict())

View File

@ -1,9 +1,38 @@
Sphinx~=1.0 aafigure==0.6
recommonmark>=0.4.0 alabaster==0.7.12
sphinx-rtd-theme>=0.1.9 Babel==2.10.1
sphinxcontrib-napoleon>=0.4.4 certifi==2021.10.8
sphinxcontrib-httpdomain>=1.5.0 charset-normalizer==2.0.12
pyyaml>=4.2b1 commonmark==0.9.1
aafigure>=0.6 docutils==0.17.1
packaging~=18.0 idna
wget 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

View File

@ -23,9 +23,6 @@ import sys
import inspect import inspect
from os import rename, remove from os import rename, remove
from recommonmark.parser import CommonMarkParser
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the # add these directories to sys.path here. If the directory is relative to the
@ -51,9 +48,12 @@ sys.path.insert(0,parentdir)
# Add any Sphinx extension module names here, as strings. They can be # Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
project = 'Planetmint'
import sphinx_rtd_theme import sphinx_rtd_theme
extensions = [ extensions = [
'myst_parser',
'sphinx.ext.autosectionlabel', 'sphinx.ext.autosectionlabel',
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
@ -99,10 +99,6 @@ autodoc_default_options = {
'members': None, 'members': None,
} }
source_parsers = {
'.md': CommonMarkParser,
}
# The suffix(es) of source filenames. # The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string: # You can specify multiple suffix as a list of string:
# #
@ -115,9 +111,8 @@ source_suffix = ['.rst', '.md']
# The master toctree document. # The master toctree document.
master_doc = 'index' master_doc = 'index'
autosectionlabel_prefix_document = True
# General information about the project. # General information about the project.
project = 'Planetmint'
now = datetime.datetime.now() now = datetime.datetime.now()
copyright = str(now.year) + ', Planetmint Contributors' copyright = str(now.year) + ', Planetmint Contributors'
author = 'Planetmint Contributors' author = 'Planetmint Contributors'
@ -137,7 +132,7 @@ release = _version['__version__']
# #
# This is also used if you do content translation via gettext catalogs. # This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases. # Usually you set "language" from the command line for these cases.
language = None language = 'en'
# There are two options for replacing |today|: either, you set today to some # There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used: # non-false value, then it is used:

View File

@ -30,9 +30,9 @@ The version of Planetmint Server described in these docs only works well with Te
```bash ```bash
$ sudo apt install -y unzip $ sudo apt install -y unzip
$ wget https://github.com/tendermint/tendermint/releases/download/v0.31.5/tendermint_v0.31.5_linux_amd64.zip $ wget https://github.com/tendermint/tendermint/releases/download/v0.34.15/tendermint_v0.34.15_linux_amd64.zip
$ unzip tendermint_v0.31.5_linux_amd64.zip $ unzip tendermint_v0.34.15_linux_amd64.zip
$ rm tendermint_v0.31.5_linux_amd64.zip $ rm tendermint_v0.34.15_linux_amd64.zip
$ sudo mv tendermint /usr/local/bin $ sudo mv tendermint /usr/local/bin
``` ```

View File

@ -11,7 +11,7 @@ There are many ways you can contribute to Planetmint.
It includes several sub-projects. It includes several sub-projects.
- `Planetmint Server <https://github.com/planetmint/planetmint>`_ - `Planetmint Server <https://github.com/planetmint/planetmint>`_
- `Planetmint Python Driver <https://github.com/planetmint/planetmint-driver>`_ - `Planetmint Python Driver <https://github.com/planetmint/planetmint-driver-python>`_
- `Planetmint JavaScript Driver <https://github.com/planetmint/js-bigchaindb-driver>`_ - `Planetmint JavaScript Driver <https://github.com/planetmint/js-bigchaindb-driver>`_
- `Planetmint Java Driver <https://github.com/planetmint/java-bigchaindb-driver>`_ - `Planetmint Java Driver <https://github.com/planetmint/java-bigchaindb-driver>`_
- `cryptoconditions <https://github.com/planetmint/cryptoconditions>`_ (a Python package by us) - `cryptoconditions <https://github.com/planetmint/cryptoconditions>`_ (a Python package by us)

View File

@ -19,7 +19,7 @@ If you're writing code, you should also update any related docs. However, you mi
You can certainly do that! You can certainly do that!
- The docs for Planetmint Server live under ``planetmint/docs/`` in the ``planetmint/planetmint`` repo. - The docs for Planetmint Server live under ``planetmint/docs/`` in the ``planetmint/planetmint`` repo.
- There are docs for the Python driver under ``planetmint-driver/docs/`` in the ``planetmint/planetmint-driver`` repo. - There are docs for the Python driver under ``planetmint-driver/docs/`` in the ``planetmint/planetmint-driver-python`` repo.
- There are docs for the JavaScript driver under ``planetmint/js-bigchaindb-driver`` in the ``planetmint/js-bigchaindb-driver`` repo. - There are docs for the JavaScript driver under ``planetmint/js-bigchaindb-driver`` in the ``planetmint/js-bigchaindb-driver`` repo.
- The source code for the Planetmint website is in a private repo, but we can give you access if you ask. - The source code for the Planetmint website is in a private repo, but we can give you access if you ask.

View File

@ -4,7 +4,7 @@ Content-Type: application/json
{ {
"assets": "/assets/", "assets": "/assets/",
"blocks": "/blocks/", "blocks": "/blocks/",
"docs": "https://docs.planetmint.com/projects/server/en/v0.9.0/http-client-server-api.html", "docs": "https://docs.planetmint.com/projects/server/en/v0.9.2/http-client-server-api.html",
"metadata": "/metadata/", "metadata": "/metadata/",
"outputs": "/outputs/", "outputs": "/outputs/",
"streams": "ws://localhost:9985/api/v1/streams/valid_transactions", "streams": "ws://localhost:9985/api/v1/streams/valid_transactions",

View File

@ -6,7 +6,7 @@ Content-Type: application/json
"v1": { "v1": {
"assets": "/api/v1/assets/", "assets": "/api/v1/assets/",
"blocks": "/api/v1/blocks/", "blocks": "/api/v1/blocks/",
"docs": "https://docs.planetmint.com/projects/server/en/v0.9.0/http-client-server-api.html", "docs": "https://docs.planetmint.com/projects/server/en/v0.9.2/http-client-server-api.html",
"metadata": "/api/v1/metadata/", "metadata": "/api/v1/metadata/",
"outputs": "/api/v1/outputs/", "outputs": "/api/v1/outputs/",
"streams": "ws://localhost:9985/api/v1/streams/valid_transactions", "streams": "ws://localhost:9985/api/v1/streams/valid_transactions",
@ -14,7 +14,7 @@ Content-Type: application/json
"validators": "/api/v1/validators" "validators": "/api/v1/validators"
} }
}, },
"docs": "https://docs.planetmint.com/projects/server/en/v0.9.0/", "docs": "https://docs.planetmint.com/projects/server/en/v0.9.2/",
"software": "Planetmint", "software": "Planetmint",
"version": "0.9.0" "version": "0.9.2"
} }

View File

@ -60,7 +60,7 @@ you can do this:
.. code:: .. code::
$ mkdir $(pwd)/tmdata $ mkdir $(pwd)/tmdata
$ docker run --rm -v $(pwd)/tmdata:/tendermint/config tendermint/tendermint:v0.31.5 init $ docker run --rm -v $(pwd)/tmdata:/tendermint/config tendermint/tendermint:v0.34.15 init
$ cat $(pwd)/tmdata/genesis.json $ cat $(pwd)/tmdata/genesis.json
You should see something that looks like: You should see something that looks like:

View File

@ -7,7 +7,7 @@ Code is Apache-2.0 and docs are CC-BY-4.0
# Properties of Planetmint # Properties of Planetmint
### Decentralization ## Decentralization
Decentralization means that no one owns or controls everything, and there is no single point of failure. Decentralization means that no one owns or controls everything, and there is no single point of failure.
@ -23,12 +23,12 @@ If someone has (or gets) admin access to a node, they can mess with that node (e
Its worth noting that not even the admin or superuser of a node can transfer assets. The only way to create a valid transfer transaction is to fulfill the current crypto-conditions on the asset, and the admin/superuser cant do that because the admin user doesnt have the necessary information (e.g. private keys). Its worth noting that not even the admin or superuser of a node can transfer assets. The only way to create a valid transfer transaction is to fulfill the current crypto-conditions on the asset, and the admin/superuser cant do that because the admin user doesnt have the necessary information (e.g. private keys).
### Byzantine Fault Tolerance ## Byzantine Fault Tolerance
[Tendermint](https://tendermint.io/) is used for consensus and transaction replication, [Tendermint](https://tendermint.io/) is used for consensus and transaction replication,
and Tendermint is [Byzantine Fault Tolerant (BFT)](https://en.wikipedia.org/wiki/Byzantine_fault_tolerance). and Tendermint is [Byzantine Fault Tolerant (BFT)](https://en.wikipedia.org/wiki/Byzantine_fault_tolerance).
### Node Diversity ## Node Diversity
Steps should be taken to make it difficult for any one actor or event to control or damage “enough” of the nodes. (Because Planetmint Server uses Tendermint, "enough" is ⅓.) There are many kinds of diversity to consider, listed below. It may be quite difficult to have high diversity of all kinds. Steps should be taken to make it difficult for any one actor or event to control or damage “enough” of the nodes. (Because Planetmint Server uses Tendermint, "enough" is ⅓.) There are many kinds of diversity to consider, listed below. It may be quite difficult to have high diversity of all kinds.
@ -39,7 +39,7 @@ Steps should be taken to make it difficult for any one actor or event to control
Note: If all the nodes are running the same code, i.e. the same implementation of Planetmint, then a bug in that code could be used to compromise all of the nodes. Ideally, there would be several different, well-maintained implementations of Planetmint Server (e.g. one in Python, one in Go, etc.), so that a consortium could also have a diversity of server implementations. Similar remarks can be made about the operating system. Note: If all the nodes are running the same code, i.e. the same implementation of Planetmint, then a bug in that code could be used to compromise all of the nodes. Ideally, there would be several different, well-maintained implementations of Planetmint Server (e.g. one in Python, one in Go, etc.), so that a consortium could also have a diversity of server implementations. Similar remarks can be made about the operating system.
### Immutability ## Immutability
The blockchain community often describes blockchains as “immutable.” If we interpret that word literally, it means that blockchain data is unchangeable or permanent, which is absurd. The data _can_ be changed. For example, a plague might drive humanity extinct; the data would then get corrupted over time due to water damage, thermal noise, and the general increase of entropy. The blockchain community often describes blockchains as “immutable.” If we interpret that word literally, it means that blockchain data is unchangeable or permanent, which is absurd. The data _can_ be changed. For example, a plague might drive humanity extinct; the data would then get corrupted over time due to water damage, thermal noise, and the general increase of entropy.

View File

@ -9,19 +9,17 @@ Code is Apache-2.0 and docs are CC-BY-4.0
There is some specialized terminology associated with Planetmint. To get started, you should at least know the following: There is some specialized terminology associated with Planetmint. To get started, you should at least know the following:
### Planetmint Node ## Planetmint Node
<<<<<<< HEAD **Planetmint node** is a machine (or logical machine) running [Planetmint Server](https://docs.planetmint.com/projects/server/en/latest/introduction.html) and related software. Each node is controlled by one person or organization.
A **Planetmint node** is a machine (or logical machine) running [Planetmint Server](https://docs.planetmint.com/projects/server/en/latest/introduction.html) and related software. Each node is controlled by one person or organization.
=======
A **Planetmint node** is a machine (or logical machine) running [Planetmint Server](https://docs.planetmint.io/projects/server/en/latest/introduction.html) and related software. Each node is controlled by one person or organization.
>>>>>>> 3bfc3298f8210b135084e823eedd47f213538088
### Planetmint Network **Planetmint node** is a machine (or logical machine) running [Planetmint Server](https://docs.planetmint.io/projects/server/en/latest/introduction.html) and related software. Each node is controlled by one person or organization.
## Planetmint Network
A set of Planetmint nodes can connect to each other to form a **Planetmint network**. Each node in the network runs the same software. A Planetmint network may have additional machines to do things such as monitoring. A set of Planetmint nodes can connect to each other to form a **Planetmint network**. Each node in the network runs the same software. A Planetmint network may have additional machines to do things such as monitoring.
### Planetmint Consortium ## Planetmint Consortium
The people and organizations that run the nodes in a Planetmint network belong to a **Planetmint consortium** (i.e. another organization). A consortium must have some sort of governance structure to make decisions. If a Planetmint network is run by a single company, then the "consortium" is just that company. The people and organizations that run the nodes in a Planetmint network belong to a **Planetmint consortium** (i.e. another organization). A consortium must have some sort of governance structure to make decisions. If a Planetmint network is run by a single company, then the "consortium" is just that company.
@ -29,7 +27,7 @@ The people and organizations that run the nodes in a Planetmint network belong t
A Planetmint network is just a bunch of connected nodes. A consortium is an organization which has a Planetmint network, and where each node in that network has a different operator. A Planetmint network is just a bunch of connected nodes. A consortium is an organization which has a Planetmint network, and where each node in that network has a different operator.
### Transactions ## Transactions
Are described in detail in `Planetmint Transactions Spec <https://github.com/planetmint/BEPs/tree/master/tx-specs/>`_ . Are described in detail in `Planetmint Transactions Spec <https://github.com/planetmint/BEPs/tree/master/tx-specs/>`_ .
@ -80,10 +78,7 @@ You could do more elaborate things too. As one example, each time someone writes
### Role-Based Access Control (RBAC) ### Role-Based Access Control (RBAC)
<<<<<<< HEAD
In September 2017, we published a [blog post about how one can define an RBAC sub-system on top of Planetmint](https://blog.planetmint.com/role-based-access-control-for-planetmint-assets-b7cada491997). In September 2017, we published a [blog post about how one can define an RBAC sub-system on top of Planetmint](https://blog.planetmint.com/role-based-access-control-for-planetmint-assets-b7cada491997).
=======
In September 2017, BigchainDB published a [blog post about how one can define an RBAC sub-system on top of Planetmint](https://blog.bigchaindb.com/role-based-access-control-for-planetmint-assets-b7cada491997).
>>>>>>> 3bfc3298f8210b135084e823eedd47f213538088
At the time of writing (January 2018), doing so required the use of a plugin, so it's not possible using standard Planetmint (which is what's available on the [IPDB Testnet](https://test.ipdb.io/>). That may change in the future. At the time of writing (January 2018), doing so required the use of a plugin, so it's not possible using standard Planetmint (which is what's available on the [IPDB Testnet](https://test.ipdb.io/>). That may change in the future.
If you're interested, `contact IPDB <contact@ipdb.global>`_. If you're interested, `contact IPDB <contact@ipdb.global>`_.

23
integration/README.md Normal file
View File

@ -0,0 +1,23 @@
<!---
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
--->
# Integration test suite
This directory contains the integration test suite for Planetmint.
The suite uses Docker Compose to spin up multiple Planetmint nodes, run tests with `pytest` as well as cli tests and teardown.
## Running the tests
Run `make test-integration` in the project root directory.
By default the integration test suite spins up four planetmint nodes. If you desire to run a different configuration you can pass `SCALE=<number of nodes>` as an environmental variable.
## Writing and documenting the tests
Tests are sometimes difficult to read. For integration tests, we try to be really explicit on what the test is doing, so please write code that is *simple* and easy to understand. We decided to use literate-programming documentation. To generate the documentation for python tests run:
```bash
make docs-integration
```

View File

@ -0,0 +1,47 @@
#!/bin/bash
# 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
# Add chain migration test
check_status () {
status=$(ssh -o "StrictHostKeyChecking=no" -i \~/.ssh/id_rsa root@$1 'bash -s' < scripts/election.sh show_election $2 | tail -n 1)
status=${status#*=}
if [ $status != $3 ]; then
exit 1
fi
}
# Read host names from shared
readarray -t HOSTNAMES < /shared/hostnames
# Split into proposer and approvers
PROPOSER=${HOSTNAMES[0]}
APPROVERS=${HOSTNAMES[@]:1}
# Propose chain migration
result=$(ssh -o "StrictHostKeyChecking=no" -i \~/.ssh/id_rsa root@${PROPOSER} 'bash -s' < scripts/election.sh migrate)
# Check if election is ongoing and approve chain migration
for APPROVER in ${APPROVERS[@]}; do
# Check if election is still ongoing
check_status ${APPROVER} $result ongoing
ssh -o "StrictHostKeyChecking=no" -i ~/.ssh/id_rsa root@${APPROVER} 'bash -s' < scripts/election.sh approve $result
done
# Status of election should be concluded
status=$(ssh -o "StrictHostKeyChecking=no" -i \~/.ssh/id_rsa root@${PROPOSER} 'bash -s' < scripts/election.sh show_election $result)
status=${status#*INFO:planetmint.commands.planetmint:}
status=("$status[@]")
# TODO: Get status, chain_id, app_hash and validators to restore planetmint on all nodes
# References:
# https://github.com/bigchaindb/BEPs/tree/master/42
# http://docs.bigchaindb.com/en/latest/installation/node-setup/bigchaindb-cli.html
for word in $status; do
echo $word
done
echo ${status#*validators=}

View File

@ -0,0 +1,33 @@
#!/bin/bash
# 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
check_status () {
status=$(ssh -o "StrictHostKeyChecking=no" -i \~/.ssh/id_rsa root@$1 'bash -s' < scripts/election.sh show_election $2 | tail -n 1)
status=${status#*=}
if [ $status != $3 ]; then
exit 1
fi
}
# Read host names from shared
readarray -t HOSTNAMES < /shared/hostnames
# Split into proposer and approvers
PROPOSER=${HOSTNAMES[0]}
APPROVERS=${HOSTNAMES[@]:1}
# Propose validator upsert
result=$(ssh -o "StrictHostKeyChecking=no" -i \~/.ssh/id_rsa root@${PROPOSER} 'bash -s' < scripts/election.sh elect 2)
# Check if election is ongoing and approve validator upsert
for APPROVER in ${APPROVERS[@]}; do
# Check if election is still ongoing
check_status ${APPROVER} $result ongoing
ssh -o "StrictHostKeyChecking=no" -i ~/.ssh/id_rsa root@${APPROVER} 'bash -s' < scripts/election.sh approve $result
done
# Status of election should be concluded
check_status ${PROPOSER} $result concluded

1
integration/python/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
docs

View File

@ -0,0 +1,22 @@
FROM python:3.9
RUN apt-get update \
&& pip install -U pip \
&& apt-get autoremove \
&& apt-get clean
RUN apt-get install -y vim
RUN apt-get update
RUN apt-get install -y build-essential cmake openssh-client openssh-server
RUN apt-get install -y zsh
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 \
blns

View File

@ -0,0 +1,95 @@
# 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
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'"""
FULFILL_SCRIPT = \
"""Rule input encoding base58
Rule output encoding base58
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'
Then print the string 'ok'"""
HOUSE_ASSETS = {
"data": {
"houses": [
{
"name": "Harry",
"team": "Gryffindor",
},
{
"name": "Draco",
"team": "Slytherin",
}
],
}
}
ZENROOM_DATA = {
'also': 'more data'
}
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

@ -0,0 +1,36 @@
# 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 typing import List
from planetmint_driver import Planetmint
class Hosts:
hostnames = []
connections = []
def __init__(self, filepath):
self.set_hostnames(filepath=filepath)
self.set_connections()
def set_hostnames(self, filepath) -> None:
with open(filepath) as f:
self.hostnames = f.readlines()
def set_connections(self) -> None:
self.connections = list(map(lambda h: Planetmint(h), self.hostnames))
def get_connection(self, index=0) -> Planetmint:
return self.connections[index]
def get_transactions(self, tx_id) -> List:
return list(map(lambda connection: connection.transactions.retrieve(tx_id), self.connections))
def assert_transaction(self, tx_id) -> None:
txs = self.get_transactions(tx_id)
for tx in txs:
assert txs[0] == tx, \
'Cannot find transaction {}'.format(tx_id)

View File

@ -0,0 +1,83 @@
# 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 Planetmint and create object
from planetmint_driver.crypto import generate_keypair
# import helper to manage multiple nodes
from .helper.hosts import Hosts
import time
def test_basic():
# Setup up connection to Planetmint integration test nodes
hosts = Hosts('/shared/hostnames')
pm_alpha = hosts.get_connection()
# genarate a keypair
alice = generate_keypair()
# create a digital asset for Alice
game_boy_token = {
'data': {
'hash': '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
'storageID': '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
},
}
# prepare the transaction with the digital asset and issue 10 tokens to bob
prepared_creation_tx = pm_alpha.transactions.prepare(
operation='CREATE',
metadata={
'hash': '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
'storageID': '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', },
signers=alice.public_key,
recipients=[([alice.public_key], 10)],
asset=game_boy_token)
# fulfill and send the transaction
fulfilled_creation_tx = pm_alpha.transactions.fulfill(
prepared_creation_tx,
private_keys=alice.private_key)
pm_alpha.transactions.send_commit(fulfilled_creation_tx)
time.sleep(1)
creation_tx_id = fulfilled_creation_tx['id']
# Assert that transaction is stored on all planetmint nodes
hosts.assert_transaction(creation_tx_id)
# Transfer
# create the output and inout for the transaction
transfer_asset = {'id': creation_tx_id}
output_index = 0
output = fulfilled_creation_tx['outputs'][output_index]
transfer_input = {'fulfillment': output['condition']['details'],
'fulfills': {'output_index': output_index,
'transaction_id': transfer_asset['id']},
'owners_before': output['public_keys']}
# prepare the transaction and use 3 tokens
prepared_transfer_tx = pm_alpha.transactions.prepare(
operation='TRANSFER',
asset=transfer_asset,
inputs=transfer_input,
metadata={'hash': '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
'storageID': '0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', },
recipients=[([alice.public_key], 10)])
# fulfill and send the transaction
fulfilled_transfer_tx = pm_alpha.transactions.fulfill(
prepared_transfer_tx,
private_keys=alice.private_key)
sent_transfer_tx = pm_alpha.transactions.send_commit(fulfilled_transfer_tx)
time.sleep(1)
transfer_tx_id = sent_transfer_tx['id']
# Assert that transaction is stored on both planetmint nodes
hosts.assert_transaction(transfer_tx_id)

View File

@ -0,0 +1,183 @@
# 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
# # Divisible assets integration testing
# This test checks if we can successfully divide assets.
# The script tests various things like:
#
# - create a transaction with a divisible asset and issue them to someone
# - check if the transaction is stored and has the right amount of tokens
# - spend some tokens
# - try to spend more tokens than available
#
# We run a series of checks for each step, that is retrieving
# the transaction from the remote system, and also checking the `amount`
# of a given transaction.
#
# This integration test is a rip-off of our
# [tutorial](https://docs.planetmint.com/projects/py-driver/en/latest/usage.html).
# ## Imports
# We need the `pytest` package to catch the `BadRequest` exception properly.
# And of course, we also need the `BadRequest`.
import pytest
from planetmint_driver.exceptions import BadRequest
# Import generate_keypair to create actors
from planetmint_driver.crypto import generate_keypair
# import helper to manage multiple nodes
from .helper.hosts import Hosts
def test_divisible_assets():
# ## Set up a connection to Planetmint
# Check [test_basic.py](./test_basic.html) to get some more details
# about the endpoint.
hosts = Hosts('/shared/hostnames')
pm = hosts.get_connection()
# Oh look, it is Alice again and she brought her friend Bob along.
alice, bob = generate_keypair(), generate_keypair()
# ## Alice creates a time sharing token
# Alice wants to go on vacation, while Bobs bike just broke down.
# Alice decides to rent her bike to Bob while she is gone.
# So she prepares a `CREATE` transaction to issues 10 tokens.
# First, she prepares an asset for a time sharing token. As you can see in
# the description, Bob and Alice agree that each token can be used to ride
# the bike for one hour.
bike_token = {
'data': {
'token_for': {
'bike': {
'serial_number': 420420
}
},
'description': 'Time share token. Each token equals one hour of riding.',
},
}
# She prepares a `CREATE` transaction and issues 10 tokens.
# Here, Alice defines in a tuple that she wants to assign
# these 10 tokens to Bob.
prepared_token_tx = pm.transactions.prepare(
operation='CREATE',
signers=alice.public_key,
recipients=[([bob.public_key], 10)],
asset=bike_token)
# She fulfills and sends the transaction.
fulfilled_token_tx = pm.transactions.fulfill(
prepared_token_tx,
private_keys=alice.private_key)
pm.transactions.send_commit(fulfilled_token_tx)
# We store the `id` of the transaction to use it later on.
bike_token_id = fulfilled_token_tx['id']
# Let's check if the transaction was successful.
assert pm.transactions.retrieve(bike_token_id), \
'Cannot find transaction {}'.format(bike_token_id)
# Bob owns 10 tokens now.
assert pm.transactions.retrieve(bike_token_id)['outputs'][0][
'amount'] == '10'
# ## Bob wants to use the bike
# Now that Bob got the tokens and the sun is shining, he wants to get out
# with the bike for three hours.
# To use the bike he has to send the tokens back to Alice.
# To learn about the details of transferring a transaction check out
# [test_basic.py](./test_basic.html)
transfer_asset = {'id': bike_token_id}
output_index = 0
output = fulfilled_token_tx['outputs'][output_index]
transfer_input = {'fulfillment': output['condition']['details'],
'fulfills': {'output_index': output_index,
'transaction_id': fulfilled_token_tx[
'id']},
'owners_before': output['public_keys']}
# To use the tokens Bob has to reassign 7 tokens to himself and the
# amount he wants to use to Alice.
prepared_transfer_tx = pm.transactions.prepare(
operation='TRANSFER',
asset=transfer_asset,
inputs=transfer_input,
recipients=[([alice.public_key], 3), ([bob.public_key], 7)])
# He signs and sends the transaction.
fulfilled_transfer_tx = pm.transactions.fulfill(
prepared_transfer_tx,
private_keys=bob.private_key)
sent_transfer_tx = pm.transactions.send_commit(fulfilled_transfer_tx)
# First, Bob checks if the transaction was successful.
assert pm.transactions.retrieve(
fulfilled_transfer_tx['id']) == sent_transfer_tx
hosts.assert_transaction(fulfilled_transfer_tx['id'])
# There are two outputs in the transaction now.
# The first output shows that Alice got back 3 tokens...
assert pm.transactions.retrieve(
fulfilled_transfer_tx['id'])['outputs'][0]['amount'] == '3'
# ... while Bob still has 7 left.
assert pm.transactions.retrieve(
fulfilled_transfer_tx['id'])['outputs'][1]['amount'] == '7'
# ## Bob wants to ride the bike again
# It's been a week and Bob wants to right the bike again.
# Now he wants to ride for 8 hours, that's a lot Bob!
# He prepares the transaction again.
transfer_asset = {'id': bike_token_id}
# This time we need an `output_index` of 1, since we have two outputs
# in the `fulfilled_transfer_tx` we created before. The first output with
# index 0 is for Alice and the second output is for Bob.
# Since Bob wants to spend more of his tokens he has to provide the
# correct output with the correct amount of tokens.
output_index = 1
output = fulfilled_transfer_tx['outputs'][output_index]
transfer_input = {'fulfillment': output['condition']['details'],
'fulfills': {'output_index': output_index,
'transaction_id': fulfilled_transfer_tx['id']},
'owners_before': output['public_keys']}
# This time Bob only provides Alice in the `recipients` because he wants
# to spend all his tokens
prepared_transfer_tx = pm.transactions.prepare(
operation='TRANSFER',
asset=transfer_asset,
inputs=transfer_input,
recipients=[([alice.public_key], 8)])
fulfilled_transfer_tx = pm.transactions.fulfill(
prepared_transfer_tx,
private_keys=bob.private_key)
# Oh Bob, what have you done?! You tried to spend more tokens than you had.
# Remember Bob, last time you spent 3 tokens already,
# so you only have 7 left.
with pytest.raises(BadRequest) as error:
pm.transactions.send_commit(fulfilled_transfer_tx)
# Now Bob gets an error saying that the amount he wanted to spent is
# higher than the amount of tokens he has left.
assert error.value.args[0] == 400
message = 'Invalid transaction (AmountError): The amount used in the ' \
'inputs `7` needs to be same as the amount used in the ' \
'outputs `8`'
assert error.value.args[2]['message'] == message
# We have to stop this test now, I am sorry, but Bob is pretty upset
# about his mistake. See you next time :)

View File

@ -0,0 +1,48 @@
# 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
# # Double Spend testing
# This test challenge the system with double spends.
from uuid import uuid4
from threading import Thread
import queue
import planetmint_driver.exceptions
from planetmint_driver.crypto import generate_keypair
from .helper.hosts import Hosts
def test_double_create():
hosts = Hosts('/shared/hostnames')
pm = hosts.get_connection()
alice = generate_keypair()
results = queue.Queue()
tx = pm.transactions.fulfill(
pm.transactions.prepare(
operation='CREATE',
signers=alice.public_key,
asset={'data': {'uuid': str(uuid4())}}),
private_keys=alice.private_key)
def send_and_queue(tx):
try:
pm.transactions.send_commit(tx)
results.put('OK')
except planetmint_driver.exceptions.TransportError:
results.put('FAIL')
t1 = Thread(target=send_and_queue, args=(tx, ))
t2 = Thread(target=send_and_queue, args=(tx, ))
t1.start()
t2.start()
results = [results.get(timeout=2), results.get(timeout=2)]
assert results.count('OK') == 1
assert results.count('FAIL') == 1

View File

@ -0,0 +1,133 @@
# 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
# # Multisignature integration testing
# This test checks if we can successfully create and transfer a transaction
# with multiple owners.
# The script tests various things like:
#
# - create a transaction with multiple owners
# - check if the transaction is stored and has the right amount of public keys
# - transfer the transaction to a third person
#
# We run a series of checks for each step, that is retrieving
# the transaction from the remote system, and also checking the public keys
# of a given transaction.
#
# This integration test is a rip-off of our mutliple signature acceptance tests.
# # Imports
import time
# For this test case we need import and use the Python driver
from planetmint_driver.crypto import generate_keypair
# Import helper to deal with multiple nodes
from .helper.hosts import Hosts
def test_multiple_owners():
# Setup up connection to Planetmint integration test nodes
hosts = Hosts('/shared/hostnames')
pm_alpha = hosts.get_connection()
# Generate Keypairs for Alice and Bob!
alice, bob = generate_keypair(), generate_keypair()
# ## Alice and Bob create a transaction
# Alice and Bob just moved into a shared flat, no one can afford these
# high rents anymore. Bob suggests to get a dish washer for the
# kitchen. Alice agrees and here they go, creating the asset for their
# dish washer.
dw_asset = {
'data': {
'dish washer': {
'serial_number': 1337
}
}
}
# They prepare a `CREATE` transaction. To have multiple owners, both
# Bob and Alice need to be the recipients.
prepared_dw_tx = pm_alpha.transactions.prepare(
operation='CREATE',
signers=alice.public_key,
recipients=(alice.public_key, bob.public_key),
asset=dw_asset)
# Now they both sign the transaction by providing their private keys.
# And send it afterwards.
fulfilled_dw_tx = pm_alpha.transactions.fulfill(
prepared_dw_tx,
private_keys=[alice.private_key, bob.private_key])
pm_alpha.transactions.send_commit(fulfilled_dw_tx)
# We store the `id` of the transaction to use it later on.
dw_id = fulfilled_dw_tx['id']
time.sleep(1)
# Use hosts to assert that the transaction is properly propagated to every node
hosts.assert_transaction(dw_id)
# Let's check if the transaction was successful.
assert pm_alpha.transactions.retrieve(dw_id), \
'Cannot find transaction {}'.format(dw_id)
# The transaction should have two public keys in the outputs.
assert len(
pm_alpha.transactions.retrieve(dw_id)['outputs'][0]['public_keys']) == 2
# ## Alice and Bob transfer a transaction to Carol.
# Alice and Bob save a lot of money living together. They often go out
# for dinner and don't cook at home. But now they don't have any dishes to
# wash, so they decide to sell the dish washer to their friend Carol.
# Hey Carol, nice to meet you!
carol = generate_keypair()
# Alice and Bob prepare the transaction to transfer the dish washer to
# Carol.
transfer_asset = {'id': dw_id}
output_index = 0
output = fulfilled_dw_tx['outputs'][output_index]
transfer_input = {'fulfillment': output['condition']['details'],
'fulfills': {'output_index': output_index,
'transaction_id': fulfilled_dw_tx[
'id']},
'owners_before': output['public_keys']}
# Now they create the transaction...
prepared_transfer_tx = pm_alpha.transactions.prepare(
operation='TRANSFER',
asset=transfer_asset,
inputs=transfer_input,
recipients=carol.public_key)
# ... and sign it with their private keys, then send it.
fulfilled_transfer_tx = pm_alpha.transactions.fulfill(
prepared_transfer_tx,
private_keys=[alice.private_key, bob.private_key])
sent_transfer_tx = pm_alpha.transactions.send_commit(fulfilled_transfer_tx)
time.sleep(1)
# Now compare if both nodes returned the same transaction
hosts.assert_transaction(fulfilled_transfer_tx['id'])
# They check if the transaction was successful.
assert pm_alpha.transactions.retrieve(
fulfilled_transfer_tx['id']) == sent_transfer_tx
# The owners before should include both Alice and Bob.
assert len(
pm_alpha.transactions.retrieve(fulfilled_transfer_tx['id'])['inputs'][0][
'owners_before']) == 2
# While the new owner is Carol.
assert pm_alpha.transactions.retrieve(fulfilled_transfer_tx['id'])[
'outputs'][0]['public_keys'][0] == carol.public_key

View File

@ -0,0 +1,100 @@
# 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
# ## Testing potentially hazardous strings
# This test uses a library of `naughty` strings (code injections, weird unicode chars., etc.) as both keys and values.
# We look for either a successful tx, or in the case that we use a naughty string as a key, and it violates some key
# constraints, we expect to receive a well formatted error message.
# ## Imports
# Since the naughty strings get encoded and decoded in odd ways,
# we'll use a regex to sweep those details under the rug.
import re
# We'll use a nice library of naughty strings...
from blns import blns
# And parameterize our test so each one is treated as a separate test case
import pytest
# For this test case we import and use the Python Driver.
from planetmint_driver.crypto import generate_keypair
from planetmint_driver.exceptions import BadRequest
# import helper to manage multiple nodes
from .helper.hosts import Hosts
naughty_strings = blns.all()
# This is our base test case, but we'll reuse it to send naughty strings as both keys and values.
def send_naughty_tx(asset, metadata):
# ## Set up a connection to Planetmint
# Check [test_basic.py](./test_basic.html) to get some more details
# about the endpoint.
hosts = Hosts('/shared/hostnames')
pm = hosts.get_connection()
# Here's Alice.
alice = generate_keypair()
# Alice is in a naughty mood today, so she creates a tx with some naughty strings
prepared_transaction = pm.transactions.prepare(
operation='CREATE',
signers=alice.public_key,
asset=asset,
metadata=metadata)
# She fulfills the transaction
fulfilled_transaction = pm.transactions.fulfill(
prepared_transaction,
private_keys=alice.private_key)
# The fulfilled tx gets sent to the pm network
try:
sent_transaction = pm.transactions.send_commit(fulfilled_transaction)
except BadRequest as e:
sent_transaction = e
# If her key contained a '.', began with a '$', or contained a NUL character
regex = r'.*\..*|\$.*|.*\x00.*'
key = next(iter(metadata))
if re.match(regex, key):
# Then she expects a nicely formatted error code
status_code = sent_transaction.status_code
error = sent_transaction.error
regex = (
r'\{\s*\n*'
r'\s*"message":\s*"Invalid transaction \(ValidationError\):\s*'
r'Invalid key name.*The key name cannot contain characters.*\n*'
r'\s*"status":\s*400\n*'
r'\s*\}\n*')
assert status_code == 400
assert re.fullmatch(regex, error), sent_transaction
# Otherwise, she expects to see her transaction in the database
elif 'id' in sent_transaction.keys():
tx_id = sent_transaction['id']
assert pm.transactions.retrieve(tx_id)
# If neither condition was true, then something weird happened...
else:
raise TypeError(sent_transaction)
@pytest.mark.parametrize("naughty_string", naughty_strings, ids=naughty_strings)
def test_naughty_keys(naughty_string):
asset = {'data': {naughty_string: 'nice_value'}}
metadata = {naughty_string: 'nice_value'}
send_naughty_tx(asset, metadata)
@pytest.mark.parametrize("naughty_string", naughty_strings, ids=naughty_strings)
def test_naughty_values(naughty_string):
asset = {'data': {'nice_key': naughty_string}}
metadata = {'nice_key': naughty_string}
send_naughty_tx(asset, metadata)

View File

@ -0,0 +1,131 @@
# 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
# # Stream Acceptance Test
# This test checks if the event stream works correctly. The basic idea of this
# test is to generate some random **valid** transaction, send them to a
# Planetmint node, and expect those transactions to be returned by the valid
# transactions Stream API. During this test, two threads work together,
# sharing a queue to exchange events.
#
# - The *main thread* first creates and sends the transactions to Planetmint;
# then it run through all events in the shared queue to check if all
# transactions sent have been validated by Planetmint.
# - The *listen thread* listens to the events coming from Planetmint and puts
# them in a queue shared with the main thread.
import queue
import json
from threading import Thread, Event
from uuid import uuid4
# For this script, we need to set up a websocket connection, that's the reason
# we import the
# [websocket](https://github.com/websocket-client/websocket-client) module
from websocket import create_connection
from planetmint_driver.crypto import generate_keypair
# import helper to manage multiple nodes
from .helper.hosts import Hosts
def test_stream():
# ## Set up the test
# We use the env variable `BICHAINDB_ENDPOINT` to know where to connect.
# Check [test_basic.py](./test_basic.html) for more information.
hosts = Hosts('/shared/hostnames')
pm = hosts.get_connection()
# *That's pretty bad, but let's do like this for now.*
WS_ENDPOINT = 'ws://{}:9985/api/v1/streams/valid_transactions'.format(hosts.hostnames[0])
# Hello to Alice again, she is pretty active in those tests, good job
# Alice!
alice = generate_keypair()
# We need few variables to keep the state, specifically we need `sent` to
# keep track of all transactions Alice sent to Planetmint, while `received`
# are the transactions Planetmint validated and sent back to her.
sent = []
received = queue.Queue()
# In this test we use a websocket. The websocket must be started **before**
# sending transactions to Planetmint, otherwise we might lose some
# transactions. The `ws_ready` event is used to synchronize the main thread
# with the listen thread.
ws_ready = Event()
# ## Listening to events
# This is the function run by the complementary thread.
def listen():
# First we connect to the remote endpoint using the WebSocket protocol.
ws = create_connection(WS_ENDPOINT)
# After the connection has been set up, we can signal the main thread
# to proceed (continue reading, it should make sense in a second.)
ws_ready.set()
# It's time to consume all events coming from the Planetmint stream API.
# Every time a new event is received, it is put in the queue shared
# with the main thread.
while True:
result = ws.recv()
received.put(result)
# Put `listen` in a thread, and start it. Note that `listen` is a local
# function and it can access all variables in the enclosing function.
t = Thread(target=listen, daemon=True)
t.start()
# ## Pushing the transactions to Planetmint
# After starting the listen thread, we wait for it to connect, and then we
# proceed.
ws_ready.wait()
# Here we prepare, sign, and send ten different `CREATE` transactions. To
# make sure each transaction is different from the other, we generate a
# random `uuid`.
for _ in range(10):
tx = pm.transactions.fulfill(
pm.transactions.prepare(
operation='CREATE',
signers=alice.public_key,
asset={'data': {'uuid': str(uuid4())}}),
private_keys=alice.private_key)
# We don't want to wait for each transaction to be in a block. By using
# `async` mode, we make sure that the driver returns as soon as the
# transaction is pushed to the Planetmint API. Remember: we expect all
# transactions to be in the shared queue: this is a two phase test,
# first we send a bunch of transactions, then we check if they are
# valid (and, in this case, they should).
pm.transactions.send_async(tx)
# The `id` of every sent transaction is then stored in a list.
sent.append(tx['id'])
# ## Check the valid transactions coming from Planetmint
# Now we are ready to check if Planetmint did its job. A simple way to
# check if all sent transactions have been processed is to **remove** from
# `sent` the transactions we get from the *listen thread*. At one point in
# time, `sent` should be empty, and we exit the test.
while sent:
# To avoid waiting forever, we have an arbitrary timeout of 5
# seconds: it should be enough time for Planetmint to create
# blocks, in fact a new block is created every second. If we hit
# the timeout, then game over ¯\\\_(ツ)\_/¯
try:
event = received.get(timeout=5)
txid = json.loads(event)['transaction_id']
except queue.Empty:
assert False, 'Did not receive all expected transactions'
# Last thing is to try to remove the `txid` from the set of sent
# transactions. If this test is running in parallel with others, we
# might get a transaction id of another test, and `remove` can fail.
# It's OK if this happens.
try:
sent.remove(txid)
except ValueError:
pass

View File

@ -0,0 +1,336 @@
# 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
# ## Imports
import time
import json
# For this test case we need the planetmint_driver.crypto package
import base58
import sha3
from cryptoconditions import Ed25519Sha256, ThresholdSha256
from planetmint_driver.crypto import generate_keypair
# Import helper to deal with multiple nodes
from .helper.hosts import Hosts
def prepare_condition_details(condition: ThresholdSha256):
condition_details = {
'subconditions': [],
'threshold': condition.threshold,
'type': condition.TYPE_NAME
}
for s in condition.subconditions:
if (s['type'] == 'fulfillment' and s['body'].TYPE_NAME == 'ed25519-sha-256'):
condition_details['subconditions'].append({
'type': s['body'].TYPE_NAME,
'public_key': base58.b58encode(s['body'].public_key).decode()
})
else:
condition_details['subconditions'].append(prepare_condition_details(s['body']))
return condition_details
def test_threshold():
# Setup connection to test nodes
hosts = Hosts('/shared/hostnames')
pm = hosts.get_connection()
# Generate Keypars for Alice, Bob an Carol!
alice, bob, carol = generate_keypair(), generate_keypair(), generate_keypair()
# ## Alice and Bob create a transaction
# Alice and Bob just moved into a shared flat, no one can afford these
# high rents anymore. Bob suggests to get a dish washer for the
# kitchen. Alice agrees and here they go, creating the asset for their
# dish washer.
dw_asset = {
'data': {
'dish washer': {
'serial_number': 1337
}
}
}
# Create subfulfillments
alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))
bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))
carol_ed25519 = Ed25519Sha256(public_key=base58.b58decode(carol.public_key))
# Create threshold condition (2/3) and add subfulfillments
threshold_sha256 = ThresholdSha256(2)
threshold_sha256.add_subfulfillment(alice_ed25519)
threshold_sha256.add_subfulfillment(bob_ed25519)
threshold_sha256.add_subfulfillment(carol_ed25519)
# Create a condition uri and details for the output object
condition_uri = threshold_sha256.condition.serialize_uri()
condition_details = prepare_condition_details(threshold_sha256)
# Assemble output and input for the handcrafted tx
output = {
'amount': '1',
'condition': {
'details': condition_details,
'uri': condition_uri,
},
'public_keys': (alice.public_key, bob.public_key, carol.public_key),
}
# The yet to be fulfilled input:
input_ = {
'fulfillment': None,
'fulfills': None,
'owners_before': (alice.public_key, bob.public_key),
}
# Assemble the handcrafted transaction
handcrafted_dw_tx = {
'operation': 'CREATE',
'asset': dw_asset,
'metadata': None,
'outputs': (output,),
'inputs': (input_,),
'version': '2.0',
'id': None,
}
# Create sha3-256 of message to sign
message = json.dumps(
handcrafted_dw_tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
)
message = sha3.sha3_256(message.encode())
# Sign message with Alice's und Bob's private key
alice_ed25519.sign(message.digest(), base58.b58decode(alice.private_key))
bob_ed25519.sign(message.digest(), base58.b58decode(bob.private_key))
# Create fulfillment and add uri to inputs
fulfillment_threshold = ThresholdSha256(2)
fulfillment_threshold.add_subfulfillment(alice_ed25519)
fulfillment_threshold.add_subfulfillment(bob_ed25519)
fulfillment_threshold.add_subcondition(carol_ed25519.condition)
fulfillment_uri = fulfillment_threshold.serialize_uri()
handcrafted_dw_tx['inputs'][0]['fulfillment'] = fulfillment_uri
# Create tx_id for handcrafted_dw_tx and send tx commit
json_str_tx = json.dumps(
handcrafted_dw_tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
)
dw_creation_txid = sha3.sha3_256(json_str_tx.encode()).hexdigest()
handcrafted_dw_tx['id'] = dw_creation_txid
pm.transactions.send_commit(handcrafted_dw_tx)
time.sleep(1)
# Assert that the tx is propagated to all nodes
hosts.assert_transaction(dw_creation_txid)
def test_weighted_threshold():
hosts = Hosts('/shared/hostnames')
pm = hosts.get_connection()
alice, bob, carol = generate_keypair(), generate_keypair(), generate_keypair()
asset = {
'data': {
'trashcan': {
'animals': ['racoon_1', 'racoon_2']
}
}
}
alice_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))
bob_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))
carol_ed25519 = Ed25519Sha256(public_key=base58.b58decode(carol.public_key))
threshold = ThresholdSha256(1)
threshold.add_subfulfillment(alice_ed25519)
sub_threshold = ThresholdSha256(2)
sub_threshold.add_subfulfillment(bob_ed25519)
sub_threshold.add_subfulfillment(carol_ed25519)
threshold.add_subfulfillment(sub_threshold)
condition_uri = threshold.condition.serialize_uri()
condition_details = prepare_condition_details(threshold)
# Assemble output and input for the handcrafted tx
output = {
'amount': '1',
'condition': {
'details': condition_details,
'uri': condition_uri,
},
'public_keys': (alice.public_key, bob.public_key, carol.public_key),
}
# The yet to be fulfilled input:
input_ = {
'fulfillment': None,
'fulfills': None,
'owners_before': (alice.public_key, bob.public_key),
}
# Assemble the handcrafted transaction
handcrafted_tx = {
'operation': 'CREATE',
'asset': asset,
'metadata': None,
'outputs': (output,),
'inputs': (input_,),
'version': '2.0',
'id': None,
}
# Create sha3-256 of message to sign
message = json.dumps(
handcrafted_tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
)
message = sha3.sha3_256(message.encode())
# Sign message with Alice's und Bob's private key
alice_ed25519.sign(message.digest(), base58.b58decode(alice.private_key))
# Create fulfillment and add uri to inputs
sub_fulfillment_threshold = ThresholdSha256(2)
sub_fulfillment_threshold.add_subcondition(bob_ed25519.condition)
sub_fulfillment_threshold.add_subcondition(carol_ed25519.condition)
fulfillment_threshold = ThresholdSha256(1)
fulfillment_threshold.add_subfulfillment(alice_ed25519)
fulfillment_threshold.add_subfulfillment(sub_fulfillment_threshold)
fulfillment_uri = fulfillment_threshold.serialize_uri()
handcrafted_tx['inputs'][0]['fulfillment'] = fulfillment_uri
# Create tx_id for handcrafted_dw_tx and send tx commit
json_str_tx = json.dumps(
handcrafted_tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
)
creation_tx_id = sha3.sha3_256(json_str_tx.encode()).hexdigest()
handcrafted_tx['id'] = creation_tx_id
pm.transactions.send_commit(handcrafted_tx)
time.sleep(1)
# Assert that the tx is propagated to all nodes
hosts.assert_transaction(creation_tx_id)
# Now transfer created asset
alice_transfer_ed25519 = Ed25519Sha256(public_key=base58.b58decode(alice.public_key))
bob_transfer_ed25519 = Ed25519Sha256(public_key=base58.b58decode(bob.public_key))
carol_transfer_ed25519 = Ed25519Sha256(public_key=base58.b58decode(carol.public_key))
transfer_condition_uri = alice_transfer_ed25519.condition.serialize_uri()
# Assemble output and input for the handcrafted tx
transfer_output = {
'amount': '1',
'condition': {
'details': {
'type': alice_transfer_ed25519.TYPE_NAME,
'public_key': base58.b58encode(alice_transfer_ed25519.public_key).decode()
},
'uri': transfer_condition_uri,
},
'public_keys': (alice.public_key,),
}
# The yet to be fulfilled input:
transfer_input_ = {
'fulfillment': None,
'fulfills': {
'transaction_id': creation_tx_id,
'output_index': 0
},
'owners_before': (alice.public_key, bob.public_key, carol.public_key),
}
# Assemble the handcrafted transaction
handcrafted_transfer_tx = {
'operation': 'TRANSFER',
'asset': {'id': creation_tx_id},
'metadata': None,
'outputs': (transfer_output,),
'inputs': (transfer_input_,),
'version': '2.0',
'id': None,
}
# Create sha3-256 of message to sign
message = json.dumps(
handcrafted_transfer_tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
)
message = sha3.sha3_256(message.encode())
message.update('{}{}'.format(
handcrafted_transfer_tx['inputs'][0]['fulfills']['transaction_id'],
handcrafted_transfer_tx['inputs'][0]['fulfills']['output_index']).encode())
# Sign message with Alice's und Bob's private key
bob_transfer_ed25519.sign(message.digest(), base58.b58decode(bob.private_key))
carol_transfer_ed25519.sign(message.digest(), base58.b58decode(carol.private_key))
sub_fulfillment_threshold = ThresholdSha256(2)
sub_fulfillment_threshold.add_subfulfillment(bob_transfer_ed25519)
sub_fulfillment_threshold.add_subfulfillment(carol_transfer_ed25519)
# Create fulfillment and add uri to inputs
fulfillment_threshold = ThresholdSha256(1)
fulfillment_threshold.add_subcondition(alice_transfer_ed25519.condition)
fulfillment_threshold.add_subfulfillment(sub_fulfillment_threshold)
fulfillment_uri = fulfillment_threshold.serialize_uri()
handcrafted_transfer_tx['inputs'][0]['fulfillment'] = fulfillment_uri
# Create tx_id for handcrafted_dw_tx and send tx commit
json_str_tx = json.dumps(
handcrafted_transfer_tx,
sort_keys=True,
separators=(',', ':'),
ensure_ascii=False,
)
transfer_tx_id = sha3.sha3_256(json_str_tx.encode()).hexdigest()
handcrafted_transfer_tx['id'] = transfer_tx_id
pm.transactions.send_commit(handcrafted_transfer_tx)
time.sleep(1)
# Assert that the tx is propagated to all nodes
hosts.assert_transaction(transfer_tx_id)

View File

@ -0,0 +1,84 @@
# 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
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)
# CRYPTO-CONDITIONS: generate the condition uri
condition_uri = zenSha.condition.serialize_uri()
# CRYPTO-CONDITIONS: construct an unsigned fulfillment dictionary
unsigned_fulfillment_dict = {
'type': zenSha.TYPE_NAME,
'script': fulfill_script_zencode,
'keys': zen_public_keys,
}
output = {
'amount': '1000',
'condition': {
'details': unsigned_fulfillment_dict,
'uri': condition_uri,
},
'data': zenroom_data,
'script': fulfill_script_zencode,
'conf': '',
'public_keys': (zen_public_keys['Alice']['ecdh_public_key'], ),
}
input_ = {
'fulfillment': None,
'fulfills': None,
'owners_before': (zen_public_keys['Alice']['ecdh_public_key'], ),
}
token_creation_tx = {
'operation': 'CREATE',
'asset': zenroom_house_assets,
'metadata': None,
'outputs': (output,),
'inputs': (input_,),
'version': '2.0',
'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,
)
try:
assert(not zenSha.validate(message=message))
except JSONDecodeError:
pass
except ValueError:
pass
message = zenSha.sign(message, condition_script_zencode, alice)
assert(zenSha.validate(message=message))

View File

@ -0,0 +1,17 @@
#!/bin/bash
# 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
# MongoDB configuration
[ "$(stat -c %U /data/db)" = mongodb ] || chown -R mongodb /data/db
# Planetmint configuration
/usr/src/app/scripts/planetmint-monit-config
nohup mongod --bind_ip_all > "$HOME/.planetmint-monit/logs/mongodb_log_$(date +%Y%m%d_%H%M%S)" 2>&1 &
# Start services
monit -d 5 -I -B

View File

@ -0,0 +1,11 @@
#!/bin/sh
# 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
rm /shared/hostnames
rm /shared/lock
rm /shared/*node_id
rm /shared/*.json
rm /shared/id_rsa.pub

81
integration/scripts/election.sh Executable file
View File

@ -0,0 +1,81 @@
#!/bin/bash
# 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
# Show tendermint node id
show_id () {
tendermint --home=/tendermint show_node_id | tail -n 1
}
# Show validator public key
show_validator () {
tendermint --home=/tendermint show_validator | tail -n 1
}
# Elect new voting power for node
elect_validator () {
planetmint election new upsert-validator $1 $2 $3 --private-key /tendermint/config/priv_validator_key.json 2>&1
}
# Propose new chain migration
propose_migration () {
planetmint election new chain-migration --private-key /tendermint/config/priv_validator_key.json 2>&1
}
# Show election state
show_election () {
planetmint election show $1 2>&1
}
# Approve election
approve_validator () {
planetmint election approve $1 --private-key /tendermint/config/priv_validator_key.json
}
# Fetch tendermint id and pubkey and create upsert proposal
elect () {
node_id=$(show_id)
validator_pubkey=$(show_validator | jq -r .value)
proposal=$(elect_validator $validator_pubkey $1 $node_id | grep SUCCESS)
echo ${proposal##* }
}
# Create chain migration proposal and return election id
migrate () {
proposal=$(propose_migration | grep SUCCESS)
echo ${proposal##* }
}
usage () {
echo "usage: TODO"
}
while [ "$1" != "" ]; do
case $1 in
show_id ) show_id
;;
show_validator ) show_validator
;;
elect ) shift
elect $1
;;
migrate ) shift
migrate
;;
show_election ) shift
show_election $1
;;
approve ) shift
approve_validator $1
;;
* ) usage
exit 1
esac
shift
done
exitcode=$?
exit $exitcode

33
integration/scripts/genesis.py Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env python3
# 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 json
import sys
def edit_genesis() -> None:
file_names = sys.argv[1:]
validators = []
for file_name in file_names:
file = open(file_name)
genesis = json.load(file)
validators.extend(genesis['validators'])
file.close()
genesis_file = open(file_names[0])
genesis_json = json.load(genesis_file)
genesis_json['validators'] = validators
genesis_file.close()
with open('/shared/genesis.json', 'w') as f:
json.dump(genesis_json, f, indent=True)
return None
if __name__ == '__main__':
edit_genesis()

View File

@ -0,0 +1,208 @@
#!/bin/bash
# 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
set -o nounset
# Check if directory for monit logs exists
if [ ! -d "$HOME/.planetmint-monit" ]; then
mkdir -p "$HOME/.planetmint-monit"
fi
monit_pid_path=${MONIT_PID_PATH:=$HOME/.planetmint-monit/monit_processes}
monit_script_path=${MONIT_SCRIPT_PATH:=$HOME/.planetmint-monit/monit_script}
monit_log_path=${MONIT_LOG_PATH:=$HOME/.planetmint-monit/logs}
monitrc_path=${MONITRC_PATH:=$HOME/.monitrc}
function usage() {
cat <<EOM
Usage: ${0##*/} [-h]
Configure Monit for Planetmint and Tendermint process management.
ENV[MONIT_PID_PATH] || --monit-pid-path PATH
Absolute path to directory where the the program's pid-file will reside.
The pid-file contains the ID(s) of the process(es). (default: ${monit_pid_path})
ENV[MONIT_SCRIPT_PATH] || --monit-script-path PATH
Absolute path to the directory where the executable program or
script is present. (default: ${monit_script_path})
ENV[MONIT_LOG_PATH] || --monit-log-path PATH
Absolute path to the directory where all the logs for processes
monitored by Monit are stored. (default: ${monit_log_path})
ENV[MONITRC_PATH] || --monitrc-path PATH
Absolute path to the monit control file(monitrc). (default: ${monitrc_path})
-h|--help
Show this help and exit.
EOM
}
while [[ $# -gt 0 ]]; do
arg="$1"
case $arg in
--monit-pid-path)
monit_pid_path="$2"
shift
;;
--monit-script-path)
monit_script_path="$2"
shift
;;
--monit-log-path)
monit_log_path="$2"
shift
;;
--monitrc-path)
monitrc_path="$2"
shift
;;
-h | --help)
usage
exit
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
shift
done
# Check if directory for monit logs exists
if [ ! -d "$monit_log_path" ]; then
mkdir -p "$monit_log_path"
fi
# Check if directory for monit pid files exists
if [ ! -d "$monit_pid_path" ]; then
mkdir -p "$monit_pid_path"
fi
cat >${monit_script_path} <<EOF
#!/bin/bash
case \$1 in
start_planetmint)
pushd \$4
nohup planetmint start > /dev/null 2>&1 &
echo \$! > \$2
popd
;;
stop_planetmint)
kill -2 \`cat \$2\`
rm -f \$2
;;
start_tendermint)
pushd \$4
nohup tendermint node \
--p2p.laddr "tcp://0.0.0.0:26656" \
--rpc.laddr "tcp://0.0.0.0:26657" \
--proxy_app="tcp://0.0.0.0:26658" \
--consensus.create_empty_blocks=false \
--p2p.pex=false >> \$3/tendermint.out.log 2>> \$3/tendermint.err.log &
echo \$! > \$2
popd
;;
stop_tendermint)
kill -2 \`cat \$2\`
rm -f \$2
;;
esac
exit 0
EOF
chmod +x ${monit_script_path}
cat >${monit_script_path}_logrotate <<EOF
#!/bin/bash
case \$1 in
rotate_tendermint_logs)
/bin/cp \$2 \$2.\$(date +%y-%m-%d)
/bin/tar -cvf \$2.\$(date +%Y%m%d_%H%M%S).tar.gz \$2.\$(date +%y-%m-%d)
/bin/rm \$2.\$(date +%y-%m-%d)
/bin/cp /dev/null \$2
;;
esac
exit 0
EOF
chmod +x ${monit_script_path}_logrotate
# Handling overwriting of control file interactively
if [ -f "$monitrc_path" ]; then
echo "$monitrc_path already exists."
read -p "Overwrite[Y]? " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
echo "Overriding $monitrc_path"
else
read -p "Enter absolute path to store Monit control file: " monitrc_path
eval monitrc_path="$monitrc_path"
if [ ! -d "$(dirname $monitrc_path)" ]; then
echo "Failed to save monit control file '$monitrc_path': No such file or directory."
exit 1
fi
fi
fi
# configure monitrc
cat >${monitrc_path} <<EOF
set httpd
port 2812
allow localhost
check process planetmint
with pidfile ${monit_pid_path}/planetmint.pid
start program "${monit_script_path} start_planetmint $monit_pid_path/planetmint.pid ${monit_log_path} ${monit_log_path}"
restart program "${monit_script_path} start_planetmint $monit_pid_path/planetmint.pid ${monit_log_path} ${monit_log_path}"
stop program "${monit_script_path} stop_planetmint $monit_pid_path/planetmint.pid ${monit_log_path} ${monit_log_path}"
check process tendermint
with pidfile ${monit_pid_path}/tendermint.pid
start program "${monit_script_path} start_tendermint ${monit_pid_path}/tendermint.pid ${monit_log_path} ${monit_log_path}"
restart program "${monit_script_path} start_tendermint ${monit_pid_path}/tendermint.pid ${monit_log_path} ${monit_log_path}"
stop program "${monit_script_path} stop_tendermint ${monit_pid_path}/tendermint.pid ${monit_log_path} ${monit_log_path}"
depends on planetmint
check file tendermint.out.log with path ${monit_log_path}/tendermint.out.log
if size > 200 MB then
exec "${monit_script_path}_logrotate rotate_tendermint_logs ${monit_log_path}/tendermint.out.log $monit_pid_path/tendermint.pid"
check file tendermint.err.log with path ${monit_log_path}/tendermint.err.log
if size > 200 MB then
exec "${monit_script_path}_logrotate rotate_tendermint_logs ${monit_log_path}/tendermint.err.log $monit_pid_path/tendermint.pid"
EOF
# Setting permissions for control file
chmod 0700 ${monitrc_path}
echo -e "Planetmint process manager configured!"
set -o errexit

View File

@ -0,0 +1,83 @@
#!/bin/bash
# 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
# Write hostname to list
echo $(hostname) >> /shared/hostnames
# Create ssh folder
mkdir ~/.ssh
# Wait for test container pubkey
while [ ! -f /shared/id_rsa.pub ]; do
echo "WAIT FOR PUBKEY"
sleep 1
done
# Add pubkey to authorized keys
cat /shared/id_rsa.pub > ~/.ssh/authorized_keys
# Allow root user login
sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/" /etc/ssh/sshd_config
# Restart ssh service
service ssh restart
# Tendermint configuration
tendermint init
# Write node id to shared folder
HOSTNAME=$(hostname)
NODE_ID=$(tendermint show_node_id | tail -n 1)
echo $NODE_ID > /shared/${HOSTNAME}_node_id
# Wait for other node ids
FILES=()
while [ ! ${#FILES[@]} == $SCALE ]; do
echo "WAIT FOR NODE IDS"
sleep 1
FILES=(/shared/*node_id)
done
# Write node ids to persistent peers
PEERS="persistent_peers = \""
for f in ${FILES[@]}; do
ID=$(cat $f)
HOST=$(echo $f | cut -c 9-20)
if [ ! $HOST == $HOSTNAME ]; then
PEERS+="${ID}@${HOST}:26656, "
fi
done
PEERS=$(echo $PEERS | rev | cut -c 2- | rev)
PEERS+="\""
sed -i "/persistent_peers = \"\"/c\\${PEERS}" /tendermint/config/config.toml
# Copy genesis.json to shared folder
cp /tendermint/config/genesis.json /shared/${HOSTNAME}_genesis.json
# Await config file of all services to be present
FILES=()
while [ ! ${#FILES[@]} == $SCALE ]; do
echo "WAIT FOR GENESIS FILES"
sleep 1
FILES=(/shared/*_genesis.json)
done
# Create genesis.json for nodes
if [ ! -f /shared/lock ]; then
echo LOCKING
touch /shared/lock
/usr/src/app/scripts/genesis.py ${FILES[@]}
fi
while [ ! -f /shared/genesis.json ]; do
echo "WAIT FOR GENESIS"
sleep 1
done
# Copy genesis.json to tendermint config
cp /shared/genesis.json /tendermint/config/genesis.json
exec "$@"

View File

@ -0,0 +1,16 @@
#!/bin/bash
# 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
# Create ssh folder
mkdir ~/.ssh
# Create ssh keys
ssh-keygen -q -t rsa -N '' -f ~/.ssh/id_rsa
# Publish pubkey to shared folder
cp ~/.ssh/id_rsa.pub /shared
exec "$@"

24
integration/scripts/test.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
# 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
# Start CLI Tests
# Test upsert new validator
/tests/upsert-new-validator.sh
# Test chain migration
# TODO: implementation not finished
#/tests/chain-migration.sh
# TODO: Implement test for voting edge cases or implicit in chain migration and upsert validator?
exitcode=$?
if [ $exitcode -ne 0 ]; then
exit $exitcode
fi
exec "$@"

View File

@ -0,0 +1,29 @@
#!/bin/bash
# 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
# Only continue if all services are ready
HOSTNAMES=()
while [ ! ${#HOSTNAMES[@]} == $SCALE ]; do
echo "WAIT FOR HOSTNAMES"
sleep 1
readarray -t HOSTNAMES < /shared/hostnames
done
for host in ${HOSTNAMES[@]}; do
while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' $host:9984)" != "200" ]]; do
echo "WAIT FOR PLANETMINT $host"
sleep 1
done
done
for host in ${HOSTNAMES[@]}; do
while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' $host:26657)" != "200" ]]; do
echo "WAIT FOR TENDERMINT $host"
sleep 1
done
done
exec "$@"

View File

@ -1,4 +1,4 @@
FROM tendermint/tendermint:v0.31.5 FROM tendermint/tendermint:v0.34.15
LABEL maintainer "contact@ipdb.global" LABEL maintainer "contact@ipdb.global"
WORKDIR / WORKDIR /
USER root USER root

View File

@ -16,4 +16,5 @@ nohup mongod --bind_ip_all > "$HOME/.planetmint-monit/logs/mongodb_log_$(date +%
# Tendermint configuration # Tendermint configuration
tendermint init tendermint init
monit -d 5 -I -B # Start services
monit -d 5 -I -B

View File

@ -17,7 +17,7 @@ stack_size=${STACK_SIZE:=4}
stack_type=${STACK_TYPE:="docker"} stack_type=${STACK_TYPE:="docker"}
stack_type_provider=${STACK_TYPE_PROVIDER:=""} stack_type_provider=${STACK_TYPE_PROVIDER:=""}
# NOTE versions prior v0.28.0 have different priv_validator format! # NOTE versions prior v0.28.0 have different priv_validator format!
tm_version=${TM_VERSION:="v0.31.5"} tm_version=${TM_VERSION:="v0.34.15"}
mongo_version=${MONGO_VERSION:="3.6"} mongo_version=${MONGO_VERSION:="3.6"}
stack_vm_memory=${STACK_VM_MEMORY:=2048} stack_vm_memory=${STACK_VM_MEMORY:=2048}
stack_vm_cpus=${STACK_VM_CPUS:=2} stack_vm_cpus=${STACK_VM_CPUS:=2}

View File

@ -3,14 +3,15 @@
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
from planetmint.common.transaction import Transaction # noqa from planetmint.transactions.common.transaction import Transaction # noqa
from planetmint import models # noqa from planetmint import models # noqa
from planetmint.upsert_validator import ValidatorElection # noqa from planetmint.upsert_validator import ValidatorElection # noqa
from planetmint.elections.vote import Vote # noqa from planetmint.transactions.types.elections.vote import Vote # noqa
from planetmint.migrations.chain_migration_election import ChainMigrationElection from planetmint.migrations.chain_migration_election import ChainMigrationElection
from planetmint.lib import Planetmint from planetmint.lib import Planetmint
from planetmint.core import App from planetmint.core import App
Transaction.register_type(Transaction.CREATE, models.Transaction) Transaction.register_type(Transaction.CREATE, models.Transaction)
Transaction.register_type(Transaction.TRANSFER, models.Transaction) Transaction.register_type(Transaction.TRANSFER, models.Transaction)
Transaction.register_type(ValidatorElection.OPERATION, ValidatorElection) Transaction.register_type(ValidatorElection.OPERATION, ValidatorElection)

View File

@ -6,7 +6,9 @@
import logging import logging
from importlib import import_module from importlib import import_module
from planetmint.config import Config from planetmint.config import Config
from planetmint.common.exceptions import ConfigurationError from planetmint.backend.exceptions import ConnectionError
from planetmint.transactions.common.exceptions import ConfigurationError
BACKENDS = { # This is path to MongoDBClass BACKENDS = { # This is path to MongoDBClass
'tarantool_db': 'planetmint.backend.tarantool.connection.TarantoolDB', 'tarantool_db': 'planetmint.backend.tarantool.connection.TarantoolDB',

View File

@ -13,7 +13,7 @@ import os
from planetmint.backend.exceptions import ConnectionError from planetmint.backend.exceptions import ConnectionError
from planetmint.backend.utils import get_planetmint_config_value, get_planetmint_config_value_or_key_error from planetmint.backend.utils import get_planetmint_config_value, get_planetmint_config_value_or_key_error
from planetmint.common.exceptions import ConfigurationError from planetmint.transactions.common.exceptions import ConfigurationError
BACKENDS = { # This is path to MongoDBClass BACKENDS = { # This is path to MongoDBClass
'tarantool': 'planetmint.backend.connection_tarantool.TarantoolDB', 'tarantool': 'planetmint.backend.connection_tarantool.TarantoolDB',

View File

@ -11,7 +11,7 @@ from planetmint.config import Config
from planetmint.backend.exceptions import (DuplicateKeyError, from planetmint.backend.exceptions import (DuplicateKeyError,
OperationError, OperationError,
ConnectionError) ConnectionError)
from planetmint.common.exceptions import ConfigurationError from planetmint.transactions.common.exceptions import ConfigurationError
from planetmint.utils import Lazy from planetmint.utils import Lazy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -12,7 +12,7 @@ from planetmint import backend
from planetmint.backend.exceptions import DuplicateKeyError from planetmint.backend.exceptions import DuplicateKeyError
from planetmint.backend.utils import module_dispatch_registrar from planetmint.backend.utils import module_dispatch_registrar
from planetmint.backend.localmongodb.connection import LocalMongoDBConnection from planetmint.backend.localmongodb.connection import LocalMongoDBConnection
from planetmint.common.transaction import Transaction from planetmint.transactions.common.transaction import Transaction
register_query = module_dispatch_registrar(backend.query) register_query = module_dispatch_registrar(backend.query)

View File

@ -10,8 +10,9 @@ import logging
from planetmint.config import Config from planetmint.config import Config
from planetmint.backend.connection import Connection from planetmint.backend.connection import Connection
from planetmint.common.exceptions import ValidationError from planetmint.transactions.common.exceptions import ValidationError
from planetmint.common.utils import validate_all_values_for_key_in_obj, validate_all_values_for_key_in_list from planetmint.transactions.common.utils import (
validate_all_values_for_key_in_obj, validate_all_values_for_key_in_list)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -7,7 +7,7 @@ import logging
import tarantool import tarantool
from planetmint.config import Config from planetmint.config import Config
from planetmint.common.exceptions import ConfigurationError from planetmint.transactions.common.exceptions import ConfigurationError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -1,6 +1,6 @@
from secrets import token_hex from secrets import token_hex
import copy import copy
from planetmint.common.memoize import HDict from planetmint.transactions.common.memoize import HDict
def get_items(_list): def get_items(_list):

View File

@ -18,10 +18,10 @@ from planetmint.backend.tarantool.connection import TarantoolDB
from planetmint.core import rollback from planetmint.core import rollback
from planetmint.migrations.chain_migration_election import ChainMigrationElection from planetmint.migrations.chain_migration_election import ChainMigrationElection
from planetmint.utils import load_node_key from planetmint.utils import load_node_key
from planetmint.common.transaction_mode_types import BROADCAST_TX_COMMIT from planetmint.transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT
from planetmint.common.exceptions import (DatabaseDoesNotExist, from planetmint.transactions.common.exceptions import (
ValidationError) DatabaseDoesNotExist, ValidationError)
from planetmint.elections.vote import Vote from planetmint.transactions.types.elections.vote import Vote
import planetmint import planetmint
from planetmint import (backend, ValidatorElection, from planetmint import (backend, ValidatorElection,
Planetmint) Planetmint)

View File

@ -24,8 +24,8 @@ import collections.abc
from functools import lru_cache from functools import lru_cache
from pkg_resources import iter_entry_points, ResolutionError from pkg_resources import iter_entry_points, ResolutionError
from planetmint.common import exceptions
from planetmint.config import Config from planetmint.config import Config
from planetmint.transactions.common import exceptions
from planetmint.validation import BaseValidationRules from planetmint.validation import BaseValidationRules
# TODO: move this to a proper configuration file for logging # TODO: move this to a proper configuration file for logging

View File

@ -8,14 +8,20 @@ with Tendermint.
""" """
import logging import logging
import sys import sys
from tendermint.abci import types_pb2
from abci.application import BaseApplication from abci.application import BaseApplication
from abci import CodeTypeOk from abci.application import OkCode
from tendermint.abci.types_pb2 import (
ResponseInfo,
ResponseInitChain,
ResponseCheckTx,
ResponseDeliverTx,
ResponseBeginBlock,
ResponseEndBlock,
ResponseCommit
)
from planetmint import Planetmint from planetmint import Planetmint
from planetmint.elections.election import Election from planetmint.transactions.types.elections.election import Election
from planetmint.version import __tm_supported_versions__
from planetmint.utils import tendermint_version_is_compatible
from planetmint.tendermint_utils import (decode_transaction, from planetmint.tendermint_utils import (decode_transaction,
calculate_hash) calculate_hash)
from planetmint.lib import Block from planetmint.lib import Block
@ -34,39 +40,36 @@ class App(BaseApplication):
transaction logic to Tendermint Core. transaction logic to Tendermint Core.
""" """
def __init__(self, abci, planetmint=None, events_queue=None,): def __init__(self, planetmint_node=None, events_queue=None):
super().__init__(abci) # super().__init__(abci)
logger.debug('Checking values of types')
logger.debug(dir(types_pb2))
self.events_queue = events_queue self.events_queue = events_queue
self.planetmint = planetmint or Planetmint() self.planetmint_node = planetmint_node or Planetmint()
self.block_txn_ids = [] self.block_txn_ids = []
self.block_txn_hash = '' self.block_txn_hash = ''
self.block_transactions = [] self.block_transactions = []
self.validators = None self.validators = None
self.new_height = None self.new_height = None
self.chain = self.planetmint.get_latest_abci_chain() self.chain = self.planetmint_node.get_latest_abci_chain()
print( f"chain: {self.chain}")
def log_abci_migration_error(self, chain_id, validators): def log_abci_migration_error(self, chain_id, validators):
logger.error('An ABCI chain migration is in process. ' logger.error('An ABCI chain migration is in process. '
'Download the new ABCI client and configure it with ' 'Download theself.planetmint_node.get_latest_abci_chain new ABCI client and configure it with '
f'chain_id={chain_id} and validators={validators}.') f'chain_id={chain_id} and validators={validators}.')
def abort_if_abci_chain_is_not_synced(self): def abort_if_abci_chain_is_not_synced(self):
if self.chain is None or self.chain['is_synced']: if self.chain is None or self.chain['is_synced']:
return return
validators = self.planetmint_node.get_validators()
validators = self.planetmint.get_validators()
self.log_abci_migration_error(self.chain['chain_id'], validators) self.log_abci_migration_error(self.chain['chain_id'], validators)
sys.exit(1) sys.exit(1)
def init_chain(self, genesis): def init_chain(self, genesis):
"""Initialize chain upon genesis or a migration""" """Initialize chain upon genesis or a migration"""
app_hash = '' app_hash = ''
height = 0 height = 0
known_chain = self.planetmint_node.get_latest_abci_chain()
known_chain = self.planetmint.get_latest_abci_chain()
print( f" known_chain: {known_chain}")
if known_chain is not None: if known_chain is not None:
chain_id = known_chain['chain_id'] chain_id = known_chain['chain_id']
@ -75,35 +78,29 @@ class App(BaseApplication):
f'the chain {chain_id} is already synced.') f'the chain {chain_id} is already synced.')
logger.error(msg) logger.error(msg)
sys.exit(1) sys.exit(1)
if chain_id != genesis.chain_id: if chain_id != genesis.chain_id:
validators = self.planetmint.get_validators() validators = self.planetmint_node.get_validators()
self.log_abci_migration_error(chain_id, validators) self.log_abci_migration_error(chain_id, validators)
sys.exit(1) sys.exit(1)
# set migration values for app hash and height # set migration values for app hash and height
block = self.planetmint.get_latest_block() block = self.planetmint_node.get_latest_block()
app_hash = '' if block is None else block['app_hash'] app_hash = '' if block is None else block['app_hash']
height = 0 if block is None else block['height'] + 1 height = 0 if block is None else block['height'] + 1
known_validators = self.planetmint_node.get_validators()
known_validators = self.planetmint.get_validators()
validator_set = [vutils.decode_validator(v) validator_set = [vutils.decode_validator(v)
for v in genesis.validators] for v in genesis.validators]
if known_validators and known_validators != validator_set: if known_validators and known_validators != validator_set:
self.log_abci_migration_error(known_chain['chain_id'], self.log_abci_migration_error(known_chain['chain_id'],
known_validators) known_validators)
sys.exit(1) sys.exit(1)
block = Block(app_hash=app_hash, height=height, transactions=[]) block = Block(app_hash=app_hash, height=height, transactions=[])
self.planetmint.store_block(block._asdict()) self.planetmint_node.store_block(block._asdict())
self.planetmint.store_validator_set(height + 1, validator_set) self.planetmint_node.store_validator_set(height + 1, validator_set)
abci_chain_height = 0 if known_chain is None else known_chain['height'] abci_chain_height = 0 if known_chain is None else known_chain['height']
self.planetmint.store_abci_chain(abci_chain_height, self.planetmint_node.store_abci_chain(abci_chain_height, genesis.chain_id, True)
genesis.chain_id, True)
self.chain = {'height': abci_chain_height, 'is_synced': True, self.chain = {'height': abci_chain_height, 'is_synced': True,
'chain_id': genesis.chain_id} 'chain_id': genesis.chain_id}
return self.abci.ResponseInitChain() return ResponseInitChain()
def info(self, request): def info(self, request):
"""Return height of the latest committed block.""" """Return height of the latest committed block."""
@ -111,15 +108,15 @@ class App(BaseApplication):
self.abort_if_abci_chain_is_not_synced() self.abort_if_abci_chain_is_not_synced()
# Check if Planetmint supports the Tendermint version # Check if Planetmint supports the Tendermint version
if not (hasattr(request, 'version') and tendermint_version_is_compatible(request.version)): # if not (hasattr(request, 'version') and tendermint_version_is_compatible(request.version)):
logger.error(f'Unsupported Tendermint version: {getattr(request, "version", "no version")}.' # logger.error(f'Unsupported Tendermint version: {getattr(request, "version", "no version")}.'
f' Currently, Planetmint only supports {__tm_supported_versions__}. Exiting!') # f' Currently, Planetmint only supports {__tm_supported_versions__}. Exiting!')
sys.exit(1) # sys.exit(1)
logger.info(f"Tendermint version: {request.version}") # logger.info(f"Tendermint version: {request.version}")
r = self.abci.ResponseInfo() r = ResponseInfo()
block = self.planetmint.get_latest_block() block = self.planetmint_node.get_latest_block()
if block: if block:
chain_shift = 0 if self.chain is None else self.chain['height'] chain_shift = 0 if self.chain is None else self.chain['height']
r.last_block_height = block['height'] - chain_shift r.last_block_height = block['height'] - chain_shift
@ -141,12 +138,12 @@ class App(BaseApplication):
logger.debug('check_tx: %s', raw_transaction) logger.debug('check_tx: %s', raw_transaction)
transaction = decode_transaction(raw_transaction) transaction = decode_transaction(raw_transaction)
if self.planetmint.is_valid_transaction(transaction): if self.planetmint_node.is_valid_transaction(transaction):
logger.debug('check_tx: VALID') logger.debug('check_tx: VALID')
return self.abci.ResponseCheckTx(code=CodeTypeOk) return ResponseCheckTx(code=OkCode)
else: else:
logger.debug('check_tx: INVALID') logger.debug('check_tx: INVALID')
return self.abci.ResponseCheckTx(code=CodeTypeError) return ResponseCheckTx(code=CodeTypeError)
def begin_block(self, req_begin_block): def begin_block(self, req_begin_block):
"""Initialize list of transaction. """Initialize list of transaction.
@ -157,13 +154,13 @@ class App(BaseApplication):
self.abort_if_abci_chain_is_not_synced() self.abort_if_abci_chain_is_not_synced()
chain_shift = 0 if self.chain is None else self.chain['height'] chain_shift = 0 if self.chain is None else self.chain['height']
logger.debug('BEGIN BLOCK, height:%s, num_txs:%s', # req_begin_block.header.num_txs not found, so removing it.
req_begin_block.header.height + chain_shift, logger.debug('BEGIN BLOCK, height:%s',
req_begin_block.header.num_txs) req_begin_block.header.height + chain_shift)
self.block_txn_ids = [] self.block_txn_ids = []
self.block_transactions = [] self.block_transactions = []
return self.abci.ResponseBeginBlock() return ResponseBeginBlock()
def deliver_tx(self, raw_transaction): def deliver_tx(self, raw_transaction):
"""Validate the transaction before mutating the state. """Validate the transaction before mutating the state.
@ -175,17 +172,17 @@ class App(BaseApplication):
self.abort_if_abci_chain_is_not_synced() self.abort_if_abci_chain_is_not_synced()
logger.debug('deliver_tx: %s', raw_transaction) logger.debug('deliver_tx: %s', raw_transaction)
transaction = self.planetmint.is_valid_transaction( transaction = self.planetmint_node.is_valid_transaction(
decode_transaction(raw_transaction), self.block_transactions) decode_transaction(raw_transaction), self.block_transactions)
if not transaction: if not transaction:
logger.debug('deliver_tx: INVALID') logger.debug('deliver_tx: INVALID')
return self.abci.ResponseDeliverTx(code=CodeTypeError) return ResponseDeliverTx(code=CodeTypeError)
else: else:
logger.debug('storing tx') logger.debug('storing tx')
self.block_txn_ids.append(transaction.id) self.block_txn_ids.append(transaction.id)
self.block_transactions.append(transaction) self.block_transactions.append(transaction)
return self.abci.ResponseDeliverTx(code=CodeTypeOk) return ResponseDeliverTx(code=OkCode)
def end_block(self, request_end_block): def end_block(self, request_end_block):
"""Calculate block hash using transaction ids and previous block """Calculate block hash using transaction ids and previous block
@ -207,21 +204,21 @@ class App(BaseApplication):
logger.debug(f'Updating pre-commit state: {self.new_height}') logger.debug(f'Updating pre-commit state: {self.new_height}')
pre_commit_state = dict(height=self.new_height, pre_commit_state = dict(height=self.new_height,
transactions=self.block_txn_ids) transactions=self.block_txn_ids)
self.planetmint.store_pre_commit_state(pre_commit_state) self.planetmint_node.store_pre_commit_state(pre_commit_state)
block_txn_hash = calculate_hash(self.block_txn_ids) block_txn_hash = calculate_hash(self.block_txn_ids)
block = self.planetmint.get_latest_block() block = self.planetmint_node.get_latest_block()
if self.block_txn_ids: if self.block_txn_ids:
self.block_txn_hash = calculate_hash([block['app_hash'], block_txn_hash]) self.block_txn_hash = calculate_hash([block['app_hash'], block_txn_hash])
else: else:
self.block_txn_hash = block['app_hash'] self.block_txn_hash = block['app_hash']
validator_update = Election.process_block(self.planetmint, validator_update = Election.process_block(self.planetmint_node,
self.new_height, self.new_height,
self.block_transactions) self.block_transactions)
return self.abci.ResponseEndBlock(validator_updates=validator_update) return ResponseEndBlock(validator_updates=validator_update)
def commit(self): def commit(self):
"""Store the new height and along with block hash.""" """Store the new height and along with block hash."""
@ -232,14 +229,14 @@ class App(BaseApplication):
# register a new block only when new transactions are received # register a new block only when new transactions are received
if self.block_txn_ids: if self.block_txn_ids:
self.planetmint.store_bulk_transactions(self.block_transactions) self.planetmint_node.store_bulk_transactions(self.block_transactions)
block = Block(app_hash=self.block_txn_hash, block = Block(app_hash=self.block_txn_hash,
height=self.new_height, height=self.new_height,
transactions=self.block_txn_ids) transactions=self.block_txn_ids)
# NOTE: storing the block should be the last operation during commit # NOTE: storing the block should be the last operation during commit
# this effects crash recovery. Refer BEP#8 for details # this effects crash recovery. Refer BEP#8 for details
self.planetmint.store_block(block._asdict()) self.planetmint_node.store_block(block._asdict())
logger.debug('Commit-ing new block with hash: apphash=%s ,' logger.debug('Commit-ing new block with hash: apphash=%s ,'
'height=%s, txn ids=%s', data, self.new_height, 'height=%s, txn ids=%s', data, self.new_height,
@ -252,7 +249,7 @@ class App(BaseApplication):
}) })
self.events_queue.put(event) self.events_queue.put(event)
return self.abci.ResponseCommit(data=data) return ResponseCommit(data=data)
def rollback(b): def rollback(b):

View File

@ -5,7 +5,7 @@
from planetmint.utils import condition_details_has_owner from planetmint.utils import condition_details_has_owner
from planetmint.backend import query from planetmint.backend import query
from planetmint.common.transaction import TransactionLink from planetmint.transactions.common.transaction import TransactionLink
class FastQuery(): class FastQuery():

View File

@ -25,30 +25,27 @@ import planetmint
from planetmint.config import Config from planetmint.config import Config
from planetmint import backend, config_utils, fastquery from planetmint import backend, config_utils, fastquery
from planetmint.models import Transaction from planetmint.models import Transaction
from planetmint.common.exceptions import (SchemaValidationError, from planetmint.transactions.common.exceptions import (
ValidationError, SchemaValidationError, ValidationError, DoubleSpend)
DoubleSpend) from planetmint.transactions.common.transaction_mode_types import (
from planetmint.common.transaction_mode_types import (BROADCAST_TX_COMMIT, BROADCAST_TX_COMMIT, BROADCAST_TX_ASYNC, BROADCAST_TX_SYNC)
BROADCAST_TX_ASYNC,
BROADCAST_TX_SYNC)
from planetmint.tendermint_utils import encode_transaction, merkleroot from planetmint.tendermint_utils import encode_transaction, merkleroot
from planetmint import exceptions as core_exceptions from planetmint import exceptions as core_exceptions
from planetmint.validation import BaseValidationRules from planetmint.validation import BaseValidationRules
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Planetmint(object): class Planetmint(object):
"""Bigchain API """Planetmint API
Create, read, sign, write transactions to the database Create, read, sign, write transactions to the database
""" """
def __init__(self, connection=None): def __init__(self, connection=None):
"""Initialize the Bigchain instance """Initialize the Planetmint instance
A Bigchain instance has several configuration parameters (e.g. host). A Planetmint instance has several configuration parameters (e.g. host).
If a parameter value is passed as an argument to the Bigchain If a parameter value is passed as an argument to the Planetmint
__init__ method, then that is the value it will have. __init__ method, then that is the value it will have.
Otherwise, the parameter value will come from an environment variable. Otherwise, the parameter value will come from an environment variable.
If that environment variable isn't set, then the value If that environment variable isn't set, then the value

View File

@ -6,7 +6,7 @@
import planetmint import planetmint
import logging import logging
from planetmint.common.exceptions import ConfigurationError from planetmint.transactions.common.exceptions import ConfigurationError
from logging.config import dictConfig as set_logging_config from logging.config import dictConfig as set_logging_config
from planetmint.config import Config, DEFAULT_LOGGING_CONFIG from planetmint.config import Config, DEFAULT_LOGGING_CONFIG
import os import os

View File

@ -1,7 +1,7 @@
import json import json
from planetmint.common.schema import TX_SCHEMA_CHAIN_MIGRATION_ELECTION from planetmint.transactions.common.schema import TX_SCHEMA_CHAIN_MIGRATION_ELECTION
from planetmint.elections.election import Election from planetmint.transactions.types.elections.election import Election
class ChainMigrationElection(Election): class ChainMigrationElection(Election):

View File

@ -4,11 +4,10 @@
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
from planetmint.backend.schema import validate_language_key from planetmint.backend.schema import validate_language_key
from planetmint.common.exceptions import (InvalidSignature, from planetmint.transactions.common.exceptions import (InvalidSignature, DuplicateTransaction)
DuplicateTransaction) from planetmint.transactions.common.schema import validate_transaction_schema
from planetmint.common.schema import validate_transaction_schema from planetmint.transactions.common.transaction import Transaction
from planetmint.common.transaction import Transaction from planetmint.transactions.common.utils import (validate_txn_obj, validate_key)
from planetmint.common.utils import (validate_txn_obj, validate_key)
class Transaction(Transaction): class Transaction(Transaction):

View File

@ -6,23 +6,28 @@
import multiprocessing as mp import multiprocessing as mp
from collections import defaultdict from collections import defaultdict
from planetmint import App, Planetmint from planetmint import App
from planetmint.lib import Planetmint
from planetmint.tendermint_utils import decode_transaction from planetmint.tendermint_utils import decode_transaction
from abci import CodeTypeOk from abci.application import OkCode
from tendermint.abci.types_pb2 import (
ResponseCheckTx,
ResponseDeliverTx,
)
class ParallelValidationApp(App): class ParallelValidationApp(App):
def __init__(self, planetmint=None, events_queue=None, abci=None): def __init__(self, planetmint=None, events_queue=None):
super().__init__(planetmint, events_queue, abci=abci) super().__init__(planetmint, events_queue)
self.parallel_validator = ParallelValidator() self.parallel_validator = ParallelValidator()
self.parallel_validator.start() self.parallel_validator.start()
def check_tx(self, raw_transaction): def check_tx(self, raw_transaction):
return self.abci.ResponseCheckTx(code=CodeTypeOk) return ResponseCheckTx(code=OkCode)
def deliver_tx(self, raw_transaction): def deliver_tx(self, raw_transaction):
self.parallel_validator.validate(raw_transaction) self.parallel_validator.validate(raw_transaction)
return self.abci.ResponseDeliverTx(code=CodeTypeOk) return ResponseDeliverTx(code=OkCode)
def end_block(self, request_end_block): def end_block(self, request_end_block):
result = self.parallel_validator.result(timeout=30) result = self.parallel_validator.result(timeout=30)

View File

@ -6,8 +6,6 @@
import logging import logging
import setproctitle import setproctitle
from abci import TmVersion, ABCI
from planetmint.config import Config from planetmint.config import Config
from planetmint.lib import Planetmint from planetmint.lib import Planetmint
from planetmint.core import App from planetmint.core import App
@ -68,18 +66,15 @@ def start(args):
setproctitle.setproctitle('planetmint') setproctitle.setproctitle('planetmint')
# Start the ABCIServer # Start the ABCIServer
abci = ABCI(TmVersion(Config().get()['tendermint']['version']))
if args.experimental_parallel_validation: if args.experimental_parallel_validation:
app = ABCIServer( app = ABCIServer(
app=ParallelValidationApp( app=ParallelValidationApp(
abci=abci.types,
events_queue=exchange.get_publisher_queue(), events_queue=exchange.get_publisher_queue(),
) )
) )
else: else:
app = ABCIServer( app = ABCIServer(
app=App( app=App(
abci=abci.types,
events_queue=exchange.get_publisher_queue(), events_queue=exchange.get_publisher_queue(),
) )
) )

View File

View File

@ -26,10 +26,10 @@ def generate_key_pair():
"""Generates a cryptographic key pair. """Generates a cryptographic key pair.
Returns: Returns:
:class:`~planetmint.common.crypto.CryptoKeypair`: A :class:`~planetmint.transactions.common.crypto.CryptoKeypair`: A
:obj:`collections.namedtuple` with named fields :obj:`collections.namedtuple` with named fields
:attr:`~planetmint.common.crypto.CryptoKeypair.private_key` and :attr:`~planetmint.transactions.common.crypto.CryptoKeypair.private_key` and
:attr:`~planetmint.common.crypto.CryptoKeypair.public_key`. :attr:`~planetmint.transactions.common.crypto.CryptoKeypair.public_key`.
""" """
# TODO FOR CC: Adjust interface so that this function becomes unnecessary # TODO FOR CC: Adjust interface so that this function becomes unnecessary

View File

@ -0,0 +1,125 @@
# 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

@ -0,0 +1,208 @@
# 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 Fulfillment, ThresholdSha256, Ed25519Sha256
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:
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]
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

@ -11,37 +11,38 @@ import jsonschema
import yaml import yaml
import rapidjson import rapidjson
from planetmint.common.exceptions import SchemaValidationError from planetmint.transactions.common.exceptions import SchemaValidationError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _load_schema(name, path=__file__): def _load_schema(name, version, path=__file__):
"""Load a schema from disk""" """Load a schema from disk"""
path = os.path.join(os.path.dirname(path), name + '.yaml') path = os.path.join(os.path.dirname(path), version, name + '.yaml')
with open(path) as handle: with open(path) as handle:
schema = yaml.safe_load(handle) schema = yaml.safe_load(handle)
fast_schema = rapidjson.Validator(rapidjson.dumps(schema)) fast_schema = rapidjson.Validator(rapidjson.dumps(schema))
return path, (schema, fast_schema) return path, (schema, fast_schema)
# TODO: make this an env var from a config file
TX_SCHEMA_VERSION = 'v2.0' TX_SCHEMA_VERSION = 'v2.0'
TX_SCHEMA_PATH, TX_SCHEMA_COMMON = _load_schema('transaction_' + TX_SCHEMA_PATH, TX_SCHEMA_COMMON = _load_schema('transaction',
TX_SCHEMA_VERSION) TX_SCHEMA_VERSION)
_, TX_SCHEMA_CREATE = _load_schema('transaction_create_' + _, TX_SCHEMA_CREATE = _load_schema('transaction_create',
TX_SCHEMA_VERSION) TX_SCHEMA_VERSION)
_, TX_SCHEMA_TRANSFER = _load_schema('transaction_transfer_' + _, TX_SCHEMA_TRANSFER = _load_schema('transaction_transfer',
TX_SCHEMA_VERSION) TX_SCHEMA_VERSION)
_, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election_' + _, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election',
TX_SCHEMA_VERSION) TX_SCHEMA_VERSION)
_, TX_SCHEMA_CHAIN_MIGRATION_ELECTION = _load_schema('transaction_chain_migration_election_' + _, TX_SCHEMA_CHAIN_MIGRATION_ELECTION = _load_schema('transaction_chain_migration_election',
TX_SCHEMA_VERSION) TX_SCHEMA_VERSION)
_, TX_SCHEMA_VOTE = _load_schema('transaction_vote_' + TX_SCHEMA_VERSION) _, TX_SCHEMA_VOTE = _load_schema('transaction_vote', TX_SCHEMA_VERSION)
def _validate_schema(schema, body): def _validate_schema(schema, body):

View File

@ -0,0 +1,174 @@
# 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: "^2\\.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
- VALIDATOR_ELECTION
- CHAIN_MIGRATION_ELECTION
- VOTE
- COMPOSE
- DECOMPOSE
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

@ -0,0 +1,51 @@
# 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

@ -0,0 +1,42 @@
# 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: object
additionalProperties: true
- type: 'null'
required:
- data

View File

@ -0,0 +1,39 @@
# 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

@ -0,0 +1,74 @@
# 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

@ -0,0 +1,34 @@
# 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

@ -13,7 +13,7 @@ Attributes:
""" """
from collections import namedtuple from collections import namedtuple
from copy import deepcopy from copy import deepcopy
from functools import reduce, lru_cache from functools import lru_cache
import rapidjson import rapidjson
import base58 import base58
@ -26,14 +26,15 @@ try:
except ImportError: except ImportError:
from sha3 import sha3_256 from sha3 import sha3_256
from planetmint.common.crypto import PrivateKey, hash_data from planetmint.transactions.common.crypto import PrivateKey, hash_data
from planetmint.common.exceptions import (KeypairMismatchException, from planetmint.transactions.common.exceptions import (
InputDoesNotExist, DoubleSpend, KeypairMismatchException, InputDoesNotExist, DoubleSpend,
InvalidHash, InvalidSignature, InvalidHash, InvalidSignature, AmountError, AssetIdMismatch)
AmountError, AssetIdMismatch, from planetmint.transactions.common.utils import serialize
ThresholdTooDeep)
from planetmint.common.utils import serialize
from .memoize import memoize_from_dict, memoize_to_dict 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 = namedtuple(
'UnspentOutput', ( 'UnspentOutput', (
@ -47,441 +48,6 @@ UnspentOutput = namedtuple(
) )
) )
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.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.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.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)
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,
}
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
raise UnsupportedTypeError(data.get('type'))
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.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)
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:
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]
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.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)
class Transaction(object): class Transaction(object):
"""A Transaction is used to create and transfer assets. """A Transaction is used to create and transfer assets.
@ -491,10 +57,10 @@ class Transaction(object):
Attributes: Attributes:
operation (str): Defines the operation of the Transaction. operation (str): Defines the operation of the Transaction.
inputs (:obj:`list` of :class:`~planetmint.common. inputs (:obj:`list` of :class:`~planetmint.transactions.common.
transaction.Input`, optional): Define the assets to transaction.Input`, optional): Define the assets to
spend. spend.
outputs (:obj:`list` of :class:`~planetmint.common. outputs (:obj:`list` of :class:`~planetmint.transactions.common.
transaction.Output`, optional): Define the assets to lock. transaction.Output`, optional): Define the assets to lock.
asset (dict): Asset payload for this Transaction. ``CREATE`` asset (dict): Asset payload for this Transaction. ``CREATE``
Transactions require a dict with a ``data`` Transactions require a dict with a ``data``
@ -521,9 +87,9 @@ class Transaction(object):
Args: Args:
operation (str): Defines the operation of the Transaction. operation (str): Defines the operation of the Transaction.
asset (dict): Asset payload for this Transaction. asset (dict): Asset payload for this Transaction.
inputs (:obj:`list` of :class:`~planetmint.common. inputs (:obj:`list` of :class:`~planetmint.transactions.common.
transaction.Input`, optional): Define the assets to transaction.Input`, optional): Define the assets to
outputs (:obj:`list` of :class:`~planetmint.common. outputs (:obj:`list` of :class:`~planetmint.transactions.common.
transaction.Output`, optional): Define the assets to transaction.Output`, optional): Define the assets to
lock. lock.
metadata (dict): Metadata to be stored along with the metadata (dict): Metadata to be stored along with the
@ -602,137 +168,6 @@ class Transaction(object):
def _hash(self): def _hash(self):
self._id = hash_data(self.serialized) self._id = hash_data(self.serialized)
@classmethod
def validate_create(cls, 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 or isinstance(asset, dict)):
raise TypeError('`asset` must be a dict or None')
if not (metadata is None or isinstance(metadata, dict)):
raise TypeError('`metadata` must be a dict or None')
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)
@classmethod
def create(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`
"""
(inputs, outputs) = cls.validate_create(tx_signers, recipients, asset, metadata)
return cls(cls.CREATE, {'data': asset}, inputs, outputs, metadata)
@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 transfer(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.TRANSFER, {'id': asset_id}, inputs, outputs, metadata)
def __eq__(self, other): def __eq__(self, other):
try: try:
other = other.to_dict() other = other.to_dict()
@ -757,7 +192,7 @@ class Transaction(object):
outputs should be returned as inputs. outputs should be returned as inputs.
Returns: Returns:
:obj:`list` of :class:`~planetmint.common.transaction. :obj:`list` of :class:`~planetmint.transactions.common.transaction.
Input` Input`
""" """
# NOTE: If no indices are passed, we just assume to take all outputs # NOTE: If no indices are passed, we just assume to take all outputs
@ -774,7 +209,7 @@ class Transaction(object):
"""Adds an input to a Transaction's list of inputs. """Adds an input to a Transaction's list of inputs.
Args: Args:
input_ (:class:`~planetmint.common.transaction. input_ (:class:`~planetmint.transactions.common.transaction.
Input`): An Input to be added to the Transaction. Input`): An Input to be added to the Transaction.
""" """
if not isinstance(input_, Input): if not isinstance(input_, Input):
@ -785,7 +220,7 @@ class Transaction(object):
"""Adds an output to a Transaction's list of outputs. """Adds an output to a Transaction's list of outputs.
Args: Args:
output (:class:`~planetmint.common.transaction. output (:class:`~planetmint.transactions.common.transaction.
Output`): An Output to be added to the Output`): An Output to be added to the
Transaction. Transaction.
""" """
@ -811,7 +246,7 @@ class Transaction(object):
Transaction. Transaction.
Returns: Returns:
:class:`~planetmint.common.transaction.Transaction` :class:`~planetmint.transactions.common.transaction.Transaction`
""" """
# TODO: Singing should be possible with at least one of all private # TODO: Singing should be possible with at least one of all private
# keys supplied to this method. # keys supplied to this method.
@ -857,7 +292,7 @@ class Transaction(object):
- ThresholdSha256. - ThresholdSha256.
Args: Args:
input_ (:class:`~planetmint.common.transaction. input_ (:class:`~planetmint.transactions.common.transaction.
Input`) The Input to be signed. Input`) The Input to be signed.
message (str): The message to be signed message (str): The message to be signed
key_pairs (dict): The keys to sign the Transaction with. key_pairs (dict): The keys to sign the Transaction with.
@ -878,7 +313,7 @@ class Transaction(object):
"""Signs a Ed25519Fulfillment. """Signs a Ed25519Fulfillment.
Args: Args:
input_ (:class:`~planetmint.common.transaction. input_ (:class:`~planetmint.transactions.common.transaction.
Input`) The input to be signed. Input`) The input to be signed.
message (str): The message to be signed message (str): The message to be signed
key_pairs (dict): The keys to sign the Transaction with. key_pairs (dict): The keys to sign the Transaction with.
@ -910,7 +345,7 @@ class Transaction(object):
"""Signs a ThresholdSha256. """Signs a ThresholdSha256.
Args: Args:
input_ (:class:`~planetmint.common.transaction. input_ (:class:`~planetmint.transactions.common.transaction.
Input`) The Input to be signed. Input`) The Input to be signed.
message (str): The message to be signed message (str): The message to be signed
key_pairs (dict): The keys to sign the Transaction with. key_pairs (dict): The keys to sign the Transaction with.
@ -962,7 +397,7 @@ class Transaction(object):
evaluate parts of the validation-checks to `True`. evaluate parts of the validation-checks to `True`.
Args: Args:
outputs (:obj:`list` of :class:`~planetmint.common. outputs (:obj:`list` of :class:`~planetmint.transactions.common.
transaction.Output`): A list of Outputs to check the transaction.Output`): A list of Outputs to check the
Inputs against. Inputs against.
@ -1025,7 +460,7 @@ class Transaction(object):
does not validate against `output_condition_uri`. does not validate against `output_condition_uri`.
Args: Args:
input_ (:class:`~planetmint.common.transaction. input_ (:class:`~planetmint.transactions.common.transaction.
Input`) The Input to be signed. Input`) The Input to be signed.
operation (str): The type of Transaction. operation (str): The type of Transaction.
message (str): The fulfillment message. message (str): The fulfillment message.
@ -1135,7 +570,7 @@ class Transaction(object):
transaction are related to the same asset id. transaction are related to the same asset id.
Args: Args:
transactions (:obj:`list` of :class:`~planetmint.common. transactions (:obj:`list` of :class:`~planetmint.transactions.common.
transaction.Transaction`): A list of Transactions. transaction.Transaction`): A list of Transactions.
Usually input Transactions that should have a matching Usually input Transactions that should have a matching
asset ID. asset ID.
@ -1197,7 +632,7 @@ class Transaction(object):
tx_body (dict): The Transaction to be transformed. tx_body (dict): The Transaction to be transformed.
Returns: Returns:
:class:`~planetmint.common.transaction.Transaction` :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) cls = Transaction.resolve_class(operation)

Some files were not shown because too many files have changed in this diff Show More