Skip to content

Commit 86f442c

Browse files
authored
improving ledger docs
1 parent 2be0a87 commit 86f442c

File tree

23 files changed

+115
-64
lines changed

23 files changed

+115
-64
lines changed

ledger/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
# Flow Ledger Package**
1+
# Flow Ledger Package
22

33
**Ledger** is a stateful fork-aware key/value storage. Any update (value change for a key) to the ledger generates a new ledger state. Updates can be applied to any recent state. These changes don't have to be sequential and ledger supports a tree of states. Ledger provides value lookup by key at a particular state (historic lookups) and can prove the existence/non-existence of a key-value pair at the given state. Ledger assumes the initial state includes all keys with an empty bytes slice as value.
44

5-
This package provides two various implementation of ledger:
5+
This package provides two ledger implementations:
66

7-
- **Complete Ledger** implements a fast, memory-efficient and reliable ledger. It holds a limited number of recently active states in memory (for speed) and uses write-ahead logs and checkpointing to provide reliability. Under the hood complete ledger uses a collection of MTries (forest). MTrie is a customized in-memory binary Patricia Merkle trie storing payloads at specific storage paths. The payload includes both key-value pair and storage paths are determined by the PathFinder. Forest utilizes unchanged sub-trie sharing between tries to save memory.
7+
- **Complete Ledger** implements a fast, memory-efficient and reliable ledger. It holds a limited number of recently used states in memory (for speed) and uses write-ahead logs and checkpointing to provide reliability. Under the hood complete ledger uses a collection of MTries(forest). MTrie is a customized in-memory binary Patricia Merkle trie storing payloads at specific storage paths. The payload includes both key-value pair and storage paths are determined by the PathFinder. Forest utilizes unchanged sub-trie sharing between tries to save memory.
88

9-
- **Partial Ledger** implements the ledger functionality for a limited subset of keys. Partial ledgers are designed to be constructed and verified by a collection of proofs from a complete ledger. The partial ledger uses a partial binary Merkle trie which holds intermediate hash value for the pruned branched and prevents updates to keys that were not part of proofs.
9+
- **Partial Ledger** implements the ledger functionality for a limited subset of keys. Partial ledgers are designed to be constructed and verified by a collection of proofs from a complete ledger. The partial ledger uses a partial binary Merkle trie which holds intermediate hash value for the pruned branched and prevents updates to keys that were not part of proofs.

ledger/common/encoding/encoding.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package encoding provides byte serialization and deserialization of trie and ledger structs.
12
package encoding
23

