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 }