func TestNegotiateContract(t *testing.T) { if testing.Short() { t.SkipNow() } rt, err := newRenterTester("TestNegotiateContract") if err != nil { t.Fatal(err) } payout := types.NewCurrency64(1e16) fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: 100, WindowEnd: 1000, Payout: payout, ValidProofOutputs: []types.SiacoinOutput{ {Value: payout, UnlockHash: types.UnlockHash{}}, {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, }, MissedProofOutputs: []types.SiacoinOutput{ // same as above {Value: payout, UnlockHash: types.UnlockHash{}}, // goes to the void, not the renter {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, }, UnlockHash: types.UnlockHash{}, RevisionNumber: 0, } fc.ValidProofOutputs[0].Value = fc.ValidProofOutputs[0].Value.Sub(fc.Tax()) fc.MissedProofOutputs[0].Value = fc.MissedProofOutputs[0].Value.Sub(fc.Tax()) txnBuilder := rt.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) } err = rt.tpool.AcceptTransactionSet(signedTxnSet) if err != nil { t.Fatal(err) } }
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) } }
// negotiateContract establishes a connection to a host and negotiates an // initial file contract according to the terms of the host. func (hu *hostUploader) negotiateContract(filesize uint64, duration types.BlockHeight, renterAddress types.UnlockHash) error { conn, err := net.DialTimeout("tcp", string(hu.settings.IPAddress), 15*time.Second) if err != nil { return err } defer conn.Close() conn.SetDeadline(time.Now().Add(30 * time.Second)) // inital calculations before connecting to host lockID := hu.renter.mu.RLock() height := hu.renter.blockHeight hu.renter.mu.RUnlock(lockID) renterCost := hu.settings.Price.Mul(types.NewCurrency64(filesize)).Mul(types.NewCurrency64(uint64(duration))) renterCost = renterCost.MulFloat(1.05) // extra buffer to guarantee we won't run out of money during revision payout := renterCost // no collateral // write rpcID if err := encoding.WriteObject(conn, modules.RPCUpload); err != nil { return errors.New("couldn't initiate RPC: " + err.Error()) } // read host key // TODO: need to save this? var hostPublicKey types.SiaPublicKey if err := encoding.ReadObject(conn, &hostPublicKey, 256); err != nil { return errors.New("couldn't read host's public key: " + err.Error()) } // create our own key by combining the renter entropy with the host key entropy := crypto.HashAll(hu.renter.entropy, hostPublicKey) ourSK, ourPK := crypto.StdKeyGen.GenerateDeterministic(entropy) ourPublicKey := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: ourPK[:], } hu.secretKey = ourSK // used to sign future revisions // send our public key if err := encoding.WriteObject(conn, ourPublicKey); err != nil { return errors.New("couldn't send our public key: " + err.Error()) } // create unlock conditions hu.unlockConditions = types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{ourPublicKey, hostPublicKey}, SignaturesRequired: 2, } // create file contract fc := types.FileContract{ FileSize: 0, FileMerkleRoot: crypto.Hash{}, // no proof possible without data WindowStart: height + duration, WindowEnd: height + duration + hu.settings.WindowSize, Payout: payout, UnlockHash: hu.unlockConditions.UnlockHash(), RevisionNumber: 0, } // outputs need account for tax fc.ValidProofOutputs = []types.SiacoinOutput{ {Value: renterCost.Sub(types.Tax(hu.renter.blockHeight, fc.Payout)), UnlockHash: renterAddress}, {Value: types.ZeroCurrency, UnlockHash: hu.settings.UnlockHash}, // no collateral } fc.MissedProofOutputs = []types.SiacoinOutput{ // same as above fc.ValidProofOutputs[0], // goes to the void, not the renter {Value: types.ZeroCurrency, UnlockHash: types.UnlockHash{}}, } // build transaction containing fc txnBuilder := hu.renter.wallet.StartTransaction() err = txnBuilder.FundSiacoins(fc.Payout) if err != nil { return err } txnBuilder.AddFileContract(fc) txn, parents := txnBuilder.View() txnSet := append(parents, txn) // calculate contract ID fcid := txn.FileContractID(0) // TODO: is it actually 0? // send txn if err := encoding.WriteObject(conn, txnSet); err != nil { txnBuilder.Drop() return errors.New("couldn't send our proposed contract: " + err.Error()) } // read back acceptance var response string if err := encoding.ReadObject(conn, &response, 128); err != nil { txnBuilder.Drop() return errors.New("couldn't read the host's response to our proposed contract: " + err.Error()) } if response != modules.AcceptResponse { txnBuilder.Drop() return errors.New("host rejected proposed contract: " + response) } // read back txn with host collateral. var hostTxnSet []types.Transaction if err := encoding.ReadObject(conn, &hostTxnSet, types.BlockSizeLimit); err != nil { txnBuilder.Drop() return errors.New("couldn't read the host's updated contract: " + err.Error()) } // check that txn is okay. For now, no collateral will be added, so the // transaction sets should be identical. if len(hostTxnSet) != len(txnSet) { txnBuilder.Drop() return errors.New("host sent bad collateral transaction") } for i := range hostTxnSet { if hostTxnSet[i].ID() != txnSet[i].ID() { txnBuilder.Drop() return errors.New("host sent bad collateral transaction") } } // sign the txn and resend // NOTE: for now, we are assuming that the transaction has not changed // since we sent it. Otherwise, the txnBuilder would have to be updated // with whatever fields were added by the host. signedTxnSet, err := txnBuilder.Sign(true) if err != nil { txnBuilder.Drop() return err } if err := encoding.WriteObject(conn, signedTxnSet); err != nil { txnBuilder.Drop() return errors.New("couldn't send the contract signed by us: " + err.Error()) } // read signed txn from host var signedHostTxnSet []types.Transaction if err := encoding.ReadObject(conn, &signedHostTxnSet, types.BlockSizeLimit); err != nil { txnBuilder.Drop() return errors.New("couldn't read the contract signed by the host: " + err.Error()) } // submit to blockchain err = hu.renter.tpool.AcceptTransactionSet(signedHostTxnSet) if err == modules.ErrDuplicateTransactionSet { // this can happen if the renter is uploading to itself err = nil } if err != nil { txnBuilder.Drop() return err } // create initial fileContract object hu.contract = fileContract{ ID: fcid, IP: hu.settings.IPAddress, WindowStart: fc.WindowStart, } lockID = hu.renter.mu.Lock() hu.renter.contracts[fcid] = fc hu.renter.mu.Unlock(lockID) return nil }
// TestApplyMissedStorageProof probes the applyMissedStorageProof method of the // consensus set. func TestApplyMissedStorageProof(t *testing.T) { if testing.Short() { t.SkipNow() } cst, err := createConsensusSetTester("TestApplyMissedStorageProof") if err != nil { t.Fatal(err) } defer cst.closeCst() // Create a block node. pb := new(processedBlock) pb.Height = cst.cs.height() // Create a file contract that's expiring and has 1 missed proof output. expiringFC := types.FileContract{ Payout: types.NewCurrency64(300e3), WindowEnd: pb.Height, MissedProofOutputs: []types.SiacoinOutput{{Value: types.NewCurrency64(290e3)}}, } // Assign the contract a 0-id. cst.cs.db.addFileContracts(types.FileContractID{}, expiringFC) cst.cs.db.addFCExpirations(pb.Height) cst.cs.db.addFCExpirationsHeight(pb.Height, types.FileContractID{}) cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) exists := cst.cs.db.inFileContracts(types.FileContractID{}) if exists { t.Error("file contract was not consumed in missed storage proof") } spoid := types.FileContractID{}.StorageProofOutputID(types.ProofMissed, 0) exists = cst.cs.db.inDelayedSiacoinOutputsHeight(pb.Height+types.MaturityDelay, spoid) if !exists { t.Error("missed proof output was never created") } exists = cst.cs.db.inSiacoinOutputs(spoid) if exists { t.Error("storage proof output made it into the siacoin output set") } exists = cst.cs.db.inFileContracts(types.FileContractID{}) if exists { t.Error("file contract remains after expiration") } // Trigger the debug panics. // not exist. defer func() { r := recover() if r != errNilItem { t.Error(r) } }() defer func() { r := recover() if r != errNilItem { t.Error(r) } // Trigger errMissingFileContract cst.cs.applyMissedStorageProof(pb, types.FileContractID(spoid)) }() defer func() { r := recover() if r != errNilItem { t.Error(r) } // Trigger errStorageProofTiming expiringFC.WindowEnd = 0 cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) }() defer func() { r := recover() if r != errNilItem { t.Error(r) } // Trigger errPayoutsAlreadyPaid from siacoin outputs. cst.cs.db.rmDelayedSiacoinOutputsHeight(pb.Height+types.MaturityDelay, spoid) cst.cs.db.addSiacoinOutputs(spoid, types.SiacoinOutput{}) cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) }() // Trigger errPayoutsAlreadyPaid from delayed outputs. cst.cs.db.rmFileContracts(types.FileContractID{}) cst.cs.db.addFileContracts(types.FileContractID{}, expiringFC) cst.cs.db.addDelayedSiacoinOutputsHeight(pb.Height+types.MaturityDelay, spoid, types.SiacoinOutput{}) cst.cs.applyMissedStorageProof(pb, types.FileContractID{}) }
// TestValidFileContractRevisions probes the validFileContractRevisions method // of the consensus set. func TestValidFileContractRevisions(t *testing.T) { if testing.Short() { t.SkipNow() } t.Parallel() cst, err := createConsensusSetTester("TestValidFileContractRevisions") if err != nil { t.Fatal(err) } defer cst.Close() // Grab an address + unlock conditions for the transaction. unlockConditions, err := cst.wallet.NextAddress() if err != nil { t.Fatal(err) } // Create a file contract for which a storage proof can be created. var fcid types.FileContractID fcid[0] = 12 simFile := make([]byte, 64*1024) rand.Read(simFile) root := crypto.MerkleRoot(simFile) fc := types.FileContract{ FileSize: 64 * 1024, FileMerkleRoot: root, WindowStart: 102, WindowEnd: 1200, Payout: types.NewCurrency64(1), UnlockHash: unlockConditions.UnlockHash(), RevisionNumber: 1, } cst.cs.dbAddFileContract(fcid, fc) // Try a working file contract revision. txn := types.Transaction{ FileContractRevisions: []types.FileContractRevision{ { ParentID: fcid, UnlockConditions: unlockConditions, NewRevisionNumber: 2, }, }, } err = cst.cs.dbValidFileContractRevisions(txn) if err != nil { t.Error(err) } // Try a transaction with an insufficient revision number. txn = types.Transaction{ FileContractRevisions: []types.FileContractRevision{ { ParentID: fcid, UnlockConditions: unlockConditions, NewRevisionNumber: 1, }, }, } err = cst.cs.dbValidFileContractRevisions(txn) if err != errLowRevisionNumber { t.Error(err) } txn = types.Transaction{ FileContractRevisions: []types.FileContractRevision{ { ParentID: fcid, UnlockConditions: unlockConditions, NewRevisionNumber: 0, }, }, } err = cst.cs.dbValidFileContractRevisions(txn) if err != errLowRevisionNumber { t.Error(err) } // Submit a file contract revision pointing to an invalid parent. txn.FileContractRevisions[0].ParentID[0]-- err = cst.cs.dbValidFileContractRevisions(txn) if err != errNilItem { t.Error(err) } txn.FileContractRevisions[0].ParentID[0]++ // Submit a file contract revision for a file contract whose window has // already opened. fc, err = cst.cs.dbGetFileContract(fcid) if err != nil { t.Fatal(err) } fc.WindowStart = 0 cst.cs.dbRemoveFileContract(fcid) cst.cs.dbAddFileContract(fcid, fc) txn.FileContractRevisions[0].NewRevisionNumber = 3 err = cst.cs.dbValidFileContractRevisions(txn) if err != errLateRevision { t.Error(err) } // Submit a file contract revision with incorrect unlock conditions. fc.WindowStart = 100 cst.cs.dbRemoveFileContract(fcid) cst.cs.dbAddFileContract(fcid, fc) txn.FileContractRevisions[0].UnlockConditions.Timelock++ err = cst.cs.dbValidFileContractRevisions(txn) if err != errWrongUnlockConditions { t.Error(err) } txn.FileContractRevisions[0].UnlockConditions.Timelock-- // Submit file contract revisions for file contracts with altered payouts. txn.FileContractRevisions[0].NewValidProofOutputs = []types.SiacoinOutput{{ Value: types.NewCurrency64(1), }} txn.FileContractRevisions[0].NewMissedProofOutputs = []types.SiacoinOutput{{ Value: types.NewCurrency64(1), }} err = cst.cs.dbValidFileContractRevisions(txn) if err != errAlteredRevisionPayouts { t.Error(err) } txn.FileContractRevisions[0].NewValidProofOutputs = nil err = cst.cs.dbValidFileContractRevisions(txn) if err != errAlteredRevisionPayouts { t.Error(err) } txn.FileContractRevisions[0].NewValidProofOutputs = []types.SiacoinOutput{{ Value: types.NewCurrency64(1), }} txn.FileContractRevisions[0].NewMissedProofOutputs = nil err = cst.cs.dbValidFileContractRevisions(txn) if err != errAlteredRevisionPayouts { t.Error(err) } }
// negotiateContract establishes a connection to a host and negotiates an // initial file contract according to the terms of the host. func negotiateContract(conn net.Conn, addr modules.NetAddress, fc types.FileContract, txnBuilder transactionBuilder, tpool transactionPool) (hostContract, error) { // allow 30 seconds for negotiation conn.SetDeadline(time.Now().Add(30 * time.Second)) // read host key var hostPublicKey types.SiaPublicKey if err := encoding.ReadObject(conn, &hostPublicKey, 256); err != nil { return hostContract{}, errors.New("couldn't read host's public key: " + err.Error()) } // create our key ourSK, ourPK, err := crypto.GenerateKeyPair() if err != nil { return hostContract{}, errors.New("failed to generate keypair: " + err.Error()) } ourPublicKey := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: ourPK[:], } // send our public key if err := encoding.WriteObject(conn, ourPublicKey); err != nil { return hostContract{}, errors.New("couldn't send our public key: " + err.Error()) } // create unlock conditions uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{ourPublicKey, hostPublicKey}, SignaturesRequired: 2, } // add UnlockHash to file contract fc.UnlockHash = uc.UnlockHash() // build transaction containing fc err = txnBuilder.FundSiacoins(fc.Payout) if err != nil { return hostContract{}, err } txnBuilder.AddFileContract(fc) txn, parents := txnBuilder.View() txnSet := append(parents, txn) // calculate contract ID fcid := txn.FileContractID(0) // TODO: is it actually 0? // send txn if err := encoding.WriteObject(conn, txnSet); err != nil { return hostContract{}, errors.New("couldn't send our proposed contract: " + err.Error()) } // read back acceptance var response string if err := encoding.ReadObject(conn, &response, 128); err != nil { return hostContract{}, errors.New("couldn't read the host's response to our proposed contract: " + err.Error()) } if response != modules.AcceptResponse { return hostContract{}, errors.New("host rejected proposed contract: " + response) } // read back txn with host collateral. var hostTxnSet []types.Transaction if err := encoding.ReadObject(conn, &hostTxnSet, types.BlockSizeLimit); err != nil { return hostContract{}, errors.New("couldn't read the host's updated contract: " + err.Error()) } // check that txn is okay. For now, no collateral will be added, so the // transaction sets should be identical. if len(hostTxnSet) != len(txnSet) { return hostContract{}, errors.New("host sent bad collateral transaction") } for i := range hostTxnSet { if hostTxnSet[i].ID() != txnSet[i].ID() { return hostContract{}, errors.New("host sent bad collateral transaction") } } // sign the txn and resend // NOTE: for now, we are assuming that the transaction has not changed // since we sent it. Otherwise, the txnBuilder would have to be updated // with whatever fields were added by the host. signedTxnSet, err := txnBuilder.Sign(true) if err != nil { return hostContract{}, err } if err := encoding.WriteObject(conn, signedTxnSet); err != nil { return hostContract{}, errors.New("couldn't send the contract signed by us: " + err.Error()) } // read signed txn from host var signedHostTxnSet []types.Transaction if err := encoding.ReadObject(conn, &signedHostTxnSet, types.BlockSizeLimit); err != nil { return hostContract{}, errors.New("couldn't read the contract signed by the host: " + err.Error()) } // submit to blockchain err = tpool.AcceptTransactionSet(signedHostTxnSet) if err == modules.ErrDuplicateTransactionSet { // this can happen if the renter is uploading to itself err = nil } if err != nil { return hostContract{}, err } // create host contract hc := hostContract{ IP: addr, ID: fcid, FileContract: fc, LastRevision: types.FileContractRevision{ ParentID: fcid, UnlockConditions: uc, NewRevisionNumber: fc.RevisionNumber, NewFileSize: fc.FileSize, NewFileMerkleRoot: fc.FileMerkleRoot, NewWindowStart: fc.WindowStart, NewWindowEnd: fc.WindowEnd, NewValidProofOutputs: []types.SiacoinOutput{fc.ValidProofOutputs[0], fc.ValidProofOutputs[1]}, NewMissedProofOutputs: []types.SiacoinOutput{fc.MissedProofOutputs[0], fc.MissedProofOutputs[1]}, NewUnlockHash: fc.UnlockHash, }, LastRevisionTxn: types.Transaction{}, SecretKey: ourSK, } return hc, nil }