chainsyncer

Blockchain syncer driver
Log | Files | Refs | LICENSE

commit da54a077e5a6850e9620afb6a39a39e5a7c11241
parent 2f22d6df1a001b23f5044a3516c00854aedd6f38
Author: nolash <dev@holbrook.no>
Date:   Wed, 17 Feb 2021 12:44:35 +0100

Add filtering

Diffstat:
Mchainsyncer/backend.py | 9++++++---
Dchainsyncer/client/__init__.py | 0
Dchainsyncer/client/block.py | 16----------------
Dchainsyncer/client/evm/response.py | 52----------------------------------------------------
Dchainsyncer/client/evm/websocket.py | 90-------------------------------------------------------------------------------
Dchainsyncer/client/translate.py | 10----------
Dchainsyncer/client/tx.py | 6------
Achainsyncer/db/models/filter.py | 40++++++++++++++++++++++++++++++++++++++++
Mchainsyncer/driver.py | 52++++++++++++++++++++++++++++++----------------------
Achainsyncer/filter.py | 34++++++++++++++++++++++++++++++++++
Mchainsyncer/runnable/tracker.py | 30+++++++++++++++++++++++-------
Mconfig/syncer.ini | 1+
Mrequirements.txt | 8+++-----
Msetup.cfg | 2+-
Msql/postgresql/1.sql | 14++++++++++++++
Asql/sqlite/1.sql | 13+++++++++++++
Asql/sqlite/2.sql | 10++++++++++
Atests/base.py | 46++++++++++++++++++++++++++++++++++++++++++++++
Atests/test_basic.py | 22++++++++++++++++++++++
19 files changed, 243 insertions(+), 212 deletions(-)

