Skip to content

Commit a055c66

Browse files
authored
feat: certstore snapshot export (#1032)
* feat: certstore snapshot export * resolve comments * roundtrip tests * export snapshot CID * return snap header in export func * GetCertStore codecov
1 parent 7f2287c commit a055c66

File tree

5 files changed

+533
-8
lines changed

5 files changed

+533
-8
lines changed

certstore/cbor_gen.go

Lines changed: 175 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

certstore/snapshot.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package certstore
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/binary"
7+
"errors"
8+
"fmt"
9+
"hash"
10+
"io"
11+
12+
"github.com/filecoin-project/go-f3/certs"
13+
"github.com/filecoin-project/go-f3/gpbft"
14+
"github.com/filecoin-project/go-state-types/cbor"
15+
cid "github.com/ipfs/go-cid"
16+
"github.com/ipfs/go-datastore"
17+
"github.com/multiformats/go-multihash"
18+
"golang.org/x/crypto/blake2b"
19+
)
20+
21+
var ErrUnknownLatestCertificate = errors.New("latest certificate is not known")
22+
23+
// ExportLatestSnapshot exports an F3 snapshot that includes the finality certificate chain until the current `latestCertificate`.
24+
//
25+
// Checkout the snapshot format specification at <https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0108.md>
26+
func (cs *Store) ExportLatestSnapshot(ctx context.Context, writer io.Writer) (cid.Cid, *SnapshotHeader, error) {
27+
if cs.latestCertificate == nil {
28+
return cid.Undef, nil, ErrUnknownLatestCertificate
29+
}
30+
return cs.ExportSnapshot(ctx, cs.latestCertificate.GPBFTInstance, writer)
31+
}
32+
33+
// ExportSnapshot exports an F3 snapshot that includes the finality certificate chain from the `Store.firstInstance` to the specified `lastInstance`.
34+
//
35+
// Checkout the snapshot format specification at <https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0108.md>
36+
func (cs *Store) ExportSnapshot(ctx context.Context, latestInstance uint64, writer io.Writer) (cid.Cid, *SnapshotHeader, error) {
37+
hasher, err := blake2b.New256(nil)
38+
if err != nil {
39+
return cid.Undef, nil, err
40+
}
41+
hashWriter := hashWriter{hasher, writer}
42+
initialPowerTable, err := cs.GetPowerTable(ctx, cs.firstInstance)
43+
if err != nil {
44+
return cid.Undef, nil, fmt.Errorf("failed to get initial power table at instance %d: %w", cs.firstInstance, err)
45+
}
46+
header := SnapshotHeader{1, cs.firstInstance, latestInstance, initialPowerTable}
47+
if _, err := header.WriteTo(hashWriter); err != nil {
48+
return cid.Undef, nil, fmt.Errorf("failed to write snapshot header: %w", err)
49+
}
50+
for i := cs.firstInstance; i <= latestInstance; i++ {
51+
cert, err := cs.ds.Get(ctx, cs.keyForCert(i))
52+
if err != nil {
53+
return cid.Undef, nil, fmt.Errorf("failed to get certificate at instance %d:: %w", i, err)
54+
}
55+
buffer := bytes.NewBuffer(cert)
56+
if _, err := writeSnapshotBlockBytes(hashWriter, buffer); err != nil {
57+
return cid.Undef, nil, err
58+
}
59+
}
60+
hash := hashWriter.hasher.Sum(nil)
61+
mh, err := multihash.Encode(hash, multihash.BLAKE2B_MIN+31)
62+
if err != nil {
63+
return cid.Undef, nil, err
64+
}
65+
66+
return cid.NewCidV1(cid.Raw, mh), &header, nil
67+
}
68+
69+
type hashWriter struct {
70+
hasher hash.Hash
71+
writer io.Writer
72+
}
73+
74+
func (w hashWriter) Write(p []byte) (n int, err error) {
75+
if _, err := w.hasher.Write(p); err != nil {
76+
return 0, err
77+
}
78+
return w.writer.Write(p)
79+
}
80+
81+
type SnapshotReader interface {
82+
io.Reader
83+
io.ByteReader
84+
}
85+
86+
// ImportSnapshotToDatastore imports an F3 snapshot into the specified Datastore
87+
//
88+
// Checkout the snapshot format specification at <https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0108.md>
89+
func ImportSnapshotToDatastore(ctx context.Context, snapshot SnapshotReader, ds datastore.Datastore) error {
90+
return importSnapshotToDatastoreWithTestingPowerTableFrequency(ctx, snapshot, ds, 0)
91+
}
92+
93+
func importSnapshotToDatastoreWithTestingPowerTableFrequency(ctx context.Context, snapshot SnapshotReader, ds datastore.Datastore, testingPowerTableFrequency uint64) error {
94+
headerBytes, err := readSnapshotBlockBytes(snapshot)
95+
if err != nil {
96+
return err
97+
}
98+
var header SnapshotHeader
99+
err = header.UnmarshalCBOR(bytes.NewReader(headerBytes))
100+
if err != nil {
101+
return fmt.Errorf("failed to decode snapshot header: %w", err)
102+
}
103+
cs, err := OpenOrCreateStore(ctx, ds, header.FirstInstance, header.InitialPowerTable)
104+
if testingPowerTableFrequency > 0 {
105+
cs.powerTableFrequency = testingPowerTableFrequency
106+
}
107+
if err != nil {
108+
return err
109+
}
110+
pt := header.InitialPowerTable
111+
for {
112+
certBytes, err := readSnapshotBlockBytes(snapshot)
113+
if err == io.EOF {
114+
break
115+
} else if err != nil {
116+
return fmt.Errorf("failed to decode finality certificate: %w", err)
117+
}
118+
var cert certs.FinalityCertificate
119+
cert.UnmarshalCBOR(bytes.NewReader(certBytes))
120+
if err = cs.Put(ctx, &cert); err != nil {
121+
return err
122+
}
123+
if pt, err = certs.ApplyPowerTableDiffs(pt, cert.PowerTableDelta); err != nil {
124+
return err
125+
}
126+
if (cert.GPBFTInstance+1)%cs.powerTableFrequency == 0 {
127+
if err := cs.putPowerTable(ctx, cert.GPBFTInstance+1, pt); err != nil {
128+
return err
129+
}
130+
}
131+
}
132+
return nil
133+
}
134+
135+
type SnapshotHeader struct {
136+
Version uint64
137+
FirstInstance uint64
138+
LatestInstance uint64
139+
InitialPowerTable gpbft.PowerEntries
140+
}
141+
142+
func (h *SnapshotHeader) WriteTo(w io.Writer) (int64, error) {
143+
return writeSnapshotCborEncodedBlock(w, h)
144+
}
145+
146+
// writeSnapshotCborEncodedBlock writes CBOR-encoded header or data block with a varint-encoded length prefix
147+
func writeSnapshotCborEncodedBlock(writer io.Writer, block cbor.Marshaler) (int64, error) {
148+
var buffer bytes.Buffer
149+
if err := block.MarshalCBOR(&buffer); err != nil {
150+
return 0, err
151+
}
152+
return writeSnapshotBlockBytes(writer, &buffer)
153+
}
154+
155+
// writeSnapshotBlockBytes writes header or data block with a varint-encoded length prefix
156+
func writeSnapshotBlockBytes(writer io.Writer, buffer *bytes.Buffer) (int64, error) {
157+
buf := make([]byte, 8)
158+
n := binary.PutUvarint(buf, uint64(buffer.Len()))
159+
len1, err := bytes.NewBuffer(buf[:n]).WriteTo(writer)
160+
if err != nil {
161+
return 0, err
162+
}
163+
len2, err := buffer.WriteTo(writer)
164+
if err != nil {
165+
return 0, err
166+
}
167+
return len1 + len2, nil
168+
}
169+
170+
func readSnapshotBlockBytes(reader SnapshotReader) ([]byte, error) {
171+
n1, err := binary.ReadUvarint(reader)
172+
if err != nil {
173+
return nil, err
174+
}
175+
buf := make([]byte, n1)
176+
n2, err := reader.Read(buf)
177+
if err != nil {
178+
return nil, err
179+
}
180+
if n2 != int(n1) {
181+
return nil, fmt.Errorf("incomplete block, %d bytes expected, %d bytes got", n1, n2)
182+
}
183+
return buf, nil
184+
}

0 commit comments

Comments
 (0)