Skip to content

Commit

Permalink
wallet: release inputs on error
Browse files Browse the repository at this point in the history
  • Loading branch information
peterjan committed May 7, 2024
1 parent a64eb58 commit 6cfd165
Show file tree
Hide file tree
Showing 10 changed files with 50 additions and 21 deletions.
12 changes: 7 additions & 5 deletions host/contracts/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,16 +158,17 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, height

fee := cm.tpool.RecommendedFee().Mul64(1000)
revisionTxn.MinerFees = append(revisionTxn.MinerFees, fee)
toSign, discard, err := cm.wallet.FundTransaction(&revisionTxn, fee)
toSign, release, err := cm.wallet.FundTransaction(&revisionTxn, fee)
if err != nil {
log.Error("failed to fund revision transaction", zap.Error(err))
return
}
defer discard()
if err := cm.wallet.SignTransaction(cs, &revisionTxn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
log.Error("failed to sign revision transaction", zap.Error(err))
return
} else if err := cm.tpool.AcceptTransactionSet([]types.Transaction{revisionTxn}); err != nil {
release()
log.Error("failed to broadcast revision transaction", zap.Error(err))
return
}
Expand Down Expand Up @@ -215,14 +216,12 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, height
StorageProofs: []types.StorageProof{sp},
},
}
intermediateToSign, discard, err := cm.wallet.FundTransaction(&resolutionTxnSet[0], fee)
intermediateToSign, release, err := cm.wallet.FundTransaction(&resolutionTxnSet[0], fee)
if err != nil {
log.Error("failed to fund resolution transaction", zap.Error(err))
registerContractAlert(alerts.SeverityError, "Failed to fund resolution transaction", err)
return
}
defer discard()

