// establishDefaults configures the default settings for the host, overwriting // any existing settings. func (h *Host) establishDefaults() error { // Configure the settings object. h.settings = modules.HostSettings{ TotalStorage: defaultTotalStorage, MaxDuration: defaultMaxDuration, WindowSize: defaultWindowSize, Price: defaultPrice, Collateral: defaultCollateral, } h.spaceRemaining = h.settings.TotalStorage // Generate signing key, for revising contracts. sk, pk, err := crypto.GenerateKeyPair() if err != nil { return err } h.secretKey = sk h.publicKey = types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } // Subscribe to the consensus set. err = h.initConsensusSubscription() if err != nil { return err } return nil }
// makeSignedAnnouncement creates a []byte that contains an encoded and signed // host announcement for the given net address. func makeSignedAnnouncement(na modules.NetAddress) ([]byte, error) { sk, pk, err := crypto.GenerateKeyPair() if err != nil { return nil, err } spk := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } return modules.CreateAnnouncement(na, spk, sk) }
// BenchmarkStandaloneValid times how long it takes to verify a single // large transaction, with a certain number of signatures func BenchmarkStandaloneValid(b *testing.B) { numSigs := 7 // make a transaction numSigs with valid inputs with valid signatures b.ReportAllocs() txn := Transaction{} sk := make([]crypto.SecretKey, numSigs) pk := make([]crypto.PublicKey, numSigs) for i := 0; i < numSigs; i++ { s, p, err := crypto.GenerateKeyPair() if err != nil { b.Fatal(err) } sk[i] = s pk[i] = p uc := UnlockConditions{ PublicKeys: []SiaPublicKey{ {Algorithm: SignatureEd25519, Key: pk[i][:]}, }, SignaturesRequired: 1, } txn.SiacoinInputs = append(txn.SiacoinInputs, SiacoinInput{ UnlockConditions: uc, }) copy(txn.SiacoinInputs[i].ParentID[:], encoding.Marshal(i)) txn.TransactionSignatures = append(txn.TransactionSignatures, TransactionSignature{ CoveredFields: CoveredFields{WholeTransaction: true}, }) copy(txn.TransactionSignatures[i].ParentID[:], encoding.Marshal(i)) } // Transaction must be constructed before signing for i := 0; i < numSigs; i++ { sigHash := txn.SigHash(i) sig0, err := crypto.SignHash(sigHash, sk[i]) if err != nil { b.Fatal(err) } txn.TransactionSignatures[i].Signature = sig0[:] } b.ResetTimer() for i := 0; i < b.N; i++ { err := txn.StandaloneValid(10) if err != nil { b.Fatal(err) } } }
// establishDefaults configures the default settings for the host, overwriting // any existing settings. func (h *Host) establishDefaults() error { // Configure the settings object. h.settings = modules.HostInternalSettings{ MaxDownloadBatchSize: uint64(defaultMaxDownloadBatchSize), MaxDuration: defaultMaxDuration, MaxReviseBatchSize: uint64(defaultMaxReviseBatchSize), WindowSize: defaultWindowSize, Collateral: defaultCollateral, CollateralBudget: defaultCollateralBudget, MaxCollateral: defaultMaxCollateral, MinStoragePrice: defaultStoragePrice, MinContractPrice: defaultContractPrice, MinDownloadBandwidthPrice: defaultDownloadBandwidthPrice, MinUploadBandwidthPrice: defaultUploadBandwidthPrice, } // Generate signing key, for revising contracts. sk, pk, err := crypto.GenerateKeyPair() if err != nil { return err } h.secretKey = sk h.publicKey = types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } // Subscribe to the consensus set. err = h.initConsensusSubscription() if err != nil { return err } return nil }
// TestTransactionValidSignatures probes the validSignatures method of the // Transaction type. func TestTransactionValidSignatures(t *testing.T) { // Create keys for use in signing and verifying. sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } // Create UnlockConditions with 3 keys, 2 of which are required. The first // possible key is a standard signature. The second key is an unknown // signature type, which should always be accepted. The final type is an // entropy type, which should never be accepted. uc := UnlockConditions{ PublicKeys: []SiaPublicKey{ {Algorithm: SignatureEd25519, Key: pk[:]}, {}, {Algorithm: SignatureEntropy}, }, SignaturesRequired: 2, } // Create a transaction with each type of unlock condition. txn := Transaction{ SiacoinInputs: []SiacoinInput{ {UnlockConditions: uc}, }, FileContractRevisions: []FileContractRevision{ {UnlockConditions: uc}, }, SiafundInputs: []SiafundInput{ {UnlockConditions: uc}, }, } txn.FileContractRevisions[0].ParentID[0] = 1 // can't overlap with other objects txn.SiafundInputs[0].ParentID[0] = 2 // can't overlap with other objects // Create the signatures that spend the output. txn.TransactionSignatures = []TransactionSignature{ // First signatures use cryptography. { Timelock: 5, CoveredFields: CoveredFields{WholeTransaction: true}, }, { CoveredFields: CoveredFields{WholeTransaction: true}, }, { CoveredFields: CoveredFields{WholeTransaction: true}, }, // The second signatures should always work for being unrecognized // types. {PublicKeyIndex: 1}, {PublicKeyIndex: 1}, {PublicKeyIndex: 1}, } txn.TransactionSignatures[1].ParentID[0] = 1 txn.TransactionSignatures[2].ParentID[0] = 2 txn.TransactionSignatures[4].ParentID[0] = 1 txn.TransactionSignatures[5].ParentID[0] = 2 sigHash0 := txn.SigHash(0) sigHash1 := txn.SigHash(1) sigHash2 := txn.SigHash(2) sig0, err := crypto.SignHash(sigHash0, sk) if err != nil { t.Fatal(err) } sig1, err := crypto.SignHash(sigHash1, sk) if err != nil { t.Fatal(err) } sig2, err := crypto.SignHash(sigHash2, sk) if err != nil { t.Fatal(err) } txn.TransactionSignatures[0].Signature = sig0[:] txn.TransactionSignatures[1].Signature = sig1[:] txn.TransactionSignatures[2].Signature = sig2[:] // Check that the signing was successful. err = txn.validSignatures(10) if err != nil { t.Error(err) } // Corrupt one of the sigantures. sig0[0]++ txn.TransactionSignatures[0].Signature = sig0[:] err = txn.validSignatures(10) if err == nil { t.Error("Corrupted a signature but the txn was still accepted as valid!") } sig0[0]-- txn.TransactionSignatures[0].Signature = sig0[:] // Fail the validCoveredFields check. txn.TransactionSignatures[0].CoveredFields.SiacoinInputs = []uint64{33} err = txn.validSignatures(10) if err == nil { t.Error("failed to flunk the validCoveredFields check") } txn.TransactionSignatures[0].CoveredFields.SiacoinInputs = nil // Double spend a SiacoinInput, FileContractTermination, and SiafundInput. txn.SiacoinInputs = append(txn.SiacoinInputs, SiacoinInput{UnlockConditions: UnlockConditions{}}) err = txn.validSignatures(10) if err == nil { t.Error("failed to double spend a siacoin input") } txn.SiacoinInputs = txn.SiacoinInputs[:len(txn.SiacoinInputs)-1] txn.FileContractRevisions = append(txn.FileContractRevisions, FileContractRevision{UnlockConditions: UnlockConditions{}}) err = txn.validSignatures(10) if err == nil { t.Error("failed to double spend a file contract termination") } txn.FileContractRevisions = txn.FileContractRevisions[:len(txn.FileContractRevisions)-1] txn.SiafundInputs = append(txn.SiafundInputs, SiafundInput{UnlockConditions: UnlockConditions{}}) err = txn.validSignatures(10) if err == nil { t.Error("failed to double spend a siafund input") } txn.SiafundInputs = txn.SiafundInputs[:len(txn.SiafundInputs)-1] // Add a frivilous signature txn.TransactionSignatures = append(txn.TransactionSignatures, TransactionSignature{}) err = txn.validSignatures(10) if err != ErrFrivilousSignature { t.Error(err) } txn.TransactionSignatures = txn.TransactionSignatures[:len(txn.TransactionSignatures)-1] // Replace one of the cryptography signatures with an always-accepted // signature. This should get rejected because the always-accepted // signature has already been used. tmpTxn0 := txn.TransactionSignatures[0] txn.TransactionSignatures[0] = TransactionSignature{PublicKeyIndex: 1} err = txn.validSignatures(10) if err != ErrPublicKeyOveruse { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Fail the timelock check for signatures. err = txn.validSignatures(4) if err != ErrPrematureSignature { t.Error(err) } // Try to spend an entropy signature. txn.TransactionSignatures[0] = TransactionSignature{PublicKeyIndex: 2} err = txn.validSignatures(10) if err != ErrEntropyKey { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Try to point to a nonexistent public key. txn.TransactionSignatures[0] = TransactionSignature{PublicKeyIndex: 5} err = txn.validSignatures(10) if err != ErrInvalidPubKeyIndex { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Insert a malformed public key into the transaction. txn.SiacoinInputs[0].UnlockConditions.PublicKeys[0].Key = []byte{'b', 'a', 'd'} err = txn.validSignatures(10) if err == nil { t.Error(err) } txn.SiacoinInputs[0].UnlockConditions.PublicKeys[0].Key = pk[:] // Insert a malformed signature into the transaction. txn.TransactionSignatures[0].Signature = []byte{'m', 'a', 'l'} err = txn.validSignatures(10) if err == nil { t.Error(err) } txn.TransactionSignatures[0] = tmpTxn0 // Try to spend a transaction when not every required signature is // available. txn.TransactionSignatures = txn.TransactionSignatures[1:] err = txn.validSignatures(10) if err != ErrMissingSignatures { t.Error(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) } }
// 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") } }
// TestThreadedProbeHosts tests the threadedProbeHosts method. func TestThreadedProbeHosts(t *testing.T) { if testing.Short() { t.SkipNow() } hdb := bareHostDB() hdb.persist = &memPersist{} // create a host to send to threadedProbeHosts sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } h := new(hostEntry) h.NetAddress = "foo" h.AcceptingContracts = true h.PublicKey = types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } h.Reliability = baseWeight // enough to withstand a few failures // define a helper function for running threadedProbeHosts. We send the // hostEntry, close the channel, and then call threadedProbeHosts. // threadedProbeHosts will receive the host, loop once, and return after // seeing the channel has closed. // // NOTE: since threadedProbeHosts decrements hdb.threadGroup, we Add(100) // to prevent it from going negative. This is acceptable because we don't // call hdb.Close in this test. hdb.threadGroup.Add(100) runProbe := func(h *hostEntry) { hdb.scanPool <- h close(hdb.scanPool) hdb.threadedProbeHosts() // reset hdb.scanPool hdb.scanPool = make(chan *hostEntry, 1) } // make the dial fail hdb.dialer = probeDialer(func(modules.NetAddress, time.Duration) (net.Conn, error) { return nil, net.UnknownNetworkError("fail") }) runProbe(h) if len(hdb.ActiveHosts()) != 0 { t.Error("unresponsive host was added") } // make the RPC fail hdb.dialer = probeDialer(func(modules.NetAddress, time.Duration) (net.Conn, error) { ourPipe, theirPipe := net.Pipe() ourPipe.Close() return theirPipe, nil }) runProbe(h) if len(hdb.ActiveHosts()) != 0 { t.Error("unresponsive host was added") } // normal host hdb.dialer = probeDialer(func(modules.NetAddress, time.Duration) (net.Conn, error) { // create an in-memory conn and spawn a goroutine to handle our half ourConn, theirConn := net.Pipe() go func() { // read the RPC encoding.ReadObject(ourConn, new(types.Specifier), types.SpecifierLen) // write host settings crypto.WriteSignedObject(ourConn, modules.HostExternalSettings{ AcceptingContracts: true, NetAddress: "probed", }, sk) ourConn.Close() }() return theirConn, nil }) runProbe(h) if len(hdb.ActiveHosts()) != 1 { t.Error("host was not added") } }
// TestThreadedProbeHostsCorruption tests the threadedProbeHosts method, // specifically checking for corruption of the hostdb if the weight of a host // changes after a scan. func TestThreadedProbeHostsCorruption(t *testing.T) { if testing.Short() { t.SkipNow() } hdb := bareHostDB() hdb.persist = &memPersist{} // create a host to send to threadedProbeHosts sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } h := new(hostEntry) h.NetAddress = "foo" h.AcceptingContracts = true h.PublicKey = types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } h.Reliability = baseWeight // enough to withstand a few failures // define a helper function for running threadedProbeHosts. We send the // hostEntry, close the channel, and then call threadedProbeHosts. // threadedProbeHosts will receive the host, loop once, and return after // seeing the channel has closed. // // NOTE: since threadedProbeHosts decrements hdb.threadGroup, we Add(100) // to prevent it from going negative. This is acceptable because we don't // call hdb.Close in this test. hdb.threadGroup.Add(100) runProbe := func(h *hostEntry) { hdb.scanPool <- h close(hdb.scanPool) hdb.threadedProbeHosts() // reset hdb.scanPool hdb.scanPool = make(chan *hostEntry, 1) } // Add a normal host. hdb.dialer = probeDialer(func(modules.NetAddress, time.Duration) (net.Conn, error) { // create an in-memory conn and spawn a goroutine to handle our half ourConn, theirConn := net.Pipe() go func() { // read the RPC encoding.ReadObject(ourConn, new(types.Specifier), types.SpecifierLen) // write host settings crypto.WriteSignedObject(ourConn, modules.HostExternalSettings{ AcceptingContracts: true, StoragePrice: types.NewCurrency64(15e6), NetAddress: "probed", }, sk) ourConn.Close() }() return theirConn, nil }) runProbe(h) if len(hdb.ActiveHosts()) != 1 { t.Error("host was not added") } // Add the host again, this time changing the storage price, which will // change the weight of the host, which at one point would cause a // corruption of the host tree. hdb.dialer = probeDialer(func(modules.NetAddress, time.Duration) (net.Conn, error) { // create an in-memory conn and spawn a goroutine to handle our half ourConn, theirConn := net.Pipe() go func() { // read the RPC encoding.ReadObject(ourConn, new(types.Specifier), types.SpecifierLen) // write host settings crypto.WriteSignedObject(ourConn, modules.HostExternalSettings{ AcceptingContracts: true, StoragePrice: types.NewCurrency64(15e3), // Lower than the previous, to cause a higher weight. NetAddress: "probed", }, sk) ourConn.Close() }() return theirConn, nil }) runProbe(h) if len(hdb.ActiveHosts()) != 1 { t.Error("host was not added") } // Check that the host tree has not been corrupted. err = repeatCheck(hdb.hostTree) if err != nil { t.Error(err) } err = uniformTreeVerification(hdb, 1) if err != nil { 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 }
// TestAnnouncementHandling checks that CreateAnnouncement and // DecodeAnnouncement work together correctly. func TestAnnouncementHandling(t *testing.T) { t.Parallel() // Create the keys that will be used to generate the announcement. sk, pk, err := crypto.GenerateKeyPair() if err != nil { t.Fatal(err) } spk := types.SiaPublicKey{ Algorithm: types.SignatureEd25519, Key: pk[:], } addr := NetAddress("f.o:1234") // Generate the announcement. annBytes, err := CreateAnnouncement(addr, spk, sk) if err != nil { t.Fatal(err) } // Decode the announcement decAddr, decPubKey, err := DecodeAnnouncement(annBytes) if err != nil { t.Fatal(err) } if decPubKey.Algorithm != spk.Algorithm { t.Error("decoded announcement has the wrong algorithm on the public key") } if decAddr != addr { t.Error("decoded announcement has the wrong net address") } if !bytes.Equal(decPubKey.Key, spk.Key) { t.Error("decoded announcement has the wrong public key") } // Corrupt the data, and see that decoding fails. Decoding should fail // because the signature should not be valid anymore. // // First 16 bytes are the host announcement prefix, followed by 8 bytes // describing the length of the net address, followed by the net address. // Corrupt the net address. annBytes[25]++ _, _, err = DecodeAnnouncement(annBytes) if err != crypto.ErrInvalidSignature { t.Error(err) } annBytes[25]-- // The final byte is going to be a part of the signature. Corrupt the final // byte and verify that there's an error. lastIndex := len(annBytes) - 1 annBytes[lastIndex]++ _, _, err = DecodeAnnouncement(annBytes) if err != crypto.ErrInvalidSignature { t.Error(err) } annBytes[lastIndex]-- // Pass in a bad specifier - change the host announcement type. annBytes[0]++ _, _, err = DecodeAnnouncement(annBytes) if err != ErrAnnNotAnnouncement { t.Error(err) } annBytes[0]-- // Pass in a bad signature algorithm. 16 bytes to pass the specifier, 8+8 bytes to pass the net address. annBytes[33]++ _, _, err = DecodeAnnouncement(annBytes) if err != ErrAnnUnrecognizedSignature { t.Error(err) } annBytes[33]-- // Cause the decoding to fail altogether. _, _, err = DecodeAnnouncement(annBytes[:12]) if err == nil { t.Error(err) } }
// 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 }