Add filtering
This commit is contained in:
parent
2f22d6df1a
commit
da54a077e5
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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))
|
@ -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)
|
@ -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)
|
@ -1,6 +0,0 @@
|
||||
class Tx:
|
||||
|
||||
def __init__(self, block, tx_number, obj):
|
||||
self.block = block
|
||||
self.tx_number = tx_number
|
||||
self.obj = obj
|
40
chainsyncer/db/models/filter.py
Normal file
40
chainsyncer/db/models/filter.py
Normal file
@ -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
|
@ -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
|
||||
|
34
chainsyncer/filter.py
Normal file
34
chainsyncer/filter.py
Normal file
@ -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'
|
@ -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))
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
[syncer]
|
||||
loop_interval = 1
|
||||
chain_spec =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
);
|
||||
|
13
sql/sqlite/1.sql
Normal file
13
sql/sqlite/1.sql
Normal file
@ -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
|
||||
);
|
10
sql/sqlite/2.sql
Normal file
10
sql/sqlite/2.sql
Normal file
@ -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)
|
||||
);
|
46
tests/base.py
Normal file
46
tests/base.py
Normal file
@ -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)
|
||||
|
||||
|
22
tests/test_basic.py
Normal file
22
tests/test_basic.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user