// negotiateRevision sends the revision and new piece data to the host. func negotiateRevision(conn net.Conn, rev types.FileContractRevision, piece []byte, secretKey crypto.SecretKey) (types.Transaction, error) { conn.SetDeadline(time.Now().Add(5 * time.Minute)) // sufficient to transfer 4 MB over 100 kbps defer conn.SetDeadline(time.Time{}) // reset timeout after each revision // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(rev.ParentID), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // renter key is always first -- see negotiateContract }}, } // sign the transaction encodedSig, _ := crypto.SignHash(signedTxn.SigHash(0), secretKey) // no error possible signedTxn.TransactionSignatures[0].Signature = encodedSig[:] // send the transaction if err := encoding.WriteObject(conn, signedTxn); err != nil { return types.Transaction{}, errors.New("couldn't send revision transaction: " + err.Error()) } // host sends acceptance var response string if err := encoding.ReadObject(conn, &response, 128); err != nil { return types.Transaction{}, errors.New("couldn't read host acceptance: " + err.Error()) } if response != modules.AcceptResponse { return types.Transaction{}, errors.New("host rejected revision: " + response) } // transfer piece if _, err := conn.Write(piece); err != nil { return types.Transaction{}, errors.New("couldn't transfer piece: " + err.Error()) } // read txn signed by host var signedHostTxn types.Transaction if err := encoding.ReadObject(conn, &signedHostTxn, types.BlockSizeLimit); err != nil { return types.Transaction{}, errors.New("couldn't read signed revision transaction: " + err.Error()) } if signedHostTxn.ID() != signedTxn.ID() { return types.Transaction{}, errors.New("host sent bad signed transaction") } return signedHostTxn, nil }
// managedRPCRevise is an RPC that allows a renter to revise a file contract. It will // read new revisions in a loop until the renter sends a termination signal. func (h *Host) managedRPCRevise(conn net.Conn) error { // read ID of contract to be revised var fcid types.FileContractID if err := encoding.ReadObject(conn, &fcid, crypto.HashSize); err != nil { return errors.New("couldn't read contract ID: " + err.Error()) } // remove conn deadline while we wait for lock and rebuild the Merkle tree. err := conn.SetDeadline(time.Now().Add(15 * time.Minute)) if err != nil { return err } h.mu.RLock() obligation, exists := h.obligationsByID[fcid] h.mu.RUnlock() if !exists { return errors.New("no record of that contract") } // need to protect against two simultaneous revisions to the same // contract; this can cause inconsistency and data loss, making storage // proofs impossible // // TODO: DOS vector - the host has locked the obligation even though the // renter has not proven themselves to be the owner of the file contract. obligation.mu.Lock() defer obligation.mu.Unlock() // open the file in append mode file, err := os.OpenFile(obligation.Path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0660) if err != nil { return err } // rebuild current Merkle tree tree := crypto.NewTree() err = tree.ReadSegments(file) if err != nil { // Error does not need to be checked when closing the file, already // there have been issues related to the filesystem. _ = file.Close() return err } // accept new revisions in a loop. The final good transaction will be // submitted to the blockchain. revisionErr := func() error { for { // allow 5 minutes between revisions err := conn.SetDeadline(time.Now().Add(5 * time.Minute)) if err != nil { return err } // read proposed revision var revTxn types.Transaction if err = encoding.ReadObject(conn, &revTxn, types.BlockSizeLimit); err != nil { return errors.New("couldn't read revision: " + err.Error()) } // an empty transaction indicates completion if revTxn.ID() == (types.Transaction{}).ID() { return nil } // allow 5 minutes for each revision err = conn.SetDeadline(time.Now().Add(5 * time.Minute)) if err != nil { return err } // check revision against original file contract h.mu.RLock() err = h.considerRevision(revTxn, obligation) h.mu.RUnlock() if err != nil { // There is nothing that can be done if there is an error while // writing to a connection. _ = encoding.WriteObject(conn, err.Error()) return err } // indicate acceptance if err := encoding.WriteObject(conn, modules.AcceptResponse); err != nil { return errors.New("couldn't write acceptance: " + err.Error()) } // read piece // TODO: simultaneously read into tree and file rev := revTxn.FileContractRevisions[0] piece := make([]byte, rev.NewFileSize-obligation.fileSize()) _, err = io.ReadFull(conn, piece) if err != nil { return errors.New("couldn't read piece data: " + err.Error()) } // verify Merkle root err = tree.ReadSegments(bytes.NewReader(piece)) if err != nil { return errors.New("couldn't verify Merkle root: " + err.Error()) } if tree.Root() != rev.NewFileMerkleRoot { return errors.New("revision has bad Merkle root") } // manually sign the transaction revTxn.TransactionSignatures = append(revTxn.TransactionSignatures, types.TransactionSignature{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 1, // host key is always second }) encodedSig, err := crypto.SignHash(revTxn.SigHash(1), h.secretKey) if err != nil { return err } revTxn.TransactionSignatures[1].Signature = encodedSig[:] // append piece to file if _, err := file.Write(piece); err != nil { return errors.New("couldn't write new data to file: " + err.Error()) } // save updated obligation to disk h.mu.Lock() h.reviseObligation(revTxn) h.mu.Unlock() // send the signed transaction - this must be the last thing that happens. if err := encoding.WriteObject(conn, revTxn); err != nil { return errors.New("couldn't write signed revision transaction: " + err.Error()) } } }() err = file.Close() if err != nil { return err } err = h.tpool.AcceptTransactionSet([]types.Transaction{obligation.RevisionTransaction}) if err != nil { h.log.Println("WARN: transaction pool rejected revision transaction: " + err.Error()) } return revisionErr }
// buildExplorerTransaction takes a transaction and the height + id of the // block it appears in an uses that to build an explorer transaction. func (srv *Server) buildExplorerTransaction(height types.BlockHeight, parent types.BlockID, txn types.Transaction) (et ExplorerTransaction) { // Get the header information for the transaction. et.ID = txn.ID() et.Height = height et.Parent = parent et.RawTransaction = txn // Add the siacoin outputs that correspond with each siacoin input. for _, sci := range txn.SiacoinInputs { sco, exists := srv.explorer.SiacoinOutput(sci.ParentID) if build.DEBUG && !exists { panic("could not find corresponding siacoin output") } et.SiacoinInputOutputs = append(et.SiacoinInputOutputs, sco) } for i := range txn.SiacoinOutputs { et.SiacoinOutputIDs = append(et.SiacoinOutputIDs, txn.SiacoinOutputID(uint64(i))) } // Add all of the valid and missed proof ids as extra data to the file // contracts. for i, fc := range txn.FileContracts { fcid := txn.FileContractID(uint64(i)) var fcvpoids []types.SiacoinOutputID var fcmpoids []types.SiacoinOutputID for j := range fc.ValidProofOutputs { fcvpoids = append(fcvpoids, fcid.StorageProofOutputID(types.ProofValid, uint64(j))) } for j := range fc.MissedProofOutputs { fcmpoids = append(fcmpoids, fcid.StorageProofOutputID(types.ProofMissed, uint64(j))) } et.FileContractIDs = append(et.FileContractIDs, fcid) et.FileContractValidProofOutputIDs = append(et.FileContractValidProofOutputIDs, fcvpoids) et.FileContractMissedProofOutputIDs = append(et.FileContractMissedProofOutputIDs, fcmpoids) } // Add all of the valid and missed proof ids as extra data to the file // contract revisions. for _, fcr := range txn.FileContractRevisions { var fcrvpoids []types.SiacoinOutputID var fcrmpoids []types.SiacoinOutputID for j := range fcr.NewValidProofOutputs { fcrvpoids = append(fcrvpoids, fcr.ParentID.StorageProofOutputID(types.ProofValid, uint64(j))) } for j := range fcr.NewMissedProofOutputs { fcrmpoids = append(fcrmpoids, fcr.ParentID.StorageProofOutputID(types.ProofMissed, uint64(j))) } et.FileContractValidProofOutputIDs = append(et.FileContractValidProofOutputIDs, fcrvpoids) et.FileContractMissedProofOutputIDs = append(et.FileContractMissedProofOutputIDs, fcrmpoids) } // Add all of the output ids and outputs corresponding with each storage // proof. for _, sp := range txn.StorageProofs { fileContract, fileContractRevisions, fileContractExists, _ := srv.explorer.FileContractHistory(sp.ParentID) if !fileContractExists && build.DEBUG { panic("could not find a file contract connected with a storage proof") } var storageProofOutputs []types.SiacoinOutput if len(fileContractRevisions) > 0 { storageProofOutputs = fileContractRevisions[len(fileContractRevisions)-1].NewValidProofOutputs } else { storageProofOutputs = fileContract.ValidProofOutputs } var storageProofOutputIDs []types.SiacoinOutputID for i := range storageProofOutputs { storageProofOutputIDs = append(storageProofOutputIDs, sp.ParentID.StorageProofOutputID(types.ProofValid, uint64(i))) } et.StorageProofOutputIDs = append(et.StorageProofOutputIDs, storageProofOutputIDs) et.StorageProofOutputs = append(et.StorageProofOutputs, storageProofOutputs) } // Add the siafund outputs that correspond to each siacoin input. for _, sci := range txn.SiafundInputs { sco, exists := srv.explorer.SiafundOutput(sci.ParentID) if build.DEBUG && !exists { panic("could not find corresponding siafund output") } et.SiafundInputOutputs = append(et.SiafundInputOutputs, sco) } for i := range txn.SiafundOutputs { et.SiafundOutputIDs = append(et.SiafundOutputIDs, txn.SiafundOutputID(uint64(i))) } for _, sfi := range txn.SiafundInputs { et.SiaClaimOutputIDs = append(et.SiaClaimOutputIDs, sfi.ParentID.SiaClaimOutputID()) } return et }
// revise revises fc to cover piece and uploads both the revision and the // piece data to the host. func (hu *hostUploader) revise(fc types.FileContract, piece []byte, height types.BlockHeight) error { hu.conn.SetDeadline(time.Now().Add(5 * time.Minute)) // sufficient to transfer 4 MB over 100 kbps defer hu.conn.SetDeadline(time.Time{}) // reset timeout after each revision // calculate new merkle root err := hu.tree.ReadSegments(bytes.NewReader(piece)) if err != nil { return err } // create revision rev := types.FileContractRevision{ ParentID: hu.contract.ID, UnlockConditions: hu.unlockConditions, NewRevisionNumber: fc.RevisionNumber + 1, NewFileSize: fc.FileSize + uint64(len(piece)), NewFileMerkleRoot: hu.tree.Root(), NewWindowStart: fc.WindowStart, NewWindowEnd: fc.WindowEnd, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: fc.UnlockHash, } // transfer value of piece from renter to host safeDuration := uint64(fc.WindowStart - height + 20) // buffer in case host is behind piecePrice := types.NewCurrency64(uint64(len(piece))).Mul(types.NewCurrency64(safeDuration)).Mul(hu.settings.Price) // prevent a negative currency panic if piecePrice.Cmp(fc.ValidProofOutputs[0].Value) > 0 { // probably not enough money, but the host might accept it anyway piecePrice = fc.ValidProofOutputs[0].Value } rev.NewValidProofOutputs[0].Value = rev.NewValidProofOutputs[0].Value.Sub(piecePrice) // less returned to renter rev.NewValidProofOutputs[1].Value = rev.NewValidProofOutputs[1].Value.Add(piecePrice) // more given to host rev.NewMissedProofOutputs[0].Value = rev.NewMissedProofOutputs[0].Value.Sub(piecePrice) // less returned to renter rev.NewMissedProofOutputs[1].Value = rev.NewMissedProofOutputs[1].Value.Add(piecePrice) // more given to void // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(hu.contract.ID), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // renter key is always first -- see negotiateContract }}, } // sign the transaction encodedSig, err := crypto.SignHash(signedTxn.SigHash(0), hu.secretKey) if err != nil { return err } signedTxn.TransactionSignatures[0].Signature = encodedSig[:] // send the transaction if err := encoding.WriteObject(hu.conn, signedTxn); err != nil { return err } // host sends acceptance var response string if err := encoding.ReadObject(hu.conn, &response, 128); err != nil { return err } if response != modules.AcceptResponse { return errors.New("host rejected revision: " + response) } // transfer piece if _, err := hu.conn.Write(piece); err != nil { return err } // read txn signed by host var signedHostTxn types.Transaction if err := encoding.ReadObject(hu.conn, &signedHostTxn, types.BlockSizeLimit); err != nil { return err } if signedHostTxn.ID() != signedTxn.ID() { return errors.New("host sent bad signed transaction") } else if err = signedHostTxn.StandaloneValid(height); err != nil { return err } hu.lastTxn = signedHostTxn return nil }
// addTransaction is called from addBlockDB, and delegates the adding // of information to the database to the functions defined above func (tx *boltTx) addTransaction(txn types.Transaction) { // Store this for quick lookup txid := txn.ID() // Append each input to the list of modifications for _, input := range txn.SiacoinInputs { tx.addAddress(input.UnlockConditions.UnlockHash(), txid) tx.addSiacoinInput(input.ParentID, txid) } // Handle all the transaction outputs for i, output := range txn.SiacoinOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(txn.SiacoinOutputID(uint64(i)), txid) } // Handle each file contract individually for i, contract := range txn.FileContracts { fcid := txn.FileContractID(uint64(i)) tx.addNewHash("FileContracts", hashFilecontract, crypto.Hash(fcid), fcInfo{ Contract: txid, }) for j, output := range contract.ValidProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(fcid.StorageProofOutputID(true, uint64(j)), txid) } for j, output := range contract.MissedProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(fcid.StorageProofOutputID(false, uint64(j)), txid) } tx.addAddress(contract.UnlockHash, txid) } // Update the list of revisions for _, revision := range txn.FileContractRevisions { tx.addFcRevision(revision.ParentID, txid) // Note the old outputs will still be there in the // database. This is to provide information to the // people who may just need it. for i, output := range revision.NewValidProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(revision.ParentID.StorageProofOutputID(true, uint64(i)), txid) } for i, output := range revision.NewMissedProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(revision.ParentID.StorageProofOutputID(false, uint64(i)), txid) } tx.addAddress(revision.NewUnlockHash, txid) } // Update the list of storage proofs for _, proof := range txn.StorageProofs { tx.addFcProof(proof.ParentID, txid) } // Append all the siafund inputs to the modification list for _, input := range txn.SiafundInputs { tx.addSiafundInput(input.ParentID, txid) } // Handle all the siafund outputs for i, output := range txn.SiafundOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewSFOutput(txn.SiafundOutputID(uint64(i)), txid) } tx.putObject("Hashes", txid, hashTransaction) }
// rpcRevise is an RPC that allows a renter to revise a file contract. It will // read new revisions in a loop until the renter sends a termination signal. func (h *Host) rpcRevise(conn net.Conn) error { // read ID of contract to be revised var fcid types.FileContractID if err := encoding.ReadObject(conn, &fcid, crypto.HashSize); err != nil { return err } lockID := h.mu.RLock() obligation, exists := h.obligationsByID[fcid] h.mu.RUnlock(lockID) if !exists { return errors.New("no record of that contract") } // need to protect against two simultaneous revisions to the same // contract; this can cause inconsistency and data loss, making storage // proofs impossible obligation.mu.Lock() defer obligation.mu.Unlock() // open the file in append mode file, err := os.OpenFile(obligation.Path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0660) if err != nil { return err } defer func() { // if a newly-created file was not updated, remove it if stat, _ := file.Stat(); stat.Size() == 0 { os.Remove(obligation.Path) } file.Close() }() // rebuild current Merkle tree tree := crypto.NewTree() buf := make([]byte, crypto.SegmentSize) for { _, err := io.ReadFull(file, buf) if err == io.EOF { break } else if err != nil && err != io.ErrUnexpectedEOF { return err } tree.Push(buf) } // accept new revisions in a loop. The final good transaction will be // submitted to the blockchain. var finalTxn types.Transaction defer func() { h.tpool.AcceptTransactionSet([]types.Transaction{finalTxn}) }() for { // read proposed revision var revTxn types.Transaction if err := encoding.ReadObject(conn, &revTxn, types.BlockSizeLimit); err != nil { return err } // an empty transaction indicates completion if revTxn.ID() == (types.Transaction{}).ID() { break } // check revision against original file contract lockID = h.mu.RLock() err := h.considerRevision(revTxn, obligation) h.mu.RUnlock(lockID) if err != nil { encoding.WriteObject(conn, err.Error()) continue // don't terminate loop; subsequent revisions may be okay } // indicate acceptance if err := encoding.WriteObject(conn, modules.AcceptResponse); err != nil { return err } // read piece // TODO: simultaneously read into tree? rev := revTxn.FileContractRevisions[0] piece := make([]byte, rev.NewFileSize-obligation.FileContract.FileSize) _, err = io.ReadFull(conn, piece) if err != nil { return err } // verify Merkle root r := bytes.NewReader(piece) for { _, err := io.ReadFull(r, buf) if err == io.EOF { break } else if err != nil && err != io.ErrUnexpectedEOF { return err } tree.Push(buf) } if tree.Root() != rev.NewFileMerkleRoot { return errors.New("revision has bad Merkle root") } // manually sign the transaction revTxn.TransactionSignatures = append(revTxn.TransactionSignatures, types.TransactionSignature{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 1, // host key is always second }) encodedSig, err := crypto.SignHash(revTxn.SigHash(1), h.secretKey) if err != nil { return err } revTxn.TransactionSignatures[1].Signature = encodedSig[:] // send the signed transaction if err := encoding.WriteObject(conn, revTxn); err != nil { return err } // append piece to file if _, err := file.Write(piece); err != nil { return err } // save updated obligation to disk lockID = h.mu.Lock() h.spaceRemaining -= int64(len(piece)) obligation.FileContract.RevisionNumber = rev.NewRevisionNumber obligation.FileContract.FileSize = rev.NewFileSize obligation.FileContract.FileMerkleRoot = rev.NewFileMerkleRoot h.obligationsByID[obligation.ID] = obligation heightObligations := h.obligationsByHeight[obligation.FileContract.WindowStart+StorageProofReorgDepth] for i := range heightObligations { if heightObligations[i].ID == obligation.ID { heightObligations[i] = obligation } } h.save() h.mu.Unlock(lockID) finalTxn = revTxn } return nil }
// rpcContract is an RPC that negotiates a file contract. If the // negotiation is successful, the file is downloaded and the host begins // submitting proofs of storage. func (h *Host) rpcContract(conn net.Conn) (err error) { // Read the contract terms. var terms modules.ContractTerms err = encoding.ReadObject(conn, &terms, maxContractLen) if err != nil { return } // Consider the contract terms. If they are unacceptable, return an error // describing why. lockID := h.mu.RLock() err = h.considerTerms(terms) h.mu.RUnlock(lockID) if err != nil { err = encoding.WriteObject(conn, err.Error()) return } // terms are acceptable; allocate space for file lockID = h.mu.Lock() file, path, err := h.allocate(terms.FileSize) h.mu.Unlock(lockID) if err != nil { return } defer file.Close() // rollback everything if something goes wrong defer func() { lockID := h.mu.Lock() defer h.mu.Unlock(lockID) if err != nil { h.deallocate(terms.FileSize, path) } }() // signal that we are ready to download file err = encoding.WriteObject(conn, modules.AcceptTermsResponse) if err != nil { return } // simultaneously download file and calculate its Merkle root. tee := io.TeeReader( // use a LimitedReader to ensure we don't read indefinitely io.LimitReader(conn, int64(terms.FileSize)), // each byte we read from tee will also be written to file file, ) merkleRoot, err := crypto.ReaderMerkleRoot(tee) if err != nil { return } // Data has been sent, read in the unsigned transaction with the file // contract. var unsignedTxn types.Transaction err = encoding.ReadObject(conn, &unsignedTxn, maxContractLen) if err != nil { return } // Verify that the transaction matches the agreed upon terms, and that the // Merkle root in the file contract matches our independently calculated // Merkle root. err = verifyTransaction(unsignedTxn, terms, merkleRoot) if err != nil { err = errors.New("transaction does not satisfy terms: " + err.Error()) return } // Add the collateral to the transaction, but do not sign the transaction. collateralTxn, txnBuilder, err := h.addCollateral(unsignedTxn, terms) if err != nil { return } err = encoding.WriteObject(conn, collateralTxn) if err != nil { return } // Read in the renter-signed transaction and check that it matches the // previously accepted transaction. var signedTxn types.Transaction err = encoding.ReadObject(conn, &signedTxn, maxContractLen) if err != nil { return } if collateralTxn.ID() != signedTxn.ID() { err = errors.New("signed transaction does not match the transaction with collateral") return } // Add the signatures from the renter signed transaction, and then sign the // transaction, then submit the transaction. for _, sig := range signedTxn.TransactionSignatures { txnBuilder.AddTransactionSignature(sig) if err != nil { return } } txnSet, err := txnBuilder.Sign(true) if err != nil { return } err = h.tpool.AcceptTransactionSet(txnSet) if err != nil { return } // Add this contract to the host's list of obligations. fcid := signedTxn.FileContractID(0) fc := signedTxn.FileContracts[0] proofHeight := fc.WindowStart + StorageProofReorgDepth co := contractObligation{ ID: fcid, FileContract: fc, Path: path, } lockID = h.mu.Lock() h.obligationsByHeight[proofHeight] = append(h.obligationsByHeight[proofHeight], co) h.obligationsByID[fcid] = co h.save() h.mu.Unlock(lockID) // Send an ack to the renter that all is well. err = encoding.WriteObject(conn, true) if err != nil { return } // TODO: we don't currently watch the blockchain to make sure that the // transaction actually gets into the blockchain. return }
// rpcRevise is an RPC that allows a renter to revise a file contract. It will // read new revisions in a loop until the renter sends a termination signal. func (h *Host) rpcRevise(conn net.Conn) error { // read ID of contract to be revised var fcid types.FileContractID if err := encoding.ReadObject(conn, &fcid, crypto.HashSize); err != nil { return errors.New("couldn't read contract ID: " + err.Error()) } // remove conn deadline while we wait for lock and rebuild the Merkle tree conn.SetDeadline(time.Time{}) h.mu.RLock() obligation, exists := h.obligationsByID[fcid] h.mu.RUnlock() if !exists { return errors.New("no record of that contract") } // need to protect against two simultaneous revisions to the same // contract; this can cause inconsistency and data loss, making storage // proofs impossible obligation.mu.Lock() defer obligation.mu.Unlock() // open the file in append mode file, err := os.OpenFile(obligation.Path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0660) if err != nil { return err } // rebuild current Merkle tree tree := crypto.NewTree() err = tree.ReadSegments(file) if err != nil { file.Close() return err } // accept new revisions in a loop. The final good transaction will be // submitted to the blockchain. revisionErr := func() error { for { // allow 2 minutes between revisions conn.SetDeadline(time.Now().Add(2 * time.Minute)) // read proposed revision var revTxn types.Transaction if err := encoding.ReadObject(conn, &revTxn, types.BlockSizeLimit); err != nil { return errors.New("couldn't read revision: " + err.Error()) } // an empty transaction indicates completion if revTxn.ID() == (types.Transaction{}).ID() { return nil } // allow 5 minutes for each revision conn.SetDeadline(time.Now().Add(5 * time.Minute)) // check revision against original file contract h.mu.RLock() err := h.considerRevision(revTxn, *obligation) h.mu.RUnlock() if err != nil { encoding.WriteObject(conn, err.Error()) continue // don't terminate loop; subsequent revisions may be okay } // indicate acceptance if err := encoding.WriteObject(conn, modules.AcceptResponse); err != nil { return errors.New("couldn't write acceptance: " + err.Error()) } // read piece // TODO: simultaneously read into tree and file rev := revTxn.FileContractRevisions[0] last := obligation.LastRevisionTxn.FileContractRevisions[0] piece := make([]byte, rev.NewFileSize-last.NewFileSize) _, err = io.ReadFull(conn, piece) if err != nil { return errors.New("couldn't read piece data: " + err.Error()) } // verify Merkle root err = tree.ReadSegments(bytes.NewReader(piece)) if err != nil { return errors.New("couldn't verify Merkle root: " + err.Error()) } if tree.Root() != rev.NewFileMerkleRoot { return errors.New("revision has bad Merkle root") } // manually sign the transaction revTxn.TransactionSignatures = append(revTxn.TransactionSignatures, types.TransactionSignature{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 1, // host key is always second }) encodedSig, err := crypto.SignHash(revTxn.SigHash(1), h.secretKey) if err != nil { return err } revTxn.TransactionSignatures[1].Signature = encodedSig[:] // send the signed transaction if err := encoding.WriteObject(conn, revTxn); err != nil { return errors.New("couldn't write signed revision transaction: " + err.Error()) } // append piece to file if _, err := file.Write(piece); err != nil { return errors.New("couldn't write new data to file: " + err.Error()) } // save updated obligation to disk h.mu.Lock() obligation.LastRevisionTxn = revTxn h.spaceRemaining -= int64(len(piece)) h.save() h.mu.Unlock() } }() file.Close() // if a newly-created file was not updated, remove it if obligation.LastRevisionTxn.FileContractRevisions[0].NewRevisionNumber == 0 { os.Remove(obligation.Path) return revisionErr } err = h.tpool.AcceptTransactionSet([]types.Transaction{obligation.LastRevisionTxn}) if err != nil { h.log.Println("WARN: transaction pool rejected revision transaction: " + err.Error()) } return revisionErr }