|
36 | 36 | // ProtocolVersion502 fixed hosts performing invalid MaxCollateral |
37 | 37 | // validation on partial rollover refreshes. |
38 | 38 | ProtocolVersion502 = rhp4.ProtocolVersion{5, 0, 2} |
| 39 | + |
| 40 | + // ProtocolVersion510 added RHP account pools. |
| 41 | + ProtocolVersion510 = rhp4.ProtocolVersion{5, 1, 0} |
39 | 42 | ) |
40 | 43 |
|
41 | 44 | var ( |
@@ -229,6 +232,26 @@ type ( |
229 | 232 | Usage rhp4.Usage `json:"usage"` |
230 | 233 | } |
231 | 234 |
|
| 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 | + |
232 | 255 | // RPCSectorRootsResult contains the result of executing the sector roots RPC. |
233 | 256 | RPCSectorRootsResult struct { |
234 | 257 | Revision types.V2FileContract `json:"revision"` |
@@ -815,6 +838,149 @@ func RPCReplenishAccounts(ctx context.Context, t TransportClient, p RPCReplenish |
815 | 838 | }, nil |
816 | 839 | } |
817 | 840 |
|
| 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 | + |
818 | 984 | // RPCLatestRevision returns the latest revision of a contract. |
819 | 985 | func RPCLatestRevision(ctx context.Context, t TransportClient, contractID types.FileContractID) (resp rhp4.RPCLatestRevisionResponse, err error) { |
820 | 986 | req := rhp4.RPCLatestRevisionRequest{ContractID: contractID} |
|
0 commit comments