From 14e656379c89c279265385d2dd60b2c6e889b401 Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Sun, 17 Jun 2018 11:06:55 +0200 Subject: [PATCH] Initial commit --- namekrs.py | 105 +++++++++++++++++++++++++++++++++ timestamper.py | 156 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 261 insertions(+) create mode 100644 namekrs.py create mode 100644 timestamper.py diff --git a/namekrs.py b/namekrs.py new file mode 100644 index 0000000..768e56a --- /dev/null +++ b/namekrs.py @@ -0,0 +1,105 @@ +import datetime +import json +import argparse +import logging + +import requests +from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException +from timestamper import rpcurl_from_config + +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(levelname)s] %(message)s') + +class NamecoinStore(object): + identity = None + + def __init__(self, url='http://127.0.0.1:8336', identity=None): + self.client = AuthServiceProxy(url) + self.identity = identity + + def set(self, key, value): + name = self.identity.format(key) + data = json.dumps(value) + + reg_txid, nonce = self.client.name_new(name) + logging.debug('reg_txid, nonce: %r %r', reg_txid, nonce) + txid = self.client.name_firstupdate(name, nonce, reg_txid, data) + logging.debug('txid: %r', txid) + return txid + + def get(self, key): + try: + hist = self.client.name_history(self.identity.format(key)) + except JSONRPCException as exc: + # FIXME maybe? + if str(exc).split(': ')[0] == '-4': + logging.exception('No record found') + return None + + raise + + last_tx = self.client.gettransaction(hist[-1]['txid']) + + return { + 'history': hist, + 'last_tx': last_tx + } + +def bind_parser(p): + def deco(func): + p.set_defaults(func=func) + return func + return deco + +parser = argparse.ArgumentParser() +parser.add_argument('--rpc', help='namecoind RPC URL (defaults to data obtained from ~/.namecoin.conf)') +sub = parser.add_subparsers() + +verify_parser = sub.add_parser('verify', help='lookup KRS database') +verify_parser.add_argument('id', help='lookup ID') + +@bind_parser(verify_parser) +def verify(args, store): + print(store.get(args.id)) + +publish_parser = sub.add_parser('publish', help='publish mojepanstwo.pl data') +publish_parser.add_argument('--limit', type=int, help='limit number of records', default=20) +publish_parser.add_argument('--page', type=int, help='start with specified page', default=1) +publish_parser.add_argument('-n', '--dry-run', action='store_true', help='only parse data') + +@bind_parser(publish_parser) +def publish(args, store): + output = {} + + data = requests.get('https://api-v3.mojepanstwo.pl/dane/krs_podmioty.json', { + 'limit': args.limit, + 'page': args.page + }).json() + logging.info('Total objects: %d, selected: %d', data['Count'], len(data['Dataobject'])) + + for obj in data['Dataobject']: + if obj['data']['krs_podmioty.nip'] == '0': + logging.info('Ignoring %s (no NIP/VATID)', obj['mp_url']) + continue + k = obj['data']['krs_podmioty.nip'] + + output[k] = obj['mp_url'] + logging.info('%s => %r', k, output[k]) + + if args.dry_run: + logging.info('Dry run, not publishing') + return + + for k, v in output.items(): + logging.info('Publishing %s => %r', k, v) + store.set(k, v) + + +def main(): + args = parser.parse_args() + store = NamecoinStore( + args.rpc or rpcurl_from_config('namecoin', ''), + 'krs-test/{}') + args.func(args, store) + +if __name__ == "__main__": + main() diff --git a/timestamper.py b/timestamper.py new file mode 100644 index 0000000..0da17ad --- /dev/null +++ b/timestamper.py @@ -0,0 +1,156 @@ +from bitcoinrpc.authproxy import AuthServiceProxy, JSONRPCException +from datetime import datetime +import json +import hashlib +import sys +import platform +import os + + +class Timestamper(object): + """Basic timestamping interface""" + + def verify(self, digest): + """Checks if digest (hash) is present in selected blockchain and returns + its timestamp and (unique) transaction hash/id in which it has been + announced. Returns None if it has not been found.""" + raise NotImplemented + + def publish(self, digest): + """Publishes digest onto selected blockchain and returns transaction + hash/id (which should match with one reported by verify call)""" + raise NotImplemented + + def hash_file(self, fd): + """Returns sha256 hash of provided open fd.""" + + filehash = hashlib.sha256() + + for chunk in iter(lambda: fd.read(8192), b""): + filehash.update(chunk) + + return filehash.hexdigest() + + def verify_file(self, fd): + """Verifies file from open fd using sha256.""" + + return self.verify(self.hash_file(fd)) + + def publish_file(self, fd): + """Publishes file from open fd using sha256.""" + + return self.publish(self.hash_file(fd)) + + +class NamecoinTimestamper(Timestamper): + """Simple Namecoin proof-of-existence timestamper implementation based on + poe/ identity prefix. + """ + + IDENTITY = 'poe/{}' + + def __init__(self, url='http://127.0.0.1:8336'): + self.client = AuthServiceProxy(url) + + def verify(self, digest): + """Namecoin poe/ verification implementation""" + + try: + hist = self.client.name_history(self.IDENTITY.format(digest)) + except JSONRPCException as exc: + # FIXME maybe? + if str(exc).split(': ')[0] == '-4': + return None + + raise + + txid = hist[0]['txid'] + tx = self.client.gettransaction(txid) + + return { + 'txid': txid, + 'timestamp': datetime.utcfromtimestamp(tx['time']), + } + + def publish(self, digest): + """Namecoin poe/ publishing implementation""" + + name = self.IDENTITY.format(digest) + reg_txid, nonce = self.client.name_new(name) + + txid = self.client.name_firstupdate(name, nonce, reg_txid, json.dumps({ + 'ver': 0, + })) + + return txid + +def parse_bitcoin_conf(fd): + """Returns dict from bitcoin.conf-like configuration file from open fd""" + conf = {} + for l in fd: + l = l.strip() + if not l.startswith('#') and '=' in l: + key, value = l.split('=', 1) + conf[key] = value + + return conf + +def coin_config_path(coin): + """Returns bitcoin.conf-like configuration path for provided coin""" + + paths = { + # FIXME use proper AppData path + 'Windows': '~\AppData\Roaming\{0}\{0}.conf', + 'Darwin': '~/Library/Application Support/{0}/{0}.conf', + + # Fallback path (Linux, FreeBSD...) + None: '~/.{0}/{0}.conf', + } + + path = paths.get(platform.system(), paths[None]) + + return os.path.expanduser(path.format(coin)) + +def rpcurl_from_config(coin, default=None, config_path=None): + """Returns RPC URL loaded from bitcoin.conf-like configuration of desired + currency""" + + config_path = config_path or coin_config_path(coin) + cookie_path = os.path.join(os.path.dirname(config_path), '.cookie') + + credentials = '' + + try: + with open(config_path) as fd: + conf = parse_bitcoin_conf(fd) + if 'rpcpassword' in conf: + # Password authentication + credentials = '{rpcuser}:{rpcpassword}'.format(**conf) + elif os.path.exists(cookie_path): + # Cookie authentication + with open(cookie_path) as cfd: + credentials = cfd.read().decode('utf-8').strip() \ + .replace('/', '%2F') + else: + return default + + return 'http://{0}@127.0.0.1:{1}/' \ + .format(credentials, conf.get('rpcport', 8336)) + except: + return default + +def main(): + ts = NamecoinTimestamper(rpcurl_from_config('namecoin', 'http://127.0.0.1:8336/')) + + for f in sys.argv[1:]: + with open(f) as fd: + timestamp = ts.verify_file(fd) + + if timestamp: + print('File {} found at: {}'.format(f, timestamp['timestamp'])) + else: + print('File {} not found'.format(f)) + + +if __name__ == "__main__": + main()