// considerContract checks that the provided transaction matches the host's // terms, and doesn't contain any flagrant errors. func (h *Host) considerContract(txn types.Transaction, renterKey types.SiaPublicKey) error { // Check that there is only one file contract. // TODO: check that the txn is empty except for the contract? if len(txn.FileContracts) != 1 { return errors.New("transaction should have only one file contract") } // convenience variables fc := txn.FileContracts[0] duration := fc.WindowStart - h.blockHeight voidAddress := types.UnlockHash{} // check contract fields for sanity and acceptability switch { case fc.FileSize != 0: return errors.New("initial file size must be 0") case fc.WindowStart <= h.blockHeight: return errors.New("window start cannot be in the past") case duration < h.MinDuration || duration > h.MaxDuration: return errors.New("duration is out of bounds") case fc.WindowEnd <= fc.WindowStart: return errors.New("window cannot end before it starts") case fc.WindowEnd-fc.WindowStart < h.WindowSize: return errors.New("challenge window is not large enough") case fc.FileMerkleRoot != crypto.Hash{}: return errors.New("bad file contract Merkle root") case fc.Payout.IsZero(): return errors.New("bad file contract payout") case len(fc.ValidProofOutputs) != 2: return errors.New("bad file contract valid proof outputs") case len(fc.MissedProofOutputs) != 2: return errors.New("bad file contract missed proof outputs") case !fc.ValidProofOutputs[1].Value.IsZero(), !fc.MissedProofOutputs[1].Value.IsZero(): return errors.New("file contract collateral is not zero") case fc.ValidProofOutputs[1].UnlockHash != h.UnlockHash: return errors.New("file contract valid proof output not sent to host") case fc.MissedProofOutputs[1].UnlockHash != voidAddress: return errors.New("file contract missed proof output not sent to void") } // check unlock hash uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterKey, h.publicKey}, SignaturesRequired: 2, } if fc.UnlockHash != uc.UnlockHash() { return errors.New("bad file contract unlock hash") } return nil }
// SubmitBlock takes a solved block and submits it to the blockchain. // SubmitBlock should not be called with a lock. func (m *Miner) SubmitBlock(b types.Block) error { // Give the block to the consensus set. err := m.cs.AcceptBlock(b) // Add the miner to the blocks list if the only problem is that it's stale. if err == modules.ErrNonExtendingBlock { m.mu.Lock() m.blocksFound = append(m.blocksFound, b.ID()) m.mu.Unlock() } if err != nil { m.tpool.PurgeTransactionPool() m.log.Println("ERROR: an invalid block was submitted:", err) return err } m.mu.Lock() defer m.mu.Unlock() // Grab a new address for the miner. Call may fail if the wallet is locked // or if the wallet addresses have been exhausted. m.blocksFound = append(m.blocksFound, b.ID()) var uc types.UnlockConditions uc, err = m.wallet.NextAddress() if err == nil { // Only update the address if there was no error. m.address = uc.UnlockHash() } return err }
// managedSubmitBlock takes a solved block and submits it to the blockchain. // managedSubmitBlock should not be called with a lock. func (m *Miner) managedSubmitBlock(b types.Block) error { // Give the block to the consensus set. err := m.cs.AcceptBlock(b) // Add the miner to the blocks list if the only problem is that it's stale. if err == modules.ErrNonExtendingBlock { m.mu.Lock() m.persist.BlocksFound = append(m.persist.BlocksFound, b.ID()) m.mu.Unlock() m.log.Println("Mined a stale block - block appears valid but does not extend the blockchain") return err } if err == modules.ErrBlockUnsolved { m.log.Println("Mined an unsolved block - header submission appears to be incorrect") return err } if err != nil { m.tpool.PurgeTransactionPool() m.log.Critical("ERROR: an invalid block was submitted:", err) return err } m.mu.Lock() defer m.mu.Unlock() // Grab a new address for the miner. Call may fail if the wallet is locked // or if the wallet addresses have been exhausted. m.persist.BlocksFound = append(m.persist.BlocksFound, b.ID()) var uc types.UnlockConditions uc, err = m.wallet.NextAddress() if err != nil { return err } m.persist.Address = uc.UnlockHash() return m.saveSync() }
// 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 }
// submitBlock takes a solved block and submits it to the blockchain. // submitBlock should not be called with a lock. func (m *Miner) SubmitBlock(b types.Block) error { // Give the block to the consensus set. err := m.cs.AcceptBlock(b) if err != nil { m.tpool.PurgeTransactionPool() m.log.Println("ERROR: an invalid block was submitted:", err) return err } lockID := m.mu.Lock() defer m.mu.Unlock(lockID) // Grab a new address for the miner. Call may fail if the wallet is locked // or if the wallet addresses have been exhausted. m.blocksFound = append(m.blocksFound, b.ID()) var uc types.UnlockConditions uc, err = m.wallet.NextAddress() if err == nil { // Special case: only update the address if there was no error. m.address = uc.UnlockHash() } return 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") } }
// 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 }
// verifyKeys checks a set of keys on disk to see that they can spend funds // sent to their address. func verifyKeys(uc types.UnlockConditions, folder string, keyname string) error { keysRequired := uc.SignaturesRequired totalKeys := uint64(len(uc.PublicKeys)) // Load the keys from disk back into memory, then verify that the keys on // disk are able to sign outputs in transactions. loadedKeys := make([]KeyPair, totalKeys) for i := 0; i < len(loadedKeys); i++ { err := encoding.ReadFile(filepath.Join(folder, keyname+"_Key"+strconv.Itoa(i)+FileExtension), &loadedKeys[i]) if err != nil { return err } if loadedKeys[i].Header != FileHeader { return ErrUnknownHeader } if loadedKeys[i].Version != FileVersion { return ErrUnknownVersion } } // Check that the keys can be used to spend transactions. for _, loadedKey := range loadedKeys { if loadedKey.UnlockConditions.UnlockHash() != uc.UnlockHash() { return ErrCorruptedKey } } // Create a transaction for the keys to sign. txn := types.Transaction{ SiafundInputs: []types.SiafundInput{ types.SiafundInput{ UnlockConditions: loadedKeys[0].UnlockConditions, }, }, } // Loop through and sign the transaction multiple times. All keys will be // used at least once by the time the loop terminates. var i uint64 for i != totalKeys { // i tracks which key is next to be used. If i + RequiredKeys results // in going out-of-bounds, reduce i so that the last key will be used // for the final signature. if i+keysRequired > totalKeys { i = totalKeys - keysRequired } var j uint64 for j < keysRequired { txn.TransactionSignatures = append(txn.TransactionSignatures, types.TransactionSignature{ PublicKeyIndex: i, CoveredFields: types.CoveredFields{WholeTransaction: true}, }) sigHash := txn.SigHash(int(j)) sig, err := crypto.SignHash(sigHash, loadedKeys[i].SecretKey) if err != nil { return err } txn.TransactionSignatures[j].Signature = sig[:] i++ j++ } // Check that the signature is valid. err := txn.StandaloneValid(0) if err != nil { return err } // Delete all of the signatures for the next iteration. txn.TransactionSignatures = nil } return nil }
// considerContract checks that the provided transaction matches the host's // terms, and doesn't contain any flagrant errors. func (h *Host) considerContract(txn types.Transaction, renterKey types.SiaPublicKey, filesize uint64, merkleRoot crypto.Hash) error { // Check that there is only one file contract. if len(txn.FileContracts) != 1 { return errors.New("transaction should have only one file contract") } // convenience variables fc := txn.FileContracts[0] duration := fc.WindowStart - h.blockHeight minPayment := types.NewCurrency64(filesize).Mul(types.NewCurrency64(uint64(duration))).Mul(h.settings.Price) expectedOutputSum := types.PostTax(h.blockHeight, fc.Payout) // check contract fields for sanity and acceptability switch { // Check for legal filesize and content. case fc.FileSize != filesize: return errors.New("bad initial file size") case fc.FileSize >= uint64(h.spaceRemaining): return ErrHostCapacity case fc.FileMerkleRoot != merkleRoot: return errors.New("bad file contract Merkle root") // Check for legal duration and proof window. case fc.WindowStart <= h.blockHeight: return errors.New("window start cannot be in the past") case duration < h.settings.MinDuration || duration > h.settings.MaxDuration: return errors.New("duration is out of bounds") case fc.WindowEnd <= fc.WindowStart: return errors.New("window cannot end before it starts") case fc.WindowEnd-fc.WindowStart < h.settings.WindowSize: return errors.New("challenge window is not large enough") // Check for legal payout. case fc.Payout.IsZero(): return errors.New("bad file contract payout") case len(fc.ValidProofOutputs) != 2: return errors.New("bad file contract valid proof outputs") case len(fc.MissedProofOutputs) != 2: return errors.New("bad file contract missed proof outputs") case fc.ValidProofOutputs[0].Value.Add(fc.ValidProofOutputs[1].Value).Cmp(expectedOutputSum) != 0, fc.MissedProofOutputs[0].Value.Add(fc.MissedProofOutputs[1].Value).Cmp(expectedOutputSum) != 0: return errors.New("file contract outputs do not sum to original payout") case fc.ValidProofOutputs[1].UnlockHash != h.settings.UnlockHash: return errors.New("file contract valid proof output not sent to host") case fc.ValidProofOutputs[1].Value.Cmp(minPayment) < 0: return ErrLowPayment case fc.MissedProofOutputs[0].Value.Cmp(fc.ValidProofOutputs[0].Value) != 0: return errors.New("file contract missed renter payout does not match valid payout") case fc.MissedProofOutputs[1].UnlockHash != (types.UnlockHash{}): return errors.New("file contract missed proof output not sent to void") } // check unlock hash uc := types.UnlockConditions{ PublicKeys: []types.SiaPublicKey{renterKey, h.publicKey}, SignaturesRequired: 2, } if fc.UnlockHash != uc.UnlockHash() { return errors.New("bad file contract unlock hash") } return nil }
// 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 }