// TestApplySiacoinOutputs probes the applySiacoinOutput method of the // consensus set. func TestApplySiacoinOutputs(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplySiacoinOutputs") if err != nil { t.Fatal(err) } // Create a block node to use with application. bn := new(blockNode) // Apply a transaction with a single siacoin output. txn := types.Transaction{ SiacoinOutputs: []types.SiacoinOutput{{}}, } cst.cs.applySiacoinOutputs(bn, txn) scoid := txn.SiacoinOutputID(0) _, exists := cst.cs.siacoinOutputs[scoid] if !exists { t.Error("Failed to create siacoin output") } if len(cst.cs.siacoinOutputs) != 3 { // 3 because createConsensusSetTester has 2 initially. t.Error("siacoin outputs not correctly updated") } if len(bn.siacoinOutputDiffs) != 1 { t.Error("block node was not updated for single element transaction") } if bn.siacoinOutputDiffs[0].Direction != modules.DiffApply { t.Error("wrong diff direction applied when creating a siacoin output") } if bn.siacoinOutputDiffs[0].ID != scoid { t.Error("wrong id used when creating a siacoin output") } // Apply a transaction with 2 siacoin outputs. txn = types.Transaction{ SiacoinOutputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(1)}, {Value: types.NewCurrency64(2)}, }, } cst.cs.applySiacoinOutputs(bn, txn) scoid0 := txn.SiacoinOutputID(0) scoid1 := txn.SiacoinOutputID(1) _, exists = cst.cs.siacoinOutputs[scoid0] if !exists { t.Error("Failed to create siacoin output") } _, exists = cst.cs.siacoinOutputs[scoid1] if !exists { t.Error("Failed to create siacoin output") } if len(cst.cs.siacoinOutputs) != 5 { // 5 because createConsensusSetTester has 2 initially. t.Error("siacoin outputs not correctly updated") } if len(bn.siacoinOutputDiffs) != 3 { t.Error("block node was not updated correctly") } }
// applyFileContracts iterates through all of the file contracts in a // transaction and applies them to the state, updating the diffs in the block // node. func (cs *State) applyFileContracts(bn *blockNode, t types.Transaction) { for i, fc := range t.FileContracts { // Sanity check - the file contract should not exists within the state. fcid := t.FileContractID(i) if build.DEBUG { _, exists := cs.fileContracts[fcid] if exists { panic(ErrMisuseApplyFileContracts) } } fcd := modules.FileContractDiff{ Direction: modules.DiffApply, ID: fcid, FileContract: fc, } bn.fileContractDiffs = append(bn.fileContractDiffs, fcd) cs.commitFileContractDiff(fcd, modules.DiffApply) // Get the portion of the contract that goes into the siafund pool and // add it to the siafund pool. sfpd := modules.SiafundPoolDiff{ Previous: cs.siafundPool, Adjusted: cs.siafundPool.Add(fc.Tax()), } bn.siafundPoolDiffs = append(bn.siafundPoolDiffs, sfpd) cs.commitSiafundPoolDiff(sfpd, modules.DiffApply) } return }
// 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 }
// 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 }
// validSiacoins checks that the siacoin inputs and outputs are valid in the // context of the current consensus set. func validSiacoins(tx *bolt.Tx, t types.Transaction) error { scoBucket := tx.Bucket(SiacoinOutputs) var inputSum types.Currency for _, sci := range t.SiacoinInputs { // Check that the input spends an existing output. scoBytes := scoBucket.Get(sci.ParentID[:]) if scoBytes == nil { return errMissingSiacoinOutput } // Check that the unlock conditions match the required unlock hash. var sco types.SiacoinOutput err := encoding.Unmarshal(scoBytes, &sco) if build.DEBUG && err != nil { panic(err) } if sci.UnlockConditions.UnlockHash() != sco.UnlockHash { return errWrongUnlockConditions } inputSum = inputSum.Add(sco.Value) } if inputSum.Cmp(t.SiacoinOutputSum()) != 0 { return errSiacoinInputOutputMismatch } return nil }
// applySiacoinOutputs takes all of the siacoin outputs in a transaction and // applies them to the state, updating the diffs in the processed block. func applySiacoinOutputs(tx *bolt.Tx, pb *processedBlock, t types.Transaction) { // Add all siacoin outputs to the unspent siacoin outputs list. for i, sco := range t.SiacoinOutputs { scoid := t.SiacoinOutputID(uint64(i)) scod := modules.SiacoinOutputDiff{ Direction: modules.DiffApply, ID: scoid, SiacoinOutput: sco, } pb.SiacoinOutputDiffs = append(pb.SiacoinOutputDiffs, scod) commitSiacoinOutputDiff(tx, scod, modules.DiffApply) } }
// applySiafundOutput applies a siafund output to the consensus set. func applySiafundOutputs(tx *bolt.Tx, pb *processedBlock, t types.Transaction) { for i, sfo := range t.SiafundOutputs { sfoid := t.SiafundOutputID(uint64(i)) sfo.ClaimStart = getSiafundPool(tx) sfod := modules.SiafundOutputDiff{ Direction: modules.DiffApply, ID: sfoid, SiafundOutput: sfo, } pb.SiafundOutputDiffs = append(pb.SiafundOutputDiffs, sfod) commitSiafundOutputDiff(tx, sfod, modules.DiffApply) } }
// removeSiafundOutputs removes all of the siafund outputs of a transaction // from the unconfirmed consensus set. func (tp *TransactionPool) removeSiafundOutputs(t types.Transaction) { for i, _ := range t.SiafundOutputs { // Sanity check - the output should exist in the unconfirmed set as // there is no dependent transaction which could have spent the output. sfoid := t.SiafundOutputID(i) if build.DEBUG { _, exists := tp.siafundOutputs[sfoid] if !exists { panic("trying to remove nonexisting siafund output from unconfirmed set") } } delete(tp.siafundOutputs, sfoid) } }
// removeSiacoinOutputs removes all of the siacoin outputs of a transaction // from the unconfirmed consensus set. func (tp *TransactionPool) removeSiacoinOutputs(t types.Transaction) { for i, _ := range t.SiacoinOutputs { scoid := t.SiacoinOutputID(i) // Sanity check - the output should exist in the unconfirmed set as // there should be no transaction dependents who have spent the output. if build.DEBUG { _, exists := tp.siacoinOutputs[scoid] if !exists { panic("trying to delete missing siacoin output") } } delete(tp.siacoinOutputs, scoid) } }
// removeFileContracts removes all of the file contracts of a transaction from // the unconfirmed consensus set. func (tp *TransactionPool) removeFileContracts(t types.Transaction) { for i, _ := range t.FileContracts { fcid := t.FileContractID(i) // Sanity check - file contract should be in the unconfirmed set as // there should be no dependent transactions who have terminated the // contract. if build.DEBUG { _, exists := tp.fileContracts[fcid] if !exists { panic("trying to remove missing file contract") } } delete(tp.fileContracts, fcid) } }
// applySiafundOutput applies a siafund output to the consensus set. func (cs *ConsensusSet) applySiafundOutputs(tx *bolt.Tx, pb *processedBlock, t types.Transaction) error { for i, sfo := range t.SiafundOutputs { sfoid := t.SiafundOutputID(i) sfo.ClaimStart = getSiafundPool(tx) sfod := modules.SiafundOutputDiff{ Direction: modules.DiffApply, ID: sfoid, SiafundOutput: sfo, } pb.SiafundOutputDiffs = append(pb.SiafundOutputDiffs, sfod) err := cs.commitTxSiafundOutputDiff(tx, sfod, modules.DiffApply) if err != nil { return err } } return nil }
// applySiacoinOutputs takes all of the siacoin outputs in a transaction and // applies them to the state, updating the diffs in the processed block. func (cs *ConsensusSet) applySiacoinOutputs(tx *bolt.Tx, pb *processedBlock, t types.Transaction) error { // Add all siacoin outputs to the unspent siacoin outputs list. scoBucket := tx.Bucket(SiacoinOutputs) for i, sco := range t.SiacoinOutputs { scoid := t.SiacoinOutputID(i) scod := modules.SiacoinOutputDiff{ Direction: modules.DiffApply, ID: scoid, SiacoinOutput: sco, } pb.SiacoinOutputDiffs = append(pb.SiacoinOutputDiffs, scod) err := cs.commitBucketSiacoinOutputDiff(scoBucket, scod, modules.DiffApply) if err != nil { return err } } 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) } }
// TestDuplicateStorageProof applies a storage proof which has already been // applied. func TestDuplicateStorageProof(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestDuplicateStorageProof") if err != nil { t.Fatal(err) } // Create a block node. bn := new(blockNode) bn.height = cst.cs.height() // Create a file contract for the storage proof to prove. txn0 := types.Transaction{ FileContracts: []types.FileContract{ { Payout: types.NewCurrency64(300e3), ValidProofOutputs: []types.SiacoinOutput{ {Value: types.NewCurrency64(290e3)}, }, }, }, } cst.cs.applyFileContracts(bn, txn0) fcid := txn0.FileContractID(0) // Apply a single storage proof. txn1 := types.Transaction{ StorageProofs: []types.StorageProof{{ParentID: fcid}}, } cst.cs.applyStorageProofs(bn, txn1) // Trigger a panic by applying the storage proof again. defer func() { r := recover() if r != ErrDuplicateValidProofOutput { t.Error("failed to trigger ErrDuplicateValidProofOutput:", r) } }() cst.cs.applyFileContracts(bn, txn0) // File contract was consumed by the first proof. cst.cs.applyStorageProofs(bn, txn1) }
// addSignatures will sign a transaction using a spendable key, with support // for multisig spendable keys. Because of the restricted input, the function // is compatible with both siacoin inputs and siafund inputs. func addSignatures(txn *types.Transaction, cf types.CoveredFields, uc types.UnlockConditions, parentID crypto.Hash, spendKey spendableKey) (newSigIndices []int, err error) { // Try to find the matching secret key for each public key - some public // keys may not have a match. Some secret keys may be used multiple times, // which is why public keys are used as the outer loop. totalSignatures := uint64(0) for i, siaPubKey := range uc.PublicKeys { // Search for the matching secret key to the public key. for j := range spendKey.SecretKeys { pubKey := spendKey.SecretKeys[j].PublicKey() if bytes.Compare(siaPubKey.Key, pubKey[:]) != 0 { continue } // Found the right secret key, add a signature. sig := types.TransactionSignature{ ParentID: parentID, CoveredFields: cf, PublicKeyIndex: uint64(i), } newSigIndices = append(newSigIndices, len(txn.TransactionSignatures)) txn.TransactionSignatures = append(txn.TransactionSignatures, sig) sigIndex := len(txn.TransactionSignatures) - 1 sigHash := txn.SigHash(sigIndex) encodedSig, err := crypto.SignHash(sigHash, spendKey.SecretKeys[j]) if err != nil { return nil, err } txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] // Count that the signature has been added, and break out of the // secret key loop. totalSignatures++ break } // If there are enough signatures to satisfy the unlock conditions, // break out of the outer loop. if totalSignatures == uc.SignaturesRequired { break } } return newSigIndices, nil }
// applySiacoinOutputs takes all of the siacoin outputs in a transaction and // applies them to the state, updating the diffs in the block node. func (cs *State) applySiacoinOutputs(bn *blockNode, t types.Transaction) { // Add all siacoin outputs to the unspent siacoin outputs list. for i, sco := range t.SiacoinOutputs { // Sanity check - the output should not exist within the state. scoid := t.SiacoinOutputID(i) if build.DEBUG { _, exists := cs.siacoinOutputs[scoid] if exists { panic(ErrMisuseApplySiacoinOutput) } } scod := modules.SiacoinOutputDiff{ Direction: modules.DiffApply, ID: scoid, SiacoinOutput: sco, } bn.siacoinOutputDiffs = append(bn.siacoinOutputDiffs, scod) cs.commitSiacoinOutputDiff(scod, modules.DiffApply) } }
// 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) }
// addSignatures will sign a transaction using a spendable key, with support // for multisig spendable keys. Because of the restricted input, the function // is compatible with both siacoin inputs and siafund inputs. func addSignatures(txn *types.Transaction, cf types.CoveredFields, uc types.UnlockConditions, parentID crypto.Hash, key spendableKey) error { usedIndices := make(map[int]struct{}) for i := range key.secretKeys { found := false keyIndex := 0 pubKey := key.secretKeys[i].PublicKey() for i, siaPublicKey := range uc.PublicKeys { _, exists := usedIndices[i] if !exists && bytes.Compare(pubKey[:], siaPublicKey.Key) == 0 { found = true keyIndex = i break } } if !found && build.DEBUG { panic("transaction builder cannot sign an input that it added") } usedIndices[keyIndex] = struct{}{} // Create the unsigned transaction signature. sig := types.TransactionSignature{ ParentID: parentID, CoveredFields: cf, PublicKeyIndex: uint64(keyIndex), } txn.TransactionSignatures = append(txn.TransactionSignatures, sig) // Get the signature. sigIndex := len(txn.TransactionSignatures) - 1 sigHash := txn.SigHash(sigIndex) encodedSig, err := crypto.SignHash(sigHash, key.secretKeys[i]) if err != nil { return err } txn.TransactionSignatures[sigIndex].Signature = encodedSig[:] } return nil }
// 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 }
// applyFileContracts iterates through all of the file contracts in a // transaction and applies them to the state, updating the diffs in the proccesed // block. func applyFileContracts(tx *bolt.Tx, pb *processedBlock, t types.Transaction) { for i, fc := range t.FileContracts { fcid := t.FileContractID(uint64(i)) fcd := modules.FileContractDiff{ Direction: modules.DiffApply, ID: fcid, FileContract: fc, } pb.FileContractDiffs = append(pb.FileContractDiffs, fcd) commitFileContractDiff(tx, fcd, modules.DiffApply) // Get the portion of the contract that goes into the siafund pool and // add it to the siafund pool. sfp := getSiafundPool(tx) sfpd := modules.SiafundPoolDiff{ Direction: modules.DiffApply, Previous: sfp, Adjusted: sfp.Add(types.Tax(blockHeight(tx), fc.Payout)), } pb.SiafundPoolDiffs = append(pb.SiafundPoolDiffs, sfpd) commitSiafundPoolDiff(tx, sfpd, modules.DiffApply) } }
// applyFileContracts iterates through all of the file contracts in a // transaction and applies them to the state, updating the diffs in the proccesed // block. func (cs *ConsensusSet) applyFileContracts(tx *bolt.Tx, pb *processedBlock, t types.Transaction) error { for i, fc := range t.FileContracts { fcid := t.FileContractID(i) fcd := modules.FileContractDiff{ Direction: modules.DiffApply, ID: fcid, FileContract: fc, } pb.FileContractDiffs = append(pb.FileContractDiffs, fcd) cs.commitTxFileContractDiff(tx, fcd, modules.DiffApply) // Get the portion of the contract that goes into the siafund pool and // add it to the siafund pool. sfp := getSiafundPool(tx) sfpd := modules.SiafundPoolDiff{ Direction: modules.DiffApply, Previous: sfp, Adjusted: sfp.Add(fc.Tax()), } pb.SiafundPoolDiffs = append(pb.SiafundPoolDiffs, sfpd) cs.commitTxSiafundPoolDiff(tx, sfpd, modules.DiffApply) } return nil }
// validUnconfirmedSiacoins checks that all siacoin inputs and outputs are // valid in the context of the unconfirmed consensus set. func (tp *TransactionPool) validUnconfirmedSiacoins(t types.Transaction) (err error) { var inputSum types.Currency for _, sci := range t.SiacoinInputs { // All inputs must have corresponding outputs in the unconfirmed set. sco, exists := tp.siacoinOutputs[sci.ParentID] if !exists { return ErrUnrecognizedSiacoinInput } // The unlock conditions provided must match the unlock hash of the // corresponding output. if sci.UnlockConditions.UnlockHash() != sco.UnlockHash { return ErrBadUnlockConditions } inputSum = inputSum.Add(sco.Value) } // The sum of all inputs must equal the sum of all outputs. if inputSum.Cmp(t.SiacoinOutputSum()) != 0 { return ErrSiacoinOverspend } return }
// applySiafundOutputs takes all of the siafund outputs in a transaction and // applies them to the state, updating the diffs in the block node. func (cs *State) applySiafundOutputs(bn *blockNode, t types.Transaction) { for i, sfo := range t.SiafundOutputs { // Sanity check - the output should not exist within the blockchain. sfoid := t.SiafundOutputID(i) if build.DEBUG { _, exists := cs.siafundOutputs[sfoid] if exists { panic(ErrMisuseApplySiafundOutput) } } // Set the claim start. sfo.ClaimStart = cs.siafundPool // Create and apply the diff. sfod := modules.SiafundOutputDiff{ Direction: modules.DiffApply, ID: sfoid, SiafundOutput: sfo, } bn.siafundOutputDiffs = append(bn.siafundOutputDiffs, sfod) cs.commitSiafundOutputDiff(sfod, modules.DiffApply) } }
// 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 }
// verifyKeysSiag_1_0 is a copy-pasted version of the verifyKeys method // from siag 1.0. func verifyKeysSiag_1_0(uc types.UnlockConditions, folder string, keyname string) error { keysRequired := uc.SignaturesRequired totalKeys := uint64(len(uc.PublicKeys)) loadedKeys := make([]KeyPairSiag_1_0, totalKeys) for i := 0; i < len(loadedKeys); i++ { err := encoding.ReadFile(filepath.Join(folder, keyname+"_Key"+strconv.Itoa(i)+".siakey"), &loadedKeys[i]) if err != nil { return err } } for _, loadedKey := range loadedKeys { if loadedKey.UnlockConditions.UnlockHash() != uc.UnlockHash() { return errors.New("ErrCorruptedKey") } } txn := types.Transaction{ SiafundInputs: []types.SiafundInput{ types.SiafundInput{ UnlockConditions: loadedKeys[0].UnlockConditions, }, }, } var i uint64 for i != totalKeys { if i+keysRequired > totalKeys { i = totalKeys - keysRequired } var j uint64 for j < keysRequired { txn.TransactionSignatures = append(txn.TransactionSignatures, types.TransactionSignature{ PublicKeyIndex: i, CoveredFields: types.CoveredFields{WholeTransaction: true}, }) sigHash := txn.SigHash(int(j)) sig, err := crypto.SignHash(sigHash, loadedKeys[i].SecretKey) if err != nil { return err } txn.TransactionSignatures[j].Signature = sig[:] i++ j++ } err := txn.StandaloneValid(0) if err != nil { return err } txn.TransactionSignatures = nil } return nil }
// negotiateRevision sends a revision and actions to the host for approval, // completing one iteration of the revision loop. func negotiateRevision(conn net.Conn, rev types.FileContractRevision, secretKey crypto.SecretKey) (types.Transaction, error) { // create transaction containing the revision signedTxn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{rev}, TransactionSignatures: []types.TransactionSignature{{ ParentID: crypto.Hash(rev.ParentID), CoveredFields: types.CoveredFields{FileContractRevisions: []uint64{0}}, PublicKeyIndex: 0, // renter key is always first -- see formContract }}, } // sign the transaction encodedSig, _ := crypto.SignHash(signedTxn.SigHash(0), secretKey) // no error possible signedTxn.TransactionSignatures[0].Signature = encodedSig[:] // send the revision if err := encoding.WriteObject(conn, rev); err != nil { return types.Transaction{}, errors.New("couldn't send revision: " + err.Error()) } // read acceptance if err := modules.ReadNegotiationAcceptance(conn); err != nil { return types.Transaction{}, errors.New("host did not accept revision: " + err.Error()) } // send the new transaction signature if err := encoding.WriteObject(conn, signedTxn.TransactionSignatures[0]); err != nil { return types.Transaction{}, errors.New("couldn't send transaction signature: " + err.Error()) } // read the host's acceptance and transaction signature // NOTE: if the host sends ErrStopResponse, we should continue processing // the revision, but return the error anyway. responseErr := modules.ReadNegotiationAcceptance(conn) if responseErr != nil && responseErr != modules.ErrStopResponse { return types.Transaction{}, errors.New("host did not accept transaction signature: " + responseErr.Error()) } var hostSig types.TransactionSignature if err := encoding.ReadObject(conn, &hostSig, 16e3); err != nil { return types.Transaction{}, errors.New("couldn't read host's signature: " + err.Error()) } // add the signature to the transaction and verify it // NOTE: we can fake the blockheight here because it doesn't affect // verification; it just needs to be above the fork height and below the // contract expiration (which was checked earlier). verificationHeight := rev.NewWindowStart - 1 signedTxn.TransactionSignatures = append(signedTxn.TransactionSignatures, hostSig) if err := signedTxn.StandaloneValid(verificationHeight); err != nil { return types.Transaction{}, err } // if the host sent ErrStopResponse, return it return signedTxn, responseErr }
// TestPartialConfirmationWeave checks that the transaction pool correctly // accepts a transaction set which has parents that have been accepted by the // consensus set but not the whole set has been accepted by the consensus set, // this time weaving the dependencies, such that the first transaction is not // in the consensus set, the second is, and the third has both as dependencies. func TestPartialConfirmationWeave(t *testing.T) { if testing.Short() { t.SkipNow() } tpt, err := createTpoolTester("TestPartialConfirmation") if err != nil { t.Fatal(err) } defer tpt.Close() // Step 1: create an output to the empty address in a tx. // Step 2: create a second output to the empty address in another tx. // Step 3: create a transaction using both those outputs. // Step 4: mine the txn set in step 2 // Step 5: Submit the complete set. // Create a transaction with a single output to a fully controlled address. emptyUH := types.UnlockConditions{}.UnlockHash() builder1 := tpt.wallet.StartTransaction() funding1 := types.NewCurrency64(1e9) err = builder1.FundSiacoins(funding1) if err != nil { t.Fatal(err) } scOutput1 := types.SiacoinOutput{ Value: funding1, UnlockHash: emptyUH, } i1 := builder1.AddSiacoinOutput(scOutput1) tSet1, err := builder1.Sign(true) if err != nil { t.Fatal(err) } // Submit to the transaction pool and mine the block, to minimize // complexity. err = tpt.tpool.AcceptTransactionSet(tSet1) if err != nil { t.Fatal(err) } _, err = tpt.miner.AddBlock() if err != nil { t.Fatal(err) } // Create a second output to the fully controlled address, to fund the // second transaction in the weave. builder2 := tpt.wallet.StartTransaction() funding2 := types.NewCurrency64(2e9) err = builder2.FundSiacoins(funding2) if err != nil { t.Fatal(err) } scOutput2 := types.SiacoinOutput{ Value: funding2, UnlockHash: emptyUH, } i2 := builder2.AddSiacoinOutput(scOutput2) tSet2, err := builder2.Sign(true) if err != nil { t.Fatal(err) } // Submit to the transaction pool and mine the block, to minimize // complexity. err = tpt.tpool.AcceptTransactionSet(tSet2) if err != nil { t.Fatal(err) } _, err = tpt.miner.AddBlock() if err != nil { t.Fatal(err) } // Create a passthrough transaction for output1 and output2, so that they // can be used as unconfirmed dependencies. txn1 := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ ParentID: tSet1[len(tSet1)-1].SiacoinOutputID(i1), }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: funding1, UnlockHash: emptyUH, }}, } txn2 := types.Transaction{ SiacoinInputs: []types.SiacoinInput{{ ParentID: tSet2[len(tSet2)-1].SiacoinOutputID(i2), }}, SiacoinOutputs: []types.SiacoinOutput{{ Value: funding2, UnlockHash: emptyUH, }}, } // Create a child transaction that depends on inputs from both txn1 and // txn2. child := types.Transaction{ SiacoinInputs: []types.SiacoinInput{ { ParentID: txn1.SiacoinOutputID(0), }, { ParentID: txn2.SiacoinOutputID(0), }, }, SiacoinOutputs: []types.SiacoinOutput{{ Value: funding1.Add(funding2), }}, } // Get txn2 accepted into the consensus set. err = tpt.tpool.AcceptTransactionSet([]types.Transaction{txn2}) if err != nil { t.Fatal(err) } _, err = tpt.miner.AddBlock() if err != nil { t.Fatal(err) } // Try to get the set of txn1, txn2, and child accepted into the // transaction pool. err = tpt.tpool.AcceptTransactionSet([]types.Transaction{txn1, txn2, child}) if err != nil { t.Fatal(err) } }
// 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 }
// 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 }