trie -> kv store

* simplify data storage to key-value, tries are not relevant for NBC
* locked-down version of lmdb dependency
* easier to build / maintain on various platforms
This commit is contained in:
Jacek Sieka 2020-01-15 16:06:50 +01:00 committed by tersec
parent 3dcdce137a
commit 2a67ac3c05
17 changed files with 365 additions and 62 deletions

3
.gitmodules vendored
View File

@ -136,3 +136,6 @@
url = https://github.com/status-im/nim-bearssl.git
ignore = dirty
branch = master
[submodule "vendor/lmdb"]
path = vendor/lmdb
url = https://github.com/status-im/lmdb.git

View File

@ -7,7 +7,6 @@ cache:
directories:
- vendor/nimbus-build-system/vendor/Nim/bin
- vendor/go/bin
- rocksdbCache
- jsonTestsCache
git:
@ -27,7 +26,6 @@ matrix:
before_install:
- export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib"
- sudo apt-get -q update
- sudo apt-get install -y librocksdb-dev
- os: linux
arch: arm64
sudo: required
@ -37,14 +35,10 @@ matrix:
before_install:
- export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:/usr/local/lib"
- sudo apt-get -q update
- sudo apt-get install -y libpcre3-dev librocksdb-dev
- sudo apt-get install -y libpcre3-dev
- os: osx
env:
- NPROC=2
before_install:
- launchctl setenv LIBRARY_PATH /usr/local/lib # for RocksDB
# build our own rocksdb to test with a fixed version that we think works
- vendor/nimbus-build-system/scripts/build_rocksdb.sh rocksdbCache
install:

View File

