// negotiateRevision sends a revision and actions to the host for approval, // completing one iteration of the revision loop. func negotiateRevision(conn net.Conn, rev types.FileContractRevision, secretKey crypto.SecretKey) (types.Transaction, error) { // 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 formContract }}, } // sign the transaction encodedSig, _ := crypto.SignHash(signedTxn.SigHash(0), secretKey) // no error possible signedTxn.TransactionSignatures[0].Signature = encodedSig[:] // send the revision if err := encoding.WriteObject(conn, rev); err != nil { return types.Transaction{}, errors.New("couldn't send revision: " + err.Error()) } // read acceptance if err := modules.ReadNegotiationAcceptance(conn); err != nil { return types.Transaction{}, errors.New("host did not accept revision: " + err.Error()) } // send the new transaction signature if err := encoding.WriteObject(conn, signedTxn.TransactionSignatures[0]); err != nil { return types.Transaction{}, errors.New("couldn't send transaction signature: " + err.Error()) } // read the host's acceptance and transaction signature // NOTE: if the host sends ErrStopResponse, we should continue processing // the revision, but return the error anyway. responseErr := modules.ReadNegotiationAcceptance(conn) if responseErr != nil && responseErr != modules.ErrStopResponse { return types.Transaction{}, errors.New("host did not accept transaction signature: " + responseErr.Error()) } var hostSig types.TransactionSignature if err := encoding.ReadObject(conn, &hostSig, 16e3); err != nil { return types.Transaction{}, errors.New("couldn't read host's signature: " + err.Error()) } // add the signature to the transaction and verify it // NOTE: we can fake the blockheight here because it doesn't affect // verification; it just needs to be above the fork height and below the // contract expiration (which was checked earlier). verificationHeight := rev.NewWindowStart - 1 signedTxn.TransactionSignatures = append(signedTxn.TransactionSignatures, hostSig) if err := signedTxn.StandaloneValid(verificationHeight); err != nil { return types.Transaction{}, err } // if the host sent ErrStopResponse, return it return signedTxn, responseErr }
// verifyKeysSiag_1_0 is a copy-pasted version of the verifyKeys method // from siag 1.0. func verifyKeysSiag_1_0(uc types.UnlockConditions, folder string, keyname string) error { keysRequired := uc.SignaturesRequired totalKeys := uint64(len(uc.PublicKeys)) loadedKeys := make([]KeyPairSiag_1_0, totalKeys) for i := 0; i < len(loadedKeys); i++ { err := encoding.ReadFile(filepath.Join(folder, keyname+"_Key"+strconv.Itoa(i)+".siakey"), &loadedKeys[i]) if err != nil { return err } } for _, loadedKey := range loadedKeys { if loadedKey.UnlockConditions.UnlockHash() != uc.UnlockHash() { return errors.New("ErrCorruptedKey") } } txn := types.Transaction{ SiafundInputs: []types.SiafundInput{ types.SiafundInput{ UnlockConditions: loadedKeys[0].UnlockConditions, }, }, } var i uint64 for i != totalKeys { if i+keysRequired > totalKeys { i = totalKeys - keysRequired } var j uint64 for j < keysRequired { txn.TransactionSignatures = append(txn.TransactionSignatures, types.TransactionSignature{ PublicKeyIndex: i, CoveredFields: types.CoveredFields{WholeTransaction: true}, }) sigHash := txn.SigHash(int(j)) sig, err := crypto.SignHash(sigHash, loadedKeys[i].SecretKey) if err != nil { return err } txn.TransactionSignatures[j].Signature = sig[:] i++ j++ } err := txn.StandaloneValid(0) if err != nil { return err } txn.TransactionSignatures = nil } return nil }
// 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 }
// BenchmarkStandaloneValid times how long it takes to verify a single // large transaction, with a certain number of signatures func BenchmarkStandaloneValid(b *testing.B) { numSigs := 7 // make a transaction numSigs with valid inputs with valid signatures b.ReportAllocs() txn := Transaction{} sk := make([]crypto.SecretKey, numSigs) pk := make([]crypto.PublicKey, numSigs) for i := 0; i < numSigs; i++ { s, p, err := crypto.GenerateKeyPair() if err != nil { b.Fatal(err) } sk[i] = s pk[i] = p uc := UnlockConditions{ PublicKeys: []SiaPublicKey{ {Algorithm: SignatureEd25519, Key: pk[i][:]}, }, SignaturesRequired: 1, } txn.SiacoinInputs = append(txn.SiacoinInputs, SiacoinInput{ UnlockConditions: uc, }) copy(txn.SiacoinInputs[i].ParentID[:], encoding.Marshal(i)) txn.TransactionSignatures = append(txn.TransactionSignatures, TransactionSignature{ CoveredFields: CoveredFields{WholeTransaction: true}, }) copy(txn.TransactionSignatures[i].ParentID[:], encoding.Marshal(i)) } // Transaction must be constructed before signing for i := 0; i < numSigs; i++ { sigHash := txn.SigHash(i) sig0, err := crypto.SignHash(sigHash, sk[i]) if err != nil { b.Fatal(err) } txn.TransactionSignatures[i].Signature = sig0[:] } b.ResetTimer() for i := 0; i < b.N; i++ { err := txn.StandaloneValid(10) if err != nil { b.Fatal(err) } } }
// addSignatures will sign a transaction using a spendable key, with support // for multisig spendable keys. Because of the restricted input, the function // is compatible with both siacoin inputs and siafund inputs. func addSignatures(txn *types.Transaction, cf types.CoveredFields, uc types.UnlockConditions, parentID crypto.Hash, spendKey spendableKey) (newSigIndices []int, err error) { // Try to find the matching secret key for each public key - some public // keys may not have a match. Some secret keys may be used multiple times, // which is why public keys are used as the outer loop. totalSignatures := uint64(0) for i, siaPubKey := range uc.PublicKeys { // Search for the matching secret key to the public key. for j := range spendKey.SecretKeys { pubKey := spendKey.SecretKeys[j].PublicKey() if bytes.Compare(siaPubKey.Key, pubKey[:]) != 0 { continue } // Found the right secret key, add a signature. sig := types.TransactionSignature{ ParentID: parentID, CoveredFields: cf, PublicKeyIndex: uint64(i), } newSigIndices = append(newSigIndices, len(txn.TransactionSignatures)) txn.TransactionSignatures = append(txn.TransactionSignatures, sig) sigIndex := len(txn.TransactionSignatures) - 1 sigHash := txn.SigHash(sigIndex) encodedSig, err := crypto.SignHash(sigHash, spendKey.SecretKeys[j]) if err != nil { return nil, err } txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] // Count that the signature has been added, and break out of the // secret key loop. totalSignatures++ break } // If there are enough signatures to satisfy the unlock conditions, // break out of the outer loop. if totalSignatures == uc.SignaturesRequired { break } } return newSigIndices, nil }
// verifyRecentRevision confirms that the host and contractor agree upon the current // state of the contract being revised. func verifyRecentRevision(conn net.Conn, contract modules.RenterContract) error { // send contract ID if err := encoding.WriteObject(conn, contract.ID); err != nil { return errors.New("couldn't send contract ID: " + err.Error()) } // read challenge var challenge crypto.Hash if err := encoding.ReadObject(conn, &challenge, 32); err != nil { return errors.New("couldn't read challenge: " + err.Error()) } // sign and return sig, err := crypto.SignHash(challenge, contract.SecretKey) if err != nil { return err } else if err := encoding.WriteObject(conn, sig); err != nil { return errors.New("couldn't send challenge response: " + err.Error()) } // read acceptance if err := modules.ReadNegotiationAcceptance(conn); err != nil { return errors.New("host did not accept revision request: " + err.Error()) } // read last revision and signatures var lastRevision types.FileContractRevision var hostSignatures []types.TransactionSignature if err := encoding.ReadObject(conn, &lastRevision, 2048); err != nil { return errors.New("couldn't read last revision: " + err.Error()) } if err := encoding.ReadObject(conn, &hostSignatures, 2048); err != nil { return errors.New("couldn't read host signatures: " + err.Error()) } // Check that the unlock hashes match; if they do not, something is // seriously wrong. Otherwise, check that the revision numbers match. if lastRevision.UnlockConditions.UnlockHash() != contract.LastRevision.UnlockConditions.UnlockHash() { return errors.New("unlock conditions do not match") } else if lastRevision.NewRevisionNumber != contract.LastRevision.NewRevisionNumber { return &recentRevisionError{contract.LastRevision.NewRevisionNumber, lastRevision.NewRevisionNumber} } // NOTE: we can fake the blockheight here because it doesn't affect // verification; it just needs to be above the fork height and below the // contract expiration (which was checked earlier). return modules.VerifyFileContractRevisionTransactionSignatures(lastRevision, hostSignatures, contract.FileContract.WindowStart-1) }
// CreateAnnouncement will take a host announcement and encode it, returning // the exact []byte that should be added to the arbitrary data of a // transaction. func CreateAnnouncement(addr NetAddress, pk types.SiaPublicKey, sk crypto.SecretKey) (signedAnnouncement []byte, err error) { if err := addr.IsValid(); err != nil { return nil, err } // Create the HostAnnouncement and marshal it. annBytes := encoding.Marshal(HostAnnouncement{ Specifier: PrefixHostAnnouncement, NetAddress: addr, PublicKey: pk, }) // Create a signature for the announcement. annHash := crypto.HashBytes(annBytes) sig, err := crypto.SignHash(annHash, sk) if err != nil { return nil, err } // Return the signed announcement. return append(annBytes, sig[:]...), nil }
// addSignatures will sign a transaction using a spendable key, with support // for multisig spendable keys. Because of the restricted input, the function // is compatible with both siacoin inputs and siafund inputs. func addSignatures(txn *types.Transaction, cf types.CoveredFields, uc types.UnlockConditions, parentID crypto.Hash, key spendableKey) error { usedIndices := make(map[int]struct{}) for i := range key.secretKeys { found := false keyIndex := 0 pubKey := key.secretKeys[i].PublicKey() for i, siaPublicKey := range uc.PublicKeys { _, exists := usedIndices[i] if !exists && bytes.Compare(pubKey[:], siaPublicKey.Key) == 0 { found = true keyIndex = i break } } if !found && build.DEBUG { panic("transaction builder cannot sign an input that it added") } usedIndices[keyIndex] = struct{}{} // Create the unsigned transaction signature. sig := types.TransactionSignature{ ParentID: parentID, CoveredFields: cf, PublicKeyIndex: uint64(keyIndex), } txn.TransactionSignatures = append(txn.TransactionSignatures, sig) // Get the signature. sigIndex := len(txn.TransactionSignatures) - 1 sigHash := txn.SigHash(sigIndex) encodedSig, err := crypto.SignHash(sigHash, key.secretKeys[i]) if err != nil { return err } txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] } return nil }
// createRevisionSignature creates a signature for a file contract revision // that signs on the file contract revision. The renter should have already // provided the signature. createRevisionSignature will check to make sure that // the renter's signature is valid. func createRevisionSignature(fcr types.FileContractRevision, renterSig types.TransactionSignature, secretKey crypto.SecretKey, blockHeight types.BlockHeight) (types.Transaction, error) { hostSig := types.TransactionSignature{ ParentID: crypto.Hash(fcr.ParentID), PublicKeyIndex: 1, CoveredFields: types.CoveredFields{ FileContractRevisions: []uint64{0}, }, } txn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{fcr}, TransactionSignatures: []types.TransactionSignature{renterSig, hostSig}, } sigHash := txn.SigHash(1) encodedSig, err := crypto.SignHash(sigHash, secretKey) if err != nil { return types.Transaction{}, err } txn.TransactionSignatures[1].Signature = encodedSig[:] err = modules.VerifyFileContractRevisionTransactionSignatures(fcr, txn.TransactionSignatures, blockHeight) if err != nil { return types.Transaction{}, err } return txn, nil }
func TestReviseContract(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() ct, err := newContractorTester("TestReviseContract") if err != nil { t.Fatal(err) } // get an address ourAddr, err := ct.wallet.NextAddress() if err != nil { t.Fatal(err) } // generate keys sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } renterPubKey := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterPubKey, renterPubKey}, SignaturesRequired: 1, } // create file contract payout := types.NewCurrency64(1e16) fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: 100, WindowEnd: 1000, Payout: payout, UnlockHash: uc.UnlockHash(), RevisionNumber: 0, } // outputs need account for tax fc.ValidProofOutputs = []types.SiacoinOutput{ {Value: types.PostTax(ct.contractor.blockHeight, payout), UnlockHash: ourAddr.UnlockHash()}, {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, // no collateral } fc.MissedProofOutputs = []types.SiacoinOutput{ // same as above fc.ValidProofOutputs[0], // goes to the void, not the hostdb {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, } txnBuilder := ct.wallet.StartTransaction() err = txnBuilder.FundSiacoins(fc.Payout) if err != nil { t.Fatal(err) } txnBuilder.AddFileContract(fc) signedTxnSet, err := txnBuilder.Sign(true) if err != nil { t.Fatal(err) } // submit contract err = ct.tpool.AcceptTransactionSet(signedTxnSet) if err != nil { t.Fatal(err) } // create revision fcid := signedTxnSet[len(signedTxnSet)-1].FileContractID(0) rev := types.FileContractRevision{ ParentID: fcid, UnlockConditions: uc, NewFileSize: 10, NewWindowStart: 100, NewWindowEnd: 1000, NewRevisionNumber: 1, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, } // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // hostdb key is always first -- see negotiateContract }}, } // sign the transaction encodedSig, err := crypto.SignHash(signedTxn.SigHash(0), sk) if err != nil { t.Fatal(err) } signedTxn.TransactionSignatures[0].Signature = encodedSig[:] err = signedTxn.StandaloneValid(ct.contractor.blockHeight) if err != nil { t.Fatal(err) } // submit revision err = ct.tpool.AcceptTransactionSet([]types.Transaction{signedTxn}) if err != nil { t.Fatal(err) } }
// 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 }
// SendSiagSiafunds sends siafunds to another address. The siacoins stored in // the siafunds are sent to an address in the wallet. func (w *Wallet) SendSiagSiafunds(amount types.Currency, dest types.UnlockHash, keyfiles []string) (types.Transaction, error) { if len(keyfiles) < 1 { return types.Transaction{}, ErrNoKeyfile } // Load the siafund keys and verify they are sufficient to sign the // transaction. skps := make([]SiagKeyPair, len(keyfiles)) for i, keyfile := range keyfiles { err := encoding.ReadFile(keyfile, &skps[i]) if err != nil { return types.Transaction{}, err } if skps[i].Header != SiagFileHeader { return types.Transaction{}, ErrUnknownHeader } if skps[i].Version != SiagFileVersion { return types.Transaction{}, ErrUnknownVersion } } // Check that all of the loaded files have the same address, and that there // are enough to create the transaction. baseUnlockHash := skps[0].UnlockConditions.UnlockHash() for _, skp := range skps { if skp.UnlockConditions.UnlockHash() != baseUnlockHash { return types.Transaction{}, ErrInconsistentKeys } } if uint64(len(skps)) < skps[0].UnlockConditions.SignaturesRequired { return types.Transaction{}, ErrInsufficientKeys } // Check that there are enough siafunds in the key to complete the spend. lockID := w.mu.RLock() var availableSiafunds types.Currency var sfoids []types.SiafundOutputID for sfoid, sfo := range w.siafundOutputs { if sfo.UnlockHash == baseUnlockHash { availableSiafunds = availableSiafunds.Add(sfo.Value) sfoids = append(sfoids, sfoid) } if availableSiafunds.Cmp(amount) >= 0 { break } } w.mu.RUnlock(lockID) if availableSiafunds.Cmp(amount) < 0 { return types.Transaction{}, ErrInsufficientSiafunds } // Truncate the keys to exactly the number needed. skps = skps[:skps[0].UnlockConditions.SignaturesRequired] // Assemble the base transction, including a 10 siacoin fee if possible. id, err := w.RegisterTransaction(types.Transaction{}) if err != nil { return types.Transaction{}, err } // Add a miner fee - if funding the transaction fails, we'll just send a // transaction with no fee. txn, err := w.FundTransaction(id, types.NewCurrency64(TransactionFee)) if err == nil { txn, _, err = w.AddMinerFee(id, types.NewCurrency64(TransactionFee)) if err != nil { return types.Transaction{}, err } } // Add the siafund inputs to the transcation. for _, sfoid := range sfoids { // Get an address for the siafund claims. lockID := w.mu.Lock() claimDest, _, err := w.coinAddress(false) w.mu.Unlock(lockID) if err != nil { return types.Transaction{}, err } // Assemble the SiafundInput to spend this output. sfi := types.SiafundInput{ ParentID: sfoid, UnlockConditions: skps[0].UnlockConditions, ClaimUnlockHash: claimDest, } txn, _, err = w.AddSiafundInput(id, sfi) if err != nil { return types.Transaction{}, err } } // Add the siafund output to the transaction. sfo := types.SiafundOutput{ Value: amount, UnlockHash: dest, } txn, _, err = w.AddSiafundOutput(id, sfo) if err != nil { return types.Transaction{}, err } // Add a refund siafund output if needed. if amount.Cmp(availableSiafunds) != 0 { refund := availableSiafunds.Sub(amount) sfo := types.SiafundOutput{ Value: refund, UnlockHash: baseUnlockHash, } txn, _, err = w.AddSiafundOutput(id, sfo) if err != nil { return types.Transaction{}, err } } // Add signatures for the siafund inputs. sigIndex := 0 for _, sfoid := range sfoids { for _, key := range skps { txnSig := types.TransactionSignature{ ParentID: crypto.Hash(sfoid), CoveredFields: types.CoveredFields{WholeTransaction: true}, PublicKeyIndex: uint64(key.Index), } txn.TransactionSignatures = append(txn.TransactionSignatures, txnSig) sigHash := txn.SigHash(sigIndex) encodedSig, err := crypto.SignHash(sigHash, key.SecretKey) if err != nil { return types.Transaction{}, err } txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] txn, _, err = w.AddTransactionSignature(id, txn.TransactionSignatures[sigIndex]) if err != nil { return types.Transaction{}, err } sigIndex++ } } // Sign the transaction. txn, err = w.SignTransaction(id, true) if err != nil { return types.Transaction{}, err } err = w.tpool.AcceptTransaction(txn) if err != nil { return types.Transaction{}, err } return txn, nil }
// verifyKeys checks a set of keys on disk to see that they can spend funds // sent to their address. func verifyKeys(uc types.UnlockConditions, folder string, keyname string) error { keysRequired := uc.SignaturesRequired totalKeys := uint64(len(uc.PublicKeys)) // Load the keys from disk back into memory, then verify that the keys on // disk are able to sign outputs in transactions. loadedKeys := make([]KeyPair, totalKeys) for i := 0; i < len(loadedKeys); i++ { err := encoding.ReadFile(filepath.Join(folder, keyname+"_Key"+strconv.Itoa(i)+FileExtension), &loadedKeys[i]) if err != nil { return err } if loadedKeys[i].Header != FileHeader { return ErrUnknownHeader } if loadedKeys[i].Version != FileVersion { return ErrUnknownVersion } } // Check that the keys can be used to spend transactions. for _, loadedKey := range loadedKeys { if loadedKey.UnlockConditions.UnlockHash() != uc.UnlockHash() { return ErrCorruptedKey } } // Create a transaction for the keys to sign. txn := types.Transaction{ SiafundInputs: []types.SiafundInput{ types.SiafundInput{ UnlockConditions: loadedKeys[0].UnlockConditions, }, }, } // Loop through and sign the transaction multiple times. All keys will be // used at least once by the time the loop terminates. var i uint64 for i != totalKeys { // i tracks which key is next to be used. If i + RequiredKeys results // in going out-of-bounds, reduce i so that the last key will be used // for the final signature. if i+keysRequired > totalKeys { i = totalKeys - keysRequired } var j uint64 for j < keysRequired { txn.TransactionSignatures = append(txn.TransactionSignatures, types.TransactionSignature{ PublicKeyIndex: i, CoveredFields: types.CoveredFields{WholeTransaction: true}, }) sigHash := txn.SigHash(int(j)) sig, err := crypto.SignHash(sigHash, loadedKeys[i].SecretKey) if err != nil { return err } txn.TransactionSignatures[j].Signature = sig[:] i++ j++ } // Check that the signature is valid. err := txn.StandaloneValid(0) if err != nil { return err } // Delete all of the signatures for the next iteration. txn.TransactionSignatures = nil } return nil }
// FundTransaction adds siacoins to a transaction that the wallet knows how to // spend. The exact amount of coins are always added, and this is achieved by // creating two transactions. The first transaciton, the parent, spends a set // of outputs that add up to at least the desired amount, and then creates a // single output of the exact amount and a second refund output. func (w *Wallet) FundTransaction(id string, amount types.Currency) (t types.Transaction, err error) { counter := w.mu.Lock() defer w.mu.Unlock(counter) // Create a parent transaction and supply it with enough inputs to cover // 'amount'. parentTxn := types.Transaction{} fundingOutputs, fundingTotal, err := w.findOutputs(amount) if err != nil { return } for _, output := range fundingOutputs { output.age = w.age key := w.keys[output.output.UnlockHash] newInput := types.SiacoinInput{ ParentID: output.id, UnlockConditions: key.unlockConditions, } parentTxn.SiacoinInputs = append(parentTxn.SiacoinInputs, newInput) } // Create and add the output that will be used to fund the standard // transaction. parentDest, parentSpendConds, err := w.coinAddress(false) // false indicates that the address should not be visible to the user exactOutput := types.SiacoinOutput{ Value: amount, UnlockHash: parentDest, } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, exactOutput) // Create a refund output if needed. if amount.Cmp(fundingTotal) != 0 { var refundDest types.UnlockHash refundDest, _, err = w.coinAddress(false) // false indicates that the address should not be visible to the user if err != nil { return } refundOutput := types.SiacoinOutput{ Value: fundingTotal.Sub(amount), UnlockHash: refundDest, } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, refundOutput) } // Sign all of the inputs to the parent trancstion. coveredFields := types.CoveredFields{WholeTransaction: true} for _, input := range parentTxn.SiacoinInputs { sig := types.TransactionSignature{ ParentID: crypto.Hash(input.ParentID), CoveredFields: coveredFields, PublicKeyIndex: 0, } parentTxn.TransactionSignatures = append(parentTxn.TransactionSignatures, sig) // Hash the transaction according to the covered fields. coinAddress := input.UnlockConditions.UnlockHash() sigIndex := len(parentTxn.TransactionSignatures) - 1 secKey := w.keys[coinAddress].secretKey sigHash := parentTxn.SigHash(sigIndex) // Get the signature. var encodedSig crypto.Signature encodedSig, err = crypto.SignHash(sigHash, secKey) if err != nil { return } parentTxn.TransactionSignatures[sigIndex].Signature = types.Signature(encodedSig[:]) } // Add the exact output to the wallet's knowledgebase before releasing the // lock, to prevent the wallet from using the exact output elsewhere. key := w.keys[parentSpendConds.UnlockHash()] key.outputs[parentTxn.SiacoinOutputID(0)] = &knownOutput{ id: parentTxn.SiacoinOutputID(0), output: exactOutput, spendable: true, age: w.age, } // Send the transaction to the transaction pool. err = w.tpool.AcceptTransaction(parentTxn) if err != nil { return } // Get the transaction that was originally meant to be funded. openTxn, exists := w.transactions[id] if !exists { err = ErrInvalidID return } txn := openTxn.transaction // Add the exact output. newInput := types.SiacoinInput{ ParentID: parentTxn.SiacoinOutputID(0), UnlockConditions: parentSpendConds, } openTxn.inputs = append(openTxn.inputs, len(txn.SiacoinInputs)) txn.SiacoinInputs = append(txn.SiacoinInputs, newInput) t = *txn return }
// SignTransaction signs the transaction, then deletes the transaction from the // wallet's internal memory, then returns the transaction. func (w *Wallet) SignTransaction(id string, wholeTransaction bool) (txn types.Transaction, err error) { counter := w.mu.Lock() defer w.mu.Unlock(counter) // Fetch the transaction. openTxn, exists := w.transactions[id] if !exists { err = ErrInvalidID return } txn = *openTxn.transaction // Get the coveredfields struct. var coveredFields types.CoveredFields if wholeTransaction { coveredFields = types.CoveredFields{WholeTransaction: true} } else { for i := range txn.MinerFees { coveredFields.MinerFees = append(coveredFields.MinerFees, uint64(i)) } for i := range txn.SiacoinInputs { coveredFields.SiacoinInputs = append(coveredFields.SiacoinInputs, uint64(i)) } for i := range txn.SiacoinOutputs { coveredFields.SiacoinOutputs = append(coveredFields.SiacoinOutputs, uint64(i)) } for i := range txn.FileContracts { coveredFields.FileContracts = append(coveredFields.FileContracts, uint64(i)) } for i := range txn.StorageProofs { coveredFields.StorageProofs = append(coveredFields.StorageProofs, uint64(i)) } for i := range txn.ArbitraryData { coveredFields.ArbitraryData = append(coveredFields.ArbitraryData, uint64(i)) } for i := range txn.TransactionSignatures { coveredFields.TransactionSignatures = append(coveredFields.TransactionSignatures, uint64(i)) } } // For each input in the transaction that we added, provide a signature. for _, inputIndex := range openTxn.inputs { input := txn.SiacoinInputs[inputIndex] sig := types.TransactionSignature{ ParentID: crypto.Hash(input.ParentID), CoveredFields: coveredFields, PublicKeyIndex: 0, } txn.TransactionSignatures = append(txn.TransactionSignatures, sig) // Hash the transaction according to the covered fields. coinAddress := input.UnlockConditions.UnlockHash() sigIndex := len(txn.TransactionSignatures) - 1 secKey := w.keys[coinAddress].secretKey sigHash := txn.SigHash(sigIndex) // Get the signature. var encodedSig crypto.Signature encodedSig, err = crypto.SignHash(sigHash, secKey) if err != nil { return } txn.TransactionSignatures[sigIndex].Signature = types.Signature(encodedSig[:]) } // Delete the open transaction. delete(w.transactions, id) return }
// FormContract forms a contract with a host and submits the contract // transaction to tpool. func FormContract(params ContractParams, txnBuilder transactionBuilder, tpool transactionPool) (modules.RenterContract, error) { // extract vars from params, for convenience host, filesize, startHeight, endHeight, refundAddress := params.Host, params.Filesize, params.StartHeight, params.EndHeight, params.RefundAddress // create our key ourSK, ourPK, err := crypto.GenerateKeyPair() if err != nil { return modules.RenterContract{}, err } ourPublicKey := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: ourPK[:], } // create unlock conditions uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{ourPublicKey, host.PublicKey}, SignaturesRequired: 2, } // calculate cost to renter and cost to host // TODO: clarify/abstract this math storageAllocation := host.StoragePrice.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) hostCollateral := host.Collateral.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) if hostCollateral.Cmp(host.MaxCollateral) > 0 { // TODO: if we have to cap the collateral, it probably means we shouldn't be using this host // (ok within a factor of 2) hostCollateral = host.MaxCollateral } hostPayout := hostCollateral.Add(host.ContractPrice) payout := storageAllocation.Add(hostPayout).Mul64(10406).Div64(10000) // renter pays for siafund fee renterCost := payout.Sub(hostCollateral) // check for negative currency if types.PostTax(startHeight, payout).Cmp(hostPayout) < 0 { return modules.RenterContract{}, errors.New("payout smaller than host payout") } // create file contract fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: endHeight, WindowEnd: endHeight + host.WindowSize, Payout: payout, UnlockHash: uc.UnlockHash(), RevisionNumber: 0, ValidProofOutputs: []types.SiacoinOutput{ // outputs need to account for tax {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // collateral is returned to host {Value: hostPayout, UnlockHash: host.UnlockHash}, }, MissedProofOutputs: []types.SiacoinOutput{ // same as above {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // same as above {Value: hostPayout, UnlockHash: host.UnlockHash}, // once we start doing revisions, we'll move some coins to the host and some to the void {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, }, } // calculate transaction fee _, maxFee := tpool.FeeEstimation() fee := maxFee.Mul64(estTxnSize) // build transaction containing fc err = txnBuilder.FundSiacoins(renterCost.Add(fee)) if err != nil { return modules.RenterContract{}, err } txnBuilder.AddFileContract(fc) // add miner fee txnBuilder.AddMinerFee(fee) // create initial transaction set txn, parentTxns := txnBuilder.View() txnSet := append(parentTxns, txn) // initiate connection conn, err := net.DialTimeout("tcp", string(host.NetAddress), 15*time.Second) if err != nil { return modules.RenterContract{}, err } defer func() { _ = conn.Close() }() // allot time for sending RPC ID + verifySettings extendDeadline(conn, modules.NegotiateSettingsTime) if err = encoding.WriteObject(conn, modules.RPCFormContract); err != nil { return modules.RenterContract{}, err } // verify the host's settings and confirm its identity host, err = verifySettings(conn, host) if err != nil { return modules.RenterContract{}, err } if !host.AcceptingContracts { return modules.RenterContract{}, errors.New("host is not accepting contracts") } // allot time for negotiation extendDeadline(conn, modules.NegotiateFileContractTime) // send acceptance, txn signed by us, and pubkey if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send initial acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, txnSet); err != nil { return modules.RenterContract{}, errors.New("couldn't send the contract signed by us: " + err.Error()) } if err = encoding.WriteObject(conn, ourSK.PublicKey()); err != nil { return modules.RenterContract{}, errors.New("couldn't send our public key: " + err.Error()) } // read acceptance and txn signed by host if err = modules.ReadNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("host did not accept our proposed contract: " + err.Error()) } // host now sends any new parent transactions, inputs and outputs that // were added to the transaction var newParents []types.Transaction var newInputs []types.SiacoinInput var newOutputs []types.SiacoinOutput if err = encoding.ReadObject(conn, &newParents, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added parents: " + err.Error()) } if err = encoding.ReadObject(conn, &newInputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added inputs: " + err.Error()) } if err = encoding.ReadObject(conn, &newOutputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added outputs: " + err.Error()) } // merge txnAdditions with txnSet txnBuilder.AddParents(newParents) for _, input := range newInputs { txnBuilder.AddSiacoinInput(input) } for _, output := range newOutputs { txnBuilder.AddSiacoinOutput(output) } // sign the txn signedTxnSet, err := txnBuilder.Sign(true) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign transaction: "+err.Error())) } // calculate signatures added by the transaction builder var addedSignatures []types.TransactionSignature _, _, _, addedSignatureIndices := txnBuilder.ViewAdded() for _, i := range addedSignatureIndices { addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i]) } // create initial (no-op) revision, transaction, and signature initRevision := types.FileContractRevision{ ParentID: signedTxnSet[len(signedTxnSet)-1].FileContractID(0), UnlockConditions: uc, NewRevisionNumber: 1, NewFileSize: fc.FileSize, NewFileMerkleRoot: fc.FileMerkleRoot, NewWindowStart: fc.WindowStart, NewWindowEnd: fc.WindowEnd, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: fc.UnlockHash, } renterRevisionSig := types.TransactionSignature{ ParentID: crypto.Hash(initRevision.ParentID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{ FileContractRevisions: []uint64{0}, }, } revisionTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{initRevision}, TransactionSignatures: []types.TransactionSignature{renterRevisionSig}, } encodedSig, err := crypto.SignHash(revisionTxn.SigHash(0), ourSK) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign revision transaction: "+err.Error())) } revisionTxn.TransactionSignatures[0].Signature = encodedSig[:] // Send acceptance and signatures if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send transaction acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, addedSignatures); err != nil { return modules.RenterContract{}, errors.New("couldn't send added signatures: " + err.Error()) } if err = encoding.WriteObject(conn, revisionTxn.TransactionSignatures[0]); err != nil { return modules.RenterContract{}, errors.New("couldn't send revision signature: " + err.Error()) } // Read the host acceptance and signatures. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return modules.RenterContract{}, errors.New("host did not accept our signatures: " + err.Error()) } var hostSigs []types.TransactionSignature if err = encoding.ReadObject(conn, &hostSigs, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's signatures: " + err.Error()) } for _, sig := range hostSigs { txnBuilder.AddTransactionSignature(sig) } var hostRevisionSig types.TransactionSignature if err = encoding.ReadObject(conn, &hostRevisionSig, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's revision signature: " + err.Error()) } revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostRevisionSig) // Construct the final transaction. txn, parentTxns = txnBuilder.View() txnSet = append(parentTxns, txn) // Submit to blockchain. err = tpool.AcceptTransactionSet(txnSet) if err == modules.ErrDuplicateTransactionSet { // as long as it made it into the transaction pool, we're good err = nil } if err != nil { return modules.RenterContract{}, err } // calculate contract ID fcid := txn.FileContractID(0) return modules.RenterContract{ FileContract: fc, ID: fcid, LastRevision: initRevision, LastRevisionTxn: revisionTxn, NetAddress: host.NetAddress, SecretKey: ourSK, }, nil }
// 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 }
// Renew negotiates a new contract for data already stored with a host, and // submits the new contract transaction to tpool. func Renew(contract modules.RenterContract, params ContractParams, txnBuilder transactionBuilder, tpool transactionPool) (modules.RenterContract, error) { // extract vars from params, for convenience host, filesize, startHeight, endHeight, refundAddress := params.Host, params.Filesize, params.StartHeight, params.EndHeight, params.RefundAddress ourSK := contract.SecretKey // calculate cost to renter and cost to host storageAllocation := host.StoragePrice.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) hostCollateral := host.Collateral.Mul64(filesize).Mul64(uint64(endHeight - startHeight)) if hostCollateral.Cmp(host.MaxCollateral) > 0 { // TODO: if we have to cap the collateral, it probably means we shouldn't be using this host // (ok within a factor of 2) hostCollateral = host.MaxCollateral } // Calculate additional basePrice and baseCollateral. If the contract // height did not increase, basePrice and baseCollateral are zero. var basePrice, baseCollateral types.Currency if endHeight+host.WindowSize > contract.LastRevision.NewWindowEnd { timeExtension := uint64((endHeight + host.WindowSize) - contract.LastRevision.NewWindowEnd) basePrice = host.StoragePrice.Mul64(contract.LastRevision.NewFileSize).Mul64(timeExtension) // cost of data already covered by contract, i.e. lastrevision.Filesize baseCollateral = host.Collateral.Mul64(contract.LastRevision.NewFileSize).Mul64(timeExtension) // same but collateral } hostPayout := hostCollateral.Add(host.ContractPrice).Add(basePrice) payout := storageAllocation.Add(hostCollateral.Add(host.ContractPrice)).Mul64(10406).Div64(10000) // renter covers siafund fee renterCost := payout.Sub(hostCollateral) // check for negative currency if types.PostTax(startHeight, payout).Cmp(hostPayout) < 0 { return modules.RenterContract{}, errors.New("payout smaller than host payout") } else if hostCollateral.Cmp(baseCollateral) < 0 { return modules.RenterContract{}, errors.New("new collateral smaller than old collateral") } // create file contract fc := types.FileContract{ FileSize: contract.LastRevision.NewFileSize, FileMerkleRoot: contract.LastRevision.NewFileMerkleRoot, WindowStart: endHeight, WindowEnd: endHeight + host.WindowSize, Payout: payout, UnlockHash: contract.LastRevision.NewUnlockHash, RevisionNumber: 0, ValidProofOutputs: []types.SiacoinOutput{ // renter {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // host {Value: hostPayout, UnlockHash: host.UnlockHash}, }, MissedProofOutputs: []types.SiacoinOutput{ // renter {Value: types.PostTax(startHeight, payout).Sub(hostPayout), UnlockHash: refundAddress}, // host gets its unused collateral back, plus the contract price {Value: hostCollateral.Sub(baseCollateral).Add(host.ContractPrice), UnlockHash: host.UnlockHash}, // void gets the spent storage fees, plus the collateral being risked {Value: basePrice.Add(baseCollateral), UnlockHash: types.UnlockHash{}}, }, } // calculate transaction fee _, maxFee := tpool.FeeEstimation() fee := maxFee.Mul64(estTxnSize) // build transaction containing fc err := txnBuilder.FundSiacoins(renterCost.Add(fee)) if err != nil { return modules.RenterContract{}, err } txnBuilder.AddFileContract(fc) // add miner fee txnBuilder.AddMinerFee(fee) // create initial transaction set txn, parentTxns := txnBuilder.View() txnSet := append(parentTxns, txn) // initiate connection conn, err := net.DialTimeout("tcp", string(host.NetAddress), 15*time.Second) if err != nil { return modules.RenterContract{}, err } defer func() { _ = conn.Close() }() // allot time for sending RPC ID, verifyRecentRevision, and verifySettings extendDeadline(conn, modules.NegotiateRecentRevisionTime+modules.NegotiateSettingsTime) if err = encoding.WriteObject(conn, modules.RPCRenewContract); err != nil { return modules.RenterContract{}, errors.New("couldn't initiate RPC: " + err.Error()) } // verify that both parties are renewing the same contract if err = verifyRecentRevision(conn, contract); err != nil { return modules.RenterContract{}, errors.New("revision exchange failed: " + err.Error()) } // verify the host's settings and confirm its identity host, err = verifySettings(conn, host) if err != nil { return modules.RenterContract{}, errors.New("settings exchange failed: " + err.Error()) } if !host.AcceptingContracts { return modules.RenterContract{}, errors.New("host is not accepting contracts") } // allot time for negotiation extendDeadline(conn, modules.NegotiateRenewContractTime) // send acceptance, txn signed by us, and pubkey if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send initial acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, txnSet); err != nil { return modules.RenterContract{}, errors.New("couldn't send the contract signed by us: " + err.Error()) } if err = encoding.WriteObject(conn, ourSK.PublicKey()); err != nil { return modules.RenterContract{}, errors.New("couldn't send our public key: " + err.Error()) } // read acceptance and txn signed by host if err = modules.ReadNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("host did not accept our proposed contract: " + err.Error()) } // host now sends any new parent transactions, inputs and outputs that // were added to the transaction var newParents []types.Transaction var newInputs []types.SiacoinInput var newOutputs []types.SiacoinOutput if err = encoding.ReadObject(conn, &newParents, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added parents: " + err.Error()) } if err = encoding.ReadObject(conn, &newInputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added inputs: " + err.Error()) } if err = encoding.ReadObject(conn, &newOutputs, types.BlockSizeLimit); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's added outputs: " + err.Error()) } // merge txnAdditions with txnSet txnBuilder.AddParents(newParents) for _, input := range newInputs { txnBuilder.AddSiacoinInput(input) } for _, output := range newOutputs { txnBuilder.AddSiacoinOutput(output) } // sign the txn signedTxnSet, err := txnBuilder.Sign(true) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign transaction: "+err.Error())) } // calculate signatures added by the transaction builder var addedSignatures []types.TransactionSignature _, _, _, addedSignatureIndices := txnBuilder.ViewAdded() for _, i := range addedSignatureIndices { addedSignatures = append(addedSignatures, signedTxnSet[len(signedTxnSet)-1].TransactionSignatures[i]) } // create initial (no-op) revision, transaction, and signature initRevision := types.FileContractRevision{ ParentID: signedTxnSet[len(signedTxnSet)-1].FileContractID(0), UnlockConditions: contract.LastRevision.UnlockConditions, NewRevisionNumber: 1, NewFileSize: fc.FileSize, NewFileMerkleRoot: fc.FileMerkleRoot, NewWindowStart: fc.WindowStart, NewWindowEnd: fc.WindowEnd, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: fc.UnlockHash, } renterRevisionSig := types.TransactionSignature{ ParentID: crypto.Hash(initRevision.ParentID), PublicKeyIndex: 0, CoveredFields: types.CoveredFields{ FileContractRevisions: []uint64{0}, }, } revisionTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{initRevision}, TransactionSignatures: []types.TransactionSignature{renterRevisionSig}, } encodedSig, err := crypto.SignHash(revisionTxn.SigHash(0), ourSK) if err != nil { return modules.RenterContract{}, modules.WriteNegotiationRejection(conn, errors.New("failed to sign revision transaction: "+err.Error())) } revisionTxn.TransactionSignatures[0].Signature = encodedSig[:] // Send acceptance and signatures if err = modules.WriteNegotiationAcceptance(conn); err != nil { return modules.RenterContract{}, errors.New("couldn't send transaction acceptance: " + err.Error()) } if err = encoding.WriteObject(conn, addedSignatures); err != nil { return modules.RenterContract{}, errors.New("couldn't send added signatures: " + err.Error()) } if err = encoding.WriteObject(conn, revisionTxn.TransactionSignatures[0]); err != nil { return modules.RenterContract{}, errors.New("couldn't send revision signature: " + err.Error()) } // Read the host acceptance and signatures. err = modules.ReadNegotiationAcceptance(conn) if err != nil { return modules.RenterContract{}, errors.New("host did not accept our signatures: " + err.Error()) } var hostSigs []types.TransactionSignature if err = encoding.ReadObject(conn, &hostSigs, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's signatures: " + err.Error()) } for _, sig := range hostSigs { txnBuilder.AddTransactionSignature(sig) } var hostRevisionSig types.TransactionSignature if err = encoding.ReadObject(conn, &hostRevisionSig, 2e3); err != nil { return modules.RenterContract{}, errors.New("couldn't read the host's revision signature: " + err.Error()) } revisionTxn.TransactionSignatures = append(revisionTxn.TransactionSignatures, hostRevisionSig) // Construct the final transaction. txn, parentTxns = txnBuilder.View() txnSet = append(parentTxns, txn) // Submit to blockchain. err = tpool.AcceptTransactionSet(txnSet) if err == modules.ErrDuplicateTransactionSet { // as long as it made it into the transaction pool, we're good err = nil } if err != nil { return modules.RenterContract{}, err } // calculate contract ID fcid := txn.FileContractID(0) return modules.RenterContract{ FileContract: fc, ID: fcid, LastRevision: initRevision, LastRevisionTxn: revisionTxn, MerkleRoots: contract.MerkleRoots, NetAddress: host.NetAddress, SecretKey: ourSK, }, nil }
// TestTransactionValidSignatures probes the validSignatures method of the // Transaction type. func TestTransactionValidSignatures(t *testing.T) { // Create keys for use in signing and verifying. sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } // Create UnlockConditions with 3 keys, 2 of which are required. The first // possible key is a standard signature. The second key is an unknown // signature type, which should always be accepted. The final type is an // entropy type, which should never be accepted. uc := UnlockConditions{ PublicKeys: []SiaPublicKey{ {Algorithm: SignatureEd25519, Key: pk[:]}, {}, {Algorithm: SignatureEntropy}, }, SignaturesRequired: 2, } // Create a transaction with each type of unlock condition. txn := Transaction{ SiacoinInputs: []SiacoinInput{ {UnlockConditions: uc}, }, FileContractRevisions: []FileContractRevision{ {UnlockConditions: uc}, }, SiafundInputs: []SiafundInput{ {UnlockConditions: uc}, }, } txn.FileContractRevisions[0].ParentID[0] = 1 // can't overlap with other objects txn.SiafundInputs[0].ParentID[0] = 2 // can't overlap with other objects // Create the signatures that spend the output. txn.TransactionSignatures = []TransactionSignature{ // First signatures use cryptography. { Timelock: 5, CoveredFields: CoveredFields{WholeTransaction: true}, }, { CoveredFields: CoveredFields{WholeTransaction: true}, }, { CoveredFields: CoveredFields{WholeTransaction: true}, }, // The second signatures should always work for being unrecognized // types. {PublicKeyIndex: 1}, {PublicKeyIndex: 1}, {PublicKeyIndex: 1}, } txn.TransactionSignatures[1].ParentID[0] = 1 txn.TransactionSignatures[2].ParentID[0] = 2 txn.TransactionSignatures[4].ParentID[0] = 1 txn.TransactionSignatures[5].ParentID[0] = 2 sigHash0 := txn.SigHash(0) sigHash1 := txn.SigHash(1) sigHash2 := txn.SigHash(2) sig0, err := crypto.SignHash(sigHash0, sk) if err != nil { t.Fatal(err) } sig1, err := crypto.SignHash(sigHash1, sk) if err != nil { t.Fatal(err) } sig2, err := crypto.SignHash(sigHash2, sk) if err != nil { t.Fatal(err) } txn.TransactionSignatures[0].Signature = sig0[:] txn.TransactionSignatures[1].Signature = sig1[:] txn.TransactionSignatures[2].Signature = sig2[:] // Check that the signing was successful. err = txn.validSignatures(10) if err != nil { t.Error(err) } // Corrupt one of the sigantures. sig0[0]++ txn.TransactionSignatures[0].Signature = sig0[:] err = txn.validSignatures(10) if err == nil { t.Error("Corrupted a signature but the txn was still accepted as valid!") } sig0[0]-- txn.TransactionSignatures[0].Signature = sig0[:] // Fail the validCoveredFields check. txn.TransactionSignatures[0].CoveredFields.SiacoinInputs = []uint64{33} err = txn.validSignatures(10) if err == nil { t.Error("failed to flunk the validCoveredFields check") } txn.TransactionSignatures[0].CoveredFields.SiacoinInputs = nil // Double spend a SiacoinInput, FileContractTermination, and SiafundInput. txn.SiacoinInputs = append(txn.SiacoinInputs, SiacoinInput{UnlockConditions: UnlockConditions{}}) err = txn.validSignatures(10) if err == nil { t.Error("failed to double spend a siacoin input") } txn.SiacoinInputs = txn.SiacoinInputs[:len(txn.SiacoinInputs)-1] txn.FileContractRevisions = append(txn.FileContractRevisions, FileContractRevision{UnlockConditions: UnlockConditions{}}) err = txn.validSignatures(10) if err == nil { t.Error("failed to double spend a file contract termination") } txn.FileContractRevisions = txn.FileContractRevisions[:len(txn.FileContractRevisions)-1] txn.SiafundInputs = append(txn.SiafundInputs, SiafundInput{UnlockConditions: UnlockConditions{}}) err = txn.validSignatures(10) if err == nil { t.Error("failed to double spend a siafund input") } txn.SiafundInputs = txn.SiafundInputs[:len(txn.SiafundInputs)-1] // Add a frivilous signature txn.TransactionSignatures = append(txn.TransactionSignatures, TransactionSignature{}) err = txn.validSignatures(10) if err != ErrFrivilousSignature { t.Error(err) } txn.TransactionSignatures = txn.TransactionSignatures[:len(txn.TransactionSignatures)-1] // Replace one of the cryptography signatures with an always-accepted // signature. This should get rejected because the always-accepted // signature has already been used. tmpTxn0 := txn.TransactionSignatures[0] txn.TransactionSignatures[0] = TransactionSignature{PublicKeyIndex: 1} err = txn.validSignatures(10) if err != ErrPublicKeyOveruse { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Fail the timelock check for signatures. err = txn.validSignatures(4) if err != ErrPrematureSignature { t.Error(err) } // Try to spend an entropy signature. txn.TransactionSignatures[0] = TransactionSignature{PublicKeyIndex: 2} err = txn.validSignatures(10) if err != ErrEntropyKey { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Try to point to a nonexistent public key. txn.TransactionSignatures[0] = TransactionSignature{PublicKeyIndex: 5} err = txn.validSignatures(10) if err != ErrInvalidPubKeyIndex { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Insert a malformed public key into the transaction. txn.SiacoinInputs[0].UnlockConditions.PublicKeys[0].Key = []byte{'b', 'a', 'd'} err = txn.validSignatures(10) if err == nil { t.Error(err) } txn.SiacoinInputs[0].UnlockConditions.PublicKeys[0].Key = pk[:] // Insert a malformed signature into the transaction. txn.TransactionSignatures[0].Signature = []byte{'m', 'a', 'l'} err = txn.validSignatures(10) if err == nil { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Try to spend a transaction when not every required signature is // available. txn.TransactionSignatures = txn.TransactionSignatures[1:] err = txn.validSignatures(10) if err != ErrMissingSignatures { t.Error(err) } }
// testFileContractRevision creates and revises a file contract on the // blockchain. func (cst *consensusSetTester) testFileContractRevision() { // COMPATv0.4.0 - Step the block height up past the hardfork amount. This // code stops nondeterministic failures when producing storage proofs that // is related to buggy old code. for cst.cs.dbBlockHeight() <= 10 { _, err := cst.miner.AddBlock() if err != nil { panic(err) } } // Create a file (as a bytes.Buffer) that will be used for the file // contract. filesize := uint64(4e3) file := randFile(filesize) merkleRoot, err := crypto.ReaderMerkleRoot(file) if err != nil { panic(err) } file.Seek(0, 0) // Create a spendable unlock hash for the file contract. sk, pk, err := crypto.GenerateKeyPair() if err != nil { panic(err) } uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{{ Algorithm: types.SignatureEd25519, Key: pk[:], }}, SignaturesRequired: 1, } // Create a file contract that will be revised. validProofDest := randAddress() payout := types.NewCurrency64(400e6) fc := types.FileContract{ FileSize: filesize, FileMerkleRoot: crypto.Hash{}, WindowStart: cst.cs.dbBlockHeight() + 2, WindowEnd: cst.cs.dbBlockHeight() + 3, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{{ UnlockHash: validProofDest, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, MissedProofOutputs: []types.SiacoinOutput{{ UnlockHash: types.UnlockHash{}, Value: types.PostTax(cst.cs.dbBlockHeight(), payout), }}, UnlockHash: uc.UnlockHash(), } // Submit a transaction with the file contract. txnBuilder := cst.wallet.StartTransaction() err = txnBuilder.FundSiacoins(payout) if err != nil { panic(err) } fcIndex := txnBuilder.AddFileContract(fc) txnSet, err := txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Submit a revision for the file contract. ti := len(txnSet) - 1 fcid := txnSet[ti].FileContractID(fcIndex) fcr := types.FileContractRevision{ ParentID: fcid, UnlockConditions: uc, NewRevisionNumber: 69292, NewFileSize: filesize, NewFileMerkleRoot: merkleRoot, NewWindowStart: cst.cs.dbBlockHeight() + 1, NewWindowEnd: cst.cs.dbBlockHeight() + 2, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: uc.UnlockHash(), } ts := types.TransactionSignature{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{WholeTransaction: true}, PublicKeyIndex: 0, } txn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{fcr}, TransactionSignatures: []types.TransactionSignature{ts}, } encodedSig, err := crypto.SignHash(txn.SigHash(0), sk) if err != nil { panic(err) } txn.TransactionSignatures[0].Signature = encodedSig[:] err = cst.tpool.AcceptTransactionSet([]types.Transaction{txn}) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Create and submit a storage proof for the file contract. segmentIndex, err := cst.cs.StorageProofSegment(fcid) if err != nil { panic(err) } segment, hashSet, err := crypto.BuildReaderProof(file, segmentIndex) if err != nil { panic(err) } sp := types.StorageProof{ ParentID: fcid, HashSet: hashSet, } copy(sp.Segment[:], segment) txnBuilder = cst.wallet.StartTransaction() txnBuilder.AddStorageProof(sp) txnSet, err = txnBuilder.Sign(true) if err != nil { panic(err) } err = cst.tpool.AcceptTransactionSet(txnSet) if err != nil { panic(err) } _, err = cst.miner.AddBlock() if err != nil { panic(err) } // Check that the file contract has been removed. _, err = cst.cs.dbGetFileContract(fcid) if err != errNilItem { panic("file contract should not exist in the database") } }
// 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 }
// 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 }