// validUnconfirmedTransaction checks that the transaction would be valid in a // block that contained all of the other unconfirmed transactions. func (tp *TransactionPool) validUnconfirmedTransaction(t types.Transaction) (err error) { // Check that the transaction follows 'Standard.md' guidelines. err = tp.IsStandardTransaction(t) if err != nil { return } // StandaloneValid will check things like signatures and properties that // should be inherent to the transaction. (storage proof rules, etc.) err = t.StandaloneValid(tp.consensusSetHeight) if err != nil { return } // Check the validity of the componenets in the context of the confirmed // and unconfirmed set. err = tp.validUnconfirmedSiacoins(t) if err != nil { return } err = tp.validUnconfirmedStorageProofs(t) if err != nil { return } err = tp.validUnconfirmedFileContractRevisions(t) if err != nil { return } err = tp.validUnconfirmedSiafunds(t) if err != nil { return } return }
// validTransaction checks that all fields are valid within the current // consensus state. If not an error is returned. func validTransaction(tx *bolt.Tx, t types.Transaction) error { // StandaloneValid will check things like signatures and properties that // should be inherent to the transaction. (storage proof rules, etc.) err := t.StandaloneValid(blockHeight(tx)) if err != nil { return err } // Check that each portion of the transaction is legal given the current // consensus set. err = validSiacoins(tx, t) if err != nil { return err } err = validStorageProofs(tx, t) if err != nil { return err } err = validFileContractRevisions(tx, t) if err != nil { return err } err = validSiafunds(tx, t) if err != nil { return err } return nil }
// negotiateRevision sends a revision and actions to the host for approval, // completing one iteration of the revision loop. func negotiateRevision(conn net.Conn, rev types.FileContractRevision, secretKey crypto.SecretKey) (types.Transaction, error) { // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(rev.ParentID), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // renter key is always first -- see formContract }}, } // sign the transaction encodedSig, _ := crypto.SignHash(signedTxn.SigHash(0), secretKey) // no error possible signedTxn.TransactionSignatures[0].Signature = encodedSig[:] // send the revision if err := encoding.WriteObject(conn, rev); err != nil { return types.Transaction{}, errors.New("couldn't send revision: " + err.Error()) } // read acceptance if err := modules.ReadNegotiationAcceptance(conn); err != nil { return types.Transaction{}, errors.New("host did not accept revision: " + err.Error()) } // send the new transaction signature if err := encoding.WriteObject(conn, signedTxn.TransactionSignatures[0]); err != nil { return types.Transaction{}, errors.New("couldn't send transaction signature: " + err.Error()) } // read the host's acceptance and transaction signature // NOTE: if the host sends ErrStopResponse, we should continue processing // the revision, but return the error anyway. responseErr := modules.ReadNegotiationAcceptance(conn) if responseErr != nil && responseErr != modules.ErrStopResponse { return types.Transaction{}, errors.New("host did not accept transaction signature: " + responseErr.Error()) } var hostSig types.TransactionSignature if err := encoding.ReadObject(conn, &hostSig, 16e3); err != nil { return types.Transaction{}, errors.New("couldn't read host's signature: " + err.Error()) } // add the signature to the transaction and verify it // NOTE: we can fake the blockheight here because it doesn't affect // verification; it just needs to be above the fork height and below the // contract expiration (which was checked earlier). verificationHeight := rev.NewWindowStart - 1 signedTxn.TransactionSignatures = append(signedTxn.TransactionSignatures, hostSig) if err := signedTxn.StandaloneValid(verificationHeight); err != nil { return types.Transaction{}, err } // if the host sent ErrStopResponse, return it return signedTxn, responseErr }
// verifyKeysSiag_1_0 is a copy-pasted version of the verifyKeys method // from siag 1.0. func verifyKeysSiag_1_0(uc types.UnlockConditions, folder string, keyname string) error { keysRequired := uc.SignaturesRequired totalKeys := uint64(len(uc.PublicKeys)) loadedKeys := make([]KeyPairSiag_1_0, totalKeys) for i := 0; i < len(loadedKeys); i++ { err := encoding.ReadFile(filepath.Join(folder, keyname+"_Key"+strconv.Itoa(i)+".siakey"), &loadedKeys[i]) if err != nil { return err } } for _, loadedKey := range loadedKeys { if loadedKey.UnlockConditions.UnlockHash() != uc.UnlockHash() { return errors.New("ErrCorruptedKey") } } txn := types.Transaction{ SiafundInputs: []types.SiafundInput{ types.SiafundInput{ UnlockConditions: loadedKeys[0].UnlockConditions, }, }, } var i uint64 for i != totalKeys { if i+keysRequired > totalKeys { i = totalKeys - keysRequired } var j uint64 for j < keysRequired { txn.TransactionSignatures = append(txn.TransactionSignatures, types.TransactionSignature{ PublicKeyIndex: i, CoveredFields: types.CoveredFields{WholeTransaction: true}, }) sigHash := txn.SigHash(int(j)) sig, err := crypto.SignHash(sigHash, loadedKeys[i].SecretKey) if err != nil { return err } txn.TransactionSignatures[j].Signature = sig[:] i++ j++ } err := txn.StandaloneValid(0) if err != nil { return err } txn.TransactionSignatures = nil } return nil }
// reviseObligation takes a file contract revision + transaction and applies it // to an existing obligation. func (h *Host) reviseObligation(revisionTransaction types.Transaction) { // Sanity checks - there should be exactly one revision in the transaction, // and that revision should correspond to a known obligation. fcrlen := len(revisionTransaction.FileContractRevisions) if fcrlen != 1 { h.log.Critical("reviseObligation: revisionTransaction has the wrong number of revisions:", fcrlen) return } obligation, exists := h.obligationsByID[revisionTransaction.FileContractRevisions[0].ParentID] if !exists { h.log.Critical("reviseObligation: revisionTransaction has no corresponding obligation") return } // Expensive Sanity check - obligation being added should have a valid tranasction. if build.DEBUG { err := revisionTransaction.StandaloneValid(h.blockHeight) if err != nil { h.log.Critical("invalid transaction is being added in an obligation") } } // Update the host's statistics. h.spaceRemaining += int64(obligation.fileSize()) h.spaceRemaining -= int64(revisionTransaction.FileContractRevisions[0].NewFileSize) h.anticipatedRevenue = h.anticipatedRevenue.Sub(obligation.value()) h.anticipatedRevenue = h.anticipatedRevenue.Add(revisionTransaction.FileContractRevisions[0].NewValidProofOutputs[1].Value) // The host needs to verify that the revision transaction made it into the // blockchain. h.addActionItem(h.blockHeight+resubmissionTimeout, obligation) // Add the revision to the obligation obligation.RevisionTransaction = revisionTransaction obligation.RevisionConfirmed = false err := obligation.isSane() if err != nil { h.log.Critical("reviseObligation: obligation is not sane: " + err.Error()) } err = h.save() if err != nil { h.log.Println("WARN: failed to save host:", err) } }
// VerifyFileContractRevisionTransactionSignatures checks that the signatures // on a file contract revision are valid and cover the right fields. func VerifyFileContractRevisionTransactionSignatures(fcr types.FileContractRevision, tsigs []types.TransactionSignature, height types.BlockHeight) error { if len(tsigs) != 2 { return ErrRevisionSigCount } for _, tsig := range tsigs { // The transaction needs to be malleable so that miner fees can be // added. If the whole transaction is covered, it is doomed to have no // fees. if tsig.CoveredFields.WholeTransaction { return ErrRevisionCoveredFields } } txn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{fcr}, TransactionSignatures: tsigs, } // Check that the signatures verify. This will also check that the covered // fields object is not over-aggressive, because if the object is pointing // to elements that haven't been added to the transaction, verification // will fail. return txn.StandaloneValid(height) }
func TestReviseContract(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() ct, err := newContractorTester("TestReviseContract") if err != nil { t.Fatal(err) } // get an address ourAddr, err := ct.wallet.NextAddress() if err != nil { t.Fatal(err) } // generate keys sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } renterPubKey := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterPubKey, renterPubKey}, SignaturesRequired: 1, } // create file contract payout := types.NewCurrency64(1e16) fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: 100, WindowEnd: 1000, Payout: payout, UnlockHash: uc.UnlockHash(), RevisionNumber: 0, } // outputs need account for tax fc.ValidProofOutputs = []types.SiacoinOutput{ {Value: types.PostTax(ct.contractor.blockHeight, payout), UnlockHash: ourAddr.UnlockHash()}, {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, // no collateral } fc.MissedProofOutputs = []types.SiacoinOutput{ // same as above fc.ValidProofOutputs[0], // goes to the void, not the hostdb {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, } txnBuilder := ct.wallet.StartTransaction() err = txnBuilder.FundSiacoins(fc.Payout) if err != nil { t.Fatal(err) } txnBuilder.AddFileContract(fc) signedTxnSet, err := txnBuilder.Sign(true) if err != nil { t.Fatal(err) } // submit contract err = ct.tpool.AcceptTransactionSet(signedTxnSet) if err != nil { t.Fatal(err) } // create revision fcid := signedTxnSet[len(signedTxnSet)-1].FileContractID(0) rev := types.FileContractRevision{ ParentID: fcid, UnlockConditions: uc, NewFileSize: 10, NewWindowStart: 100, NewWindowEnd: 1000, NewRevisionNumber: 1, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, } // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(fcid), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // hostdb key is always first -- see negotiateContract }}, } // sign the transaction encodedSig, err := crypto.SignHash(signedTxn.SigHash(0), sk) if err != nil { t.Fatal(err) } signedTxn.TransactionSignatures[0].Signature = encodedSig[:] err = signedTxn.StandaloneValid(ct.contractor.blockHeight) if err != nil { t.Fatal(err) } // submit revision err = ct.tpool.AcceptTransactionSet([]types.Transaction{signedTxn}) if err != nil { t.Fatal(err) } }
// revise revises fc to cover piece and uploads both the revision and the // piece data to the host. func (hu *hostUploader) revise(fc types.FileContract, piece []byte, height types.BlockHeight) error { hu.conn.SetDeadline(time.Now().Add(5 * time.Minute)) // sufficient to transfer 4 MB over 100 kbps defer hu.conn.SetDeadline(time.Time{}) // reset timeout after each revision // calculate new merkle root err := hu.tree.ReadSegments(bytes.NewReader(piece)) if err != nil { return err } // create revision rev := types.FileContractRevision{ ParentID: hu.contract.ID, UnlockConditions: hu.unlockConditions, NewRevisionNumber: fc.RevisionNumber + 1, NewFileSize: fc.FileSize + uint64(len(piece)), NewFileMerkleRoot: hu.tree.Root(), NewWindowStart: fc.WindowStart, NewWindowEnd: fc.WindowEnd, NewValidProofOutputs: fc.ValidProofOutputs, NewMissedProofOutputs: fc.MissedProofOutputs, NewUnlockHash: fc.UnlockHash, } // transfer value of piece from renter to host safeDuration := uint64(fc.WindowStart - height + 20) // buffer in case host is behind piecePrice := types.NewCurrency64(uint64(len(piece))).Mul(types.NewCurrency64(safeDuration)).Mul(hu.settings.Price) // prevent a negative currency panic if piecePrice.Cmp(fc.ValidProofOutputs[0].Value) > 0 { // probably not enough money, but the host might accept it anyway piecePrice = fc.ValidProofOutputs[0].Value } rev.NewValidProofOutputs[0].Value = rev.NewValidProofOutputs[0].Value.Sub(piecePrice) // less returned to renter rev.NewValidProofOutputs[1].Value = rev.NewValidProofOutputs[1].Value.Add(piecePrice) // more given to host rev.NewMissedProofOutputs[0].Value = rev.NewMissedProofOutputs[0].Value.Sub(piecePrice) // less returned to renter rev.NewMissedProofOutputs[1].Value = rev.NewMissedProofOutputs[1].Value.Add(piecePrice) // more given to void // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(hu.contract.ID), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // renter key is always first -- see negotiateContract }}, } // sign the transaction encodedSig, err := crypto.SignHash(signedTxn.SigHash(0), hu.secretKey) if err != nil { return err } signedTxn.TransactionSignatures[0].Signature = encodedSig[:] // send the transaction if err := encoding.WriteObject(hu.conn, signedTxn); err != nil { return err } // host sends acceptance var response string if err := encoding.ReadObject(hu.conn, &response, 128); err != nil { return err } if response != modules.AcceptResponse { return errors.New("host rejected revision: " + response) } // transfer piece if _, err := hu.conn.Write(piece); err != nil { return err } // read txn signed by host var signedHostTxn types.Transaction if err := encoding.ReadObject(hu.conn, &signedHostTxn, types.BlockSizeLimit); err != nil { return err } if signedHostTxn.ID() != signedTxn.ID() { return errors.New("host sent bad signed transaction") } else if err = signedHostTxn.StandaloneValid(height); err != nil { return err } hu.lastTxn = signedHostTxn return nil }
// verifyKeys checks a set of keys on disk to see that they can spend funds // sent to their address. func verifyKeys(uc types.UnlockConditions, folder string, keyname string) error { keysRequired := uc.SignaturesRequired totalKeys := uint64(len(uc.PublicKeys)) // Load the keys from disk back into memory, then verify that the keys on // disk are able to sign outputs in transactions. loadedKeys := make([]KeyPair, totalKeys) for i := 0; i < len(loadedKeys); i++ { err := encoding.ReadFile(filepath.Join(folder, keyname+"_Key"+strconv.Itoa(i)+FileExtension), &loadedKeys[i]) if err != nil { return err } if loadedKeys[i].Header != FileHeader { return ErrUnknownHeader } if loadedKeys[i].Version != FileVersion { return ErrUnknownVersion } } // Check that the keys can be used to spend transactions. for _, loadedKey := range loadedKeys { if loadedKey.UnlockConditions.UnlockHash() != uc.UnlockHash() { return ErrCorruptedKey } } // Create a transaction for the keys to sign. txn := types.Transaction{ SiafundInputs: []types.SiafundInput{ types.SiafundInput{ UnlockConditions: loadedKeys[0].UnlockConditions, }, }, } // Loop through and sign the transaction multiple times. All keys will be // used at least once by the time the loop terminates. var i uint64 for i != totalKeys { // i tracks which key is next to be used. If i + RequiredKeys results // in going out-of-bounds, reduce i so that the last key will be used // for the final signature. if i+keysRequired > totalKeys { i = totalKeys - keysRequired } var j uint64 for j < keysRequired { txn.TransactionSignatures = append(txn.TransactionSignatures, types.TransactionSignature{ PublicKeyIndex: i, CoveredFields: types.CoveredFields{WholeTransaction: true}, }) sigHash := txn.SigHash(int(j)) sig, err := crypto.SignHash(sigHash, loadedKeys[i].SecretKey) if err != nil { return err } txn.TransactionSignatures[j].Signature = sig[:] i++ j++ } // Check that the signature is valid. err := txn.StandaloneValid(0) if err != nil { return err } // Delete all of the signatures for the next iteration. txn.TransactionSignatures = nil } return nil }