// managedVerifyChallengeResponse will verify that the renter's response to the // challenge provided by the host is correct. In the process, the storage // obligation and file contract revision will be loaded and returned. // // The storage obligation is returned under a storage obligation lock. func (h *Host) managedVerifyChallengeResponse(fcid types.FileContractID, challenge crypto.Hash, challengeResponse crypto.Signature) (so storageObligation, recentRevision types.FileContractRevision, revisionSigs []types.TransactionSignature, err error) { // Grab a lock before it is possible to perform any operations on the // storage obligation. Defer a call to unlock in the event of an error. If // there is no error, the storage obligation will be returned with a lock. err = h.managedTryLockStorageObligation(fcid) if err != nil { err = extendErr("could not get "+fcid.String()+" lock: ", ErrorInternal(err.Error())) return storageObligation{}, types.FileContractRevision{}, nil, err } defer func() { if err != nil { h.managedUnlockStorageObligation(fcid) } }() // Fetch the storage obligation, which has the revision, which has the // renter's public key. h.mu.RLock() defer h.mu.RUnlock() err = h.db.View(func(tx *bolt.Tx) error { so, err = getStorageObligation(tx, fcid) return err }) if err != nil { err = extendErr("could not fetch "+fcid.String()+": ", ErrorInternal(err.Error())) return storageObligation{}, types.FileContractRevision{}, nil, err } // Pull out the file contract revision and the revision's signatures from // the transaction. revisionTxn := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1] recentRevision = revisionTxn.FileContractRevisions[0] for _, sig := range revisionTxn.TransactionSignatures { // Checking for just the parent id is sufficient, an over-signed file // contract is invalid. if sig.ParentID == crypto.Hash(fcid) { revisionSigs = append(revisionSigs, sig) } } // Verify that the challegne response matches the public key. var renterPK crypto.PublicKey // Sanity check - there should be two public keys. if len(recentRevision.UnlockConditions.PublicKeys) != 2 { // The error has to be set here so that the defered error check will // unlock the storage obligation. h.log.Critical("wrong public key count in file contract revision") err = errRevisionWrongPublicKeyCount err = extendErr("wrong public key count for "+fcid.String()+": ", ErrorInternal(err.Error())) return storageObligation{}, types.FileContractRevision{}, nil, err } copy(renterPK[:], recentRevision.UnlockConditions.PublicKeys[0].Key) err = crypto.VerifyHash(challenge, renterPK, challengeResponse) if err != nil { err = extendErr("bad signature from renter: ", ErrorCommunication(err.Error())) return storageObligation{}, types.FileContractRevision{}, nil, err } return so, recentRevision, revisionSigs, 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 }
// storageSectorsDeleteHandler handles the call to delete a sector from the // storage manager. func (srv *Server) storageSectorsDeleteHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { sectorRoot, err := scanAddress(ps.ByName("merkleroot")) if err != nil { writeError(w, Error{err.Error()}, http.StatusBadRequest) return } err = srv.host.DeleteSector(crypto.Hash(sectorRoot)) if err != nil { writeError(w, Error{err.Error()}, http.StatusBadRequest) return } writeSuccess(w) }
// addAddress either creates a new list of transactions for the given // address, or adds the txid to the list if such a list already exists func (tx *boltTx) addAddress(addr types.UnlockHash, txid types.TransactionID) { tx.putObject("Hashes", crypto.Hash(addr), hashUnlockHash) oldErr := tx.err var txns []types.TransactionID tx.getObject("Addresses", addr, &txns) if oldErr == nil && tx.err == persist.ErrNilEntry { // NOTE: this is a special case where a nil entry is not an error, so // we must explicitly reset tx.err. tx.err = nil } txns = append(txns, txid) tx.putObject("Addresses", addr, txns) }
// 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 }
// TestTransactionIDs probes all of the ID functions of the Transaction type. func TestIDs(t *testing.T) { // Create every type of ID using empty fields. txn := Transaction{ SiacoinOutputs: []SiacoinOutput{{}}, FileContracts: []FileContract{{}}, SiafundOutputs: []SiafundOutput{{}}, } tid := txn.ID() scoid := txn.SiacoinOutputID(0) fcid := txn.FileContractID(0) fctpid := fcid.FileContractTerminationPayoutID(0) spidT := fcid.StorageProofOutputID(ProofValid, 0) spidF := fcid.StorageProofOutputID(ProofMissed, 0) sfoid := txn.SiafundOutputID(0) scloid := sfoid.SiaClaimOutputID() // Put all of the ids into a slice. var ids []crypto.Hash ids = append(ids, crypto.Hash(tid), crypto.Hash(scoid), crypto.Hash(fcid), crypto.Hash(fctpid), crypto.Hash(spidT), crypto.Hash(spidF), crypto.Hash(sfoid), crypto.Hash(scloid), ) // Check that each id is unique. knownIDs := make(map[crypto.Hash]struct{}) for i, id := range ids { _, exists := knownIDs[id] if exists { t.Error("id repeat for index", i) } knownIDs[id] = struct{}{} } }
// verifyChallengeResponse will verify that the renter's response to the // challenge provided by the host is correct. In the process, the storage // obligation and file contract revision will be loaded and returned. func (h *Host) verifyChallengeResponse(fcid types.FileContractID, challenge crypto.Hash, challengeResponse crypto.Signature) (*storageObligation, types.FileContractRevision, []types.TransactionSignature, error) { // Fetch the storage obligation, which has the revision, which has the // renter's public key. var so *storageObligation err := h.db.Update(func(tx *bolt.Tx) error { fso, err := getStorageObligation(tx, fcid) so = &fso return err }) if err != nil { return nil, types.FileContractRevision{}, nil, err } // Pull out the file contract revision and the revision's signatures from // the transaction. revisionTxn := so.RevisionTransactionSet[len(so.RevisionTransactionSet)-1] recentRevision := revisionTxn.FileContractRevisions[0] var revisionSigs []types.TransactionSignature for _, sig := range revisionTxn.TransactionSignatures { // Checking for just the parent id is sufficient, an over-signed file // contract is invalid. if sig.ParentID == crypto.Hash(fcid) { revisionSigs = append(revisionSigs, sig) } } // Verify that the challegne response matches the public key. var renterPK crypto.PublicKey // Sanity check - there should be two public keys. if len(recentRevision.UnlockConditions.PublicKeys) != 2 { h.log.Critical("found a revision with too few public keys") return nil, types.FileContractRevision{}, nil, errRevisionFewPublicKeys } copy(renterPK[:], recentRevision.UnlockConditions.PublicKeys[0].Key) err = crypto.VerifyHash(challenge, renterPK, challengeResponse) if err != nil { return nil, types.FileContractRevision{}, nil, err } return so, recentRevision, revisionSigs, 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 }
// validSignatures checks the validaty of all signatures in a transaction. func (t *Transaction) validSignatures(currentHeight BlockHeight) error { // Check that all covered fields objects follow the rules. err := t.validCoveredFields() if err != nil { return err } // Create the inputSignatures object for each input. sigMap := make(map[crypto.Hash]*inputSignatures) for i, input := range t.SiacoinInputs { id := crypto.Hash(input.ParentID) _, exists := sigMap[id] if exists { return ErrDoubleSpend } sigMap[id] = &inputSignatures{ remainingSignatures: input.UnlockConditions.SignaturesRequired, possibleKeys: input.UnlockConditions.PublicKeys, usedKeys: make(map[uint64]struct{}), index: i, } } for i, revision := range t.FileContractRevisions { id := crypto.Hash(revision.ParentID) _, exists := sigMap[id] if exists { return ErrDoubleSpend } sigMap[id] = &inputSignatures{ remainingSignatures: revision.UnlockConditions.SignaturesRequired, possibleKeys: revision.UnlockConditions.PublicKeys, usedKeys: make(map[uint64]struct{}), index: i, } } for i, input := range t.SiafundInputs { id := crypto.Hash(input.ParentID) _, exists := sigMap[id] if exists { return ErrDoubleSpend } sigMap[id] = &inputSignatures{ remainingSignatures: input.UnlockConditions.SignaturesRequired, possibleKeys: input.UnlockConditions.PublicKeys, usedKeys: make(map[uint64]struct{}), index: i, } } // Check all of the signatures for validity. for i, sig := range t.TransactionSignatures { // Check that sig corresponds to an entry in sigMap. inSig, exists := sigMap[crypto.Hash(sig.ParentID)] if !exists || inSig.remainingSignatures == 0 { return ErrFrivilousSignature } // Check that sig's key hasn't already been used. _, exists = inSig.usedKeys[sig.PublicKeyIndex] if exists { return ErrPublicKeyOveruse } // Check that the public key index refers to an existing public key. if sig.PublicKeyIndex >= uint64(len(inSig.possibleKeys)) { return ErrInvalidPubKeyIndex } // Check that the timelock has expired. if sig.Timelock > currentHeight { return ErrPrematureSignature } // Check that the signature verifies. Multiple signature schemes are // supported. publicKey := inSig.possibleKeys[sig.PublicKeyIndex] switch publicKey.Algorithm { case SignatureEntropy: // Entropy cannot ever be used to sign a transaction. return ErrEntropyKey case SignatureEd25519: // Decode the public key and signature. var edPK crypto.PublicKey err := encoding.Unmarshal([]byte(publicKey.Key), &edPK) if err != nil { return err } var edSig [crypto.SignatureSize]byte err = encoding.Unmarshal([]byte(sig.Signature), &edSig) if err != nil { return err } cryptoSig := crypto.Signature(edSig) sigHash := t.SigHash(i) err = crypto.VerifyHash(sigHash, edPK, cryptoSig) if err != nil { return err } default: // If the identifier is not recognized, assume that the signature // is valid. This allows more signature types to be added via soft // forking. } inSig.usedKeys[sig.PublicKeyIndex] = struct{}{} inSig.remainingSignatures-- } // Check that all inputs have been sufficiently signed. for _, reqSigs := range sigMap { if reqSigs.remainingSignatures != 0 { return ErrMissingSignatures } } return nil }
// 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 }
// 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 }
// FundSiacoins will add a siacoin input of exaclty 'amount' to the // transaction. A parent transaction may be needed to achieve an input with the // correct value. The siacoin input will not be signed until 'Sign' is called // on the transaction builder. func (tb *transactionBuilder) FundSiacoins(amount types.Currency) error { tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock() // Collect a value-sorted set of siacoin outputs. var so sortedOutputs for scoid, sco := range tb.wallet.siacoinOutputs { so.ids = append(so.ids, scoid) so.outputs = append(so.outputs, sco) } // Add all of the unconfirmed outputs as well. for _, upt := range tb.wallet.unconfirmedProcessedTransactions { for i, sco := range upt.Transaction.SiacoinOutputs { // Determine if the output belongs to the wallet. _, exists := tb.wallet.keys[sco.UnlockHash] if !exists { continue } so.ids = append(so.ids, upt.Transaction.SiacoinOutputID(uint64(i))) so.outputs = append(so.outputs, sco) } } sort.Sort(sort.Reverse(so)) // Create and fund a parent transaction that will add the correct amount of // siacoins to the transaction. var fund types.Currency // potentialFund tracks the balance of the wallet including outputs that // have been spent in other unconfirmed transactions recently. This is to // provide the user with a more useful error message in the event that they // are overspending. var potentialFund types.Currency parentTxn := types.Transaction{} var spentScoids []types.SiacoinOutputID for i := range so.ids { scoid := so.ids[i] sco := so.outputs[i] // Check that this output has not recently been spent by the wallet. spendHeight := tb.wallet.spentOutputs[types.OutputID(scoid)] // Prevent an underflow error. allowedHeight := tb.wallet.consensusSetHeight - RespendTimeout if tb.wallet.consensusSetHeight < RespendTimeout { allowedHeight = 0 } if spendHeight > allowedHeight { potentialFund = potentialFund.Add(sco.Value) continue } outputUnlockConditions := tb.wallet.keys[sco.UnlockHash].UnlockConditions if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { continue } // Add a siacoin input for this output. sci := types.SiacoinInput{ ParentID: scoid, UnlockConditions: outputUnlockConditions, } parentTxn.SiacoinInputs = append(parentTxn.SiacoinInputs, sci) spentScoids = append(spentScoids, scoid) // Add the output to the total fund fund = fund.Add(sco.Value) potentialFund = potentialFund.Add(sco.Value) if fund.Cmp(amount) >= 0 { break } } if potentialFund.Cmp(amount) >= 0 && fund.Cmp(amount) < 0 { return modules.ErrPotentialDoubleSpend } if fund.Cmp(amount) < 0 { return modules.ErrLowBalance } // Create and add the output that will be used to fund the standard // transaction. parentUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } exactOutput := types.SiacoinOutput{ Value: amount, UnlockHash: parentUnlockConditions.UnlockHash(), } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, exactOutput) // Create a refund output if needed. if amount.Cmp(fund) != 0 { refundUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } refundOutput := types.SiacoinOutput{ Value: fund.Sub(amount), UnlockHash: refundUnlockConditions.UnlockHash(), } parentTxn.SiacoinOutputs = append(parentTxn.SiacoinOutputs, refundOutput) } // Sign all of the inputs to the parent trancstion. for _, sci := range parentTxn.SiacoinInputs { _, err := addSignatures(&parentTxn, types.FullCoveredFields, sci.UnlockConditions, crypto.Hash(sci.ParentID), tb.wallet.keys[sci.UnlockConditions.UnlockHash()]) if err != nil { return err } } // Mark the parent output as spent. Must be done after the transaction is // finished because otherwise the txid and output id will change. tb.wallet.spentOutputs[types.OutputID(parentTxn.SiacoinOutputID(0))] = tb.wallet.consensusSetHeight // Add the exact output. newInput := types.SiacoinInput{ ParentID: parentTxn.SiacoinOutputID(0), UnlockConditions: parentUnlockConditions, } tb.newParents = append(tb.newParents, len(tb.parents)) tb.parents = append(tb.parents, parentTxn) tb.siacoinInputs = append(tb.siacoinInputs, len(tb.transaction.SiacoinInputs)) tb.transaction.SiacoinInputs = append(tb.transaction.SiacoinInputs, newInput) // Mark all outputs that were spent as spent. for _, scoid := range spentScoids { tb.wallet.spentOutputs[types.OutputID(scoid)] = tb.wallet.consensusSetHeight } return nil }
// FundSiafunds will add a siafund input of exaclty 'amount' to the // transaction. A parent transaction may be needed to achieve an input with the // correct value. The siafund input will not be signed until 'Sign' is called // on the transaction builder. func (tb *transactionBuilder) FundSiafunds(amount types.Currency) error { tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock() // Create and fund a parent transaction that will add the correct amount of // siafunds to the transaction. var fund types.Currency var potentialFund types.Currency parentTxn := types.Transaction{} var spentSfoids []types.SiafundOutputID for sfoid, sfo := range tb.wallet.siafundOutputs { // Check that this output has not recently been spent by the wallet. spendHeight := tb.wallet.spentOutputs[types.OutputID(sfoid)] // Prevent an underflow error. allowedHeight := tb.wallet.consensusSetHeight - RespendTimeout if tb.wallet.consensusSetHeight < RespendTimeout { allowedHeight = 0 } if spendHeight > allowedHeight { potentialFund = potentialFund.Add(sfo.Value) continue } outputUnlockConditions := tb.wallet.keys[sfo.UnlockHash].UnlockConditions if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { continue } // Add a siafund input for this output. parentClaimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } sfi := types.SiafundInput{ ParentID: sfoid, UnlockConditions: outputUnlockConditions, ClaimUnlockHash: parentClaimUnlockConditions.UnlockHash(), } parentTxn.SiafundInputs = append(parentTxn.SiafundInputs, sfi) spentSfoids = append(spentSfoids, sfoid) // Add the output to the total fund fund = fund.Add(sfo.Value) potentialFund = potentialFund.Add(sfo.Value) if fund.Cmp(amount) >= 0 { break } } if potentialFund.Cmp(amount) >= 0 && fund.Cmp(amount) < 0 { return modules.ErrPotentialDoubleSpend } if fund.Cmp(amount) < 0 { return modules.ErrLowBalance } // Create and add the output that will be used to fund the standard // transaction. parentUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } exactOutput := types.SiafundOutput{ Value: amount, UnlockHash: parentUnlockConditions.UnlockHash(), } parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, exactOutput) // Create a refund output if needed. if amount.Cmp(fund) != 0 { refundUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } refundOutput := types.SiafundOutput{ Value: fund.Sub(amount), UnlockHash: refundUnlockConditions.UnlockHash(), } parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, refundOutput) } // Sign all of the inputs to the parent trancstion. for _, sfi := range parentTxn.SiafundInputs { _, err := addSignatures(&parentTxn, types.FullCoveredFields, sfi.UnlockConditions, crypto.Hash(sfi.ParentID), tb.wallet.keys[sfi.UnlockConditions.UnlockHash()]) if err != nil { return err } } // Add the exact output. claimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } newInput := types.SiafundInput{ ParentID: parentTxn.SiafundOutputID(0), UnlockConditions: parentUnlockConditions, ClaimUnlockHash: claimUnlockConditions.UnlockHash(), } tb.newParents = append(tb.newParents, len(tb.parents)) tb.parents = append(tb.parents, parentTxn) tb.siafundInputs = append(tb.siafundInputs, len(tb.transaction.SiafundInputs)) tb.transaction.SiafundInputs = append(tb.transaction.SiafundInputs, newInput) // Mark all outputs that were spent as spent. for _, sfoid := range spentSfoids { tb.wallet.spentOutputs[types.OutputID(sfoid)] = tb.wallet.consensusSetHeight } return nil }
// consensusSetHash returns the Merkle root of the current state of consensus. func (cs *ConsensusSet) consensusSetHash() crypto.Hash { // Check is too slow to be done on a full node. if build.Release == "standard" { return crypto.Hash{} } // Items of interest: // 1. genesis block // 3. current height // 4. current target // 5. current depth // 6. current path + diffs // (7) earliest allowed timestamp of next block // 8. unspent siacoin outputs, sorted by id. // 9. open file contracts, sorted by id. // 10. unspent siafund outputs, sorted by id. // 11. delayed siacoin outputs, sorted by height, then sorted by id. // 12. siafund pool // Create a slice of hashes representing all items of interest. tree := crypto.NewTree() tree.PushObject(cs.blockRoot.Block) tree.PushObject(cs.height()) tree.PushObject(cs.currentProcessedBlock().ChildTarget) tree.PushObject(cs.currentProcessedBlock().Depth) // tree.PushObject(cs.earliestChildTimestamp(cs.currentProcessedBlock())) // Add all the blocks in the current path TODO: along with their diffs. for i := 0; i < int(cs.db.pathHeight()); i++ { tree.PushObject(cs.db.getPath(types.BlockHeight(i))) } // Add all of the siacoin outputs, sorted by id. var openSiacoinOutputs crypto.HashSlice cs.db.forEachSiacoinOutputs(func(scoid types.SiacoinOutputID, sco types.SiacoinOutput) { openSiacoinOutputs = append(openSiacoinOutputs, crypto.Hash(scoid)) }) sort.Sort(openSiacoinOutputs) for _, id := range openSiacoinOutputs { sco := cs.db.getSiacoinOutputs(types.SiacoinOutputID(id)) tree.PushObject(id) tree.PushObject(sco) } // Add all of the file contracts, sorted by id. var openFileContracts crypto.HashSlice cs.db.forEachFileContracts(func(fcid types.FileContractID, fc types.FileContract) { openFileContracts = append(openFileContracts, crypto.Hash(fcid)) }) sort.Sort(openFileContracts) for _, id := range openFileContracts { // Sanity Check - file contract should exist. fc := cs.db.getFileContracts(types.FileContractID(id)) tree.PushObject(id) tree.PushObject(fc) } // Add all of the siafund outputs, sorted by id. var openSiafundOutputs crypto.HashSlice cs.db.forEachSiafundOutputs(func(sfoid types.SiafundOutputID, sfo types.SiafundOutput) { openSiafundOutputs = append(openSiafundOutputs, crypto.Hash(sfoid)) }) sort.Sort(openSiafundOutputs) for _, id := range openSiafundOutputs { sco := cs.db.getSiafundOutputs(types.SiafundOutputID(id)) tree.PushObject(id) tree.PushObject(sco) } // Get the set of delayed siacoin outputs, sorted by maturity height then // sorted by id and add them. for i := cs.height() + 1; i <= cs.height()+types.MaturityDelay; i++ { var delayedSiacoinOutputs crypto.HashSlice if cs.db.inDelayedSiacoinOutputs(i) { cs.db.forEachDelayedSiacoinOutputsHeight(i, func(id types.SiacoinOutputID, output types.SiacoinOutput) { delayedSiacoinOutputs = append(delayedSiacoinOutputs, crypto.Hash(id)) }) } sort.Sort(delayedSiacoinOutputs) for _, delayedSiacoinOutputID := range delayedSiacoinOutputs { delayedSiacoinOutput := cs.db.getDelayedSiacoinOutputs(i, types.SiacoinOutputID(delayedSiacoinOutputID)) tree.PushObject(delayedSiacoinOutput) tree.PushObject(delayedSiacoinOutputID) } } // Add the siafund pool var siafundPool types.Currency err := cs.db.Update(func(tx *bolt.Tx) error { siafundPool = getSiafundPool(tx) return nil }) if err != nil { panic(err) } tree.PushObject(siafundPool) return tree.Root() }
// explorerHashHandler handles GET requests to /explorer/hash/:hash. func (api *API) explorerHashHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) { // Scan the hash as a hash. If that fails, try scanning the hash as an // address. hash, err := scanHash(ps.ByName("hash")) if err != nil { addr, err := scanAddress(ps.ByName("hash")) if err != nil { WriteError(w, Error{err.Error()}, http.StatusBadRequest) return } hash = crypto.Hash(addr) } // TODO: lookups on the zero hash are too expensive to allow. Need a // better way to handle this case. if hash == (crypto.Hash{}) { WriteError(w, Error{"can't lookup the empty unlock hash"}, http.StatusBadRequest) return } // Try the hash as a block id. block, height, exists := api.explorer.Block(types.BlockID(hash)) if exists { WriteJSON(w, ExplorerHashGET{ HashType: "blockid", Block: api.buildExplorerBlock(height, block), }) return } // Try the hash as a transaction id. block, height, exists = api.explorer.Transaction(types.TransactionID(hash)) if exists { var txn types.Transaction for _, t := range block.Transactions { if t.ID() == types.TransactionID(hash) { txn = t } } WriteJSON(w, ExplorerHashGET{ HashType: "transactionid", Transaction: api.buildExplorerTransaction(height, block.ID(), txn), }) return } // Try the hash as a siacoin output id. txids := api.explorer.SiacoinOutputID(types.SiacoinOutputID(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "siacoinoutputid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as a file contract id. txids = api.explorer.FileContractID(types.FileContractID(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "filecontractid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as a siafund output id. txids = api.explorer.SiafundOutputID(types.SiafundOutputID(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "siafundoutputid", Blocks: blocks, Transactions: txns, }) return } // Try the hash as an unlock hash. Unlock hash is checked last because // unlock hashes do not have collision-free guarantees. Someone can create // an unlock hash that collides with another object id. They will not be // able to use the unlock hash, but they can disrupt the explorer. This is // handled by checking the unlock hash last. Anyone intentionally creating // a colliding unlock hash (such a collision can only happen if done // intentionally) will be unable to find their unlock hash in the // blockchain through the explorer hash lookup. txids = api.explorer.UnlockHash(types.UnlockHash(hash)) if len(txids) != 0 { txns, blocks := api.buildTransactionSet(txids) WriteJSON(w, ExplorerHashGET{ HashType: "unlockhash", Blocks: blocks, Transactions: txns, }) return } // Hash not found, return an error. WriteError(w, Error{"unrecognized hash used as input to /explorer/hash"}, http.StatusBadRequest) }
// 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 }
// 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") } }
// Sign will sign any inputs added by 'FundSiacoins' or 'FundSiafunds' and // return a transaction set that contains all parents prepended to the // transaction. If more fields need to be added, a new transaction builder will // need to be created. // // If the whole transaction flag is set to true, then the whole transaction // flag will be set in the covered fields object. If the whole transaction flag // is set to false, then the covered fields object will cover all fields that // have already been added to the transaction, but will also leave room for // more fields to be added. func (tb *transactionBuilder) Sign(wholeTransaction bool) ([]types.Transaction, error) { // Create the coveredfields struct. txn := tb.transaction 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.FileContractRevisions { coveredFields.FileContractRevisions = append(coveredFields.FileContractRevisions, uint64(i)) } for i := range txn.StorageProofs { coveredFields.StorageProofs = append(coveredFields.StorageProofs, uint64(i)) } for i := range txn.SiafundInputs { coveredFields.SiafundInputs = append(coveredFields.SiafundInputs, uint64(i)) } for i := range txn.SiafundOutputs { coveredFields.SiafundOutputs = append(coveredFields.SiafundOutputs, uint64(i)) } for i := range txn.ArbitraryData { coveredFields.ArbitraryData = append(coveredFields.ArbitraryData, uint64(i)) } } // TransactionSignatures don't get covered by the 'WholeTransaction' flag, // and must be covered manually. for i := range txn.TransactionSignatures { coveredFields.TransactionSignatures = append(coveredFields.TransactionSignatures, uint64(i)) } // For each siacoin input in the transaction that we added, provide a // signature. lockID := tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock(lockID) for _, inputIndex := range tb.siacoinInputs { input := txn.SiacoinInputs[inputIndex] key := tb.wallet.keys[input.UnlockConditions.UnlockHash()] err := addSignatures(&txn, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key) if err != nil { return nil, err } } for _, inputIndex := range tb.siafundInputs { input := txn.SiafundInputs[inputIndex] key := tb.wallet.keys[input.UnlockConditions.UnlockHash()] err := addSignatures(&txn, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key) if err != nil { return nil, err } } // Get the transaction set and delete the transaction from the registry. txnSet := append(tb.parents, txn) return txnSet, nil }
// FundSiafunds will add a siafund input of exaclty 'amount' to the // transaction. A parent transaction may be needed to achieve an input with the // correct value. The siafund input will not be signed until 'Sign' is called // on the transaction builder. // // TODO: The implementation of FundSiacoins is known to have quirks/bugs // (non-fatal), and has diverged from the implementation of FundSiacoins. The // implementations should be converged once again. func (tb *transactionBuilder) FundSiafunds(amount types.Currency) error { lockID := tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock(lockID) // Create and fund a parent transaction that will add the correct amount of // siafunds to the transaction. var fund types.Currency parentTxn := types.Transaction{} for scoid, sco := range tb.wallet.siafundOutputs { // Check that this output has not recently been spent by the wallet. spendHeight := tb.wallet.spentOutputs[types.OutputID(scoid)] if spendHeight > tb.wallet.consensusSetHeight-RespendTimeout { continue } outputUnlockConditions := tb.wallet.keys[sco.UnlockHash].unlockConditions if tb.wallet.consensusSetHeight < outputUnlockConditions.Timelock { continue } // Mark the output as spent. tb.wallet.spentOutputs[types.OutputID(scoid)] = tb.wallet.consensusSetHeight // Add a siafund input for this output. parentClaimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } sci := types.SiafundInput{ ParentID: scoid, UnlockConditions: outputUnlockConditions, ClaimUnlockHash: parentClaimUnlockConditions.UnlockHash(), } parentTxn.SiafundInputs = append(parentTxn.SiafundInputs, sci) // Add the output to the total fund fund = fund.Add(sco.Value) if fund.Cmp(amount) >= 0 { break } } // Create and add the output that will be used to fund the standard // transaction. parentUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } exactOutput := types.SiafundOutput{ Value: amount, UnlockHash: parentUnlockConditions.UnlockHash(), } parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, exactOutput) // Create a refund output if needed. if amount.Cmp(fund) != 0 { refundUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } refundOutput := types.SiafundOutput{ Value: fund.Sub(amount), UnlockHash: refundUnlockConditions.UnlockHash(), } parentTxn.SiafundOutputs = append(parentTxn.SiafundOutputs, refundOutput) } // Sign all of the inputs to the parent trancstion. for _, sfi := range parentTxn.SiafundInputs { err := addSignatures(&parentTxn, types.FullCoveredFields, sfi.UnlockConditions, crypto.Hash(sfi.ParentID), tb.wallet.keys[sfi.UnlockConditions.UnlockHash()]) if err != nil { return err } } // Add the exact output. claimUnlockConditions, err := tb.wallet.nextPrimarySeedAddress() if err != nil { return err } newInput := types.SiafundInput{ ParentID: parentTxn.SiafundOutputID(0), UnlockConditions: parentUnlockConditions, ClaimUnlockHash: claimUnlockConditions.UnlockHash(), } tb.parents = append(tb.parents, parentTxn) tb.siafundInputs = append(tb.siafundInputs, len(tb.transaction.SiafundInputs)) tb.transaction.SiafundInputs = append(tb.transaction.SiafundInputs, newInput) return 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) } }
// 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 }
// 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 }
// Sign will sign any inputs added by 'FundSiacoins' or 'FundSiafunds' and // return a transaction set that contains all parents prepended to the // transaction. If more fields need to be added, a new transaction builder will // need to be created. // // If the whole transaction flag is set to true, then the whole transaction // flag will be set in the covered fields object. If the whole transaction flag // is set to false, then the covered fields object will cover all fields that // have already been added to the transaction, but will also leave room for // more fields to be added. // // Sign should not be called more than once. If, for some reason, there is an // error while calling Sign, the builder should be dropped. func (tb *transactionBuilder) Sign(wholeTransaction bool) ([]types.Transaction, error) { if tb.signed { return nil, errBuilderAlreadySigned } // Create the coveredfields struct. var coveredFields types.CoveredFields if wholeTransaction { coveredFields = types.CoveredFields{WholeTransaction: true} } else { for i := range tb.transaction.MinerFees { coveredFields.MinerFees = append(coveredFields.MinerFees, uint64(i)) } for i := range tb.transaction.SiacoinInputs { coveredFields.SiacoinInputs = append(coveredFields.SiacoinInputs, uint64(i)) } for i := range tb.transaction.SiacoinOutputs { coveredFields.SiacoinOutputs = append(coveredFields.SiacoinOutputs, uint64(i)) } for i := range tb.transaction.FileContracts { coveredFields.FileContracts = append(coveredFields.FileContracts, uint64(i)) } for i := range tb.transaction.FileContractRevisions { coveredFields.FileContractRevisions = append(coveredFields.FileContractRevisions, uint64(i)) } for i := range tb.transaction.StorageProofs { coveredFields.StorageProofs = append(coveredFields.StorageProofs, uint64(i)) } for i := range tb.transaction.SiafundInputs { coveredFields.SiafundInputs = append(coveredFields.SiafundInputs, uint64(i)) } for i := range tb.transaction.SiafundOutputs { coveredFields.SiafundOutputs = append(coveredFields.SiafundOutputs, uint64(i)) } for i := range tb.transaction.ArbitraryData { coveredFields.ArbitraryData = append(coveredFields.ArbitraryData, uint64(i)) } } // TransactionSignatures don't get covered by the 'WholeTransaction' flag, // and must be covered manually. for i := range tb.transaction.TransactionSignatures { coveredFields.TransactionSignatures = append(coveredFields.TransactionSignatures, uint64(i)) } // For each siacoin input in the transaction that we added, provide a // signature. tb.wallet.mu.Lock() defer tb.wallet.mu.Unlock() for _, inputIndex := range tb.siacoinInputs { input := tb.transaction.SiacoinInputs[inputIndex] key := tb.wallet.keys[input.UnlockConditions.UnlockHash()] newSigIndices, err := addSignatures(&tb.transaction, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key) if err != nil { return nil, err } tb.transactionSignatures = append(tb.transactionSignatures, newSigIndices...) tb.signed = true // Signed is set to true after one successful signature to indicate that future signings can cause issues. } for _, inputIndex := range tb.siafundInputs { input := tb.transaction.SiafundInputs[inputIndex] key := tb.wallet.keys[input.UnlockConditions.UnlockHash()] newSigIndices, err := addSignatures(&tb.transaction, coveredFields, input.UnlockConditions, crypto.Hash(input.ParentID), key) if err != nil { return nil, err } tb.transactionSignatures = append(tb.transactionSignatures, newSigIndices...) tb.signed = true // Signed is set to true after one successful signature to indicate that future signings can cause issues. } // Get the transaction set and delete the transaction from the registry. txnSet := append(tb.parents, tb.transaction) return txnSet, nil }
// addNewSFOutput does the same thing as addNewOutput does, except for siafunds func (tx *boltTx) addNewSFOutput(outputID types.SiafundOutputID, txid types.TransactionID) { otx := outputTransactions{txid, types.TransactionID{}} tx.addNewHash("SiafundOutputs", hashFundOutputID, crypto.Hash(outputID), otx) }
// 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 }
// addBlockDB parses a block and adds it to the database func (e *Explorer) addBlockDB(b types.Block) error { // Special case for the genesis block, which does not have a // valid parent, and for testing, as tests will not always use // blocks in consensus var blocktarget types.Target if b.ID() == e.genesisBlockID { blocktarget = types.RootDepth e.blockchainHeight = 0 } else { var exists bool blocktarget, exists = e.cs.ChildTarget(b.ParentID) if build.DEBUG { if build.Release == "testing" { blocktarget = types.RootDepth } else if !exists { panic("Applied block not in consensus") } } } // Check if the block exsts. var exists bool dbErr := e.db.View(func(tx *bolt.Tx) error { id := b.ID() block := tx.Bucket([]byte("Blocks")).Get(id[:]) exists = block != nil return nil }) if dbErr != nil { return dbErr } if exists { return nil } tx, err := newBoltTx(e.db) if err != nil { return err } defer tx.Rollback() // Construct the struct that will be inside the heights map blockStruct := blockData{ Block: b, Height: e.blockchainHeight, } tx.addNewHash("Blocks", hashBlock, crypto.Hash(b.ID()), blockStruct) bSum := modules.ExplorerBlockData{ ID: b.ID(), Timestamp: b.Timestamp, Target: blocktarget, Size: uint64(len(encoding.Marshal(b))), } tx.putObject("Heights", e.blockchainHeight, bSum) tx.putObject("Hashes", crypto.Hash(b.ID()), hashBlock) // Insert the miner payouts as new outputs for i, payout := range b.MinerPayouts { tx.addAddress(payout.UnlockHash, types.TransactionID(b.ID())) tx.addNewOutput(b.MinerPayoutID(uint64(i)), types.TransactionID(b.ID())) } // Insert each transaction for i, txn := range b.Transactions { tx.addNewHash("Transactions", hashTransaction, crypto.Hash(txn.ID()), txInfo{b.ID(), i}) tx.addTransaction(txn) } return tx.commit() }
// addTransaction is called from addBlockDB, and delegates the adding // of information to the database to the functions defined above func (tx *boltTx) addTransaction(txn types.Transaction) { // Store this for quick lookup txid := txn.ID() // Append each input to the list of modifications for _, input := range txn.SiacoinInputs { tx.addAddress(input.UnlockConditions.UnlockHash(), txid) tx.addSiacoinInput(input.ParentID, txid) } // Handle all the transaction outputs for i, output := range txn.SiacoinOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(txn.SiacoinOutputID(uint64(i)), txid) } // Handle each file contract individually for i, contract := range txn.FileContracts { fcid := txn.FileContractID(uint64(i)) tx.addNewHash("FileContracts", hashFilecontract, crypto.Hash(fcid), fcInfo{ Contract: txid, }) for j, output := range contract.ValidProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(fcid.StorageProofOutputID(true, uint64(j)), txid) } for j, output := range contract.MissedProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(fcid.StorageProofOutputID(false, uint64(j)), txid) } tx.addAddress(contract.UnlockHash, txid) } // Update the list of revisions for _, revision := range txn.FileContractRevisions { tx.addFcRevision(revision.ParentID, txid) // Note the old outputs will still be there in the // database. This is to provide information to the // people who may just need it. for i, output := range revision.NewValidProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(revision.ParentID.StorageProofOutputID(true, uint64(i)), txid) } for i, output := range revision.NewMissedProofOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewOutput(revision.ParentID.StorageProofOutputID(false, uint64(i)), txid) } tx.addAddress(revision.NewUnlockHash, txid) } // Update the list of storage proofs for _, proof := range txn.StorageProofs { tx.addFcProof(proof.ParentID, txid) } // Append all the siafund inputs to the modification list for _, input := range txn.SiafundInputs { tx.addSiafundInput(input.ParentID, txid) } // Handle all the siafund outputs for i, output := range txn.SiafundOutputs { tx.addAddress(output.UnlockHash, txid) tx.addNewSFOutput(txn.SiafundOutputID(uint64(i)), txid) } tx.putObject("Hashes", txid, hashTransaction) }
// consensusSetHash returns the Merkle root of the current state of consensus. func (cs *State) consensusSetHash() crypto.Hash { // Items of interest: // 1. genesis block // 3. current height // 4. current target // 5. current depth // 6. earliest allowed timestamp of next block // 7. current path, ordered by height. // 8. unspent siacoin outputs, sorted by id. // 9. open file contracts, sorted by id. // 10. unspent siafund outputs, sorted by id. // 11. delayed siacoin outputs, sorted by height, then sorted by id. // TODO: Add the diff set ? // Create a slice of hashes representing all items of interest. tree := crypto.NewTree() tree.PushObject(cs.blockRoot.block) tree.PushObject(cs.height()) tree.PushObject(cs.currentBlockNode().childTarget) tree.PushObject(cs.currentBlockNode().depth) tree.PushObject(cs.currentBlockNode().earliestChildTimestamp()) // Add all the blocks in the current path. for i := 0; i < len(cs.currentPath); i++ { tree.PushObject(cs.currentPath[types.BlockHeight(i)]) } // Add all of the siacoin outputs, sorted by id. var openSiacoinOutputs crypto.HashSlice for siacoinOutputID, _ := range cs.siacoinOutputs { openSiacoinOutputs = append(openSiacoinOutputs, crypto.Hash(siacoinOutputID)) } sort.Sort(openSiacoinOutputs) for _, id := range openSiacoinOutputs { sco, exists := cs.siacoinOutputs[types.SiacoinOutputID(id)] if !exists { panic("trying to push nonexistent siacoin output") } tree.PushObject(id) tree.PushObject(sco) } // Add all of the file contracts, sorted by id. var openFileContracts crypto.HashSlice for fileContractID, _ := range cs.fileContracts { openFileContracts = append(openFileContracts, crypto.Hash(fileContractID)) } sort.Sort(openFileContracts) for _, id := range openFileContracts { // Sanity Check - file contract should exist. fc, exists := cs.fileContracts[types.FileContractID(id)] if !exists { panic("trying to push a nonexistent file contract") } tree.PushObject(id) tree.PushObject(fc) } // Add all of the siafund outputs, sorted by id. var openSiafundOutputs crypto.HashSlice for siafundOutputID, _ := range cs.siafundOutputs { openSiafundOutputs = append(openSiafundOutputs, crypto.Hash(siafundOutputID)) } sort.Sort(openSiafundOutputs) for _, id := range openSiafundOutputs { sco, exists := cs.siafundOutputs[types.SiafundOutputID(id)] if !exists { panic("trying to push nonexistent siafund output") } tree.PushObject(id) tree.PushObject(sco) } // Get the set of delayed siacoin outputs, sorted by maturity height then // sorted by id and add them. for i := cs.height() + 1; i <= cs.height()+types.MaturityDelay; i++ { var delayedSiacoinOutputs crypto.HashSlice for id := range cs.delayedSiacoinOutputs[i] { delayedSiacoinOutputs = append(delayedSiacoinOutputs, crypto.Hash(id)) } sort.Sort(delayedSiacoinOutputs) for _, delayedSiacoinOutputID := range delayedSiacoinOutputs { delayedSiacoinOutput, exists := cs.delayedSiacoinOutputs[i][types.SiacoinOutputID(delayedSiacoinOutputID)] if !exists { panic("trying to push nonexistent delayed siacoin output") } tree.PushObject(delayedSiacoinOutput) tree.PushObject(delayedSiacoinOutputID) } } return tree.Root() }