diff --git a/chainsyncer/backend.py b/chainsyncer/backend.py @@ -2,6 +2,9 @@ import logging import uuid +# third-party imports +from chainlib.chain import ChainSpec + # local imports from chainsyncer.db.models.sync import BlockchainSync from chainsyncer.db.models.base import SessionBase @@ -176,7 +179,7 @@ class SyncerBackend: @staticmethod - def live(chain, block_height): + def live(chain_spec, block_height): """Creates a new open-ended syncer session starting at the given block height. :param chain: Chain spec of chain that syncer is running for. @@ -188,13 +191,13 @@ class SyncerBackend: """ object_id = None session = SessionBase.create_session() - o = BlockchainSync(chain, block_height, 0, None) + o = BlockchainSync(str(chain_spec), block_height, 0, None) session.add(o) session.commit() object_id = o.id session.close() - return SyncerBackend(chain, object_id) + return SyncerBackend(chain_spec, object_id) class MemBackend: diff --git a/chainsyncer/client/__init__.py b/chainsyncer/client/__init__.py diff --git a/chainsyncer/client/block.py b/chainsyncer/client/block.py @@ -1,16 +0,0 @@ -class Block: - - def __init__(self, hsh, obj): - self.hash = hsh - self.obj = obj - - - def tx(self, idx): - return NotImplementedError - - - def number(self): - return NotImplementedError - - - diff --git a/chainsyncer/client/evm/response.py b/chainsyncer/client/evm/response.py @@ -1,52 +0,0 @@ -import json - -from chainsyncer.client import translate -from chainsyncer.client.block import Block -from chainsyncer.client.tx import Tx - - -translations = { - 'block_number': translate.hex_to_int, - 'get_block': json.dumps, - 'number': translate.hex_to_int, - } - - -class EVMResponse: - - def __init__(self, item, response_object): - self.response_object = response_object - self.item = item - self.fn = translations[self.item] - - - def get_error(self): - return self.response_object.get('error') - - - def get_result(self): - r = self.fn(self.response_object.get('result')) - if r == 'null': - return None - return r - - -class EVMTx(Tx): - - def __init__(self, block, tx_number, obj): - super(EVMTx, self).__init__(block, tx_number, obj) - - -class EVMBlock(Block): - - def tx(self, idx): - o = self.obj['transactions'][idx] - return Tx(self, idx, o) - - - def number(self): - return translate.hex_to_int(self.obj['number']) - - - def __str__(self): - return str('block {} {}'.format(self.number(), self.hash)) diff --git a/chainsyncer/client/evm/websocket.py b/chainsyncer/client/evm/websocket.py @@ -1,90 +0,0 @@ -# standard imports -import logging -import uuid -import json - -# third-party imports -import websocket -from hexathon import add_0x - -# local imports -from .response import EVMResponse -from chainsyncer.error import RequestError -from chainsyncer.client.evm.response import EVMBlock - -logg = logging.getLogger() - - -class EVMWebsocketClient: - - def __init__(self, url): - self.url = url - self.conn = websocket.create_connection(url) - - - def __del__(self): - self.conn.close() - - - def block_number(self): - req_id = str(uuid.uuid4()) - req = { - 'jsonrpc': '2.0', - 'method': 'eth_blockNumber', - 'id': str(req_id), - 'params': [], - } - self.conn.send(json.dumps(req)) - r = self.conn.recv() - res = EVMResponse('block_number', json.loads(r)) - err = res.get_error() - if err != None: - raise RequestError(err) - - return res.get_result() - - - def block_by_integer(self, n): - req_id = str(uuid.uuid4()) - nhx = '0x' + n.to_bytes(8, 'big').hex() - req = { - 'jsonrpc': '2.0', - 'method': 'eth_getBlockByNumber', - 'id': str(req_id), - 'params': [nhx, False], - } - self.conn.send(json.dumps(req)) - r = self.conn.recv() - res = EVMResponse('get_block', json.loads(r)) - err = res.get_error() - if err != None: - raise RequestError(err) - - j = res.get_result() - if j == None: - return None - o = json.loads(j) - return EVMBlock(o['hash'], o) - - - def block_by_hash(self, hx_in): - req_id = str(uuid.uuid4()) - hx = add_0x(hx_in) - req ={ - 'jsonrpc': '2.0', - 'method': 'eth_getBlockByHash', - 'id': str(req_id), - 'params': [hx, False], - } - self.conn.send(json.dumps(req)) - r = self.conn.recv() - res = EVMResponse('get_block', json.loads(r)) - err = res.get_error() - if err != None: - raise RequestError(err) - - j = res.get_result() - if j == None: - return None - o = json.loads(j) - return EVMBlock(o['hash'], o) diff --git a/chainsyncer/client/translate.py b/chainsyncer/client/translate.py @@ -1,10 +0,0 @@ -# third-party imports -from hexathon import strip_0x - - -def hex_to_int(hx, endianness='big'): - hx = strip_0x(hx) - if len(hx) % 2 == 1: - hx = '0' + hx - b = bytes.fromhex(hx) - return int.from_bytes(b, endianness) diff --git a/chainsyncer/client/tx.py b/chainsyncer/client/tx.py @@ -1,6 +0,0 @@ -class Tx: - - def __init__(self, block, tx_number, obj): - self.block = block - self.tx_number = tx_number - self.obj = obj diff --git a/chainsyncer/db/models/filter.py b/chainsyncer/db/models/filter.py @@ -0,0 +1,40 @@ +# standard imports +import hashlib + +# third-party imports +from sqlalchemy import Column, String, Integer, BLOB +from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method + +# local imports +from .base import SessionBase + + +zero_digest = '{:<064s'.format('0') + + +class BlockchainSyncFilter(SessionBase): + + __tablename__ = 'chain_sync_filter' + + chain_sync_id = Column(Integer, ForeignKey='chain_sync.id') + flags = Column(BLOB) + digest = Column(String) + count = Column(Integer) + + @staticmethod + def set(self, names): + + + def __init__(self, names, chain_sync, digest=None): + if len(names) == 0: + digest = zero_digest + elif digest == None: + h = hashlib.new('sha256') + for n in names: + h.update(n.encode('utf-8') + b'\x00') + z = h.digest() + digest = z.hex() + self.digest = digest + self.count = len(names) + self.flags = bytearray((len(names) -1 ) / 8 + 1) + self.chain_sync_id = chain_sync.id diff --git a/chainsyncer/driver.py b/chainsyncer/driver.py @@ -3,6 +3,15 @@ import uuid import logging import time +# external imports +from chainlib.eth.block import ( + block_by_number, + Block, + ) + +# local imports +from chainsyncer.filter import SyncFilter + logg = logging.getLogger() @@ -19,7 +28,7 @@ class Syncer: self.cursor = None self.running = True self.backend = backend - self.filter = [] + self.filter = SyncFilter() self.progress_callback = progress_callback @@ -33,64 +42,63 @@ class Syncer: def add_filter(self, f): - self.filter.append(f) + self.filter.add(f) -class MinedSyncer(Syncer): +class BlockSyncer(Syncer): - def __init__(self, backend, progress_callback): - super(MinedSyncer, self).__init__(backend, progress_callback) + def __init__(self, backend, progress_callback=noop_progress): + super(BlockSyncer, self).__init__(backend, progress_callback) - def loop(self, interval, getter): + def loop(self, interval, conn): g = self.backend.get() last_tx = g[1] last_block = g[0] self.progress_callback('loop started', last_block, last_tx) while self.running and Syncer.running_global: while True: - block = self.get(getter) - if block == None: + try: + block = self.get(conn) + except Exception: break last_block = block.number - self.process(getter, block) + self.process(conn, block) start_tx = 0 self.progress_callback('processed block {}'.format(self.backend.get()), last_block, last_tx) time.sleep(self.yield_delay) - #self.progress_callback('loop ended', last_block + 1, last_tx) + self.progress_callback('loop ended', last_block + 1, last_tx) time.sleep(interval) -class HeadSyncer(MinedSyncer): +class HeadSyncer(BlockSyncer): - def __init__(self, backend, progress_callback): + def __init__(self, backend, progress_callback=noop_progress): super(HeadSyncer, self).__init__(backend, progress_callback) - def process(self, getter, block): + def process(self, conn, block): logg.debug('process block {}'.format(block)) i = 0 tx = None while True: try: - #self.filter[0].handle(getter, block, None) tx = block.tx(i) self.progress_callback('processing {}'.format(repr(tx)), block.number, i) self.backend.set(block.number, i) - for f in self.filter: - f.handle(getter, block, tx) - self.progress_callback('applied filter {} on {}'.format(f.name(), repr(tx)), block.number, i) + self.filter.apply(conn, block, tx) except IndexError as e: self.backend.set(block.number + 1, 0) break i += 1 - def get(self, getter): + def get(self, conn): (block_number, tx_number) = self.backend.get() block_hash = [] - uu = uuid.uuid4() - res = getter.get(block_number) - logg.debug('get {}'.format(res)) + o = block_by_number(block_number) + r = conn.do(o) + b = Block(r) + logg.debug('get {}'.format(b)) - return res + return b diff --git a/chainsyncer/filter.py b/chainsyncer/filter.py @@ -0,0 +1,34 @@ +# standard imports +import logging + +logg = logging.getLogger(__name__) + +class SyncFilter: + + def __init__(self, safe=True): + self.safe = safe + self.filters = [] + + + def add(self, fltr): + if getattr(fltr, 'filter') == None: + raise ValueError('filter object must implement have method filter') + logg.debug('added filter {}'.format(str(fltr))) + + self.filters.append(fltr) + + + def apply(self, conn, block, tx): + for f in self.filters: + logg.debug('applying filter {}'.format(str(f))) + f.filter(conn, block, tx) + + +class NoopFilter(SyncFilter): + + def filter(self, conn, block, tx): + logg.debug('noop filter :received\n{} {}'.format(block, tx)) + + + def __str__(self): + return 'noopfilter' diff --git a/chainsyncer/runnable/tracker.py b/chainsyncer/runnable/tracker.py @@ -7,14 +7,19 @@ import argparse import sys import re -# third-party imports +# external imports import confini from chainlib.eth.connection import HTTPConnection +from chainlib.eth.block import block_latest +from chainlib.chain import ChainSpec + +# local imports from chainsyncer.driver import HeadSyncer from chainsyncer.db import dsn_from_config from chainsyncer.db.models.base import SessionBase from chainsyncer.backend import SyncerBackend from chainsyncer.error import LoopDone +from chainsyncer.filter import NoopFilter logging.basicConfig(level=logging.WARNING) logg = logging.getLogger() @@ -39,6 +44,7 @@ argparser.add_argument('-c', type=str, default=config_dir, help='config root to argparser.add_argument('-i', '--chain-spec', type=str, dest='i', help='chain spec') argparser.add_argument('--abi-dir', dest='abi_dir', type=str, help='Directory containing bytecode and abi') argparser.add_argument('--env-prefix', default=os.environ.get('CONFINI_ENV_PREFIX'), dest='env_prefix', type=str, help='environment prefix for variables to overwrite configuration') +argparser.add_argument('--offset', type=int, help='block number to start sync') argparser.add_argument('-q', type=str, default='cic-eth', help='celery queue to submit transaction tasks to') argparser.add_argument('-v', help='be verbose', action='store_true') argparser.add_argument('-vv', help='be more verbose', action='store_true') @@ -55,7 +61,7 @@ config = confini.Config(config_dir, args.env_prefix) config.process() # override args args_override = { - 'CHAIN_SPEC': getattr(args, 'i'), + 'SYNCER_CHAIN_SPEC': getattr(args, 'i'), 'ETH_PROVIDER': getattr(args, 'p'), } config.dict_override(args_override, 'cli flag') @@ -70,19 +76,29 @@ queue = args.q dsn = dsn_from_config(config) SessionBase.connect(dsn) -c = HTTPConnection(config.get('ETH_PROVIDER')) -chain = config.get('CHAIN_SPEC') +conn = HTTPConnection(config.get('ETH_PROVIDER')) + +chain = ChainSpec.from_chain_str(config.get('SYNCER_CHAIN_SPEC')) + +block_offset = args.offset def main(): - block_offset = c.block_number() + global block_offset + + if block_offset == None: + o = block_latest() + r = conn.do(o) + block_offset = r[1] syncer_backend = SyncerBackend.live(chain, 0) syncer = HeadSyncer(syncer_backend) + fltr = NoopFilter() + syncer.add_filter(fltr) try: - logg.debug('block offset {} {}'.format(block_offset, c)) - syncer.loop(int(config.get('SYNCER_LOOP_INTERVAL')), c) + logg.debug('block offset {}'.format(block_offset)) + syncer.loop(int(config.get('SYNCER_LOOP_INTERVAL')), conn) except LoopDone as e: sys.stderr.write("sync '{}' done at block {}\n".format(args.mode, e)) diff --git a/config/syncer.ini b/config/syncer.ini @@ -1,2 +1,3 @@ [syncer] loop_interval = 1 +chain_spec = diff --git a/requirements.txt b/requirements.txt @@ -1,8 +1,6 @@ psycopg2==2.8.6 SQLAlchemy==1.3.20 -py-evm==0.3.0a20 -eth-tester==0.5.0b3 -confini==0.3.6b2 +confini~=0.3.6b2 semver==2.13.0 -hexathon==0.0.1a2 -chainlib~=0.0.1a7 +hexathon~=0.0.1a3 +chainlib~=0.0.1a13 diff --git a/setup.cfg b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = chainsyncer -version = 0.0.1a5 +version = 0.0.1a6 description = Generic blockchain syncer driver author = Louis Holbrook author_email = dev@holbrook.no diff --git a/sql/postgresql/1.sql b/sql/postgresql/1.sql @@ -6,7 +6,21 @@ CREATE TABLE IF NOT EXISTS chain_sync ( tx_start int not null default 0, block_cursor int not null default 0, tx_cursor int not null default 0, + flags bytea not null, + num_flags int not null, block_target int default null, date_created timestamp not null, date_updated timestamp default null ); + +DROP TABLE chain_sync_filter; +CREATE TABLE IF NOT EXISTS chain_sync_filter ( + id serial primary key not null, + chain_sync_id int not null, + flags bytea default null, + count int not null default 0, + digest char(64) not null default '0000000000000000000000000000000000000000000000000000000000000000', + CONSTRAINT fk_chain_sync + FOREIGN KEY(chain_sync_id) + REFERENCES chain_sync(id) +); diff --git a/sql/sqlite/1.sql b/sql/sqlite/1.sql @@ -0,0 +1,13 @@ +CREATE TABLE IF NOT EXISTS chain_sync ( + id serial primary key not null, + blockchain varchar not null, + block_start int not null default 0, + tx_start int not null default 0, + block_cursor int not null default 0, + tx_cursor int not null default 0, + flags bytea not null, + num_flags int not null, + block_target int default null, + date_created timestamp not null, + date_updated timestamp default null +); diff --git a/sql/sqlite/2.sql b/sql/sqlite/2.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS chain_sync_filter ( + id serial primary key not null, + chain_sync_id int not null, + flags bytea default null, + count int not null default 0, + digest char(64) not null default '0000000000000000000000000000000000000000000000000000000000000000', + CONSTRAINT fk_chain_sync + FOREIGN KEY(chain_sync_id) + REFERENCES chain_sync(id) +); diff --git a/tests/base.py b/tests/base.py @@ -0,0 +1,46 @@ +import unittest +import tempfile +import os +#import pysqlite + +from chainsyncer.db import dsn_from_config +from chainsyncer.db.models.base import SessionBase + +script_dir = os.path.realpath(os.path.dirname(__file__)) + + +class TestBase(unittest.TestCase): + + def setUp(self): + db_dir = tempfile.mkdtemp() + self.db_path = os.path.join(db_dir, 'test.sqlite') + config = { + 'DATABASE_ENGINE': 'sqlite', + 'DATABASE_DRIVER': 'pysqlite', + 'DATABASE_NAME': self.db_path, + } + dsn = dsn_from_config(config) + SessionBase.poolable = False + SessionBase.transactional = False + SessionBase.procedural = False + SessionBase.connect(dsn, debug=True) + + f = open(os.path.join(script_dir, '..', 'sql', 'sqlite', '1.sql'), 'r') + sql = f.read() + f.close() + + conn = SessionBase.engine.connect() + conn.execute(sql) + + f = open(os.path.join(script_dir, '..', 'sql', 'sqlite', '2.sql'), 'r') + sql = f.read() + f.close() + + conn = SessionBase.engine.connect() + conn.execute(sql) + + def tearDown(self): + SessionBase.disconnect() + os.unlink(self.db_path) + + diff --git a/tests/test_basic.py b/tests/test_basic.py @@ -0,0 +1,22 @@ +# standard imports +import unittest + +# external imports +from chainlib.chain import ChainSpec + +# local imports +from chainsyncer.backend import SyncerBackend + +# testutil imports +from tests.base import TestBase + + +class TestBasic(TestBase): + + def test_hello(self): + chain_spec = ChainSpec('evm', 'bloxberg', 8996, 'foo') + backend = SyncerBackend(chain_spec, 'foo') + + +if __name__ == '__main__': + unittest.main()