Compare commits
9 Commits
e3325eac3f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73fa74a35b | ||
|
|
864b78e872 | ||
|
|
2753a5d2d9 | ||
|
|
3fb5868dd4 | ||
|
|
f2ebb049e4 | ||
|
|
279fea7276 | ||
|
|
1c9134d87f | ||
|
|
1957cec35a | ||
|
|
a9b201c04d |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
73
README.md
Normal file
73
README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# iavlread - extract data from the state of a Cosmos SDK blockchain
|
||||
|
||||
This is a simple tool read state data from the snapshot of a [Cosmos SDK] block chain. The state is stored in the `application.db` leveldb database, in the form of an [IAVL tree]. This tool walks the IAVL tree to get values from the state, at a desired block height.
|
||||
|
||||
### Installation
|
||||
|
||||
It's really just two Python files. `iavlread` is the CLI tool, and `iavltree.py` is the library which actually handles the data structure. It can also be used as a package in Python code (`store.ipynb` shows a few examples). To use `iavlread`, it's easiest to just clone this repository and make a symlink to `iavlread` from `$HOME/bin` or other directory which in `$PATH`.
|
||||
|
||||
### Usage
|
||||
|
||||
I'm just going to show a couple of examples on a snapshot of the [Allora] testnet, as it can be downloaded e.g. [here](https://www.imperator.co/services/chain-services/testnets/allora). That's what I've been using this for, although I'd assume it works the same for other CosmosSDK blockchains.
|
||||
|
||||
We assume that we're inside the snapshot, so the application db is at the path `data/application.db` from the current working directory. Otherwise, it can be specified with using `-d path_to_database`.
|
||||
|
||||
To get the **maximum/minimum block height** contained in the snapshot
|
||||
|
||||
$ iavlread max_height s/k:emissions/
|
||||
5224814
|
||||
$ iavlread min_height s/k:emissions/
|
||||
5221360
|
||||
|
||||
Here `s/k:emissions/` is the prefix for a specific IAVL tree, the one corresponding to the emissions module. Other prefixes are `s/k:mint/`, `s/k:bank/`, `s/k:staking/`, `s/k:acc/`. They should generally produce the same min/max height, but that is not guaranteed.
|
||||
|
||||
To **count** the items in a keeper, use the `count` subcommand:
|
||||
|
||||
$ iavlread count s/k:emissions/
|
||||
19070982
|
||||
|
||||
We can also count only the items with a specific key (i.e. one item of the keeper).
|
||||
|
||||
$ iavlread count s/k:emissions/ 62
|
||||
9714538
|
||||
|
||||
Here 62 corresponds to the `latestOneOutInfererInfererNetworkRegrets` field, which has type `Map[Triple[uint64, string, string], TimestampedValue]`. So the keys for the individual item in the map are quadruples consisting of 62, an integer, and two strings.
|
||||
|
||||
To **iterate through all items** whose key starts with 62, use the `iterate` command:
|
||||
|
||||
$ iavlread -kQss -vpb,2=float iterate s/k:emissions/ 62 | head -n5
|
||||
([62, 1, 'allo1004k7wqa4spns0wlct6mvxnmfysae07w2p75xc', 'allo1004k7wqa4spns0wlct6mvxnmfysae07w2p75xc'], [(1, 1655577), (2, -2.538390795862665)])
|
||||
([62, 1, 'allo1004k7wqa4spns0wlct6mvxnmfysae07w2p75xc', 'allo123s56yuz7dkyh54gs7gstulrfzwj3d6ucwlwk2'], [(1, 1655577), (2, -2.9735713547935196)])
|
||||
([62, 1, 'allo1004k7wqa4spns0wlct6mvxnmfysae07w2p75xc', 'allo136dfvvuhazgyqyls2f68qzr5l3p07uhnk9mmk0'], [(1, 1655577), (2, -2.4482888033843784)])
|
||||
([62, 1, 'allo1004k7wqa4spns0wlct6mvxnmfysae07w2p75xc', 'allo15fvg8gk63u8ydf3znaaxrrukgwulezjz4azf68'], [(1, 1655577), (2, -2.4736050386063284)])
|
||||
([62, 1, 'allo1004k7wqa4spns0wlct6mvxnmfysae07w2p75xc', 'allo16k9af4uu7vpm5hy68t6u62jylc5frv747mz5lw'], [(1, 1655577), (2, -2.4686292734004254)])
|
||||
|
||||
The option `-kQss` specifies the key format (a 64 bit integer `Q` followed by two strings `s`; see below). And `-vpb,2=float` specifies the value format: a protocol buffer whose field number 2 is a `float`.
|
||||
|
||||
If we want to restrict to keys which start with 62 and 60 (i.e. get one-out regrets for topic 60 only)
|
||||
|
||||
$ iavlread -kQss count s/k:emissions/ 62 60 | head -n5
|
||||
10650
|
||||
$ iavlread -kQss -vpb,2=float iterate s/k:emissions/ 62 60 | head -n5
|
||||
([62, 60, 'allo107zfy4xrp5plt0jmutaj9feer02v6r30amku78', 'allo107zfy4xrp5plt0jmutaj9feer02v6r30amku78'], [(1, 5070658), (2, 0.004613475335253211)])
|
||||
([62, 60, 'allo107zfy4xrp5plt0jmutaj9feer02v6r30amku78', 'allo10q5h8afpwjh5x3vazxzwwkxfhpzfys9wxxw3q8'], [(1, 4785658), (2, 0.18922985130794248)])
|
||||
([62, 60, 'allo107zfy4xrp5plt0jmutaj9feer02v6r30amku78', 'allo10vgaxk57dkk0fd255r3gxn5quwzxaqq95m2cz2'], [(1, 4837018), (2, 0.6616848257922094)])
|
||||
([62, 60, 'allo107zfy4xrp5plt0jmutaj9feer02v6r30amku78', 'allo10w45atfjsh9q6vsk7mx74xh0pvuf8r42vnmt5p'], [(1, 4802038), (2, 0.010614832362991345)])
|
||||
([62, 60, 'allo107zfy4xrp5plt0jmutaj9feer02v6r30amku78', 'allo12hnfamvwumkfm6dnc42rt8q3yevyqpuzkdtwat'], [(1, 4471438), (2, 0.2668976650081499)])
|
||||
|
||||
Or if we know the full key, we can use `get` instead of `iterate`. E.g. to get a balance from the bank module:
|
||||
|
||||
$ iavlread -kbs -vint get s/k:bank/ 2 570DD38DC5BAF3112A7C83A420ED399A8E59C5FC uallo
|
||||
350
|
||||
|
||||
We can also get the value for at a different block height (by default, the max block height is used):
|
||||
|
||||
$ iavlread -H 5221360 -kbs -vint get s/k:bank/ 2 570DD38DC5BAF3112A7C83A420ED399A8E59C5FC uallo
|
||||
10
|
||||
|
||||
Or we can get all past updates to the value (that are contained in the snapshot):
|
||||
|
||||
$ iavlread -kbs -vint history s/k:bank/ 2 570DD38DC5BAF3112A7C83A420ED399A8E59C5FC uallo
|
||||
5224814 150
|
||||
5224813 30
|
||||
5224812 60
|
||||
155
iavlread
Executable file
155
iavlread
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import plyvel
|
||||
import iavltree
|
||||
import json
|
||||
import struct
|
||||
|
||||
def decode_protobuf(subformats: dict, format_prefix: str, data: bytes):
|
||||
result = []
|
||||
for (k,v) in iavltree.parse_pb(data):
|
||||
idx = f'{format_prefix}.{k}'
|
||||
if idx in subformats:
|
||||
f = subformats[idx]
|
||||
if f == 'pb':
|
||||
decoded_value = decode_protobuf(subformats, idx, v)
|
||||
elif f == 'pbdict':
|
||||
decoded_value = dict(decode_protobuf(subformats, idx, v))
|
||||
else:
|
||||
decoded_value = decode_output(f, v)
|
||||
else:
|
||||
decoded_value = v
|
||||
result.append((k, decoded_value))
|
||||
return result
|
||||
|
||||
def decode_output(format: str, data: bytes) -> str:
|
||||
if format == 'str':
|
||||
return data.decode('utf-8')
|
||||
elif format == 'int':
|
||||
return int(data)
|
||||
elif format == 'float':
|
||||
return float(data)
|
||||
elif format == 'u64':
|
||||
return struct.unpack('>Q', data[:8])[0]
|
||||
elif format == 'u32':
|
||||
return struct.unpack('>I', data[:4])[0]
|
||||
elif format == 'u16':
|
||||
return struct.unpack('>H', data[:2])[0]
|
||||
elif format == 'u8':
|
||||
return struct.unpack('>B', data[:1])[0]
|
||||
elif format == 'i64':
|
||||
return struct.unpack('>q', data[:8])[0]
|
||||
elif format == 'i32':
|
||||
return struct.unpack('>i', data[:4])[0]
|
||||
elif format == 'i16':
|
||||
return struct.unpack('>h', data[:2])[0]
|
||||
elif format == 'i8':
|
||||
return struct.unpack('>b', data[:1])[0]
|
||||
elif format == 'i64ord':
|
||||
return struct.unpack('>Q', data[:8])[0] - (1<<63)
|
||||
elif format == 'i32ord':
|
||||
return struct.unpack('>I', data[:4])[0] - (1<<31)
|
||||
elif format == 'i16ord':
|
||||
return struct.unpack('>H', data[:2])[0] - (1<<15)
|
||||
elif format == 'i8ord':
|
||||
return struct.unpack('>B', data[:1])[0] - (1<<7)
|
||||
elif format.startswith('pbdict'):
|
||||
subformats = {'.' + id: subformat for x in format.split(',')[1:] for id, subformat in (x.split('='),)}
|
||||
return dict(decode_protobuf(subformats, '', data))
|
||||
elif format.startswith('pb'):
|
||||
subformats = {'.' + id: subformat for x in format.split(',')[1:] for id, subformat in (x.split('='),)}
|
||||
return decode_protobuf(subformats, '', data)
|
||||
else:
|
||||
return data
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description="Read the IAVL tree in a cosmos snapshot")
|
||||
|
||||
parser.add_argument('-d', '--database', help='Path to database (application.db folder)')
|
||||
parser.add_argument('-H', '--height', type=int, help='Block height')
|
||||
# parser.add_argument('-j', '--json', action='store_true', help='JSON output')
|
||||
|
||||
def add_key_cmd(subparsers, cmd, help, optional: bool):
|
||||
subp = subparsers.add_parser(cmd, help = help)
|
||||
subp.add_argument('-k', '--keyformat', help='Key format for maps (e.g. Qss)')
|
||||
subp.add_argument('-v', '--valueformat', help='Value format')
|
||||
subp.add_argument('prefix', help = 'Prefix (e.g. "s/k:emissions/")')
|
||||
subp.add_argument('key', nargs='*' if optional else '+', help = 'Key parts')
|
||||
return subp
|
||||
|
||||
subparsers = parser.add_subparsers(required=True, dest='cmd')
|
||||
p_max_height = subparsers.add_parser('max_height', help = 'Get the max block height in the snapshot')
|
||||
p_max_height.add_argument('prefix', help = 'Prefix (e.g. "s/k:emissions/")')
|
||||
p_min_height = subparsers.add_parser('min_height', help = 'Get the min block height in the snapshot')
|
||||
p_min_height.add_argument('prefix', help = 'Prefix (e.g. "s/k:emissions/")')
|
||||
add_key_cmd(subparsers, 'get', 'Retrieve a single item', False)
|
||||
add_key_cmd(subparsers, 'history', 'Get all stored past values of the item', False)
|
||||
add_key_cmd(subparsers, 'count', 'Count number of items with a prefix', True)
|
||||
add_key_cmd(subparsers, 'iterate', 'Iterate over items with some prefix', True)
|
||||
add_key_cmd(subparsers, 'iterate_keys', 'Iterate over items with some prefix, output keys only', True)
|
||||
add_key_cmd(subparsers, 'iterate_values', 'Iterate over items with some prefix, output values only', True)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
def run(args):
|
||||
dbpath = args.database if args.database is not None else 'data/application.db'
|
||||
keyformat = args.keyformat if hasattr(args, 'keyformat') and args.keyformat is not None else ''
|
||||
valueformat = args.valueformat if hasattr(args, 'valueformat') and args.valueformat is not None else 'b'
|
||||
|
||||
if args.cmd == 'max_height' or args.cmd == 'min_height' or args.key is None or len(args.key) == 0:
|
||||
key = None
|
||||
else:
|
||||
if len(args.key) > len(keyformat) + 1:
|
||||
raise Exception('Too many key elements for keyformat')
|
||||
key = [int(args.key[0])]
|
||||
for f, k in zip(keyformat, args.key[1:]):
|
||||
if f in ['i', 'I', 'q', 'Q']:
|
||||
key.append(int(k))
|
||||
else:
|
||||
key.append(k)
|
||||
|
||||
with plyvel.DB(dbpath) as db:
|
||||
if args.height is None or args.cmd == 'max_height':
|
||||
height = iavltree.max_height(db, args.prefix.encode('utf-8'))
|
||||
else:
|
||||
height = args.height
|
||||
|
||||
if args.cmd == 'max_height':
|
||||
print(height)
|
||||
elif args.cmd == 'min_height':
|
||||
hmin, _ = iavltree.min_max_height(db, args.prefix.encode('utf-8'))
|
||||
print(hmin)
|
||||
elif args.cmd == 'get':
|
||||
result = iavltree.get(db, args.prefix, height, keyformat, key)
|
||||
|
||||
if result is not None:
|
||||
print(decode_output(valueformat, result))
|
||||
elif args.cmd == 'history':
|
||||
it = iavltree.history(db, args.prefix, keyformat, key, height)
|
||||
|
||||
try:
|
||||
for h, v in it:
|
||||
print(f'{h} {decode_output(valueformat, v)}')
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
elif args.cmd == 'count':
|
||||
result = iavltree.count(db, args.prefix, height, keyformat, key = key)
|
||||
|
||||
print(result)
|
||||
elif args.cmd == 'iterate' or args.cmd == 'iterate_keys' or args.cmd == 'iterate_values':
|
||||
it = iavltree.iterate(db, args.prefix, height, keyformat, key = key)
|
||||
|
||||
try:
|
||||
for k, v in it:
|
||||
if args.cmd == 'iterate_keys':
|
||||
print(' '.join([str(x) for x in k]))
|
||||
elif args.cmd == 'iterate_values':
|
||||
print(decode_output(valueformat,v))
|
||||
else:
|
||||
print(' '.join([str(x) for x in k]), decode_output(valueformat, v))
|
||||
except BrokenPipeError:
|
||||
pass
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = get_args()
|
||||
run(args)
|
||||
@@ -1,9 +1,8 @@
|
||||
import plyvel
|
||||
import struct
|
||||
import numpy as np
|
||||
|
||||
# functions for reading IAVL tree
|
||||
def read_varint(x: bytes, offset: int = 0) -> int:
|
||||
def read_varint(x: bytes, offset: int = 0) -> tuple[int, int]:
|
||||
result = 0
|
||||
factor = 1
|
||||
|
||||
@@ -15,7 +14,7 @@ def read_varint(x: bytes, offset: int = 0) -> int:
|
||||
return result // 2, offset+i+1
|
||||
factor *= 128
|
||||
|
||||
def read_uvarint(x: bytes, offset: int = 0) -> int:
|
||||
def read_uvarint(x: bytes, offset: int = 0) -> tuple[int, int]:
|
||||
result = 0
|
||||
factor = 1
|
||||
|
||||
@@ -27,6 +26,20 @@ def read_uvarint(x: bytes, offset: int = 0) -> int:
|
||||
return result, offset+i+1
|
||||
factor *= 128
|
||||
|
||||
def write_uvarint(x: int) -> list[int]:
|
||||
if x < 0:
|
||||
raise Exception('write_uvarint only supports positive integers')
|
||||
elif x == 0:
|
||||
return [0]
|
||||
|
||||
result = []
|
||||
while x > 0:
|
||||
result.append(128 + x % 128)
|
||||
x //= 128
|
||||
result[-1] -= 128
|
||||
return result
|
||||
|
||||
|
||||
def read_key(key: bytes) -> tuple[int, int] | None:
|
||||
if not key.startswith(b's'):
|
||||
return None
|
||||
@@ -43,7 +56,6 @@ def write_key(key: tuple[int, int]) -> bytes:
|
||||
return b's' + version + nonce
|
||||
|
||||
def read_node(node: bytes) -> tuple[int, int, bytes, tuple[int, int], tuple[int, int]] | tuple[int, int, list[int], bytes] | tuple[int, int]:
|
||||
|
||||
if node.startswith(b's'):
|
||||
return read_key(node)
|
||||
|
||||
@@ -74,53 +86,36 @@ def read_node(node: bytes) -> tuple[int, int, bytes, tuple[int, int], tuple[int,
|
||||
|
||||
return (height, length, key, (left_version, left_nonce), (right_version, right_nonce))
|
||||
|
||||
def walk(tree, version, searchkey):
|
||||
if (version, 1) not in tree:
|
||||
def get_raw(db: plyvel.DB, prefix: bytes, version: int, searchkey: bytes) -> None | tuple[int, int, int, int, bytes, bytes]:
|
||||
key = db.get(prefix + write_key((version, 1)))
|
||||
if key is None:
|
||||
return None
|
||||
|
||||
node = tree[(version, 1)]
|
||||
if len(node) == 2: # root copy?
|
||||
node = tree[node]
|
||||
|
||||
while node[0] > 0:
|
||||
nodekey = node[2]
|
||||
if searchkey < nodekey:
|
||||
next = node[3]
|
||||
else:
|
||||
next = node[4]
|
||||
|
||||
node = tree[next]
|
||||
|
||||
return node[3]
|
||||
|
||||
def walk_disk_raw(db, prefix: bytes, version: int, searchkey: bytes) -> None | bytes:
|
||||
|
||||
root = db.get(prefix + write_key((version, 1)))
|
||||
if root is None:
|
||||
return None
|
||||
|
||||
node = read_node(root)
|
||||
node = read_node(key)
|
||||
|
||||
if len(node) == 2: # root copy?
|
||||
node = read_node(db.get(prefix + write_key(node)))
|
||||
key = node
|
||||
node = read_node(db.get(prefix + write_key(key)))
|
||||
|
||||
while node[0] > 0:
|
||||
# print(node)
|
||||
|
||||
nodekey = node[2]
|
||||
if searchkey < nodekey:
|
||||
next = node[3]
|
||||
key = node[3]
|
||||
else:
|
||||
next = node[4]
|
||||
key = node[4]
|
||||
|
||||
node = read_node(db.get(prefix + write_key(next)))
|
||||
node = read_node(db.get(prefix + write_key(key)))
|
||||
|
||||
if node[2] == searchkey:
|
||||
return node[3]
|
||||
(version, nonce) = key
|
||||
(height, length, itemkey, value) = node
|
||||
return (version, nonce, height, length, itemkey, value)
|
||||
else:
|
||||
return None
|
||||
|
||||
def walk_disk_next_key_raw(db, prefix: bytes, version: int, searchkey: bytes) -> None | bytes:
|
||||
def get_next_key_raw(db: plyvel.DB, prefix: bytes, version: int, searchkey: bytes) -> None | bytes:
|
||||
root = db.get(prefix + write_key((version, 1)))
|
||||
if root is None:
|
||||
return None
|
||||
@@ -146,10 +141,11 @@ def walk_disk_next_key_raw(db, prefix: bytes, version: int, searchkey: bytes) ->
|
||||
|
||||
return lowest_geq_key
|
||||
|
||||
def walk_disk(db, prefix: str, version: int, format: str, searchkey: list) -> None | bytes:
|
||||
return walk_disk_raw(db, prefix.encode('utf-8'), version, encode_key(format, searchkey))
|
||||
def get(db: plyvel.DB, prefix: str, version: int, format: str, searchkey: list) -> None | bytes:
|
||||
x = get_raw(db, prefix.encode('utf-8'), version, encode_key(format, searchkey))
|
||||
return x[5] if x is not None else None
|
||||
|
||||
def parse_struct(data):
|
||||
def parse_pb(data):
|
||||
n = 0
|
||||
results = []
|
||||
|
||||
@@ -180,26 +176,51 @@ def next_key(db, k: bytes) -> bytes | None:
|
||||
finally:
|
||||
it.close()
|
||||
|
||||
def max_height(db) -> int:
|
||||
def max_height(db: plyvel.DB, prefix: bytes | str) -> int:
|
||||
if isinstance(prefix, str):
|
||||
prefix = prefix.encode('utf-8')
|
||||
|
||||
testnr = 1<<63
|
||||
|
||||
for i in range(62, -1, -1):
|
||||
prefix = b's/k:emissions/s'
|
||||
n = next_key(db, prefix + struct.pack('>Q', testnr))
|
||||
n = next_key(db, prefix + b's' + struct.pack('>Q', testnr) + struct.pack('>I', 1))
|
||||
|
||||
if n is not None and n.startswith(prefix):
|
||||
# print(f'{testnr:16x} is low')
|
||||
# print(f'{testnr} is low')
|
||||
testnr += 1 << i
|
||||
else:
|
||||
# print(f'{testnr:16x} is high')
|
||||
# print(f'{testnr} is high')
|
||||
testnr -= 1 << i
|
||||
|
||||
n = next_key(db, prefix + struct.pack('>Q', testnr))
|
||||
n = db.get(prefix + struct.pack('>Q', testnr))
|
||||
if n is not None and n.startswith(prefix):
|
||||
return testnr
|
||||
else:
|
||||
return testnr - 1
|
||||
|
||||
def min_max_height(db: plyvel.DB, prefix: bytes) -> tuple[int, int]:
|
||||
if isinstance(prefix, str):
|
||||
prefix = prefix.encode('utf-8')
|
||||
|
||||
hmax = max_height(db, prefix)
|
||||
|
||||
h = 1<<hmax.bit_length()
|
||||
inc = h>>1
|
||||
|
||||
for _ in range(25):
|
||||
if h > hmax:
|
||||
highenough = True
|
||||
else:
|
||||
root = db.get(prefix + write_key((h, 1)))
|
||||
highenough = root is not None
|
||||
# print(h, highenough, inc)
|
||||
(h, inc) = (h + (1 - 2*highenough) * inc, inc >> 1)
|
||||
if not highenough:
|
||||
h += 1
|
||||
|
||||
return (h, hmax)
|
||||
|
||||
|
||||
# encode and decode keys
|
||||
def encode_key(format: str, key: list) -> bytes:
|
||||
result_bytes = []
|
||||
@@ -217,6 +238,10 @@ def encode_key(format: str, key: list) -> bytes:
|
||||
result_bytes += list(struct.pack('>Q', key[i+1]))
|
||||
elif f == 'q':
|
||||
result_bytes += list(struct.pack('>Q', key[i+1] + (1<<63)))
|
||||
elif f == 'b':
|
||||
data = list(bytes.fromhex(key[i+1]))
|
||||
result_bytes += write_uvarint(len(data))
|
||||
result_bytes += data
|
||||
|
||||
return bytes(result_bytes)
|
||||
|
||||
@@ -244,6 +269,11 @@ def decode_key(format: str, key: bytes) -> list:
|
||||
v = struct.unpack('>Q', key[idx:idx+8])[0]
|
||||
result.append(v - (1<<63))
|
||||
idx += 8
|
||||
elif f == 'b':
|
||||
length, offset = read_uvarint(key[idx:])
|
||||
data = key[idx+offset:idx+offset+length]
|
||||
result.append(data.hex().upper())
|
||||
idx += offset + length
|
||||
|
||||
if idx < len(key):
|
||||
result.append(key[idx:])
|
||||
@@ -252,26 +282,32 @@ def decode_key(format: str, key: bytes) -> list:
|
||||
|
||||
# iteration
|
||||
class IAVLTreeIteratorRaw:
|
||||
def __init__(self, db, prefix: bytes, version: int, start: bytes | None = None, end: bytes | None = None):
|
||||
def __init__(self, db: plyvel.DB, prefix: bytes, version: int, start: bytes | None = None, end: bytes | None = None):
|
||||
self.db = db
|
||||
self.prefix = prefix
|
||||
self.version = version
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.stack = []
|
||||
self.lookups = []
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def get_node(self, key):
|
||||
key_enc = self.prefix + write_key(key)
|
||||
self.lookups.append(key_enc)
|
||||
return self.db.get(key_enc)
|
||||
|
||||
def __next__(self):
|
||||
if len(self.stack) == 0:
|
||||
# get root node
|
||||
root = db.get(self.prefix + write_key((self.version, 1)))
|
||||
root = self.get_node((self.version, 1))
|
||||
if root is None:
|
||||
raise StopIteration
|
||||
node = read_node(root)
|
||||
if len(node) == 2: # link to other root node
|
||||
node = read_node(db.get(self.prefix + write_key(node)))
|
||||
node = read_node(self.get_node(node))
|
||||
self.stack.append(((self.version, 1), node))
|
||||
|
||||
# walk tree to either last before start or first after start
|
||||
@@ -282,7 +318,7 @@ class IAVLTreeIteratorRaw:
|
||||
next = node[3]
|
||||
else:
|
||||
next = node[4]
|
||||
node = read_node(db.get(self.prefix + write_key(next)))
|
||||
node = read_node(self.get_node(next))
|
||||
self.stack.append((next, node))
|
||||
|
||||
# return early if we ended up at first item after start
|
||||
@@ -308,13 +344,13 @@ class IAVLTreeIteratorRaw:
|
||||
raise StopIteration
|
||||
|
||||
# go right
|
||||
node = read_node(db.get(self.prefix + write_key(key)))
|
||||
node = read_node(self.get_node(key))
|
||||
self.stack.append((key, node))
|
||||
|
||||
# go left until at a leaf
|
||||
while node[0] > 0:
|
||||
key = node[3]
|
||||
node = read_node(db.get(self.prefix + write_key(key)))
|
||||
node = read_node(self.get_node(key))
|
||||
self.stack.append((key, node))
|
||||
|
||||
if self.end is not None and node[2] >= self.end:
|
||||
@@ -323,11 +359,9 @@ class IAVLTreeIteratorRaw:
|
||||
return (node[2], node[3])
|
||||
|
||||
class IAVLTreeIterator:
|
||||
def __init__(self, db, prefix: str, version: int, format: str, start: list | None = None, end: list | None = None):
|
||||
def __init__(self, db: plyvel.DB, prefix: bytes, version: int, format: str, start: bytes | None = None, end: bytes | None = None):
|
||||
self.format = format
|
||||
start_enc = encode_key(format, start) if start is not None else None
|
||||
end_enc = encode_key(format, end) if end is not None else None
|
||||
self.inner = IAVLTreeIteratorRaw(db, prefix.encode('utf-8'), version, start = start_enc, end = end_enc)
|
||||
self.inner = IAVLTreeIteratorRaw(db, prefix, version, start, end)
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
@@ -336,15 +370,58 @@ class IAVLTreeIterator:
|
||||
(k, v) = next(self.inner)
|
||||
return (decode_key(self.format, k), v)
|
||||
|
||||
def iterate(db, prefix, version, format = '', field = None, start = None, end = None):
|
||||
if field is not None:
|
||||
return IAVLTreeIterator(db, prefix, version, format, start = [field], end = [field+1] if field < 255 else None)
|
||||
else:
|
||||
return IAVLTreeIterator(db, prefix, version, format, start = start, end = end)
|
||||
def next_bs(x: bytes) -> bytes | None:
|
||||
if len(x) == 0:
|
||||
return None
|
||||
|
||||
def indexof_raw(db, prefix: bytes, version: int, key: bytes) -> int:
|
||||
x_enc = None
|
||||
for i in range(len(x),0,-1):
|
||||
if x[i-1] != 255:
|
||||
x_enc = x[:i-1] + bytes([x[i-1] + 1]) + bytes([0 for _ in range(len(x)-i)])
|
||||
break
|
||||
|
||||
return x_enc
|
||||
|
||||
def iterate(db: plyvel.DB, prefix, version, format = '', key = None, start = None, end = None):
|
||||
prefix_enc = prefix.encode('utf-8')
|
||||
|
||||
if key is not None:
|
||||
start_enc = encode_key(format, key)
|
||||
end_enc = next_bs(start_enc)
|
||||
else:
|
||||
start_enc = encode_key(format, start) if start is not None else None
|
||||
end_enc = encode_key(format, end) if end is not None else None
|
||||
|
||||
return IAVLTreeIterator(db, prefix_enc, version, format, start = start_enc, end = end_enc)
|
||||
|
||||
def count(db: plyvel.DB, prefix, version, format = '', key = None, start = None, end = None):
|
||||
prefix_enc = prefix.encode('utf-8')
|
||||
|
||||
if key is not None:
|
||||
start_enc = encode_key(format, key)
|
||||
end_enc = next_bs(start_enc)
|
||||
else:
|
||||
start_enc = encode_key(format, start) if start is not None else None
|
||||
end_enc = encode_key(format, end) if end is not None else None
|
||||
|
||||
startidx = indexof_raw(db, prefix_enc, version, start_enc) if start_enc is not None else 0
|
||||
|
||||
if end_enc is not None:
|
||||
endidx = indexof_raw(db, prefix_enc, version, end_enc)
|
||||
else:
|
||||
# get full count
|
||||
it = IAVLTreeIteratorRaw(db, prefix_enc, version)
|
||||
try:
|
||||
next(it)
|
||||
endidx = it.stack[0][1][1] # just read the length field of the root element
|
||||
except StopIteration:
|
||||
endidx = 0
|
||||
|
||||
return endidx - startidx
|
||||
|
||||
def indexof_raw(db: plyvel.DB, prefix: bytes, version: int, key: bytes) -> int:
|
||||
"""
|
||||
Find how many items come before `key` in the tree. If `key` doesn't exist, how many
|
||||
Find how many items come before `key` in the tree. If `key` doesn't exist, how many
|
||||
items come before the slot it would get inserted at
|
||||
"""
|
||||
it = IAVLTreeIteratorRaw(db, prefix, version, start=key)
|
||||
@@ -352,13 +429,39 @@ def indexof_raw(db, prefix: bytes, version: int, key: bytes) -> int:
|
||||
next(it)
|
||||
except StopIteration:
|
||||
# get root count
|
||||
return read_node(db.get(prefix + write_key(it.stack[0][0])))[1]
|
||||
|
||||
return it.stack[0][1][1]
|
||||
|
||||
keys = [p[1][3] for p, c in zip(it.stack, it.stack[1:]) if c[0] == p[1][4]]
|
||||
keys_encoded = [prefix + write_key(k) for k in keys]
|
||||
count = sum([read_node(db.get(k))[1] for k in keys_encoded])
|
||||
|
||||
return count
|
||||
|
||||
def indexof(db, prefix: str, version: int, format: str, key: list) -> int:
|
||||
def indexof(db: plyvel.DB, prefix: str, version: int, format: str, key: list) -> int:
|
||||
return indexof_raw(db, prefix.encode('utf-8'), version, encode_key(format, key))
|
||||
|
||||
class IAVLTreeHistoryIterator:
|
||||
def __init__(self, db: plyvel.DB, prefix: bytes, key: bytes, max_height: int, min_height: int = 0):
|
||||
self.db = db
|
||||
self.prefix = prefix
|
||||
self.key = key
|
||||
self.height = max_height
|
||||
self.min_height = min_height
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self) -> tuple[int, bytes]:
|
||||
if self.height < self.min_height:
|
||||
raise StopIteration
|
||||
result = get_raw(self.db, self.prefix, self.height, self.key)
|
||||
if result is None:
|
||||
raise StopIteration
|
||||
(h, _, _, _, _, v) = result
|
||||
self.height = h - 1
|
||||
return (h, v)
|
||||
|
||||
def history(db: plyvel.DB, prefix: str, format: str, key: list, max_height: int, min_height: int = 0) -> IAVLTreeHistoryIterator:
|
||||
prefix_enc = prefix.encode('utf-8')
|
||||
key_enc = encode_key(format, key)
|
||||
return IAVLTreeHistoryIterator(db, prefix_enc, key_enc, max_height, min_height)
|
||||
29
perftest.py
Executable file
29
perftest.py
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python
|
||||
import plyvel
|
||||
import iavltree
|
||||
from tqdm import tqdm
|
||||
|
||||
with plyvel.DB('../node/nodedir/data/application.db') as db:
|
||||
height = iavltree.max_height(db)
|
||||
|
||||
total = iavltree.count(db, 's/k:emissions/', height, 'Qss', key = [62])
|
||||
progress = tqdm(total = total)
|
||||
|
||||
it = iavltree.iterate(db, 's/k:emissions/', height, 'Qss', key = [62])
|
||||
|
||||
for k, v in it:
|
||||
progress.update(1)
|
||||
|
||||
progress.close()
|
||||
|
||||
keys = it.inner.lookups
|
||||
|
||||
print(f'Number of items: {total}')
|
||||
print(f'Lookups needed: {len(keys)}')
|
||||
|
||||
with plyvel.DB('../node/nodedir/data/application.db') as db:
|
||||
progress = tqdm(total = len(keys))
|
||||
for k in keys:
|
||||
db.get(k)
|
||||
progress.update(1)
|
||||
progress.close()
|
||||
113
store.ipynb
113
store.ipynb
@@ -2,50 +2,54 @@
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import plyvel\n",
|
||||
"from itertools import islice\n",
|
||||
"import iavltree"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# db = plyvel.DB('../node/nodedir/data/application.db')\n",
|
||||
"height = iavltree.max_height(db)\n",
|
||||
"height"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"[k for k, v in iavltree.iterate(db, 's/k:mint/', height)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"dict(iavltree.parse_pb(next(iavltree.iterate(db, 's/k:mint/', height, key = [138]))[1]))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"it = iavltree.iterate(db, 's/k:emissions/', height, key = [62, 64], format = 'Qss')\n",
|
||||
"ooiiregrets = [(k[2],k[3],value[1],float(value[2])) for k,v in it for value in (dict(iavltree.parse_pb(v)),)]\n",
|
||||
"\n",
|
||||
"%run -i read_tree.py"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# db = plyvel.DB('../testnode/nodedir/data/application.db')\n",
|
||||
"max_height(db)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"[k for k, v in iterate(db, 's/k:mint/', 5224815)]"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"parse_struct(next(iterate(db, 's/k:mint/', 5224815, field = 138))[1])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"[k for k,v in iterate(db, 's/k:emissions/', 5224815, start = [62, 60], end = [62, 61], format = 'Qss')]"
|
||||
"len(ooiiregrets), len(it.inner.lookups)"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -54,16 +58,20 @@
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import numpy as np\n",
|
||||
"keynames = {0: \"Params\", 1: \"TotalStake\", 2: \"TopicStake\", 3: \"Rewards\", 4: \"NextTopicId\", 5: \"Topics\", 6: \"TopicWorkers\", 7: \"TopicReputers\", 8: \"DelegatorStake\", 9: \"DelegateStakePlacement\", 10: \"TargetStake\", 11: \"Inferences\", 12: \"Forecasts\", 13: \"WorkerNodes\", 14: \"ReputerNodes\", 15: \"LatestInferencesTs\", 16: \"ActiveTopics\", 17: \"AllInferences\", 18: \"AllForecasts\", 19: \"AllLossBundles\", 20: \"StakeRemoval\", 21: \"StakeByReputerAndTopicId\", 22: \"DelegateStakeRemoval\", 23: \"AllTopicStakeSum\", 24: \"AddressTopics\", 24: \"WhitelistAdmins\", 25: \"ChurnableTopics\", 26: \"RewardableTopics\", 27: \"NetworkLossBundles\", 28: \"NetworkRegrets\", 29: \"StakeByReputerAndTopicId\", 30: \"ReputerScores\", 31: \"InferenceScores\", 32: \"ForecastScores\", 33: \"ReputerListeningCoefficient\", 34: \"InfererNetworkRegrets\", 35: \"ForecasterNetworkRegrets\", 36: \"OneInForecasterNetworkRegrets\", 37: \"OneInForecasterSelfNetworkRegrets\", 38: \"UnfulfilledWorkerNonces\", 39: \"UnfulfilledReputerNonces\", 40: \"FeeRevenueEpoch\", 41: \"TopicFeeRevenue\", 42: \"PreviousTopicWeight\", 43: \"PreviousReputerRewardFraction\", 44: \"PreviousInferenceRewardFraction\", 45: \"PreviousForecastRewardFraction\", 46: \"InfererScoreEmas\", 47: \"ForecasterScoreEmas\", 48: \"ReputerScoreEmas\", 49: \"TopicRewardNonce\", 50: \"DelegateRewardPerShare\", 51: \"PreviousPercentageRewardToStakedReputers\", 52: \"StakeRemovalsByBlock\", 53: \"DelegateStakeRemovalsByBlock\", 54: \"StakeRemovalsByActor\", 55: \"DelegateStakeRemovalsByActor\", 56: \"TopicLastWorkerCommit\", 57: \"TopicLastReputerCommit\", 58: \"TopicLastWorkerPayload\", 59: \"TopicLastReputerPayload\", 60: \"OpenWorkerWindows\", 61: \"LatestNaiveInfererNetworkRegrets\", 62: \"LatestOneOutInfererInfererNetworkRegrets\", 63: \"LatestOneOutInfererForecasterNetworkRegrets\", 64: \"LatestOneOutForecasterInfererNetworkRegrets\", 65: \"LatestOneOutForecasterForecasterNetworkRegrets\", 66: \"PreviousForecasterScoreRatio\", 67: \"LastDripBlock\", 68: \"TopicToNextPossibleChurningBlock\", 69: \"BlockToActiveTopics\", 70: \"BlockToLowestActiveTopicWeight\", 71: \"PreviousTopicQuantileInfererScoreEma\", 72: \"PreviousTopicQuantileForecasterScoreEma\", 73: \"PreviousTopicQuantileReputerScoreEma\", 74: \"CountInfererInclusionsInTopic\", 75: \"CountForecasterInclusionsInTopic\", 76: \"ActiveInferers\", 77: \"ActiveForecasters\", 78: \"ActiveReputers\", 79: \"LowestInfererScoreEma\", 80: \"LowestForecasterScoreEma\", 81: \"LowestReputerScoreEma\", 82: \"LossBundles\", 83: \"TotalSumPreviousTopicWeights\", 84: \"RewardCurrentBlockEmission\", 85: \"GlobalWhitelist\", 86: \"TopicCreatorWhitelist\", 87: \"TopicWorkerWhitelist\", 88: \"TopicReputerWhitelist\", 89: \"TopicWorkerWhitelistEnabled\", 90: \"TopicReputerWhitelistEnabled\", 91: \"LastMedianInferences\", 92: \"MadInferences\", 93: \"InitialInfererEmaScore\", 94: \"InitialForecasterEmaScore\", 95: \"InitialReputerEmaScore\", 96: \"GlobalWorkerWhitelist\", 97: \"GlobalReputerWhitelist\", 98: \"GlobalAdminWhitelist\", 99: \"LatestRegretStdNorm\", 100: \"LatestInfererWeights\", 101: \"LatestForecasterWeights\", 102: \"NetworkInferences\", 103: \"OutlierResistantNetworkInferences\", 104: \"MonthlyReputerRewards\", 105: \"MonthlyTopicRewards\",}\n",
|
||||
"lens = np.zeros(256, dtype = int)\n",
|
||||
"\n",
|
||||
"with plyvel.DB('../testnode/nodedir/data/application.db') as db:\n",
|
||||
" height = max_height(db)\n",
|
||||
" for field in range(255):\n",
|
||||
" count1 = indexof(db, 's/k:emissions/', height, '', [field])\n",
|
||||
" count2 = indexof(db, 's/k:emissions/', height, '', [field+1])\n",
|
||||
" lens[field] = count2 - count1\n",
|
||||
"for field in range(255):\n",
|
||||
" lens[field] = iavltree.count(db, 's/k:emissions/', height, key = [field])\n",
|
||||
"\n",
|
||||
"np.argsort(lens)"
|
||||
"order = np.lexsort((np.arange(256)[::-1], lens))[::-1]\n",
|
||||
"\n",
|
||||
"for i in range(len(order)):\n",
|
||||
" if lens[order[i]] == 0 and order[i] not in keynames:\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" print(f'{keynames[order[i]]:50} {lens[order[i]]:9d}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -102,6 +110,19 @@
|
||||
"\n",
|
||||
"# found"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 16,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# allora testnet module addresses\n",
|
||||
"# mod allorapendingrewards 54C6D62FF29ECFEE9A5F0366DEC0F9CB44C10BB4\n",
|
||||
"# mod allorarewards F3CA54C42E5B7DC7CB2A347B21E77AC248D914D2\n",
|
||||
"# mod allorastaking 3C19B4642DA1C2DBB7E44679FA48F72FD9A97E5E\n",
|
||||
"# mod ecosystem 570DD38DC5BAF3112A7C83A420ED399A8E59C5FC"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
Reference in New Issue
Block a user