diff --git a/consensus/validation.go b/consensus/validation.go index 32bb1523..fa939deb 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -670,16 +670,6 @@ func validateV2Siafunds(ms *MidState, txn types.V2Transaction) error { } func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { - // Contract resolutions are height-sensitive, and thus can be invalidated by - // shallow reorgs; to minimize disruption, we require that transactions - // containing a resolution do not create new outputs. Creating, revising or - // resolving contracts *is* permitted, as these effects are generally not - // "built upon" as quickly as outputs, and therefore cause less disruption. - if len(txn.FileContractResolutions) > 0 && - (len(txn.SiacoinOutputs) > 0 || len(txn.SiafundOutputs) > 0) { - return errors.New("transaction both resolves a file contract and creates new outputs") - } - revised := make(map[types.Hash256]int) resolved := make(map[types.Hash256]int) validateParent := func(fce types.V2FileContractElement) error { @@ -759,6 +749,8 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return fmt.Errorf("does not increase revision number (%v -> %v)", cur.RevisionNumber, rev.RevisionNumber) case !revOutputSum.Equals(curOutputSum): return fmt.Errorf("modifies output sum (%d H -> %d H)", curOutputSum, revOutputSum) + case rev.MissedHostValue.Cmp(cur.MissedHostValue) > 0: + return fmt.Errorf("has missed host value (%d H) exceeding old value (%d H)", rev.MissedHostValue, cur.MissedHostValue) case rev.TotalCollateral != cur.TotalCollateral: return errors.New("modifies total collateral") case rev.ProofHeight < ms.base.childHeight(): diff --git a/consensus/validation_test.go b/consensus/validation_test.go index b3d76b6b..a8f03427 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -1815,3 +1815,251 @@ func TestV2RevisionApply(t *testing.T) { applyContractChanges(au) checkRevision(t, 100) } + +func TestV2RenewalResolution(t *testing.T) { + n, genesisBlock := testnet() + + n.HardforkOak.Height = 0 + n.HardforkTax.Height = 0 + n.HardforkFoundation.Height = 0 + n.InitialTarget = types.BlockID{0xFF} + n.HardforkV2.AllowHeight = 0 + n.HardforkV2.RequireHeight = 0 + + pk := types.GeneratePrivateKey() + addr := types.AnyoneCanSpend().Address() + fc := types.V2FileContract{ + ProofHeight: 100, + ExpirationHeight: 150, + RenterPublicKey: pk.PublicKey(), + HostPublicKey: pk.PublicKey(), + HostOutput: types.SiacoinOutput{ + Address: addr, Value: types.Siacoins(10), + }, + RenterOutput: types.SiacoinOutput{ + Address: addr, Value: types.Siacoins(10), + }, + MissedHostValue: types.Siacoins(10), + } + cs := n.GenesisState() + sigHash := cs.ContractSigHash(fc) + fc.HostSignature = pk.SignHash(sigHash) + fc.RenterSignature = pk.SignHash(sigHash) + + genesisTxn := types.V2Transaction{ + SiacoinOutputs: []types.SiacoinOutput{ + {Address: addr, Value: types.Siacoins(1000)}, + }, + FileContracts: []types.V2FileContract{fc}, + } + genesisBlock.V2 = &types.V2BlockData{ + Transactions: []types.V2Transaction{genesisTxn}, + } + contractID := genesisTxn.V2FileContractID(genesisTxn.ID(), 0) + fces := make(map[types.Hash256]types.V2FileContractElement) + genesisOutput := genesisTxn.EphemeralSiacoinOutput(0) + applyChanges := func(au ApplyUpdate) { + au.ForEachV2FileContractElement(func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + switch { + case res != nil: + delete(fces, fce.ID) + case rev != nil: + fces[fce.ID] = *rev + default: + fces[fce.ID] = fce + } + }) + + au.ForEachSiacoinElement(func(sce types.SiacoinElement, created, spent bool) { + if sce.ID == genesisOutput.ID { + genesisOutput = sce + } + }) + + // update proofs + au.UpdateElementProof(&genesisOutput.StateElement) + for key, fce := range fces { + au.UpdateElementProof(&fce.StateElement) + fces[key] = fce + } + } + + // confirm the contract + cs, au := ApplyBlock(cs, genesisBlock, V1BlockSupplement{}, time.Time{}) + applyChanges(au) + + tests := []struct { + desc string + renewFn func(*types.V2Transaction) + errors bool + errString string + }{ + { + desc: "valid renewal", + renewFn: func(vt *types.V2Transaction) {}, // no changes should be a valid renewal + }, + { + desc: "valid renewal - no renter rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = types.ZeroCurrency + // subtract the renter cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "valid renewal - no host rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.HostRollover = types.ZeroCurrency + // subtract the host cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.HostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "valid renewal - partial host rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2) + // subtract the host cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.HostOutput.Value.Div64(2)).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "valid renewal - partial renter rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2) + // subtract the host cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value.Div64(2)).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "invalid renewal - not enough host funds", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2) + // do not adjust the change output + }, + errors: true, + errString: "do not equal outputs", + }, + { + desc: "invalid renewal - not enough renter funds", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2) + // do not adjust the change output + }, + errors: true, + errString: "do not equal outputs", + }, + { + desc: "invalid renewal - host rollover escape", + renewFn: func(txn *types.V2Transaction) { + // tests that the file contract renewal rollover cannot be used + // outside of the new file contract. i.e. a siacoin output should + // not be able to be created using the funds from a rollover. This + // ensures that the maturity delay is enforced for renewals. + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.HostOutput.Value = types.Siacoins(1) + renewal.NewContract.MissedHostValue = types.Siacoins(1) + // adjust the file contract tax + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(cs.V2FileContractTax(renewal.NewContract)) + escapeAmount := renewal.HostRollover.Sub(renewal.NewContract.HostOutput.Value) + txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{Value: escapeAmount, Address: types.VoidAddress}) + }, + errors: true, + errString: "exceeding new contract cost", + }, + { + desc: "invalid renewal - renter rollover escape", + renewFn: func(txn *types.V2Transaction) { + // tests that the file contract renewal rollover cannot be used + // outside of the new file contract. i.e. a siacoin output should + // not be able to be created using the funds from a rollover. This + // ensures that the maturity delay is enforced for renewals. + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.RenterOutput.Value = types.Siacoins(1) + // adjust the file contract tax + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(cs.V2FileContractTax(renewal.NewContract)) + escapeAmount := renewal.RenterRollover.Sub(renewal.NewContract.RenterOutput.Value) + txn.SiacoinOutputs = append(txn.SiacoinOutputs, types.SiacoinOutput{Value: escapeAmount, Address: types.VoidAddress}) + }, + errors: true, + errString: "exceeding new contract cost", + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + finalRevision := fc + finalRevision.RevisionNumber = types.MaxRevisionNumber + finalRevision.RenterSignature = types.Signature{} + finalRevision.HostSignature = types.Signature{} + + fc := types.V2FileContract{ + ProofHeight: 100, + ExpirationHeight: 150, + RenterPublicKey: pk.PublicKey(), + HostPublicKey: pk.PublicKey(), + HostOutput: types.SiacoinOutput{ + Address: addr, Value: types.Siacoins(10), + }, + RenterOutput: types.SiacoinOutput{ + Address: addr, Value: types.Siacoins(10), + }, + MissedHostValue: types.Siacoins(10), + } + tax := cs.V2FileContractTax(fc) + renewTxn := types.V2Transaction{ + FileContractResolutions: []types.V2FileContractResolution{ + { + Parent: fces[types.Hash256(contractID)], + Resolution: &types.V2FileContractRenewal{ + FinalRevision: finalRevision, + NewContract: fc, + RenterRollover: types.Siacoins(10), + HostRollover: types.Siacoins(10), + }, + }, + }, + SiacoinInputs: []types.V2SiacoinInput{ + { + Parent: genesisOutput, + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.AnyoneCanSpend(), + }, + }, + }, + SiacoinOutputs: []types.SiacoinOutput{ + {Address: addr, Value: genesisOutput.SiacoinOutput.Value.Sub(tax)}, + }, + } + resolution, ok := renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + if !ok { + t.Fatal("expected renewal resolution") + } + + // modify the renewal + test.renewFn(&renewTxn) + + // sign the renewal + sigHash := cs.RenewalSigHash(*resolution) + resolution.RenterSignature = pk.SignHash(sigHash) + resolution.HostSignature = pk.SignHash(sigHash) + // apply the renewal + ms := NewMidState(cs) + err := ValidateV2Transaction(ms, renewTxn) + switch { + case test.errors && err == nil: + t.Fatal("expected error") + case test.errors && test.errString == "": + t.Fatalf("received error %q, missing error string to compare", err) + case test.errors && !strings.Contains(err.Error(), test.errString): + t.Fatalf("expected error %q to contain %q", err, test.errString) + case !test.errors && err != nil: + t.Fatalf("unexpected error: %q", err) + } + }) + } +} diff --git a/rhp/v4/encoding.go b/rhp/v4/encoding.go new file mode 100644 index 00000000..79586659 --- /dev/null +++ b/rhp/v4/encoding.go @@ -0,0 +1,655 @@ +package rhp + +import ( + "bytes" + + "go.sia.tech/core/types" +) + +// EncodeTo implements types.EncoderTo. +func (ad AccountDeposit) EncodeTo(e *types.Encoder) { + ad.Account.EncodeTo(e) + types.V2Currency(ad.Amount).EncodeTo(e) +} + +// DecodeFrom implements types.DecoderFrom. +func (ad *AccountDeposit) DecodeFrom(d *types.Decoder) { + ad.Account.DecodeFrom(d) + (*types.V2Currency)(&ad.Amount).DecodeFrom(d) +} + +// EncodeTo implements types.EncoderTo. +func (hp HostPrices) EncodeTo(e *types.Encoder) { + types.V2Currency(hp.ContractPrice).EncodeTo(e) + types.V2Currency(hp.Collateral).EncodeTo(e) + types.V2Currency(hp.StoragePrice).EncodeTo(e) + types.V2Currency(hp.IngressPrice).EncodeTo(e) + types.V2Currency(hp.EgressPrice).EncodeTo(e) + types.V2Currency(hp.FreeSectorPrice).EncodeTo(e) + e.WriteUint64(hp.TipHeight) + e.WriteTime(hp.ValidUntil) + hp.Signature.EncodeTo(e) +} + +// DecodeFrom implements types.DecoderFrom. +func (hp *HostPrices) DecodeFrom(d *types.Decoder) { + (*types.V2Currency)(&hp.ContractPrice).DecodeFrom(d) + (*types.V2Currency)(&hp.Collateral).DecodeFrom(d) + (*types.V2Currency)(&hp.StoragePrice).DecodeFrom(d) + (*types.V2Currency)(&hp.IngressPrice).DecodeFrom(d) + (*types.V2Currency)(&hp.EgressPrice).DecodeFrom(d) + (*types.V2Currency)(&hp.FreeSectorPrice).DecodeFrom(d) + hp.TipHeight = d.ReadUint64() + hp.ValidUntil = d.ReadTime() + hp.Signature.DecodeFrom(d) +} + +// EncodeTo implements types.EncoderTo. +func (hs HostSettings) EncodeTo(e *types.Encoder) { + e.Write(hs.ProtocolVersion[:]) + e.WriteString(hs.Release) + hs.WalletAddress.EncodeTo(e) + e.WriteBool(hs.AcceptingContracts) + types.V2Currency(hs.MaxCollateral).EncodeTo(e) + e.WriteUint64(hs.MaxContractDuration) + e.WriteUint64(hs.MaxSectorDuration) + e.WriteUint64(hs.MaxSectorBatchSize) + e.WriteUint64(hs.RemainingStorage) + e.WriteUint64(hs.TotalStorage) + hs.Prices.EncodeTo(e) +} + +// DecodeFrom implements types.DecoderFrom. +func (hs *HostSettings) DecodeFrom(d *types.Decoder) { + d.Read(hs.ProtocolVersion[:]) + hs.Release = d.ReadString() + hs.WalletAddress.DecodeFrom(d) + hs.AcceptingContracts = d.ReadBool() + (*types.V2Currency)(&hs.MaxCollateral).DecodeFrom(d) + hs.MaxContractDuration = d.ReadUint64() + hs.MaxSectorDuration = d.ReadUint64() + hs.MaxSectorBatchSize = d.ReadUint64() + hs.RemainingStorage = d.ReadUint64() + hs.TotalStorage = d.ReadUint64() + hs.Prices.DecodeFrom(d) +} + +// EncodeTo implements types.EncoderTo. +func (a Account) EncodeTo(e *types.Encoder) { e.Write(a[:]) } + +// DecodeFrom implements types.DecoderFrom. +func (a *Account) DecodeFrom(d *types.Decoder) { d.Read(a[:]) } + +func (at AccountToken) encodeTo(e *types.Encoder) { + at.Account.EncodeTo(e) + e.WriteTime(at.ValidUntil) + at.Signature.EncodeTo(e) +} + +func (at *AccountToken) decodeFrom(d *types.Decoder) { + at.Account.DecodeFrom(d) + at.ValidUntil = d.ReadTime() + at.Signature.DecodeFrom(d) +} + +func (r *RPCError) encodeTo(e *types.Encoder) { + e.WriteUint8(r.Code) + e.WriteString(r.Description) +} +func (r *RPCError) decodeFrom(d *types.Decoder) { + r.Code = d.ReadUint8() + r.Description = d.ReadString() +} +func (r *RPCError) maxLen() int { + return 1024 +} + +const ( + reasonableObjectSize = 10 * 1024 + reasonableTransactionSetSize = 100 * 1024 +) + +func sizeof(v types.EncoderTo) int { + var buf bytes.Buffer + e := types.NewEncoder(&buf) + v.EncodeTo(e) + e.Flush() + return buf.Len() +} + +var ( + sizeofCurrency = sizeof(types.V2Currency{}) + sizeofHash = sizeof(types.Hash256{}) + sizeofSignature = sizeof(types.Signature{}) + sizeofContract = sizeof(types.V2FileContract{}) + sizeofPrices = sizeof(HostPrices{}) + sizeofAccount = sizeof(Account{}) + sizeofAccountToken = sizeof(types.EncoderFunc(AccountToken{}.encodeTo)) +) + +// An Object can be sent or received via a Transport. +type Object interface { + encodeTo(*types.Encoder) + decodeFrom(*types.Decoder) + maxLen() int +} + +func (*RPCSettingsRequest) encodeTo(*types.Encoder) {} +func (*RPCSettingsRequest) decodeFrom(*types.Decoder) {} +func (*RPCSettingsRequest) maxLen() int { return 0 } + +func (r *RPCSettingsResponse) encodeTo(e *types.Encoder) { + r.Settings.EncodeTo(e) +} +func (r *RPCSettingsResponse) decodeFrom(d *types.Decoder) { + r.Settings.DecodeFrom(d) +} +func (r *RPCSettingsResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCFormContractParams) encodeTo(e *types.Encoder) { + r.RenterPublicKey.EncodeTo(e) + r.RenterAddress.EncodeTo(e) + types.V2Currency(r.Allowance).EncodeTo(e) + types.V2Currency(r.Collateral).EncodeTo(e) + e.WriteUint64(r.ProofHeight) +} + +func (r *RPCFormContractParams) decodeFrom(d *types.Decoder) { + r.RenterPublicKey.DecodeFrom(d) + r.RenterAddress.DecodeFrom(d) + (*types.V2Currency)(&r.Allowance).DecodeFrom(d) + (*types.V2Currency)(&r.Collateral).DecodeFrom(d) + r.ProofHeight = d.ReadUint64() +} + +func (r *RPCFormContractRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + r.Contract.encodeTo(e) + r.Basis.EncodeTo(e) + types.V2Currency(r.MinerFee).EncodeTo(e) + types.EncodeSlice(e, r.RenterInputs) + types.EncodeSlice(e, r.RenterParents) +} +func (r *RPCFormContractRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + r.Contract.decodeFrom(d) + r.Basis.DecodeFrom(d) + (*types.V2Currency)(&r.MinerFee).DecodeFrom(d) + types.DecodeSlice(d, &r.RenterInputs) + types.DecodeSlice(d, &r.RenterParents) +} +func (r *RPCFormContractRequest) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCFormContractResponse) encodeTo(e *types.Encoder) { + types.EncodeSlice(e, r.HostInputs) +} + +func (r *RPCFormContractResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.HostInputs) +} +func (r *RPCFormContractResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCFormContractSecondResponse) encodeTo(e *types.Encoder) { + r.RenterContractSignature.EncodeTo(e) + types.EncodeSlice(e, r.RenterSatisfiedPolicies) +} +func (r *RPCFormContractSecondResponse) decodeFrom(d *types.Decoder) { + r.RenterContractSignature.DecodeFrom(d) + types.DecodeSlice(d, &r.RenterSatisfiedPolicies) +} +func (r *RPCFormContractSecondResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCFormContractThirdResponse) encodeTo(e *types.Encoder) { + r.Basis.EncodeTo(e) + types.EncodeSlice(e, r.TransactionSet) +} +func (r *RPCFormContractThirdResponse) decodeFrom(d *types.Decoder) { + r.Basis.DecodeFrom(d) + types.DecodeSlice(d, &r.TransactionSet) +} +func (r *RPCFormContractThirdResponse) maxLen() int { + return reasonableTransactionSetSize +} + +func (r *RPCRenewContractParams) encodeTo(e *types.Encoder) { + r.ContractID.EncodeTo(e) + types.V2Currency(r.Allowance).EncodeTo(e) + types.V2Currency(r.Collateral).EncodeTo(e) + e.WriteUint64(r.ProofHeight) +} + +func (r *RPCRenewContractParams) decodeFrom(d *types.Decoder) { + r.ContractID.DecodeFrom(d) + (*types.V2Currency)(&r.Allowance).DecodeFrom(d) + (*types.V2Currency)(&r.Collateral).DecodeFrom(d) + r.ProofHeight = d.ReadUint64() +} + +func (r *RPCRenewContractRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + r.Renewal.encodeTo(e) + types.V2Currency(r.MinerFee).EncodeTo(e) + r.Basis.EncodeTo(e) + types.EncodeSlice(e, r.RenterInputs) + types.EncodeSlice(e, r.RenterParents) + r.ChallengeSignature.EncodeTo(e) +} +func (r *RPCRenewContractRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + r.Renewal.decodeFrom(d) + (*types.V2Currency)(&r.MinerFee).DecodeFrom(d) + r.Basis.DecodeFrom(d) + types.DecodeSlice(d, &r.RenterInputs) + types.DecodeSlice(d, &r.RenterParents) + r.ChallengeSignature.DecodeFrom(d) +} +func (r *RPCRenewContractRequest) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCRenewContractResponse) encodeTo(e *types.Encoder) { + types.EncodeSlice(e, r.HostInputs) +} +func (r *RPCRenewContractResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.HostInputs) +} +func (r *RPCRenewContractResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCRenewContractSecondResponse) encodeTo(e *types.Encoder) { + r.RenterRenewalSignature.EncodeTo(e) + types.EncodeSlice(e, r.RenterSatisfiedPolicies) +} +func (r *RPCRenewContractSecondResponse) decodeFrom(d *types.Decoder) { + r.RenterRenewalSignature.DecodeFrom(d) + types.DecodeSlice(d, &r.RenterSatisfiedPolicies) +} +func (r *RPCRenewContractSecondResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCRenewContractThirdResponse) encodeTo(e *types.Encoder) { + r.Basis.EncodeTo(e) + types.EncodeSlice(e, r.TransactionSet) +} +func (r *RPCRenewContractThirdResponse) decodeFrom(d *types.Decoder) { + r.Basis.DecodeFrom(d) + types.DecodeSlice(d, &r.TransactionSet) +} +func (r *RPCRenewContractThirdResponse) maxLen() int { + return reasonableTransactionSetSize +} + +func (r *RPCRefreshContractParams) encodeTo(e *types.Encoder) { + r.ContractID.EncodeTo(e) + types.V2Currency(r.Allowance).EncodeTo(e) + types.V2Currency(r.Collateral).EncodeTo(e) +} + +func (r *RPCRefreshContractParams) decodeFrom(d *types.Decoder) { + r.ContractID.DecodeFrom(d) + (*types.V2Currency)(&r.Allowance).DecodeFrom(d) + (*types.V2Currency)(&r.Collateral).DecodeFrom(d) +} + +func (r *RPCRefreshContractRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + r.Refresh.encodeTo(e) + types.V2Currency(r.MinerFee).EncodeTo(e) + r.Basis.EncodeTo(e) + types.EncodeSlice(e, r.RenterInputs) + types.EncodeSlice(e, r.RenterParents) + r.ChallengeSignature.EncodeTo(e) +} +func (r *RPCRefreshContractRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + r.Refresh.decodeFrom(d) + (*types.V2Currency)(&r.MinerFee).DecodeFrom(d) + r.Basis.DecodeFrom(d) + types.DecodeSlice(d, &r.RenterInputs) + types.DecodeSlice(d, &r.RenterParents) + r.ChallengeSignature.DecodeFrom(d) +} +func (r *RPCRefreshContractRequest) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCRefreshContractResponse) encodeTo(e *types.Encoder) { + types.EncodeSlice(e, r.HostInputs) +} +func (r *RPCRefreshContractResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.HostInputs) +} +func (r *RPCRefreshContractResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCRefreshContractSecondResponse) encodeTo(e *types.Encoder) { + r.RenterRenewalSignature.EncodeTo(e) + types.EncodeSlice(e, r.RenterSatisfiedPolicies) +} +func (r *RPCRefreshContractSecondResponse) decodeFrom(d *types.Decoder) { + r.RenterRenewalSignature.DecodeFrom(d) + types.DecodeSlice(d, &r.RenterSatisfiedPolicies) +} +func (r *RPCRefreshContractSecondResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCRefreshContractThirdResponse) encodeTo(e *types.Encoder) { + r.Basis.EncodeTo(e) + types.EncodeSlice(e, r.TransactionSet) +} +func (r *RPCRefreshContractThirdResponse) decodeFrom(d *types.Decoder) { + r.Basis.DecodeFrom(d) + types.DecodeSlice(d, &r.TransactionSet) +} +func (r *RPCRefreshContractThirdResponse) maxLen() int { + return reasonableTransactionSetSize +} + +func (r *RPCFreeSectorsRequest) encodeTo(e *types.Encoder) { + r.ContractID.EncodeTo(e) + r.Prices.EncodeTo(e) + types.EncodeSliceFn(e, r.Indices, func(e *types.Encoder, v uint64) { + e.WriteUint64(v) + }) + r.ChallengeSignature.EncodeTo(e) +} +func (r *RPCFreeSectorsRequest) decodeFrom(d *types.Decoder) { + r.ContractID.DecodeFrom(d) + r.Prices.DecodeFrom(d) + types.DecodeSliceFn(d, &r.Indices, func(d *types.Decoder) uint64 { + return d.ReadUint64() + }) + r.ChallengeSignature.DecodeFrom(d) +} +func (r *RPCFreeSectorsRequest) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCFreeSectorsResponse) encodeTo(e *types.Encoder) { + types.EncodeSlice(e, r.OldSubtreeHashes) + types.EncodeSlice(e, r.OldLeafHashes) + r.NewMerkleRoot.EncodeTo(e) +} +func (r *RPCFreeSectorsResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.OldSubtreeHashes) + types.DecodeSlice(d, &r.OldLeafHashes) + r.NewMerkleRoot.DecodeFrom(d) +} +func (r *RPCFreeSectorsResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCFreeSectorsSecondResponse) encodeTo(e *types.Encoder) { + r.RenterSignature.EncodeTo(e) +} +func (r *RPCFreeSectorsSecondResponse) decodeFrom(d *types.Decoder) { + r.RenterSignature.DecodeFrom(d) +} +func (r *RPCFreeSectorsSecondResponse) maxLen() int { + return sizeofSignature +} + +func (r *RPCFreeSectorsThirdResponse) encodeTo(e *types.Encoder) { + r.HostSignature.EncodeTo(e) +} +func (r *RPCFreeSectorsThirdResponse) decodeFrom(d *types.Decoder) { + r.HostSignature.DecodeFrom(d) +} +func (r *RPCFreeSectorsThirdResponse) maxLen() int { + return sizeofSignature +} + +func (r *RPCAppendSectorsRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + types.EncodeSlice(e, r.Sectors) + r.ContractID.EncodeTo(e) + r.ChallengeSignature.EncodeTo(e) +} +func (r *RPCAppendSectorsRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + types.DecodeSlice(d, &r.Sectors) + r.ContractID.DecodeFrom(d) + r.ChallengeSignature.DecodeFrom(d) +} +func (r *RPCAppendSectorsRequest) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCAppendSectorsResponse) encodeTo(e *types.Encoder) { + types.EncodeSliceFn(e, r.Accepted, (*types.Encoder).WriteBool) + types.EncodeSlice(e, r.SubtreeRoots) + r.NewMerkleRoot.EncodeTo(e) +} +func (r *RPCAppendSectorsResponse) decodeFrom(d *types.Decoder) { + types.DecodeSliceFn(d, &r.Accepted, (*types.Decoder).ReadBool) + types.DecodeSlice(d, &r.SubtreeRoots) + r.NewMerkleRoot.DecodeFrom(d) +} +func (r *RPCAppendSectorsResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCAppendSectorsSecondResponse) encodeTo(e *types.Encoder) { + r.RenterSignature.EncodeTo(e) +} +func (r *RPCAppendSectorsSecondResponse) decodeFrom(d *types.Decoder) { + r.RenterSignature.DecodeFrom(d) +} +func (r *RPCAppendSectorsSecondResponse) maxLen() int { + return sizeofSignature +} + +func (r *RPCAppendSectorsThirdResponse) encodeTo(e *types.Encoder) { + r.HostSignature.EncodeTo(e) +} +func (r *RPCAppendSectorsThirdResponse) decodeFrom(d *types.Decoder) { + r.HostSignature.DecodeFrom(d) +} +func (r *RPCAppendSectorsThirdResponse) maxLen() int { + return sizeofSignature +} + +func (r *RPCLatestRevisionRequest) encodeTo(e *types.Encoder) { + r.ContractID.EncodeTo(e) +} +func (r *RPCLatestRevisionRequest) decodeFrom(d *types.Decoder) { + r.ContractID.DecodeFrom(d) +} +func (r *RPCLatestRevisionRequest) maxLen() int { + return sizeofHash +} + +func (r *RPCLatestRevisionResponse) encodeTo(e *types.Encoder) { + r.Contract.EncodeTo(e) +} +func (r *RPCLatestRevisionResponse) decodeFrom(d *types.Decoder) { + r.Contract.DecodeFrom(d) +} +func (r *RPCLatestRevisionResponse) maxLen() int { + return sizeofContract +} + +func (r *RPCReadSectorRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + r.Token.encodeTo(e) + r.Root.EncodeTo(e) + e.WriteUint64(r.Offset) + e.WriteUint64(r.Length) +} +func (r *RPCReadSectorRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + r.Token.decodeFrom(d) + r.Root.DecodeFrom(d) + r.Offset = d.ReadUint64() + r.Length = d.ReadUint64() +} +func (r *RPCReadSectorRequest) maxLen() int { + return sizeofPrices + sizeofAccountToken + sizeofHash + 8 + 8 +} + +func (r *RPCReadSectorResponse) encodeTo(e *types.Encoder) { + types.EncodeSlice(e, r.Proof) + e.WriteBytes(r.Sector) +} +func (r *RPCReadSectorResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.Proof) + r.Sector = d.ReadBytes() +} +func (r *RPCReadSectorResponse) maxLen() int { + return reasonableObjectSize + 8 + SectorSize +} + +func (r *RPCReadSectorStreamedResponse) encodeTo(e *types.Encoder) { + panic("use RPCReadSectorResponse") // developer error +} +func (r *RPCReadSectorStreamedResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.Proof) + r.DataLength = d.ReadUint64() +} +func (r *RPCReadSectorStreamedResponse) maxLen() int { + return reasonableObjectSize + 8 + SectorSize +} + +func (r RPCWriteSectorStreamingRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + r.Token.encodeTo(e) + e.WriteUint64(r.Duration) + e.WriteUint64(r.DataLength) +} +func (r *RPCWriteSectorStreamingRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + r.Token.decodeFrom(d) + r.Duration = d.ReadUint64() + r.DataLength = d.ReadUint64() +} +func (r *RPCWriteSectorStreamingRequest) maxLen() int { + return sizeofPrices + sizeofAccountToken + 8 + 8 +} + +func (r *RPCWriteSectorResponse) encodeTo(e *types.Encoder) { + r.Root.EncodeTo(e) +} +func (r *RPCWriteSectorResponse) decodeFrom(d *types.Decoder) { + r.Root.DecodeFrom(d) +} +func (r *RPCWriteSectorResponse) maxLen() int { + return sizeofHash +} + +func (r *RPCSectorRootsRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + r.ContractID.EncodeTo(e) + r.RenterSignature.EncodeTo(e) + e.WriteUint64(r.Offset) + e.WriteUint64(r.Length) +} +func (r *RPCSectorRootsRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + r.ContractID.DecodeFrom(d) + r.RenterSignature.DecodeFrom(d) + r.Offset = d.ReadUint64() + r.Length = d.ReadUint64() +} +func (r *RPCSectorRootsRequest) maxLen() int { + return sizeofPrices + 32 + sizeofSignature + 8 + 8 +} + +func (r *RPCSectorRootsResponse) encodeTo(e *types.Encoder) { + types.EncodeSlice(e, r.Proof) + types.EncodeSlice(e, r.Roots) + r.HostSignature.EncodeTo(e) +} +func (r *RPCSectorRootsResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.Proof) + types.DecodeSlice(d, &r.Roots) + r.HostSignature.DecodeFrom(d) +} +func (r *RPCSectorRootsResponse) maxLen() int { + return 1 << 20 // 1 MiB +} + +func (r *RPCAccountBalanceRequest) encodeTo(e *types.Encoder) { + r.Account.EncodeTo(e) +} +func (r *RPCAccountBalanceRequest) decodeFrom(d *types.Decoder) { + r.Account.DecodeFrom(d) +} +func (r *RPCAccountBalanceRequest) maxLen() int { + return sizeofAccount +} + +func (r *RPCAccountBalanceResponse) encodeTo(e *types.Encoder) { + types.V2Currency(r.Balance).EncodeTo(e) +} +func (r *RPCAccountBalanceResponse) decodeFrom(d *types.Decoder) { + (*types.V2Currency)(&r.Balance).DecodeFrom(d) +} +func (r *RPCAccountBalanceResponse) maxLen() int { + return sizeofCurrency +} + +func (r *RPCFundAccountsRequest) encodeTo(e *types.Encoder) { + r.ContractID.EncodeTo(e) + types.EncodeSlice(e, r.Deposits) + r.RenterSignature.EncodeTo(e) +} +func (r *RPCFundAccountsRequest) decodeFrom(d *types.Decoder) { + r.ContractID.DecodeFrom(d) + types.DecodeSlice(d, &r.Deposits) + r.RenterSignature.DecodeFrom(d) +} +func (r *RPCFundAccountsRequest) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCFundAccountsResponse) encodeTo(e *types.Encoder) { + types.EncodeSliceCast[types.V2Currency](e, r.Balances) + r.HostSignature.EncodeTo(e) +} +func (r *RPCFundAccountsResponse) decodeFrom(d *types.Decoder) { + types.DecodeSliceCast[types.V2Currency, types.Currency](d, &r.Balances) + r.HostSignature.DecodeFrom(d) +} +func (r *RPCFundAccountsResponse) maxLen() int { + return reasonableObjectSize +} + +func (r *RPCVerifySectorRequest) encodeTo(e *types.Encoder) { + r.Prices.EncodeTo(e) + r.Token.encodeTo(e) + r.Root.EncodeTo(e) + e.WriteUint64(r.LeafIndex) +} +func (r *RPCVerifySectorRequest) decodeFrom(d *types.Decoder) { + r.Prices.DecodeFrom(d) + r.Token.decodeFrom(d) + r.Root.DecodeFrom(d) + r.LeafIndex = d.ReadUint64() +} +func (r *RPCVerifySectorRequest) maxLen() int { + return 1024 +} + +func (r *RPCVerifySectorResponse) encodeTo(e *types.Encoder) { + types.EncodeSlice(e, r.Proof) + e.Write(r.Leaf[:]) +} +func (r *RPCVerifySectorResponse) decodeFrom(d *types.Decoder) { + types.DecodeSlice(d, &r.Proof) + d.Read(r.Leaf[:]) +} +func (r *RPCVerifySectorResponse) maxLen() int { + return reasonableObjectSize +} diff --git a/rhp/v4/errors.go b/rhp/v4/errors.go new file mode 100644 index 00000000..777f3016 --- /dev/null +++ b/rhp/v4/errors.go @@ -0,0 +1,64 @@ +package rhp + +import ( + "errors" + "fmt" +) + +// Error codes. +const ( + ErrorCodeTransport = iota + 1 + ErrorCodeHostError + ErrorCodeBadRequest + ErrorCodeDecoding + ErrorCodePayment +) + +// An RPCError pairs a human-readable error description with a status code. +type RPCError struct { + Code uint8 + Description string +} + +var ( + // ErrTokenExpired is returned when an account token has expired. + ErrTokenExpired = NewRPCError(ErrorCodeBadRequest, "account token expired") + // ErrPricesExpired is returned when the host's prices have expired. + ErrPricesExpired = NewRPCError(ErrorCodeBadRequest, "prices expired") + // ErrInvalidSignature is returned when a signature is invalid. + ErrInvalidSignature = NewRPCError(ErrorCodeBadRequest, "invalid signature") + // ErrNotEnoughFunds is returned when a client has insufficient funds to + // pay for an RPC. + ErrNotEnoughFunds = NewRPCError(ErrorCodePayment, "not enough funds") + // ErrHostFundError is returned when the host encounters an error while + // funding a formation or renewal transaction. + ErrHostFundError = NewRPCError(ErrorCodeHostError, "host funding error") + // ErrSectorNotFound is returned when the host is not storing a sector. + ErrSectorNotFound = NewRPCError(ErrorCodeHostError, "sector not found") + // ErrNotEnoughStorage is returned when the host does not have enough + // storage to store a sector. + ErrNotEnoughStorage = NewRPCError(ErrorCodeHostError, "not enough storage") + + // ErrHostInternalError is a catch-all for any error that occurs on the host + // side and is not the client's fault. + ErrHostInternalError = NewRPCError(ErrorCodeHostError, "internal error") +) + +// Error implements error. +func (e RPCError) Error() string { + return fmt.Sprintf("%v (%v)", e.Description, e.Code) +} + +// NewRPCError returns a new RPCError with the given code and description. +func NewRPCError(code uint8, desc string) error { + return &RPCError{Code: code, Description: desc} +} + +// ErrorCode returns the code of err. If err is not an RPCError, ErrorCode +// returns ErrorCodeTransport. +func ErrorCode(err error) uint8 { + if re := new(RPCError); errors.As(err, &re) { + return re.Code + } + return ErrorCodeTransport +} diff --git a/rhp/v4/merkle.go b/rhp/v4/merkle.go new file mode 100644 index 00000000..4992938b --- /dev/null +++ b/rhp/v4/merkle.go @@ -0,0 +1,135 @@ +package rhp + +import ( + "io" + "math/bits" + + "go.sia.tech/core/internal/blake2b" + rhp2 "go.sia.tech/core/rhp/v2" + "go.sia.tech/core/types" +) + +const ( + // LeafSize is the size of one leaf in bytes. + LeafSize = rhp2.LeafSize + + // LeavesPerSector is the number of leaves in one sector. + LeavesPerSector = rhp2.LeavesPerSector +) + +// SectorRoot computes the Merkle root of a sector. +func SectorRoot(sector *[SectorSize]byte) types.Hash256 { + return rhp2.SectorRoot(sector) +} + +// ReaderRoot returns the Merkle root of the supplied stream, which must contain +// an integer multiple of leaves. +func ReaderRoot(r io.Reader) (types.Hash256, error) { + return rhp2.ReaderRoot(r) +} + +// ReadSector reads a single sector from r and calculates its root. +func ReadSector(r io.Reader) (types.Hash256, *[SectorSize]byte, error) { + return rhp2.ReadSector(r) +} + +// MetaRoot calculates the root of a set of existing Merkle roots. +func MetaRoot(roots []types.Hash256) types.Hash256 { + return rhp2.MetaRoot(roots) +} + +// BuildSectorProof builds a Merkle proof for a given range within a sector. +func BuildSectorProof(sector *[SectorSize]byte, start, end uint64) []types.Hash256 { + return rhp2.BuildProof(sector, start, end, nil) +} + +// A RangeProofVerifier allows range proofs to be verified in streaming fashion. +type RangeProofVerifier = rhp2.RangeProofVerifier + +// NewRangeProofVerifier returns a RangeProofVerifier for the sector range +// [start, end). +func NewRangeProofVerifier(start, end uint64) *RangeProofVerifier { + return rhp2.NewRangeProofVerifier(start, end) +} + +// VerifyLeafProof verifies the Merkle proof for a given leaf within a sector. +func VerifyLeafProof(proof []types.Hash256, leaf [64]byte, leafIndex uint64, root types.Hash256) bool { + return rhp2.VerifySectorRangeProof(proof, []types.Hash256{blake2b.SumLeaf(&leaf)}, leafIndex, leafIndex+1, LeavesPerSector, root) +} + +// BuildAppendProof builds a Merkle proof for appending a set of sectors to a +// contract. +func BuildAppendProof(sectorRoots, appended []types.Hash256) ([]types.Hash256, types.Hash256) { + var acc blake2b.Accumulator + for _, h := range sectorRoots { + acc.AddLeaf(h) + } + var subtreeRoots []types.Hash256 + for i, h := range acc.Trees { + if acc.NumLeaves&(1< 0 { + acc.Trees[i] = subtreeRoots[0] + subtreeRoots = subtreeRoots[1:] + } + } + if acc.Root() != oldRoot { + return false + } + for _, h := range appended { + acc.AddLeaf(h) + } + return acc.Root() == newRoot +} + +// BuildSectorRootsProof builds a Merkle proof for a range of sectors within a +// contract. +func BuildSectorRootsProof(sectorRoots []types.Hash256, start, end uint64) []types.Hash256 { + return rhp2.BuildSectorRangeProof(sectorRoots, start, end) +} + +// VerifySectorRootsProof verifies a Merkle proof produced by +// BuildSectorRootsProof. +func VerifySectorRootsProof(proof, sectorRoots []types.Hash256, numSectors, start, end uint64, root types.Hash256) bool { + return rhp2.VerifySectorRangeProof(proof, sectorRoots, start, end, numSectors, root) +} + +func convertFreeActions(freed []uint64, numSectors uint64) []rhp2.RPCWriteAction { + as := make([]rhp2.RPCWriteAction, 0, len(freed)+1) + // swap + for i, n := range freed { + as = append(as, rhp2.RPCWriteAction{ + Type: rhp2.RPCWriteActionSwap, + A: n, + B: numSectors - uint64(i) - 1, + }) + } + // trim + return append(as, rhp2.RPCWriteAction{ + Type: rhp2.RPCWriteActionTrim, + A: uint64(len(freed)), + }) +} + +// BuildFreeSectorsProof builds a Merkle proof for freeing a set of sectors. +func BuildFreeSectorsProof(sectorRoots []types.Hash256, freed []uint64) (treeHashes, leafHashes []types.Hash256) { + return rhp2.BuildDiffProof(convertFreeActions(freed, uint64(len(sectorRoots))), sectorRoots) +} + +// VerifyFreeSectorsProof verifies a Merkle proof produced by +// BuildFreeSectorsProof. +func VerifyFreeSectorsProof(treeHashes, leafHashes []types.Hash256, freed []uint64, numSectors uint64, oldRoot types.Hash256, newRoot types.Hash256) bool { + return rhp2.VerifyDiffProof(convertFreeActions(freed, numSectors), numSectors, treeHashes, leafHashes, oldRoot, newRoot, nil) +} diff --git a/rhp/v4/merkle_test.go b/rhp/v4/merkle_test.go new file mode 100644 index 00000000..83457ebb --- /dev/null +++ b/rhp/v4/merkle_test.go @@ -0,0 +1,84 @@ +package rhp + +import ( + "math/bits" + "testing" + + "go.sia.tech/core/types" + "golang.org/x/crypto/blake2b" + "lukechampine.com/frand" +) + +func leafHash(seg []byte) types.Hash256 { + return blake2b.Sum256(append([]byte{0}, seg...)) +} + +func nodeHash(left, right types.Hash256) types.Hash256 { + return blake2b.Sum256(append([]byte{1}, append(left[:], right[:]...)...)) +} + +func refSectorRoot(sector *[SectorSize]byte) types.Hash256 { + roots := make([]types.Hash256, LeavesPerSector) + for i := range roots { + roots[i] = leafHash(sector[i*LeafSize:][:LeafSize]) + } + return recNodeRoot(roots) +} + +func recNodeRoot(roots []types.Hash256) types.Hash256 { + switch len(roots) { + case 0: + return types.Hash256{} + case 1: + return roots[0] + default: + // split at largest power of two + split := 1 << (bits.Len(uint(len(roots)-1)) - 1) + return nodeHash( + recNodeRoot(roots[:split]), + recNodeRoot(roots[split:]), + ) + } +} + +func TestSectorRoot(t *testing.T) { + // test some known roots + var sector [SectorSize]byte + if SectorRoot(§or).String() != "h:50ed59cecd5ed3ca9e65cec0797202091dbba45272dafa3faa4e27064eedd52c" { + t.Error("wrong Merkle root for empty sector") + } + sector[0] = 1 + if SectorRoot(§or).String() != "h:8c20a2c90a733a5139cc57e45755322e304451c3434b0c0a0aad87f2f89a44ab" { + t.Error("wrong Merkle root for sector[0] = 1") + } + sector[0] = 0 + sector[SectorSize-1] = 1 + if SectorRoot(§or).String() != "h:d0ab6691d76750618452e920386e5f6f98fdd1219a70a06f06ef622ac6c6373c" { + t.Error("wrong Merkle root for sector[SectorSize-1] = 1") + } + + // test some random roots against a reference implementation + for i := 0; i < 5; i++ { + frand.Read(sector[:]) + if SectorRoot(§or) != refSectorRoot(§or) { + t.Error("SectorRoot does not match reference implementation") + } + } + + // SectorRoot should not allocate + allocs := testing.AllocsPerRun(5, func() { + _ = SectorRoot(§or) + }) + if allocs > 0 { + t.Error("expected SectorRoot to allocate 0 times, got", allocs) + } +} + +func BenchmarkSectorRoot(b *testing.B) { + b.ReportAllocs() + var sector [SectorSize]byte + b.SetBytes(SectorSize) + for i := 0; i < b.N; i++ { + _ = SectorRoot(§or) + } +} diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go new file mode 100644 index 00000000..478a911b --- /dev/null +++ b/rhp/v4/rhp.go @@ -0,0 +1,770 @@ +package rhp + +import ( + "bytes" + "encoding/hex" + "fmt" + "io" + "time" + + "go.sia.tech/core/consensus" + "go.sia.tech/core/types" +) + +const ( + // ProofWindow is the number of blocks a host has to submit a proof after + // the contract expires. + ProofWindow = 144 // 24 hours + + // SectorSize is the size of one sector in bytes. + SectorSize = 1 << 22 // 4 MiB +) + +// RPC identifiers. +var ( + RPCAccountBalanceID = types.NewSpecifier("AccountBalance") + RPCFormContractID = types.NewSpecifier("FormContract") + RPCFundAccountsID = types.NewSpecifier("FundAccounts") + RPCLatestRevisionID = types.NewSpecifier("LatestRevision") + RPCAppendSectorsID = types.NewSpecifier("AppendSectors") + RPCFreeSectorsID = types.NewSpecifier("FreeSectors") + RPCReadSectorID = types.NewSpecifier("ReadSector") + RPCRenewContractID = types.NewSpecifier("RenewContract") + RPCRefreshContractID = types.NewSpecifier("RefreshContract") + RPCSectorRootsID = types.NewSpecifier("SectorRoots") + RPCSettingsID = types.NewSpecifier("Settings") + RPCWriteSectorID = types.NewSpecifier("WriteSector") + RPCVerifySectorID = types.NewSpecifier("VerifySector") +) + +func round4KiB(n uint64) uint64 { + return (n + (1<<12 - 1)) &^ (1<<12 - 1) +} + +// Usage contains the cost breakdown and collateral of executing an RPC. +type Usage struct { + RPC types.Currency `json:"rpc"` + Storage types.Currency `json:"storage"` + Egress types.Currency `json:"egress"` + Ingress types.Currency `json:"ingress"` + AccountFunding types.Currency `json:"accountFunding"` + RiskedCollateral types.Currency `json:"collateral"` +} + +// RenterCost returns the total cost of executing the RPC. +func (u Usage) RenterCost() types.Currency { + return u.RPC.Add(u.Storage).Add(u.Egress).Add(u.Ingress).Add(u.AccountFunding) +} + +// HostRiskedCollateral returns the amount of collateral the host must risk +func (u Usage) HostRiskedCollateral() types.Currency { + return u.RiskedCollateral +} + +// Add returns the sum of two Usages. +func (u Usage) Add(b Usage) Usage { + return Usage{ + RPC: u.RPC.Add(b.RPC), + Storage: u.Storage.Add(b.Storage), + Egress: u.Egress.Add(b.Egress), + Ingress: u.Ingress.Add(b.Ingress), + AccountFunding: u.AccountFunding.Add(b.AccountFunding), + RiskedCollateral: u.RiskedCollateral.Add(b.RiskedCollateral), + } +} + +// HostPrices specify a time-bound set of parameters used to calculate the cost +// of various RPCs. +type HostPrices struct { + ContractPrice types.Currency `json:"contractPrice"` + Collateral types.Currency `json:"collateral"` + StoragePrice types.Currency `json:"storagePrice"` + IngressPrice types.Currency `json:"ingressPrice"` + EgressPrice types.Currency `json:"egressPrice"` + FreeSectorPrice types.Currency `json:"freeSectorPrice"` + TipHeight uint64 `json:"tipHeight"` + ValidUntil time.Time `json:"validUntil"` + + // covers above fields + Signature types.Signature `json:"signature"` +} + +// RPCReadSectorCost returns the cost of reading a sector of the given length. +func (hp HostPrices) RPCReadSectorCost(length uint64) Usage { + return Usage{ + Egress: hp.EgressPrice.Mul64(round4KiB(length)), + } +} + +// RPCWriteSectorCost returns the cost of executing the WriteSector RPC with the +// given sector length and duration. +func (hp HostPrices) RPCWriteSectorCost(sectorLength uint64, duration uint64) Usage { + return hp.StoreSectorCost(duration).Add(Usage{ + Ingress: hp.IngressPrice.Mul64(round4KiB(sectorLength)), + }) +} + +// StoreSectorCost returns the cost of storing a sector for the given duration. +func (hp HostPrices) StoreSectorCost(duration uint64) Usage { + return Usage{ + Storage: hp.StoragePrice.Mul64(SectorSize).Mul64(duration), + RiskedCollateral: hp.Collateral.Mul64(SectorSize).Mul64(duration), + } +} + +// RPCSectorRootsCost returns the cost of fetching sector roots for the given length. +func (hp HostPrices) RPCSectorRootsCost(length uint64) Usage { + return Usage{ + Egress: hp.EgressPrice.Mul64(round4KiB(32 * length)), + } +} + +// RPCVerifySectorCost returns the cost of building a proof for the specified +// sector. +func (hp HostPrices) RPCVerifySectorCost() Usage { + return Usage{ + Egress: hp.EgressPrice.Mul64(SectorSize), + } +} + +// RPCFreeSectorsCost returns the cost of removing sectors from a contract. +func (hp HostPrices) RPCFreeSectorsCost(sectors int) Usage { + return Usage{ + RPC: hp.FreeSectorPrice.Mul64(uint64(sectors)), + } +} + +// RPCAppendSectorsCost returns the cost of appending sectors to a contract. The duration +// parameter is the number of blocks until the contract's expiration height. +func (hp HostPrices) RPCAppendSectorsCost(sectors, duration uint64) Usage { + usage := hp.StoreSectorCost(duration) + usage.Storage = usage.Storage.Mul64(sectors) + usage.RiskedCollateral = usage.RiskedCollateral.Mul64(sectors) + return usage +} + +// SigHash returns the hash of the host settings used for signing. +func (hp HostPrices) SigHash() types.Hash256 { + h := types.NewHasher() + types.V2Currency(hp.ContractPrice).EncodeTo(h.E) + types.V2Currency(hp.Collateral).EncodeTo(h.E) + types.V2Currency(hp.StoragePrice).EncodeTo(h.E) + types.V2Currency(hp.IngressPrice).EncodeTo(h.E) + types.V2Currency(hp.EgressPrice).EncodeTo(h.E) + types.V2Currency(hp.FreeSectorPrice).EncodeTo(h.E) + h.E.WriteUint64(hp.TipHeight) + h.E.WriteTime(hp.ValidUntil) + return h.Sum() +} + +// Validate checks the host prices for validity. It returns an error if the +// prices have expired or the signature is invalid. +func (hp *HostPrices) Validate(pk types.PublicKey) error { + if time.Until(hp.ValidUntil) <= 0 { + return ErrPricesExpired + } + if !pk.VerifyHash(hp.SigHash(), hp.Signature) { + return ErrInvalidSignature + } + return nil +} + +// HostSettings specify the settings of a host. +type HostSettings struct { + ProtocolVersion [3]uint8 `json:"protocolVersion"` + Release string `json:"release"` + WalletAddress types.Address `json:"walletAddress"` + AcceptingContracts bool `json:"acceptingContracts"` + MaxCollateral types.Currency `json:"maxCollateral"` + MaxContractDuration uint64 `json:"maxContractDuration"` + MaxSectorDuration uint64 `json:"maxSectorDuration"` + MaxSectorBatchSize uint64 `json:"maxSectorBatchSize"` + RemainingStorage uint64 `json:"remainingStorage"` + TotalStorage uint64 `json:"totalStorage"` + Prices HostPrices `json:"prices"` +} + +// An Account represents an ephemeral balance that can be funded via contract +// revision and spent to pay for RPCs. +type Account types.PublicKey + +// String implements fmt.Stringer. +func (a Account) String() string { return fmt.Sprintf("acct:%x", a[:]) } + +// MarshalText implements encoding.TextMarshaler. +func (a Account) MarshalText() []byte { return []byte(a.String()) } + +// UnmarshalText implements encoding.TextUnmarshaler. +func (a *Account) UnmarshalText(b []byte) error { + n, err := hex.Decode(a[:], bytes.TrimPrefix(b, []byte("acct:"))) + if err != nil { + return fmt.Errorf("decoding acct: failed: %w", err) + } else if n < len(a) { + return io.ErrUnexpectedEOF + } + return nil +} + +// An AccountToken authorizes an account action. +type AccountToken struct { + Account Account `json:"account"` + ValidUntil time.Time `json:"validUntil"` + Signature types.Signature `json:"signature"` +} + +// SigHash returns the hash of the account token used for signing. +func (at *AccountToken) SigHash() types.Hash256 { + h := types.NewHasher() + at.Account.EncodeTo(h.E) + h.E.WriteTime(at.ValidUntil) + return h.Sum() +} + +// Validate verifies the account token is valid for use. It returns an error if +// the token has expired or the signature is invalid. +func (at AccountToken) Validate() error { + if time.Now().After(at.ValidUntil) { + return NewRPCError(ErrorCodeBadRequest, "account token expired") + } else if !types.PublicKey(at.Account).VerifyHash(at.SigHash(), at.Signature) { + return ErrInvalidSignature + } + return nil +} + +// GenerateAccount generates a pair of private key and Account from a secure +// entropy source. +func GenerateAccount() (types.PrivateKey, Account) { + sk := types.GeneratePrivateKey() + return sk, Account(sk.PublicKey()) +} + +type ( + // RPCSettingsRequest implements Object. + RPCSettingsRequest struct{} + + // RPCSettingsResponse implements Object. + RPCSettingsResponse struct { + Settings HostSettings `json:"settings"` + } + + // RPCFormContractParams includes the contract details required to construct + // a contract. + RPCFormContractParams struct { + RenterPublicKey types.PublicKey `json:"renterPublicKey"` + RenterAddress types.Address `json:"renterAddress"` + Allowance types.Currency `json:"allowance"` + Collateral types.Currency `json:"collateral"` + ProofHeight uint64 `json:"proofHeight"` + } + + // RPCFormContractRequest implements Object. + RPCFormContractRequest struct { + Prices HostPrices `json:"prices"` + Contract RPCFormContractParams `json:"contract"` + MinerFee types.Currency `json:"minerFee"` + Basis types.ChainIndex `json:"basis"` + RenterInputs []types.SiacoinElement `json:"renterInputs"` + RenterParents []types.V2Transaction `json:"renterParents"` + } + + // RPCFormContractResponse implements Object. + RPCFormContractResponse struct { + HostInputs []types.V2SiacoinInput `json:"hostInputs"` + } + + // RPCFormContractSecondResponse implements Object. + RPCFormContractSecondResponse struct { + RenterContractSignature types.Signature `json:"renterContractSignature"` + RenterSatisfiedPolicies []types.SatisfiedPolicy `json:"renterSatisfiedPolicies"` + } + + // RPCFormContractThirdResponse implements Object. + RPCFormContractThirdResponse struct { + Basis types.ChainIndex `json:"basis"` + TransactionSet []types.V2Transaction `json:"transactionSet"` + } + + // RPCRefreshContractParams includes the contract details required to refresh + // a contract. + RPCRefreshContractParams struct { + ContractID types.FileContractID `json:"contractID"` + Allowance types.Currency `json:"allowance"` + Collateral types.Currency `json:"collateral"` + } + + // RPCRefreshContractRequest implements Object. + RPCRefreshContractRequest struct { + Prices HostPrices `json:"prices"` + Refresh RPCRefreshContractParams `json:"refresh"` + MinerFee types.Currency `json:"minerFee"` + Basis types.ChainIndex `json:"basis"` + RenterInputs []types.SiacoinElement `json:"renterInputs"` + RenterParents []types.V2Transaction `json:"renterParents"` + ChallengeSignature types.Signature `json:"challengeSignature"` + } + // RPCRefreshContractResponse implements Object. + RPCRefreshContractResponse struct { + HostInputs []types.V2SiacoinInput `json:"hostInputs"` + } + // RPCRefreshContractSecondResponse implements Object. + RPCRefreshContractSecondResponse struct { + RenterRenewalSignature types.Signature `json:"renterRenewalSignature"` + RenterSatisfiedPolicies []types.SatisfiedPolicy `json:"renterSatisfiedPolicies"` + } + // RPCRefreshContractThirdResponse implements Object. + RPCRefreshContractThirdResponse struct { + Basis types.ChainIndex `json:"basis"` + TransactionSet []types.V2Transaction `json:"transactionSet"` + } + + // RPCRenewContractParams includes the contract details required to create + // a renewal. + RPCRenewContractParams struct { + ContractID types.FileContractID `json:"contractID"` + Allowance types.Currency `json:"allowance"` + Collateral types.Currency `json:"collateral"` + ProofHeight uint64 `json:"proofHeight"` + } + + // RPCRenewContractRequest implements Object. + RPCRenewContractRequest struct { + Prices HostPrices `json:"prices"` + Renewal RPCRenewContractParams `json:"renewal"` + MinerFee types.Currency `json:"minerFee"` + Basis types.ChainIndex `json:"basis"` + RenterInputs []types.SiacoinElement `json:"renterInputs"` + RenterParents []types.V2Transaction `json:"renterParents"` + ChallengeSignature types.Signature `json:"challengeSignature"` + } + // RPCRenewContractResponse implements Object. + RPCRenewContractResponse struct { + HostInputs []types.V2SiacoinInput `json:"hostInputs"` + } + // RPCRenewContractSecondResponse implements Object. + RPCRenewContractSecondResponse struct { + RenterRenewalSignature types.Signature `json:"renterRenewalSignature"` + RenterSatisfiedPolicies []types.SatisfiedPolicy `json:"renterSatisfiedPolicies"` + } + // RPCRenewContractThirdResponse implements Object. + RPCRenewContractThirdResponse struct { + Basis types.ChainIndex `json:"basis"` + TransactionSet []types.V2Transaction `json:"transactionSet"` + } + + // RPCFreeSectorsRequest implements Object. + RPCFreeSectorsRequest struct { + ContractID types.FileContractID `json:"contractID"` + Prices HostPrices `json:"prices"` + Indices []uint64 `json:"indices"` + // A ChallengeSignature proves the renter can modify the contract. + ChallengeSignature types.Signature `json:"challengeSignature"` + } + // RPCFreeSectorsResponse implements Object. + RPCFreeSectorsResponse struct { + OldSubtreeHashes []types.Hash256 `json:"oldSubtreeHashes"` + OldLeafHashes []types.Hash256 `json:"oldLeafHashes"` + NewMerkleRoot types.Hash256 `json:"newMerkleRoot"` + } + // RPCFreeSectorsSecondResponse implements Object. + RPCFreeSectorsSecondResponse struct { + RenterSignature types.Signature `json:"renterSignature"` + } + // RPCFreeSectorsThirdResponse implements Object. + RPCFreeSectorsThirdResponse struct { + HostSignature types.Signature `json:"hostSignature"` + } + + // RPCLatestRevisionRequest implements Object. + RPCLatestRevisionRequest struct { + ContractID types.FileContractID `json:"contractID"` + } + // RPCLatestRevisionResponse implements Object. + RPCLatestRevisionResponse struct { + Contract types.V2FileContract `json:"contract"` + } + + // RPCReadSectorRequest implements Object. + RPCReadSectorRequest struct { + Prices HostPrices `json:"prices"` + Token AccountToken `json:"token"` + Root types.Hash256 `json:"root"` + Offset uint64 `json:"offset"` + Length uint64 `json:"length"` + } + + // RPCAppendSectorsRequest implements Object. + RPCAppendSectorsRequest struct { + Prices HostPrices `json:"prices"` + Sectors []types.Hash256 `json:"sectors"` + ContractID types.FileContractID `json:"contractID"` + ChallengeSignature types.Signature `json:"challengeSignature"` + } + // RPCAppendSectorsResponse implements Object. + RPCAppendSectorsResponse struct { + Accepted []bool `json:"accepted"` + SubtreeRoots []types.Hash256 `json:"subtreeRoots"` + NewMerkleRoot types.Hash256 `json:"newMerkleRoot"` + } + // RPCAppendSectorsSecondResponse implements Object. + RPCAppendSectorsSecondResponse struct { + RenterSignature types.Signature `json:"renterSignature"` + } + // RPCAppendSectorsThirdResponse implements Object. + RPCAppendSectorsThirdResponse struct { + HostSignature types.Signature `json:"hostSignature"` + } + + // RPCReadSectorResponse implements Object. + RPCReadSectorResponse struct { + Proof []types.Hash256 `json:"proof"` + Sector []byte `json:"sector"` + } + + // RPCReadSectorStreamedResponse implements Object. + RPCReadSectorStreamedResponse struct { + Proof []types.Hash256 `json:"proof"` + DataLength uint64 `json:"dataLength"` + } + + // RPCWriteSectorStreamingRequest implements Object. + RPCWriteSectorStreamingRequest struct { + Prices HostPrices `json:"prices"` + Token AccountToken `json:"token"` + Duration uint64 `json:"duration"` + DataLength uint64 `json:"dataLength"` // extended to SectorSize by host + } + + // RPCWriteSectorRequest implements Object. + RPCWriteSectorRequest struct { + Prices HostPrices `json:"prices"` + Token AccountToken `json:"token"` + Duration uint64 `json:"duration"` + Sector []byte `json:"sector"` // extended to SectorSize by host + } + + // RPCWriteSectorResponse implements Object. + RPCWriteSectorResponse struct { + Root types.Hash256 `json:"root"` + } + + // RPCSectorRootsRequest implements Object. + RPCSectorRootsRequest struct { + Prices HostPrices `json:"prices"` + ContractID types.FileContractID `json:"contractID"` + RenterSignature types.Signature `json:"renterSignature"` + Offset uint64 `json:"offset"` + Length uint64 `json:"length"` + } + // RPCSectorRootsResponse implements Object. + RPCSectorRootsResponse struct { + Proof []types.Hash256 `json:"proof"` + Roots []types.Hash256 `json:"roots"` + HostSignature types.Signature `json:"hostSignature"` + } + + // RPCAccountBalanceRequest implements Object. + RPCAccountBalanceRequest struct { + Account Account `json:"account"` + } + // RPCAccountBalanceResponse implements Object. + RPCAccountBalanceResponse struct { + Balance types.Currency `json:"balance"` + } + + // RPCVerifySectorRequest implements Object. + RPCVerifySectorRequest struct { + Prices HostPrices `json:"prices"` + Token AccountToken `json:"token"` + Root types.Hash256 `json:"root"` + LeafIndex uint64 `json:"leafIndex"` + } + + // RPCVerifySectorResponse implements Object. + RPCVerifySectorResponse struct { + Proof []types.Hash256 `json:"proof"` + Leaf [64]byte `json:"leaf"` + } + + // An AccountDeposit represents a transfer into an account. + AccountDeposit struct { + Account Account `json:"account"` + Amount types.Currency `json:"amount"` + } + + // RPCFundAccountsRequest implements Object. + RPCFundAccountsRequest struct { + ContractID types.FileContractID `json:"contractID"` + Deposits []AccountDeposit `json:"deposits"` + RenterSignature types.Signature `json:"renterSignature"` + } + // RPCFundAccountsResponse implements Object. + RPCFundAccountsResponse struct { + Balances []types.Currency `json:"balances"` + HostSignature types.Signature `json:"hostSignature"` + } +) + +// ChallengeSigHash returns the hash of the challenge signature used for +// signing. +func (r *RPCFreeSectorsRequest) ChallengeSigHash(revisionNumber uint64) types.Hash256 { + h := types.NewHasher() + r.ContractID.EncodeTo(h.E) + h.E.WriteUint64(revisionNumber) + return h.Sum() +} + +// ValidChallengeSignature checks the challenge signature for validity. +func (r *RPCFreeSectorsRequest) ValidChallengeSignature(fc types.V2FileContract) bool { + return fc.RenterPublicKey.VerifyHash(r.ChallengeSigHash(fc.RevisionNumber+1), r.ChallengeSignature) +} + +// ChallengeSigHash returns the hash of the challenge signature used for +// signing. +func (r *RPCAppendSectorsRequest) ChallengeSigHash(revisionNumber uint64) types.Hash256 { + h := types.NewHasher() + r.ContractID.EncodeTo(h.E) + h.E.WriteUint64(revisionNumber) + return h.Sum() +} + +// ValidChallengeSignature checks the challenge signature for validity. +func (r *RPCAppendSectorsRequest) ValidChallengeSignature(fc types.V2FileContract) bool { + return fc.RenterPublicKey.VerifyHash(r.ChallengeSigHash(fc.RevisionNumber+1), r.ChallengeSignature) +} + +// ChallengeSigHash returns the challenge sighash used for proving ownership +// of a contract for the renew RPC. +func (r *RPCRenewContractRequest) ChallengeSigHash(lastRevisionNumber uint64) types.Hash256 { + h := types.NewHasher() + r.Renewal.ContractID.EncodeTo(h.E) + h.E.WriteUint64(lastRevisionNumber) + return h.Sum() +} + +// ValidChallengeSignature checks the challenge signature for validity. +func (r *RPCRenewContractRequest) ValidChallengeSignature(existing types.V2FileContract) bool { + return existing.RenterPublicKey.VerifyHash(r.ChallengeSigHash(existing.RevisionNumber), r.ChallengeSignature) +} + +// ChallengeSigHash returns the challenge sighash used for proving ownership +// of a contract for the renew RPC. +func (r *RPCRefreshContractRequest) ChallengeSigHash(lastRevisionNumber uint64) types.Hash256 { + h := types.NewHasher() + r.Refresh.ContractID.EncodeTo(h.E) + h.E.WriteUint64(lastRevisionNumber) + return h.Sum() +} + +// ValidChallengeSignature checks the challenge signature for validity. +func (r *RPCRefreshContractRequest) ValidChallengeSignature(existing types.V2FileContract) bool { + return existing.RenterPublicKey.VerifyHash(r.ChallengeSigHash(existing.RevisionNumber), r.ChallengeSignature) +} + +// NewContract creates a new file contract with the given settings. +func NewContract(p HostPrices, cp RPCFormContractParams, hostKey types.PublicKey, hostAddress types.Address) (types.V2FileContract, Usage) { + return types.V2FileContract{ + Filesize: 0, + FileMerkleRoot: types.Hash256{}, + ProofHeight: cp.ProofHeight, + ExpirationHeight: cp.ProofHeight + ProofWindow, + RenterOutput: types.SiacoinOutput{ + Value: cp.Allowance, + Address: cp.RenterAddress, + }, + HostOutput: types.SiacoinOutput{ + Value: cp.Collateral.Add(p.ContractPrice), + Address: hostAddress, + }, + MissedHostValue: cp.Collateral, + TotalCollateral: cp.Collateral, + RenterPublicKey: cp.RenterPublicKey, + HostPublicKey: hostKey, + RevisionNumber: 0, + }, Usage{ + RPC: p.ContractPrice, + } +} + +// ContractCost calculates the cost to the host and renter for forming a contract. +func ContractCost(cs consensus.State, p HostPrices, fc types.V2FileContract, minerFee types.Currency) (renter, host types.Currency) { + renter = fc.RenterOutput.Value.Add(p.ContractPrice).Add(minerFee).Add(cs.V2FileContractTax(fc)) + host = fc.TotalCollateral + return +} + +// RenewalCost calculates the cost to the host and renter for renewing a contract. +func RenewalCost(cs consensus.State, p HostPrices, r types.V2FileContractRenewal, minerFee types.Currency) (renter, host types.Currency) { + renter = r.NewContract.RenterOutput.Value.Add(p.ContractPrice).Add(minerFee).Add(cs.V2FileContractTax(r.NewContract)).Sub(r.RenterRollover) + host = r.NewContract.TotalCollateral.Sub(r.HostRollover) + return +} + +// RefreshCost calculates the cost to the host and renter for refreshing a contract. +func RefreshCost(cs consensus.State, p HostPrices, r types.V2FileContractRenewal, minerFee types.Currency) (renter, host types.Currency) { + renter = r.NewContract.RenterOutput.Value.Add(p.ContractPrice).Add(minerFee).Add(cs.V2FileContractTax(r.NewContract)).Sub(r.RenterRollover) + host = r.NewContract.HostOutput.Value.Sub(p.ContractPrice).Sub(r.HostRollover) + return +} + +// PayWithContract modifies a contract to transfer the amount from the renter and +// deduct collateral from the host. It returns an RPC error if the contract does not +// have sufficient funds. +func PayWithContract(fc *types.V2FileContract, usage Usage) error { + amount, collateral := usage.RenterCost(), usage.HostRiskedCollateral() + // verify the contract can pay the amount before modifying + if fc.RenterOutput.Value.Cmp(amount) < 0 { + return NewRPCError(ErrorCodePayment, fmt.Sprintf("insufficient renter funds: %v < %v", fc.RenterOutput.Value, amount)) + } else if fc.MissedHostValue.Cmp(collateral) < 0 { + return NewRPCError(ErrorCodePayment, fmt.Sprintf("insufficient host collateral: %v < %v", fc.MissedHostValue, amount)) + } + fc.RevisionNumber++ + fc.RenterOutput.Value = fc.RenterOutput.Value.Sub(amount) + fc.HostOutput.Value = fc.HostOutput.Value.Add(amount) + fc.MissedHostValue = fc.MissedHostValue.Sub(collateral) + // clear signatures + fc.RenterSignature = types.Signature{} + fc.HostSignature = types.Signature{} + return nil +} + +// ReviseForFreeSectors creates a contract revision for the free sectors RPC +func ReviseForFreeSectors(fc types.V2FileContract, prices HostPrices, newRoot types.Hash256, deletions int) (types.V2FileContract, Usage, error) { + fc.Filesize -= SectorSize * uint64(deletions) + usage := prices.RPCFreeSectorsCost(deletions) + if err := PayWithContract(&fc, usage); err != nil { + return fc, Usage{}, err + } + fc.FileMerkleRoot = newRoot + return fc, usage, nil +} + +// ReviseForAppendSectors creates a contract revision for the append sectors RPC +func ReviseForAppendSectors(fc types.V2FileContract, prices HostPrices, root types.Hash256, appended uint64) (types.V2FileContract, Usage, error) { + growth := appended - min(appended, (fc.Capacity-fc.Filesize)/SectorSize) + fc.Filesize += SectorSize * appended + fc.Capacity += SectorSize * growth + fc.FileMerkleRoot = root + usage := prices.RPCAppendSectorsCost(growth, fc.ExpirationHeight-prices.TipHeight) + if err := PayWithContract(&fc, usage); err != nil { + return fc, Usage{}, err + } + return fc, usage, nil +} + +// ReviseForSectorRoots creates a contract revision for the sector roots RPC +func ReviseForSectorRoots(fc types.V2FileContract, prices HostPrices, numRoots uint64) (types.V2FileContract, Usage, error) { + usage := prices.RPCSectorRootsCost(numRoots) + err := PayWithContract(&fc, usage) + return fc, usage, err +} + +// ReviseForFundAccounts creates a contract revision for the fund accounts RPC +func ReviseForFundAccounts(fc types.V2FileContract, amount types.Currency) (types.V2FileContract, Usage, error) { + usage := Usage{AccountFunding: amount} + err := PayWithContract(&fc, usage) + return fc, usage, err +} + +// MinRenterAllowance returns the minimum allowance required to justify the given +// host collateral. +func MinRenterAllowance(hp HostPrices, duration uint64, collateral types.Currency) types.Currency { + maxCollateralBytes := collateral.Div(hp.Collateral).Div64(duration) + return hp.StoragePrice.Mul64(duration).Mul(maxCollateralBytes) +} + +// RenewContract creates a contract renewal for the renew RPC +func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContractParams) (types.V2FileContractRenewal, Usage) { + var renewal types.V2FileContractRenewal + // clear the old contract + renewal.FinalRevision = fc + renewal.FinalRevision.RevisionNumber = types.MaxRevisionNumber + renewal.FinalRevision.Filesize = 0 + renewal.FinalRevision.FileMerkleRoot = types.Hash256{} + renewal.FinalRevision.RenterSignature = types.Signature{} + renewal.FinalRevision.HostSignature = types.Signature{} + + // create the new contract + renewal.NewContract = fc + renewal.NewContract.RevisionNumber = 0 + renewal.NewContract.RenterSignature = types.Signature{} + renewal.NewContract.HostSignature = types.Signature{} + renewal.NewContract.ExpirationHeight = rp.ProofHeight + ProofWindow + renewal.NewContract.ProofHeight = rp.ProofHeight + // the renter output value only needs to cover the new allowance + renewal.NewContract.RenterOutput.Value = rp.Allowance + + // risked collateral must be calculated using the full duration to ensure the + // host is incentivized to store the data. Existing locked collateral will be + // rolled into the new contract, so cost to the host is not excessive. + riskedCollateral := prices.Collateral.Mul64(fc.Filesize).Mul64(renewal.NewContract.ExpirationHeight - prices.TipHeight) + + // total collateral includes the additional requested collateral and + // the risked collateral from the existing storage. + renewal.NewContract.TotalCollateral = rp.Collateral.Add(riskedCollateral) + // missed host value should only include the new collateral value + renewal.NewContract.MissedHostValue = rp.Collateral + + // storage cost is the difference between the new and old contract since the + // old contract already paid for the storage up to the current expiration + // height. + storageCost := prices.StoragePrice.Mul64(fc.Filesize).Mul64(renewal.NewContract.ExpirationHeight - fc.ExpirationHeight) + + // host output value includes the locked + risked collateral, the additional + // storage cost, and the contract price. + renewal.NewContract.HostOutput.Value = renewal.NewContract.TotalCollateral.Add(storageCost).Add(prices.ContractPrice) + + // if the existing locked collateral is greater than the new required + // collateral, the host will only lock the new required collateral. Otherwise, + // roll over the existing locked collateral. The host will need to fund + // the difference. + if fc.TotalCollateral.Cmp(renewal.NewContract.TotalCollateral) > 0 { + renewal.HostRollover = renewal.NewContract.TotalCollateral + } else { + renewal.HostRollover = fc.TotalCollateral + } + + // if the remaining renter output is greater than the required allowance, + // only roll over the new allowance. Otherwise, roll over the remaining + // allowance. The renter will need to fund the difference. + if fc.RenterOutput.Value.Cmp(rp.Allowance) > 0 { + renewal.RenterRollover = rp.Allowance + } else { + renewal.RenterRollover = fc.RenterOutput.Value + } + return renewal, Usage{ + RPC: prices.ContractPrice, + Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), + RiskedCollateral: renewal.NewContract.TotalCollateral.Sub(renewal.NewContract.MissedHostValue), + } +} + +// RefreshContract creates a contract renewal for the refresh RPC. +func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshContractParams) (types.V2FileContractRenewal, Usage) { + var renewal types.V2FileContractRenewal + + // clear the old contract + renewal.FinalRevision = fc + renewal.FinalRevision.RevisionNumber = types.MaxRevisionNumber + renewal.FinalRevision.RenterSignature = types.Signature{} + renewal.FinalRevision.HostSignature = types.Signature{} + + // create the new contract + renewal.NewContract = fc + renewal.NewContract.RevisionNumber = 0 + renewal.NewContract.RenterSignature = types.Signature{} + renewal.NewContract.HostSignature = types.Signature{} + // add the additional allowance and collateral + renewal.NewContract.RenterOutput.Value = fc.RenterOutput.Value.Add(rp.Allowance) + renewal.NewContract.HostOutput.Value = fc.HostOutput.Value.Add(rp.Collateral).Add(prices.ContractPrice) + renewal.NewContract.MissedHostValue = fc.MissedHostValue.Add(rp.Collateral) + // total collateral includes the additional requested collateral + renewal.NewContract.TotalCollateral = fc.TotalCollateral.Add(rp.Collateral) + // roll over everything from the existing contract + renewal.HostRollover = fc.HostOutput.Value + renewal.RenterRollover = fc.RenterOutput.Value + return renewal, Usage{ + RPC: prices.ContractPrice, + Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), + RiskedCollateral: renewal.NewContract.TotalCollateral.Sub(renewal.NewContract.MissedHostValue), + } +} diff --git a/rhp/v4/rhp_test.go b/rhp/v4/rhp_test.go new file mode 100644 index 00000000..ae4f0f00 --- /dev/null +++ b/rhp/v4/rhp_test.go @@ -0,0 +1,21 @@ +package rhp + +import ( + "testing" + + "go.sia.tech/core/types" +) + +func TestMinRenterAllowance(t *testing.T) { + hp := HostPrices{ + StoragePrice: types.NewCurrency64(1), // 1 H per byte per block + Collateral: types.NewCurrency64(2), // 2 H per byte per block + } + + collateral := types.Siacoins(2) + minAllowance := MinRenterAllowance(hp, 1, collateral) + expected := types.Siacoins(1) + if !minAllowance.Equals(expected) { + t.Fatalf("expected %v, got %v", expected, minAllowance) + } +} diff --git a/rhp/v4/transport.go b/rhp/v4/transport.go new file mode 100644 index 00000000..5ca2d756 --- /dev/null +++ b/rhp/v4/transport.go @@ -0,0 +1,66 @@ +package rhp + +import ( + "io" + + "go.sia.tech/core/types" +) + +func withEncoder(w io.Writer, fn func(*types.Encoder)) error { + e := types.NewEncoder(w) + fn(e) + return e.Flush() +} + +func withDecoder(r io.Reader, maxLen int, fn func(*types.Decoder)) error { + d := types.NewDecoder(io.LimitedReader{R: r, N: int64(maxLen)}) + fn(d) + return d.Err() +} + +// ReadID reads an RPC ID from the stream. +func ReadID(r io.Reader) (id types.Specifier, err error) { + err = withDecoder(r, 16, id.DecodeFrom) + return +} + +// WriteRequest writes a request to the stream. +func WriteRequest(w io.Writer, id types.Specifier, o Object) error { + return withEncoder(w, func(e *types.Encoder) { + id.EncodeTo(e) + if o == nil { + return + } + o.encodeTo(e) + }) +} + +// ReadRequest reads a request from the stream. +func ReadRequest(r io.Reader, o Object) error { + return withDecoder(r, o.maxLen(), func(d *types.Decoder) { + o.decodeFrom(d) + }) +} + +// WriteResponse writes a response to the stream. Note that RPCError implements +// Object, and may be used as a response to any RPC. +func WriteResponse(w io.Writer, o Object) error { + return withEncoder(w, func(e *types.Encoder) { + _, isErr := o.(*RPCError) + e.WriteBool(isErr) + o.encodeTo(e) + }) +} + +// ReadResponse reads a response from the stream into r. +func ReadResponse(r io.Reader, o Object) error { + return withDecoder(r, (*RPCError)(nil).maxLen()+o.maxLen(), func(d *types.Decoder) { + if d.ReadBool() { + r := new(RPCError) + r.decodeFrom(d) + d.SetErr(r) + return + } + o.decodeFrom(d) + }) +} diff --git a/rhp/v4/validation.go b/rhp/v4/validation.go new file mode 100644 index 00000000..a36d7815 --- /dev/null +++ b/rhp/v4/validation.go @@ -0,0 +1,222 @@ +package rhp + +import ( + "errors" + "fmt" + + "go.sia.tech/core/types" +) + +// Validate validates a read sector request. +func (req *RPCReadSectorRequest) Validate(pk types.PublicKey) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } else if err := req.Token.Validate(); err != nil { + return fmt.Errorf("token is invalid: %w", err) + } + switch { + case req.Length == 0: + return errors.New("length must be greater than 0") + case req.Offset+req.Length > SectorSize: + return errors.New("read request exceeds sector bounds") + case (req.Offset+req.Length)%LeafSize != 0: + return errors.New("read request must be segment aligned") + } + return nil +} + +// Validate validates a write sector request. +func (req *RPCWriteSectorStreamingRequest) Validate(pk types.PublicKey, maxDuration uint64) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } else if err := req.Token.Validate(); err != nil { + return fmt.Errorf("token is invalid: %w", err) + } + switch { + case req.Duration == 0: + return errors.New("duration must be greater than 0") + case req.Duration > maxDuration: + return fmt.Errorf("duration exceeds maximum: %d > %d", req.Duration, maxDuration) + case req.DataLength == 0: + return errors.New("sector must not be empty") + case req.DataLength%LeafSize != 0: + return errors.New("sector must be segment aligned") + case req.DataLength > SectorSize: + return errors.New("sector exceeds sector bounds") + } + return nil +} + +// Validate validates a modify sectors request. Signatures are not validated. +func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxActions uint64) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } else if uint64(len(req.Indices)) > maxActions { + return fmt.Errorf("removing too many sectors at once: %d > %d", len(req.Indices), maxActions) + } + seen := make(map[uint64]bool) + sectors := fc.Filesize / SectorSize + for _, index := range req.Indices { + if index >= sectors { + return fmt.Errorf("sector index %d exceeds contract sectors %d", index, sectors) + } else if seen[index] { + return fmt.Errorf("duplicate sector index %d", index) + } + seen[index] = true + } + return nil +} + +// Validate validates a sector roots request. Signatures are not validated. +func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxSectors uint64) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } + + contractSectors := fc.Filesize / SectorSize + switch { + case req.Length == 0: + return errors.New("length must be greater than 0") + case req.Length+req.Offset > contractSectors: + return fmt.Errorf("read request range exceeds contract sectors: %d > %d", req.Length+req.Offset, contractSectors) + case req.Length > maxSectors: + return fmt.Errorf("read request range exceeds maximum sectors: %d > %d", req.Length, maxSectors) + } + return nil +} + +// Validate validates a form contract request. Prices are not validated +func (req *RPCFormContractRequest) Validate(pk types.PublicKey, tip types.ChainIndex, maxCollateral types.Currency, maxDuration uint64) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } + + // validate the request fields + switch { + case req.MinerFee.IsZero(): + return errors.New("miner fee must be greater than 0") + case req.Basis == (types.ChainIndex{}): + return errors.New("basis must be set") + case len(req.RenterInputs) == 0: + return errors.New("renter inputs must not be empty") + } + + // validate the contract fields + hp := req.Prices + expirationHeight := req.Contract.ProofHeight + ProofWindow + duration := expirationHeight - hp.TipHeight + // calculate the minimum allowance required for the contract based on the + // host's locked collateral and the contract duration + minRenterAllowance := MinRenterAllowance(hp, duration, req.Contract.Collateral) + + switch { + case expirationHeight <= tip.Height: // must be validated against tip instead of prices + return errors.New("contract expiration height is in the past") + case req.Contract.Allowance.IsZero(): + return errors.New("allowance must be greater than zero") + case req.Contract.Collateral.Cmp(maxCollateral) > 0: + return fmt.Errorf("collateral %v exceeds max collateral %v", req.Contract.Collateral, maxCollateral) + case duration > maxDuration: + return fmt.Errorf("contract duration %v exceeds max duration %v", duration, maxDuration) + case req.Contract.Allowance.Cmp(minRenterAllowance) < 0: + return fmt.Errorf("allowance %v is less than minimum allowance %v", req.Contract.Allowance, minRenterAllowance) + default: + return nil + } +} + +// Validate validates a renew contract request. Prices are not validated +func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.ChainIndex, existingProofHeight uint64, maxCollateral types.Currency, maxDuration uint64) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } + + // validate the request fields + switch { + case req.MinerFee.IsZero(): + return errors.New("miner fee must be greater than 0") + case req.Basis == (types.ChainIndex{}): + return errors.New("basis must be set") + case req.Renewal.ProofHeight <= existingProofHeight: + return fmt.Errorf("renewal proof height must be greater than existing proof height %v", existingProofHeight) + } + + // validate the contract fields + hp := req.Prices + expirationHeight := req.Renewal.ProofHeight + ProofWindow + duration := expirationHeight - hp.TipHeight + // calculate the minimum allowance required for the contract based on the + // host's locked collateral and the contract duration + minRenterAllowance := MinRenterAllowance(hp, duration, req.Renewal.Collateral) + + switch { + case expirationHeight <= tip.Height: // must be validated against tip instead of prices + return errors.New("contract expiration height is in the past") + case req.Renewal.Allowance.IsZero(): + return errors.New("allowance must be greater than zero") + case req.Renewal.Collateral.Cmp(maxCollateral) > 0: + return fmt.Errorf("collateral %v exceeds max collateral %v", req.Renewal.Collateral, maxCollateral) + case duration > maxDuration: + return fmt.Errorf("contract duration %v exceeds max duration %v", duration, maxDuration) + case req.Renewal.Allowance.Cmp(minRenterAllowance) < 0: + return fmt.Errorf("allowance %v is less than minimum allowance %v", req.Renewal.Allowance, minRenterAllowance) + default: + return nil + } +} + +// Validate validates a refresh contract request. Prices are not validated +func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, expirationHeight uint64, maxCollateral types.Currency) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } + + // validate the request fields + switch { + case req.MinerFee.IsZero(): + return errors.New("miner fee must be greater than 0") + case req.Basis == (types.ChainIndex{}): + return errors.New("basis must be set") + } + + // validate the contract fields + hp := req.Prices + // calculate the minimum allowance required for the contract based on the + // host's locked collateral and the contract duration + minRenterAllowance := MinRenterAllowance(hp, expirationHeight-req.Prices.TipHeight, req.Refresh.Collateral) + + switch { + case req.Refresh.Allowance.IsZero(): + return errors.New("allowance must be greater than zero") + case req.Refresh.Collateral.Cmp(maxCollateral) > 0: + return fmt.Errorf("collateral %v exceeds max collateral %v", req.Refresh.Collateral, maxCollateral) + case req.Refresh.Allowance.Cmp(minRenterAllowance) < 0: + return fmt.Errorf("allowance %v is less than minimum allowance %v", req.Refresh.Allowance, minRenterAllowance) + default: + return nil + } +} + +// Validate checks that the request is valid +func (req *RPCVerifySectorRequest) Validate(pk types.PublicKey) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } else if err := req.Token.Validate(); err != nil { + return fmt.Errorf("token is invalid: %w", err) + } else if req.LeafIndex >= LeavesPerSector { + return fmt.Errorf("leaf index must be less than %d", LeavesPerSector) + } + return nil +} + +// Validate checks that the request is valid +func (req *RPCAppendSectorsRequest) Validate(pk types.PublicKey, maxActions uint64) error { + if err := req.Prices.Validate(pk); err != nil { + return fmt.Errorf("prices are invalid: %w", err) + } else if len(req.Sectors) == 0 { + return errors.New("no sectors to append") + } else if uint64(len(req.Sectors)) > maxActions { + return fmt.Errorf("too many sectors to append: %d > %d", len(req.Sectors), maxActions) + } + return nil +}