@ -52,7 +52,6 @@ Nimbus has 4 external dependencies:
* Go 1.12 (for compiling libp2p daemon - being phased out)
* Developer tools (C compiler, Make, Bash, Git)
* [RocksDB](https://github.com/facebook/rocksdb/)
* PCRE
Nim is not an external dependency, Nimbus will build its own local copy.
@ -62,13 +61,13 @@ Nim is not an external dependency, Nimbus will build its own local copy.
On common Linux distributions the dependencies can be installed with:
```sh
# Debian and Ubuntu
sudo apt-get install build-essential git golang-go librocksdb-dev libpcre3-dev
sudo apt-get install build-essential git golang-go libpcre3-dev
# Fedora
dnf install @development-tools go rocksdb-devel pcre
dnf install @development-tools go pcre
# Archlinux, using an AUR manager for pcre-static
yourAURmanager -S base-devel go rocksdb pcre-static
yourAURmanager -S base-devel go pcre-static
```
### MacOS
@ -76,14 +75,14 @@ yourAURmanager -S base-devel go rocksdb pcre-static
Assuming you use [Homebrew](https://brew.sh/) to manage packages
```sh
brew install go rocksdb pcre
brew install go pcre
```
### Windows
* install [Go](https://golang.org/doc/install#windows)
You can install the developer tools by following the instruction in our [Windows dev environment section](#windows-dev-environment).
It also provides a downloading script for prebuilt PCRE and RocksDB.
It also provides a downloading script for prebuilt PCRE.
If you choose to install Go from source, both Go and Nimbus requires the same initial steps of installing Mingw.
@ -220,7 +219,7 @@ Variables -> Path -> Edit -> New -> C:\mingw-w64\mingw64\bin (it's "C:\mingw-w64
Install [Git for Windows](https://gitforwindows.org/) and use a "Git Bash" shell to clone and build nim-beacon-chain.
If you don't want to compile RocksDB and SQLite separately, you can fetch pre-compiled DLLs with:
If you don't want to compile PCRE separately, you can fetch pre-compiled DLLs with:
```bash
mingw32-make # this first invocation will update the Git submodules
mingw32-make fetch-dlls # this will place the right DLLs for your architecture in the "build/" directory
@ -286,19 +285,6 @@ sudo apt-get install git libgflags-dev libsnappy-dev libpcre3-dev
mkdir status
cd status
# Install rocksdb
git clone https://github.com/facebook/rocksdb.git
cd rocksdb
make shared_lib
sudo make install-shared
cd ..
# Raspberry pi doesn't include /usr/local/lib in library search path
# Add it to your profile
echo '# Local compiles (nimbus - rocksdb)' >> ~/.profile
echo 'export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH' >> ~/.profile
echo '' >> ~/.profile
# Install Go at least 1.12 (Buster only includes up to 1.11)
# Raspbian is 32-bit, so the package is go1.XX.X.linux-armv6l.tar.gz (and not arm64)
curl -O https://storage.googleapis.com/golang/go1.13.3.linux-armv6l.tar.gz

View File

@ -1,14 +1,14 @@
import
json, tables, options,
chronicles, serialization, json_serialization, eth/common/eth_types_json_serialization,
options,
serialization,
spec/[datatypes, digest, crypto],
eth/trie/db, ssz
kvstore, ssz
type
BeaconChainDB* = ref object
## Database storing resolved blocks and states - resolved blocks are such
## blocks that form a chain back to the tail block.
backend: TrieDatabaseRef
backend: KVStoreRef
DbKeyKind = enum
kHashToState
@ -61,7 +61,7 @@ func subkey(root: Eth2Digest, slot: Slot): auto =
ret
proc init*(T: type BeaconChainDB, backend: TrieDatabaseRef): BeaconChainDB =
proc init*(T: type BeaconChainDB, backend: KVStoreRef): BeaconChainDB =
T(backend: backend)
proc putBlock*(db: BeaconChainDB, key: Eth2Digest, value: SignedBeaconBlock) =
@ -99,22 +99,18 @@ proc putTailBlock*(db: BeaconChainDB, key: Eth2Digest) =
db.backend.put(subkey(kTailBlock), key.data)
proc get(db: BeaconChainDB, key: auto, T: typedesc): Option[T] =
let res = db.backend.get(key)
if res.len != 0:
var res: Option[T]
discard db.backend.get(key, proc (data: openArray[byte]) =
try:
some(SSZ.decode(res, T))
res = some(SSZ.decode(data, T))
except SerializationError:
none(T)
else:
none(T)
discard
)
res
proc getBlock*(db: BeaconChainDB, key: Eth2Digest): Option[SignedBeaconBlock] =
db.get(subkey(SignedBeaconBlock, key), SignedBeaconBlock)
proc getBlock*(db: BeaconChainDB, slot: Slot): Option[SignedBeaconBlock] =
# TODO implement this
discard
proc getState*(db: BeaconChainDB, key: Eth2Digest): Option[BeaconState] =
db.get(subkey(BeaconState, key), BeaconState)

View File

@ -3,10 +3,10 @@ import
os, net, tables, random, strutils, times, sequtils,
# Nimble packages
stew/[objects, bitseqs, byteutils], stew/ranges/ptr_arith,
stew/[objects, bitseqs, byteutils],
chronos, chronicles, confutils, metrics,
json_serialization/std/[options, sets], serialization/errors,
eth/trie/db, eth/trie/backends/rocksdb_backend, eth/async_utils,
kvstore, kvstore_lmdb, eth/async_utils,
# Local modules
spec/[datatypes, digest, crypto, beaconstate, helpers, validator, network],
@ -135,7 +135,7 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async
networkId = getPersistentNetIdentity(conf)
nickname = if conf.nodeName == "auto": shortForm(networkId)
else: conf.nodeName
db = BeaconChainDB.init(trieDB newChainDb(conf.databaseDir))
db = BeaconChainDB.init(kvStore LmdbStoreRef.init(conf.databaseDir))
var mainchainMonitor: MainchainMonitor

94
beacon_chain/kvstore.nim Normal file
View File

@ -0,0 +1,94 @@
# Simple Key-Value store database interface
import
tables, hashes, sets
type
MemoryStoreRef* = ref object of RootObj
records: Table[seq[byte], seq[byte]]
DataProc* = proc(val: openArray[byte])
PutProc = proc (db: RootRef, key, val: openArray[byte]) {.gcsafe.}
GetProc = proc (db: RootRef, key: openArray[byte], onData: DataProc): bool {.gcsafe.}
DelProc = proc (db: RootRef, key: openArray[byte]) {.gcsafe.}
ContainsProc = proc (db: RootRef, key: openArray[byte]): bool {.gcsafe.}
KVStoreRef* = ref object
## Key-Value store virtual interface
obj: RootRef
putProc: PutProc
getProc: GetProc
delProc: DelProc
containsProc: ContainsProc
template put*(db: KVStoreRef, key, val: openArray[byte]) =
## Store ``value`` at ``key`` - overwrites existing value if already present
db.putProc(db.obj, key, val)
template get*(db: KVStoreRef, key: openArray[byte], onData: untyped): bool =
## Retrive value at ``key`` and call ``onData`` with the value. The data is
## valid for the duration of the callback.
## ``onData``: ``proc(data: openArray[byte])``
## returns true if found and false otherwise.
db.getProc(db.obj, key, onData)
template del*(db: KVStoreRef, key: openArray[byte]) =
## Remove value at ``key`` from store - do nothing if the value is not present
db.delProc(db.obj, key)
template contains*(db: KVStoreRef, key: openArray[byte]): bool =
## Return true iff ``key`` has a value in store
db.containsProc(db.obj, key)
proc get*(db: MemoryStoreRef, key: openArray[byte], onData: DataProc): bool =
let key = @key
db.records.withValue(key, v):
onData(v[])
return true
proc del*(db: MemoryStoreRef, key: openArray[byte]) =
# TODO: This is quite inefficient and it won't be necessary once
# https://github.com/nim-lang/Nim/issues/7457 is developed.
let key = @key
db.records.del(key)
proc contains*(db: MemoryStoreRef, key: openArray[byte]): bool =
db.records.contains(@key)
proc put*(db: MemoryStoreRef, key, val: openArray[byte]) =
# TODO: This is quite inefficient and it won't be necessary once
# https://github.com/nim-lang/Nim/issues/7457 is developed.
let key = @key
db.records[key] = @val
proc init*(T: type MemoryStoreRef): T =
T(
records: initTable[seq[byte], seq[byte]]()
)
proc putImpl[T](db: RootRef, key, val: openArray[byte]) =
mixin put
put(T(db), key, val)
proc getImpl[T](db: RootRef, key: openArray[byte], onData: DataProc): bool =
mixin get
get(T(db), key, onData)
proc delImpl[T](db: RootRef, key: openArray[byte]) =
mixin del
del(T(db), key)
proc containsImpl[T](db: RootRef, key: openArray[byte]): bool =
mixin contains
contains(T(db), key)
func kvStore*[T: RootRef](x: T): KVStoreRef =
mixin del, get, put, contains
KVStoreRef(
obj: x,
putProc: putImpl[T],
getProc: getImpl[T],
delProc: delImpl[T],
containsProc: containsImpl[T]
)

View File

@ -0,0 +1,159 @@
## Implementation of KVStore based on LMDB
## TODO: crashes on win32, investigate
import os
import ./kvstore
{.compile: "../vendor/lmdb/libraries/liblmdb/mdb.c".}
{.compile: "../vendor/lmdb/libraries/liblmdb/midl.c".}
const
MDB_NOSUBDIR = 0x4000
MDB_RDONLY = 0x20000
MDB_NOTFOUND = -30798
when defined(cpu64):
const LMDB_MAP_SIZE = 1024'u64 * 1024'u64 * 1024'u64 * 10'u64 # 10TB enough?
else:
const LMDB_MAP_SIZE = 1024'u64 * 1024'u64 * 1024'u64 # 32bit limitation
type
MDB_Env = distinct pointer
MDB_Txn = distinct pointer
MDB_Dbi = distinct cuint
MDB_val = object
mv_size: csize
mv_data: pointer
LmdbError* = object of CatchableError
# Used subset of the full LMDB API
proc mdb_env_create(env: var MDB_Env): cint {.importc.}
proc mdb_env_open(env: MDB_Env, path: cstring, flags: cuint, mode: cint): cint {.importc.}
proc mdb_txn_begin(env: MDB_Env, parent: MDB_Txn, flags: cuint, txn: var MDB_Txn): cint {.importc.}
proc mdb_txn_commit(txn: MDB_Txn): cint {.importc.}
proc mdb_txn_abort(txn: MDB_Txn) {.importc.}
proc mdb_dbi_open(txn: MDB_Txn, name: cstring, flags: cuint, dbi: var MDB_Dbi): cint {.importc.}
proc mdb_env_close(env: MDB_Env) {.importc.}
proc mdb_strerror(err: cint): cstring {.importc.}
proc mdb_get(txn: MDB_Txn, dbi: MDB_Dbi, key: var MDB_val, data: var MDB_val): cint {.importc.}
proc mdb_del(txn: MDB_Txn, dbi: MDB_Dbi, key: var MDB_val, data: ptr MDB_val): cint {.importc.}
proc mdb_put(txn: MDB_Txn, dbi: MDB_Dbi, key: var MDB_val, data: var MDB_val, flags: cuint): cint {.importc.}
proc mdb_env_set_mapsize(env: MDB_Env, size: uint64): cint {.importc.}
func raiseLmdbError(err: cint) {.noreturn.} =
let tmp = mdb_strerror(err)
raise (ref LmdbError)(msg: $tmp)
type
LmdbStoreRef* = ref object of RootObj
env: MDB_Env
template init(T: type MDB_Val, val: openArray[byte]): T =
T(
mv_size: val.len,
mv_data: unsafeAddr val[0]
)
proc begin(db: LmdbStoreRef, flags: cuint): tuple[txn: MDB_Txn, dbi: MDB_Dbi] =
var
txn: MDB_Txn
dbi: MDB_Dbi
if (let x = mdb_txn_begin(db.env, nil, flags, txn); x != 0):
raiseLmdbError(x)
if (let x = mdb_dbi_open(txn, nil, 0, dbi); x != 0):
mdb_txn_abort(txn)
raiseLmdbError(x)
(txn, dbi)
proc get*(db: LmdbStoreRef, key: openarray[byte], onData: DataProc): bool =
if key.len == 0:
return
var
(txn, dbi) = db.begin(MDB_RDONLY)
dbKey = MDB_Val.init(key)
dbVal: MDB_val
# abort ok for read-only and easier for exception safety
defer: mdb_txn_abort(txn)
if (let x = mdb_get(txn, dbi, dbKey, dbVal); x != 0):
if x == MDB_NOTFOUND:
return false
raiseLmdbError(x)
if not onData.isNil:
onData(toOpenArrayByte(cast[cstring](dbVal.mv_data), 0, dbVal.mv_size.int - 1))
true
proc put*(db: LmdbStoreRef, key, value: openarray[byte]) =
if key.len == 0: return
var
(txn, dbi) = db.begin(0)
dbKey = MDB_Val.init(key)
dbVal = MDB_Val.init(value)
if (let x = mdb_put(txn, dbi, dbKey, dbVal, 0); x != 0):
mdb_txn_abort(txn)
raiseLmdbError(x)
if (let x = mdb_txn_commit(txn); x != 0):
raiseLmdbError(x)
proc contains*(db: LmdbStoreRef, key: openarray[byte]): bool =
db.get(key, nil)
proc del*(db: LmdbStoreRef, key: openarray[byte]) =
if key.len == 0: return
var
(txn, dbi) = db.begin(0)
dbKey = MDB_Val.init(key)
if (let x = mdb_del(txn, dbi, dbKey, nil); x != 0):
mdb_txn_abort(txn)
if x != MDB_NOTFOUND:
raiseLmdbError(x)
return
if (let x = mdb_txn_commit(txn); x != 0):
raiseLmdbError(x)
proc close*(db: LmdbStoreRef) =
mdb_env_close(db.env)
proc init*(T: type LmdbStoreRef, basePath: string, readOnly = false): T =
var
env: MDB_Env
if (let x = mdb_env_create(env); x != 0):
raiseLmdbError(x)
let dataDir = basePath / "nimbus.lmdb"
if (let x = mdb_env_set_mapsize(env, LMDB_MAP_SIZE); x != 0):
mdb_env_close(env)
raiseLmdbError(x)
var openFlags = MDB_NOSUBDIR
if readOnly: openFlags = openFlags or MDB_RDONLY
# file mode ignored on windows
if (let x = mdb_env_open(env, dataDir, openFlags.cuint, 0o664.cint); x != 0):
mdb_env_close(env)
raiseLmdbError(x)
T(env: env)

View File

@ -19,7 +19,7 @@
import
macros, hashes, json, strutils, tables,
stew/[byteutils, bitseqs], chronicles, eth/common,
stew/[byteutils, bitseqs], chronicles,
../version, ../ssz/types, ./crypto, ./digest
# TODO Data types:

View File

@ -21,11 +21,11 @@
import
chronicles,
nimcrypto/[sha2, hash, utils], eth/common/eth_types_json_serialization,
nimcrypto/[sha2, hash, utils],
hashes
export
eth_types_json_serialization, hash.`$`
hash.`$`
type
Eth2Digest* = MDigest[32 * 8] ## `hash32` from spec

View File

@ -12,7 +12,7 @@ import
endians, stew/shims/macros, options, algorithm, options,
stew/[bitops2, bitseqs, objects, varints, ptrops, ranges/ptr_arith], stint,
faststreams/input_stream, serialization, serialization/testing/tracing,
nimcrypto/sha2, blscurve, eth/common,
nimcrypto/sha2, blscurve,
./spec/[crypto, datatypes, digest],
./ssz/[types, bytes_reader]

View File

@ -3,7 +3,7 @@ FROM debian:bullseye-slim AS build
SHELL ["/bin/bash", "-c"]
RUN apt-get -qq update \
&& apt-get -qq -y install build-essential make wget librocksdb-dev libpcre3-dev golang-go git &>/dev/null \
&& apt-get -qq -y install build-essential make wget libpcre3-dev golang-go git &>/dev/null \
&& apt-get -qq clean
# let Docker cache this between Git revision and testnet version changes
@ -36,7 +36,7 @@ FROM debian:bullseye-slim
SHELL ["/bin/bash", "-c"]
RUN apt-get -qq update \
&& apt-get -qq -y install librocksdb-dev libpcre3 psmisc &>/dev/null \
&& apt-get -qq -y install libpcre3 psmisc &>/dev/null \
&& apt-get -qq clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

View File

@ -18,6 +18,8 @@ import # Unit test
./test_block_pool,
./test_helpers,
./test_interop,
./test_kvstore,
./test_kvstore_lmdb,
./test_ssz,
./test_state_transition,
./test_sync_protocol,

View File

@ -7,17 +7,16 @@
{.used.}
import options, unittest, sequtils, eth/trie/[db],
../beacon_chain/[beacon_chain_db, extras, interop, ssz],
import options, unittest, sequtils,
../beacon_chain/[beacon_chain_db, extras, interop, ssz, kvstore],
../beacon_chain/spec/[beaconstate, datatypes, digest, crypto],
# test utilies
./testutil, ./testblockutil
suite "Beacon chain DB" & preset():
timedTest "empty database" & preset():
var
db = init(BeaconChainDB, newMemoryDB())
db = init(BeaconChainDB, kvStore MemoryStoreRef.init())
check:
when const_preset=="minimal":
@ -28,7 +27,7 @@ suite "Beacon chain DB" & preset():
timedTest "sanity check blocks" & preset():
var
db = init(BeaconChainDB, newMemoryDB())
db = init(BeaconChainDB, kvStore MemoryStoreRef.init())
let
blck = SignedBeaconBlock()
@ -46,7 +45,7 @@ suite "Beacon chain DB" & preset():
timedTest "sanity check states" & preset():
var
db = init(BeaconChainDB, newMemoryDB())
db = init(BeaconChainDB, kvStore MemoryStoreRef.init())
let
state = BeaconState()
@ -60,7 +59,7 @@ suite "Beacon chain DB" & preset():
timedTest "find ancestors" & preset():
var
db = init(BeaconChainDB, newMemoryDB())
db = init(BeaconChainDB, kvStore MemoryStoreRef.init())
x: ValidatorSig
y = init(ValidatorSig, x.getBytes())
@ -101,7 +100,7 @@ suite "Beacon chain DB" & preset():
# serialization where an all-zero default-initialized bls signature could
# not be deserialized because the deserialization was too strict.
var
db = init(BeaconChainDB, newMemoryDB())
db = init(BeaconChainDB, kvStore MemoryStoreRef.init())
let
state = initialize_beacon_state_from_eth1(

45
tests/test_kvstore.nim Normal file
View File

@ -0,0 +1,45 @@
{.used.}
import
unittest,
../beacon_chain/kvstore
proc testKVStore*(db: KVStoreRef) =
let
key = [0'u8, 1, 2, 3]
value = [3'u8, 2, 1, 0]
value2 = [5'u8, 2, 1, 0]
check:
db != nil
not db.get(key, proc(data: openArray[byte]) = discard)
not db.contains(key)
db.del(key) # does nothing
db.put(key, value)
check:
db.contains(key)
db.get(key, proc(data: openArray[byte]) =
check data == value
)
db.put(key, value2) # overwrite old value
check:
db.contains(key)
db.get(key, proc(data: openArray[byte]) =
check data == value2
)
db.del(key)
check:
not db.get(key, proc(data: openArray[byte]) = discard)
not db.contains(key)
db.del(key) # does nothing
suite "MemoryStoreRef":
test "KVStore interface":
testKVStore(kvStore MemoryStoreRef.init())

View File

@ -0,0 +1,24 @@
{.used.}
import
os,
unittest,
../beacon_chain/[kvstore, kvstore_lmdb],
./test_kvstore
suite "LMDB":
setup:
let
path = os.getTempDir() / "test_kvstore_lmdb"
os.removeDir(path)
os.createDir(path)
teardown:
os.removeDir(path)
test "KVStore interface":
let db = LmdbStoreRef.init(path)
defer: db.close()
testKVStore(kvStore db)

View File

@ -7,8 +7,8 @@
import
algorithm, strformat, stats, times, std/monotimes, stew/endians2,
chronicles, eth/trie/[db],
../beacon_chain/[beacon_chain_db, block_pool, extras, ssz, beacon_node_types],
chronicles,
../beacon_chain/[beacon_chain_db, block_pool, extras, ssz, kvstore, beacon_node_types],
../beacon_chain/spec/[digest, beaconstate, datatypes],
testblockutil
@ -73,7 +73,7 @@ template timedTest*(name, body) =
testTimes.add (f, name)
proc makeTestDB*(tailState: BeaconState, tailBlock: SignedBeaconBlock): BeaconChainDB =
result = init(BeaconChainDB, newMemoryDB())
result = init(BeaconChainDB, kvStore MemoryStoreRef.init())
BlockPool.preInit(result, tailState, tailBlock)
proc makeTestDB*(validators: int): BeaconChainDB =

1
vendor/lmdb vendored Submodule

@ -0,0 +1 @@
Subproject commit c8ecc17b38e164e6a728d66a9b1d05bc18dd3ace