Skip to content

Commit

Permalink
First draft of migration code
Browse files Browse the repository at this point in the history
  • Loading branch information
fasmat committed Sep 11, 2023
1 parent de3f9c1 commit bc3a2e2
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 38 deletions.
24 changes: 3 additions & 21 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package config

import (
"encoding/hex"
"errors"
"fmt"
"math"
"os"
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand Down
18 changes: 12 additions & 6 deletions initialization/initialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand All @@ -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)
}
174 changes: 174 additions & 0 deletions initialization/migrate_metadata.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 2 additions & 2 deletions initialization/vrf_search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions oracle/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down
65 changes: 60 additions & 5 deletions shared/post_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Loading

0 comments on commit bc3a2e2

Please sign in to comment.