diff --git a/rhp/v4/rpc.go b/rhp/v4/rpc.go index 17da549..d16c578 100644 --- a/rhp/v4/rpc.go +++ b/rhp/v4/rpc.go @@ -124,8 +124,8 @@ type ( Cost types.Currency `json:"cost"` } - // RPCModifySectorsResult contains the result of executing the modify sectors RPC. - RPCModifySectorsResult struct { + // RPCRemoveSectorsResult contains the result of executing the remove sectors RPC. + RPCRemoveSectorsResult struct { Revision types.V2FileContract `json:"revision"` Cost types.Currency `json:"cost"` } @@ -309,55 +309,55 @@ func RPCVerifySector(ctx context.Context, t TransportClient, prices rhp4.HostPri }, nil } -// RPCModifySectors modifies sectors on the host. -func RPCModifySectors(ctx context.Context, t TransportClient, cs consensus.State, prices rhp4.HostPrices, sk types.PrivateKey, contract ContractRevision, actions []rhp4.ModifyAction) (RPCModifySectorsResult, error) { - req := rhp4.RPCModifySectorsRequest{ +// RPCRemoveSectors modifies sectors on the host. +func RPCRemoveSectors(ctx context.Context, t TransportClient, cs consensus.State, prices rhp4.HostPrices, sk types.PrivateKey, contract ContractRevision, indices []uint64) (RPCRemoveSectorsResult, error) { + req := rhp4.RPCRemoveSectorsRequest{ ContractID: contract.ID, Prices: prices, - Actions: actions, + Indices: indices, } req.ChallengeSignature = sk.SignHash(req.ChallengeSigHash(contract.Revision.RevisionNumber + 1)) s := t.DialStream(ctx) defer s.Close() - if err := rhp4.WriteRequest(s, rhp4.RPCModifySectorsID, &req); err != nil { - return RPCModifySectorsResult{}, fmt.Errorf("failed to write request: %w", err) + if err := rhp4.WriteRequest(s, rhp4.RPCRemoveSectorsID, &req); err != nil { + return RPCRemoveSectorsResult{}, fmt.Errorf("failed to write request: %w", err) } - numSectors := (contract.Revision.Filesize + rhp4.SectorSize - 1) / rhp4.SectorSize - var resp rhp4.RPCModifySectorsResponse + // numSectors := (contract.Revision.Filesize + rhp4.SectorSize - 1) / rhp4.SectorSize + var resp rhp4.RPCRemoveSectorsResponse if err := rhp4.ReadResponse(s, &resp); err != nil { - return RPCModifySectorsResult{}, fmt.Errorf("failed to read response: %w", err) - } else if !rhp4.VerifyModifySectorsProof(actions, numSectors, resp.OldSubtreeHashes, resp.OldLeafHashes, contract.Revision.FileMerkleRoot, resp.NewMerkleRoot) { - return RPCModifySectorsResult{}, ErrInvalidProof - } + return RPCRemoveSectorsResult{}, fmt.Errorf("failed to read response: %w", err) + } /*else if !rhp4.VerifyModifySectorsProof(actions, numSectors, resp.OldSubtreeHashes, resp.OldLeafHashes, contract.Revision.FileMerkleRoot, resp.NewMerkleRoot) { + return RPCRemoveSectorsResult{}, ErrInvalidProof + }*/ // TODO: verify proof - revision, err := rhp4.ReviseForModifySectors(contract.Revision, prices, resp.NewMerkleRoot, actions) + revision, err := rhp4.ReviseForRemoveSectors(contract.Revision, prices, resp.NewMerkleRoot, len(indices)) if err != nil { - return RPCModifySectorsResult{}, fmt.Errorf("failed to revise contract: %w", err) + return RPCRemoveSectorsResult{}, fmt.Errorf("failed to revise contract: %w", err) } sigHash := cs.ContractSigHash(revision) revision.RenterSignature = sk.SignHash(sigHash) - signatureResp := rhp4.RPCModifySectorsSecondResponse{ + signatureResp := rhp4.RPCRemoveSectorsSecondResponse{ RenterSignature: revision.RenterSignature, } if err := rhp4.WriteResponse(s, &signatureResp); err != nil { - return RPCModifySectorsResult{}, fmt.Errorf("failed to write signature response: %w", err) + return RPCRemoveSectorsResult{}, fmt.Errorf("failed to write signature response: %w", err) } - var hostSignature rhp4.RPCModifySectorsThirdResponse + var hostSignature rhp4.RPCRemoveSectorsThirdResponse if err := rhp4.ReadResponse(s, &hostSignature); err != nil { - return RPCModifySectorsResult{}, fmt.Errorf("failed to read host signatures: %w", err) + return RPCRemoveSectorsResult{}, fmt.Errorf("failed to read host signatures: %w", err) } // validate the host signature if !contract.Revision.HostPublicKey.VerifyHash(sigHash, hostSignature.HostSignature) { - return RPCModifySectorsResult{}, rhp4.ErrInvalidSignature + return RPCRemoveSectorsResult{}, rhp4.ErrInvalidSignature } // return the signed revision - return RPCModifySectorsResult{ + return RPCRemoveSectorsResult{ Revision: revision, Cost: contract.Revision.RenterOutput.Value.Sub(revision.RenterOutput.Value), }, nil @@ -495,7 +495,7 @@ func RPCSectorRoots(ctx context.Context, t TransportClient, cs consensus.State, RenterSignature: revision.RenterSignature, } - if err := req.Validate(contract.Revision.HostPublicKey, revision); err != nil { + if err := req.Validate(contract.Revision.HostPublicKey, revision, length); err != nil { return RPCSectorRootsResult{}, fmt.Errorf("invalid request: %w", err) } diff --git a/rhp/v4/rpc_test.go b/rhp/v4/rpc_test.go index b126fc8..220fd2c 100644 --- a/rhp/v4/rpc_test.go +++ b/rhp/v4/rpc_test.go @@ -4,8 +4,10 @@ import ( "bytes" "context" "io" + "maps" "net" "reflect" + "slices" "strings" "sync" "testing" @@ -177,7 +179,7 @@ func TestSettings(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -238,7 +240,7 @@ func TestFormContract(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -298,7 +300,7 @@ func TestFormContractBasis(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -356,7 +358,7 @@ func TestRPCRefresh(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -469,7 +471,7 @@ func TestRPCRenew(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -631,7 +633,7 @@ func TestAccounts(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -733,7 +735,7 @@ func TestReadWriteSector(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -830,7 +832,7 @@ func TestAppendSectors(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -948,7 +950,7 @@ func TestVerifySector(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -1024,7 +1026,7 @@ func TestVerifySector(t *testing.T) { } } -func TestRPCModifySectors(t *testing.T) { +func TestRPCRemoveSectors(t *testing.T) { log := zaptest.NewLogger(t) n, genesis := testutil.V2Network() hostKey, renterKey := types.GeneratePrivateKey(), types.GeneratePrivateKey() @@ -1042,7 +1044,7 @@ func TestRPCModifySectors(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -1130,44 +1132,23 @@ func TestRPCModifySectors(t *testing.T) { assertRevision(t, appendResult.Revision, roots) revision.Revision = appendResult.Revision - // swap two random sectors - swapA, swapB := frand.Uint64n(uint64(len(roots)/2)), frand.Uint64n(uint64(len(roots)/2))+uint64(len(roots)/2) - actions := []proto4.ModifyAction{ - {Type: proto4.ActionSwap, A: swapA, B: swapB}, + // randomly remove half the sectors + indices := make(map[uint64]bool) + var newRoots []types.Hash256 + for len(indices) < len(roots)/2 { + i := frand.Uint64n(uint64(len(roots))) + indices[i] = true } - modifyResult, err := rhp4.RPCModifySectors(context.Background(), transport, cs, settings.Prices, renterKey, revision, actions) - if err != nil { - t.Fatal(err) - } - roots[swapA], roots[swapB] = roots[swapB], roots[swapA] - assertRevision(t, modifyResult.Revision, roots) - revision.Revision = modifyResult.Revision - - // delete the last 10 sectors - actions = []proto4.ModifyAction{ - {Type: proto4.ActionTrim, N: uint64(len(roots) / 2)}, - } - modifyResult, err = rhp4.RPCModifySectors(context.Background(), transport, cs, settings.Prices, renterKey, revision, actions) - if err != nil { - t.Fatal(err) - } - trimmed, roots := roots[len(roots)/2:], roots[:len(roots)/2] - assertRevision(t, modifyResult.Revision, roots) - revision.Revision = modifyResult.Revision - - // update a random sector with one of the trimmed sectors - updateIdx := frand.Intn(len(roots)) - trimmedIdx := frand.Intn(len(trimmed)) - actions = []proto4.ModifyAction{ - {Type: proto4.ActionUpdate, Root: trimmed[trimmedIdx], A: uint64(updateIdx)}, + for i, root := range roots { + if !indices[uint64(i)] { + newRoots = append(newRoots, root) + } } - - modifyResult, err = rhp4.RPCModifySectors(context.Background(), transport, cs, settings.Prices, renterKey, revision, actions) + removeResult, err := rhp4.RPCRemoveSectors(context.Background(), transport, cs, settings.Prices, renterKey, revision, slices.Collect(maps.Keys(indices))) if err != nil { t.Fatal(err) } - roots[updateIdx] = trimmed[trimmedIdx] - assertRevision(t, modifyResult.Revision, roots) + assertRevision(t, removeResult.Revision, newRoots) } func TestRPCSectorRoots(t *testing.T) { @@ -1188,7 +1169,7 @@ func TestRPCSectorRoots(t *testing.T) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -1298,7 +1279,7 @@ func BenchmarkWrite(b *testing.B) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -1388,7 +1369,7 @@ func BenchmarkRead(b *testing.B) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ @@ -1490,7 +1471,7 @@ func BenchmarkContractUpload(b *testing.B) { MaxCollateral: types.Siacoins(10000), MaxContractDuration: 1000, MaxSectorDuration: 3 * 144, - MaxModifyActions: 100, + MaxSectorBatchSize: 100, RemainingStorage: 100 * proto4.SectorSize, TotalStorage: 100 * proto4.SectorSize, Prices: proto4.HostPrices{ diff --git a/rhp/v4/server.go b/rhp/v4/server.go index 9958840..adf7132 100644 --- a/rhp/v4/server.go +++ b/rhp/v4/server.go @@ -250,20 +250,12 @@ func (s *Server) handleRPCWriteSector(stream net.Conn) error { }) } -func (s *Server) handleRPCModifySectors(stream net.Conn) error { - var req rhp4.RPCModifySectorsRequest +func (s *Server) handleRPCRemoveSectors(stream net.Conn) error { + var req rhp4.RPCRemoveSectorsRequest if err := rhp4.ReadRequest(stream, &req); err != nil { return errorDecodingError("failed to read request: %v", err) } - settings := s.settings.RHP4Settings() - if err := req.Validate(s.hostKey.PublicKey(), settings.MaxModifyActions); err != nil { - return errorBadRequest("request invalid: %v", err) - } - prices := req.Prices - - cs := s.chain.TipState() - state, unlock, err := s.lockContractForRevision(req.ContractID) if err != nil { return fmt.Errorf("failed to lock contract: %w", err) @@ -275,53 +267,55 @@ func (s *Server) handleRPCModifySectors(stream net.Conn) error { } fc := state.Revision - cost := prices.RPCModifySectorsCost(req.Actions) + + settings := s.settings.RHP4Settings() + if err := req.Validate(s.hostKey.PublicKey(), fc, settings.MaxSectorBatchSize); err != nil { + return errorBadRequest("request invalid: %v", err) + } + prices := req.Prices + + cost := prices.RPCRemoveSectorsCost(len(req.Indices)) // validate the payment without modifying the contract if fc.RenterOutput.Value.Cmp(cost) < 0 { return rhp4.NewRPCError(rhp4.ErrorCodePayment, fmt.Sprintf("renter output value %v is less than cost %v", fc.RenterOutput.Value, cost)) } - roots := state.Roots - for _, action := range req.Actions { - switch action.Type { - case rhp4.ActionTrim: - if action.N > uint64(len(roots)) { - return errorBadRequest("trim count %v exceeds sector count %v", action.N, len(roots)) - } - roots = roots[:len(roots)-int(action.N)] - case rhp4.ActionUpdate: - if action.N >= uint64(len(roots)) { - return errorBadRequest("update index %v exceeds sector count %v", action.A, len(roots)) - } - roots[action.N] = action.Root - case rhp4.ActionSwap: - if action.A >= uint64(len(roots)) || action.B >= uint64(len(roots)) { - return errorBadRequest("swap indices %v and %v exceed sector count %v", action.A, action.B, len(roots)) - } - roots[action.A], roots[action.B] = roots[action.B], roots[action.A] - default: - return errorBadRequest("unknown action type %v", action.Type) + // validate that all indices are within the expected range + indices := make(map[uint64]bool) + for _, i := range req.Indices { + if i >= uint64(len(state.Roots)) { + return errorBadRequest("index %v exceeds sector count %v", i, len(state.Roots)) + } + indices[i] = true + } + + roots := state.Roots[:0] + for i, root := range state.Roots { + if indices[uint64(i)] { + continue } + roots = append(roots, root) } - treeHashes, leafHashes := rhp4.BuildModifySectorsProof(req.Actions, state.Roots) - resp := rhp4.RPCModifySectorsResponse{ - OldSubtreeHashes: treeHashes, - OldLeafHashes: leafHashes, - NewMerkleRoot: rhp4.MetaRoot(roots), + // treeHashes, leafHashes := rhp4.BuildModifySectorsProof(req.Actions, state.Roots) // TODO: build proof + resp := rhp4.RPCRemoveSectorsResponse{ + // OldSubtreeHashes: treeHashes, + // OldLeafHashes: leafHashes, + NewMerkleRoot: rhp4.MetaRoot(roots), } if err := rhp4.WriteResponse(stream, &resp); err != nil { return fmt.Errorf("failed to write response: %w", err) } - var renterSigResponse rhp4.RPCModifySectorsSecondResponse + var renterSigResponse rhp4.RPCRemoveSectorsSecondResponse if err := rhp4.ReadResponse(stream, &renterSigResponse); err != nil { return errorDecodingError("failed to read renter signature response: %v", err) } - revision, err := rhp4.ReviseForModifySectors(fc, prices, resp.NewMerkleRoot, req.Actions) + revision, err := rhp4.ReviseForRemoveSectors(fc, prices, resp.NewMerkleRoot, len(req.Indices)) if err != nil { return fmt.Errorf("failed to revise contract: %w", err) } + cs := s.chain.TipState() sigHash := cs.ContractSigHash(revision) if !fc.RenterPublicKey.VerifyHash(sigHash, renterSigResponse.RenterSignature) { @@ -336,7 +330,7 @@ func (s *Server) handleRPCModifySectors(stream net.Conn) error { if err != nil { return fmt.Errorf("failed to revise contract: %w", err) } - return rhp4.WriteResponse(stream, &rhp4.RPCModifySectorsThirdResponse{ + return rhp4.WriteResponse(stream, &rhp4.RPCRemoveSectorsThirdResponse{ HostSignature: revision.HostSignature, }) } @@ -348,7 +342,7 @@ func (s *Server) handleRPCAppendSectors(stream net.Conn) error { } settings := s.settings.RHP4Settings() - if err := req.Validate(s.hostKey.PublicKey(), settings.MaxModifyActions); err != nil { + if err := req.Validate(s.hostKey.PublicKey(), settings.MaxSectorBatchSize); err != nil { return errorBadRequest("request invalid: %v", err) } prices := req.Prices @@ -489,7 +483,8 @@ func (s *Server) handleRPCSectorRoots(stream net.Conn) error { defer unlock() // validate the request fields - if err := req.Validate(s.hostKey.PublicKey(), state.Revision); err != nil { + settings := s.settings.RHP4Settings() + if err := req.Validate(s.hostKey.PublicKey(), state.Revision, settings.MaxSectorBatchSize); err != nil { return rhp4.NewRPCError(rhp4.ErrorCodeBadRequest, err.Error()) } prices := req.Prices @@ -1055,8 +1050,8 @@ func (s *Server) handleHostStream(stream net.Conn, log *zap.Logger) { err = s.handleRPCRenewContract(stream) case rhp4.RPCLatestRevisionID: err = s.handleRPCLatestRevision(stream) - case rhp4.RPCModifySectorsID: - err = s.handleRPCModifySectors(stream) + case rhp4.RPCRemoveSectorsID: + err = s.handleRPCRemoveSectors(stream) case rhp4.RPCSectorRootsID: err = s.handleRPCSectorRoots(stream) // account