Skip to content

Commit

Permalink
rhp4: add explicit refresh RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
n8maninger committed Oct 7, 2024
1 parent 80357af commit a05e2a3
Show file tree
Hide file tree
Showing 5 changed files with 629 additions and 203 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.23.0

require (
go.etcd.io/bbolt v1.3.11
go.sia.tech/core v0.4.8-0.20241003192046-425f95763c90
go.sia.tech/core v0.4.8-0.20241007234521-ea8efbfea700
go.sia.tech/mux v1.3.0
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.27.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ 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.20241003192046-425f95763c90 h1:R/G7XXyzLKelfGBGT6XQpih379v5jJx6T4AsjLwyjJc=
go.sia.tech/core v0.4.8-0.20241003192046-425f95763c90/go.mod h1:j2Ke8ihV8or7d2VDrFZWcCkwSVHO0DNMQJAGs9Qop2M=
go.sia.tech/core v0.4.8-0.20241007234354-9a5ba0c39b78 h1:jeYj562vvomoojUFR53Gdky/tkCIboTBR9/8Kf11rBE=
go.sia.tech/core v0.4.8-0.20241007234354-9a5ba0c39b78/go.mod h1:j2Ke8ihV8or7d2VDrFZWcCkwSVHO0DNMQJAGs9Qop2M=
go.sia.tech/core v0.4.8-0.20241007234521-ea8efbfea700 h1:LUaaExNKRMdWEuzaHrSbjrMNkE5yTykuw9lXLXIZHYg=
go.sia.tech/core v0.4.8-0.20241007234521-ea8efbfea700/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=
Expand Down
140 changes: 137 additions & 3 deletions rhp/v4/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ type (
RenewalSet TransactionSet `json:"renewalSet"`
Cost types.Currency `json:"cost"`
}

// RPCRefreshContractResult contains the result of executing the refresh contract RPC.
RPCRefreshContractResult struct {
Contract ContractRevision `json:"contract"`
RenewalSet TransactionSet `json:"renewalSet"`
Cost types.Currency `json:"cost"`
}
)

