From 6e43a9358ec1a57f4650fb6c16a45bbdaca9d008 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Thu, 3 Oct 2024 15:23:40 -0400 Subject: [PATCH 1/3] rhp4,chain: simplify renew, move proof updates to chain manager --- chain/manager.go | 79 +++++++++++---- go.mod | 2 +- go.sum | 4 +- rhp/v4/rpc.go | 64 +++++-------- rhp/v4/server.go | 244 ++++++++++------------------------------------- 5 files changed, 136 insertions(+), 257 deletions(-) diff --git a/chain/manager.go b/chain/manager.go index 32efce9..944cd81 100644 --- a/chain/manager.go +++ b/chain/manager.go @@ -949,7 +949,7 @@ func (m *Manager) V2TransactionSet(basis types.ChainIndex, txn types.V2Transacti m.revalidatePool() // update the transaction's basis to match tip - _, txns, err := m.updateV2TransactionSet(basis, []types.V2Transaction{txn}) + txns, err := m.UpdateV2TransactionSet([]types.V2Transaction{txn}, basis, m.tipState.Index) if err != nil { return types.ChainIndex{}, nil, fmt.Errorf("failed to update transaction set basis: %w", err) } else if len(txns) == 0 { @@ -1086,37 +1086,76 @@ func (m *Manager) AddPoolTransactions(txns []types.Transaction) (known bool, err return } -// updateV2TransactionSet updates the basis of a transaction set to the current -// tip. If the basis is already the tip, the transaction set is returned as-is. -// Any transactions that were confirmed are removed from the set. Any ephemeral -// state elements that were created by an update are updated. +// UpdateStateElement updates the basis of a state element from "from" to "to". +// If from and to are equal, the state element is not modified. +func (m *Manager) UpdateStateElement(se *types.StateElement, from, to types.ChainIndex) error { + if from == to { + return nil + } + + revert, apply, err := m.reorgPath(from, to) + if err != nil { + return fmt.Errorf("couldn't determine reorg path from %v to %v: %w", from, to, err) + } else if len(revert)+len(apply) > 144 { + return fmt.Errorf("reorg path from %v to %v is too long (-%v +%v)", from, to, len(revert), len(apply)) + } + for _, index := range revert { + b, _, cs, ok := blockAndParent(m.store, index.ID) + if !ok { + return fmt.Errorf("missing reverted block at index %v", index) + } else if b.V2 == nil { + return fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", from, to, index) + } + // NOTE: since we are post-hardfork, we don't need a v1 supplement + cru := consensus.RevertBlock(cs, b, consensus.V1BlockSupplement{}) + cru.UpdateElementProof(se) + } + + for _, index := range apply { + b, _, cs, ok := blockAndParent(m.store, index.ID) + if !ok { + return fmt.Errorf("missing applied block at index %v", index) + } else if b.V2 == nil { + return fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", from, to, index) + } + // NOTE: since we are post-hardfork, we don't need a v1 supplement or ancestorTimestamp + _, cau := consensus.ApplyBlock(cs, b, consensus.V1BlockSupplement{}, time.Time{}) + cau.UpdateElementProof(se) + } + return nil +} + +// UpdateV2TransactionSet updates the basis of a transaction set from "from" to "to". +// If from and to are equal, the transaction set is returned as-is. +// Any transactions that were confirmed are removed from the set. +// Any ephemeral state elements that were created by an update are updated. // // If it is undesirable to modify the transaction set, deep-copy it // before calling this method. -func (m *Manager) updateV2TransactionSet(basis types.ChainIndex, txns []types.V2Transaction) (types.ChainIndex, []types.V2Transaction, error) { - if basis == m.tipState.Index { - return basis, txns, nil +func (m *Manager) UpdateV2TransactionSet(txns []types.V2Transaction, from, to types.ChainIndex) ([]types.V2Transaction, error) { + if from == to { + return txns, nil } // bring txns up-to-date - revert, apply, err := m.reorgPath(basis, m.tipState.Index) + revert, apply, err := m.reorgPath(from, to) if err != nil { - return types.ChainIndex{}, nil, fmt.Errorf("couldn't determine reorg path from %v to %v: %w", basis, m.tipState.Index, err) + return nil, fmt.Errorf("couldn't determine reorg path from %v to %v: %w", from, to, err) } else if len(revert)+len(apply) > 144 { - return types.ChainIndex{}, nil, fmt.Errorf("reorg path from %v to %v is too long (-%v +%v)", basis, m.tipState.Index, len(revert), len(apply)) + return nil, fmt.Errorf("reorg path from %v to %v is too long (-%v +%v)", from, to, len(revert), len(apply)) } for _, index := range revert { b, _, cs, ok := blockAndParent(m.store, index.ID) if !ok { - return types.ChainIndex{}, nil, fmt.Errorf("missing reverted block at index %v", index) + return nil, fmt.Errorf("missing reverted block at index %v", index) } else if b.V2 == nil { - return types.ChainIndex{}, nil, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", basis, m.tipState.Index, index) + return nil, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", from, to, index) } // NOTE: since we are post-hardfork, we don't need a v1 supplement cru := consensus.RevertBlock(cs, b, consensus.V1BlockSupplement{}) for i := range txns { if !updateTxnProofs(&txns[i], cru.UpdateElementProof, cs.Elements.NumLeaves) { - return types.ChainIndex{}, nil, fmt.Errorf("transaction %v references element that does not exist in our chain", txns[i].ID()) + return nil, fmt.Errorf("transaction %v references element that does not exist in our chain", txns[i].ID()) } } } @@ -1124,9 +1163,9 @@ func (m *Manager) updateV2TransactionSet(basis types.ChainIndex, txns []types.V2 for _, index := range apply { b, _, cs, ok := blockAndParent(m.store, index.ID) if !ok { - return types.ChainIndex{}, nil, fmt.Errorf("missing applied block at index %v", index) + return nil, fmt.Errorf("missing applied block at index %v", index) } else if b.V2 == nil { - return types.ChainIndex{}, nil, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", basis, m.tipState.Index, index) + return nil, fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", from, to, index) } // NOTE: since we are post-hardfork, we don't need a v1 supplement or ancestorTimestamp cs, cau := consensus.ApplyBlock(cs, b, consensus.V1BlockSupplement{}, time.Time{}) @@ -1154,7 +1193,6 @@ func (m *Manager) updateV2TransactionSet(basis types.ChainIndex, txns []types.V2 // remove any transactions that were confirmed in this block continue } - rem = append(rem, txns[i]) // update the state elements for any confirmed ephemeral elements for j := range txns[i].SiacoinInputs { @@ -1182,11 +1220,12 @@ func (m *Manager) updateV2TransactionSet(basis types.ChainIndex, txns []types.V2 // NOTE: all elements guaranteed to exist from here on, so no // need to check this return value - updateTxnProofs(&rem[len(rem)-1], cau.UpdateElementProof, cs.Elements.NumLeaves) + updateTxnProofs(&txns[i], cau.UpdateElementProof, cs.Elements.NumLeaves) + rem = append(rem, txns[i]) } txns = rem } - return m.tipState.Index, txns, nil + return txns, nil } // AddV2PoolTransactions validates a transaction set and adds it to the txpool. @@ -1220,7 +1259,7 @@ func (m *Manager) AddV2PoolTransactions(basis types.ChainIndex, txns []types.V2T } // update the transaction set to the current tip - _, txns, err := m.updateV2TransactionSet(basis, txns) + txns, err := m.UpdateV2TransactionSet(txns, basis, m.tipState.Index) if err != nil { return false, m.markBadTxnSet(setID, fmt.Errorf("failed to update set basis: %w", err)) } else if len(txns) == 0 { diff --git a/go.mod b/go.mod index 61a0be5..e93a82f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.23.0 require ( go.etcd.io/bbolt v1.3.11 - go.sia.tech/core v0.4.8-0.20241002233523-418236837b5a + go.sia.tech/core v0.4.8-0.20241003192046-425f95763c90 go.sia.tech/mux v1.3.0 go.uber.org/zap v1.27.0 golang.org/x/crypto v0.27.0 diff --git a/go.sum b/go.sum index c93ea9f..f5b7a6e 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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.4.8-0.20241002233523-418236837b5a h1:Lz7h42eZCk8iI4/agdWNwlVneqaybxkV1lor+Eavzso= -go.sia.tech/core v0.4.8-0.20241002233523-418236837b5a/go.mod h1:j2Ke8ihV8or7d2VDrFZWcCkwSVHO0DNMQJAGs9Qop2M= +go.sia.tech/core v0.4.8-0.20241003192046-425f95763c90 h1:R/G7XXyzLKelfGBGT6XQpih379v5jJx6T4AsjLwyjJc= +go.sia.tech/core v0.4.8-0.20241003192046-425f95763c90/go.mod h1:j2Ke8ihV8or7d2VDrFZWcCkwSVHO0DNMQJAGs9Qop2M= go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= go.sia.tech/mux v1.3.0/go.mod h1:I46++RD4beqA3cW9Xm9SwXbezwPqLvHhVs9HLpDtt58= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= diff --git a/rhp/v4/rpc.go b/rhp/v4/rpc.go index 7e372f7..4680a60 100644 --- a/rhp/v4/rpc.go +++ b/rhp/v4/rpc.go @@ -649,48 +649,28 @@ func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer } renterCost, hostCost := rhp4.RenewalCost(cs, p, renewal, renewalTxn.MinerFee) - - basis := cs.Index // start with a decent basis and overwrite it if a setup transaction is needed - var renewalParents []types.V2Transaction - if !renterCost.IsZero() { - setupTxn := types.V2Transaction{ - SiacoinOutputs: []types.SiacoinOutput{ - {Address: renewal.NewContract.RenterOutput.Address, Value: renterCost}, - }, - } - var err error - var toSign []int - basis, toSign, err = signer.FundV2Transaction(&setupTxn, renterCost) - if err != nil { - return RPCRenewContractResult{}, fmt.Errorf("failed to fund transaction: %w", err) - } - signer.SignV2Inputs(&setupTxn, toSign) - - basis, renewalParents, err = tp.V2TransactionSet(basis, setupTxn) - if err != nil { - return RPCRenewContractResult{}, fmt.Errorf("failed to get transaction set: %w", err) - } - setupTxn = renewalParents[len(renewalParents)-1] - - renewalTxn.SiacoinInputs = append(renewalTxn.SiacoinInputs, types.V2SiacoinInput{ - Parent: setupTxn.EphemeralSiacoinOutput(0), - }) - signer.SignV2Inputs(&renewalTxn, []int{0}) + req := rhp4.RPCRenewContractRequest{ + Prices: p, + Renewal: params, + MinerFee: renewalTxn.MinerFee, + Basis: cs.Index, } - renterSiacoinElements := make([]types.SiacoinElement, 0, len(renewalTxn.SiacoinInputs)) - for _, i := range renewalTxn.SiacoinInputs { - renterSiacoinElements = append(renterSiacoinElements, i.Parent) + basis, toSign, err := signer.FundV2Transaction(&renewalTxn, renterCost) + if err != nil { + return RPCRenewContractResult{}, fmt.Errorf("failed to fund transaction: %w", err) } + signer.SignV2Inputs(&renewalTxn, toSign) - req := rhp4.RPCRenewContractRequest{ - Prices: p, - Renewal: params, - MinerFee: renewalTxn.MinerFee, - Basis: basis, - RenterInputs: renterSiacoinElements, - RenterParents: renewalParents, + req.Basis, req.RenterParents, err = tp.V2TransactionSet(basis, renewalTxn) + if err != nil { + return RPCRenewContractResult{}, fmt.Errorf("failed to get transaction set: %w", err) } + for _, si := range renewalTxn.SiacoinInputs { + req.RenterInputs = append(req.RenterInputs, si.Parent) + } + req.RenterParents = req.RenterParents[:len(req.RenterParents)-1] // last transaction is the renewal + sigHash := req.ChallengeSigHash(existing.RevisionNumber) req.ChallengeSignature = signer.SignHash(sigHash) @@ -714,8 +694,14 @@ func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer } // verify the host added enough inputs - if !hostInputSum.Equals(hostCost) { + if n := hostInputSum.Cmp(hostCost); n < 0 { return RPCRenewContractResult{}, fmt.Errorf("expected host to fund %v, got %v", hostCost, hostInputSum) + } else if n > 0 { + // add change output + renewalTxn.SiacoinOutputs = append(renewalTxn.SiacoinOutputs, types.SiacoinOutput{ + Address: existing.HostOutput.Address, + Value: hostInputSum.Sub(hostCost), + }) } // sign the renter inputs @@ -728,7 +714,7 @@ func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer renterPolicyResp := rhp4.RPCRenewContractSecondResponse{ RenterRenewalSignature: renewal.RenterSignature, } - for _, si := range renewalTxn.SiacoinInputs[:len(renterSiacoinElements)] { + for _, si := range renewalTxn.SiacoinInputs[:len(req.RenterInputs)] { renterPolicyResp.RenterSatisfiedPolicies = append(renterPolicyResp.RenterSatisfiedPolicies, si.SatisfiedPolicy) } if err := rhp4.WriteResponse(s, &renterPolicyResp); err != nil { diff --git a/rhp/v4/server.go b/rhp/v4/server.go index 8c74a95..4114cf7 100644 --- a/rhp/v4/server.go +++ b/rhp/v4/server.go @@ -12,16 +12,11 @@ import ( "go.sia.tech/core/consensus" rhp4 "go.sia.tech/core/rhp/v4" "go.sia.tech/core/types" - "go.sia.tech/coreutils/chain" "go.sia.tech/coreutils/wallet" "go.uber.org/zap" "lukechampine.com/frand" ) -// maxBasisDiff is the maximum number of blocks by which a transaction's basis -// can differ from the current tip. -const maxBasisDiff = 20 - var protocolVersion = [3]byte{4, 0, 0} type ( @@ -59,9 +54,16 @@ type ( AddV2PoolTransactions(types.ChainIndex, []types.V2Transaction) (known bool, err error) // RecommendedFee returns the recommended fee per weight RecommendedFee() types.Currency - // UpdatesSince returns at most max updates on the path between index and the - // Manager's current tip. - UpdatesSince(index types.ChainIndex, maxBlocks int) (rus []chain.RevertUpdate, aus []chain.ApplyUpdate, err error) + + UpdateStateElement(se *types.StateElement, from, to types.ChainIndex) error + // UpdateV2TransactionSet updates the basis of a transaction set from "from" to "to". + // If from and to are equal, the transaction set is returned as-is. + // Any transactions that were confirmed are removed from the set. + // Any ephemeral state elements that were created by an update are updated. + // + // If it is undesirable to modify the transaction set, deep-copy it + // before calling this method. + UpdateV2TransactionSet(txns []types.V2Transaction, from, to types.ChainIndex) ([]types.V2Transaction, error) } // A Syncer broadcasts transactions to its peers. @@ -580,9 +582,14 @@ func (s *Server) handleRPCFormContract(stream net.Conn) error { // update renter input basis to reflect our funding basis if basis != req.Basis { - if err := updateSiacoinElementBasis(s.chain, req.Basis, basis, formationTxn.SiacoinInputs[:len(req.RenterInputs)]); err != nil { + hostInputs := formationTxn.SiacoinInputs[len(formationTxn.SiacoinInputs)-len(req.RenterInputs)] + formationTxn.SiacoinInputs = formationTxn.SiacoinInputs[:len(formationTxn.SiacoinInputs)-len(req.RenterInputs)] + txnset, err := s.chain.UpdateV2TransactionSet([]types.V2Transaction{formationTxn}, req.Basis, basis) + if err != nil { return errorBadRequest("failed to update renter inputs from %q to %q: %v", req.Basis, basis, err) } + formationTxn = txnset[0] + formationTxn.SiacoinInputs = append(formationTxn.SiacoinInputs, hostInputs) } // send the host inputs to the renter @@ -684,20 +691,12 @@ func (s *Server) handleRPCRenewContract(stream net.Conn) error { return errorBadRequest("invalid challenge signature") } + cs := s.chain.TipState() renewal := rhp4.NewRenewal(existing, prices, req.Renewal) - - // create the renewal transaction + renterCost, hostCost := rhp4.RenewalCost(cs, prices, renewal, req.MinerFee) renewalTxn := types.V2Transaction{ MinerFee: req.MinerFee, - FileContractResolutions: []types.V2FileContractResolution{ - { - Resolution: &renewal, - }, - }, } - // calculate the renter funding - cs := s.chain.TipState() - renterCost, hostCost := rhp4.RenewalCost(cs, prices, renewal, renewalTxn.MinerFee) // add the renter inputs var renterInputSum types.Currency @@ -708,61 +707,49 @@ func (s *Server) handleRPCRenewContract(stream net.Conn) error { renterInputSum = renterInputSum.Add(si.SiacoinOutput.Value) } - // validate the renter added enough inputs - if !renterInputSum.Equals(renterCost) { + if n := renterInputSum.Cmp(renterCost); n < 0 { return errorBadRequest("expected renter to fund %v, got %v", renterInputSum, renterCost) + } else if n > 0 { + // if the renter added too much, add a change output + renewalTxn.SiacoinOutputs = append(renewalTxn.SiacoinOutputs, types.SiacoinOutput{ + Address: renewal.NewContract.RenterOutput.Address, + Value: renterInputSum.Sub(renterCost), + }) } - fceBasis, fce, err := s.contractor.ContractElement(req.Renewal.ContractID) + elementBasis, fce, err := s.contractor.ContractElement(req.Renewal.ContractID) if err != nil { return fmt.Errorf("failed to get contract element: %w", err) } - renewalTxn.FileContractResolutions[0].Parent = fce - basis := cs.Index // start with a decent basis and overwrite it if a setup transaction is needed - var renewalParents []types.V2Transaction - if !hostCost.IsZero() { - // fund the locked collateral - setupTxn := types.V2Transaction{ - SiacoinOutputs: []types.SiacoinOutput{ - {Address: renewal.NewContract.HostOutput.Address, Value: hostCost}, - }, - } - - var toSign []int - basis, toSign, err = s.wallet.FundV2Transaction(&setupTxn, hostCost, true) - if errors.Is(err, wallet.ErrNotEnoughFunds) { - return rhp4.ErrHostFundError - } else if err != nil { - return fmt.Errorf("failed to fund transaction: %w", err) - } - // sign the transaction inputs - s.wallet.SignV2Inputs(&setupTxn, toSign) - - basis, renewalParents, err = s.chain.V2TransactionSet(basis, setupTxn) - if err != nil { - return fmt.Errorf("failed to get setup transaction set: %w", err) - } - - renewalTxn.SiacoinInputs = append(renewalTxn.SiacoinInputs, types.V2SiacoinInput{ - Parent: renewalParents[len(renewalParents)-1].EphemeralSiacoinOutput(0), - }) - s.wallet.SignV2Inputs(&renewalTxn, []int{len(renewalTxn.SiacoinInputs) - 1}) + basis, toSign, err := s.wallet.FundV2Transaction(&renewalTxn, hostCost, true) + if errors.Is(err, wallet.ErrNotEnoughFunds) { + return rhp4.ErrHostFundError + } else if err != nil { + return fmt.Errorf("failed to fund transaction: %w", err) } - // update renter input basis to reflect our funding basis + // update renter inputs to reflect our chain basis if basis != req.Basis { - if err := updateSiacoinElementBasis(s.chain, req.Basis, basis, renewalTxn.SiacoinInputs[:len(req.RenterInputs)]); err != nil { + hostInputs := renewalTxn.SiacoinInputs[len(renewalTxn.SiacoinInputs)-len(req.RenterInputs):] + renewalTxn.SiacoinInputs = renewalTxn.SiacoinInputs[:len(renewalTxn.SiacoinInputs)-len(req.RenterInputs)] + updated, err := s.chain.UpdateV2TransactionSet([]types.V2Transaction{renewalTxn}, req.Basis, basis) + if err != nil { return errorBadRequest("failed to update renter inputs from %q to %q: %v", req.Basis, basis, err) } + renewalTxn = updated[0] + renewalTxn.SiacoinInputs = append(renewalTxn.SiacoinInputs, hostInputs...) } - // update the file contract element to reflect the funding basis - if fceBasis != basis { - if err := updateStateElementBasis(s.chain, fceBasis, basis, &fce.StateElement); err != nil { - return errorBadRequest("failed to update file contract basis: %v", err) + + if elementBasis != basis { + if err := s.chain.UpdateStateElement(&fce.StateElement, elementBasis, basis); err != nil { + return fmt.Errorf("failed to update contract element: %w", err) } } - + renewalTxn.FileContractResolutions = []types.V2FileContractResolution{ + {Parent: fce, Resolution: &renewal}, + } + s.wallet.SignV2Inputs(&renewalTxn, toSign) // send the host inputs to the renter hostInputsResp := rhp4.RPCRenewContractResponse{ HostInputs: renewalTxn.SiacoinInputs[len(req.RenterInputs):], @@ -787,11 +774,9 @@ func (s *Server) handleRPCRenewContract(stream net.Conn) error { renewal.RenterSignature = renterSigResp.RenterRenewalSignature // apply the renter's signatures - for i := range renewalTxn.SiacoinInputs[:len(req.RenterInputs)] { - renewalTxn.SiacoinInputs[i].SatisfiedPolicy = renterSigResp.RenterSatisfiedPolicies[i] + for i, policy := range renterSigResp.RenterSatisfiedPolicies { + renewalTxn.SiacoinInputs[i].SatisfiedPolicy = policy } - - // sign the renewal renewal.HostSignature = s.hostKey.SignHash(renewalSigHash) // add the renter's parents to our transaction pool to ensure they are valid @@ -802,19 +787,12 @@ func (s *Server) handleRPCRenewContract(stream net.Conn) error { } } - // add the setup parents to the transaction pool - if len(renewalParents) > 0 { - if _, err := s.chain.AddV2PoolTransactions(basis, renewalParents); err != nil { - return errorBadRequest("failed to add setup parents to transaction pool: %v", err) - } - } - // get the full updated transaction set for the renewal transaction basis, renewalSet, err := s.chain.V2TransactionSet(basis, renewalTxn) if err != nil { return fmt.Errorf("failed to get transaction set: %w", err) } else if _, err = s.chain.AddV2PoolTransactions(basis, renewalSet); err != nil { - return errorBadRequest("failed to broadcast setup transaction: %v", err) + return errorBadRequest("failed to broadcast renewal set: %v", err) } // broadcast the transaction set s.syncer.BroadcastV2TransactionSet(basis, renewalSet) @@ -864,130 +842,6 @@ func (s *Server) handleRPCVerifySector(stream net.Conn) error { return rhp4.WriteResponse(stream, &resp) } -func updateStateElementBasis(cm ChainManager, base, target types.ChainIndex, element *types.StateElement) error { - reverted, applied, err := cm.UpdatesSince(base, 144) - if err != nil { - return err - } - - if len(reverted)+len(applied) > maxBasisDiff { - return fmt.Errorf("too many updates between %v and %v", base, target) - } - - for _, cru := range reverted { - revertedIndex := types.ChainIndex{ - Height: cru.State.Index.Height + 1, - ID: cru.Block.ParentID, - } - if revertedIndex == target { - return nil - } - - cru.UpdateElementProof(element) - base = revertedIndex - } - - for _, cau := range applied { - if cau.State.Index == target { - return nil - } - - modified := make(map[types.Hash256]types.StateElement) - cau.ForEachSiacoinElement(func(sce types.SiacoinElement, created bool, spent bool) { - if created { - modified[sce.ID] = sce.StateElement - } - }) - - cau.UpdateElementProof(element) - base = cau.State.Index - } - - if base != target { - return fmt.Errorf("failed to update basis to target %v, current %v", target, base) - } - return nil -} - -// updateSiacoinElementBasis is a helper to update a transaction's siacoin elements -// to the target basis. If an error is returned, inputs must be considered invalid. -func updateSiacoinElementBasis(cm ChainManager, base, target types.ChainIndex, inputs []types.V2SiacoinInput) error { - reverted, applied, err := cm.UpdatesSince(base, 144) - if err != nil { - return err - } - - if len(reverted)+len(applied) > maxBasisDiff { - return fmt.Errorf("too many updates between %v and %v", base, target) - } - - for _, cru := range reverted { - revertedIndex := types.ChainIndex{ - Height: cru.State.Index.Height + 1, - ID: cru.Block.ParentID, - } - if revertedIndex == target { - return nil - } - - modified := make(map[types.Hash256]types.StateElement) - cru.ForEachSiacoinElement(func(sce types.SiacoinElement, created bool, spent bool) { - if created { - modified[sce.ID] = types.StateElement{ - ID: sce.ID, - LeafIndex: types.UnassignedLeafIndex, - } - } - }) - - for i := range inputs { - if se, ok := modified[inputs[i].Parent.ID]; ok { - inputs[i].Parent.StateElement = se - } - - if inputs[i].Parent.LeafIndex == types.UnassignedLeafIndex { - continue - } else if inputs[i].Parent.LeafIndex >= cru.State.Elements.NumLeaves { - return fmt.Errorf("siacoin input %v is not in the correct state", inputs[i].Parent.ID) - } - cru.UpdateElementProof(&inputs[i].Parent.StateElement) - } - base = revertedIndex - } - - for _, cau := range applied { - if cau.State.Index == target { - return nil - } - - modified := make(map[types.Hash256]types.StateElement) - cau.ForEachSiacoinElement(func(sce types.SiacoinElement, created bool, spent bool) { - if created { - modified[sce.ID] = sce.StateElement - } - }) - - for i := range inputs { - if se, ok := modified[inputs[i].Parent.ID]; ok { - inputs[i].Parent.StateElement = se - } - - if inputs[i].Parent.LeafIndex == types.UnassignedLeafIndex { - continue - } else if inputs[i].Parent.LeafIndex >= cau.State.Elements.NumLeaves { - return fmt.Errorf("siacoin input %v is not in the correct state", inputs[i].Parent.ID) - } - cau.UpdateElementProof(&inputs[i].Parent.StateElement) - } - base = cau.State.Index - } - - if base != target { - return fmt.Errorf("failed to update basis to target %v, current %v", target, base) - } - return nil -} - func (s *Server) handleHostStream(stream net.Conn, log *zap.Logger) { defer stream.Close() From 80357af108181a63dada62e247285f7915ac7e76 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Mon, 7 Oct 2024 14:42:05 -0700 Subject: [PATCH 2/3] rhp4,chain: address comments --- chain/manager.go | 162 +++++++++++++++++++---------------------------- rhp/v4/rpc.go | 1 - rhp/v4/server.go | 12 +++- 3 files changed, 73 insertions(+), 102 deletions(-) diff --git a/chain/manager.go b/chain/manager.go index 944cd81..39e408b 100644 --- a/chain/manager.go +++ b/chain/manager.go @@ -1041,103 +1041,7 @@ func (m *Manager) markBadTxnSet(setID types.Hash256, err error) error { return err } -// AddPoolTransactions validates a transaction set and adds it to the txpool. If -// any transaction references an element (SiacoinOutput, SiafundOutput, or -// FileContract) not present in the blockchain, that element must be created by -// a previous transaction in the set. -// -// If any transaction in the set is invalid, the entire set is rejected and none -// of the transactions are added to the pool. If all of the transactions are -// already known to the pool, AddPoolTransactions returns true. -func (m *Manager) AddPoolTransactions(txns []types.Transaction) (known bool, err error) { - m.mu.Lock() - defer m.mu.Unlock() - m.revalidatePool() - - setID, known := m.checkDupTxnSet(txns, nil) - if known { - return true, m.txpool.invalidTxnSets[setID] - } - - // validate as a standalone set - ms := consensus.NewMidState(m.tipState) - for _, txn := range txns { - ts := m.store.SupplementTipTransaction(txn) - if err := consensus.ValidateTransaction(ms, txn, ts); err != nil { - return false, m.markBadTxnSet(setID, fmt.Errorf("transaction %v is invalid: %w", txn.ID(), err)) - } - ms.ApplyTransaction(txn, ts) - } - - for _, txn := range txns { - txid := txn.ID() - if _, ok := m.txpool.indices[txid]; ok { - continue // skip transactions already in pool - } - m.txpool.ms.ApplyTransaction(txn, m.store.SupplementTipTransaction(txn)) - m.txpool.indices[txid] = len(m.txpool.txns) - m.txpool.txns = append(m.txpool.txns, txn) - m.txpool.weight += m.tipState.TransactionWeight(txn) - } - - // invalidate caches - m.txpool.medianFee = nil - m.txpool.parentMap = nil - return -} - -// UpdateStateElement updates the basis of a state element from "from" to "to". -// If from and to are equal, the state element is not modified. -func (m *Manager) UpdateStateElement(se *types.StateElement, from, to types.ChainIndex) error { - if from == to { - return nil - } - - revert, apply, err := m.reorgPath(from, to) - if err != nil { - return fmt.Errorf("couldn't determine reorg path from %v to %v: %w", from, to, err) - } else if len(revert)+len(apply) > 144 { - return fmt.Errorf("reorg path from %v to %v is too long (-%v +%v)", from, to, len(revert), len(apply)) - } - for _, index := range revert { - b, _, cs, ok := blockAndParent(m.store, index.ID) - if !ok { - return fmt.Errorf("missing reverted block at index %v", index) - } else if b.V2 == nil { - return fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", from, to, index) - } - // NOTE: since we are post-hardfork, we don't need a v1 supplement - cru := consensus.RevertBlock(cs, b, consensus.V1BlockSupplement{}) - cru.UpdateElementProof(se) - } - - for _, index := range apply { - b, _, cs, ok := blockAndParent(m.store, index.ID) - if !ok { - return fmt.Errorf("missing applied block at index %v", index) - } else if b.V2 == nil { - return fmt.Errorf("reorg path from %v to %v contains a non-v2 block (%v)", from, to, index) - } - // NOTE: since we are post-hardfork, we don't need a v1 supplement or ancestorTimestamp - _, cau := consensus.ApplyBlock(cs, b, consensus.V1BlockSupplement{}, time.Time{}) - cau.UpdateElementProof(se) - } - return nil -} - -// UpdateV2TransactionSet updates the basis of a transaction set from "from" to "to". -// If from and to are equal, the transaction set is returned as-is. -// Any transactions that were confirmed are removed from the set. -// Any ephemeral state elements that were created by an update are updated. -// -// If it is undesirable to modify the transaction set, deep-copy it -// before calling this method. -func (m *Manager) UpdateV2TransactionSet(txns []types.V2Transaction, from, to types.ChainIndex) ([]types.V2Transaction, error) { - if from == to { - return txns, nil - } - - // bring txns up-to-date +func (m *Manager) updateV2TransactionProofs(txns []types.V2Transaction, from, to types.ChainIndex) ([]types.V2Transaction, error) { revert, apply, err := m.reorgPath(from, to) if err != nil { return nil, fmt.Errorf("couldn't determine reorg path from %v to %v: %w", from, to, err) @@ -1228,6 +1132,68 @@ func (m *Manager) UpdateV2TransactionSet(txns []types.V2Transaction, from, to ty return txns, nil } +// AddPoolTransactions validates a transaction set and adds it to the txpool. If +// any transaction references an element (SiacoinOutput, SiafundOutput, or +// FileContract) not present in the blockchain, that element must be created by +// a previous transaction in the set. +// +// If any transaction in the set is invalid, the entire set is rejected and none +// of the transactions are added to the pool. If all of the transactions are +// already known to the pool, AddPoolTransactions returns true. +func (m *Manager) AddPoolTransactions(txns []types.Transaction) (known bool, err error) { + m.mu.Lock() + defer m.mu.Unlock() + m.revalidatePool() + + setID, known := m.checkDupTxnSet(txns, nil) + if known { + return true, m.txpool.invalidTxnSets[setID] + } + + // validate as a standalone set + ms := consensus.NewMidState(m.tipState) + for _, txn := range txns { + ts := m.store.SupplementTipTransaction(txn) + if err := consensus.ValidateTransaction(ms, txn, ts); err != nil { + return false, m.markBadTxnSet(setID, fmt.Errorf("transaction %v is invalid: %w", txn.ID(), err)) + } + ms.ApplyTransaction(txn, ts) + } + + for _, txn := range txns { + txid := txn.ID() + if _, ok := m.txpool.indices[txid]; ok { + continue // skip transactions already in pool + } + m.txpool.ms.ApplyTransaction(txn, m.store.SupplementTipTransaction(txn)) + m.txpool.indices[txid] = len(m.txpool.txns) + m.txpool.txns = append(m.txpool.txns, txn) + m.txpool.weight += m.tipState.TransactionWeight(txn) + } + + // invalidate caches + m.txpool.medianFee = nil + m.txpool.parentMap = nil + return +} + +// UpdateV2TransactionSet updates the basis of a transaction set from "from" to "to". +// If from and to are equal, the transaction set is returned as-is. +// Any transactions that were confirmed are removed from the set. +// Any ephemeral state elements that were created by an update are updated. +// +// If it is undesirable to modify the transaction set, deep-copy it +// before calling this method. +func (m *Manager) UpdateV2TransactionSet(txns []types.V2Transaction, from, to types.ChainIndex) ([]types.V2Transaction, error) { + if from == to { + return txns, nil + } + + m.mu.Lock() + defer m.mu.Unlock() + return m.updateV2TransactionProofs(txns, from, to) +} + // AddV2PoolTransactions validates a transaction set and adds it to the txpool. // If any transaction references an element (SiacoinOutput, SiafundOutput, or // FileContract) not present in the blockchain, that element must be created by @@ -1259,7 +1225,7 @@ func (m *Manager) AddV2PoolTransactions(basis types.ChainIndex, txns []types.V2T } // update the transaction set to the current tip - txns, err := m.UpdateV2TransactionSet(txns, basis, m.tipState.Index) + txns, err := m.updateV2TransactionProofs(txns, basis, m.tipState.Index) if err != nil { return false, m.markBadTxnSet(setID, fmt.Errorf("failed to update set basis: %w", err)) } else if len(txns) == 0 { diff --git a/rhp/v4/rpc.go b/rhp/v4/rpc.go index 4680a60..886c749 100644 --- a/rhp/v4/rpc.go +++ b/rhp/v4/rpc.go @@ -653,7 +653,6 @@ func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer Prices: p, Renewal: params, MinerFee: renewalTxn.MinerFee, - Basis: cs.Index, } basis, toSign, err := signer.FundV2Transaction(&renewalTxn, renterCost) diff --git a/rhp/v4/server.go b/rhp/v4/server.go index 4114cf7..1e09dfc 100644 --- a/rhp/v4/server.go +++ b/rhp/v4/server.go @@ -55,7 +55,6 @@ type ( // RecommendedFee returns the recommended fee per weight RecommendedFee() types.Currency - UpdateStateElement(se *types.StateElement, from, to types.ChainIndex) error // UpdateV2TransactionSet updates the basis of a transaction set from "from" to "to". // If from and to are equal, the transaction set is returned as-is. // Any transactions that were confirmed are removed from the set. @@ -729,7 +728,7 @@ func (s *Server) handleRPCRenewContract(stream net.Conn) error { return fmt.Errorf("failed to fund transaction: %w", err) } - // update renter inputs to reflect our chain basis + // update renter inputs to reflect our chain state if basis != req.Basis { hostInputs := renewalTxn.SiacoinInputs[len(renewalTxn.SiacoinInputs)-len(req.RenterInputs):] renewalTxn.SiacoinInputs = renewalTxn.SiacoinInputs[:len(renewalTxn.SiacoinInputs)-len(req.RenterInputs)] @@ -742,9 +741,16 @@ func (s *Server) handleRPCRenewContract(stream net.Conn) error { } if elementBasis != basis { - if err := s.chain.UpdateStateElement(&fce.StateElement, elementBasis, basis); err != nil { + tempTxn := types.V2Transaction{ + FileContractResolutions: []types.V2FileContractResolution{ + {Parent: fce, Resolution: &renewal}, + }, + } + updated, err := s.chain.UpdateV2TransactionSet([]types.V2Transaction{tempTxn}, elementBasis, basis) + if err != nil { return fmt.Errorf("failed to update contract element: %w", err) } + fce = updated[0].FileContractResolutions[0].Parent } renewalTxn.FileContractResolutions = []types.V2FileContractResolution{ {Parent: fce, Resolution: &renewal}, From b304db9e3644fb24c2ad21fbb196b5f8a29f85a3 Mon Sep 17 00:00:00 2001 From: Nate Maninger Date: Mon, 7 Oct 2024 16:50:06 -0700 Subject: [PATCH 3/3] rhp4: fix renew signatures --- rhp/v4/rpc.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rhp/v4/rpc.go b/rhp/v4/rpc.go index 886c749..08eb4c6 100644 --- a/rhp/v4/rpc.go +++ b/rhp/v4/rpc.go @@ -659,7 +659,6 @@ func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer if err != nil { return RPCRenewContractResult{}, fmt.Errorf("failed to fund transaction: %w", err) } - signer.SignV2Inputs(&renewalTxn, toSign) req.Basis, req.RenterParents, err = tp.V2TransactionSet(basis, renewalTxn) if err != nil { @@ -704,7 +703,7 @@ func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer } // sign the renter inputs - signer.SignV2Inputs(&renewalTxn, []int{0}) + signer.SignV2Inputs(&renewalTxn, toSign) // sign the renewal renewalSigHash := cs.RenewalSigHash(renewal) renewal.RenterSignature = signer.SignHash(renewalSigHash)