Skip to content

Commit

Permalink
Merge pull request #575 from SiaFoundation/nate/fix-rejections-for-pa…
Browse files Browse the repository at this point in the history
…rtial-parents

Retry formation transaction sets
  • Loading branch information
n8maninger authored Jan 15, 2025
2 parents a2dcfb2 + 4097277 commit deb74a2
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

# Fixed an issue with contracts sometimes being rejected if one of their parent transactions was confirmed in an earlier block
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.24
github.com/shopspring/decimal v1.4.0
go.sia.tech/core v0.9.0
go.sia.tech/coreutils v0.9.1
go.sia.tech/coreutils v0.10.0
go.sia.tech/jape v0.12.1
go.sia.tech/mux v1.3.0
go.sia.tech/web/hostd v0.56.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.sia.tech/core v0.9.0 h1:qV7V8nkNaPvBEhkbwgrETTkb7JCMcAnKUQt9nUumP4k=
go.sia.tech/core v0.9.0/go.mod h1:3NAvYHuzAZg9vP6pyIMOxjTkgHBQ3vx9cXTqRF6oEa4=
go.sia.tech/coreutils v0.9.1 h1:2SukWrF9o18HIG+BNmNk4SZw48+h+cM0gR8jMtG2cQ4=
go.sia.tech/coreutils v0.9.1/go.mod h1:A/tNYSBdryxCFaIpvW04YvdQ4e2j8iGuHoIw70+ZYXc=
go.sia.tech/coreutils v0.10.0 h1:uKkxq2llz49vDRUDdGlPbHCMKEf5OJhCDkGxIZiLedA=
go.sia.tech/coreutils v0.10.0/go.mod h1:/m6/PFV377MeJ42uO1xd+4//B1TNaQYF7DURrILHDvA=
go.sia.tech/jape v0.12.1 h1:xr+o9V8FO8ScRqbSaqYf9bjj1UJ2eipZuNcI1nYousU=
go.sia.tech/jape v0.12.1/go.mod h1:wU+h6Wh5olDjkPXjF0tbZ1GDgoZ6VTi4naFw91yyWC4=
go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c=
Expand Down
111 changes: 111 additions & 0 deletions host/contracts/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,117 @@ func TestContractLifecycle(t *testing.T) {
assertContractStatus(t, node.Contracts, rev.Revision.ParentID, contracts.ContractStatusPending)
assertContractMetrics(t, node.Store, 0, 0, types.ZeroCurrency, types.ZeroCurrency)
})

t.Run("partially confirmed formation set", func(t *testing.T) {
hostKey, renterKey := types.GeneratePrivateKey(), types.GeneratePrivateKey()

dir := t.TempDir()
log := zaptest.NewLogger(t)

network, genesis := testutil.V1Network()
node := testutil.NewHostNode(t, hostKey, network, genesis, log)

result := make(chan error, 1)
if _, err := node.Volumes.AddVolume(context.Background(), filepath.Join(dir, "data.dat"), 10, result); err != nil {
t.Fatal(err)
} else if err := <-result; err != nil {
t.Fatal(err)
}

testutil.MineAndSync(t, node, node.Wallet.Address(), int(network.MaturityDelay+5))

renterFunds := types.Siacoins(500)
hostCollateral := types.Siacoins(1000)

contract := rhp2.PrepareContractFormation(renterKey.PublicKey(), hostKey.PublicKey(), renterFunds, hostCollateral, node.Chain.Tip().Height+10, rhp2.HostSettings{WindowSize: 10}, node.Wallet.Address())
state := node.Chain.TipState()
formationCost := rhp2.ContractFormationCost(state, contract, types.ZeroCurrency)
// create a thread of multiple ephemeral outputs

contractUnlockConditions := types.UnlockConditions{
PublicKeys: []types.UnlockKey{
renterKey.PublicKey().UnlockKey(),
hostKey.PublicKey().UnlockKey(),
},
SignaturesRequired: 2,
}
formationSet := []types.Transaction{
{
ArbitraryData: [][]byte{[]byte("setup txn 1")},
SiacoinOutputs: []types.SiacoinOutput{
{Address: node.Wallet.Address(), Value: formationCost.Add(hostCollateral)},
},
},
{
ArbitraryData: [][]byte{[]byte("setup txn 2")},
SiacoinOutputs: []types.SiacoinOutput{
{Address: node.Wallet.Address(), Value: formationCost.Add(hostCollateral)},
},
},
{
FileContracts: []types.FileContract{contract},
},
}
// fund the formation transaction
toSign, err := node.Wallet.FundTransaction(&formationSet[0], formationCost.Add(hostCollateral), true)
if err != nil {
t.Fatal("failed to fund transaction:", err)
}
node.Wallet.SignTransaction(&formationSet[0], toSign, types.CoveredFields{WholeTransaction: true})

// add and sign the ephemeral inputs
formationSet[1].SiacoinInputs = []types.SiacoinInput{
{
ParentID: formationSet[0].SiacoinOutputID(0),
UnlockConditions: types.StandardUnlockConditions(hostKey.PublicKey()),
},
}
node.Wallet.SignTransaction(&formationSet[1], []types.Hash256{types.Hash256(formationSet[0].SiacoinOutputID(0))}, types.CoveredFields{WholeTransaction: true})

formationSet[2].SiacoinInputs = []types.SiacoinInput{
{
ParentID: formationSet[1].SiacoinOutputID(0),
UnlockConditions: types.StandardUnlockConditions(hostKey.PublicKey()),
},
}
node.Wallet.SignTransaction(&formationSet[2], []types.Hash256{types.Hash256(formationSet[1].SiacoinOutputID(0))}, types.CoveredFields{WholeTransaction: true})

revision := types.FileContractRevision{
ParentID: formationSet[2].FileContractID(0),
UnlockConditions: contractUnlockConditions,
FileContract: formationSet[2].FileContracts[0],
}

// broadcast the first transaction only
if _, err := node.Chain.AddPoolTransactions([]types.Transaction{formationSet[0]}); err != nil {
t.Fatal(err)
}

revision.RevisionNumber = 1
sigHash := hashRevision(revision)
rev := contracts.SignedRevision{
Revision: revision,
HostSignature: hostKey.SignHash(sigHash),
RenterSignature: renterKey.SignHash(sigHash),
}
if err := node.Contracts.AddContract(rev, formationSet, hostCollateral, contracts.Usage{}); err != nil {
t.Fatal(err)
}

assertContractStatus(t, node.Contracts, rev.Revision.ParentID, contracts.ContractStatusPending)
// pending contracts do not contribute to metrics
assertContractMetrics(t, node.Store, 0, 0, types.ZeroCurrency, types.ZeroCurrency)

// mine to rebroadcast the formation set
testutil.MineAndSync(t, node, types.VoidAddress, 1)
assertContractStatus(t, node.Contracts, rev.Revision.ParentID, contracts.ContractStatusPending)
assertContractMetrics(t, node.Store, 0, 0, types.ZeroCurrency, types.ZeroCurrency)

// mine to confirm the contract
testutil.MineAndSync(t, node, types.VoidAddress, 1)
assertContractStatus(t, node.Contracts, rev.Revision.ParentID, contracts.ContractStatusActive)
assertContractMetrics(t, node.Store, 1, 0, hostCollateral, types.ZeroCurrency)
})
}