// add the intermediate output to the proof transaction
resolutionTxnSet[1].SiacoinInputs = append(resolutionTxnSet[1].SiacoinInputs, types.SiacoinInput{
ParentID: resolutionTxnSet[0].SiacoinOutputID(0),
Expand All @@ -231,12 +230,15 @@ func (cm *ContractManager) handleContractAction(id types.FileContractID, height
proofToSign := []types.Hash256{types.Hash256(resolutionTxnSet[1].SiacoinInputs[0].ParentID)}
start = time.Now()
if err := cm.wallet.SignTransaction(cs, &resolutionTxnSet[0], intermediateToSign, types.CoveredFields{WholeTransaction: true}); err != nil { // sign the intermediate transaction
release()
log.Error("failed to sign resolution intermediate transaction", zap.Error(err))
return
} else if err := cm.wallet.SignTransaction(cs, &resolutionTxnSet[1], proofToSign, types.CoveredFields{WholeTransaction: true}); err != nil { // sign the proof transaction
release()
log.Error("failed to sign resolution transaction", zap.Error(err))
return
} else if err := cm.tpool.AcceptTransactionSet(resolutionTxnSet); err != nil { // broadcast the transaction set
release()
buf, _ := json.Marshal(resolutionTxnSet)
log.Error("failed to broadcast resolution transaction set", zap.Error(err), zap.ByteString("transactionSet", buf))
registerContractAlert(alerts.SeverityError, "Failed to broadcast resolution transaction set", err)
Expand Down
5 changes: 3 additions & 2 deletions host/contracts/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ func formContract(renterKey, hostKey types.PrivateKey, start, end uint64, renter
txn := types.Transaction{
FileContracts: []types.FileContract{contract},
}
toSign, discard, err := w.FundTransaction(&txn, formationCost.Add(hostPayout)) // we're funding both sides of the payout
toSign, release, err := w.FundTransaction(&txn, formationCost.Add(hostPayout)) // we're funding both sides of the payout
if err != nil {
return contracts.SignedRevision{}, fmt.Errorf("failed to fund transaction: %w", err)
}
defer discard()
if err := w.SignTransaction(state, &txn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
return contracts.SignedRevision{}, fmt.Errorf("failed to sign transaction: %w", err)
} else if err := tp.AcceptTransactionSet([]types.Transaction{txn}); err != nil {
release()
return contracts.SignedRevision{}, fmt.Errorf("failed to accept transaction set: %w", err)
}
revision := types.FileContractRevision{
Expand Down
3 changes: 2 additions & 1 deletion host/settings/announce.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,16 @@ func (m *ConfigManager) Announce() error {
if err != nil {
return fmt.Errorf("failed to fund transaction: %w", err)
}
defer release()
// sign the transaction
err = m.wallet.SignTransaction(m.cm.TipState(), &txn, toSign, types.CoveredFields{WholeTransaction: true})
if err != nil {
release()
return fmt.Errorf("failed to sign transaction: %w", err)
}
// broadcast the transaction
err = m.tp.AcceptTransactionSet([]types.Transaction{txn})
if err != nil {
release()
return fmt.Errorf("failed to broadcast transaction: %w", err)
}
m.log.Debug("broadcast announcement", zap.String("transactionID", txn.ID().String()), zap.String("netaddress", settings.NetAddress), zap.String("cost", minerFee.ExactString()))
Expand Down
3 changes: 2 additions & 1 deletion internal/test/renter.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,14 +109,15 @@ func (r *Renter) FormContract(ctx context.Context, hostAddr string, hostKey type
if err != nil {
return crhp2.ContractRevision{}, fmt.Errorf("failed to fund transaction: %w", err)
}
defer release()

if err := r.wallet.SignTransaction(cs, &formationTxn, toSign, explicitCoveredFields(formationTxn)); err != nil {
release()
return crhp2.ContractRevision{}, fmt.Errorf("failed to sign transaction: %w", err)
}

revision, _, err := rhp2.RPCFormContract(ctx, t, r.privKey, []types.Transaction{formationTxn})
if err != nil {
release()
return crhp2.ContractRevision{}, fmt.Errorf("failed to form contract: %w", err)
}
return revision, nil
Expand Down
8 changes: 7 additions & 1 deletion internal/test/rhp/v3/rhp.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,6 @@ func (s *Session) RenewContract(revision *rhp2.ContractRevision, hostAddr types.
if err != nil {
return rhp2.ContractRevision{}, nil, fmt.Errorf("failed to fund transaction: %w", err)
}
defer release()

clearingSigHash := hashFinalRevision(clearingRevision, renewal)
renewReq := &rhp3.RPCRenewContractRequest{
Expand All @@ -472,13 +471,16 @@ func (s *Session) RenewContract(revision *rhp2.ContractRevision, hostAddr types.
FinalRevisionSignature: renterKey.SignHash(clearingSigHash),
}
if err := stream.WriteResponse(renewReq); err != nil {
release()
return rhp2.ContractRevision{}, nil, fmt.Errorf("failed to write renew request: %w", err)
}

var hostAdditions rhp3.RPCRenewContractHostAdditions
if err := stream.ReadResponse(&hostAdditions, 4096); err != nil {
release()
return rhp2.ContractRevision{}, nil, fmt.Errorf("failed to read host additions response: %w", err)
} else if !s.hostKey.VerifyHash(clearingSigHash, hostAdditions.FinalRevisionSignature) {
release()
return rhp2.ContractRevision{}, nil, fmt.Errorf("host final revision signature invalid")
}
// add the host's additions to the transaction set
Expand All @@ -488,6 +490,7 @@ func (s *Session) RenewContract(revision *rhp2.ContractRevision, hostAddr types.

// sign the transaction
if err := s.w.SignTransaction(state, &renewTxn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
return rhp2.ContractRevision{}, nil, fmt.Errorf("failed to sign transaction: %w", err)
}

Expand All @@ -506,13 +509,16 @@ func (s *Session) RenewContract(revision *rhp2.ContractRevision, hostAddr types.
},
}
if err := stream.WriteResponse(renterSigsResp); err != nil {
release()
return rhp2.ContractRevision{}, nil, fmt.Errorf("failed to write renter signatures: %w", err)
}

var hostSigsResp rhp3.RPCRenewSignatures
if err := stream.ReadResponse(&hostSigsResp, 4096); err != nil {
release()
return rhp2.ContractRevision{}, nil, fmt.Errorf("failed to read host signatures: %w", err)
} else if err := validateHostRevisionSignature(hostSigsResp.RevisionSignature, renewRevision.ParentID, renewSigHash, s.hostKey); err != nil {
release()
return rhp2.ContractRevision{}, nil, fmt.Errorf("invalid host revision signature: %w", err)
}
return rhp2.ContractRevision{
Expand Down
3 changes: 2 additions & 1 deletion internal/test/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ func (w *Wallet) SendSiacoins(outputs []types.SiacoinOutput) (txn types.Transact
if err != nil {
return types.Transaction{}, fmt.Errorf("failed to fund transaction: %w", err)
}
defer release()
if err := w.SignTransaction(w.ChainManager().TipState(), &txn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
return txn, fmt.Errorf("failed to sign transaction: %w", err)
} else if err := w.tp.AcceptTransactionSet([]types.Transaction{txn}); err != nil {
release()
return txn, fmt.Errorf("failed to accept transaction set: %w", err)
}
return txn, nil
Expand Down
18 changes: 14 additions & 4 deletions rhp/v2/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (sh *SessionHandler) rpcFormContract(s *session, log *zap.Logger) (contract

// calculate the host's collateral and add the inputs to the transaction
renterInputs, renterOutputs := len(formationTxn.SiacoinInputs), len(formationTxn.SiacoinOutputs)
toSign, discard, err := sh.wallet.FundTransaction(formationTxn, hostCollateral)
toSign, release, err := sh.wallet.FundTransaction(formationTxn, hostCollateral)
if err != nil {
remoteErr := ErrHostInternalError
if errors.Is(err, wallet.ErrNotEnoughFunds) {
Expand All @@ -167,7 +167,6 @@ func (sh *SessionHandler) rpcFormContract(s *session, log *zap.Logger) (contract
s.t.WriteResponseErr(fmt.Errorf("failed to fund formation transaction: %w", remoteErr))
return contracts.Usage{}, fmt.Errorf("failed to fund formation transaction: %w", err)
}
defer discard()

// create an initial revision for the contract
initialRevision := rhp.InitialRevision(formationTxn, hostPub.UnlockKey(), renterPub.UnlockKey())
Expand All @@ -180,14 +179,17 @@ func (sh *SessionHandler) rpcFormContract(s *session, log *zap.Logger) (contract
Outputs: formationTxn.SiacoinOutputs[renterOutputs:],
}
if err := s.writeResponse(hostAdditionsResp, 30*time.Second); err != nil {
release()
return contracts.Usage{}, fmt.Errorf("failed to write host additions: %w", err)
}

// read and validate the renter's signatures
var renterSignaturesResp rhp2.RPCFormContractSignatures
if err := s.readResponse(&renterSignaturesResp, 10*minMessageSize, 30*time.Second); err != nil {
release()
return contracts.Usage{}, fmt.Errorf("failed to read renter signatures: %w", err)
} else if err := validateRenterRevisionSignature(renterSignaturesResp.RevisionSignature, initialRevision.ParentID, sigHash, renterPub); err != nil {
release()
err := fmt.Errorf("contract rejected: validation failed: %w", err)
s.t.WriteResponseErr(err)
return contracts.Usage{}, err
Expand All @@ -198,9 +200,11 @@ func (sh *SessionHandler) rpcFormContract(s *session, log *zap.Logger) (contract

// sign and broadcast the formation transaction
if err = sh.wallet.SignTransaction(sh.cm.TipState(), formationTxn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
s.t.WriteResponseErr(ErrHostInternalError)
return contracts.Usage{}, fmt.Errorf("failed to sign formation transaction: %w", err)
} else if err = sh.tpool.AcceptTransactionSet(formationTxnSet); err != nil {
release()
err = fmt.Errorf("failed to broadcast formation transaction: %w", err)
buf, _ := json.Marshal(formationTxnSet)
log.Error("failed to broadcast formation transaction", zap.Error(err), zap.String("txnset", string(buf)))
Expand Down Expand Up @@ -325,7 +329,7 @@ func (sh *SessionHandler) rpcRenewAndClearContract(s *session, log *zap.Logger)
}

renterInputs, renterOutputs := len(renewalTxn.SiacoinInputs), len(renewalTxn.SiacoinOutputs)
toSign, discard, err := sh.wallet.FundTransaction(&renewalTxn, lockedCollateral)
toSign, release, err := sh.wallet.FundTransaction(&renewalTxn, lockedCollateral)
if err != nil {
remoteErr := ErrHostInternalError
if errors.Is(err, wallet.ErrNotEnoughFunds) {
Expand All @@ -334,29 +338,32 @@ func (sh *SessionHandler) rpcRenewAndClearContract(s *session, log *zap.Logger)
s.t.WriteResponseErr(fmt.Errorf("failed to fund renewal transaction: %w", remoteErr))
return contracts.Usage{}, fmt.Errorf("failed to fund renewal transaction: %w", err)
}
defer discard()

// send the renter the host additions to the renewal txn
hostAdditionsResp := &rhp2.RPCFormContractAdditions{
Inputs: renewalTxn.SiacoinInputs[renterInputs:],
Outputs: renewalTxn.SiacoinOutputs[renterOutputs:],
}
if err = s.writeResponse(hostAdditionsResp, 30*time.Second); err != nil {
release()
return contracts.Usage{}, fmt.Errorf("failed to write host additions: %w", err)
}

// read the renter's signatures for the renewal
var renterSigsResp rhp2.RPCRenewAndClearContractSignatures
if err = s.readResponse(&renterSigsResp, minMessageSize, 30*time.Second); err != nil {
release()
return contracts.Usage{}, fmt.Errorf("failed to read renter signatures: %w", err)
} else if len(renterSigsResp.RevisionSignature.Signature) != 64 {
release()
return contracts.Usage{}, fmt.Errorf("invalid renter signature length: %w", ErrInvalidRenterSignature)
}

// add the renter's signatures to the formation transaction
renewalTxn.Signatures = append(renewalTxn.Signatures, renterSigsResp.ContractSignatures...)
// sign the transaction
if err = sh.wallet.SignTransaction(state, &renewalTxn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
s.t.WriteResponseErr(ErrHostInternalError)
return contracts.Usage{}, fmt.Errorf("failed to sign renewal transaction: %w", err)
}
Expand All @@ -368,6 +375,7 @@ func (sh *SessionHandler) rpcRenewAndClearContract(s *session, log *zap.Logger)
clearingRevSigHash := rhp.HashRevision(clearingRevision)
// important: verify using the existing contract's renter key
if !s.contract.RenterKey().VerifyHash(clearingRevSigHash, renterSigsResp.FinalRevisionSignature) {
release()
err := fmt.Errorf("failed to verify clearing revision signature: %w", ErrInvalidRenterSignature)
s.t.WriteResponseErr(err)
return contracts.Usage{}, err
Expand All @@ -377,6 +385,7 @@ func (sh *SessionHandler) rpcRenewAndClearContract(s *session, log *zap.Logger)
renewalSigHash := rhp.HashRevision(initialRevision)
renterRenewalSig := *(*types.Signature)(renterSigsResp.RevisionSignature.Signature)
if !renterKey.VerifyHash(renewalSigHash, renterRenewalSig) {
release()
err := fmt.Errorf("failed to verify renewal revision signature: %w", ErrInvalidRenterSignature)
s.t.WriteResponseErr(err)
return contracts.Usage{}, err
Expand All @@ -396,6 +405,7 @@ func (sh *SessionHandler) rpcRenewAndClearContract(s *session, log *zap.Logger)
// broadcast the transaction
renewalTxnSet = append(renewalParents, renewalTxn)
if err = sh.tpool.AcceptTransactionSet(renewalTxnSet); err != nil {
release()
err = fmt.Errorf("failed to broadcast renewal transaction: %w", err)
s.t.WriteResponseErr(err)
return contracts.Usage{}, err
Expand Down
10 changes: 6 additions & 4 deletions rhp/v2/rpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,18 +175,19 @@ func TestRenew(t *testing.T) {
}

cost := rhp2.ContractRenewalCost(state, renewed, settings.ContractPrice, types.ZeroCurrency, basePrice)
toSign, discard, err := renter.Wallet().FundTransaction(&renewalTxn, cost)
toSign, release, err := renter.Wallet().FundTransaction(&renewalTxn, cost)
if err != nil {
t.Fatal(err)
}
defer discard()

if err := renter.Wallet().SignTransaction(host.TipState(), &renewalTxn, toSign, wallet.ExplicitCoveredFields(renewalTxn)); err != nil {
release()
t.Fatal(err)
}

renewal, _, err := session.RenewContract(context.Background(), []types.Transaction{renewalTxn}, settings.BaseRPCPrice)
if err != nil {
release()
t.Fatal(err)
}

Expand Down Expand Up @@ -285,18 +286,19 @@ func TestRenew(t *testing.T) {
}

cost := rhp2.ContractRenewalCost(state, renewed, settings.ContractPrice, types.ZeroCurrency, basePrice)
toSign, discard, err := renter.Wallet().FundTransaction(&renewalTxn, cost)
toSign, release, err := renter.Wallet().FundTransaction(&renewalTxn, cost)
if err != nil {
t.Fatal(err)
}
defer discard()

if err := renter.Wallet().SignTransaction(host.TipState(), &renewalTxn, toSign, wallet.ExplicitCoveredFields(renewalTxn)); err != nil {
release()
t.Fatal(err)
}

// try to renew the contract without paying the remaining value, should fail
if _, _, err := session.RenewContract(context.Background(), []types.Transaction{renewalTxn}, types.ZeroCurrency); err == nil {
release()
t.Fatal("expected renewal to fail")
} else if err := session.Close(); err != nil {
t.Fatal(err)
Expand Down
6 changes: 5 additions & 1 deletion rhp/v3/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,26 +346,28 @@ func (sh *SessionHandler) handleRPCRenew(s *rhp3.Stream, log *zap.Logger) (contr
s.WriteResponseErr(fmt.Errorf("failed to fund renewal transaction: %w", remoteErr))
return contracts.Usage{}, fmt.Errorf("failed to fund renewal transaction: %w", err)
}
defer release()

hostAdditions := &rhp3.RPCRenewContractHostAdditions{
SiacoinInputs: renewalTxn.SiacoinInputs[renterInputs:],
SiacoinOutputs: renewalTxn.SiacoinOutputs[renterOutputs:],
FinalRevisionSignature: signedClearingRevision.HostSignature,
}
if err := s.WriteResponse(hostAdditions); err != nil {
release()
return contracts.Usage{}, fmt.Errorf("failed to write host additions: %w", err)
}

var renterSigsResp rhp3.RPCRenewSignatures
if err := s.ReadRequest(&renterSigsResp, 10*maxRequestSize); err != nil {
release()
return contracts.Usage{}, fmt.Errorf("failed to read renter signatures: %w", err)
}

// create the initial revision and verify the renter's signature
renewalRevision := rhp.InitialRevision(&renewalTxn, hostUnlockKey, req.RenterKey)
renewalSigHash := rhp.HashRevision(renewalRevision)
if err := validateRenterRevisionSignature(renterSigsResp.RevisionSignature, renewalRevision.ParentID, renewalSigHash, renterKey); err != nil {
release()
err := fmt.Errorf("failed to verify renter revision signature: %w", ErrInvalidRenterSignature)
s.WriteResponseErr(err)
return contracts.Usage{}, err
Expand Down Expand Up @@ -400,11 +402,13 @@ func (sh *SessionHandler) handleRPCRenew(s *rhp3.Stream, log *zap.Logger) (contr

// sign and broadcast the transaction
if err := sh.wallet.SignTransaction(sh.chain.TipState(), &renewalTxn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
s.WriteResponseErr(fmt.Errorf("failed to sign renewal transaction: %w", ErrHostInternalError))
return contracts.Usage{}, fmt.Errorf("failed to sign renewal transaction: %w", err)
}
renewalTxnSet := append(parents, renewalTxn)
if err := sh.tpool.AcceptTransactionSet(renewalTxnSet); err != nil {
release()
err = fmt.Errorf("failed to broadcast renewal transaction: %w", err)
s.WriteResponseErr(err)
return contracts.Usage{}, err
Expand Down
3 changes: 2 additions & 1 deletion wallet/wallet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,6 @@ func TestWalletUTXOSelection(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer release()

if len(txn.SiacoinInputs) != 11 {
t.Fatalf("expected 10 additional defrag inputs, got %v", len(toSign)-1)
Expand All @@ -399,8 +398,10 @@ func TestWalletUTXOSelection(t *testing.T) {
}

if err := w.SignTransaction(w.TipState(), &txn, toSign, types.CoveredFields{WholeTransaction: true}); err != nil {
release()
t.Fatal(err)
} else if err := w.TPool().AcceptTransactionSet([]types.Transaction{txn}); err != nil {
release()
t.Fatal(err)
}
}

0 comments on commit 6cfd165

Please sign in to comment.