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