// 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 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 }
// 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 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 }