gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/persist_journal.go (about)

     1  package contractor
     2  
     3  // The contractor achieves efficient persistence using a JSON transaction
     4  // journal. It enables efficient ACID transactions on JSON objects.
     5  //
     6  // The journal represents a single JSON object, containing all of the
     7  // contractor's persisted data. The object is serialized as an "initial
     8  // object" followed by a series of update sets, one per line. Each update
     9  // specifies a modification.
    10  //
    11  // During operation, the object is first loaded by reading the file and
    12  // applying each update to the initial object. It is subsequently modified by
    13  // appending update sets to the file, one per line. At any time, a
    14  // "checkpoint" may be created, which clears the journal and starts over with
    15  // a new initial object. This allows for compaction of the journal file.
    16  //
    17  // In the event of power failure or other serious disruption, the most recent
    18  // update set may be only partially written. Partially written update sets are
    19  // simply ignored when reading the journal.
    20  
    21  import (
    22  	"encoding/json"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  
    27  	"gitlab.com/NebulousLabs/errors"
    28  	"gitlab.com/SkynetLabs/skyd/build"
    29  	"gitlab.com/SkynetLabs/skyd/skymodules"
    30  	"gitlab.com/SkynetLabs/skyd/skymodules/renter/proto"
    31  	"go.sia.tech/siad/crypto"
    32  	"go.sia.tech/siad/modules"
    33  	"go.sia.tech/siad/persist"
    34  	"go.sia.tech/siad/types"
    35  )
    36  
    37  var journalMeta = persist.Metadata{
    38  	Header:  "Contractor Journal",
    39  	Version: "1.1.1",
    40  }
    41  
    42  type journalPersist struct {
    43  	Allowance       skymodules.Allowance                `json:"allowance"`
    44  	BlockHeight     types.BlockHeight                   `json:"blockheight"`
    45  	CachedRevisions map[string]proto.V130CachedRevision `json:"cachedrevisions"`
    46  	Contracts       map[string]proto.V130Contract       `json:"contracts"`
    47  	CurrentPeriod   types.BlockHeight                   `json:"currentperiod"`
    48  	LastChange      modules.ConsensusChangeID           `json:"lastchange"`
    49  	OldContracts    []proto.V130Contract                `json:"oldcontracts"`
    50  	RenewedIDs      map[string]string                   `json:"renewedids"`
    51  }
    52  
    53  // A journal is a log of updates to a JSON object.
    54  type journal struct {
    55  	f        *os.File
    56  	filename string
    57  }
    58  
    59  // Close closes the underlying file.
    60  func (j *journal) Close() error {
    61  	return j.f.Close()
    62  }
    63  
    64  // openJournal opens the supplied journal and decodes the reconstructed
    65  // journalPersist into data.
    66  func openJournal(filename string, data *journalPersist) (*journal, error) {
    67  	// Open file handle for reading and writing.
    68  	f, err := os.OpenFile(filename, os.O_RDWR, 0)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	// Decode the metadata.
    74  	dec := json.NewDecoder(f)
    75  	var meta persist.Metadata
    76  	if err = dec.Decode(&meta); err != nil {
    77  		return nil, err
    78  	} else if meta.Header != journalMeta.Header {
    79  		return nil, fmt.Errorf("expected header %q, got %q", journalMeta.Header, meta.Header)
    80  	} else if meta.Version != journalMeta.Version {
    81  		return nil, fmt.Errorf("journal version (%s) is incompatible with the current version (%s)", meta.Version, journalMeta.Version)
    82  	}
    83  
    84  	// Decode the initial object.
    85  	if err = dec.Decode(data); err != nil {
    86  		return nil, err
    87  	}
    88  
    89  	// Make sure all maps are properly initialized.
    90  	if data.CachedRevisions == nil {
    91  		data.CachedRevisions = map[string]proto.V130CachedRevision{}
    92  	}
    93  	if data.Contracts == nil {
    94  		data.Contracts = map[string]proto.V130Contract{}
    95  	}
    96  	if data.RenewedIDs == nil {
    97  		data.RenewedIDs = map[string]string{}
    98  	}
    99  
   100  	// Decode each set of updates and apply them to data.
   101  	for {
   102  		var set updateSet
   103  		if err = dec.Decode(&set); errors.Contains(err, io.EOF) || errors.Contains(err, io.ErrUnexpectedEOF) {
   104  			// unexpected EOF means the last update was corrupted; skip it
   105  			break
   106  		} else if err != nil {
   107  			// skip corrupted update sets
   108  			continue
   109  		}
   110  		for _, u := range set {
   111  			u.apply(data)
   112  		}
   113  	}
   114  
   115  	return &journal{
   116  		f:        f,
   117  		filename: filename,
   118  	}, nil
   119  }
   120  
   121  type journalUpdate interface {
   122  	apply(*journalPersist)
   123  }
   124  
   125  type marshaledUpdate struct {
   126  	Type     string          `json:"type"`
   127  	Data     json.RawMessage `json:"data"`
   128  	Checksum crypto.Hash     `json:"checksum"`
   129  }
   130  
   131  type updateSet []journalUpdate
   132  
   133  // UnmarshalJSON unmarshals an array of marshaledUpdates as a set of
   134  // journalUpdates.
   135  func (set *updateSet) UnmarshalJSON(b []byte) error {
   136  	var marshaledSet []marshaledUpdate
   137  	if err := json.Unmarshal(b, &marshaledSet); err != nil {
   138  		return err
   139  	}
   140  	for _, u := range marshaledSet {
   141  		if crypto.HashBytes(u.Data) != u.Checksum {
   142  			return errors.New("bad checksum")
   143  		}
   144  		var err error
   145  		switch u.Type {
   146  		case "uploadRevision":
   147  			var ur updateUploadRevision
   148  			err = json.Unmarshal(u.Data, &ur)
   149  			*set = append(*set, ur)
   150  		case "downloadRevision":
   151  			var dr updateDownloadRevision
   152  			err = json.Unmarshal(u.Data, &dr)
   153  			*set = append(*set, dr)
   154  		case "cachedUploadRevision":
   155  			var cur updateCachedUploadRevision
   156  			err = json.Unmarshal(u.Data, &cur)
   157  			*set = append(*set, cur)
   158  		case "cachedDownloadRevision":
   159  			var cdr updateCachedDownloadRevision
   160  			err = json.Unmarshal(u.Data, &cdr)
   161  			*set = append(*set, cdr)
   162  		}
   163  		if err != nil {
   164  			return err
   165  		}
   166  	}
   167  	return nil
   168  }
   169  
   170  // updateUploadRevision is a journalUpdate that records the new data
   171  // associated with uploading a sector to a host.
   172  type updateUploadRevision struct {
   173  	NewRevisionTxn     types.Transaction `json:"newrevisiontxn"`
   174  	NewSectorRoot      crypto.Hash       `json:"newsectorroot"`
   175  	NewSectorIndex     int               `json:"newsectorindex"`
   176  	NewUploadSpending  types.Currency    `json:"newuploadspending"`
   177  	NewStorageSpending types.Currency    `json:"newstoragespending"`
   178  }
   179  
   180  // apply sets the LastRevision, LastRevisionTxn, UploadSpending, and
   181  // DownloadSpending fields of the contract being revised. It also adds the new
   182  // Merkle root to the contract's Merkle root set.
   183  func (u updateUploadRevision) apply(data *journalPersist) {
   184  	if len(u.NewRevisionTxn.FileContractRevisions) == 0 {
   185  		build.Critical("updateUploadRevision is missing its FileContractRevision")
   186  		return
   187  	}
   188  
   189  	rev := u.NewRevisionTxn.FileContractRevisions[0]
   190  	c := data.Contracts[rev.ParentID.String()]
   191  	c.LastRevisionTxn = u.NewRevisionTxn
   192  
   193  	if u.NewSectorIndex == len(c.MerkleRoots) {
   194  		c.MerkleRoots = append(c.MerkleRoots, u.NewSectorRoot)
   195  	} else if u.NewSectorIndex < len(c.MerkleRoots) {
   196  		c.MerkleRoots[u.NewSectorIndex] = u.NewSectorRoot
   197  	} else {
   198  		// Shouldn't happen. TODO: Correctly handle error.
   199  	}
   200  
   201  	c.UploadSpending = u.NewUploadSpending
   202  	c.StorageSpending = u.NewStorageSpending
   203  	data.Contracts[rev.ParentID.String()] = c
   204  }
   205  
   206  // updateUploadRevision is a journalUpdate that records the new data
   207  // associated with downloading a sector from a host.
   208  type updateDownloadRevision struct {
   209  	NewRevisionTxn      types.Transaction `json:"newrevisiontxn"`
   210  	NewDownloadSpending types.Currency    `json:"newdownloadspending"`
   211  }
   212  
   213  // apply sets the LastRevision, LastRevisionTxn, and DownloadSpending fields
   214  // of the contract being revised.
   215  func (u updateDownloadRevision) apply(data *journalPersist) {
   216  	if len(u.NewRevisionTxn.FileContractRevisions) == 0 {
   217  		build.Critical("updateDownloadRevision is missing its FileContractRevision")
   218  		return
   219  	}
   220  	rev := u.NewRevisionTxn.FileContractRevisions[0]
   221  	c := data.Contracts[rev.ParentID.String()]
   222  	c.LastRevisionTxn = u.NewRevisionTxn
   223  	c.DownloadSpending = u.NewDownloadSpending
   224  	data.Contracts[rev.ParentID.String()] = c
   225  }
   226  
   227  // updateCachedUploadRevision is a journalUpdate that records the unsigned
   228  // revision sent to the host during a sector upload, along with the Merkle
   229  // root of the new sector.
   230  type updateCachedUploadRevision struct {
   231  	Revision    types.FileContractRevision `json:"revision"`
   232  	SectorRoot  crypto.Hash                `json:"sectorroot"`
   233  	SectorIndex int                        `json:"sectorindex"`
   234  }
   235  
   236  // apply sets the Revision field of the cachedRevision associated with the
   237  // contract being revised, as well as the Merkle root of the new sector.
   238  func (u updateCachedUploadRevision) apply(data *journalPersist) {
   239  	c := data.CachedRevisions[u.Revision.ParentID.String()]
   240  	c.Revision = u.Revision
   241  	if u.SectorIndex == len(c.MerkleRoots) {
   242  		c.MerkleRoots = append(c.MerkleRoots, u.SectorRoot)
   243  	} else if u.SectorIndex < len(c.MerkleRoots) {
   244  		c.MerkleRoots[u.SectorIndex] = u.SectorRoot
   245  	} else {
   246  		// Shouldn't happen. TODO: Add correct error handling.
   247  	}
   248  	data.CachedRevisions[u.Revision.ParentID.String()] = c
   249  }
   250  
   251  // updateCachedDownloadRevision is a journalUpdate that records the unsigned
   252  // revision sent to the host during a sector download.
   253  type updateCachedDownloadRevision struct {
   254  	Revision types.FileContractRevision `json:"revision"`
   255  }
   256  
   257  // apply sets the Revision field of the cachedRevision associated with the
   258  // contract being revised.
   259  func (u updateCachedDownloadRevision) apply(data *journalPersist) {
   260  	c := data.CachedRevisions[u.Revision.ParentID.String()]
   261  	c.Revision = u.Revision
   262  	data.CachedRevisions[u.Revision.ParentID.String()] = c
   263  }