# -*- coding:utf-8 -*-
# (C) Toons MIT Licence
import os
import json
import flask
import dposlib
import traceback
from dposlib import rest
from dposlib.util.data import loadJson, dumpJson
from dposlib.ark import crypto
from dposlib.util.bin import hexlify
# create the application instance
app = flask.Flask("ARK multisig server")
app.config.update(
# 600 seconds = 10 minutes lifetime session
PERMANENT_SESSION_LIFETIME=300,
# used to encrypt cookies
# secret key is generated each time app is restarted
SECRET_KEY=os.urandom(24),
# JS can't access cookies
SESSION_COOKIE_HTTPONLY=True,
# bi use of https
SESSION_COOKIE_SECURE=False,
# update cookies on each request
# cookie are outdated after PERMANENT_SESSION_LIFETIME seconds of idle
SESSION_REFRESH_EACH_REQUEST=True,
# reload templates without server restart
TEMPLATES_AUTO_RELOAD=True,
)
[docs]def load(network, ms_publicKey, txid):
"""
Load a transaction from a specific registry.
Args:
network (:class:`str`): blockchain name
ms_publicKey (:class:`str`): encoded-compresed public key as hex string
txid (:class:`str`): transaction id
Returns:
:class:`dict`: transaction data
"""
registry = loadJson(
os.path.join(dposlib.ROOT, ".registry", network, ms_publicKey)
)
return registry.get(txid, False)
[docs]def pop(network, tx):
"""
Remove a transaction from registry. Wallet registry is removed if empty.
Args:
network (:class:`str`): blockchain name
publicKey (:class:`str`): encoded-compresed public key as hex string
"""
path = os.path.join(
dposlib.ROOT, ".registry", network, tx["senderPublicKey"]
)
registry = loadJson(path)
tx.pop("id", False)
registry.pop(identify(tx), False)
if not(len(registry)):
os.remove(path)
else:
dumpJson(registry, path)
[docs]def dump(network, tx):
"""
Add a transaction into registry. ``senderPublicKey`` field is used to
create registry if it does not exist.
Args:
network (:class:`str`):
blockchain name
tx (:class:`dict` or :class:`dposlib.blockchain.Transaction`):
transaction to store
"""
path = os.path.join(
dposlib.ROOT, ".registry", network, tx["senderPublicKey"]
)
registry = loadJson(path)
tx.pop("id", False)
id_ = identify(tx)
registry[id_] = tx
dumpJson(registry, path)
return id_
[docs]def identify(tx):
"""
Identify a transaction.
Args:
tx (:class:`dict` or :class:`dposlib.blockchain.Transaction`):
transaction to identify
Returns:
:class:`str`: transaction id used by registries
"""
return crypto.getIdFromBytes(
crypto.getBytes(
tx,
exclude_sig=True,
exclude_multi_sig=True,
exclude_second_sig=True
)
)
def append(network, *transactions):
response = {}
for tx in transactions:
idx = transactions.index(tx) + 1
try:
if not isinstance(tx, dposlib.core.Transaction):
tx = dposlib.core.Transaction(tx, ignore_bad_fields=True)
signatures = tx.get("signatures", [])
if len(signatures) == 0:
response["errors"] = response.get("errors", []) + [
"transaction #%d rejected (one signature is mandatory)"
% idx
]
elif tx.get("nonce", 1) <= tx._nonce:
response["errors"] = response.get("errors", []) + [
"transaction #%d rejected (bad nonce)"
% idx
]
else:
checks = []
publicKeys = \
tx["asset"].get("multiSignature", {}).get(
"publicKeys", []
) \
if tx["type"] == 4 else tx._multisignature.get(
"publicKeys", []
)
serialized = crypto.getBytes(
tx,
exclude_sig=True,
exclude_second_sig=True,
exclude_multi_sig=True
)
for sig in signatures:
pk_idx, sig = int(sig[0:2], 16), sig[2:]
checks.append(crypto.verifySignatureFromBytes(
serialized, publicKeys[pk_idx],
sig
))
if False in checks:
response["errors"] = response.get("errors", []) + [
"transaction #%d rejected (bad signature)"
% idx
]
else:
id_ = dump(network, tx)
response["success"] = response.get("success", []) + [
"transaction #%d successfully posted" % (idx)
]
response["ids"] = response.get("ids", []) + [id_]
except Exception as error:
response["errors"] = response.get("errors", []) + [
"transaction #%d rejected (%r)" % (idx, error)
]
return json.dumps(response), 201
def broadcast(network, tx):
tx["id"] = crypto.getId(tx)
response = rest.POST.api.transactions(transactions=[tx])
if len(response.get("data", {}).get("broadcast", [])):
code = 200
pop(network, tx)
else:
code = 202
dump(network, tx)
return json.dumps(response), code
@app.after_request
def apply_caching(response):
response.headers["Content-Type"] = "application/json; charset=utf-8"
return response
@app.errorhandler(Exception)
def handle_exception(error):
tl = traceback.format_exc().splitlines()
return json.dumps({"python error": [tl[0], tl[-1]]})
[docs]@app.route("/multisignature/<string:network>", methods=["GET"])
def getAll(network):
"""
``GET /multisignature/{network}`` endpoint. Return all public keys issuing
multisignature transactions.
Args:
network (:class:`str`): blockchain network name
Returns:
:class:`dict`: all registries
"""
result = {}
search_path = os.path.join(dposlib.ROOT, ".registry", network)
if os.path.exists(search_path) and os.path.isdir(search_path):
for name in os.listdir(search_path):
result[name] = loadJson(os.path.join(search_path, name))
return json.dumps({"success": True, "data": result}), 200
else:
return json.dumps({"success": False})
[docs]@app.route(
"/multisignature/<string:network>/<string:ms_publicKey>", methods=["GET"]
)
def getWallet(network, ms_publicKey):
"""
``GET /multisignature/{network}/{ms_publicKey}`` endpoint. Return all
pending transactions issued by a specific public key.
"""
if flask.request.method != "GET":
return json.dumps({
"success": False,
"API error": "GET request only allowed here"
})
wallet = loadJson(
os.path.join(dposlib.ROOT, ".registry", network, ms_publicKey)
)
if len(wallet):
return json.dumps({"success": True, "data": wallet}), 200
else:
return json.dumps({"success": False})
[docs]@app.route(
"/multisignature/<string:network>/<string:ms_publicKey>/<string:txid>",
methods=["GET"]
)
def getTransaction(network, ms_publicKey, txid):
"""
``GET /multisignature/{network}/{ms_publicKey}/{txid}`` endpoint. Return
specific pending transaction from a specific public key.
"""
if flask.request.method != "GET":
return json.dumps({
"success": False,
"API error": "GET request only allowed here"
})
tx = load(network, ms_publicKey, txid)
if tx:
return json.dumps({"success": True, "data": tx}), 200
else:
return json.dumps({"success": False})
[docs]@app.route(
"/multisignature/<string:network>/<string:ms_publicKey>"
"/<string:txid>/serial", methods=["GET"]
)
def getSerial(network, ms_publicKey, txid):
"""
``GET /multisignature/{network}/{ms_publicKey}/{txid}/serial`` endpoint.
Return specific pending transaction serial from a specific public key.
"""
if network != getattr(rest.cfg, "network", False):
rest.use(network)
if flask.request.method != "GET":
return json.dumps({
"success": False,
"API error": "GET request only allowed here"
})
tx = load(network, ms_publicKey, txid)
if tx:
return json.dumps({
"success": True,
"data": hexlify(crypto.getBytes(tx))
}), 200
else:
return json.dumps({"success": False})
[docs]@app.route("/multisignature/<string:network>/create", methods=["POST"])
def registerWallet(network):
"""
``POST /multisignature/{network}/create`` endpoint. Register as
multisignature wallet::
data = {
"info": {
"senderPublicKey": wallet_public_key_issuing_transaction,
"min": minimum_signature_required,
"publicKeys": public_key_list
}
}
Once created on server, registration transaction have to be remotly signed.
See :func:`putSignature`.
"""
if network != getattr(rest.cfg, "network", False):
rest.use(network)
if flask.request.method == "POST":
data = json.loads(flask.request.data)
if "info" not in data:
return json.dumps({"error": "no info"})
tx = dposlib.core.registerMultiSignature(
data["info"]["min"], *data["info"]["publicKeys"]
)
tx.senderPublicKey = data["info"]["senderPublicKey"]
tx.useDynamicFee(data["info"].get("fee", "avgFee"))
tx.setFee()
return append(network, tx)
else:
return json.dumps({
"success": False,
"API error": "POST request only allowed here"
})
[docs]@app.route("/multisignature/<string:network>/post", methods=["POST"])
def postNewTransactions(network):
"""
``POST /multisignature/{network}/post`` endpoint. Post transaction
from multisignature wallet to be remotly signed::
data = {"transactions": [tx1, tx2, ... txi ..., txn]}
See :func:`putSignature`.
"""
if network != getattr(rest.cfg, "network", False):
rest.use(network)
if flask.request.method == "POST":
data = json.loads(flask.request.data)
if "transactions" not in data:
return json.dumps({
"success": False,
"API error": "transaction(s) not found"
})
return append(network, *data.get("transactions", []))
else:
return json.dumps({
"success": False,
"API error": "POST request only allowed here"
})
[docs]@app.route(
"/multisignature/<string:network>/<string:ms_publicKey>/put",
methods=["PUT"]
)
def putSignature(network, ms_publicKey):
"""
``PUT /multisignature/{network}/{ms_publicKey}/put`` endpoint. Add
signature to a pending transaction::
data = {
"info": {
"id": pending_transaction_id,
"signature": signature,
"publicKey": associated_public_key
} [ + {
"fee": optional_fee_value_to_use
} ]
}
"""
if network != getattr(rest.cfg, "network", False):
rest.use(network)
if flask.request.method == "PUT":
data = json.loads(flask.request.data)
if "info" not in data:
return json.dumps({"error": "no info"})
txid = data["info"]["id"]
tx = load(network, ms_publicKey, txid)
if not tx:
return json.dumps({
"success": False,
"API error": "transaction %s not found" % txid
})
tx = dposlib.core.Transaction(tx)
publicKey = data["info"]["publicKey"]
signature = data["info"]["signature"]
publicKeys = \
tx["asset"].get("multiSignature", {}).get("publicKeys", []) \
if tx["type"] == 4 else tx._multisignature.get("publicKeys", [])
if publicKey not in (
publicKeys + [tx._secondPublicKey, tx._publicKey]
):
return json.dumps({
"success": False,
"API error": "public key %s not allowed here" % publicKey
})
# sign type 4
# signatures field is full
if tx.type == 4 and len(tx.get("signatures", [])) == len(publicKeys):
if publicKey == tx._publicKey:
# and signature matches type 4 issuer's public key
if crypto.verifySignatureFromBytes(
crypto.getBytes(tx), publicKey, signature
):
tx.signature = signature
dump(network, tx)
if tx._secondPublicKey is None:
# if no need to signSign --> broadcast tx and return
# network response
return broadcast(network, tx)
else:
return json.dumps({
"success": True,
"message": "issuer signature added"
})
else:
return json.dumps({
"success": False,
"API error": "signature does not match issuer key"
})
# signSign
elif publicKey == tx._secondPublicKey:
# if tx already signed by issuer
if "signature" in tx:
# and signature matches type 4 issuer 's second public key
if crypto.verifySignatureFromBytes(
crypto.getBytes(tx), publicKey, signature
):
# signSign, broadcast and return network response
tx.signSignature = signature
return broadcast(network, tx)
else:
return json.dumps({
"success": False,
"API error": "signature does not match issuer "
"second key"
})
else:
return json.dumps({
"success": False,
"API error": "transaction have to be signed first"
})
# verify owner signature
check = crypto.verifySignatureFromBytes(
crypto.getBytes(
tx, exclude_sig=True, exclude_multi_sig=True,
exclude_second_sig=True
), publicKey, signature
)
# if signature matches
if check and publicKey in publicKeys:
index = publicKeys.index(publicKey)
# set is used here to remove doubles
tx["signatures"] = list(
set(tx.get("signatures", []) + ["%02x" % index + signature])
)
if tx["type"] != 4 and \
len(tx.get("signatures", [])) >= tx._multisignature["min"]:
return broadcast(network, tx)
else:
dump(network, tx)
return json.dumps({
"success": True,
"message": "signature added to transaction",
}), 201
else:
return json.dumps({
"success": False,
"API error": "signature not accepted"
})
else:
return json.dumps({
"success": False,
"API error": "PUT request only allowed here"
})