Skip to content

Commit 7e990b1

Browse files
committed
add account pools
1 parent 1d28a32 commit 7e990b1

5 files changed

Lines changed: 1067 additions & 5 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
default: major
3+
---
4+
5+
# Add RHP4 pool RPC handlers, renter helpers, and Contractor interface methods for shared balance pools.
6+
7+
Hosts can now back accounts with shared balance pools so renters don't need to pre-fund and continually rebalance every account. A renter funds a pool once and attaches as many accounts as they want; debits drain the account's own balance first and fall through to attached pools, which keeps per-account allowances small and reduces the total capital sitting idle in account balances. This is the host-side companion to the new RHP4 pool RPCs.
8+
9+
Extends the RHP4 `Contractor` interface with `PoolBalances`, `CreditPoolsWithContract`, `AttachPools`, and `DetachPools` (existing implementations must be updated). Adds host-side handlers for the new pool RPCs and renter-side helpers `RPCFundPools`, `RPCReplenishPools`, `RPCAttachPools`, and `RPCDetachPools`. `DebitAccount` semantics are extended to drain the account's own balance first, then attached pools in attachment order.

rhp/v4/rpc.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,26 @@ type (
229229
Usage rhp4.Usage `json:"usage"`
230230
}
231231

232+
// A PoolBalance pairs a pool key with its current balance.
233+
PoolBalance struct {
234+
Pool rhp4.Account `json:"pool"`
235+
Balance types.Currency `json:"balance"`
236+
}
237+
238+
// RPCReplenishPoolsParams contains the parameters for the replenish pools RPC.
239+
RPCReplenishPoolsParams struct {
240+
Pools []rhp4.Account `json:"pools"`
241+
Target types.Currency `json:"target"`
242+
Contract ContractRevision `json:"contract"`
243+
}
244+
245+
// RPCReplenishPoolsResult contains the result of executing the replenish pools RPC.
246+
RPCReplenishPoolsResult struct {
247+
Revision types.V2FileContract `json:"revision"`
248+
Deposits []rhp4.AccountDeposit `json:"deposits"`
249+
Usage rhp4.Usage `json:"usage"`
250+
}
251+
232252
// RPCSectorRootsResult contains the result of executing the sector roots RPC.
233253
RPCSectorRootsResult struct {
234254
Revision types.V2FileContract `json:"revision"`
@@ -815,6 +835,149 @@ func RPCReplenishAccounts(ctx context.Context, t TransportClient, p RPCReplenish
815835
}, nil
816836
}
817837

