From bc3a2e2d4504a573f59aa3607433e9b95114f905 Mon Sep 17 00:00:00 2001 From: Matthias <5011972+fasmat@users.noreply.github.com> Date: Thu, 3 Aug 2023 14:40:39 +0000 Subject: [PATCH] First draft of migration code --- config/config.go | 24 +--- initialization/initialization.go | 18 ++- initialization/migrate_metadata.go | 174 +++++++++++++++++++++++++++++ initialization/vrf_search_test.go | 4 +- oracle/oracle.go | 4 +- shared/post_metadata.go | 65 ++++++++++- verifying/verifying_options.go | 5 +- 7 files changed, 256 insertions(+), 38 deletions(-) create mode 100644 initialization/migrate_metadata.go diff --git a/config/config.go b/config/config.go index ab28421b0..6b399cef3 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,6 @@ package config import ( "encoding/hex" - "errors" "fmt" "math" "os" @@ -128,7 +127,7 @@ type InitOpts struct { MaxFileSize uint64 ProviderID *uint32 Throttle bool - Scrypt ScryptParams + Scrypt shared.ScryptParams // ComputeBatchSize must be greater than 0 ComputeBatchSize uint64 @@ -150,25 +149,8 @@ func (o *InitOpts) TotalFiles(labelsPerUnit uint64) int { return int(math.Ceil(float64(o.TotalLabels(labelsPerUnit)) / float64(o.MaxFileNumLabels()))) } -type ScryptParams struct { - N, R, P uint -} - -func (p *ScryptParams) Validate() error { - if p.N == 0 { - return errors.New("scrypt parameter N cannot be 0") - } - if p.R == 0 { - return errors.New("scrypt parameter r cannot be 0") - } - if p.P == 0 { - return errors.New("scrypt parameter p cannot be 0") - } - return nil -} - -func DefaultLabelParams() ScryptParams { - return ScryptParams{ +func DefaultLabelParams() shared.ScryptParams { + return shared.ScryptParams{ N: 8192, R: 1, P: 1, diff --git a/initialization/initialization.go b/initialization/initialization.go index e72ef226c..903d35fec 100644 --- a/initialization/initialization.go +++ b/initialization/initialization.go @@ -171,8 +171,8 @@ type Initializer struct { // these values are atomics so they can be read from multiple other goroutines safely // write is protected by mtx - nonceValue atomic.Pointer[[]byte] nonce atomic.Pointer[uint64] + nonceValue atomic.Pointer[[]byte] lastPosition atomic.Pointer[uint64] numLabelsWritten atomic.Uint64 @@ -681,13 +681,18 @@ func (init *Initializer) verifyMetadata(m *shared.PostMetadata) error { func (init *Initializer) saveMetadata() error { v := shared.PostMetadata{ + Version: 1, + NodeId: init.nodeId, CommitmentAtxId: init.commitmentAtxId, - LabelsPerUnit: init.cfg.LabelsPerUnit, - NumUnits: init.opts.NumUnits, - MaxFileSize: init.opts.MaxFileSize, - Nonce: init.nonce.Load(), - LastPosition: init.lastPosition.Load(), + + LabelsPerUnit: init.cfg.LabelsPerUnit, + NumUnits: init.opts.NumUnits, + MaxFileSize: init.opts.MaxFileSize, + Scrypt: init.opts.Scrypt, + + Nonce: init.nonce.Load(), + LastPosition: init.lastPosition.Load(), } if init.nonceValue.Load() != nil { v.NonceValue = *init.nonceValue.Load() @@ -696,5 +701,6 @@ func (init *Initializer) saveMetadata() error { } func (init *Initializer) loadMetadata() (*shared.PostMetadata, error) { + // TODO(mafa): migrate metadata if needed before loading it return LoadMetadata(init.opts.DataDir) } diff --git a/initialization/migrate_metadata.go b/initialization/migrate_metadata.go new file mode 100644 index 000000000..4b1b1b728 --- /dev/null +++ b/initialization/migrate_metadata.go @@ -0,0 +1,174 @@ +package initialization + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/natefinch/atomic" + "go.uber.org/zap" + + "github.com/spacemeshos/post/config" + "github.com/spacemeshos/post/oracle" + "github.com/spacemeshos/post/shared" +) + +var migrateData map[int]func(dir string, logger *zap.Logger) (err error) + +func init() { + migrateData = make(map[int]func(dir string, logger *zap.Logger) (err error)) + migrateData[0] = migrateV0 +} + +type MetadataVersion struct { + Version int `json:",omitempty"` +} + +// MigratePoST migrates the PoST metadata file to the latest version. +func MigratePoST(dir string, logger *zap.Logger) (err error) { + logger.Info("checking PoST for migrations") + + filename := filepath.Join(dir, MetadataFileName) + file, err := os.Open(filename) + switch { + case os.IsNotExist(err): + return ErrStateMetadataFileMissing + case err != nil: + return fmt.Errorf("could not open metadata file: %w", err) + } + defer file.Close() + + version := MetadataVersion{} + if err := json.NewDecoder(file).Decode(&version); err != nil { + return fmt.Errorf("failed to determine metadata version: %w", err) + } + + if version.Version == len(migrateData) { + logger.Info("PoST is up to date, no migration needed") + return nil + } + + if version.Version > len(migrateData) { + return fmt.Errorf("PoST metadata version %d is newer than the latest supported version %d", version.Version, len(migrateData)) + } + + logger.Info("determined PoST version", zap.Int("version", version.Version)) + + for v := version.Version; v < len(migrateData); v++ { + if err := migrateData[v](dir, logger); err != nil { + return fmt.Errorf("failed to migrate metadata from version %d to version %d: %w", v, v+1, err) + } + + logger.Info("migrated PoST successfully to version", zap.Int("version", v+1)) + } + + logger.Info("PoST migration process finished successfully") + return nil +} + +type postMetadataV0 struct { + NodeId []byte + CommitmentAtxId []byte + + LabelsPerUnit uint64 + NumUnits uint32 + MaxFileSize uint64 + Nonce *uint64 `json:",omitempty"` + NonceValue shared.NonceValue `json:",omitempty"` + LastPosition *uint64 `json:",omitempty"` +} + +// migrateV0 upgrades PoST from version 0 to version 1. +// +// - add version field to postdata_metadata.json (missing in version 0) +// - add NonceValue field to postdata_metadata.json if missing (was introduced before migrations, not every PoST version 0 metadata file has it) +// - re-encode NodeId and CommitmentAtxId as hex strings. +// - add Scrypt field to postdata_metadata.json (missing in version 0), assume default mainnet values. +func migrateV0(dir string, logger *zap.Logger) (err error) { + filename := filepath.Join(dir, MetadataFileName) + file, err := os.Open(filename) + switch { + case os.IsNotExist(err): + return ErrStateMetadataFileMissing + case err != nil: + return fmt.Errorf("could not read metadata file: %w", err) + } + defer file.Close() + + old := postMetadataV0{} + if err := json.NewDecoder(file).Decode(&old); err != nil { + return fmt.Errorf("failed to determine metadata version: %w", err) + } + + var nodeID shared.NodeID + if len(old.NodeId) != 32 { + return fmt.Errorf("invalid node ID length: %d", len(old.NodeId)) + } + copy(nodeID[:], old.NodeId) + + var commitmentAtxID shared.ATXID + if len(old.CommitmentAtxId) != 32 { + return fmt.Errorf("invalid commitment ATX ID length: %d", len(old.CommitmentAtxId)) + } + copy(commitmentAtxID[:], old.CommitmentAtxId) + + new := shared.PostMetadata{ + Version: 1, + + NodeId: nodeID, + CommitmentAtxId: commitmentAtxID, + + LabelsPerUnit: old.LabelsPerUnit, + NumUnits: old.NumUnits, + MaxFileSize: old.MaxFileSize, + Scrypt: config.DefaultLabelParams(), // we don't know the scrypt params, but on mainnet they are the default ones + + Nonce: old.Nonce, + NonceValue: old.NonceValue, + LastPosition: old.LastPosition, + } + + if new.Nonce != nil && new.NonceValue == nil { + // there is a nonce in the metadata but no nonce value + commitment := oracle.CommitmentBytes(new.NodeId[:], new.CommitmentAtxId[:]) + cpuProviderID := CPUProviderID() + + wo, err := oracle.New( + oracle.WithProviderID(&cpuProviderID), + oracle.WithCommitment(commitment), + oracle.WithVRFDifficulty(make([]byte, 32)), // we are not looking for it, so set difficulty to 0 + oracle.WithScryptParams(new.Scrypt), + oracle.WithLogger(logger), + ) + if err != nil { + return fmt.Errorf("failed to create oracle: %w", err) + } + + result, err := wo.Position(*new.Nonce) + if err != nil { + return fmt.Errorf("failed to compute nonce value: %w", err) + } + copy(new.NonceValue, result.Output) + } + + tmp, err := os.Create(fmt.Sprintf("%s.tmp", filename)) + if err != nil { + return fmt.Errorf("create temporary file %s: %w", tmp.Name(), err) + } + defer tmp.Close() + + if err := json.NewEncoder(tmp).Encode(new); err != nil { + return fmt.Errorf("failed to encode metadata during migration: %w", err) + } + + if err := tmp.Close(); err != nil { + return fmt.Errorf("failed to close tmp file %s: %w", tmp.Name(), err) + } + + if err := atomic.ReplaceFile(tmp.Name(), filename); err != nil { + return fmt.Errorf("save file from %s, %s: %w", tmp.Name(), filename, err) + } + + return nil +} diff --git a/initialization/vrf_search_test.go b/initialization/vrf_search_test.go index a2f61ad5a..ac2f99936 100644 --- a/initialization/vrf_search_test.go +++ b/initialization/vrf_search_test.go @@ -9,9 +9,9 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zaptest" - "github.com/spacemeshos/post/config" "github.com/spacemeshos/post/internal/postrs" "github.com/spacemeshos/post/oracle" + "github.com/spacemeshos/post/shared" ) func TestCheckLabel(t *testing.T) { @@ -20,7 +20,7 @@ func TestCheckLabel(t *testing.T) { oracle.WithProviderID(&cpuProviderID), oracle.WithCommitment(make([]byte, 32)), oracle.WithVRFDifficulty(make([]byte, 32)), - oracle.WithScryptParams(config.ScryptParams{ + oracle.WithScryptParams(shared.ScryptParams{ N: 2, R: 1, P: 1, diff --git a/oracle/oracle.go b/oracle/oracle.go index 44659a026..1f37792c1 100644 --- a/oracle/oracle.go +++ b/oracle/oracle.go @@ -8,8 +8,8 @@ import ( "go.uber.org/zap" - "github.com/spacemeshos/post/config" "github.com/spacemeshos/post/internal/postrs" + "github.com/spacemeshos/post/shared" ) // ErrWorkOracleClosed is returned when calling a method on an already closed WorkOracle instance. @@ -84,7 +84,7 @@ func WithVRFDifficulty(difficulty []byte) OptionFunc { // WithScryptParams sets the parameters for the scrypt algorithm. // At the moment only configuring N is supported. r and p are fixed at 1 (due to limitations in the OpenCL implementation). -func WithScryptParams(params config.ScryptParams) OptionFunc { +func WithScryptParams(params shared.ScryptParams) OptionFunc { return func(opts *option) error { if params.P != 1 || params.R != 1 { return errors.New("invalid scrypt params: only r = 1, p = 1 are supported for initialization") diff --git a/shared/post_metadata.go b/shared/post_metadata.go index 1b388380e..ee2ed16e0 100644 --- a/shared/post_metadata.go +++ b/shared/post_metadata.go @@ -3,19 +3,44 @@ package shared import ( "encoding/hex" "encoding/json" + "errors" ) +// ErrStateMetadataFileMissing is returned when the metadata file is missing. +var ErrStateMetadataFileMissing = errors.New("metadata file is missing") + // PostMetadata is the data associated with the PoST init procedure, persisted in the datadir next to the init files. type PostMetadata struct { - NodeId []byte - CommitmentAtxId []byte + Version int `json:",omitempty"` + + NodeId NodeID + CommitmentAtxId ATXID LabelsPerUnit uint64 NumUnits uint32 MaxFileSize uint64 - Nonce *uint64 `json:",omitempty"` - NonceValue NonceValue `json:",omitempty"` - LastPosition *uint64 `json:",omitempty"` + Scrypt ScryptParams + + Nonce *uint64 `json:",omitempty"` + NonceValue NonceValue `json:",omitempty"` + LastPosition *uint64 `json:",omitempty"` +} + +type ScryptParams struct { + N, R, P uint +} + +func (p *ScryptParams) Validate() error { + if p.N == 0 { + return errors.New("scrypt parameter N cannot be 0") + } + if p.R == 0 { + return errors.New("scrypt parameter r cannot be 0") + } + if p.P == 0 { + return errors.New("scrypt parameter p cannot be 0") + } + return nil } type NonceValue []byte @@ -32,3 +57,33 @@ func (n *NonceValue) UnmarshalJSON(data []byte) (err error) { *n, err = hex.DecodeString(hexString) return } + +type NodeID []byte + +func (n NodeID) MarshalJSON() ([]byte, error) { + return json.Marshal(hex.EncodeToString(n)) +} + +func (n *NodeID) UnmarshalJSON(data []byte) (err error) { + var hexString string + if err = json.Unmarshal(data, &hexString); err != nil { + return + } + *n, err = hex.DecodeString(hexString) + return +} + +type ATXID []byte + +func (a ATXID) MarshalJSON() ([]byte, error) { + return json.Marshal(hex.EncodeToString(a[:])) +} + +func (a *ATXID) UnmarshalJSON(data []byte) (err error) { + var hexString string + if err = json.Unmarshal(data, &hexString); err != nil { + return + } + *a, err = hex.DecodeString(hexString) + return +} diff --git a/verifying/verifying_options.go b/verifying/verifying_options.go index ca7070df2..b8835438e 100644 --- a/verifying/verifying_options.go +++ b/verifying/verifying_options.go @@ -4,12 +4,13 @@ import ( "errors" "github.com/spacemeshos/post/config" + "github.com/spacemeshos/post/shared" ) type option struct { powFlags config.PowFlags // scrypt parameters for labels initialization - labelScrypt config.ScryptParams + labelScrypt shared.ScryptParams powCreatorId []byte } @@ -33,7 +34,7 @@ func applyOpts(options ...OptionFunc) (*option, error) { type OptionFunc func(*option) error -func WithLabelScryptParams(params config.ScryptParams) OptionFunc { +func WithLabelScryptParams(params shared.ScryptParams) OptionFunc { return func(o *option) error { o.labelScrypt = params return nil