// applyFileContracts iterates through all of the file contracts in a // transaction and applies them to the state, updating the diffs in the block // node. func (cs *State) applyFileContracts(bn *blockNode, t types.Transaction) { for i, fc := range t.FileContracts { // Sanity check - the file contract should not exists within the state. fcid := t.FileContractID(i) if build.DEBUG { _, exists := cs.fileContracts[fcid] if exists { panic(ErrMisuseApplyFileContracts) } } fcd := modules.FileContractDiff{ Direction: modules.DiffApply, ID: fcid, FileContract: fc, } bn.fileContractDiffs = append(bn.fileContractDiffs, fcd) cs.commitFileContractDiff(fcd, modules.DiffApply) // Get the portion of the contract that goes into the siafund pool and // add it to the siafund pool. sfpd := modules.SiafundPoolDiff{ Previous: cs.siafundPool, Adjusted: cs.siafundPool.Add(fc.Tax()), } bn.siafundPoolDiffs = append(bn.siafundPoolDiffs, sfpd) cs.commitSiafundPoolDiff(sfpd, modules.DiffApply) } return }
// removeFileContracts removes all of the file contracts of a transaction from // the unconfirmed consensus set. func (tp *TransactionPool) removeFileContracts(t types.Transaction) { for i, _ := range t.FileContracts { fcid := t.FileContractID(i) // Sanity check - file contract should be in the unconfirmed set as // there should be no dependent transactions who have terminated the // contract. if build.DEBUG { _, exists := tp.fileContracts[fcid] if !exists { panic("trying to remove missing file contract") } } delete(tp.fileContracts, fcid) } }
// TestDuplicateStorageProof applies a storage proof which has already been // applied. func TestDuplicateStorageProof(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestDuplicateStorageProof") if err != nil { t.Fatal(err) } // Create a block node. bn := new(blockNode) bn.height = cst.cs.height() // Create a file contract for the storage proof to prove. txn0 := types.Transaction{ FileContracts: []types.FileContract{ { Payout: types.NewCurrency64(300e3), ValidProofOutputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(290e3)}, }, }, }, } cst.cs.applyFileContracts(bn, txn0) fcid := txn0.FileContractID(0) // Apply a single storage proof. txn1 := types.Transaction{ StorageProofs: []types.StorageProof{{ParentID: fcid}}, } cst.cs.applyStorageProofs(bn, txn1) // Trigger a panic by applying the storage proof again. defer func() { r := recover() if r != ErrDuplicateValidProofOutput { t.Error("failed to trigger ErrDuplicateValidProofOutput:", r) } }() cst.cs.applyFileContracts(bn, txn0) // File contract was consumed by the first proof. cst.cs.applyStorageProofs(bn, txn1) }
// applyFileContracts iterates through all of the file contracts in a // transaction and applies them to the state, updating the diffs in the proccesed // block. func applyFileContracts(tx *bolt.Tx, pb *processedBlock, t types.Transaction) { for i, fc := range t.FileContracts { fcid := t.FileContractID(uint64(i)) fcd := modules.FileContractDiff{ Direction: modules.DiffApply, ID: fcid, FileContract: fc, } pb.FileContractDiffs = append(pb.FileContractDiffs, fcd) commitFileContractDiff(tx, fcd, modules.DiffApply) // Get the portion of the contract that goes into the siafund pool and // add it to the siafund pool. sfp := getSiafundPool(tx) sfpd := modules.SiafundPoolDiff{ Direction: modules.DiffApply, Previous: sfp, Adjusted: sfp.Add(types.Tax(blockHeight(tx), fc.Payout)), } pb.SiafundPoolDiffs = append(pb.SiafundPoolDiffs, sfpd) commitSiafundPoolDiff(tx, sfpd, modules.DiffApply) } }
// applyFileContracts iterates through all of the file contracts in a // transaction and applies them to the state, updating the diffs in the proccesed // block. func (cs *ConsensusSet) applyFileContracts(tx *bolt.Tx, pb *processedBlock, t types.Transaction) error { for i, fc := range t.FileContracts { fcid := t.FileContractID(i) fcd := modules.FileContractDiff{ Direction: modules.DiffApply, ID: fcid, FileContract: fc, } pb.FileContractDiffs = append(pb.FileContractDiffs, fcd) cs.commitTxFileContractDiff(tx, fcd, modules.DiffApply) // Get the portion of the contract that goes into the siafund pool and // add it to the siafund pool. sfp := getSiafundPool(tx) sfpd := modules.SiafundPoolDiff{ Direction: modules.DiffApply, Previous: sfp, Adjusted: sfp.Add(fc.Tax()), } pb.SiafundPoolDiffs = append(pb.SiafundPoolDiffs, sfpd) cs.commitTxSiafundPoolDiff(tx, sfpd, modules.DiffApply) } return nil }
// 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 }
// 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) }
// TestApplyStorageProofs probes the applyStorageProofs method of the consensus // set. func TestApplyStorageProofs(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplyStorageProofs") if err != nil { t.Fatal(err) } // Create a block node to use with application. bn := new(blockNode) bn.height = cst.cs.height() // Apply a transaction with two file contracts - there is a reason to // create a storage proof. txn := types.Transaction{ FileContracts: []types.FileContract{ { Payout: types.NewCurrency64(300e3), ValidProofOutputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(290e3)}, }, }, {}, { Payout: types.NewCurrency64(600e3), ValidProofOutputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(280e3)}, {Value: types.NewCurrency64(300e3)}, }, }, }, } cst.cs.applyFileContracts(bn, txn) fcid0 := txn.FileContractID(0) fcid1 := txn.FileContractID(1) fcid2 := txn.FileContractID(2) // Apply a single storage proof. txn = types.Transaction{ StorageProofs: []types.StorageProof{{ParentID: fcid0}}, } cst.cs.applyStorageProofs(bn, txn) _, exists := cst.cs.fileContracts[fcid0] if exists { t.Error("Storage proof did not disable a file contract.") } if len(cst.cs.fileContracts) != 2 { t.Error("file contracts not correctly updated") } if len(bn.fileContractDiffs) != 4 { // 3 creating the initial contracts, 1 for the storage proof. t.Error("block node was not updated for single element transaction") } if bn.fileContractDiffs[3].Direction != modules.DiffRevert { t.Error("wrong diff direction applied when revising a file contract") } if bn.fileContractDiffs[3].ID != fcid0 { t.Error("wrong id used when revising a file contract") } spoid0 := fcid0.StorageProofOutputID(types.ProofValid, 0) sco, exists := cst.cs.delayedSiacoinOutputs[bn.height+types.MaturityDelay][spoid0] if !exists { t.Error("storage proof output not created after applying a storage proof") } if sco.Value.Cmp(types.NewCurrency64(290e3)) != 0 { t.Error("storage proof output was created with the wrong value") } // Apply a transaction with 2 storage proofs. txn = types.Transaction{ StorageProofs: []types.StorageProof{ {ParentID: fcid1}, {ParentID: fcid2}, }, } cst.cs.applyStorageProofs(bn, txn) _, exists = cst.cs.fileContracts[fcid1] if exists { t.Error("Storage proof failed to consume file contract.") } _, exists = cst.cs.fileContracts[fcid2] if exists { t.Error("storage proof did not consume file contract") } if len(cst.cs.fileContracts) != 0 { t.Error("file contracts not correctly updated") } if len(bn.fileContractDiffs) != 6 { t.Error("block node was not updated correctly") } spoid1 := fcid1.StorageProofOutputID(types.ProofValid, 0) _, exists = cst.cs.siacoinOutputs[spoid1] if exists { t.Error("output created when file contract had no corresponding output") } spoid2 := fcid2.StorageProofOutputID(types.ProofValid, 0) sco, exists = cst.cs.delayedSiacoinOutputs[bn.height+types.MaturityDelay][spoid2] if !exists { t.Error("no output created by first output of file contract") } if sco.Value.Cmp(types.NewCurrency64(280e3)) != 0 { t.Error("first siacoin output created has wrong value") } spoid3 := fcid2.StorageProofOutputID(types.ProofValid, 1) sco, exists = cst.cs.delayedSiacoinOutputs[bn.height+types.MaturityDelay][spoid3] if !exists { t.Error("second output not created for storage proof") } if sco.Value.Cmp(types.NewCurrency64(300e3)) != 0 { t.Error("second siacoin output has wrong value") } if cst.cs.siafundPool.Cmp(types.NewCurrency64(30e3)) != 0 { t.Error("siafund pool not being added up correctly") } }
// TestApplyFileContractRevisions probes the applyFileContractRevisions method // of the consensus set. func TestApplyFileContractRevisions(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplyFileContractRevisions") if err != nil { t.Fatal(err) } // Create a block node to use with application. bn := new(blockNode) // Apply a transaction with two file contracts - that way there is // something to revise. txn := types.Transaction{ FileContracts: []types.FileContract{ {}, {Payout: types.NewCurrency64(1)}, }, } cst.cs.applyFileContracts(bn, txn) fcid0 := txn.FileContractID(0) fcid1 := txn.FileContractID(1) // Apply a single file contract revision. txn = types.Transaction{ FileContractRevisions: []types.FileContractRevision{ { ParentID: fcid0, NewFileSize: 1, }, }, } cst.cs.applyFileContractRevisions(bn, txn) fc, exists := cst.cs.fileContracts[fcid0] if !exists { t.Error("Revision killed a file contract") } if fc.FileSize != 1 { t.Error("file contract filesize not properly updated") } if len(cst.cs.fileContracts) != 2 { t.Error("file contracts not correctly updated") } if len(bn.fileContractDiffs) != 4 { // 2 creating the initial contracts, 1 to remove the old, 1 to add the revision. t.Error("block node was not updated for single element transaction") } if bn.fileContractDiffs[2].Direction != modules.DiffRevert { t.Error("wrong diff direction applied when revising a file contract") } if bn.fileContractDiffs[3].Direction != modules.DiffApply { t.Error("wrong diff direction applied when revising a file contract") } if bn.fileContractDiffs[2].ID != fcid0 { t.Error("wrong id used when revising a file contract") } if bn.fileContractDiffs[3].ID != fcid0 { t.Error("wrong id used when revising a file contract") } // Apply a transaction with 2 file contract revisions. txn = types.Transaction{ FileContractRevisions: []types.FileContractRevision{ { ParentID: fcid0, NewFileSize: 2, }, { ParentID: fcid1, NewFileSize: 3, }, }, } cst.cs.applyFileContractRevisions(bn, txn) fc0, exists := cst.cs.fileContracts[fcid0] if !exists { t.Error("Revision ate file contract") } fc1, exists := cst.cs.fileContracts[fcid1] if !exists { t.Error("Revision ate file contract") } if fc0.FileSize != 2 { t.Error("Revision not correctly applied") } if fc1.FileSize != 3 { t.Error("Revision not correctly applied") } if len(cst.cs.fileContracts) != 2 { t.Error("file contracts not correctly updated") } if len(bn.fileContractDiffs) != 8 { t.Error("block node was not updated correctly") } }
// TestApplyFileContracts probes the applyFileContracts method of the // consensus set. func TestApplyFileContracts(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplyFileContracts") if err != nil { t.Fatal(err) } // Create a block node to use with application. bn := new(blockNode) // Apply a transaction with a single file contract. txn := types.Transaction{ FileContracts: []types.FileContract{{}}, } cst.cs.applyFileContracts(bn, txn) fcid := txn.FileContractID(0) _, exists := cst.cs.fileContracts[fcid] if !exists { t.Error("Failed to create file contract") } if len(cst.cs.fileContracts) != 1 { t.Error("file contracts not correctly updated") } if len(bn.fileContractDiffs) != 1 { t.Error("block node was not updated for single element transaction") } if bn.fileContractDiffs[0].Direction != modules.DiffApply { t.Error("wrong diff direction applied when creating a file contract") } if bn.fileContractDiffs[0].ID != fcid { t.Error("wrong id used when creating a file contract") } // Apply a transaction with 2 file contracts. txn = types.Transaction{ FileContracts: []types.FileContract{ {Payout: types.NewCurrency64(1)}, {Payout: types.NewCurrency64(300e3)}, }, } cst.cs.applyFileContracts(bn, txn) fcid0 := txn.FileContractID(0) fcid1 := txn.FileContractID(1) _, exists = cst.cs.fileContracts[fcid0] if !exists { t.Error("Failed to create file contract") } _, exists = cst.cs.fileContracts[fcid1] if !exists { t.Error("Failed to create file contract") } if len(cst.cs.fileContracts) != 3 { t.Error("file contracts not correctly updated") } if len(bn.fileContractDiffs) != 3 { t.Error("block node was not updated correctly") } if cst.cs.siafundPool.Cmp(types.NewCurrency64(10e3)) != 0 { t.Error("siafund pool did not update correctly upon creation of a file contract") } }
// TestUploadConstraints checks that file contract negotiation correctly // rejects contracts that don't meet required criteria. func TestUploadConstraints(t *testing.T) { if testing.Short() { t.SkipNow() } ht, err := newHostTester("TestUploadConstraints") if err != nil { t.Fatal(err) } h := ht.host settings := h.Settings() settings.TotalStorage = 10e3 err = h.SetSettings(settings) if err != nil { t.Fatal(err) } // Create a valid file contract transaction. filesize := uint64(5e3) merkleRoot := crypto.Hash{51, 23} windowStart := ht.cs.Height() + 1 + settings.MinDuration windowEnd := ht.cs.Height() + 1 + settings.MinDuration + 1 + settings.WindowSize currencyDuration := types.NewCurrency64(1 + uint64(settings.MinDuration)) payment := types.NewCurrency64(filesize).Mul(settings.Price).Mul(currencyDuration) payout := payment.Mul(types.NewCurrency64(20)) refund := types.PostTax(ht.cs.Height(), payout).Sub(payment) renterKey := types.SiaPublicKey{} txn := types.Transaction{ FileContracts: []types.FileContract{{ FileSize: filesize, FileMerkleRoot: merkleRoot, WindowStart: windowStart, WindowEnd: windowEnd, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{ { Value: refund, UnlockHash: types.UnlockHash{}, }, { Value: payment, UnlockHash: settings.UnlockHash, }, }, MissedProofOutputs: []types.SiacoinOutput{ { Value: refund, UnlockHash: types.UnlockHash{}, }, { Value: payment, UnlockHash: types.UnlockHash{}, }, }, UnlockHash: types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterKey, h.publicKey}, SignaturesRequired: 2, }.UnlockHash(), RevisionNumber: 3, }}, } err = h.considerContract(txn, renterKey, filesize, merkleRoot) if err != nil { t.Fatal(err) } // Test that under-paid file contracts get rejected. underPayment := types.NewCurrency64(filesize * 5 / 6).Mul(settings.Price).Mul(currencyDuration) underRefund := types.PostTax(ht.cs.Height(), payout).Sub(underPayment) txn.FileContracts[0].ValidProofOutputs[0].Value = underRefund txn.FileContracts[0].ValidProofOutputs[1].Value = underPayment txn.FileContracts[0].MissedProofOutputs[0].Value = underRefund txn.FileContracts[0].MissedProofOutputs[1].Value = underPayment err = h.considerContract(txn, renterKey, filesize, merkleRoot) if err != ErrLowPayment { t.Fatal(err) } // Test that too-large files get rejected. largeFilesize := uint64(10001) largeFilePayment := types.NewCurrency64(largeFilesize).Mul(settings.Price).Mul(currencyDuration) largeFileRefund := types.PostTax(ht.cs.Height(), payout).Sub(largeFilePayment) txn.FileContracts[0].FileSize = largeFilesize txn.FileContracts[0].ValidProofOutputs[0].Value = largeFileRefund txn.FileContracts[0].ValidProofOutputs[1].Value = largeFilePayment txn.FileContracts[0].MissedProofOutputs[0].Value = largeFileRefund txn.FileContracts[0].MissedProofOutputs[1].Value = largeFilePayment err = h.considerContract(txn, renterKey, largeFilesize, merkleRoot) if err != ErrHostCapacity { t.Fatal(err) } // Reset the file contract to a working contract, and create an obligation // from the transaction. txn.FileContracts[0].FileSize = filesize txn.FileContracts[0].ValidProofOutputs[0].Value = refund txn.FileContracts[0].ValidProofOutputs[1].Value = payment txn.FileContracts[0].MissedProofOutputs[0].Value = refund txn.FileContracts[0].MissedProofOutputs[1].Value = payment obligation := &contractObligation{ ID: txn.FileContractID(0), OriginTransaction: txn, } // Create a legal revision transaction. newFileSize := filesize + uint64(4e3) revisedPayment := payment.Add(types.NewCurrency64(newFileSize - filesize).Mul(currencyDuration).Mul(settings.Price)) revisedRefund := types.PostTax(ht.cs.Height(), payout).Sub(revisedPayment) revisionTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{{ ParentID: txn.FileContractID(0), UnlockConditions: types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterKey, h.publicKey}, SignaturesRequired: 2, }, NewRevisionNumber: txn.FileContracts[0].RevisionNumber + 1, NewFileSize: newFileSize, NewFileMerkleRoot: merkleRoot, NewWindowStart: windowStart, NewWindowEnd: windowEnd, NewValidProofOutputs: []types.SiacoinOutput{ { Value: revisedRefund, UnlockHash: types.UnlockHash{}, }, { Value: revisedPayment, UnlockHash: settings.UnlockHash, }, }, NewMissedProofOutputs: []types.SiacoinOutput{ { Value: revisedRefund, UnlockHash: types.UnlockHash{}, }, { Value: revisedPayment, UnlockHash: types.UnlockHash{}, }, }, NewUnlockHash: txn.FileContracts[0].UnlockHash, }}, } err = ht.host.considerRevision(revisionTxn, obligation) if err != nil { t.Fatal(err) } // Test that too large revisions get rejected. settings.TotalStorage = 3e3 ht.host.SetSettings(settings) if ht.host.spaceRemaining != 3e3 { t.Fatal("host is not getting the correct space remaining") } err = ht.host.considerRevision(revisionTxn, obligation) if err != ErrHostCapacity { t.Fatal(err) } // Test that file revisions get accepted if the updated file size is too // large but just the added data is small enough (regression test). settings.TotalStorage = 8e3 ht.host.SetSettings(settings) err = ht.host.considerRevision(revisionTxn, obligation) if err != nil { t.Fatal(err) } // Test that underpaid revisions get rejected. revisedUnderPayment := payment.Add(types.NewCurrency64(newFileSize - filesize - 1e3).Mul(currencyDuration).Mul(settings.Price)) revisedUnderRefund := types.PostTax(ht.cs.Height(), payout).Sub(revisedUnderPayment) revisionTxn.FileContractRevisions[0].NewValidProofOutputs[0].Value = revisedUnderRefund revisionTxn.FileContractRevisions[0].NewValidProofOutputs[1].Value = revisedUnderPayment revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[0].Value = revisedUnderRefund revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[1].Value = revisedUnderPayment revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[1].Value = revisedUnderPayment revisionTxn.FileContractRevisions[0].NewMissedProofOutputs[1].Value = revisedUnderPayment err = ht.host.considerRevision(revisionTxn, obligation) if err != ErrLowPayment { t.Fatal(err) } }
// 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 }