mirror of
https://github.com/planetmint/planetmint.git
synced 2025-11-25 06:55:45 +00:00
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:
commit
ac7c1171b8
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"]
|
|
||||||
@ -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 \
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
42
Makefile
42
Makefile
@ -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/
|
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
#
|
||||||
|
|||||||
89
acceptance/python/src/conftest.py
Normal file
89
acceptance/python/src/conftest.py
Normal 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
|
||||||
85
acceptance/python/src/test_zenroom.py
Normal file
85
acceptance/python/src/test_zenroom.py
Normal 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))
|
||||||
53
docker-compose.integration.yml
Normal file
53
docker-compose.integration.yml
Normal 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:
|
||||||
@ -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
|
||||||
@ -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
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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.
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
|||||||
|
|
||||||
It’s 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 can’t do that because the admin user doesn’t have the necessary information (e.g. private keys).
|
It’s 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 can’t do that because the admin user doesn’t 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.
|
||||||
|
|
||||||
|
|||||||
@ -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
23
integration/README.md
Normal 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
|
||||||
|
```
|
||||||
47
integration/cli/chain-migration.sh
Executable file
47
integration/cli/chain-migration.sh
Executable 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=}
|
||||||
33
integration/cli/upsert-new-validator.sh
Executable file
33
integration/cli/upsert-new-validator.sh
Executable 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
1
integration/python/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
docs
|
||||||
22
integration/python/Dockerfile
Normal file
22
integration/python/Dockerfile
Normal 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
|
||||||
|
|
||||||
95
integration/python/src/conftest.py
Normal file
95
integration/python/src/conftest.py
Normal 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
|
||||||
36
integration/python/src/helper/hosts.py
Normal file
36
integration/python/src/helper/hosts.py
Normal 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)
|
||||||
83
integration/python/src/test_basic.py
Normal file
83
integration/python/src/test_basic.py
Normal 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)
|
||||||
183
integration/python/src/test_divisible_asset.py
Normal file
183
integration/python/src/test_divisible_asset.py
Normal 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 :)
|
||||||
48
integration/python/src/test_double_spend.py
Normal file
48
integration/python/src/test_double_spend.py
Normal 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
|
||||||
133
integration/python/src/test_multiple_owners.py
Normal file
133
integration/python/src/test_multiple_owners.py
Normal 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
|
||||||
100
integration/python/src/test_naughty_strings.py
Normal file
100
integration/python/src/test_naughty_strings.py
Normal 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)
|
||||||
131
integration/python/src/test_stream.py
Normal file
131
integration/python/src/test_stream.py
Normal 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
|
||||||
336
integration/python/src/test_threshold.py
Normal file
336
integration/python/src/test_threshold.py
Normal 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)
|
||||||
84
integration/python/src/test_zenroom.py
Normal file
84
integration/python/src/test_zenroom.py
Normal 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))
|
||||||
17
integration/scripts/all-in-one.bash
Executable file
17
integration/scripts/all-in-one.bash
Executable 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
|
||||||
11
integration/scripts/clean-shared.sh
Executable file
11
integration/scripts/clean-shared.sh
Executable 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
81
integration/scripts/election.sh
Executable 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
33
integration/scripts/genesis.py
Executable 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()
|
||||||
208
integration/scripts/planetmint-monit-config
Executable file
208
integration/scripts/planetmint-monit-config
Executable 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
|
||||||
83
integration/scripts/pre-config-planetmint.sh
Executable file
83
integration/scripts/pre-config-planetmint.sh
Executable 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 "$@"
|
||||||
16
integration/scripts/pre-config-test.sh
Executable file
16
integration/scripts/pre-config-test.sh
Executable 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
24
integration/scripts/test.sh
Executable 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 "$@"
|
||||||
29
integration/scripts/wait-for-planetmint.sh
Executable file
29
integration/scripts/wait-for-planetmint.sh
Executable 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 "$@"
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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__)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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__)
|
||||||
|
|
||||||
|
|||||||
@ -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__)
|
||||||
|
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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():
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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):
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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(),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
0
planetmint/transactions/__init__.py
Normal file
0
planetmint/transactions/__init__.py
Normal file
0
planetmint/transactions/common/__init__.py
Normal file
0
planetmint/transactions/common/__init__.py
Normal 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
|
||||||
125
planetmint/transactions/common/input.py
Normal file
125
planetmint/transactions/common/input.py
Normal 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)
|
||||||
208
planetmint/transactions/common/output.py
Normal file
208
planetmint/transactions/common/output.py
Normal 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)
|
||||||
@ -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):
|
||||||
174
planetmint/transactions/common/schema/v3.0/transaction.yaml
Normal file
174
planetmint/transactions/common/schema/v3.0/transaction.yaml
Normal 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"
|
||||||
@ -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}$"
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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
|
||||||
@ -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}$"
|
||||||
@ -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
Loading…
x
Reference in New Issue
Block a user