From 880908d857bbe81dc6bf7e4df94abc439f2744a3 Mon Sep 17 00:00:00 2001 From: Inso <insomniak.fr@gmail.com> Date: Sat, 13 Dec 2014 23:22:12 +0100 Subject: [PATCH] New ucoinpy API --- lib/ucoinpy/__init__.py | 21 + lib/ucoinpy/api/__init__.py | 0 lib/ucoinpy/api/bma/__init__.py | 139 +++++++ lib/ucoinpy/api/bma/blockchain/__init__.py | 151 +++++++ lib/ucoinpy/api/bma/network/__init__.py | 35 ++ .../api/bma/network/peering/__init__.py | 51 +++ lib/ucoinpy/api/bma/tx/__init__.py | 44 ++ lib/ucoinpy/api/bma/wot/__init__.py | 89 +++++ lib/ucoinpy/documents/__init__.py | 41 ++ lib/ucoinpy/documents/block.py | 287 +++++++++++++ lib/ucoinpy/documents/certification.py | 86 ++++ lib/ucoinpy/documents/membership.py | 125 ++++++ lib/ucoinpy/documents/peer.py | 148 +++++++ lib/ucoinpy/documents/status.py | 84 ++++ lib/ucoinpy/documents/transaction.py | 297 ++++++++++++++ lib/ucoinpy/key/__init__.py | 36 ++ lib/ucoinpy/key/hdwallet.py | 378 ++++++++++++++++++ 17 files changed, 2012 insertions(+) create mode 100644 lib/ucoinpy/__init__.py create mode 100644 lib/ucoinpy/api/__init__.py create mode 100644 lib/ucoinpy/api/bma/__init__.py create mode 100644 lib/ucoinpy/api/bma/blockchain/__init__.py create mode 100644 lib/ucoinpy/api/bma/network/__init__.py create mode 100644 lib/ucoinpy/api/bma/network/peering/__init__.py create mode 100644 lib/ucoinpy/api/bma/tx/__init__.py create mode 100644 lib/ucoinpy/api/bma/wot/__init__.py create mode 100644 lib/ucoinpy/documents/__init__.py create mode 100644 lib/ucoinpy/documents/block.py create mode 100644 lib/ucoinpy/documents/certification.py create mode 100644 lib/ucoinpy/documents/membership.py create mode 100644 lib/ucoinpy/documents/peer.py create mode 100644 lib/ucoinpy/documents/status.py create mode 100644 lib/ucoinpy/documents/transaction.py create mode 100644 lib/ucoinpy/key/__init__.py create mode 100644 lib/ucoinpy/key/hdwallet.py diff --git a/lib/ucoinpy/__init__.py b/lib/ucoinpy/__init__.py new file mode 100644 index 00000000..354362cd --- /dev/null +++ b/lib/ucoinpy/__init__.py @@ -0,0 +1,21 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +PROTOCOL_VERSION="1" + +MANAGED_API=["BASIC_MERKLED_API"] diff --git a/lib/ucoinpy/api/__init__.py b/lib/ucoinpy/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lib/ucoinpy/api/bma/__init__.py b/lib/ucoinpy/api/bma/__init__.py new file mode 100644 index 00000000..fbd95eaa --- /dev/null +++ b/lib/ucoinpy/api/bma/__init__.py @@ -0,0 +1,139 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + + +__all__ = ['api'] + +__author__ = 'Caner Candan' +__version__ = '0.10.0' +__nonsense__ = 'uCoin' + +import requests, logging, json +# import pylibscrypt + +logger = logging.getLogger("ucoin") + + +class ConnectionHandler(object): + """Helper class used by other API classes to ease passing server connection information.""" + + def __init__(self, server, port): + """ + Arguments: + - `server`: server hostname + - `port`: port number + """ + + self.server = server + self.port = port + + def __str__(self): + return 'connection info: %s:%d' % (self.server, self.port) + + +class API(object): + """APIRequest is a class used as an interface. The intermediate derivated classes are the modules and the leaf classes are the API requests.""" + + def __init__(self, connection_handler, module): + """ + Asks a module in order to create the url used then by derivated classes. + + Arguments: + - `module`: module name + - `connection_handler`: connection handler + """ + + self.module = module + self.connection_handler = connection_handler + self.headers = {} + + def reverse_url(self, path): + """ + Reverses the url using self.url and path given in parameter. + + Arguments: + - `path`: the request path + """ + + server, port = self.connection_handler.server, self.connection_handler.port + + url = 'http://%s:%d/%s' % (server, port, self.module) + return url + path + + def get(self, **kwargs): + """wrapper of overloaded __get__ method.""" + + return self.__get__(**kwargs) + + def post(self, **kwargs): + """wrapper of overloaded __post__ method.""" + + logger.debug('do some work with') + + data = self.__post__(**kwargs) + + logger.debug('and send back') + + return data + + def __get__(self, **kwargs): + """interface purpose for GET request""" + + pass + + def __post__(self, **kwargs): + """interface purpose for POST request""" + + pass + + def requests_get(self, path, **kwargs): + """ + Requests GET wrapper in order to use API parameters. + + Arguments: + - `path`: the request path + """ + + response = requests.get(self.reverse_url(path), params=kwargs, headers=self.headers) + + if response.status_code != 200: + raise ValueError('status code != 200 => %d (%s)' % (response.status_code, response.text)) + + return response + + def requests_post(self, path, **kwargs): + """ + Requests POST wrapper in order to use API parameters. + + Arguments: + - `path`: the request path + """ + + response = requests.post(self.reverse_url(path), data=kwargs, headers=self.headers) + + if response.status_code != 200: + raise ValueError('status code != 200 => %d (%s)' % (response.status_code, response.text)) + + return response + + def merkle_easy_parser(self, path, begin=None, end=None): + root = self.requests_get(path, leaves='true').json() + for leaf in root['leaves'][begin:end]: + yield self.requests_get(path, leaf=leaf).json()['leaf'] + +from . import network, blockchain, tx, wot diff --git a/lib/ucoinpy/api/bma/blockchain/__init__.py b/lib/ucoinpy/api/bma/blockchain/__init__.py new file mode 100644 index 00000000..03890265 --- /dev/null +++ b/lib/ucoinpy/api/bma/blockchain/__init__.py @@ -0,0 +1,151 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/blockchain") + + +class Blockchain(API): + def __init__(self, connection_handler, module='blockchain'): + super(Blockchain, self).__init__(connection_handler, module) + + +class Parameters(Blockchain): + """GET the blockchain parameters used by this node.""" + + def __get__(self, **kwargs): + return self.requests_get('/parameters', **kwargs).json() + + +class Membership(Blockchain): + """POST a Membership document.""" + + def __post__(self, **kwargs): + assert 'membership' in kwargs + + return self.requests_post('/membership', **kwargs).json() + + +class Block(Blockchain): + """GET/POST a block from/to the blockchain.""" + + def __init__(self, connection_handler, number=None): + """ + Use the number parameter in order to select a block number. + + Arguments: + - `number`: block number to select + """ + + super(Block, self).__init__(connection_handler) + + self.number = number + + def __get__(self, **kwargs): + assert self.number is not None + return self.requests_get('/block/%d' % self.number, **kwargs).json() + + def __post__(self, **kwargs): + assert 'block' in kwargs + assert 'signature' in kwargs + + return self.requests_post('/block', **kwargs).json() + + +class Current(Blockchain): + """GET, same as block/[number], but return last accepted block.""" + + def __get__(self, **kwargs): + return self.requests_get('/current', **kwargs).json() + + +class Hardship(Blockchain): + """GET hardship level for given member's fingerprint for writing next block.""" + + def __init__(self, connection_handler, fingerprint): + """ + Use the number parameter in order to select a block number. + + Arguments: + - `fingerprint`: member fingerprint + """ + + super(Hardship, self).__init__(connection_handler) + + self.fingerprint = fingerprint + + def __get__(self, **kwargs): + assert self.fingerprint is not None + return self.requests_get('/hardship/%s' % self.fingerprint.upper(), **kwargs).json() + + +class Newcomers(Blockchain): + """GET, return block numbers containing newcomers.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/newcomers', **kwargs).json() + + +class Certifications(Blockchain): + """GET, return block numbers containing certifications.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/certs', **kwargs).json() + + +class Joiners(Blockchain): + """GET, return block numbers containing joiners.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/joiners', **kwargs).json() + + +class Actives(Blockchain): + """GET, return block numbers containing actives.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/actives', **kwargs).json() + + +class Leavers(Blockchain): + """GET, return block numbers containing leavers.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/leavers', **kwargs).json() + + +class Excluded(Blockchain): + """GET, return block numbers containing excluded.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/excluded', **kwargs).json() + + +class UD(Blockchain): + """GET, return block numbers containing universal dividend.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/ud', **kwargs).json() + + +class TX(Blockchain): + """GET, return block numbers containing transactions.""" + + def __get__(self, **kwargs): + return self.requests_get('/with/tx', **kwargs).json() diff --git a/lib/ucoinpy/api/bma/network/__init__.py b/lib/ucoinpy/api/bma/network/__init__.py new file mode 100644 index 00000000..3d6c73ec --- /dev/null +++ b/lib/ucoinpy/api/bma/network/__init__.py @@ -0,0 +1,35 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/network") + + +class Network(API): + def __init__(self, connection_handler, module='network'): + super(Network, self).__init__(connection_handler, module) + + +class Peering(Network): + """GET peering information about a peer.""" + + def __get__(self, **kwargs): + return self.requests_get('/peering', **kwargs).json() + +from . import peering diff --git a/lib/ucoinpy/api/bma/network/peering/__init__.py b/lib/ucoinpy/api/bma/network/peering/__init__.py new file mode 100644 index 00000000..3ad7cde3 --- /dev/null +++ b/lib/ucoinpy/api/bma/network/peering/__init__.py @@ -0,0 +1,51 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import Network, logging + +logger = logging.getLogger("ucoin/network/peering") + + +class Base(Network): + def __init__(self, connection_handler): + super(Base, self).__init__(connection_handler, 'network/peering') + + +class Peers(Base): + """GET peering entries of every node inside the currency network.""" + + def __get__(self, **kwargs): + """creates a generator with one peering entry per iteration.""" + + return self.merkle_easy_parser('/peers') + + def __post__(self, **kwargs): + assert 'entry' in kwargs + assert 'signature' in kwargs + + return self.requests_post('/peers', **kwargs).json() + + +class Status(Base): + """POST a network status document to this node in order notify of its status.""" + + def __post__(self, **kwargs): + assert 'status' in kwargs + assert 'signature' in kwargs + + return self.requests_post('/status', **kwargs).json() diff --git a/lib/ucoinpy/api/bma/tx/__init__.py b/lib/ucoinpy/api/bma/tx/__init__.py new file mode 100644 index 00000000..a7adc5f3 --- /dev/null +++ b/lib/ucoinpy/api/bma/tx/__init__.py @@ -0,0 +1,44 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/tx") + + +class Tx(API): + def __init__(self, connection_handler, module='tx'): + super(Tx, self).__init__(connection_handler, module) + + +class Process(Tx): + """POST a transaction.""" + + def __post__(self, **kwargs): + assert 'transaction' in kwargs + assert 'signature' in kwargs + + return self.requests_post('/process', **kwargs).json() + + +class Sources(Tx): + """Get transaction sources.""" + + def __get__(self, **kwargs): + assert self.pubkey is not None + return self.requests_get('/sources/%d' % self.pubkey, **kwargs).json() diff --git a/lib/ucoinpy/api/bma/wot/__init__.py b/lib/ucoinpy/api/bma/wot/__init__.py new file mode 100644 index 00000000..3e1c694a --- /dev/null +++ b/lib/ucoinpy/api/bma/wot/__init__.py @@ -0,0 +1,89 @@ +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# +# Authors: +# Caner Candan <caner@candan.fr>, http://caner.candan.fr +# + +from .. import API, logging + +logger = logging.getLogger("ucoin/wot") + + +class WOT(API): + def __init__(self, connection_handler, module='wot'): + super(WOT, self).__init__(connection_handler, module) + + +class Add(WOT): + """POST Public key data.""" + + def __post__(self, **kwargs): + assert 'pubkey' in kwargs + assert 'self' in kwargs + assert 'other' in kwargs + + return self.requests_post('/add', **kwargs).json() + + +class Lookup(WOT): + """GET Public key data.""" + + def __init__(self, connection_handler, search, module='wot'): + super(WOT, self).__init__(connection_handler, module) + + self.search = search + + def __get__(self, **kwargs): + assert self.search is not None + + return self.requests_get('/lookup/%s' % self.search, **kwargs).json() + + +class CertifiersOf(WOT): + """GET Certification data over a member.""" + + def __init__(self, connection_handler, search, module='wot'): + super(WOT, self).__init__(connection_handler, module) + + self.search = search + + def __get__(self, **kwargs): + assert self.search is not None + + return self.requests_get('/certifiers-of/%s' % self.search, **kwargs).json() + + +class CertifiedBy(WOT): + """GET Certification data from a member.""" + + def __init__(self, connection_handler, search, module='wot'): + super(WOT, self).__init__(connection_handler, module) + + self.search = search + + def __get__(self, **kwargs): + assert self.search is not None + + return self.requests_get('/certified-by/%s' % self.search, **kwargs).json() + + +class Members(WOT): + """GET List all current members of the Web of Trust.""" + + def __init__(self, connection_handler, module='wot'): + super(WOT, self).__init__(connection_handler, module) + + def __get__(self, **kwargs): + return self.requests_get('/members', **kwargs).json() diff --git a/lib/ucoinpy/documents/__init__.py b/lib/ucoinpy/documents/__init__.py new file mode 100644 index 00000000..14a7f83d --- /dev/null +++ b/lib/ucoinpy/documents/__init__.py @@ -0,0 +1,41 @@ +''' +Created on 3 déc. 2014 + +@author: inso +''' +import base58 +import re +from ..key import Base58Encoder +from nacl.encoding import Base64Encoder + + +class Document: + re_version = re.compile("Version: ([0-9]+)\n") + re_currency = re.compile("Currency: ([^\n]+)\n") + re_signature = re.compile("([A-Za-z0-9+/]+(?:=|==)?)\n") + + def __init__(self, version, currency, signatures): + self.version = version + self.currency = currency + self.signatures = signatures + + def sign(self, keys): + ''' + Sign the current document. + Warning : current signatures will be replaced with the new ones. + ''' + self.signatures = [] + for k in keys: + self.signatures.append(k.sign(self.raw(), Base64Encoder)) + + def signed_raw(self): + ''' + If keys are None, returns the raw + current signatures + If keys are present, returns the raw signed by these keys + ''' + raw = self.raw() + signed_raw = raw + for s in self.signatures: + if s is not None: + signed_raw += s + "\n" + return signed_raw diff --git a/lib/ucoinpy/documents/block.py b/lib/ucoinpy/documents/block.py new file mode 100644 index 00000000..1bf08afe --- /dev/null +++ b/lib/ucoinpy/documents/block.py @@ -0,0 +1,287 @@ +''' +Created on 2 déc. 2014 + +@author: inso +''' + +from .. import PROTOCOL_VERSION +from . import Document +from .certification import SelfCertification, Certification +from .membership import Membership +from .transaction import Transaction + +import re + + +class Block(Document): + ''' +Version: VERSION +Type: Block +Currency: CURRENCY +Nonce: NONCE +Number: BLOCK_NUMBER +PoWMin: NUMBER_OF_ZEROS +Time: GENERATED_ON +MedianTime: MEDIAN_DATE +UniversalDividend: DIVIDEND_AMOUNT +Issuer: ISSUER_KEY +PreviousHash: PREVIOUS_HASH +PreviousIssuer: PREVIOUS_ISSUER_KEY +Parameters: PARAMETERS +MembersCount: WOT_MEM_COUNT +Identities: +PUBLIC_KEY:SIGNATURE:TIMESTAMP:USER_ID +... +Joiners: +PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID +... +Actives: +PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID +... +Leavers: +PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID +... +Excluded: +PUBLIC_KEY +... +Certifications: +PUBKEY_FROM:PUBKEY_TO:BLOCK_NUMBER:SIGNATURE +... +Transactions: +COMPACT_TRANSACTION +... +BOTTOM_SIGNATURE + ''' + + re_type = re.compile("Type: (Block)\n") + re_noonce = re.compile("Nonce: ([0-9]+)\n") + re_number = re.compile("Number: ([0-9]+)\n") + re_powmin = re.compile("PoWMin: ([0-9]+)\n") + re_time = re.compile("Time: ([0-9]+)\n") + re_mediantime = re.compile("MedianTime: ([0-9]+)\n") + re_universaldividend = re.compile("UniversalDividend: ([0-9]+)\n") + re_issuer = re.compile("Issuer: ([1-9A-Za-z][^OIl]{42,45})\n") + re_previoushash = re.compile("PreviousHash: ([0-9a-fA-F]{5,40})\n") + re_previousissuer = re.compile("PreviousIssuer: ([1-9A-Za-z][^OIl]{42,45})\n") + re_parameters = re.compile("Parameters: ([0-9]+\.[0-9]+):([0-9]+):([0-9]+):([0-9]+):\ +([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):([0-9]+):\ +([0-9]+\.[0-9]+)\n") + re_memberscount = re.compile("MembersCount: ([0-9]+)\n") + re_identities = re.compile("Identities:\n") + re_joiners = re.compile("Joiners:\n") + re_actives = re.compile("Actives:\n") + re_leavers = re.compile("Leavers:\n") + re_excluded = re.compile("Excluded:\n") + re_certifications = re.compile("Certifications:\n") + re_transactions = re.compile("Transactions:\n") + + def __init__(self, version, currency, noonce, number, powmin, time, + mediantime, ud, issuer, prev_hash, prev_issuer, + parameters, members_count, identities, joiners, + actives, leavers, excluded, certifications, + transactions, signature): + ''' + Constructor + ''' + if signature: + super().__init__(version, currency, [signature]) + else: + super().__init__(version, currency, []) + self.noonce = noonce + self.number = number + self.powmin = powmin + self.time = time + self.mediantime = mediantime + self.ud = ud + self.issuer = issuer + self.prev_hash = prev_hash + self.prev_issuer = prev_issuer + self.parameters = parameters + self.members_count = members_count + self.identities = identities + self.joiners = joiners + self.actives = actives + self.leavers = leavers + self.excluded = excluded + self.certifications = certifications + self.transactions = transactions + + @classmethod + def from_signed_raw(cls, raw, signature=None): + lines = raw.splitlines(True) + n = 0 + + version = int(Block.re_version.match(lines[n]).group(1)) + n = n + 1 + + Block.re_type.match(lines[n]).group(1) + n = n + 1 + + currency = Block.re_currency.match(lines[n]).group(1) + n = n + 1 + + noonce = int(Block.re_noonce.match(lines[n]).group(1)) + n = n + 1 + + number = int(Block.re_number.match(lines[n]).group(1)) + n = n + 1 + + powmin = int(Block.re_powmin.match(lines[n]).group(1)) + n = n + 1 + + time = int(Block.re_time.match(lines[n]).group(1)) + n = n + 1 + + mediantime = int(Block.re_mediantime.match(lines[n]).group(1)) + n = n + 1 + + ud = Block.re_universaldividend.match(lines[n]) + if ud is not None: + ud = int(ud.group(1)) + n = n + 1 + + issuer = Block.re_issuer.match(lines[n]).group(1) + n = n + 1 + + prev_hash = None + prev_issuer = None + if number > 0: + prev_hash = Block.re_previoushash.match(lines[n]).group(1) + n = n + 1 + + prev_issuer = Block.re_previousissuer.match(lines[n]).group(1) + n = n + 1 + + parameters = None + if number == 0: + parameters = Block.re_parameters.match(lines[n]).groups() + n = n + 1 + + members_count = int(Block.re_memberscount.match(lines[n]).group(1)) + n = n + 1 + + identities = [] + joiners = [] + actives = [] + leavers = [] + excluded = [] + certifications = [] + transactions = [] + + if Block.re_identities.match(lines[n]) is not None: + n = n + 1 + while Block.re_joiners.match(lines[n]) is None: + selfcert = SelfCertification.from_inline(version, currency, lines[n]) + identities.append(selfcert) + n = n + 1 + + if Block.re_joiners.match(lines[n]): + n = n + 1 + while Block.re_actives.match(lines[n]) is None: + membership = Membership.from_inline(version, currency, "IN", lines[n]) + joiners.append(membership) + n = n + 1 + + if Block.re_actives.match(lines[n]): + n = n + 1 + while Block.re_leavers.match(lines[n]) is None: + membership = Membership.from_inline(version, currency, "IN", lines[n]) + actives.append(membership) + n = n + 1 + + if Block.re_leavers.match(lines[n]): + n = n + 1 + while Block.re_excluded.match(lines[n]) is None: + membership = Membership.from_inline(version, currency, "OUT", lines[n]) + leavers.append(membership) + n = n + 1 + + if Block.re_excluded.match(lines[n]): + n = n + 1 + while Block.re_certifications.match(lines[n]) is None: + membership = Membership.from_inline(version, currency, "OUT", lines[n]) + excluded.append(membership) + n = n + 1 + + if Block.re_certifications.match(lines[n]): + n = n + 1 + while Block.re_transactions.match(lines[n]) is None: + certification = Certification.from_inline(version, currency, + prev_hash, lines[n]) + certifications.append(certification) + n = n + 1 + + if Block.re_transactions.match(lines[n]): + n = n + 1 + while not Block.re_signature.match(lines[n]): + transaction = Transaction.from_compact(version, lines[n]) + transactions.append(transaction) + n = n + 1 + + signature = Block.re_signature.match(lines[n]).group(1) + + return cls(version, currency, noonce, number, powmin, time, + mediantime, ud, issuer, prev_hash, prev_issuer, + parameters, members_count, identities, joiners, + actives, leavers, excluded, certifications, + transactions, signature) + + def raw(self): + doc = """Version: {0} +Type: Block +Currency: {1} +Nonce: {2} +Number: {3} +PoWMin: {4} +Time: {5} +MedianTime: {6} +""".format(self.version, + self.currency, + self.noonce, + self.number, + self.powmin, + self.time, + self.mediantime) + if self.ud: + doc += "UniversalDividend: {0}\n".format(self.ud) + + doc += "Issuer: {0}\n".format(self.issuer) + + if self.number == 0: + str_params = ":".join(self.parameters) + doc += "Parameters: {0}\n".format(str_params) + else: + doc += "PreviousHash: {0}\n\ +PreviousIssuer: {1}\n".format(self.prev_hash, self.prev_issuer) + + doc += "MembersCount: {0}\n".format(self.members_count) + + doc += "Identities:\n" + for identity in self.identities: + doc += "{0}\n".format(identity.inline()) + + doc += "Joiners:\n" + for joiner in self.joiners: + doc += "{0}\n".format(joiner.inline()) + + doc += "Actives:\n" + for active in self.actives: + doc += "{0}\n".format(active.inline()) + + doc += "Leavers:\n" + for leaver in self.leavers: + doc += "{0]\n".format(leaver.inline()) + + doc += "Excluded:\n" + for exclude in self.excluded: + doc += "{0}\n".format(exclude.inline()) + + doc += "Certifications:\n" + for cert in self.certifications: + doc += "{0}\n".format(cert.inline()) + + doc += "Transactions:\n" + for transaction in self.transactions: + doc += "{0}\n".format(transaction.inline()) + + return doc diff --git a/lib/ucoinpy/documents/certification.py b/lib/ucoinpy/documents/certification.py new file mode 100644 index 00000000..3d758409 --- /dev/null +++ b/lib/ucoinpy/documents/certification.py @@ -0,0 +1,86 @@ +''' +Created on 2 déc. 2014 + +@author: inso +''' +import re + +from . import Document + + +class SelfCertification(Document): + ''' + A document discribing a self certification. + ''' + + re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):([A-Za-z0-9+/]+(?:=|==)?):([0-9]+):([^\n]+)\n") + re_uid = re.compile("UID:([^\n]+)\n") + re_timestamp = re.compile("META:TS:([0-9]+)\n") + + def __init__(self, version, currency, pubkey, ts, uid, signature): + if signature: + super().__init__(version, currency, [signature]) + else: + super().__init__(version, currency, []) + self.pubkey = pubkey + self.timestamp = ts + self.uid = uid + + @classmethod + def from_inline(cls, version, currency, inline): + selfcert_data = SelfCertification.re_inline.match(inline) + pubkey = selfcert_data.group(1) + signature = selfcert_data.group(2) + ts = int(selfcert_data.group(3)) + uid = selfcert_data.group(4) + return cls(version, currency, pubkey, ts, uid, signature) + + def raw(self): + return """UID:{0} +META:TS:{1}""".format(self.uid(), self.ts()) + + def inline(self): + return "{0}:{1}:{2}:{3}".format(self.pubkey, self.signatures[0], + self.timestamp, self.uid) + + +class Certification(Document): + ''' + A document describing a certification. + ''' + + re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):\ +([1-9A-Za-z][^OIl]{42,45}):([0-9]+):([A-Za-z0-9+/]+(?:=|==)?)\n") + re_timestamp = re.compile("META:TS:([0-9]+)-([0-9a-fA-F]{5,40})\n") + + def __init__(self, version, currency, pubkey_from, pubkey_to, + blockhash, blocknumber, signature): + ''' + Constructor + ''' + super().__init__(version, currency, [signature]) + self.pubkey_from = pubkey_from + self.pubkey_to = pubkey_to + self.blockhash = blockhash + self.blocknumber = blocknumber + + @classmethod + def from_inline(cls, version, currency, blockhash, inline): + cert_data = Certification.re_inline.match(inline) + pubkey_from = cert_data.group(1) + pubkey_to = cert_data.group(2) + blocknumber = int(cert_data.group(3)) + if blocknumber == 0: + blockhash = "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709" + signature = cert_data.group(4) + return cls(version, currency, pubkey_from, pubkey_to, + blockhash, blocknumber, signature) + + def raw(self, selfcert): + return """{0} +META:TS:{1}-{2}""".format(selfcert.signed_raw(), self.blockhash, self.blocknumber) + + def inline(self): + return "{0}:{1}:{2}:{3}".format(self.pubkey_from, self.pubkey_to, + self.blocknumber, self.signatures[0]) + diff --git a/lib/ucoinpy/documents/membership.py b/lib/ucoinpy/documents/membership.py new file mode 100644 index 00000000..3f142a7f --- /dev/null +++ b/lib/ucoinpy/documents/membership.py @@ -0,0 +1,125 @@ +''' +Created on 2 déc. 2014 + +@author: inso +''' +from .. import PROTOCOL_VERSION +from . import Document + +import re + + +class Membership(Document): + ''' + This is a utility class to generate membership documents : + Version: VERSION + Type: Membership + Currency: CURRENCY_NAME + Issuer: ISSUER + Block: NUMBER-HASH + Membership: MEMBERSHIP_TYPE + UserID: USER_ID + CertTS: CERTIFICATION_TS + ''' + + # PUBLIC_KEY:SIGNATURE:NUMBER:HASH:TIMESTAMP:USER_ID + re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):([A-Za-z0-9+/]+(?:=|==)?):\ +([0-9]+):([0-9a-fA-F]{5,40}):([0-9]+):([^\n]+)\n") + re_type = re.compile("Type: (Membership)") + re_issuer = re.compile("Issuer: ([1-9A-Za-z][^OIl]{42,45})\n") + re_block = re.compile("Block: ([0-9]+)-([0-9a-fA-F]{5,40})\n") + re_membership_type = re.compile("Membership: (IN|OUT)") + re_userid = re.compile("UserID: ([^\n]+)\n") + re_certts = re.compile("CertTS: ([0-9]+)\n") + + + + def __init__(self, version, currency, issuer, block_number, block_hash, + membership_type, uid, cert_ts, signature): + ''' + Constructor + ''' + if signature: + super().__init__(version, currency, [signature]) + else: + super().__init__(version, currency, []) + self.issuer = issuer + self.block_number = block_number + self.block_hash = block_hash + self.membership_type = membership_type + self.uid = uid + self.cert_ts = cert_ts + + @classmethod + def from_inline(cls, version, currency, membership_type, inline): + data = Membership.re_inline.match(inline) + issuer = data.group(1) + signature = data.group(2) + block_number = int(data.group(3)) + block_hash = data.group(4) + cert_ts = int(data.group(5)) + uid = data.group(6) + return cls(version, currency, issuer, block_number, + block_hash, membership_type, uid, cert_ts, signature) + + @classmethod + def from_signed_raw(cls, raw, signature=None): + lines = raw.splitlines(True) + n = 0 + + version = int(Membership.re_version.match(lines[n]).group(1)) + n = n + 1 + + Membership.re_type.match(lines[n]).group(1) + n = n + 1 + + currency = Membership.re_currency.match(lines[n]).group(1) + n = n + 1 + + issuer = Membership.re_issuer.match(lines[n]).group(1) + n = n + 1 + + blockid = Membership.re_block.match(lines[n]) + blocknumber = int(blockid.group(1)) + blockhash = blockid.group(2) + n = n + 1 + + membership_type = Membership.re_membership_type.match(lines[n]).group(1) + n = n + 1 + + uid = Membership.re_userid.match(lines[n]).group(1) + n = n + 1 + + cert_ts = int(Membership.re_certts.match(lines[n]).group(1)) + n = n + 1 + + signature = Membership.re_signature.match(lines[n]).group(1) + n = n + 1 + + return cls(version, currency, issuer, blocknumber, blockhash, + membership_type, uid, cert_ts, signature) + + def raw(self): + return """Version: {0} +Type: Membership +Currency: {1} +Issuer: {2} +Block: {3}-{4} +Membership: {5} +UserID: {6} +CertTS: {7} +""".format(self.version, + self.currency, + self.issuer, + self.block_number, self.block_hash, + self.membership_type, + self.uid, + self.cert_ts) + + def inline(self): + return "{0}:{1}:{2}:{3}:{4}:{5}".format(self.issuer, + self.signatures[0], + self.block_number, + self.block_hash, + self.cert_ts, + self.uid) diff --git a/lib/ucoinpy/documents/peer.py b/lib/ucoinpy/documents/peer.py new file mode 100644 index 00000000..1148785a --- /dev/null +++ b/lib/ucoinpy/documents/peer.py @@ -0,0 +1,148 @@ +''' +Created on 2 déc. 2014 + +@author: inso +''' + +import re + +from . import Document +from .. import PROTOCOL_VERSION, MANAGED_API + + +class Peer(Document): + """ + Version: VERSION + Type: Peer + Currency: CURRENCY_NAME + PublicKey: NODE_PUBLICKEY + Block: BLOCK + Endpoints: + END_POINT_1 + END_POINT_2 + END_POINT_3 + [...] + """ + + re_type = re.compile("Type: (Peer)") + re_pubkey = re.compile("PublicKey: ([1-9A-Za-z][^OIl]{42,45})\n") + re_block = re.compile("Block: ([0-9]+-[0-9a-fA-F]{5,40})\n") + re_endpoints = re.compile("Endpoints:\n") + + def __init__(self, version, currency, pubkey, blockid, + endpoints, signature): + if signature: + super().__init__(version, currency, [signature]) + else: + super().__init__(version, currency, []) + + self.pubkey = pubkey + self.blockid = blockid + self.endpoints = endpoints + + @classmethod + def from_signed_raw(cls, raw): + lines = raw.splitlines(True) + n = 0 + + version = int(Peer.re_version.match(lines[n]).group(1)) + n = n + 1 + + Peer.re_type.match(lines[n]).group(1) + n = n + 1 + + currency = Peer.re_currency.match(lines[n]).group(1) + n = n + 1 + + pubkey = Peer.re_pubkey.match(lines[n]).group(1) + n = n + 1 + + blockid = Peer.re_block.match(lines[n]).group(1) + n = n + 1 + + Peer.re_endpoints.match(lines[n]) + n = n + 1 + + endpoints = [] + while not Peer.re_signature.match(lines[n]): + endpoint = Endpoint.from_inline(lines[n]) + endpoints.append(endpoint) + n = n + 1 + + signature = Peer.re_signature.match(lines[n]).group(1) + + return cls(version, currency, pubkey, blockid, endpoints, signature) + + def raw(self): + doc = """Version: {0} +Type: Peer +Currency: {1} +PublicKey: {2} +Block: {3} +Endpoints: +""".format(self.version, self.currency, self.pubkey, self.blockid) + + for endpoint in self.endpoints: + doc += "{0}\n".format(endpoint.inline()) + + doc += "{0}\n".format(self.signatures[0]) + return doc + + +class Endpoint(): + """ + Describing endpoints + """ + + @staticmethod + def from_inline(inline): + for api in MANAGED_API: + if (inline.startswith(api)): + if (api == "BASIC_MERKLED_API"): + return BMAEndpoint.from_inline(inline) + return UnknownEndpoint.from_inline(inline) + + +class UnknownEndpoint(Endpoint): + + def __init__(self, api, properties): + self.api = api + self.properties = properties + + @classmethod + def from_inline(cls, inline): + api = inline.split()[0] + properties = inline.split()[1:] + return cls(api, properties) + + def inline(self): + doc = self.api + for p in self.properties: + doc += " {0}".format(p) + return doc + + +class BMAEndpoint(Endpoint): + re_inline = re.compile('^BASIC_MERKLED_API(?: ([a-z_][a-z0-9-_.]+))?(?: ([0-9.]+))?(?: ([0-9a-f:]+))?(?: ([0-9]+))$') + + @classmethod + def from_inline(cls, inline): + m = BMAEndpoint.re_inline.match(inline) + server = m.group(1) + ipv4 = m.group(2) + ipv6 = m.group(3) + port = int(m.group(4)) + return cls(server, ipv4, ipv6, port) + + def __init__(self, server, ipv4, ipv6, port): + self.server = server + self.ipv4 = ipv4 + self.ipv6 = ipv6 + self.port = port + + def inline(self): + return "BASIC_MERKLED_API {DNS} {IPv4} {IPv6} {PORT}" \ + .format(DNS=self.server, + IPv4=self.ipv4, + IPv6=self.ipv6, + PORT=self.port) diff --git a/lib/ucoinpy/documents/status.py b/lib/ucoinpy/documents/status.py new file mode 100644 index 00000000..5b25bfae --- /dev/null +++ b/lib/ucoinpy/documents/status.py @@ -0,0 +1,84 @@ +''' +Created on 2 déc. 2014 + +@author: inso +''' + +import re +from . import Document + + +class Status(Document): + ''' + Version: VERSION + Type: Status + Currency: CURRENCY_NAME + Status: STATUS + Block: BLOCK + From: SENDER + To: RECIPIENT + ''' + + re_type = re.compile("Type: (Status)") + re_status = re.compile("Status: (NEW|NEW_BACK|UP|UP_BACK|DOWN)") + re_block = re.compile("Block: ([0-9]+-[0-9a-fA-F]{5,40})\n") + re_from = re.compile("From: ([1-9A-Za-z][^OIl]{42,45})\n") + re_to = re.compile("To: ([1-9A-Za-z][^OIl]{42,45})\n") + + def __init__(self, version, currency, status, blockid, sender, + recipient, signature): + ''' + Constructor + ''' + if signature: + super().__init__(version, currency, [signature]) + else: + super().__init__(version, currency, []) + + self.status = status + self.blockid = blockid + self.sender = sender + self.recipient = recipient + + @classmethod + def from_signed_raw(cls, raw): + lines = raw.splitlines(True) + n = 0 + + version = int(Status.re_version.match(lines[n]).group(1)) + n = n + 1 + + Status.re_type.match(lines[n]).group(1) + n = n + 1 + + currency = Status.re_currency.match(lines[n]).group(1) + n = n + 1 + + status = Status.re_status.match(lines[n]).group(1) + n = n + 1 + + blockid = Status.re_block.match(lines[n]).group(1) + n = n + 1 + + sender = Status.re_from.match(lines[n]).group(1) + n = n + 1 + + recipient = Status.re_to.match(lines[n]).group(1) + n = n + 1 + + signature = Status.re_signature.match(lines[n]).group(1) + n = n + 1 + + return cls(version, currency, status, blockid, + sender, recipient, signature) + + def raw(self): + return '''Version: {0} +Type: Status +Currency: {1} +Status: {2} +Block: {3} +From: {4} +To: {5} +'''.format(self.version, self.currency, self.status, + self.blockid, self.sender, self.recipient) diff --git a/lib/ucoinpy/documents/transaction.py b/lib/ucoinpy/documents/transaction.py new file mode 100644 index 00000000..e5fd76ed --- /dev/null +++ b/lib/ucoinpy/documents/transaction.py @@ -0,0 +1,297 @@ +''' +Created on 2 déc. 2014 + +@author: inso +''' + +from . import Document +import re + + +class Transaction(Document): + ''' +Document format : +Version: VERSION +Type: Transaction +Currency: CURRENCY_NAME +Issuers: +PUBLIC_KEY +... +Inputs: +INDEX:SOURCE:NUMBER:FINGERPRINT:AMOUNT +... +Outputs: +PUBLIC_KEY:AMOUNT +... +Comment: COMMENT +... + + +Compact format : +TX:VERSION:NB_ISSUERS:NB_INPUTS:NB_OUTPUTS:HAS_COMMENT +PUBLIC_KEY:INDEX +... +INDEX:SOURCE:FINGERPRINT:AMOUNT +... +PUBLIC_KEY:AMOUNT +... +COMMENT +SIGNATURE +... + ''' + + re_type = re.compile("Type: (Transaction)\n") + re_header = re.compile("TX:([0-9])+:([0-9])+:([0-9])+:([0-9])+:(0|1)\n") + re_issuers = re.compile("Issuers:\n") + re_inputs = re.compile("Inputs:\n") + re_outputs = re.compile("Outputs:\n") + re_compact_comment = re.compile("-----@@@-----([^\n]+)\n") + re_comment = re.compile("Comment:(?:)?([^\n]*)\n") + re_pubkey = re.compile("([1-9A-Za-z][^OIl]{42,45})\n") + + def __init__(self, version, currency, issuers, inputs, outputs, + comment, signatures): + ''' + Constructor + ''' + if signatures: + super().__init__(version, currency, signatures) + else: + super().__init__(version, currency, []) + + self.issuers = issuers + self.inputs = inputs + self.outputs = outputs + self.comment = comment + + @classmethod + def from_compact(cls, currency, compact): + lines = compact.splitlines(True) + n = 0 + + header_data = Transaction.re_header.match(lines[n]) + version = int(header_data.group(1)) + issuers_num = int(header_data.group(2)) + inputs_num = int(header_data.group(3)) + outputs_num = int(header_data.group(4)) + n = n + 1 + + issuers = [] + inputs = [] + outputs = [] + signatures = [] + + for i in range(0, issuers_num): + issuer = Transaction.re_pubkey.match(lines[n]).group(1) + issuers.append(issuer) + n = n + 1 + + for i in range(0, inputs_num): + input_source = InputSource.from_inline(lines[n]) + inputs.append(input_source) + n = n + 1 + + for i in range(0, outputs_num): + output_source = OutputSource.from_inline(lines[n]) + outputs.append(output_source) + n = n + 1 + + comment = None + if Transaction.re_comment.match(lines[n]): + comment = Transaction.re_compact_comment.match(lines[n]).group(1) + n = n + 1 + + while n < len(lines): + signatures.append(Transaction.re_signature.match(lines[n]).group(1)) + n = n + 1 + + return cls(version, currency, issuers, inputs, outputs, comment, signatures) + + @classmethod + def from_signed_raw(cls, raw): + lines = raw.splitlines(True) + n = 0 + + version = int(Transaction.re_version.match(lines[n]).group(1)) + n = n + 1 + + Transaction.re_type.match(lines[n]).group(1) + n = n + 1 + + currency = Transaction.re_currency.match(lines[n]).group(1) + n = n + 1 + + issuers = [] + inputs = [] + outputs = [] + signatures = [] + + if Transaction.re_issuers.match(lines[n]): + n = n + 1 + while Transaction.re_inputs.match(lines[n]) is None: + issuer = Transaction.re_pubkey.match(lines[n]).group(1) + issuers.append(issuer) + n = n + 1 + + if Transaction.re_inputs.match(lines[n]): + n = n + 1 + while Transaction.re_outputs.match(lines[n]) is None: + input_source = InputSource.from_inline(lines[n]) + inputs.append(input_source) + n = n + 1 + + if Transaction.re_outputs.match(lines[n]) is not None: + n = n + 1 + while not Transaction.re_comment.match(lines[n]): + output = OutputSource.from_inline(lines[n]) + outputs.append(output) + n = n + 1 + + comment = Transaction.re_comment.match(lines[n]).group(1) + n = n + 1 + + if Transaction.re_signature.match(lines[n]) is not None: + while n < len(lines): + sign = Transaction.re_signature.match(lines[n]).group(1) + signatures.append(sign) + n = n + 1 + + return cls(version, currency, issuers, inputs, outputs, + comment, signatures) + + def raw(self): + doc = """Version: {0} +Type: Transaction +Currency: {1} +Issuers: +""".format(self.version, + self.currency) + + for p in self.issuers: + doc += "{0}\n".format(p) + + doc += "Inputs:\n" + for i in self.inputs: + doc += "{0}\n".format(i.inline()) + + doc += "Outputs:\n" + for o in self.outputs: + doc += "{0}\n".format(o.inline()) + + doc += "Comment: " + if self.comment: + doc += "{0}".format(self.comment) + doc += "\n" + + for signature in self.signatures: + doc += "{0}\n".format(signature) + + return doc + + def compact(self): + ''' + Return a transaction in its compact format. + ''' + """TX:VERSION:NB_ISSUERS:NB_INPUTS:NB_OUTPUTS:HAS_COMMENT +PUBLIC_KEY:INDEX +... +INDEX:SOURCE:FINGERPRINT:AMOUNT +... +PUBLIC_KEY:AMOUNT +... +COMMENT +""" + doc = "TX:{0}:{1}:{2}:{3}:{4}".format(self.version, + self.issuers.len, + self.inputs.len, + self.outputs.len, + '1' if self.Comment else '0') + for pubkey in self.issuers: + doc += "{0}\n".format(pubkey) + for i in self.inputs: + doc += "{0}\n".format(i.compact()) + for o in self.outputs: + doc += "{0}\n".format(o.inline()) + if self.comment: + doc += "-----@@@----- {0}\n".format(self.comment) + for s in self.signatures: + doc += "{0}\n".format(s) + + return doc + + +class SimpleTransaction(Transaction): + ''' +As transaction class, but for only one issuer. +... + ''' + def __init__(self, version, currency, issuer, + single_input, outputs, comment, signature): + ''' + Constructor + ''' + super().__init__(version, currency, [issuer], [single_input], + outputs, comment, [signature]) + + +class InputSource(): + ''' + A Transaction INPUT + + Compact : + INDEX:SOURCE:FINGERPRINT:AMOUNT + ''' + re_inline = re.compile("([0-9]+):(D|T):([0-9]+):\ +([0-9a-fA-F]{5,40}):([0-9]+)\n") + re_compact = re.compile("([0-9]+):(D|T):([0-9a-fA-F]{5,40}):([0-9]+)\n") + + def __init__(self, index, source, number, txhash, amount): + self.index = index + self.source = source + self.number = number + self.txhash = txhash + self.amount = amount + + @classmethod + def from_inline(cls, inline): + data = InputSource.re_inline.match(inline) + index = int(data.group(1)) + source = data.group(2) + number = int(data.group(3)) + txhash = data.group(4) + amount = int(data.group(5)) + return cls(index, source, number, txhash, amount) + + def inline(self): + return "{0}:{1}:{2}:{3}:{4}".format(self.index, + self.source, + self.number, + self.txhash, + self.amount) + + def compact(self): + return "{0}:{1}:{2}:{3}".format(self.index, + self.source, + self.txhash, + self.amount) + + +class OutputSource(): + ''' + A Transaction OUTPUT + ''' + re_inline = re.compile("([1-9A-Za-z][^OIl]{42,45}):([0-9]+)") + + def __init__(self, pubkey, amount): + self.pubkey = pubkey + self.amount = amount + + @classmethod + def from_inline(cls, inline): + data = OutputSource.re_inline.match(inline) + pubkey = data.group(1) + amount = int(data.group(2)) + return cls(pubkey, amount) + + def inline(self): + return "{0}:{1}".format(self.pubkey, self.amount) diff --git a/lib/ucoinpy/key/__init__.py b/lib/ucoinpy/key/__init__.py new file mode 100644 index 00000000..1733e547 --- /dev/null +++ b/lib/ucoinpy/key/__init__.py @@ -0,0 +1,36 @@ +''' +Ucoin public and private keys + +@author: inso +''' + +import base58 +import base64 +import scrypt +from nacl.signing import SigningKey as NaclSigningKey + + +SEED_LENGTH = 32 # Length of the key +crypto_sign_BYTES = 64 +SCRYPT_PARAMS = {'N': 4096, + 'r': 16, + 'p': 1 + } + + +class SigningKey(NaclSigningKey): + def __init__(self, password, salt): + seed = scrypt.hash(password, salt, + SCRYPT_PARAMS['N'], SCRYPT_PARAMS['r'], SCRYPT_PARAMS['p'], + SEED_LENGTH) + seedb64 = base64.b64encode(seed) + + +class Base58Encoder(object): + @staticmethod + def encode(data): + return base58.b58encode(data) + + @staticmethod + def decode(data): + return base58.b58decode(data) diff --git a/lib/ucoinpy/key/hdwallet.py b/lib/ucoinpy/key/hdwallet.py new file mode 100644 index 00000000..dffb5fff --- /dev/null +++ b/lib/ucoinpy/key/hdwallet.py @@ -0,0 +1,378 @@ +''' +HD Wallet inspired from Bip32 wallets. + +@author: inso +''' +''' +import os +import hmac +import hashlib +import ed25519 +import struct +import base58 +import base64 + +from hashlib import sha256 +from ecdsa.curves import SECP256k1 +from ecdsa.ecdsa import int_to_string, string_to_int +from ecdsa.numbertheory import square_root_mod_prime as sqrt_mod + +MIN_ENTROPY_LEN = 128 # bits +HDWALLET_HARDENED = 0x80000000 # choose from hardened set of child keys +CURVE_GEN = ecdsa.ecdsa.generator_secp256k1 +CURVE_ORDER = CURVE_GEN.order() +FIELD_ORDER = SECP256k1.curve.p() +INFINITY = ecdsa.ellipticcurve.INFINITY + + +class HDWalletKey(object): + + # Static initializers to create from entropy or external formats + # + @staticmethod + def fromEntropy(entropy, public=False): + "Create a HDWallet using supplied entropy >= MIN_ENTROPY_LEN" + if entropy == None: + entropy = os.urandom(MIN_ENTROPY_LEN/8) # Python doesn't have os.random() + if not len(entropy) >= MIN_ENTROPY_LEN/8: + raise ValueError("Initial entropy %i must be at least %i bits" % + (len(entropy), MIN_ENTROPY_LEN)) + I = hmac.new("UCoin seed", entropy, hashlib.sha512).digest() + + Il, Ir = I[:32], I[32:] + # FIXME test Il for 0 or less than SECP256k1 prime field order + key = HDWalletKey(secret=Il, chain=Ir, depth=0, index=0, fpr='\0\0\0\0', public=False) + if public: + key.SetPublic() + return key + + @staticmethod + def fromExtendedKey(xkey, public=False): + """ + Create a HDWallet by importing from extended private or public key string + + If public is True, return a public-only key regardless of input type. + """ + # Sanity checks + raw = base58.b58decode_check(xkey) + # To fix + #if len(raw) != 78: + # raise ValueError("extended key format wrong length") + + # Verify address version/type + #version = raw[:4] + #if version == EX_MAIN_PRIVATE: + # raise ValueError("unknown extended key version") + + # Extract remaining fields + depth = ord(raw[4]) + fpr = raw[5:9] + child = struct.unpack(">L", raw[9:13])[0] + chain = raw[13:45] + secret = raw[45:78] + + # Extract private key or public key point + if keytype == 'xprv': + secret = secret[1:] + else: + # Recover public curve point from compressed key + lsb = ord(secret[0]) & 1 + x = string_to_int(secret[1:]) + ys = (x**3+7) % FIELD_ORDER # y^2 = x^3 + 7 mod p + y = sqrt_mod(ys, FIELD_ORDER) + if y & 1 != lsb: + y = FIELD_ORDER-y + point = ecdsa.ellipticcurve.Point(SECP256k1.curve, x, y) + secret = ecdsa.VerifyingKey.from_public_point(point, curve=SECP256k1) + + is_pubkey = (keytype == 'xpub') + key = HDWalletKey(secret=secret, chain=chain, depth=depth, index=child, + fpr=fpr, public=is_pubkey) + if not is_pubkey and public: + key = key.SetPublic() + return key + + + # Normal class initializer + def __init__(self, secret, chain, depth, index, fpr, public=False): + """ + Create a public or private BIP32Key using key material and chain code. + + secret This is the source material to generate the keypair, either a + 32-byte string representation of a private key, or the ECDSA + library object representing a public key. + + chain This is a 32-byte string representation of the chain code + + depth Child depth; parent increments its own by one when assigning this + + index Child index + + fpr Parent fingerprint + + public If true, this keypair will only contain a public key and can only create + a public key chain. + """ + + self.public = public + if public is False: + self.k = ed25519.SigningKey(base58.b58decode(secret)) + self.K = self.k.get_verifying_key() + else: + self.k = None + self.K = secret + + self.C = chain + self.depth = depth + self.index = index + self.parent_fpr = fpr + + # Internal methods not intended to be called externally + def _hmac(self, data): + """ + Calculate the HMAC-SHA512 of input data using the chain code as key. + + Returns a tuple of the left and right halves of the HMAC + """ + I = hmac.new(self.C, data, hashlib.sha512).digest() + return (I[:32], I[32:]) + + def _CKDpriv(self, i): + """ + Create a child key of index 'i'. + + If the most significant bit of 'i' is set, then select from the + hardened key set, otherwise, select a regular child key. + + Returns a BIP32Key constructed with the child key parameters, + or None if i index would result in an invalid key. + """ + # Index as bytes, BE + i_str = struct.pack(">L", i) + + # Data to HMAC + if i & HDWALLET_HARDENED: + data = b'\0' + self.k.to_string() + i_str + else: + data = self.PublicKey() + i_str + # Get HMAC of data + (Il, Ir) = self._hmac(data) + + # Construct new key material from Il and current private key + Il_int = string_to_int(Il) + if Il_int > CURVE_ORDER: + return None + pvt_int = string_to_int(self.k.to_string()) + k_int = (Il_int + pvt_int) % CURVE_ORDER + if (k_int == 0): + return None + secret = (b'\0'*32 + int_to_string(k_int))[-32:] + + # Construct and return a new BIP32Key + return HDWalletKey(secret=secret, chain=Ir, depth=self.depth+1, + index=i, fpr=self.Fingerprint(), public=False) + + def _CKDpub(self, i): + """ + Create a publicly derived child key of index 'i'. + + If the most significant bit of 'i' is set, this is + an error. + + Returns a HDWalletKey constructed with the child key parameters, + or None if index would result in invalid key. + """ + + if i & HDWALLET_HARDENED: + raise Exception("Cannot create a hardened child key using public child derivation") + + # Data to HMAC. Same as CKDpriv() for public child key. + data = self.PublicKey() + struct.pack(">L", i) + + # Get HMAC of data + (Il, Ir) = self.hmac(data) + + # Construct curve point Il*G+K + Il_int = string_to_int(Il) + if Il_int >= CURVE_ORDER: + return None + point = Il_int*CURVE_GEN + self.K.pubkey.point + if point == INFINITY: + return None + + # Retrieve public key based on curve point + K_i = ed25519.VerifyingKey.from_public_point(point, curve=SECP256k1) + + # Construct and return a new BIP32Key + return HDWalletKey(secret=K_i, chain=Ir, depth=self.depth, index=i, fpr=self.Fingerprint(), public=True) + + + # Public methods + # + def ChildKey(self, i): + """ + Create and return a child key of this one at index 'i'. + + The index 'i' should be summed with BIP32_HARDEN to indicate + to use the private derivation algorithm. + """ + if self.public is False: + return self.CKDpriv(i) + else: + return self.CKDpub(i) + + + def SetPublic(self): + "Convert a private BIP32Key into a public one" + self.k = None + self.public = True + + + def PrivateKey(self): + "Return private key as string" + if self.public: + raise Exception("Publicly derived deterministic keys have no private half") + else: + return self.k.to_string() + + + def PublicKey(self): + "Return compressed public key encoding" + if self.K.pubkey.point.y() & 1: + ck = b'\3'+int_to_string(self.K.pubkey.point.x()) + else: + ck = b'\2'+int_to_string(self.K.pubkey.point.x()) + return ck + + + def ChainCode(self): + "Return chain code as string" + return self.C + + + def Identifier(self): + "Return key identifier as string" + cK = self.PublicKey() + return hashlib.new('ripemd160', sha256(cK).digest()).digest() + + + def Fingerprint(self): + "Return key fingerprint as string" + return self.Identifier()[:4] + + + def Address(self): + "Return compressed public key address" + vh160 = '\x00'+self.Identifier() + return Base58.check_encode(vh160) + + + def WalletImportFormat(self): + "Returns private key encoded for wallet import" + if self.public: + raise Exception("Publicly derived deterministic keys have no private half") + raw = '\x80' + self.k.to_string() + '\x01' # Always compressed + return Base58.check_encode(raw) + + + def ExtendedKey(self, private=True, encoded=True): + "Return extended private or public key as string, optionally Base58 encoded" + if self.public is True and private is True: + raise Exception("Cannot export an extended private key from a public-only deterministic key") + version = EX_MAIN_PRIVATE if private else EX_MAIN_PUBLIC + depth = chr(self.depth) + fpr = self.parent_fpr + child = struct.pack('>L', self.index) + chain = self.C + if self.public is True or private is False: + data = self.PublicKey() + else: + data = '\x00' + self.PrivateKey() + raw = version+depth+fpr+child+chain+data + if not encoded: + return raw + else: + return Base58.check_encode(raw) + + # Debugging methods + # + def dump(self): + "Dump key fields mimicking the BIP0032 test vector format" + print " * Identifier" + print " * (hex): ", self.Identifier().encode('hex') + print " * (fpr): ", self.Fingerprint().encode('hex') + print " * (main addr):", self.Address() + if self.public is False: + print " * Secret key" + print " * (hex): ", self.PrivateKey().encode('hex') + print " * (wif): ", self.WalletImportFormat() + print " * Public key" + print " * (hex): ", self.PublicKey().encode('hex') + print " * Chain code" + print " * (hex): ", self.C.encode('hex') + print " * Serialized" + print " * (pub hex): ", self.ExtendedKey(private=False, encoded=False).encode('hex') + print " * (prv hex): ", self.ExtendedKey(private=True, encoded=False).encode('hex') + print " * (pub b58): ", self.ExtendedKey(private=False, encoded=True) + print " * (prv b58): ", self.ExtendedKey(private=True, encoded=True) + + +if __name__ == "__main__": + import sys + + # BIP0032 Test vector 1 + entropy='000102030405060708090A0B0C0D0E0F'.decode('hex') + m = BIP32Key.fromEntropy(entropy) + print "Test vector 1:" + print "Master (hex):", entropy.encode('hex') + print "* [Chain m]" + m.dump() + + print "* [Chain m/0h]" + m = m.ChildKey(0+BIP32_HARDEN) + m.dump() + + print "* [Chain m/0h/1]" + m = m.ChildKey(1) + m.dump() + + print "* [Chain m/0h/1/2h]" + m = m.ChildKey(2+BIP32_HARDEN) + m.dump() + + print "* [Chain m/0h/1/2h/2]" + m = m.ChildKey(2) + m.dump() + + print "* [Chain m/0h/1/2h/2/1000000000]" + m = m.ChildKey(1000000000) + m.dump() + + # BIP0032 Test vector 2 + entropy = 'fffcf9f6f3f0edeae7e4e1dedbd8d5d2cfccc9c6c3c0bdbab7b4b1aeaba8a5a29f9c999693908d8a8784817e7b7875726f6c696663605d5a5754514e4b484542'.decode('hex') + m = BIP32Key.fromEntropy(entropy) + print "Test vector 2:" + print "Master (hex):", entropy.encode('hex') + print "* [Chain m]" + m.dump() + + print "* [Chain m/0]" + m = m.ChildKey(0) + m.dump() + + print "* [Chain m/0/2147483647h]" + m = m.ChildKey(2147483647+BIP32_HARDEN) + m.dump() + + print "* [Chain m/0/2147483647h/1]" + m = m.ChildKey(1) + m.dump() + + print "* [Chain m/0/2147483647h/1/2147483646h]" + m = m.ChildKey(2147483646+BIP32_HARDEN) + m.dump() + + print "* [Chain m/0/2147483647h/1/2147483646h/2]" + m = m.ChildKey(2) + m.dump() +''' \ No newline at end of file -- GitLab