func TestV2ContractLifecycle(t *testing.T) {
Expand Down
37 changes: 33 additions & 4 deletions host/contracts/update.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package contracts

import (
"errors"
"fmt"

"go.sia.tech/core/consensus"
Expand Down Expand Up @@ -196,13 +197,15 @@ func (cm *Manager) ProcessActions(index types.ChainIndex) error {
log.Debug("skipping formation set missing file contract")
continue
}
if _, err := cm.chain.AddPoolTransactions(formationSet); err != nil {
formationTxn := formationSet[len(formationSet)-1]
contractID := formationSet[len(formationSet)-1].FileContractID(0)
log := log.With(zap.Stringer("contractID", contractID), zap.Stringer("transactionID", formationTxn.ID()))
formationSet, err := tryFormationBroadcast(cm.chain, formationSet)
if err != nil {
log.Error("failed to add formation transaction to pool", zap.Error(err))
continue
}
cm.syncer.BroadcastTransactionSet(formationSet)
contractID := formationSet[len(formationSet)-1].FileContractID(0)
log.Debug("rebroadcast formation transaction", zap.Stringer("contractID", contractID), zap.String("transactionID", formationSet[len(formationSet)-1].ID().String()))
log.Debug("rebroadcast formation transaction", zap.String("transactionID", formationTxn.ID().String()))
}

for _, revision := range actions.BroadcastRevision {
Expand Down Expand Up @@ -597,3 +600,29 @@ func (cm *Manager) UpdateChainState(tx UpdateStateTx, reverted []chain.RevertUpd
}
return nil
}

// tryFormationBroadcast is a helper function that attempts to broadcast a formation
// transaction set. Due to the nature of the transaction pool, it is possible
// a transaction will not be accepted if one of the parent transactions has
// already been confirmed. This function will retry all of the transactions in the set
// sequentially to attempt to remove any transactions that may have already been confirmed.
func tryFormationBroadcast(cm ChainManager, txnset []types.Transaction) ([]types.Transaction, error) {
if len(txnset) == 0 {
return nil, nil
} else if len(txnset[len(txnset)-1].FileContracts) == 0 {
return nil, errors.New("missing file contract")
}
formationTxn := txnset[len(txnset)-1]
_, err := cm.AddPoolTransactions(txnset)
if err == nil {
return txnset, nil
}

for _, txn := range txnset {
cm.AddPoolTransactions(append(cm.UnconfirmedParents(txn), txn)) // error is ignored because it will be caught below
}

txnset = append(cm.UnconfirmedParents(formationTxn), formationTxn)
_, err = cm.AddPoolTransactions(txnset)
return txnset, err
}

0 comments on commit deb74a2

Please sign in to comment.