-
Notifications
You must be signed in to change notification settings - Fork 13
feat: certstore snapshot export #1032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
740fe1a
896dde4
9ac1e74
6ac2aca
0ebf227
415f470
3cde113
7cc7b77
246fdf9
836c1d7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
package certstore | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"hash" | ||
"io" | ||
|
||
"github.com/filecoin-project/go-f3/certs" | ||
"github.com/filecoin-project/go-f3/gpbft" | ||
"github.com/filecoin-project/go-state-types/cbor" | ||
cid "github.com/ipfs/go-cid" | ||
"github.com/ipfs/go-datastore" | ||
"github.com/multiformats/go-multihash" | ||
"golang.org/x/crypto/blake2b" | ||
) | ||
|
||
var ErrUnknownLatestCertificate = errors.New("latest certificate is not known") | ||
|
||
// ExportLatestSnapshot exports an F3 snapshot that includes the finality certificate chain until the current `latestCertificate`. | ||
// | ||
// Checkout the snapshot format specification at <https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0108.md> | ||
func (cs *Store) ExportLatestSnapshot(ctx context.Context, writer io.Writer) (cid.Cid, *SnapshotHeader, error) { | ||
if cs.latestCertificate == nil { | ||
return cid.Undef, nil, ErrUnknownLatestCertificate | ||
} | ||
return cs.ExportSnapshot(ctx, cs.latestCertificate.GPBFTInstance, writer) | ||
} | ||
|
||
// ExportSnapshot exports an F3 snapshot that includes the finality certificate chain from the `Store.firstInstance` to the specified `lastInstance`. | ||
// | ||
// Checkout the snapshot format specification at <https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0108.md> | ||
func (cs *Store) ExportSnapshot(ctx context.Context, latestInstance uint64, writer io.Writer) (cid.Cid, *SnapshotHeader, error) { | ||
hasher, err := blake2b.New256(nil) | ||
if err != nil { | ||
return cid.Undef, nil, err | ||
} | ||
hashWriter := hashWriter{hasher, writer} | ||
initialPowerTable, err := cs.GetPowerTable(ctx, cs.firstInstance) | ||
if err != nil { | ||
return cid.Undef, nil, fmt.Errorf("failed to get initial power table at instance %d: %w", cs.firstInstance, err) | ||
} | ||
header := SnapshotHeader{1, cs.firstInstance, latestInstance, initialPowerTable} | ||
if _, err := header.WriteTo(hashWriter); err != nil { | ||
return cid.Undef, nil, fmt.Errorf("failed to write snapshot header: %w", err) | ||
} | ||
for i := cs.firstInstance; i <= latestInstance; i++ { | ||
cert, err := cs.ds.Get(ctx, cs.keyForCert(i)) | ||
if err != nil { | ||
return cid.Undef, nil, fmt.Errorf("failed to get certificate at instance %d:: %w", i, err) | ||
} | ||
buffer := bytes.NewBuffer(cert) | ||
if _, err := writeSnapshotBlockBytes(hashWriter, buffer); err != nil { | ||
return cid.Undef, nil, err | ||
} | ||
} | ||
hash := hashWriter.hasher.Sum(nil) | ||
mh, err := multihash.Encode(hash, multihash.BLAKE2B_MIN+31) | ||
if err != nil { | ||
return cid.Undef, nil, err | ||
} | ||
|
||
return cid.NewCidV1(cid.Raw, mh), &header, nil | ||
} | ||
|
||
type hashWriter struct { | ||
hasher hash.Hash | ||
writer io.Writer | ||
} | ||
|
||
func (w hashWriter) Write(p []byte) (n int, err error) { | ||
if _, err := w.hasher.Write(p); err != nil { | ||
return 0, err | ||
} | ||
return w.writer.Write(p) | ||
} | ||
|
||
type SnapshotReader interface { | ||
io.Reader | ||
io.ByteReader | ||
} | ||
|
||
// ImportSnapshotToDatastore imports an F3 snapshot into the specified Datastore | ||
// | ||
// Checkout the snapshot format specification at <https://github.com/filecoin-project/FIPs/blob/master/FRCs/frc-0108.md> | ||
func ImportSnapshotToDatastore(ctx context.Context, snapshot SnapshotReader, ds datastore.Datastore) error { | ||
return importSnapshotToDatastoreWithTestingPowerTableFrequency(ctx, snapshot, ds, 0) | ||
} | ||
|
||
func importSnapshotToDatastoreWithTestingPowerTableFrequency(ctx context.Context, snapshot SnapshotReader, ds datastore.Datastore, testingPowerTableFrequency uint64) error { | ||
headerBytes, err := readSnapshotBlockBytes(snapshot) | ||
if err != nil { | ||
return err | ||
} | ||
var header SnapshotHeader | ||
err = header.UnmarshalCBOR(bytes.NewReader(headerBytes)) | ||
if err != nil { | ||
return fmt.Errorf("failed to decode snapshot header: %w", err) | ||
} | ||
cs, err := OpenOrCreateStore(ctx, ds, header.FirstInstance, header.InitialPowerTable) | ||
if testingPowerTableFrequency > 0 { | ||
cs.powerTableFrequency = testingPowerTableFrequency | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
pt := header.InitialPowerTable | ||
for { | ||
certBytes, err := readSnapshotBlockBytes(snapshot) | ||
if err == io.EOF { | ||
break | ||
} else if err != nil { | ||
return fmt.Errorf("failed to decode finality certificate: %w", err) | ||
} | ||
var cert certs.FinalityCertificate | ||
cert.UnmarshalCBOR(bytes.NewReader(certBytes)) | ||
if err = cs.Put(ctx, &cert); err != nil { | ||
return err | ||
} | ||
if pt, err = certs.ApplyPowerTableDiffs(pt, cert.PowerTableDelta); err != nil { | ||
return err | ||
} | ||
if (cert.GPBFTInstance+1)%cs.powerTableFrequency == 0 { | ||
if err := cs.putPowerTable(ctx, cert.GPBFTInstance+1, pt); err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
type SnapshotHeader struct { | ||
Version uint64 | ||
FirstInstance uint64 | ||
LatestInstance uint64 | ||
InitialPowerTable gpbft.PowerEntries | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to fix the upper limit length for this just to be on the safe side? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current limit works fine on mainnet |
||
} | ||
|
||
func (h *SnapshotHeader) WriteTo(w io.Writer) (int64, error) { | ||
return writeSnapshotCborEncodedBlock(w, h) | ||
} | ||
|
||
// writeSnapshotCborEncodedBlock writes CBOR-encoded header or data block with a varint-encoded length prefix | ||
func writeSnapshotCborEncodedBlock(writer io.Writer, block cbor.Marshaler) (int64, error) { | ||
var buffer bytes.Buffer | ||
if err := block.MarshalCBOR(&buffer); err != nil { | ||
return 0, err | ||
} | ||
return writeSnapshotBlockBytes(writer, &buffer) | ||
} | ||
|
||
// writeSnapshotBlockBytes writes header or data block with a varint-encoded length prefix | ||
func writeSnapshotBlockBytes(writer io.Writer, buffer *bytes.Buffer) (int64, error) { | ||
buf := make([]byte, 8) | ||
n := binary.PutUvarint(buf, uint64(buffer.Len())) | ||
len1, err := bytes.NewBuffer(buf[:n]).WriteTo(writer) | ||
if err != nil { | ||
return 0, err | ||
} | ||
len2, err := buffer.WriteTo(writer) | ||
if err != nil { | ||
return 0, err | ||
} | ||
return len1 + len2, nil | ||
} | ||
|
||
func readSnapshotBlockBytes(reader SnapshotReader) ([]byte, error) { | ||
n1, err := binary.ReadUvarint(reader) | ||
if err != nil { | ||
return nil, err | ||
} | ||
buf := make([]byte, n1) | ||
n2, err := reader.Read(buf) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if n2 != int(n1) { | ||
return nil, fmt.Errorf("incomplete block, %d bytes expected, %d bytes got", n1, n2) | ||
} | ||
return buf, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have questions about how to use it, especially with go-car, because IDK if we can provide the header with the CID afterwards.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a draft Forest implementation of v2 snapshot export. The flow is
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
go-car has
ReplaceRootsInFile
which will update the header after write as long as you start off with a dummy root CID that's the same length: https://pkg.go.dev/github.com/ipld/go-car/v2#ReplaceRootsInFileThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks Rod