34
import (
@@ -7,15 +8,14 @@ import (
78
"github.com/dapperlabs/flow-go/ledger/common/utils"
89
)
910

10-
// Version encoder/decoder code only supports
11-
// decoding data with version smaller or equal to this value
12-
// bumping this number prevents older versions of the code
13-
// to deal with the newer version of data
11+
// Version captures the maximum version of encoding that this code supports
12+
// in other words this code encodes the data with the latest version and
13+
// can only decode data with version smaller or equal to this value
14+
// bumping this number prevents older versions of the code to deal with the newer version of data
1415
// codes should be updated with backward compatibility if needed
15-
// and act differently based on the version
1616
const Version = uint16(0)
1717

18-
// Type capture the type of encoded entity
18+
// Type capture the type of encoded entity (e.g. State, Key, Value, Path)
1919
type Type uint8
2020

2121
const (
@@ -49,7 +49,7 @@ const (
4949
)
5050

5151
func (e Type) String() string {
52-
return [...]string{"Unknown", "State", "KeyPart", "Key", "Value", "Path", "Payload", "Proof", "BatchProof"}[e]
52+
return [...]string{"Unknown", "State", "KeyPart", "Key", "Value", "Path", "Payload", "Proof", "BatchProof", "Query", "Update", "Trie Update"}[e]
5353
}
5454

5555
// CheckVersion extracts encoding bytes from a raw encoded message
@@ -545,7 +545,7 @@ func decodeTrieUpdate(inp []byte) (*ledger.TrieUpdate, error) {
545545
return &ledger.TrieUpdate{RootHash: rh, Paths: paths, Payloads: payloads}, nil
546546
}
547547

548-
// EncodeProof encodes the content of a proof into a byte slice
548+
// EncodeTrieProof encodes the content of a proof into a byte slice
549549
func EncodeTrieProof(p *ledger.TrieProof) []byte {
550550
if p == nil {
551551
return []byte{}

ledger/common/hash.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"github.com/dapperlabs/flow-go/ledger/common/utils"
77
)
88

9+
// default value and default hash value for a default node
910
var emptySlice []byte
1011
var defaultLeafHash = HashLeaf([]byte("default:"), emptySlice)
1112

ledger/common/pathfinder/pathfinder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// Package pathfinder computes the trie storage path for any given key/value pair
12
package pathfinder
23

34
import (

ledger/common/proof.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
)
99

1010
// TODO move this to proof itself
11+
1112
// VerifyTrieProof verifies the proof, by constructing all the
1213
// hash from the leaf to the root and comparing the rootHash
1314
func VerifyTrieProof(p *ledger.TrieProof, expectedState ledger.State) bool {

ledger/common/utils/test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,28 @@ func TwoBytesPath(inp uint16) ledger.Path {
2121
return ledger.Path(b)
2222
}
2323

24+
// LightPayload returns a payload with 2 byte key and 2 byte value
2425
func LightPayload(key uint16, value uint16) *ledger.Payload {
2526
k := ledger.Key{KeyParts: []ledger.KeyPart{ledger.KeyPart{Type: 0, Value: Uint16ToBinary(key)}}}
2627
v := ledger.Value(Uint16ToBinary(value))
2728
return &ledger.Payload{Key: k, Value: v}
2829
}
2930

31+
// LightPayload8 returns a payload with 1 byte key and 1 byte value
3032
func LightPayload8(key uint8, value uint8) *ledger.Payload {
3133
k := ledger.Key{KeyParts: []ledger.KeyPart{ledger.KeyPart{Type: 0, Value: []byte{key}}}}
3234
v := ledger.Value([]byte{value})
3335
return &ledger.Payload{Key: k, Value: v}
3436
}
3537

38+
// KeyPartFixture returns a key part fixture
3639
func KeyPartFixture(typ uint16, val string) *ledger.KeyPart {
3740
kp1t := uint16(typ)
3841
kp1v := []byte(val)
3942
return ledger.NewKeyPart(kp1t, kp1v)
4043
}
4144

45+
// QueryFixture returns a query fixture
4246
func QueryFixture() *ledger.Query {
4347
sc, _ := hex.DecodeString("6a7a565add94fb36069d79e8725c221cd1e5740742501ef014ea6db999fd98ad")
4448
k1p1 := ledger.NewKeyPart(uint16(1), []byte("1"))
@@ -53,6 +57,7 @@ func QueryFixture() *ledger.Query {
5357
return u
5458
}
5559

60+
// UpdateFixture returns an update fixture
5661
func UpdateFixture() *ledger.Update {
5762
sc, _ := hex.DecodeString("6a7a565add94fb36069d79e8725c221cd1e5740742501ef014ea6db999fd98ad")
5863
k1p1 := ledger.NewKeyPart(uint16(1), []byte("1"))
@@ -69,11 +74,13 @@ func UpdateFixture() *ledger.Update {
6974
return u
7075
}
7176

77+
// RootHashFixture returns a root hash fixture
7278
func RootHashFixture() ledger.RootHash {
7379
sc, _ := hex.DecodeString("6a7a565add94fb36069d79e8725c221cd1e5740742501ef014ea6db999fd98ad")
7480
return ledger.RootHash(sc)
7581
}
7682

83+
// TrieProofFixture returns a trie proof fixture
7784
func TrieProofFixture() (*ledger.TrieProof, ledger.State) {
7885
p := ledger.NewTrieProof()
7986
p.Path = TwoBytesPath(330)
@@ -90,6 +97,7 @@ func TrieProofFixture() (*ledger.TrieProof, ledger.State) {
9097
return p, ledger.State(sc)
9198
}
9299

100+
// TrieBatchProofFixture returns a trie batch proof fixture
93101
func TrieBatchProofFixture() (*ledger.TrieBatchProof, ledger.State) {
94102
p1 := ledger.NewTrieProof()
95103
p1.Path = TwoBytesPath(330)
@@ -170,6 +178,7 @@ func RandomPayloads(n int, minByteSize int, maxByteSize int) []*ledger.Payload {
170178
return res
171179
}
172180

181+
// RandomValues returns n random values with variable sizes (minByteSize <= size < maxByteSize)
173182
func RandomValues(n int, minByteSize, maxByteSize int) []ledger.Value {
174183
if minByteSize > maxByteSize {
175184
panic("minByteSize cannot be smaller then maxByteSize")

ledger/common/utils/utils.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
// IsBitSet returns if the bit at index `idx` in the byte array `b` is set to 1 (big endian)
11-
// TODO: remove error return
11+
// TODO: remove error return to improve performance
1212
func IsBitSet(b []byte, idx int) (bool, error) {
1313
if idx >= len(b)*8 {
1414
return false, fmt.Errorf("input (%v) only has %d bits, can't look up bit %d", b, len(b)*8, idx)
@@ -17,7 +17,7 @@ func IsBitSet(b []byte, idx int) (bool, error) {
1717
}
1818

1919
// SetBit sets the bit at position i in the byte array b to 1
20-
// TODO: remove error return
20+
// TODO: remove error return to improve performance
2121
func SetBit(b []byte, i int) error {
2222
if i >= len(b)*8 {
2323
return fmt.Errorf("input (%v) only has %d bits, can't set bit %d", b, len(b)*8, i)
@@ -48,22 +48,26 @@ func Uint64ToBinary(integer uint64) []byte {
4848
return b
4949
}
5050

51+
// AppendUint8 appends the value byte to the input slice
5152
func AppendUint8(input []byte, value uint8) []byte {
5253
return append(input, byte(value))
5354
}
5455

56+
// AppendUint16 appends the value bytes to the input slice (big endian)
5557
func AppendUint16(input []byte, value uint16) []byte {
5658
buffer := make([]byte, 2)
5759
binary.BigEndian.PutUint16(buffer, value)
5860
return append(input, buffer...)
5961
}
6062

63+
// AppendUint32 appends the value bytes to the input slice (big endian)
6164
func AppendUint32(input []byte, value uint32) []byte {
6265
buffer := make([]byte, 4)
6366
binary.BigEndian.PutUint32(buffer, value)
6467
return append(input, buffer...)
6568
}
6669

70+
// AppendUint64 appends the value bytes to the input slice (big endian)
6771
func AppendUint64(input []byte, value uint64) []byte {
6872
buffer := make([]byte, 8)
6973
binary.BigEndian.PutUint64(buffer, value)
@@ -90,34 +94,39 @@ func AppendLongData(input []byte, data []byte) []byte {
9094
return input
9195
}
9296

97+
// ReadSlice reads `size` bytes from the input
9398
func ReadSlice(input []byte, size int) (value []byte, rest []byte, err error) {
9499
if len(input) < size {
95100
return nil, input, fmt.Errorf("input size is too small to be splited %d < %d ", len(input), size)
96101
}
97102
return input[:size], input[size:], nil
98103
}
99104

105+
// ReadUint8 reads a uint8 from the input and returns the rest
100106
func ReadUint8(input []byte) (value uint8, rest []byte, err error) {
101107
if len(input) < 1 {
102108
return 0, input, fmt.Errorf("input size (%d) is too small to read a uint8", len(input))
103109
}
104110
return uint8(input[0]), input[1:], nil
105111
}
106112

113+
// ReadUint16 reads a uint16 from the input and returns the rest
107114
func ReadUint16(input []byte) (value uint16, rest []byte, err error) {
108115
if len(input) < 2 {
109116
return 0, input, fmt.Errorf("input size (%d) is too small to read a uint16", len(input))
110117
}
111118
return binary.BigEndian.Uint16(input[:2]), input[2:], nil
112119
}
113120

121+
// ReadUint32 reads a uint32 from the input and returns the rest
114122
func ReadUint32(input []byte) (value uint32, rest []byte, err error) {
115123
if len(input) < 4 {
116124
return 0, input, fmt.Errorf("input size (%d) is too small to read a uint32", len(input))
117125
}
118126
return binary.BigEndian.Uint32(input[:4]), input[4:], nil
119127
}
120128

129+
// ReadUint64 reads a uint64 from the input and returns the rest
121130
func ReadUint64(input []byte) (value uint64, rest []byte, err error) {
122131
if len(input) < 8 {
123132
return 0, input, fmt.Errorf("input size (%d) is too small to read a uint64", len(input))
@@ -185,6 +194,7 @@ func ReadLongDataFromReader(reader io.Reader) ([]byte, error) {
185194
return buf, nil
186195
}
187196

197+
// ReadFromBuffer reads 'length' bytes from the input
188198
func ReadFromBuffer(reader io.Reader, length int) ([]byte, error) {
189199
if length == 0 {
190200
return nil, nil

ledger/complete/ledger.go

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/prometheus/client_golang/prometheus"
8+
"github.com/rs/zerolog"
89

910
"github.com/dapperlabs/flow-go/ledger"
1011
"github.com/dapperlabs/flow-go/ledger/common/encoding"
@@ -16,26 +17,29 @@ import (
1617
)
1718

1819
// Ledger (complete) is a fast memory-efficient fork-aware thread-safe trie-based key/value storage.
19-
// Ledger holds an array of registers (key value pairs) and keep tracks of changes over a limited time.
20+
// Ledger holds an array of registers (key-value pairs) and keeps tracks of changes over a limited time.
2021
// Each register is referenced by an ID (key) and holds a value (byte slice).
21-
// Ledger provides atomic batched update and read (with or without proofs) operation given a list of keys.
22+
// Ledger provides atomic batched updates and read (with or without proofs) operation given a list of keys.
2223
// Every update to the Ledger creates a new state which captures the state of the storage.
23-
// Under the hood, it uses binary merkle tries to generate inclusion and noninclusion proofs.
24-
// Ledger is fork-aware that means any update can applied at any previous state which forms a tree of tries (forest).
25-
// The forrest is in memory but all changes (e.g. register updates) are captured inside write-ahead-logs for crash recovery reasons.
26-
// In order to limit the memory usage and maintain the performance storage only keeps limited number of
27-
// tries and purge the old ones (LRU-based); in other words Ledger is not designed to be used
28-
// for archival use but make it possible for other software components to reconstruct very old tries using write-ahead logs.
24+
// Under the hood, it uses binary Merkle tries to generate inclusion and non-inclusion proofs.
25+
// Ledger is fork-aware which means any update can be applied at any previous state which forms a tree of tries (forest).
26+
// The forest is in memory but all changes (e.g. register updates) are captured inside write-ahead-logs for crash recovery reasons.
27+
// In order to limit the memory usage and maintain the performance storage only keeps a limited number of
28+
// tries and purge the old ones (LRU-based); in other words, Ledger is not designed to be used
29+
// for archival usage but make it possible for other software components to reconstruct very old tries using write-ahead logs.
2930
type Ledger struct {
3031
forest *mtrie.Forest
3132
wal *wal.LedgerWAL
3233
metrics module.LedgerMetrics
34+
logger zerolog.Logger
3335
}
3436

35-
const CacheSize = 1000
36-
3737
// NewLedger creates a new in-memory trie-backed ledger storage with persistence.
38-
func NewLedger(dbDir string, capacity int, metrics module.LedgerMetrics, reg prometheus.Registerer) (*Ledger, error) {
38+
func NewLedger(dbDir string,
39+
capacity int,
40+
metrics module.LedgerMetrics,
41+
log zerolog.Logger,
42+
reg prometheus.Registerer) (*Ledger, error) {
3943

4044
w, err := wal.NewWAL(nil, reg, dbDir, capacity, pathfinder.PathByteSize, wal.SegmentSize)
4145
if err != nil {
@@ -48,10 +52,14 @@ func NewLedger(dbDir string, capacity int, metrics module.LedgerMetrics, reg pro
4852
if err != nil {
4953
return nil, fmt.Errorf("cannot create forest: %w", err)
5054
}
55+
56+
logger := log.With().Str("ledger", "complete").Logger()
57+
5158
storage := &Ledger{
5259
forest: forest,
5360
wal: w,
5461
metrics: metrics,
62+
logger: logger,
5563
}
5664

5765
err = w.ReplayOnForest(forest)
@@ -157,7 +165,10 @@ func (l *Ledger) Set(update *ledger.Update) (newState ledger.State, err error) {
157165
l.metrics.UpdateDurationPerItem(durationPerValue)
158166
}
159167

160-
// TODO log info state
168+
l.logger.Info().Hex("from", update.State()).
169+
Hex("to", newRootHash[:]).
170+
Int("update_size", update.Size()).
171+
Msg("ledger updated")
161172
return ledger.State(newRootHash), nil
162173
}
163174

@@ -189,6 +200,7 @@ func (l *Ledger) CloseStorage() {
189200
_ = l.wal.Close()
190201
}
191202

203+
// MemSize return the amount of memory used by ledger
192204
// TODO implement an approximate MemSize method
193205
func (l *Ledger) MemSize() (int64, error) {
194206
return 0, nil
@@ -204,6 +216,7 @@ func (l *Ledger) ForestSize() int {
204216
return l.forest.Size()
205217
}
206218

219+
// Checkpointer returns a checkpointer instance
207220
func (l *Ledger) Checkpointer() (*wal.Checkpointer, error) {
208221
checkpointer, err := l.wal.NewCheckpointer()
209222
if err != nil {

ledger/complete/ledger_benchmark_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"testing"
88
"time"
99

10+
"github.com/rs/zerolog"
11+
1012
"github.com/dapperlabs/flow-go/ledger"
1113
"github.com/dapperlabs/flow-go/ledger/common/encoding"
1214
"github.com/dapperlabs/flow-go/ledger/common/utils"
@@ -38,7 +40,7 @@ func benchmarkStorage(steps int, b *testing.B) {
3840
b.Fatal(err)
3941
}
4042

41-
led, err := complete.NewLedger(dir, steps+1, &metrics.NoopCollector{}, nil)
43+
led, err := complete.NewLedger(dir, steps+1, &metrics.NoopCollector{}, zerolog.Logger{}, nil)
4244
defer led.Done()
4345
if err != nil {
4446
b.Fatal("can't create a new complete ledger")

0 commit comments

Comments
 (0)