func callSingleRoundtripRPC(ctx context.Context, t TransportClient, rpcID types.Specifier, req, resp rhp4.Object) error {
Expand Down Expand Up @@ -630,7 +637,7 @@ func RPCFormContract(ctx context.Context, t TransportClient, tp TxPool, signer F

// RPCRenewContract renews a contract with a host.
func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer FormContractSigner, cs consensus.State, p rhp4.HostPrices, existing types.V2FileContract, params rhp4.RPCRenewContractParams) (RPCRenewContractResult, error) {
renewal := rhp4.NewRenewal(existing, p, params)
renewal := rhp4.RenewContract(existing, p, params)
renewalTxn := types.V2Transaction{
MinerFee: tp.RecommendedFee().Mul64(1000),
FileContractResolutions: []types.V2FileContractResolution{
Expand Down Expand Up @@ -659,7 +666,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 {
Expand Down Expand Up @@ -704,7 +710,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)
Expand Down Expand Up @@ -755,3 +761,131 @@ func RPCRenewContract(ctx context.Context, t TransportClient, tp TxPool, signer
Cost: renterCost,
}, nil
}

// RPCRefreshContract renews a contract with a host.
func RPCRefreshContract(ctx context.Context, t TransportClient, tp TxPool, signer FormContractSigner, cs consensus.State, p rhp4.HostPrices, existing types.V2FileContract, params rhp4.RPCRefreshContractParams) (RPCRefreshContractResult, error) {
renewal := rhp4.RefreshContract(existing, p, params)
renewalTxn := types.V2Transaction{
MinerFee: tp.RecommendedFee().Mul64(1000),
FileContractResolutions: []types.V2FileContractResolution{
{
Parent: types.V2FileContractElement{
StateElement: types.StateElement{
// the other parts of the state element are not required
// for signing the transaction. Let the host fill them
// in.
ID: types.Hash256(params.ContractID),
},
},
Resolution: &renewal,
},
},
}

renterCost, hostCost := rhp4.RefreshCost(cs, p, renewal, renewalTxn.MinerFee)
req := rhp4.RPCRefreshContractRequest{
Prices: p,
Refresh: params,
MinerFee: renewalTxn.MinerFee,
}

basis, toSign, err := signer.FundV2Transaction(&renewalTxn, renterCost)
if err != nil {
return RPCRefreshContractResult{}, fmt.Errorf("failed to fund transaction: %w", err)
}
signer.SignV2Inputs(&renewalTxn, toSign)

req.Basis, req.RenterParents, err = tp.V2TransactionSet(basis, renewalTxn)
if err != nil {
return RPCRefreshContractResult{}, 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)

s := t.DialStream(ctx)
defer s.Close()

if err := rhp4.WriteRequest(s, rhp4.RPCRefreshContractID, &req); err != nil {
return RPCRefreshContractResult{}, fmt.Errorf("failed to write request: %w", err)
}

var hostInputsResp rhp4.RPCRefreshContractResponse
if err := rhp4.ReadResponse(s, &hostInputsResp); err != nil {
return RPCRefreshContractResult{}, fmt.Errorf("failed to read host inputs response: %w", err)
}

// add the host inputs to the transaction
var hostInputSum types.Currency
for _, si := range hostInputsResp.HostInputs {
hostInputSum = hostInputSum.Add(si.Parent.SiacoinOutput.Value)
renewalTxn.SiacoinInputs = append(renewalTxn.SiacoinInputs, si)
}

// verify the host added enough inputs
if n := hostInputSum.Cmp(hostCost); n < 0 {
return RPCRefreshContractResult{}, 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
signer.SignV2Inputs(&renewalTxn, []int{0})
// sign the renewal
renewalSigHash := cs.RenewalSigHash(renewal)
renewal.RenterSignature = signer.SignHash(renewalSigHash)

// send the renter signatures
renterPolicyResp := rhp4.RPCRefreshContractSecondResponse{
RenterRenewalSignature: renewal.RenterSignature,
}
for _, si := range renewalTxn.SiacoinInputs[:len(req.RenterInputs)] {
renterPolicyResp.RenterSatisfiedPolicies = append(renterPolicyResp.RenterSatisfiedPolicies, si.SatisfiedPolicy)
}
if err := rhp4.WriteResponse(s, &renterPolicyResp); err != nil {
return RPCRefreshContractResult{}, fmt.Errorf("failed to write signature response: %w", err)
}

// read the finalized transaction set
var hostTransactionSetResp rhp4.RPCRefreshContractThirdResponse
if err := rhp4.ReadResponse(s, &hostTransactionSetResp); err != nil {
return RPCRefreshContractResult{}, fmt.Errorf("failed to read final response: %w", err)
}

if len(hostTransactionSetResp.TransactionSet) == 0 {
return RPCRefreshContractResult{}, fmt.Errorf("expected at least one host transaction")
}
hostRenewalTxn := hostTransactionSetResp.TransactionSet[len(hostTransactionSetResp.TransactionSet)-1]
if len(hostRenewalTxn.FileContractResolutions) != 1 {
return RPCRefreshContractResult{}, fmt.Errorf("expected exactly one resolution")
}

hostRenewal, ok := hostRenewalTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal)
if !ok {
return RPCRefreshContractResult{}, fmt.Errorf("expected renewal resolution")
}

// validate the host signature
if !existing.HostPublicKey.VerifyHash(renewalSigHash, hostRenewal.HostSignature) {
return RPCRefreshContractResult{}, errors.New("invalid host signature")
}
return RPCRefreshContractResult{
Contract: ContractRevision{
ID: params.ContractID.V2RenewalID(),
Revision: renewal.NewContract,
},
RenewalSet: TransactionSet{
Basis: hostTransactionSetResp.Basis,
Transactions: hostTransactionSetResp.TransactionSet,
},
Cost: renterCost,
}, nil
}
Loading

0 comments on commit a05e2a3

Please sign in to comment.