Skip to content

Commit e0f827a

Browse files
authored
Merge pull request #421 from SiaFoundation/nate/balance-pools
Add RHP4 pool support for shared balance pools
2 parents 1d28a32 + a0b230a commit e0f827a

5 files changed

Lines changed: 1071 additions & 6 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 `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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ var (
3636
// ProtocolVersion502 fixed hosts performing invalid MaxCollateral
3737
// validation on partial rollover refreshes.
3838
ProtocolVersion502 = rhp4.ProtocolVersion{5, 0, 2}
39+
40+
// ProtocolVersion510 added RHP account pools.
41+
ProtocolVersion510 = rhp4.ProtocolVersion{5, 1, 0}
3942
)
4043

4144
var (
@@ -229,6 +232,26 @@ type (
229232
Usage rhp4.Usage `json:"usage"`
230233
}
231234

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

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

0 commit comments

Comments
 (0)