838+
// RPCReplenishPools tops up pool balances to a target on the host. Reuses
839+
// RPCReplenishAccounts wire types — the host routes deposits to its pool
840+
// table based on the RPC ID.
841+
func RPCReplenishPools(ctx context.Context, t TransportClient, p RPCReplenishPoolsParams, cs consensus.State, signer ContractSigner) (RPCReplenishPoolsResult, error) {
842+
req := rhp4.RPCReplenishAccountsRequest{
843+
Accounts: p.Pools,
844+
Target: p.Target,
845+
ContractID: p.Contract.ID,
846+
}
847+
challengeSigHash := req.ChallengeSigHash(p.Contract.Revision.RevisionNumber)
848+
req.ChallengeSignature = signer.SignHash(challengeSigHash)
849+
850+
if err := req.Validate(); err != nil {
851+
return RPCReplenishPoolsResult{}, fmt.Errorf("invalid request: %w", err)
852+
}
853+
854+
s, err := openStream(ctx, t, defaultStreamTimeout)
855+
if err != nil {
856+
return RPCReplenishPoolsResult{}, fmt.Errorf("failed to dial stream: %w", err)
857+
}
858+
defer s.Close()
859+
860+
if err := rhp4.WriteRequest(s, rhp4.RPCReplenishPoolsID, &req); err != nil {
861+
return RPCReplenishPoolsResult{}, fmt.Errorf("failed to write request: %w", err)
862+
}
863+
864+
maxCost := p.Target.Mul64(uint64(len(p.Pools)))
865+
866+
var resp rhp4.RPCReplenishAccountsResponse
867+
if err := rhp4.ReadResponse(s, &resp); err != nil {
868+
return RPCReplenishPoolsResult{}, fmt.Errorf("failed to read response: %w", err)
869+
} else if len(resp.Deposits) != len(p.Pools) {
870+
return RPCReplenishPoolsResult{}, fmt.Errorf("expected %v deposits, got %v", len(p.Pools), len(resp.Deposits))
871+
}
872+
873+
for _, deposit := range resp.Deposits {
874+
if deposit.Amount.Cmp(p.Target) > 0 {
875+
return RPCReplenishPoolsResult{}, fmt.Errorf("expected deposit <= %v, got %v", p.Target, deposit.Amount)
876+
}
877+
}
878+
879+
totalCost := resp.TotalCost()
880+
if totalCost.IsZero() {
881+
return RPCReplenishPoolsResult{
882+
Revision: p.Contract.Revision,
883+
Deposits: resp.Deposits,
884+
}, nil
885+
} else if totalCost.Cmp(maxCost) > 0 {
886+
return RPCReplenishPoolsResult{}, fmt.Errorf("expected cost <= %v, got %v", maxCost, totalCost)
887+
}
888+
889+
revision, usage, err := rhp4.ReviseForReplenish(p.Contract.Revision, totalCost)
890+
if err != nil {
891+
return RPCReplenishPoolsResult{}, fmt.Errorf("failed to revise contract: %w", err)
892+
}
893+
894+
sigHash := cs.ContractSigHash(revision)
895+
revision.RenterSignature = signer.SignHash(sigHash)
896+
897+
signatureResp := rhp4.RPCReplenishAccountsSecondResponse{
898+
RenterSignature: revision.RenterSignature,
899+
}
900+
if err := rhp4.WriteResponse(s, &signatureResp); err != nil {
901+
return RPCReplenishPoolsResult{}, fmt.Errorf("failed to write signature response: %w", err)
902+
}
903+
904+
var hostSignature rhp4.RPCReplenishAccountsThirdResponse
905+
if err := rhp4.ReadResponse(s, &hostSignature); err != nil {
906+
return RPCReplenishPoolsResult{}, fmt.Errorf("failed to read host signatures: %w", err)
907+
} else if !p.Contract.Revision.HostPublicKey.VerifyHash(sigHash, hostSignature.HostSignature) {
908+
return RPCReplenishPoolsResult{}, fmt.Errorf("failed to validate host signature: %w", rhp4.ErrInvalidSignature)
909+
}
910+
revision.HostSignature = hostSignature.HostSignature
911+
return RPCReplenishPoolsResult{
912+
Revision: revision,
913+
Deposits: resp.Deposits,
914+
Usage: usage,
915+
}, nil
916+
}
917+
918+
// PoolAttachInput pairs an account with the pool keypair authorizing the
919+
// attachment.
920+
type PoolAttachInput struct {
921+
Account rhp4.Account
922+
PoolKey types.PrivateKey
923+
}
924+
925+
// PoolDetachInput identifies an attachment to sever, paired with the private
926+
// key that signs the request. The signer may be either the account's or the
927+
// pool's key.
928+
type PoolDetachInput struct {
929+
Account rhp4.Account
930+
Pool rhp4.Account
931+
Signer types.PrivateKey
932+
}
933+
934+
// RPCAttachPools batches one or more attachments. Each entry is signed by
935+
// its pool's private key. validity bounds the replay window for the whole
936+
// batch.
937+
func RPCAttachPools(ctx context.Context, t TransportClient, inputs []PoolAttachInput, validity time.Duration) error {
938+
hostKey := t.PeerKey()
939+
deadline := time.Now().Add(validity)
940+
attachments := make([]rhp4.PoolAttachment, 0, len(inputs))
941+
for _, in := range inputs {
942+
a := rhp4.PoolAttachment{
943+
Account: in.Account,
944+
Pool: rhp4.Account(in.PoolKey.PublicKey()),
945+
ValidUntil: deadline,
946+
}
947+
a.Signature = in.PoolKey.SignHash(a.SigHash(hostKey))
948+
attachments = append(attachments, a)
949+
}
950+
req := rhp4.RPCAttachPoolsRequest{Attachments: attachments}
951+
if err := req.Validate(); err != nil {
952+
return fmt.Errorf("invalid request: %w", err)
953+
}
954+
var resp rhp4.RPCAttachPoolsResponse
955+
return callSingleRoundtripRPC(ctx, t, rhp4.RPCAttachPoolsID, &req, &resp)
956+
}
957+
958+
// RPCDetachPools batches one or more detachments. Each entry is signed by
959+
// either the account's or the pool's private key.
960+
func RPCDetachPools(ctx context.Context, t TransportClient, inputs []PoolDetachInput, validity time.Duration) error {
961+
hostKey := t.PeerKey()
962+
deadline := time.Now().Add(validity)
963+
detachments := make([]rhp4.PoolDetachment, 0, len(inputs))
964+
for _, in := range inputs {
965+
d := rhp4.PoolDetachment{
966+
Account: in.Account,
967+
Pool: in.Pool,
968+
ValidUntil: deadline,
969+
}
970+
d.Signature = in.Signer.SignHash(d.SigHash(hostKey))
971+
detachments = append(detachments, d)
972+
}
973+
req := rhp4.RPCDetachPoolsRequest{Detachments: detachments}
974+
if err := req.Validate(); err != nil {
975+
return fmt.Errorf("invalid request: %w", err)
976+
}
977+
var resp rhp4.RPCDetachPoolsResponse
978+
return callSingleRoundtripRPC(ctx, t, rhp4.RPCDetachPoolsID, &req, &resp)
979+
}
980+
818981
// RPCLatestRevision returns the latest revision of a contract.
819982
func RPCLatestRevision(ctx context.Context, t TransportClient, contractID types.FileContractID) (resp rhp4.RPCLatestRevisionResponse, err error) {
820983
req := rhp4.RPCLatestRevisionRequest{ContractID: contractID}

0 commit comments

Comments
 (0)