diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 00000000..0a719bd3 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,26 @@ +name: Prepare Release +on: + push: + branches: [master] + +permissions: + contents: write + pull-requests: write + +jobs: + prepare-release: + if: "!contains(github.event.head_commit.message, 'chore: prepare release')" # Skip merges from releases + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + - name: Configure Git + run: | + git config --global user.name github-actions[bot] + git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 + - run: knope prepare-release --verbose + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..444c174a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,27 @@ +name: Release +on: + pull_request: + types: + - closed + branches: + - master + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4.2.2 + with: + fetch-depth: 0 + - name: Configure Git + run: | + git config --global user.name github-actions[bot] + git config --global user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: knope-dev/action@407e9ef7c272d2dd53a4e71e39a7839e29933c48 + - run: knope release --verbose + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true diff --git a/.gitignore b/.gitignore index c6f9a448..cd5fec9a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.vscode/settings.json + +.DS_Store +.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f8a33b06 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,45 @@ +## 0.9.0 (2024-12-18) + +### Breaking Changes + +- Add host public key to AccountToken + +### Features + +- Add helper for generating account tokens + +### Fixes + +- Allow v1 contracts to be resolved immediately + +## 0.8.0 (2024-12-13) + +### Breaking Changes + +#### Add revisable to RPCLatestRevision + +Adds two additional flags to the RPCLatestRevision response. The `Revisable` field indicates whether the host will accept further revisions to the contract. A host will not accept revisions too close to the proof window or revisions on contracts that have already been resolved. The `Renewed` field indicates whether the contract was renewed. If the contract was renewed, the renter can use `FileContractID.V2RenewalID` to get the ID of the new contract. + +## 0.7.3 (2024-12-12) + +### Features + +- Update `golang.org/x/crypto` from 0.30.0 to 0.31.0 + +## 0.7.2 (2024-12-12) + +### Features + +#### Allow revisions to set MaxRevisionNumber + +`MaxRevisionNumber` was previously used to finalize contracts, but that is not the case anymore, so the restriction can be removed. + +### Fixes + +- Include storage cost in renter renewal cost + +## 0.7.1 (2024-12-04) + +### Fixes + +- Automate releases diff --git a/consensus/update.go b/consensus/application.go similarity index 87% rename from consensus/update.go rename to consensus/application.go index a383a817..fe273a39 100644 --- a/consensus/update.go +++ b/consensus/application.go @@ -367,7 +367,7 @@ func (ms *MidState) addSiafundElement(id types.SiafundOutputID, sfo types.Siafun StateElement: types.StateElement{LeafIndex: types.UnassignedLeafIndex}, ID: id, SiafundOutput: sfo, - ClaimStart: ms.siafundPool, + ClaimStart: ms.siafundTaxRevenue, } ms.sfes = append(ms.sfes, sfe) ms.created[ms.sfes[len(ms.sfes)-1].ID] = len(ms.sfes) - 1 @@ -389,7 +389,7 @@ func (ms *MidState) addFileContractElement(id types.FileContractID, fc types.Fil } ms.fces = append(ms.fces, fce) ms.created[ms.fces[len(ms.fces)-1].ID] = len(ms.fces) - 1 - ms.siafundPool = ms.siafundPool.Add(ms.base.FileContractTax(fce.FileContract)) + ms.siafundTaxRevenue = ms.siafundTaxRevenue.Add(ms.base.FileContractTax(fce.FileContract)) } func (ms *MidState) reviseFileContractElement(fce types.FileContractElement, rev types.FileContract) { @@ -426,7 +426,7 @@ func (ms *MidState) addV2FileContractElement(id types.FileContractID, fc types.V } ms.v2fces = append(ms.v2fces, fce) ms.created[ms.v2fces[len(ms.v2fces)-1].ID] = len(ms.v2fces) - 1 - ms.siafundPool = ms.siafundPool.Add(ms.base.V2FileContractTax(fce.V2FileContract)) + ms.siafundTaxRevenue = ms.siafundTaxRevenue.Add(ms.base.V2FileContractTax(fce.V2FileContract)) } func (ms *MidState) reviseV2FileContractElement(fce types.V2FileContractElement, rev types.V2FileContract) { @@ -477,7 +477,7 @@ func (ms *MidState) ApplyTransaction(txn types.Transaction, ts V1TransactionSupp if !ok { panic("missing SiafundElement") } - claimPortion := ms.siafundPool.Sub(sfe.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfe.SiafundOutput.Value) + claimPortion := ms.siafundTaxRevenue.Sub(sfe.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfe.SiafundOutput.Value) ms.spendSiafundElement(sfe, txid) ms.addImmatureSiacoinElement(sfi.ParentID.ClaimOutputID(), types.SiacoinOutput{Value: claimPortion, Address: sfi.ClaimAddress}) } @@ -495,12 +495,12 @@ func (ms *MidState) ApplyTransaction(txn types.Transaction, ts V1TransactionSupp ms.reviseFileContractElement(fce, fcr.FileContract) } for _, sp := range txn.StorageProofs { - sps, ok := ts.storageProof(sp.ParentID) + fce, ok := ms.fileContractElement(ts, sp.ParentID) if !ok { panic("missing V1StorageProofSupplement") } - ms.resolveFileContractElement(sps.FileContract, true, txid) - for i, sco := range sps.FileContract.FileContract.ValidProofOutputs { + ms.resolveFileContractElement(fce, true, txid) + for i, sco := range fce.FileContract.ValidProofOutputs { ms.addImmatureSiacoinElement(sp.ParentID.ValidOutputID(i), sco) } } @@ -509,8 +509,8 @@ func (ms *MidState) ApplyTransaction(txn types.Transaction, ts V1TransactionSupp if bytes.HasPrefix(arb, types.SpecifierFoundation[:]) { var update types.FoundationAddressUpdate update.DecodeFrom(types.NewBufDecoder(arb[len(types.SpecifierFoundation):])) - ms.foundationPrimary = update.NewPrimary - ms.foundationFailsafe = update.NewFailsafe + ms.foundationSubsidy = update.NewPrimary + ms.foundationManagement = update.NewFailsafe } } } @@ -528,7 +528,7 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { } for _, sfi := range txn.SiafundInputs { ms.spendSiafundElement(sfi.Parent, txid) - claimPortion := ms.siafundPool.Sub(sfi.Parent.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfi.Parent.SiafundOutput.Value) + claimPortion := ms.siafundTaxRevenue.Sub(sfi.Parent.ClaimStart).Div64(ms.base.SiafundCount()).Mul64(sfi.Parent.SiafundOutput.Value) ms.addImmatureSiacoinElement(sfi.Parent.ID.V2ClaimOutputID(), types.SiacoinOutput{Value: claimPortion, Address: sfi.ClaimAddress}) } for i, sfo := range txn.SiafundOutputs { @@ -548,9 +548,7 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { var renter, host types.SiacoinOutput switch r := fcr.Resolution.(type) { case *types.V2FileContractRenewal: - renter, host = r.FinalRevision.RenterOutput, r.FinalRevision.HostOutput - renter.Value = renter.Value.Sub(r.RenterRollover) - host.Value = host.Value.Sub(r.HostRollover) + renter, host = r.FinalRenterOutput, r.FinalHostOutput ms.addV2FileContractElement(fce.ID.V2RenewalID(), r.NewContract) case *types.V2StorageProof: renter, host = fc.RenterOutput, fc.HostOutput @@ -568,8 +566,13 @@ func (ms *MidState) ApplyV2Transaction(txn types.V2Transaction) { }) } if txn.NewFoundationAddress != nil { - ms.foundationPrimary = *txn.NewFoundationAddress - ms.foundationFailsafe = *txn.NewFoundationAddress + // The subsidy may be waived by sending it to the void address. In this + // case, the management address is not updated (as this would + // permanently disable the subsidy). + ms.foundationSubsidy = *txn.NewFoundationAddress + if *txn.NewFoundationAddress != types.VoidAddress { + ms.foundationManagement = *txn.NewFoundationAddress + } } } @@ -665,14 +668,20 @@ func (au ApplyUpdate) UpdateElementProof(e *types.StateElement) { au.eau.updateElementProof(e) } -// ForEachSiacoinElement calls fn on each siacoin element related to au. +// ForEachSiacoinElement calls fn on each siacoin element related to the applied +// block. The created and spent flags indicate whether the element was newly +// created in the block and/or spent in the block. Note that an element may be +// both created and spent in the the same block. func (au ApplyUpdate) ForEachSiacoinElement(fn func(sce types.SiacoinElement, created, spent bool)) { for _, sce := range au.ms.sces { fn(sce, au.ms.isCreated(sce.ID), au.ms.isSpent(sce.ID)) } } -// ForEachSiafundElement calls fn on each siafund element related to au. +// ForEachSiafundElement calls fn on each siafund element related to the applied +// block. The created and spent flags indicate whether the element was newly +// created in the block and/or spent in the block. Note that an element may be +// both created and spent in the the same block. func (au ApplyUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, created, spent bool)) { for _, sfe := range au.ms.sfes { fn(sfe, au.ms.isCreated(sfe.ID), au.ms.isSpent(sfe.ID)) @@ -680,16 +689,28 @@ func (au ApplyUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, cr } // ForEachFileContractElement calls fn on each file contract element related to -// au. If the contract was revised, rev is non-nil. +// the applied block. The created flag indicates whether the contract was newly +// created. If the contract was revised, rev is non-nil and represents the state +// of the element post-application. If the block revised the contract multiple +// times, rev is the revision with the highest revision number. The resolved and +// valid flags indicate whether the contract was resolved, and if so, whether it +// was resolved via storage proof. Note that a contract may be created, revised, +// and resolved all within the same block. func (au ApplyUpdate) ForEachFileContractElement(fn func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool)) { for _, fce := range au.ms.fces { fn(fce, au.ms.isCreated(fce.ID), au.ms.revs[fce.ID], au.ms.isSpent(fce.ID), au.ms.res[fce.ID]) } } -// ForEachV2FileContractElement calls fn on each V2 file contract element -// related to au. If the contract was revised, rev is non-nil. If the contract -// was resolved, res is non-nil. +// ForEachV2FileContractElement calls fn on each v2 file contract element +// related to the applied block. The created flag indicates whether the contract +// was newly created. If the contract was revised, rev is non-nil and represents +// the state of the element post-application. If the block revised the contract +// multiple times, rev is the revision with the highest revision number. The +// resolved and valid flags indicate whether the contract was resolved, and if +// so, whether it was resolved via storage proof. Note that, within a block, a +// contract may be created and revised, or revised and resolved, but not created +// and resolved. func (au ApplyUpdate) ForEachV2FileContractElement(fn func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType)) { for _, fce := range au.ms.v2fces { fn(fce, au.ms.isCreated(fce.ID), au.ms.v2revs[fce.ID], au.ms.v2res[fce.ID]) @@ -736,10 +757,10 @@ func ApplyBlock(s State, b types.Block, bs V1BlockSupplement, targetTimestamp ti ms := NewMidState(s) ms.ApplyBlock(b, bs) - s.SiafundPool = ms.siafundPool + s.SiafundTaxRevenue = ms.siafundTaxRevenue s.Attestations += uint64(len(ms.aes)) - s.FoundationPrimaryAddress = ms.foundationPrimary - s.FoundationFailsafeAddress = ms.foundationFailsafe + s.FoundationSubsidyAddress = ms.foundationSubsidy + s.FoundationManagementAddress = ms.foundationManagement // compute updated and added elements var updated, added []elementLeaf @@ -755,7 +776,11 @@ func ApplyBlock(s State, b types.Block, bs V1BlockSupplement, targetTimestamp ti return s, ApplyUpdate{ms, eau} } -// A RevertUpdate represents the effects of reverting to a prior state. +// A RevertUpdate represents the effects of reverting to a prior state. These +// are the same effects seen as when applying the block, but should be processed +// inversely. For example, if ForEachSiacoinElement reports an element with the +// created flag set, it means the block created that element when it was +// applied; thus, when the block is reverted, the element no longer exists. type RevertUpdate struct { ms *MidState eru elementRevertUpdate @@ -768,7 +793,10 @@ func (ru RevertUpdate) UpdateElementProof(e *types.StateElement) { ru.eru.updateElementProof(e) } -// ForEachSiacoinElement calls fn on each siacoin element related to ru. +// ForEachSiacoinElement calls fn on each siacoin element related to the reverted +// block. The created and spent flags indicate whether the element was newly +// created in the block and/or spent in the block. Note that an element may be +// both created and spent in the the same block. func (ru RevertUpdate) ForEachSiacoinElement(fn func(sce types.SiacoinElement, created, spent bool)) { for i := range ru.ms.sces { sce := ru.ms.sces[len(ru.ms.sces)-i-1] @@ -776,7 +804,10 @@ func (ru RevertUpdate) ForEachSiacoinElement(fn func(sce types.SiacoinElement, c } } -// ForEachSiafundElement calls fn on each siafund element related to ru. +// ForEachSiafundElement calls fn on each siafund element related to the +// reverted block. The created and spent flags indicate whether the element was +// newly created in the block and/or spent in the block. Note that an element +// may be both created and spent in the the same block. func (ru RevertUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, created, spent bool)) { for i := range ru.ms.sfes { sfe := ru.ms.sfes[len(ru.ms.sfes)-i-1] @@ -785,7 +816,13 @@ func (ru RevertUpdate) ForEachSiafundElement(fn func(sfe types.SiafundElement, c } // ForEachFileContractElement calls fn on each file contract element related to -// ru. If the contract was revised, rev is non-nil. +// the reverted block. The created flag indicates whether the contract was newly +// created. If the contract was revised, rev is non-nil and represents the state +// of the element post-application. If the block revised the contract multiple +// times, rev is the revision with the highest revision number. The resolved and +// valid flags indicate whether the contract was resolved, and if so, whether it +// was resolved via storage proof. Note that a contract may be created, revised, +// and resolved all within the same block. func (ru RevertUpdate) ForEachFileContractElement(fn func(fce types.FileContractElement, created bool, rev *types.FileContractElement, resolved, valid bool)) { for i := range ru.ms.fces { fce := ru.ms.fces[len(ru.ms.fces)-i-1] @@ -793,9 +830,15 @@ func (ru RevertUpdate) ForEachFileContractElement(fn func(fce types.FileContract } } -// ForEachV2FileContractElement calls fn on each V2 file contract element -// related to au. If the contract was revised, rev is non-nil. If the contract -// was resolved, res is non-nil. +// ForEachV2FileContractElement calls fn on each v2 file contract element +// related to the reverted block. The created flag indicates whether the +// contract was newly created. If the contract was revised, rev is non-nil and +// represents the state of the element post-application. If the block revised +// the contract multiple times, rev is the revision with the highest revision +// number. The resolved and valid flags indicate whether the contract was +// resolved, and if so, whether it was resolved via storage proof. Note that, +// within a block, a contract may be created and revised, or revised and +// resolved, but not created and resolved. func (ru RevertUpdate) ForEachV2FileContractElement(fn func(fce types.V2FileContractElement, created bool, rev *types.V2FileContractElement, res types.V2FileContractResolutionType)) { for i := range ru.ms.v2fces { fce := ru.ms.v2fces[len(ru.ms.v2fces)-i-1] diff --git a/consensus/update_test.go b/consensus/application_test.go similarity index 83% rename from consensus/update_test.go rename to consensus/application_test.go index 6cba2adb..d0fb88d1 100644 --- a/consensus/update_test.go +++ b/consensus/application_test.go @@ -1089,11 +1089,6 @@ func TestApplyRevertBlockV2(t *testing.T) { checkUpdateElements(au, addedSCEs, spentSCEs, addedSFEs, spentSFEs) } - _ = renterPublicKey - _ = hostPublicKey - _ = checkRevertElements - _ = prev - // revert block spending sc and sf ru := RevertBlock(prev, b2, V1BlockSupplement{}) cs = prev @@ -1296,3 +1291,252 @@ func TestApplyRevertBlockV2(t *testing.T) { checkUpdateElements(au, addedSCEs, spentSCEs, addedSFEs, spentSFEs) } } + +func TestSiafunds(t *testing.T) { + n, genesisBlock := testnet() + n.HardforkV2.AllowHeight = 1 + n.HardforkV2.RequireHeight = 2 + + key := types.GeneratePrivateKey() + + giftAddress := types.StandardAddress(key.PublicKey()) + giftAmountSC := types.Siacoins(100e3) + giftAmountSF := uint64(1000) + giftTxn := types.Transaction{ + SiacoinOutputs: []types.SiacoinOutput{ + {Address: giftAddress, Value: giftAmountSC}, + }, + SiafundOutputs: []types.SiafundOutput{ + {Address: giftAddress, Value: giftAmountSF}, + }, + } + genesisBlock.Transactions = []types.Transaction{giftTxn} + db, cs := newConsensusDB(n, genesisBlock) + + signTxn := func(cs State, txn *types.V2Transaction) { + for i := range txn.SiacoinInputs { + txn.SiacoinInputs[i].SatisfiedPolicy = types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + Signatures: []types.Signature{key.SignHash(cs.InputSigHash(*txn))}, + } + } + for i := range txn.SiafundInputs { + txn.SiafundInputs[i].SatisfiedPolicy = types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + Signatures: []types.Signature{key.SignHash(cs.InputSigHash(*txn))}, + } + } + for i := range txn.FileContracts { + txn.FileContracts[i].RenterSignature = key.SignHash(cs.ContractSigHash(txn.FileContracts[i])) + txn.FileContracts[i].HostSignature = key.SignHash(cs.ContractSigHash(txn.FileContracts[i])) + } + } + mineTxns := func(txns []types.Transaction, v2txns []types.V2Transaction) (au ApplyUpdate, err error) { + b := types.Block{ + ParentID: cs.Index.ID, + Timestamp: types.CurrentTimestamp(), + MinerPayouts: []types.SiacoinOutput{{Address: types.VoidAddress, Value: cs.BlockReward()}}, + Transactions: txns, + } + if len(v2txns) > 0 { + b.V2 = &types.V2BlockData{ + Height: cs.Index.Height + 1, + Commitment: cs.Commitment(cs.TransactionsCommitment(txns, v2txns), b.MinerPayouts[0].Address), + Transactions: v2txns, + } + } + findBlockNonce(cs, &b) + if err = ValidateBlock(cs, b, V1BlockSupplement{}); err != nil { + return + } + cs, au = ApplyBlock(cs, b, V1BlockSupplement{}, db.ancestorTimestamp(b.ParentID)) + db.applyBlock(au) + return + } + + fc := types.V2FileContract{ + ProofHeight: 20, + ExpirationHeight: 30, + RenterOutput: types.SiacoinOutput{Value: types.Siacoins(5000)}, + HostOutput: types.SiacoinOutput{Value: types.Siacoins(5000)}, + RenterPublicKey: key.PublicKey(), + HostPublicKey: key.PublicKey(), + } + fcValue := fc.RenterOutput.Value.Add(fc.HostOutput.Value).Add(cs.V2FileContractTax(fc)) + + txn := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: db.sces[giftTxn.SiacoinOutputID(0)], + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: giftAddress, + Value: giftAmountSC.Sub(fcValue), + }}, + FileContracts: []types.V2FileContract{fc}, + } + signTxn(cs, &txn) + prev := cs + if _, err := mineTxns(nil, []types.V2Transaction{txn}); err != nil { + t.Fatal(err) + } + // siafund revenue should have increased + if cs.SiafundTaxRevenue != prev.SiafundTaxRevenue.Add(cs.V2FileContractTax(fc)) { + t.Fatalf("expected %v siafund revenue, got %v", prev.SiafundTaxRevenue.Add(cs.V2FileContractTax(fc)), cs.SiafundTaxRevenue) + } + + // make a siafund claim + txn = types.V2Transaction{ + SiafundInputs: []types.V2SiafundInput{{ + Parent: db.sfes[giftTxn.SiafundOutputID(0)], + ClaimAddress: giftAddress, + }}, + SiafundOutputs: []types.SiafundOutput{{ + Address: giftAddress, + Value: giftAmountSF, + }}, + } + signTxn(cs, &txn) + prev = cs + if au, err := mineTxns(nil, []types.V2Transaction{txn}); err != nil { + t.Fatal(err) + } else { + // siafund revenue should be unchanged + if cs.SiafundTaxRevenue != prev.SiafundTaxRevenue { + t.Fatalf("expected %v siafund revenue, got %v", prev.SiafundTaxRevenue, cs.SiafundTaxRevenue) + } + // should have received a timelocked siafund claim output + var claimOutput *types.SiacoinElement + au.ForEachSiacoinElement(func(sce types.SiacoinElement, _, _ bool) { + if sce.ID == txn.SiafundInputs[0].Parent.ID.V2ClaimOutputID() { + claimOutput = &sce + } + }) + if claimOutput == nil { + t.Fatal("expected siafund claim output") + } else if claimOutput.MaturityHeight != cs.MaturityHeight()-1 { + t.Fatalf("expected siafund claim output to mature at height %v, got %v", cs.MaturityHeight()-1, claimOutput.MaturityHeight) + } else if exp := cs.V2FileContractTax(fc).Div64(cs.SiafundCount() / giftAmountSF); claimOutput.SiacoinOutput.Value != exp { + t.Fatalf("expected siafund claim output value %v, got %v", exp, claimOutput.SiacoinOutput.Value) + } + } +} + +func TestFoundationSubsidy(t *testing.T) { + key := types.GeneratePrivateKey() + addr := types.StandardAddress(key.PublicKey()) + n, genesisBlock := testnet() + n.HardforkFoundation.Height = 1 + n.HardforkFoundation.PrimaryAddress = addr + n.HardforkFoundation.FailsafeAddress = addr + n.HardforkV2.AllowHeight = 1 + n.HardforkV2.RequireHeight = 1 + n.BlockInterval = 10 * time.Hour // subsidies every 10 blocks + subsidyInterval := uint64(365 * 24 * time.Hour / n.BlockInterval / 12) + genesisBlock.Transactions = []types.Transaction{{ + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: types.Siacoins(1), // funds for changing address later + }}, + }} + scoid := genesisBlock.Transactions[0].SiacoinOutputID(0) + + db, cs := newConsensusDB(n, genesisBlock) + mineBlock := func(txns []types.V2Transaction) (subsidy types.SiacoinElement, exists bool) { + b := types.Block{ + ParentID: cs.Index.ID, + Timestamp: types.CurrentTimestamp(), + MinerPayouts: []types.SiacoinOutput{{Address: types.VoidAddress, Value: cs.BlockReward()}}, + V2: &types.V2BlockData{ + Height: cs.Index.Height + 1, + Commitment: cs.Commitment(cs.TransactionsCommitment(nil, txns), types.VoidAddress), + Transactions: txns, + }, + } + bs := db.supplementTipBlock(b) + findBlockNonce(cs, &b) + if err := ValidateBlock(cs, b, bs); err != nil { + t.Fatal(err) + return + } + var au ApplyUpdate + cs, au = ApplyBlock(cs, b, bs, db.ancestorTimestamp(b.ParentID)) + db.applyBlock(au) + au.ForEachSiacoinElement(func(sce types.SiacoinElement, created, _ bool) { + if created && sce.SiacoinOutput.Address == addr { + subsidy = sce + exists = true + } + }) + return + } + + // receive initial subsidy + initialSubsidy, ok := mineBlock(nil) + if !ok { + t.Fatal("expected subsidy") + } + + // mine until we receive a normal subsidy + for range subsidyInterval - 1 { + if _, ok := mineBlock(nil); ok { + t.Fatal("unexpected subsidy") + } + } + subsidy, ok := mineBlock(nil) + if !ok { + t.Fatal("expected subsidy") + } else if subsidy.SiacoinOutput.Value != initialSubsidy.SiacoinOutput.Value.Div64(12) { + t.Fatal("expected subsidy to be 1/12 of initial subsidy") + } + // disable subsidy + txn := types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: db.sces[scoid], + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + }, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: db.sces[scoid].SiacoinOutput.Value, + }}, + NewFoundationAddress: &types.VoidAddress, + } + txn.SiacoinInputs[0].SatisfiedPolicy.Signatures = []types.Signature{key.SignHash(cs.InputSigHash(txn))} + scoid = txn.SiacoinOutputID(txn.ID(), 0) + mineBlock([]types.V2Transaction{txn}) + + // mine until we would receive another subsidy + for range subsidyInterval { + if _, ok := mineBlock(nil); ok { + t.Fatal("unexpected subsidy") + } + } + + // re-enable subsidy + txn = types.V2Transaction{ + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: db.sces[scoid], + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.PolicyPublicKey(key.PublicKey()), + }, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: db.sces[scoid].SiacoinOutput.Value, + }}, + NewFoundationAddress: &addr, + } + txn.SiacoinInputs[0].SatisfiedPolicy.Signatures = []types.Signature{key.SignHash(cs.InputSigHash(txn))} + mineBlock([]types.V2Transaction{txn}) + + // mine until we would receive another subsidy + for range subsidyInterval - 3 { + if _, ok := mineBlock(nil); ok { + t.Fatal("unexpected subsidy") + } + } + if _, ok := mineBlock(nil); !ok { + t.Fatal("expected subsidy") + } +} diff --git a/consensus/state.go b/consensus/state.go index 85757d8f..6cebe88f 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -91,19 +91,19 @@ func (n *Network) GenesisState() State { return State{ Network: n, - Index: types.ChainIndex{Height: ^uint64(0)}, - PrevTimestamps: [11]time.Time{}, - Depth: intToTarget(maxTarget), - ChildTarget: n.InitialTarget, - SiafundPool: types.ZeroCurrency, + Index: types.ChainIndex{Height: ^uint64(0)}, + PrevTimestamps: [11]time.Time{}, + Depth: intToTarget(maxTarget), + ChildTarget: n.InitialTarget, + SiafundTaxRevenue: types.ZeroCurrency, - OakTime: 0, - OakTarget: intToTarget(maxTarget), - FoundationPrimaryAddress: n.HardforkFoundation.PrimaryAddress, - FoundationFailsafeAddress: n.HardforkFoundation.FailsafeAddress, - TotalWork: Work{invTarget(intToTarget(maxTarget))}, - Difficulty: Work{invTarget(n.InitialTarget)}, - OakWork: Work{invTarget(intToTarget(maxTarget))}, + OakTime: 0, + OakTarget: intToTarget(maxTarget), + FoundationSubsidyAddress: n.HardforkFoundation.PrimaryAddress, + FoundationManagementAddress: n.HardforkFoundation.FailsafeAddress, + TotalWork: Work{invTarget(intToTarget(maxTarget))}, + Difficulty: Work{invTarget(n.InitialTarget)}, + OakWork: Work{invTarget(intToTarget(maxTarget))}, } } @@ -111,18 +111,18 @@ func (n *Network) GenesisState() State { type State struct { Network *Network `json:"-"` // network parameters are not encoded - Index types.ChainIndex `json:"index"` - PrevTimestamps [11]time.Time `json:"prevTimestamps"` // newest -> oldest - Depth types.BlockID `json:"depth"` - ChildTarget types.BlockID `json:"childTarget"` - SiafundPool types.Currency `json:"siafundPool"` + Index types.ChainIndex `json:"index"` + PrevTimestamps [11]time.Time `json:"prevTimestamps"` // newest -> oldest + Depth types.BlockID `json:"depth"` + ChildTarget types.BlockID `json:"childTarget"` + SiafundTaxRevenue types.Currency `json:"siafundTaxRevenue"` // Oak hardfork state OakTime time.Duration `json:"oakTime"` OakTarget types.BlockID `json:"oakTarget"` // Foundation hardfork state - FoundationPrimaryAddress types.Address `json:"foundationPrimaryAddress"` - FoundationFailsafeAddress types.Address `json:"foundationFailsafeAddress"` + FoundationSubsidyAddress types.Address `json:"foundationSubsidyAddress"` + FoundationManagementAddress types.Address `json:"foundationManagementAddress"` // v2 hardfork state TotalWork Work `json:"totalWork"` Difficulty Work `json:"difficulty"` @@ -139,12 +139,12 @@ func (s State) EncodeTo(e *types.Encoder) { } s.Depth.EncodeTo(e) s.ChildTarget.EncodeTo(e) - types.V2Currency(s.SiafundPool).EncodeTo(e) + types.V2Currency(s.SiafundTaxRevenue).EncodeTo(e) e.WriteUint64(uint64(s.OakTime)) s.OakTarget.EncodeTo(e) - s.FoundationPrimaryAddress.EncodeTo(e) - s.FoundationFailsafeAddress.EncodeTo(e) + s.FoundationSubsidyAddress.EncodeTo(e) + s.FoundationManagementAddress.EncodeTo(e) s.TotalWork.EncodeTo(e) s.Difficulty.EncodeTo(e) s.OakWork.EncodeTo(e) @@ -160,12 +160,12 @@ func (s *State) DecodeFrom(d *types.Decoder) { } s.Depth.DecodeFrom(d) s.ChildTarget.DecodeFrom(d) - (*types.V2Currency)(&s.SiafundPool).DecodeFrom(d) + (*types.V2Currency)(&s.SiafundTaxRevenue).DecodeFrom(d) s.OakTime = time.Duration(d.ReadUint64()) s.OakTarget.DecodeFrom(d) - s.FoundationPrimaryAddress.DecodeFrom(d) - s.FoundationFailsafeAddress.DecodeFrom(d) + s.FoundationSubsidyAddress.DecodeFrom(d) + s.FoundationManagementAddress.DecodeFrom(d) s.TotalWork.DecodeFrom(d) s.Difficulty.DecodeFrom(d) s.OakWork.DecodeFrom(d) @@ -257,22 +257,23 @@ func (s State) AncestorDepth() uint64 { } // FoundationSubsidy returns the Foundation subsidy output for the child block. -// If no subsidy is due, the returned output has a value of zero. func (s State) FoundationSubsidy() (sco types.SiacoinOutput, exists bool) { - sco.Address = s.FoundationPrimaryAddress - + if s.FoundationSubsidyAddress == types.VoidAddress { + return types.SiacoinOutput{}, false + } + sco.Address = s.FoundationSubsidyAddress subsidyPerBlock := types.Siacoins(30000) blocksPerYear := uint64(365 * 24 * time.Hour / s.BlockInterval()) blocksPerMonth := blocksPerYear / 12 hardforkHeight := s.Network.HardforkFoundation.Height if s.childHeight() < hardforkHeight || (s.childHeight()-hardforkHeight)%blocksPerMonth != 0 { - sco.Value = types.ZeroCurrency + return types.SiacoinOutput{}, false } else if s.childHeight() == hardforkHeight { sco.Value = subsidyPerBlock.Mul64(blocksPerYear) } else { sco.Value = subsidyPerBlock.Mul64(blocksPerMonth) } - return sco, !sco.Value.IsZero() + return sco, true } // NonceFactor is the factor by which all block nonces must be divisible. @@ -342,6 +343,9 @@ func (s State) V2TransactionWeight(txn types.V2Transaction) uint64 { a.EncodeTo(e) } e.Write(txn.ArbitraryData) + if txn.NewFoundationAddress != nil { + txn.NewFoundationAddress.EncodeTo(e) + } e.Flush() return uint64(wc.n) } @@ -370,12 +374,7 @@ func (s State) FileContractTax(fc types.FileContract) types.Currency { // V2FileContractTax computes the tax levied on a given v2 contract. func (s State) V2FileContractTax(fc types.V2FileContract) types.Currency { - sum := fc.RenterOutput.Value.Add(fc.HostOutput.Value) - tax := sum.Div64(25) // 4% - // round down to nearest multiple of SiafundCount - _, r := bits.Div64(0, tax.Hi, s.SiafundCount()) - _, r = bits.Div64(r, tax.Lo, s.SiafundCount()) - return tax.Sub(types.NewCurrency64(r)) + return fc.RenterOutput.Value.Add(fc.HostOutput.Value).Div64(25) // 4% } // StorageProofLeafIndex returns the leaf index used when computing or @@ -533,7 +532,7 @@ func (s State) PartialSigHash(txn types.Transaction, cf types.CoveredFields) typ // TransactionsCommitment returns the commitment hash covering the transactions // that comprise a child block. -func (s *State) TransactionsCommitment(txns []types.Transaction, v2txns []types.V2Transaction) types.Hash256 { +func (s State) TransactionsCommitment(txns []types.Transaction, v2txns []types.V2Transaction) types.Hash256 { var acc blake2b.Accumulator for _, txn := range txns { acc.AddLeaf(txn.FullHash()) @@ -574,7 +573,6 @@ func (s State) ContractSigHash(fc types.V2FileContract) types.Hash256 { func (s State) RenewalSigHash(fcr types.V2FileContractRenewal) types.Hash256 { nilSigs( &fcr.NewContract.RenterSignature, &fcr.NewContract.HostSignature, - &fcr.FinalRevision.RenterSignature, &fcr.FinalRevision.HostSignature, &fcr.RenterSignature, &fcr.HostSignature, ) return hashAll("sig/filecontractrenewal", s.v2ReplayPrefix(), fcr) @@ -588,16 +586,16 @@ func (s State) AttestationSigHash(a types.Attestation) types.Hash256 { // A MidState represents the state of the chain within a block. type MidState struct { - base State - created map[types.ElementID]int // indices into element slices - spends map[types.ElementID]types.TransactionID - revs map[types.FileContractID]*types.FileContractElement - res map[types.FileContractID]bool - v2revs map[types.FileContractID]*types.V2FileContractElement - v2res map[types.FileContractID]types.V2FileContractResolutionType - siafundPool types.Currency - foundationPrimary types.Address - foundationFailsafe types.Address + base State + created map[types.ElementID]int // indices into element slices + spends map[types.ElementID]types.TransactionID + revs map[types.FileContractID]*types.FileContractElement + res map[types.FileContractID]bool + v2revs map[types.FileContractID]*types.V2FileContractElement + v2res map[types.FileContractID]types.V2FileContractResolutionType + siafundTaxRevenue types.Currency + foundationSubsidy types.Address + foundationManagement types.Address // elements created/updated by block sces []types.SiacoinElement @@ -609,27 +607,58 @@ type MidState struct { } func (ms *MidState) siacoinElement(ts V1TransactionSupplement, id types.SiacoinOutputID) (types.SiacoinElement, bool) { - if i, ok := ms.created[types.Hash256(id)]; ok { + if i, ok := ms.created[id]; ok { return ms.sces[i], true } - return ts.siacoinElement(id) + for _, sce := range ts.SiacoinInputs { + if sce.ID == id { + return sce, true + } + } + return types.SiacoinElement{}, false } func (ms *MidState) siafundElement(ts V1TransactionSupplement, id types.SiafundOutputID) (types.SiafundElement, bool) { if i, ok := ms.created[id]; ok { return ms.sfes[i], true } - return ts.siafundElement(id) + for _, sfe := range ts.SiafundInputs { + if sfe.ID == id { + return sfe, true + } + } + return types.SiafundElement{}, false } func (ms *MidState) fileContractElement(ts V1TransactionSupplement, id types.FileContractID) (types.FileContractElement, bool) { if rev, ok := ms.revs[id]; ok { return *rev, true - } - if i, ok := ms.created[id]; ok { + } else if i, ok := ms.created[id]; ok { return ms.fces[i], true } - return ts.revision(id) + for _, fce := range ts.RevisedFileContracts { + if fce.ID == id { + return fce, true + } + } + for _, sps := range ts.StorageProofs { + if sps.FileContract.ID == id { + return sps.FileContract, true + } + } + return types.FileContractElement{}, false +} + +func (ms *MidState) storageProofWindowID(ts V1TransactionSupplement, id types.FileContractID) (types.BlockID, bool) { + if i, ok := ms.created[id]; ok && ms.fces[i].FileContract.WindowStart == ms.base.childHeight() { + return ms.base.Index.ID, true + } + for _, sps := range ts.StorageProofs { + if sps.FileContract.ID == id { + return sps.WindowID, true + } + } + return types.BlockID{}, false } func (ms *MidState) spent(id types.ElementID) (types.TransactionID, bool) { @@ -650,16 +679,16 @@ func (ms *MidState) isCreated(id types.ElementID) bool { // NewMidState constructs a MidState initialized to the provided base state. func NewMidState(s State) *MidState { return &MidState{ - base: s, - created: make(map[types.ElementID]int), - spends: make(map[types.ElementID]types.TransactionID), - revs: make(map[types.FileContractID]*types.FileContractElement), - res: make(map[types.FileContractID]bool), - v2revs: make(map[types.FileContractID]*types.V2FileContractElement), - v2res: make(map[types.FileContractID]types.V2FileContractResolutionType), - siafundPool: s.SiafundPool, - foundationPrimary: s.FoundationPrimaryAddress, - foundationFailsafe: s.FoundationFailsafeAddress, + base: s, + created: make(map[types.ElementID]int), + spends: make(map[types.ElementID]types.TransactionID), + revs: make(map[types.FileContractID]*types.FileContractElement), + res: make(map[types.FileContractID]bool), + v2revs: make(map[types.FileContractID]*types.V2FileContractElement), + v2res: make(map[types.FileContractID]types.V2FileContractResolutionType), + siafundTaxRevenue: s.SiafundTaxRevenue, + foundationSubsidy: s.FoundationSubsidyAddress, + foundationManagement: s.FoundationManagementAddress, } } @@ -712,42 +741,6 @@ func (ts *V1TransactionSupplement) DecodeFrom(d *types.Decoder) { types.DecodeSlice(d, &ts.StorageProofs) } -func (ts V1TransactionSupplement) siacoinElement(id types.SiacoinOutputID) (sce types.SiacoinElement, ok bool) { - for _, sce := range ts.SiacoinInputs { - if types.SiacoinOutputID(sce.ID) == id { - return sce, true - } - } - return -} - -func (ts V1TransactionSupplement) siafundElement(id types.SiafundOutputID) (sfe types.SiafundElement, ok bool) { - for _, sfe := range ts.SiafundInputs { - if types.SiafundOutputID(sfe.ID) == id { - return sfe, true - } - } - return -} - -func (ts V1TransactionSupplement) revision(id types.FileContractID) (fce types.FileContractElement, ok bool) { - for _, fce := range ts.RevisedFileContracts { - if types.FileContractID(fce.ID) == id { - return fce, true - } - } - return -} - -func (ts V1TransactionSupplement) storageProof(id types.FileContractID) (sps V1StorageProofSupplement, ok bool) { - for _, sps := range ts.StorageProofs { - if types.FileContractID(sps.FileContract.ID) == id { - return sps, true - } - } - return -} - // A V1BlockSupplement contains elements that are associated with a v1 block, // but not included in the block. This includes supplements for each v1 // transaction, as well as any file contracts that expired at the block's diff --git a/consensus/validation.go b/consensus/validation.go index 566227df..cb200ea9 100644 --- a/consensus/validation.go +++ b/consensus/validation.go @@ -15,9 +15,9 @@ func validateHeader(s State, parentID types.BlockID, timestamp time.Time, nonce if parentID != s.Index.ID { return errors.New("wrong parent ID") } else if timestamp.Before(s.medianTimestamp()) { - return errors.New("timestamp is too far in the past") + return errors.New("timestamp too far in the past") } else if nonce%s.NonceFactor() != 0 { - return errors.New("nonce is not divisible by required factor") + return errors.New("nonce not divisible by required factor") } else if id.CmpWork(s.ChildTarget) < 0 { return errors.New("insufficient work") } @@ -71,7 +71,7 @@ func validateMinerPayouts(s State, b types.Block) error { } } if sum != expectedSum { - return fmt.Errorf("miner payout sum (%d) does not match block reward + fees (%d)", sum, expectedSum) + return fmt.Errorf("miner payout sum (%v) does not match block reward + fees (%v)", sum, expectedSum) } return nil } @@ -90,7 +90,7 @@ func ValidateOrphan(s State, b types.Block) error { } else if err := validateMinerPayouts(s, b); err != nil { return err } else if err := validateHeader(s, b.ParentID, b.Timestamp, b.Nonce, b.ID()); err != nil { - return err + return fmt.Errorf("block has %w", err) } if b.V2 != nil { if b.V2.Height != s.Index.Height+1 { @@ -202,7 +202,7 @@ func validateSiacoins(ms *MidState, txn types.Transaction, ts V1TransactionSuppl outputSum = outputSum.Add(fee) } if inputSum.Cmp(outputSum) != 0 { - return fmt.Errorf("siacoin inputs (%d H) do not equal outputs (%d H)", inputSum, outputSum) + return fmt.Errorf("siacoin inputs (%v) do not equal outputs (%v)", inputSum, outputSum) } return nil } @@ -351,15 +351,19 @@ func validateFileContracts(ms *MidState, txn types.Transaction, ts V1Transaction } for i, sp := range txn.StorageProofs { - if txid, ok := ms.spent(types.Hash256(sp.ParentID)); ok { + if txid, ok := ms.spent(sp.ParentID); ok { return fmt.Errorf("storage proof %v conflicts with previous proof (in %v)", i, txid) } - sps, ok := ts.storageProof(sp.ParentID) + fce, ok := ms.fileContractElement(ts, sp.ParentID) if !ok { return fmt.Errorf("storage proof %v references nonexistent file contract", i) } - fc := sps.FileContract.FileContract - leafIndex := ms.base.StorageProofLeafIndex(fc.Filesize, sps.WindowID, sp.ParentID) + fc := fce.FileContract + windowID, ok := ms.storageProofWindowID(ts, sp.ParentID) + if !ok { + return fmt.Errorf("storage proof %v cannot be submitted until after window start (%v)", i, fc.WindowStart) + } + leafIndex := ms.base.StorageProofLeafIndex(fc.Filesize, windowID, sp.ParentID) leaf := storageProofLeaf(leafIndex, fc.Filesize, sp.Leaf) if leaf == nil { continue @@ -387,7 +391,7 @@ func validateArbitraryData(ms *MidState, txn types.Transaction) error { // check that the transaction is signed by a current key var signed bool for _, sci := range txn.SiacoinInputs { - if uh := sci.UnlockConditions.UnlockHash(); uh != ms.base.FoundationPrimaryAddress && uh != ms.base.FoundationFailsafeAddress { + if uh := sci.UnlockConditions.UnlockHash(); uh != ms.base.FoundationSubsidyAddress && uh != ms.base.FoundationManagementAddress { continue } for _, sig := range txn.Signatures { @@ -617,7 +621,7 @@ func validateV2Siacoins(ms *MidState, txn types.V2Transaction) error { } outputSum = outputSum.Add(txn.MinerFee) if inputSum != outputSum { - return fmt.Errorf("siacoin inputs (%d H) do not equal outputs (%d H)", inputSum, outputSum) + return fmt.Errorf("siacoin inputs (%v) do not equal outputs (%v)", inputSum, outputSum) } return nil @@ -690,32 +694,17 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return nil } - validateSignatures := func(fc types.V2FileContract, renter, host types.PublicKey, renewal bool) error { - if renewal { - // The sub-contracts of a renewal must have empty signatures; - // otherwise they would be independently valid, i.e. the atomicity - // of the renewal could be violated. Consider a host who has lost or - // deleted their contract data; all they have to do is wait for a - // renter to initiate a renewal, then broadcast just the - // finalization of the old contract, allowing them to successfully - // resolve the contract without a storage proof. - if fc.RenterSignature != (types.Signature{}) { - return errors.New("has non-empty renter signature") - } else if fc.HostSignature != (types.Signature{}) { - return errors.New("has non-empty host signature") - } - } else { - contractHash := ms.base.ContractSigHash(fc) - if !renter.VerifyHash(contractHash, fc.RenterSignature) { - return errors.New("has invalid renter signature") - } else if !host.VerifyHash(contractHash, fc.HostSignature) { - return errors.New("has invalid host signature") - } + validateSignatures := func(fc types.V2FileContract, renter, host types.PublicKey) error { + contractHash := ms.base.ContractSigHash(fc) + if !renter.VerifyHash(contractHash, fc.RenterSignature) { + return errors.New("has invalid renter signature") + } else if !host.VerifyHash(contractHash, fc.HostSignature) { + return errors.New("has invalid host signature") } return nil } - validateContract := func(fc types.V2FileContract, renewal bool) error { + validateContract := func(fc types.V2FileContract) error { switch { case fc.Filesize > fc.Capacity: return fmt.Errorf("has filesize (%v) exceeding capacity (%v)", fc.Filesize, fc.Capacity) @@ -726,14 +715,14 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { case fc.RenterOutput.Value.IsZero() && fc.HostOutput.Value.IsZero(): return fmt.Errorf("has zero value") case fc.MissedHostValue.Cmp(fc.HostOutput.Value) > 0: - return fmt.Errorf("has missed host value (%d H) exceeding valid host value (%d H)", fc.MissedHostValue, fc.HostOutput.Value) + return fmt.Errorf("has missed host value (%v) exceeding valid host value (%v)", fc.MissedHostValue, fc.HostOutput.Value) case fc.TotalCollateral.Cmp(fc.HostOutput.Value) > 0: - return fmt.Errorf("has total collateral (%d H) exceeding valid host value (%d H)", fc.TotalCollateral, fc.HostOutput.Value) + return fmt.Errorf("has total collateral (%v) exceeding valid host value (%v)", fc.TotalCollateral, fc.HostOutput.Value) } - return validateSignatures(fc, fc.RenterPublicKey, fc.HostPublicKey, renewal) + return validateSignatures(fc, fc.RenterPublicKey, fc.HostPublicKey) } - validateRevision := func(fce types.V2FileContractElement, rev types.V2FileContract, renewal bool) error { + validateRevision := func(fce types.V2FileContractElement, rev types.V2FileContract) error { cur := fce.V2FileContract if priorRev, ok := ms.v2revs[fce.ID]; ok { cur = priorRev.V2FileContract @@ -750,9 +739,9 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { case rev.RevisionNumber <= cur.RevisionNumber: 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) + return fmt.Errorf("modifies output sum (%v -> %v)", 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) + return fmt.Errorf("has missed host value (%v) exceeding old value (%v)", rev.MissedHostValue, cur.MissedHostValue) case rev.TotalCollateral != cur.TotalCollateral: return errors.New("modifies total collateral") case rev.ProofHeight < ms.base.childHeight(): @@ -761,11 +750,11 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return fmt.Errorf("leaves no time between proof height (%v) and expiration height (%v)", rev.ProofHeight, rev.ExpirationHeight) } // NOTE: very important that we verify with the *current* keys! - return validateSignatures(rev, cur.RenterPublicKey, cur.HostPublicKey, renewal) + return validateSignatures(rev, cur.RenterPublicKey, cur.HostPublicKey) } for i, fc := range txn.FileContracts { - if err := validateContract(fc, false); err != nil { + if err := validateContract(fc); err != nil { return fmt.Errorf("file contract %v %s", i, err) } } @@ -776,11 +765,7 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { return fmt.Errorf("file contract revision %v parent (%v) %s", i, fcr.Parent.ID, err) } else if cur.ProofHeight < ms.base.childHeight() { return fmt.Errorf("file contract revision %v cannot be applied to contract after proof height (%v)", i, cur.ProofHeight) - } else if rev.RevisionNumber == types.MaxRevisionNumber { - // NOTE: disallowing this means that resolutions always take - // precedence over revisions - return fmt.Errorf("file contract revision %v resolves contract", i) - } else if err := validateRevision(fcr.Parent, rev, false); err != nil { + } else if err := validateRevision(fcr.Parent, rev); err != nil { return fmt.Errorf("file contract revision %v %s", i, err) } revised[fcr.Parent.ID] = i @@ -794,25 +779,29 @@ func validateV2FileContracts(ms *MidState, txn types.V2Transaction) error { switch r := fcr.Resolution.(type) { case *types.V2FileContractRenewal: renewal := *r - old, renewed := renewal.FinalRevision, renewal.NewContract - if old.RevisionNumber != types.MaxRevisionNumber { - return fmt.Errorf("file contract renewal %v does not finalize old contract", i) - } else if err := validateRevision(fcr.Parent, old, true); err != nil { - return fmt.Errorf("file contract renewal %v final revision %s", i, err) - } else if err := validateContract(renewed, true); err != nil { - return fmt.Errorf("file contract renewal %v initial revision %s", i, err) + + if fc.RenterPublicKey != renewal.NewContract.RenterPublicKey { + return fmt.Errorf("file contract renewal %v changes renter public key", i) + } else if fc.HostPublicKey != renewal.NewContract.HostPublicKey { + return fmt.Errorf("file contract renewal %v changes host public key", i) } - rollover := renewal.RenterRollover.Add(renewal.HostRollover) - newContractCost := renewed.RenterOutput.Value.Add(renewed.HostOutput.Value).Add(ms.base.V2FileContractTax(renewed)) - if renewal.RenterRollover.Cmp(old.RenterOutput.Value) > 0 { - return fmt.Errorf("file contract renewal %v has renter rollover (%d H) exceeding old output (%d H)", i, renewal.RenterRollover, old.RenterOutput.Value) - } else if renewal.HostRollover.Cmp(old.HostOutput.Value) > 0 { - return fmt.Errorf("file contract renewal %v has host rollover (%d H) exceeding old output (%d H)", i, renewal.HostRollover, old.HostOutput.Value) - } else if rollover.Cmp(newContractCost) > 0 { - return fmt.Errorf("file contract renewal %v has rollover (%d H) exceeding new contract cost (%d H)", i, rollover, newContractCost) + // validate that the renewal value is equal to existing contract's value. + // This must be done as a sum of the outputs, since the individual payouts may have + // changed in an unbroadcast revision. + totalPayout := renewal.FinalRenterOutput.Value.Add(renewal.RenterRollover). + Add(renewal.FinalHostOutput.Value).Add(renewal.HostRollover) + existingPayout := fc.RenterOutput.Value.Add(fc.HostOutput.Value) + if totalPayout != existingPayout { + return fmt.Errorf("file contract renewal %v renewal payout (%v) does not match existing contract payout %v", i, totalPayout, existingPayout) } + newContractCost := renewal.NewContract.RenterOutput.Value.Add(renewal.NewContract.HostOutput.Value).Add(ms.base.V2FileContractTax(renewal.NewContract)) + if rollover := renewal.RenterRollover.Add(renewal.HostRollover); rollover.Cmp(newContractCost) > 0 { + return fmt.Errorf("file contract renewal %v has rollover (%v) exceeding new contract cost (%v)", i, rollover, newContractCost) + } else if err := validateContract(renewal.NewContract); err != nil { + return fmt.Errorf("file contract renewal %v initial revision %s", i, err) + } renewalHash := ms.base.RenewalSigHash(renewal) if !fc.RenterPublicKey.VerifyHash(renewalHash, renewal.RenterSignature) { return fmt.Errorf("file contract renewal %v has invalid renter signature", i) @@ -861,7 +850,7 @@ func validateFoundationUpdate(ms *MidState, txn types.V2Transaction) error { return nil } for _, in := range txn.SiacoinInputs { - if in.Parent.SiacoinOutput.Address == ms.base.FoundationPrimaryAddress { + if in.Parent.SiacoinOutput.Address == ms.base.FoundationManagementAddress { return nil } } diff --git a/consensus/validation_test.go b/consensus/validation_test.go index c5f4f85e..8db0a60e 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -3,6 +3,7 @@ package consensus import ( "bytes" "errors" + "fmt" "math" "math/bits" "strings" @@ -42,10 +43,11 @@ func testnet() (*Network, types.Block) { } type consensusDB struct { - sces map[types.SiacoinOutputID]types.SiacoinElement - sfes map[types.SiafundOutputID]types.SiafundElement - fces map[types.FileContractID]types.FileContractElement - v2fces map[types.FileContractID]types.V2FileContractElement + sces map[types.SiacoinOutputID]types.SiacoinElement + sfes map[types.SiafundOutputID]types.SiafundElement + fces map[types.FileContractID]types.FileContractElement + v2fces map[types.FileContractID]types.V2FileContractElement + blockIDs []types.BlockID } func (db *consensusDB) applyBlock(au ApplyUpdate) { @@ -97,6 +99,7 @@ func (db *consensusDB) applyBlock(au ApplyUpdate) { delete(db.v2fces, types.FileContractID(fce.ID)) } }) + db.blockIDs = append(db.blockIDs, au.ms.cie.ID) } func (db *consensusDB) revertBlock(ru RevertUpdate) { @@ -171,6 +174,14 @@ func (db *consensusDB) supplementTipBlock(b types.Block) (bs V1BlockSupplement) ts.RevisedFileContracts = append(ts.RevisedFileContracts, fce) } } + for _, sp := range txn.StorageProofs { + if fce, ok := db.fces[sp.ParentID]; ok { + ts.StorageProofs = append(ts.StorageProofs, V1StorageProofSupplement{ + FileContract: fce, + WindowID: db.blockIDs[fce.FileContract.WindowStart], + }) + } + } } return bs } @@ -324,40 +335,48 @@ func TestValidateBlock(t *testing.T) { b := types.Block{ ParentID: genesisBlock.ID(), Timestamp: types.CurrentTimestamp(), - Transactions: []types.Transaction{{ - SiacoinInputs: []types.SiacoinInput{{ - ParentID: giftTxn.SiacoinOutputID(0), - UnlockConditions: types.StandardUnlockConditions(giftPublicKey), - }}, - SiafundInputs: []types.SiafundInput{{ - ParentID: giftTxn.SiafundOutputID(0), - ClaimAddress: types.VoidAddress, - UnlockConditions: types.StandardUnlockConditions(giftPublicKey), - }}, - SiacoinOutputs: []types.SiacoinOutput{ - {Value: giftAmountSC.Sub(fc.Payout), Address: giftAddress}, - }, - SiafundOutputs: []types.SiafundOutput{ - {Value: giftAmountSF / 2, Address: giftAddress}, - {Value: giftAmountSF / 2, Address: types.VoidAddress}, - }, - FileContracts: []types.FileContract{fc}, - FileContractRevisions: []types.FileContractRevision{ - { - ParentID: giftTxn.FileContractID(0), - UnlockConditions: types.UnlockConditions{ - PublicKeys: []types.UnlockKey{renterPublicKey.UnlockKey(), hostPublicKey.UnlockKey()}, - SignaturesRequired: 2, + Transactions: []types.Transaction{ + { + SiacoinInputs: []types.SiacoinInput{{ + ParentID: giftTxn.SiacoinOutputID(0), + UnlockConditions: types.StandardUnlockConditions(giftPublicKey), + }}, + SiafundInputs: []types.SiafundInput{{ + ParentID: giftTxn.SiafundOutputID(0), + ClaimAddress: types.VoidAddress, + UnlockConditions: types.StandardUnlockConditions(giftPublicKey), + }}, + SiacoinOutputs: []types.SiacoinOutput{ + {Value: giftAmountSC.Sub(fc.Payout), Address: giftAddress}, + }, + SiafundOutputs: []types.SiafundOutput{ + {Value: giftAmountSF / 2, Address: giftAddress}, + {Value: giftAmountSF / 2, Address: types.VoidAddress}, + }, + FileContracts: []types.FileContract{fc}, + FileContractRevisions: []types.FileContractRevision{ + { + ParentID: giftTxn.FileContractID(0), + UnlockConditions: types.UnlockConditions{ + PublicKeys: []types.UnlockKey{renterPublicKey.UnlockKey(), hostPublicKey.UnlockKey()}, + SignaturesRequired: 2, + }, + FileContract: revision, }, - FileContract: revision, }, }, - }}, + }, MinerPayouts: []types.SiacoinOutput{{ Address: types.VoidAddress, Value: cs.BlockReward(), }}, } + b.Transactions[0].FileContracts[0].FileMerkleRoot = types.HashBytes(make([]byte, 65)) + b.Transactions = append(b.Transactions, types.Transaction{ + StorageProofs: []types.StorageProof{{ + ParentID: b.Transactions[0].FileContractID(0), + }}, + }) // block should be valid validBlock := deepCopyBlock(b) @@ -373,11 +392,11 @@ func TestValidateBlock(t *testing.T) { // tests with correct signatures { tests := []struct { - desc string - corrupt func(*types.Block) + errString string + corrupt func(*types.Block) }{ { - "weight that exceeds the limit", + "block exceeds maximum weight", func(b *types.Block) { data := make([]byte, cs.MaxBlockWeight()) b.Transactions = append(b.Transactions, types.Transaction{ @@ -386,25 +405,25 @@ func TestValidateBlock(t *testing.T) { }, }, { - "wrong parent ID", + "block has wrong parent ID", func(b *types.Block) { b.ParentID[0] ^= 255 }, }, { - "wrong timestamp", + "block has timestamp too far in the past", func(b *types.Block) { b.Timestamp = cs.PrevTimestamps[0].AddDate(-1, 0, 0) }, }, { - "no miner payout", + "miner payout sum (0 SC) does not match block reward + fees (300 KS)", func(b *types.Block) { b.MinerPayouts = nil }, }, { - "zero miner payout", + "miner payout has zero value", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -413,7 +432,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "incorrect miner payout", + "miner payout sum (150 KS) does not match block reward + fees (300 KS)", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -422,7 +441,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "overflowing miner payout", + "miner payouts overflow", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{ {Address: types.VoidAddress, Value: types.MaxCurrency}, @@ -431,7 +450,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "overflowing siacoin outputs", + "transaction outputs exceed inputs", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs = []types.SiacoinOutput{ @@ -441,7 +460,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "zero-valued SiacoinOutput", + "transaction creates a zero-valued output", func(b *types.Block) { txn := &b.Transactions[0] for i := range txn.SiacoinOutputs { @@ -452,7 +471,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "zero-valued SiafundOutput", + "transaction creates a zero-valued output", func(b *types.Block) { txn := &b.Transactions[0] for i := range txn.SiafundOutputs { @@ -462,14 +481,14 @@ func TestValidateBlock(t *testing.T) { }, }, { - "zero-valued MinerFee", + "transaction fee has zero value", func(b *types.Block) { txn := &b.Transactions[0] txn.MinerFees = append(txn.MinerFees, types.ZeroCurrency) }, }, { - "overflowing MinerFees", + "transaction fees overflow", func(b *types.Block) { txn := &b.Transactions[0] txn.MinerFees = append(txn.MinerFees, types.MaxCurrency) @@ -477,56 +496,58 @@ func TestValidateBlock(t *testing.T) { }, }, { - "siacoin outputs exceed inputs", + "siacoin inputs (100 SC) do not equal outputs (100.000000000000000000000001 SC)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.NewCurrency64(1)) }, }, { - "siacoin outputs less than inputs", + "siacoin inputs (100 SC) do not equal outputs (99.999999999999999999999999 SC)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Sub(types.NewCurrency64(1)) }, }, { - "siafund outputs exceed inputs", + "siafund inputs (100) do not equal outputs (101)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundOutputs[0].Value++ }, }, { - "siafund outputs less than inputs", + "siafund inputs (100) do not equal outputs (99)", func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundOutputs[0].Value-- }, }, { - "two of the same siacoin input", + fmt.Sprintf("transaction spends siacoin input %v more than once", giftTxn.SiacoinOutputID(0)), func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinInputs = append(txn.SiacoinInputs, txn.SiacoinInputs[0]) + txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(giftAmountSC) }, }, { - "two of the same siafund input", + fmt.Sprintf("transaction spends siafund input %v more than once", giftTxn.SiafundOutputID(0)), func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundInputs = append(txn.SiafundInputs, txn.SiafundInputs[0]) + txn.SiafundOutputs[0].Value += giftAmountSF }, }, { - "siacoin input claiming incorrect unlock conditions", + "siacoin input 0 claims incorrect unlock conditions", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinInputs[0].UnlockConditions.PublicKeys[0].Key[0] ^= 255 }, }, { - "siafund input claiming incorrect unlock conditions", + "siafund input 0 claims incorrect unlock conditions", func(b *types.Block) { txn := &b.Transactions[0] txn.SiafundInputs[0].UnlockConditions.PublicKeys[0].Key[0] ^= 255 @@ -565,12 +586,62 @@ func TestValidateBlock(t *testing.T) { }, }, { - "window that starts in the past", + "file contract 0 has window that starts in the past", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContracts[0].WindowStart = 0 }, }, + { + "storage proof 0 references nonexistent file contract", + func(b *types.Block) { + txn := &b.Transactions[1] + txn.StorageProofs[0].ParentID = types.FileContractID{} + }, + }, + { + "storage proof 0 cannot be submitted until after window start (100)", + func(b *types.Block) { + b.Transactions[0].FileContracts[0].WindowStart = 100 + b.Transactions[1].StorageProofs[0].ParentID = b.Transactions[0].FileContractID(0) + }, + }, + { + "file contract revision 0 conflicts with previous proof or revision", + func(b *types.Block) { + rev := revision + rev.RevisionNumber++ + b.Transactions = append(b.Transactions, types.Transaction{ + FileContractRevisions: []types.FileContractRevision{{ + ParentID: b.Transactions[1].StorageProofs[0].ParentID, + FileContract: rev, + }}, + }) + }, + }, + { + fmt.Sprintf("storage proof 1 resolves contract (%v) already resolved by storage proof 0", b.Transactions[0].FileContractID(0)), + func(b *types.Block) { + txn := &b.Transactions[1] + txn.StorageProofs = append(txn.StorageProofs, txn.StorageProofs[0]) + }, + }, + { + fmt.Sprintf("storage proof 0 conflicts with previous proof (in %v)", b.Transactions[1].ID()), + func(b *types.Block) { + b.Transactions = append(b.Transactions, types.Transaction{ + StorageProofs: b.Transactions[1].StorageProofs, + }) + }, + }, + { + fmt.Sprintf("storage proof 0 conflicts with previous proof (in %v)", b.Transactions[1].ID()), + func(b *types.Block) { + b.Transactions = append(b.Transactions, types.Transaction{ + StorageProofs: b.Transactions[1].StorageProofs, + }) + }, + }, { "window that ends before it begins", func(b *types.Block) { @@ -586,7 +657,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "incorrect payout tax", + "payout with incorrect tax", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.Siacoins(1)) @@ -594,42 +665,43 @@ func TestValidateBlock(t *testing.T) { }, }, { - "revision of nonexistent file contract", + "revises nonexistent file contract", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].ParentID[0] ^= 255 }, }, { - "revision with window that starts in past", + "file contract revision 0 has window that starts in the past", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].WindowStart = cs.Index.Height }, }, { - "revision with window that ends before it begins", + "file contract revision 0 has window that ends before it begins", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].WindowStart = txn.FileContractRevisions[0].WindowEnd }, }, { - "revision with lower revision number than its parent", + "file contract revision 0 does not have a higher revision number than its parent", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].RevisionNumber = 0 + b.Transactions = b.Transactions[:1] }, }, { - "revision claiming incorrect unlock conditions", + "file contract revision 0 claims incorrect unlock conditions", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].UnlockConditions.PublicKeys[0].Key[0] ^= 255 }, }, { - "revision having different valid payout sum", + "file contract revision 0 changes valid payout sum", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].ValidProofOutputs = append(txn.FileContractRevisions[0].ValidProofOutputs, types.SiacoinOutput{ @@ -638,7 +710,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "revision having different missed payout sum", + "file contract revision 0 changes missed payout sum", func(b *types.Block) { txn := &b.Transactions[0] txn.FileContractRevisions[0].MissedProofOutputs = append(txn.FileContractRevisions[0].MissedProofOutputs, types.SiacoinOutput{ @@ -647,7 +719,7 @@ func TestValidateBlock(t *testing.T) { }, }, { - "conflicting revisions in same transaction", + fmt.Sprintf("transaction revises file contract %v more than once", giftTxn.FileContractID(0)), func(b *types.Block) { txn := &b.Transactions[0] newRevision := txn.FileContractRevisions[0] @@ -656,12 +728,12 @@ func TestValidateBlock(t *testing.T) { }, }, { - "misordered revisions", + "file contract revision 0 does not have a higher revision number than its parent", func(b *types.Block) { newRevision := b.Transactions[0].FileContractRevisions[0] newRevision.RevisionNumber = 99 - b.Transactions = append(b.Transactions, types.Transaction{ + b.Transactions = append(b.Transactions[:1], types.Transaction{ FileContractRevisions: []types.FileContractRevision{newRevision}, }) @@ -671,34 +743,18 @@ func TestValidateBlock(t *testing.T) { }, }, { - "duplicate revisions in same block", + "file contract revision 0 does not have a higher revision number than its parent", func(b *types.Block) { txn := &b.Transactions[0] newRevision := txn.FileContractRevisions[0] - b.Transactions = append(b.Transactions, types.Transaction{ + b.Transactions = append(b.Transactions[:1], types.Transaction{ FileContractRevisions: []types.FileContractRevision{newRevision}, }) }, }, { - "double-spent siacoin input", - func(b *types.Block) { - txn := &b.Transactions[0] - txn.SiacoinInputs = append(txn.SiacoinInputs, txn.SiacoinInputs[0]) - txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.Siacoins(100)) - }, - }, - { - "double-spent siafund input", - func(b *types.Block) { - txn := &b.Transactions[0] - txn.SiafundInputs = append(txn.SiafundInputs, txn.SiafundInputs[0]) - txn.SiafundOutputs[0].Value += 100 - }, - }, - { - "transaction contains a storage proof and creates new outputs", + "transaction contains both a storage proof and other outputs", func(b *types.Block) { txn := &b.Transactions[0] txn.StorageProofs = append(txn.StorageProofs, types.StorageProof{}) @@ -713,8 +769,8 @@ func TestValidateBlock(t *testing.T) { } findBlockNonce(cs, &corruptBlock) - if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil { - t.Fatalf("accepted block with %v", test.desc) + if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil || !strings.Contains(err.Error(), test.errString) { + t.Fatalf("expected error containing %q, got %v", test.errString, err) } } } @@ -963,23 +1019,23 @@ func TestValidateV2Block(t *testing.T) { { tests := []struct { - desc string - corrupt func(*types.Block) + errString string + corrupt func(*types.Block) }{ { - "v1 transaction after v2 hardfork", + "v1 transactions are not allowed after v2 hardfork", func(b *types.Block) { b.Transactions = []types.Transaction{{}} }, }, { - "block height that does not increment parent height", + "block height does not increment parent height", func(b *types.Block) { b.V2.Height = 0 }, }, { - "weight that exceeds the limit", + "block exceeds maximum weight", func(b *types.Block) { data := make([]byte, cs.MaxBlockWeight()) b.V2.Transactions = append(b.V2.Transactions, types.V2Transaction{ @@ -988,7 +1044,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "empty v2 transaction", + "transactions cannot be empty", func(b *types.Block) { b.V2.Transactions = append(b.V2.Transactions, types.V2Transaction{}) }, @@ -1000,19 +1056,19 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "wrong timestamp", + "block has timestamp too far in the past", func(b *types.Block) { b.Timestamp = cs.PrevTimestamps[0].AddDate(-1, 0, 0) }, }, { - "no miner payout", + "must have exactly one miner payout", func(b *types.Block) { b.MinerPayouts = nil }, }, { - "zero miner payout", + "miner payout has zero value", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -1021,7 +1077,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "incorrect miner payout", + "miner payout sum (150 KS) does not match block reward + fees (300.001 KS)", func(b *types.Block) { b.MinerPayouts = []types.SiacoinOutput{{ Address: types.VoidAddress, @@ -1030,7 +1086,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "zero-valued SiacoinOutput", + "siacoin output 0 has zero value", func(b *types.Block) { txn := &b.V2.Transactions[0] for i := range txn.SiacoinOutputs { @@ -1041,7 +1097,7 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "zero-valued SiafundOutput", + "siafund output 0 has zero value", func(b *types.Block) { txn := &b.V2.Transactions[0] for i := range txn.SiafundOutputs { @@ -1051,77 +1107,77 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "zero-valued MinerFee", + "miner payout sum (300.001 KS) does not match block reward + fees (300 KS)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.MinerFee = types.ZeroCurrency }, }, { - "overflowing MinerFees", + "v2 transaction fees overflow", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.MinerFee = types.MaxCurrency }, }, { - "siacoin outputs exceed inputs", + "siacoin inputs (100 SC) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.NewCurrency64(1)) }, }, { - "siacoin outputs less than inputs", + "siacoin inputs (100 SC) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Sub(types.NewCurrency64(1)) }, }, { - "siafund outputs exceed inputs", + "siafund inputs (100 SF) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundOutputs[0].Value++ }, }, { - "siafund outputs less than inputs", + "siafund inputs (100 SF) do not equal outputs", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundOutputs[0].Value-- }, }, { - "two of the same siacoin input", + "siacoin input 1 double-spends parent output", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinInputs = append(txn.SiacoinInputs, txn.SiacoinInputs[0]) }, }, { - "two of the same siafund input", + "siafund input 1 double-spends parent output", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundInputs = append(txn.SiafundInputs, txn.SiafundInputs[0]) }, }, { - "siacoin input claiming incorrect policy", + "siacoin input 0 claims incorrect policy", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinInputs[0].SatisfiedPolicy.Policy = types.AnyoneCanSpend() }, }, { - "siafund input claiming incorrect policy", + "siafund input 0 claims incorrect policy", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundInputs[0].SatisfiedPolicy.Policy = types.AnyoneCanSpend() }, }, { - "invalid FoundationAddressUpdate", + "transaction changes Foundation address, but does not spend an input controlled by current address", func(b *types.Block) { txn := &b.V2.Transactions[0] addr := types.VoidAddress @@ -1129,35 +1185,28 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "revision that resolves contract", - func(b *types.Block) { - txn := &b.V2.Transactions[0] - txn.FileContractRevisions[0].Revision.RevisionNumber = types.MaxRevisionNumber - }, - }, - { - "revision with window that starts in past", + "file contract revision 0 has proof height (0) that has already passed", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.ProofHeight = cs.Index.Height }, }, { - "revision with window that ends before it begins", + "file contract revision 0 leaves no time between proof height (20) and expiration height (20)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.ExpirationHeight = txn.FileContractRevisions[0].Revision.ProofHeight }, }, { - "revision with lower revision number than its parent", + "file contract revision 0 does not increase revision number (0 -> 0)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.RevisionNumber = 0 }, }, { - "misordered revisions", + "file contract revision 0 does not increase revision number (100 -> 99)", func(b *types.Block) { // create a revision b.V2.Transactions[0].FileContractRevisions[0].Revision.RevisionNumber = 100 @@ -1175,14 +1224,14 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "revision having different valid payout sum", + "file contract revision 0 modifies output sum (2 SC -> 3 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractRevisions[0].Revision.HostOutput.Value = txn.FileContractRevisions[0].Revision.HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "conflicting revisions in same transaction", + fmt.Sprintf("file contract revision 1 parent (%v) has already been revised", fces[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] newRevision := txn.FileContractRevisions[0] @@ -1191,28 +1240,28 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "window that starts in the past", + "file contract 0 has proof height (0) that has already passed", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].ProofHeight = 0 }, }, { - "window that ends before it begins", + "file contract 0 leaves no time between proof height (30) and expiration height (30)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].ProofHeight = txn.FileContracts[0].ExpirationHeight }, }, { - "valid payout that does not equal missed payout", + "siacoin inputs (100 SC) do not equal outputs (101.04 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].HostOutput.Value = txn.FileContracts[0].HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "incorrect payout tax", + "siacoin inputs (100 SC) do not equal outputs (101 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinOutputs[0].Value = txn.SiacoinOutputs[0].Value.Add(types.Siacoins(1)) @@ -1220,49 +1269,49 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "missed host value exceeding valid host value", + "file contract 0 has missed host value (2 SC) exceeding valid host value (1 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].MissedHostValue = txn.FileContracts[0].HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "total collateral exceeding valid host value", + "file contract 0 has total collateral (2 SC) exceeding valid host value (1 SC)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContracts[0].TotalCollateral = txn.FileContracts[0].HostOutput.Value.Add(types.Siacoins(1)) }, }, { - "spends siacoin output not in accumulator", + fmt.Sprintf("siacoin input 0 spends output (%v) not present in the accumulator", sces[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] - txn.SiacoinInputs[0].Parent.ID[0] ^= 255 + txn.SiacoinInputs[0].Parent.StateElement.LeafIndex ^= 1 }, }, { - "spends siafund output not in accumulator", + fmt.Sprintf("siafund input 0 spends output (%v) not present in the accumulator", sfes[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] - txn.SiafundInputs[0].Parent.ID[0] ^= 255 + txn.SiafundInputs[0].Parent.StateElement.LeafIndex ^= 1 }, }, { - "superfluous siacoin spend policy preimage(s)", + "siacoin input 0 failed to satisfy spend policy: superfluous preimage(s)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiacoinInputs[0].SatisfiedPolicy.Preimages = [][32]byte{{1}} }, }, { - "superfluous siafund spend policy preimage(s)", + "siafund input 0 failed to satisfy spend policy: superfluous preimage(s)", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.SiafundInputs[0].SatisfiedPolicy.Preimages = [][32]byte{{1}} }, }, { - "transaction both resolves a file contract and creates new outputs", + fmt.Sprintf("file contract renewal 0 parent (%v) has already been revised by contract revision", fces[0].ID), func(b *types.Block) { txn := &b.V2.Transactions[0] txn.FileContractResolutions = append(txn.FileContractResolutions, types.V2FileContractResolution{ @@ -1272,14 +1321,14 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "attestation with an empty key", + "attestation 0 has empty key", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.Attestations = append(txn.Attestations, types.Attestation{}) }, }, { - "attestation with invalid signature", + "attestation 0 has invalid signature", func(b *types.Block) { txn := &b.V2.Transactions[0] txn.Attestations = append(txn.Attestations, types.Attestation{ @@ -1298,8 +1347,8 @@ func TestValidateV2Block(t *testing.T) { } findBlockNonce(cs, &corruptBlock) - if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil { - t.Fatalf("accepted block with %v", test.desc) + if err := ValidateBlock(cs, corruptBlock, db.supplementTipBlock(corruptBlock)); err == nil || !strings.Contains(err.Error(), test.errString) { + t.Fatalf("expected error containing %q, got %v", test.errString, err) } } } @@ -1453,26 +1502,6 @@ func TestValidateV2Block(t *testing.T) { }} }, }, - { - "file contract renewal that does not finalize old contract", - func(b *types.Block) { - txn := &b.V2.Transactions[0] - txn.SiacoinInputs = []types.V2SiacoinInput{{ - Parent: sces[1], - SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy}, - }} - - rev := testFces[0].V2FileContract - resolution := types.V2FileContractRenewal{ - FinalRevision: rev, - NewContract: testFces[0].V2FileContract, - } - txn.FileContractResolutions = []types.V2FileContractResolution{{ - Parent: testFces[0], - Resolution: &resolution, - }} - }, - }, { "file contract renewal with invalid final revision", func(b *types.Block) { @@ -1482,12 +1511,9 @@ func TestValidateV2Block(t *testing.T) { SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy}, }} - rev := testFces[0].V2FileContract - rev.RevisionNumber = types.MaxRevisionNumber - rev.TotalCollateral = types.ZeroCurrency resolution := types.V2FileContractRenewal{ - FinalRevision: rev, - NewContract: testFces[0].V2FileContract, + FinalRenterOutput: types.SiacoinOutput{Value: types.Siacoins(1e6)}, + NewContract: testFces[0].V2FileContract, } txn.FileContractResolutions = []types.V2FileContractResolution{{ Parent: testFces[0], @@ -1506,11 +1532,10 @@ func TestValidateV2Block(t *testing.T) { rev := testFces[0].V2FileContract rev.ExpirationHeight = rev.ProofHeight - finalRev := testFces[0].V2FileContract - finalRev.RevisionNumber = types.MaxRevisionNumber resolution := types.V2FileContractRenewal{ - FinalRevision: finalRev, - NewContract: rev, + FinalRenterOutput: rev.RenterOutput, + FinalHostOutput: rev.HostOutput, + NewContract: rev, } txn.FileContractResolutions = []types.V2FileContractResolution{{ Parent: testFces[0], @@ -1885,7 +1910,6 @@ func TestV2RenewalResolution(t *testing.T) { tests := []struct { desc string renewFn func(*types.V2Transaction) - errors bool errString string }{ { @@ -1896,6 +1920,7 @@ func TestV2RenewalResolution(t *testing.T) { desc: "valid renewal - no renter rollover", renewFn: func(txn *types.V2Transaction) { renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalRenterOutput.Value = renewal.RenterRollover 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)) @@ -1905,6 +1930,7 @@ func TestV2RenewalResolution(t *testing.T) { desc: "valid renewal - no host rollover", renewFn: func(txn *types.V2Transaction) { renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalHostOutput.Value = renewal.HostRollover 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)) @@ -1914,19 +1940,123 @@ func TestV2RenewalResolution(t *testing.T) { 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) + partial := renewal.NewContract.MissedHostValue.Div64(2) + renewal.FinalHostOutput.Value = partial + renewal.HostRollover = renewal.HostRollover.Sub(partial) // 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)) + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(partial).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) + partial := renewal.NewContract.RenterOutput.Value.Div64(2) + renewal.FinalRenterOutput.Value = partial + renewal.RenterRollover = renewal.RenterRollover.Sub(partial) // 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)) + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(partial).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "valid renewal - changed host payout", + renewFn: func(txn *types.V2Transaction) { + // transfers part of the renter payout to the host + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalHostOutput.Value = renewal.HostRollover + renewal.HostRollover = types.ZeroCurrency + renewal.FinalRenterOutput.Value = renewal.RenterRollover + renewal.RenterRollover = types.ZeroCurrency + partial := renewal.FinalRenterOutput.Value.Div64(2) + renewal.FinalRenterOutput.Value = partial + renewal.FinalHostOutput.Value = renewal.FinalHostOutput.Value.Add(partial) + // subtract the cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value).Sub(renewal.NewContract.HostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "valid renewal - changed renter payout", + renewFn: func(txn *types.V2Transaction) { + // transfers part of the host payout to the renter + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalHostOutput.Value = renewal.HostRollover + renewal.HostRollover = types.ZeroCurrency + renewal.FinalRenterOutput.Value = renewal.RenterRollover + renewal.RenterRollover = types.ZeroCurrency + partial := renewal.FinalHostOutput.Value.Div64(2) + renewal.FinalRenterOutput.Value = partial + renewal.FinalRenterOutput.Value = renewal.FinalRenterOutput.Value.Add(partial) + // subtract the cost from the change output + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.NewContract.RenterOutput.Value).Sub(renewal.NewContract.HostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + }, + { + desc: "invalid renewal - total payout exceeding parent", + renewFn: func(txn *types.V2Transaction) { + // transfers part of the renter payout to the host + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalRenterOutput.Value = renewal.FinalRenterOutput.Value.Add(types.Siacoins(1)) + }, + errString: "does not match existing contract payout", + }, + { + desc: "invalid renewal - total payout less than parent", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.RenterRollover = renewal.RenterRollover.Sub(types.Siacoins(1)) + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(types.Siacoins(1)).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + errString: "does not match existing contract payout", + }, + { + desc: "invalid renewal - total payout less than parent - no rollover", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.FinalRenterOutput.Value = renewal.RenterRollover.Sub(types.Siacoins(1)) + renewal.FinalHostOutput.Value = renewal.HostRollover + renewal.RenterRollover = types.ZeroCurrency + renewal.HostRollover = types.ZeroCurrency + txn.SiacoinOutputs[0].Value = txn.SiacoinInputs[0].Parent.SiacoinOutput.Value.Sub(renewal.FinalRenterOutput.Value).Sub(renewal.FinalHostOutput.Value).Sub(cs.V2FileContractTax(renewal.NewContract)) + }, + errString: "siacoin inputs (1 KS) do not equal outputs (1.001 KS)", // this is an inputs != outputs error because the renewal is validated there first + }, + { + desc: "invalid renewal - bad new contract renter signature", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.RenterSignature[0] ^= 1 + }, + errString: "invalid renter signature", + }, + { + desc: "invalid renewal - bad new contract host signature", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + renewal.NewContract.HostSignature[0] ^= 1 + }, + errString: "invalid host signature", + }, + { + desc: "invalid renewal - different host key", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + sk := types.GeneratePrivateKey() + renewal.NewContract.HostPublicKey = sk.PublicKey() + contractSigHash := cs.ContractSigHash(renewal.NewContract) + renewal.NewContract.HostSignature = sk.SignHash(contractSigHash) }, + errString: "changes host public key", + }, + { + desc: "invalid renewal - different renter key", + renewFn: func(txn *types.V2Transaction) { + renewal := txn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) + sk := types.GeneratePrivateKey() + renewal.NewContract.RenterPublicKey = sk.PublicKey() + contractSigHash := cs.ContractSigHash(renewal.NewContract) + renewal.NewContract.RenterSignature = sk.SignHash(contractSigHash) + }, + errString: "changes renter public key", }, { desc: "invalid renewal - not enough host funds", @@ -1935,7 +2065,6 @@ func TestV2RenewalResolution(t *testing.T) { renewal.HostRollover = renewal.NewContract.MissedHostValue.Div64(2) // do not adjust the change output }, - errors: true, errString: "do not equal outputs", }, { @@ -1945,7 +2074,6 @@ func TestV2RenewalResolution(t *testing.T) { renewal.RenterRollover = renewal.NewContract.RenterOutput.Value.Div64(2) // do not adjust the change output }, - errors: true, errString: "do not equal outputs", }, { @@ -1963,7 +2091,6 @@ func TestV2RenewalResolution(t *testing.T) { 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", }, { @@ -1980,18 +2107,12 @@ func TestV2RenewalResolution(t *testing.T) { 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{ + newContract := types.V2FileContract{ ProofHeight: 100, ExpirationHeight: 150, RenterPublicKey: pk.PublicKey(), @@ -2004,30 +2125,30 @@ func TestV2RenewalResolution(t *testing.T) { }, MissedHostValue: types.Siacoins(10), } - tax := cs.V2FileContractTax(fc) + newContract.RenterSignature = pk.SignHash(cs.ContractSigHash(newContract)) + newContract.HostSignature = pk.SignHash(cs.ContractSigHash(newContract)) + renewTxn := types.V2Transaction{ - FileContractResolutions: []types.V2FileContractResolution{ - { - Parent: fces[contractID], - Resolution: &types.V2FileContractRenewal{ - FinalRevision: finalRevision, - NewContract: fc, - RenterRollover: types.Siacoins(10), - HostRollover: types.Siacoins(10), - }, + FileContractResolutions: []types.V2FileContractResolution{{ + Parent: fces[contractID], + Resolution: &types.V2FileContractRenewal{ + FinalRenterOutput: types.SiacoinOutput{Address: fc.RenterOutput.Address, Value: types.ZeroCurrency}, + FinalHostOutput: types.SiacoinOutput{Address: fc.HostOutput.Address, Value: types.ZeroCurrency}, + NewContract: newContract, + RenterRollover: types.Siacoins(10), + HostRollover: types.Siacoins(10), }, - }, - SiacoinInputs: []types.V2SiacoinInput{ - { - Parent: genesisOutput, - SatisfiedPolicy: types.SatisfiedPolicy{ - Policy: types.AnyoneCanSpend(), - }, + }}, + SiacoinInputs: []types.V2SiacoinInput{{ + Parent: genesisOutput, + SatisfiedPolicy: types.SatisfiedPolicy{ + Policy: types.AnyoneCanSpend(), }, - }, - SiacoinOutputs: []types.SiacoinOutput{ - {Address: addr, Value: genesisOutput.SiacoinOutput.Value.Sub(tax)}, - }, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr, + Value: genesisOutput.SiacoinOutput.Value.Sub(cs.V2FileContractTax(newContract)), + }}, } resolution, ok := renewTxn.FileContractResolutions[0].Resolution.(*types.V2FileContractRenewal) if !ok { @@ -2045,13 +2166,13 @@ func TestV2RenewalResolution(t *testing.T) { ms := NewMidState(cs) err := ValidateV2Transaction(ms, renewTxn) switch { - case test.errors && err == nil: + case test.errString != "" && err == nil: t.Fatal("expected error") - case test.errors && test.errString == "": + case test.errString != "" && test.errString == "": t.Fatalf("received error %q, missing error string to compare", err) - case test.errors && !strings.Contains(err.Error(), test.errString): + case test.errString != "" && !strings.Contains(err.Error(), test.errString): t.Fatalf("expected error %q to contain %q", err, test.errString) - case !test.errors && err != nil: + case test.errString == "" && err != nil: t.Fatalf("unexpected error: %q", err) } }) diff --git a/gateway/encoding.go b/gateway/encoding.go index 31b44fa8..997029e1 100644 --- a/gateway/encoding.go +++ b/gateway/encoding.go @@ -53,36 +53,6 @@ func (h *Header) decodeFrom(d *types.Decoder) { h.NetAddress = d.ReadString() } -func (h *BlockHeader) encodeTo(e *types.Encoder) { - h.ParentID.EncodeTo(e) - e.WriteUint64(h.Nonce) - e.WriteTime(h.Timestamp) - h.MerkleRoot.EncodeTo(e) -} - -func (h *BlockHeader) decodeFrom(d *types.Decoder) { - h.ParentID.DecodeFrom(d) - h.Nonce = d.ReadUint64() - h.Timestamp = d.ReadTime() - h.MerkleRoot.DecodeFrom(d) -} - -func (h *V2BlockHeader) encodeTo(e *types.Encoder) { - h.Parent.EncodeTo(e) - e.WriteUint64(h.Nonce) - e.WriteTime(h.Timestamp) - h.TransactionsRoot.EncodeTo(e) - h.MinerAddress.EncodeTo(e) -} - -func (h *V2BlockHeader) decodeFrom(d *types.Decoder) { - h.Parent.DecodeFrom(d) - h.Nonce = d.ReadUint64() - h.Timestamp = d.ReadTime() - h.TransactionsRoot.DecodeFrom(d) - h.MinerAddress.DecodeFrom(d) -} - func (ob *V2BlockOutline) encodeTo(e *types.Encoder) { e.WriteUint64(ob.Height) ob.ParentID.EncodeTo(e) @@ -261,12 +231,12 @@ func (r *RPCSendBlk) maxResponseLen() int { return 5e6 } // RPCRelayHeader relays a header. type RPCRelayHeader struct { - Header BlockHeader + Header types.BlockHeader emptyResponse } -func (r *RPCRelayHeader) encodeRequest(e *types.Encoder) { r.Header.encodeTo(e) } -func (r *RPCRelayHeader) decodeRequest(d *types.Decoder) { r.Header.decodeFrom(d) } +func (r *RPCRelayHeader) encodeRequest(e *types.Encoder) { r.Header.EncodeTo(e) } +func (r *RPCRelayHeader) decodeRequest(d *types.Decoder) { r.Header.DecodeFrom(d) } func (r *RPCRelayHeader) maxRequestLen() int { return 32 + 8 + 8 + 32 } // RPCRelayTransactionSet relays a transaction set. @@ -364,13 +334,13 @@ func (r *RPCSendCheckpoint) maxResponseLen() int { return 5e6 + 4e3 } // RPCRelayV2Header relays a v2 block header. type RPCRelayV2Header struct { - Header V2BlockHeader + Header types.BlockHeader emptyResponse } -func (r *RPCRelayV2Header) encodeRequest(e *types.Encoder) { r.Header.encodeTo(e) } -func (r *RPCRelayV2Header) decodeRequest(d *types.Decoder) { r.Header.decodeFrom(d) } -func (r *RPCRelayV2Header) maxRequestLen() int { return 8 + 32 + 8 + 8 + 32 + 32 } +func (r *RPCRelayV2Header) encodeRequest(e *types.Encoder) { r.Header.EncodeTo(e) } +func (r *RPCRelayV2Header) decodeRequest(d *types.Decoder) { r.Header.DecodeFrom(d) } +func (r *RPCRelayV2Header) maxRequestLen() int { return 8 + 32 + 32 + 8 } // RPCRelayV2BlockOutline relays a v2 block outline. type RPCRelayV2BlockOutline struct { diff --git a/gateway/outline.go b/gateway/outline.go index 9bf9873c..15ab8b1a 100644 --- a/gateway/outline.go +++ b/gateway/outline.go @@ -1,7 +1,6 @@ package gateway import ( - "encoding/binary" "time" "go.sia.tech/core/consensus" @@ -9,42 +8,6 @@ import ( "go.sia.tech/core/types" ) -// A BlockHeader contains a Block's non-transaction data. -type BlockHeader struct { - ParentID types.BlockID - Nonce uint64 - Timestamp time.Time - MerkleRoot types.Hash256 -} - -// ID returns a hash that uniquely identifies the block. -func (h BlockHeader) ID() types.BlockID { - buf := make([]byte, 32+8+8+32) - copy(buf[:32], h.ParentID[:]) - binary.LittleEndian.PutUint64(buf[32:], h.Nonce) - binary.LittleEndian.PutUint64(buf[40:], uint64(h.Timestamp.Unix())) - copy(buf[48:], h.MerkleRoot[:]) - return types.BlockID(types.HashBytes(buf)) -} - -// A V2BlockHeader contains a V2Block's non-transaction data. -type V2BlockHeader struct { - Parent types.ChainIndex - Nonce uint64 - Timestamp time.Time - TransactionsRoot types.Hash256 - MinerAddress types.Address -} - -// ID returns a hash that uniquely identifies the block. -func (h V2BlockHeader) ID(cs consensus.State) types.BlockID { - return (&types.Block{ - Nonce: h.Nonce, - Timestamp: h.Timestamp, - V2: &types.V2BlockData{Commitment: cs.Commitment(h.TransactionsRoot, h.MinerAddress)}, - }).ID() -} - // An OutlineTransaction identifies a transaction by its full hash. The actual // transaction data may or may not be present. type OutlineTransaction struct { @@ -75,11 +38,12 @@ func (bo V2BlockOutline) commitment(cs consensus.State) types.Hash256 { // ID returns a hash that uniquely identifies the block. func (bo V2BlockOutline) ID(cs consensus.State) types.BlockID { - return (&types.Block{ - Nonce: bo.Nonce, - Timestamp: bo.Timestamp, - V2: &types.V2BlockData{Commitment: bo.commitment(cs)}, - }).ID() + return types.BlockHeader{ + ParentID: bo.ParentID, + Nonce: bo.Nonce, + Timestamp: bo.Timestamp, + Commitment: bo.commitment(cs), + }.ID() } // Missing returns the hashes of transactions that are missing from the block. diff --git a/go.mod b/go.mod index b30a5029..e0b0b2b0 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module go.sia.tech/core +module go.sia.tech/core // v0.9.0 go 1.23.1 @@ -6,7 +6,7 @@ toolchain go1.23.2 require ( go.sia.tech/mux v1.3.0 - golang.org/x/crypto v0.29.0 - golang.org/x/sys v0.27.0 + golang.org/x/crypto v0.31.0 + golang.org/x/sys v0.28.0 lukechampine.com/frand v1.5.1 ) diff --git a/go.sum b/go.sum index 5fffbe42..812a6b98 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= go.sia.tech/mux v1.3.0/go.mod h1:I46++RD4beqA3cW9Xm9SwXbezwPqLvHhVs9HLpDtt58= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q= diff --git a/knope.toml b/knope.toml new file mode 100644 index 00000000..2c1029f4 --- /dev/null +++ b/knope.toml @@ -0,0 +1,54 @@ +[package] +changelog = "CHANGELOG.md" +versioned_files = ["go.mod"] +ignore_go_major_versioning = true + +[[workflows]] +name = "document-change" + +[[workflows.steps]] +type = "CreateChangeFile" + +[[workflows]] +name = "prepare-release" + +[[workflows.steps]] +type = "Command" +command = "git switch -c release" + +[[workflows.steps]] +type = "PrepareRelease" + +[[workflows.steps]] +type = "Command" +command = "git commit -m \"chore: prepare release $version\"" +variables = { "$version" = "Version" } + +[[workflows.steps]] +type = "Command" +command = "git push --force --set-upstream origin release" + +[workflows.steps.variables] +"$version" = "Version" + +[[workflows.steps]] +type = "CreatePullRequest" +base = "master" + +[workflows.steps.title] +template = "chore: prepare release $version" +variables = { "$version" = "Version" } + +[workflows.steps.body] +template = "This PR was created automatically. Merging it will create a new release for $version\n\n$changelog" +variables = { "$changelog" = "ChangelogEntry", "$version" = "Version" } + +[[workflows]] +name = "release" + +[[workflows.steps]] +type = "Release" + +[github] +owner = "SiaFoundation" +repo = "core" diff --git a/rhp/v4/encoding.go b/rhp/v4/encoding.go index 720babaa..6205da50 100644 --- a/rhp/v4/encoding.go +++ b/rhp/v4/encoding.go @@ -77,12 +77,14 @@ func (a Account) EncodeTo(e *types.Encoder) { e.Write(a[:]) } func (a *Account) DecodeFrom(d *types.Decoder) { d.Read(a[:]) } func (at AccountToken) encodeTo(e *types.Encoder) { + at.HostKey.EncodeTo(e) at.Account.EncodeTo(e) e.WriteTime(at.ValidUntil) at.Signature.EncodeTo(e) } func (at *AccountToken) decodeFrom(d *types.Decoder) { + at.HostKey.DecodeFrom(d) at.Account.DecodeFrom(d) at.ValidUntil = d.ReadTime() at.Signature.DecodeFrom(d) @@ -263,10 +265,12 @@ func (r *RPCRenewContractResponse) maxLen() int { func (r *RPCRenewContractSecondResponse) encodeTo(e *types.Encoder) { r.RenterRenewalSignature.EncodeTo(e) + r.RenterContractSignature.EncodeTo(e) types.EncodeSlice(e, r.RenterSatisfiedPolicies) } func (r *RPCRenewContractSecondResponse) decodeFrom(d *types.Decoder) { r.RenterRenewalSignature.DecodeFrom(d) + r.RenterContractSignature.DecodeFrom(d) types.DecodeSlice(d, &r.RenterSatisfiedPolicies) } func (r *RPCRenewContractSecondResponse) maxLen() int { @@ -331,10 +335,12 @@ func (r *RPCRefreshContractResponse) maxLen() int { func (r *RPCRefreshContractSecondResponse) encodeTo(e *types.Encoder) { r.RenterRenewalSignature.EncodeTo(e) + r.RenterContractSignature.EncodeTo(e) types.EncodeSlice(e, r.RenterSatisfiedPolicies) } func (r *RPCRefreshContractSecondResponse) decodeFrom(d *types.Decoder) { r.RenterRenewalSignature.DecodeFrom(d) + r.RenterContractSignature.DecodeFrom(d) types.DecodeSlice(d, &r.RenterSatisfiedPolicies) } func (r *RPCRefreshContractSecondResponse) maxLen() int { @@ -469,9 +475,13 @@ func (r *RPCLatestRevisionRequest) maxLen() int { func (r *RPCLatestRevisionResponse) encodeTo(e *types.Encoder) { r.Contract.EncodeTo(e) + e.WriteBool(r.Revisable) + e.WriteBool(r.Renewed) } func (r *RPCLatestRevisionResponse) decodeFrom(d *types.Decoder) { r.Contract.DecodeFrom(d) + r.Revisable = d.ReadBool() + r.Renewed = d.ReadBool() } func (r *RPCLatestRevisionResponse) maxLen() int { return sizeofContract @@ -510,13 +520,11 @@ func (r *RPCReadSectorResponse) maxLen() int { func (r RPCWriteSectorRequest) encodeTo(e *types.Encoder) { r.Prices.EncodeTo(e) r.Token.encodeTo(e) - e.WriteUint64(r.Duration) e.WriteUint64(r.DataLength) } func (r *RPCWriteSectorRequest) decodeFrom(d *types.Decoder) { r.Prices.DecodeFrom(d) r.Token.decodeFrom(d) - r.Duration = d.ReadUint64() r.DataLength = d.ReadUint64() } func (r *RPCWriteSectorRequest) maxLen() int { diff --git a/rhp/v4/encoding_test.go b/rhp/v4/encoding_test.go new file mode 100644 index 00000000..124b00f8 --- /dev/null +++ b/rhp/v4/encoding_test.go @@ -0,0 +1,49 @@ +package rhp + +import ( + "bytes" + "math" + "reflect" + "testing" + "time" + + "go.sia.tech/core/types" + "lukechampine.com/frand" +) + +type rhpEncodable[T any] interface { + *T + encodeTo(*types.Encoder) + decodeFrom(*types.Decoder) +} + +func testRoundtrip[T any, PT rhpEncodable[T]](a PT) func(t *testing.T) { + return func(t *testing.T) { + buf := bytes.NewBuffer(nil) + enc := types.NewEncoder(buf) + + a.encodeTo(enc) + if err := enc.Flush(); err != nil { + t.Fatal(err) + } + + b := new(T) + dec := types.NewBufDecoder(buf.Bytes()) + PT(b).decodeFrom(dec) + + if !reflect.DeepEqual(a, b) { + t.Log(a) + t.Log(reflect.ValueOf(b).Elem()) + t.Fatal("expected rountrip to match") + } + } +} + +func TestEncodingRoundtrip(t *testing.T) { + t.Run("AccountToken", testRoundtrip(&AccountToken{ + HostKey: frand.Entropy256(), + Account: frand.Entropy256(), + ValidUntil: time.Unix(int64(frand.Intn(math.MaxInt)), 0), + Signature: types.Signature(frand.Bytes(64)), + })) +} diff --git a/rhp/v4/rhp.go b/rhp/v4/rhp.go index 496c5aa6..dc4dd102 100644 --- a/rhp/v4/rhp.go +++ b/rhp/v4/rhp.go @@ -160,18 +160,6 @@ func (hp HostPrices) SigHash() types.Hash256 { 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"` @@ -206,8 +194,21 @@ func (a *Account) UnmarshalText(b []byte) error { return nil } +// Token returns a signed account token authorizing spending from the account on the +// host. +func (a *Account) Token(renterKey types.PrivateKey, hostKey types.PublicKey) AccountToken { + token := AccountToken{ + HostKey: hostKey, + Account: Account(renterKey.PublicKey()), + ValidUntil: time.Now().Add(5 * time.Minute), + } + token.Signature = renterKey.SignHash(token.SigHash()) + return token +} + // An AccountToken authorizes an account action. type AccountToken struct { + HostKey types.PublicKey `json:"hostKey"` Account Account `json:"account"` ValidUntil time.Time `json:"validUntil"` Signature types.Signature `json:"signature"` @@ -216,22 +217,12 @@ type AccountToken struct { // SigHash returns the hash of the account token used for signing. func (at *AccountToken) SigHash() types.Hash256 { h := types.NewHasher() + at.HostKey.EncodeTo(h.E) 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) { @@ -310,6 +301,7 @@ type ( // RPCRefreshContractSecondResponse implements Object. RPCRefreshContractSecondResponse struct { RenterRenewalSignature types.Signature `json:"renterRenewalSignature"` + RenterContractSignature types.Signature `json:"renterContractSignature"` RenterSatisfiedPolicies []types.SatisfiedPolicy `json:"renterSatisfiedPolicies"` } // RPCRefreshContractThirdResponse implements Object. @@ -344,6 +336,7 @@ type ( // RPCRenewContractSecondResponse implements Object. RPCRenewContractSecondResponse struct { RenterRenewalSignature types.Signature `json:"renterRenewalSignature"` + RenterContractSignature types.Signature `json:"renterContractSignature"` RenterSatisfiedPolicies []types.SatisfiedPolicy `json:"renterSatisfiedPolicies"` } // RPCRenewContractThirdResponse implements Object. @@ -380,8 +373,16 @@ type ( ContractID types.FileContractID `json:"contractID"` } // RPCLatestRevisionResponse implements Object. + // + // The `Revisable` field indicates whether the + // host will accept further revisions to the contract. A host will not accept revisions too + // close to the proof window or revisions on contracts that have already been resolved. + // The `Renewed` field indicates whether the contract was renewed. If the contract was + // renewed, the renter can use `FileContractID.V2RenewalID` to get the ID of the new contract. RPCLatestRevisionResponse struct { - Contract types.V2FileContract `json:"contract"` + Contract types.V2FileContract `json:"contract"` + Revisable bool `json:"revisable"` + Renewed bool `json:"renewed"` } // RPCReadSectorRequest implements Object. @@ -425,7 +426,6 @@ type ( RPCWriteSectorRequest struct { Prices HostPrices `json:"prices"` Token AccountToken `json:"token"` - Duration uint64 `json:"duration"` DataLength uint64 `json:"dataLength"` // extended to SectorSize by host } @@ -581,7 +581,8 @@ func ContractCost(cs consensus.State, p HostPrices, fc types.V2FileContract, min // 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) + contractCost := r.NewContract.HostOutput.Value.Sub(r.NewContract.TotalCollateral) // (contract price + storage cost + locked collateral) - locked collateral + renter = r.NewContract.RenterOutput.Value.Add(contractCost).Add(minerFee).Add(cs.V2FileContractTax(r.NewContract)).Sub(r.RenterRollover) host = r.NewContract.TotalCollateral.Sub(r.HostRollover) return } @@ -589,6 +590,9 @@ func RenewalCost(cs consensus.State, p HostPrices, r types.V2FileContractRenewal // 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) + // the calculation is different from renewal because the host's revenue is also rolled into the refresh. + // This calculates the new collateral the host is expected to put up: + // new collateral = (new revenue + existing revenue + new collateral + existing collateral) - new revenue - (existing revenue + existing collateral) host = r.NewContract.HostOutput.Value.Sub(p.ContractPrice).Sub(r.HostRollover) return } @@ -662,12 +666,8 @@ func MinRenterAllowance(hp HostPrices, duration uint64, collateral types.Currenc // 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.FileMerkleRoot = types.Hash256{} - renewal.FinalRevision.RenterSignature = types.Signature{} - renewal.FinalRevision.HostSignature = types.Signature{} + renewal.FinalRenterOutput = fc.RenterOutput + renewal.FinalHostOutput = fc.HostOutput // create the new contract renewal.NewContract = fc @@ -709,6 +709,7 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra } else { renewal.HostRollover = fc.TotalCollateral } + renewal.FinalHostOutput.Value = renewal.FinalHostOutput.Value.Sub(renewal.HostRollover) // if the remaining renter output is greater than the required allowance, // only roll over the new allowance. Otherwise, roll over the remaining @@ -718,6 +719,8 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra } else { renewal.RenterRollover = fc.RenterOutput.Value } + renewal.FinalRenterOutput.Value = renewal.FinalRenterOutput.Value.Sub(renewal.RenterRollover) + return renewal, Usage{ RPC: prices.ContractPrice, Storage: renewal.NewContract.HostOutput.Value.Sub(renewal.NewContract.TotalCollateral).Sub(prices.ContractPrice), @@ -728,12 +731,13 @@ func RenewContract(fc types.V2FileContract, prices HostPrices, rp RPCRenewContra // 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{} + // roll over everything from the existing contract + renewal.FinalRenterOutput = fc.RenterOutput + renewal.FinalHostOutput = fc.HostOutput + renewal.FinalRenterOutput.Value = types.ZeroCurrency + renewal.FinalHostOutput.Value = types.ZeroCurrency + renewal.HostRollover = fc.HostOutput.Value + renewal.RenterRollover = fc.RenterOutput.Value // create the new contract renewal.NewContract = fc @@ -746,12 +750,9 @@ func RefreshContract(fc types.V2FileContract, prices HostPrices, rp RPCRefreshCo 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{ + // Refresh usage is only the contract price since duration is not increased 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 index ae4f0f00..0e21b0eb 100644 --- a/rhp/v4/rhp_test.go +++ b/rhp/v4/rhp_test.go @@ -1,9 +1,12 @@ package rhp import ( + "math" "testing" + "go.sia.tech/core/consensus" "go.sia.tech/core/types" + "lukechampine.com/frand" ) func TestMinRenterAllowance(t *testing.T) { @@ -19,3 +22,255 @@ func TestMinRenterAllowance(t *testing.T) { t.Fatalf("expected %v, got %v", expected, minAllowance) } } + +func TestRenewalCost(t *testing.T) { + const ( + initialProofHeight = 1000 + initialExpiration = initialProofHeight + ProofWindow + + renewalHeight = 150 + extension = 10 + renewalProofHeight = initialProofHeight + extension + renewalExpiration = renewalProofHeight + ProofWindow + renewalDuration = renewalExpiration - renewalHeight + ) + cs := consensus.State{} + prices := HostPrices{ + ContractPrice: types.NewCurrency64(100), + Collateral: types.NewCurrency64(200), + StoragePrice: types.NewCurrency64(300), + IngressPrice: types.NewCurrency64(400), + EgressPrice: types.NewCurrency64(500), + FreeSectorPrice: types.NewCurrency64(600), + } + minerFee := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) + renterKey, hostKey := types.GeneratePrivateKey().PublicKey(), types.GeneratePrivateKey().PublicKey() + + type testCase struct { + Description string + Modify func(*types.V2FileContract, *RPCRenewContractParams) + RenterCost types.Currency + HostCost types.Currency + } + + cases := []testCase{ + { + Description: "empty", + Modify: func(*types.V2FileContract, *RPCRenewContractParams) {}, + RenterCost: prices.ContractPrice, + }, + { + Description: "no storage", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + }, + RenterCost: types.Siacoins(20).Add(prices.ContractPrice), + HostCost: types.Siacoins(10), + }, + { + Description: "no storage - no renter rollover", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + // transfer all of the renter funds to the host so the renter will need to put up the entire allowance + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + RenterCost: types.Siacoins(320).Add(prices.ContractPrice), + HostCost: types.Siacoins(10), + }, + { + Description: "renewed storage - no additional funds", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + }, + RenterCost: prices.ContractPrice.Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the extension + HostCost: types.ZeroCurrency, // collateral lock up is less than rollover + }, + { + Description: "renewed storage - greater capacity", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // add storage + rev.Capacity = SectorSize * 2 + rev.Filesize = SectorSize + }, + RenterCost: prices.ContractPrice.Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the filesize & extension + HostCost: types.ZeroCurrency, // collateral lock up is less than rollover + }, + { + Description: "renewed storage", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + + // adjust the renewal params + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + }, + RenterCost: types.Siacoins(20).Add(prices.ContractPrice).Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the extension + HostCost: types.Siacoins(10).Add(prices.Collateral.Mul64(SectorSize).Mul64(renewalDuration)), // collateral is calculated for the full duration + }, + { + Description: "renewed storage - no renter rollover", + Modify: func(rev *types.V2FileContract, p *RPCRenewContractParams) { + // adjust the renewal params + p.Allowance = rev.RenterOutput.Value.Add(types.Siacoins(20)) + p.Collateral = rev.TotalCollateral.Add(types.Siacoins(10)) + + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + // transfer all the renter funds to the host so the renter will need to put up more allowance + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + RenterCost: types.Siacoins(320).Add(prices.ContractPrice).Add(prices.StoragePrice.Mul64(SectorSize).Mul64(extension)), // storage cost is calculated for just the extension + HostCost: types.Siacoins(10).Add(prices.Collateral.Mul64(SectorSize).Mul64(renewalDuration)), // collateral is calculated for the full duration + }, + } + for _, tc := range cases { + t.Run(tc.Description, func(t *testing.T) { + contract, _ := NewContract(prices, RPCFormContractParams{ + RenterPublicKey: renterKey, + RenterAddress: types.StandardAddress(renterKey), + Allowance: types.Siacoins(300), + Collateral: types.Siacoins(400), + ProofHeight: initialProofHeight, + }, hostKey, types.StandardAddress(hostKey)) + + params := RPCRenewContractParams{ + ProofHeight: renewalProofHeight, + } + tc.Modify(&contract, ¶ms) + + prices.TipHeight = renewalHeight + renewal, _ := RenewContract(contract, prices, params) + tax := cs.V2FileContractTax(renewal.NewContract) + renter, host := RenewalCost(cs, prices, renewal, minerFee) + if !renter.Equals(tc.RenterCost.Add(tax).Add(minerFee)) { + t.Errorf("expected renter cost %v, got %v", tc.RenterCost, renter.Sub(tax).Sub(minerFee)) + } else if !host.Equals(tc.HostCost) { + t.Errorf("expected host cost %v, got %v", tc.HostCost, host) + } + + contractTotal := renewal.NewContract.HostOutput.Value.Add(renewal.NewContract.RenterOutput.Value) + totalCost := renter.Add(host).Add(renewal.HostRollover).Add(renewal.RenterRollover).Sub(tax).Sub(minerFee) + switch { + case !contractTotal.Equals(totalCost): + t.Fatalf("expected contract sum %v, got %v", contractTotal, totalCost) + case contract.Filesize != renewal.NewContract.Filesize: + t.Fatalf("expected contract size %d, got %d", contract.Filesize, renewal.NewContract.Filesize) + case contract.Filesize != renewal.NewContract.Capacity: // renewals reset capacity + t.Fatalf("expected contract capacity %d, got %d", contract.Filesize, renewal.NewContract.Capacity) + } + }) + } +} + +func TestRefreshCost(t *testing.T) { + const initialProofHeight = 1000 + + cs := consensus.State{} + prices := HostPrices{ + ContractPrice: types.NewCurrency64(100), + Collateral: types.NewCurrency64(200), + StoragePrice: types.NewCurrency64(300), + IngressPrice: types.NewCurrency64(400), + EgressPrice: types.NewCurrency64(500), + FreeSectorPrice: types.NewCurrency64(600), + } + renterKey, hostKey := types.GeneratePrivateKey().PublicKey(), types.GeneratePrivateKey().PublicKey() + minerFee := types.NewCurrency64(frand.Uint64n(math.MaxUint64)) + + type testCase struct { + Description string + Modify func(*types.V2FileContract) + } + + cases := []testCase{ + { + Description: "no storage", + Modify: func(rev *types.V2FileContract) {}, + }, + { + Description: "no storage - no renter rollover", + Modify: func(rev *types.V2FileContract) { + // transfer all of the renter funds to the host so the renter rolls over nothing + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + }, + { + Description: "renewed storage", + Modify: func(rev *types.V2FileContract) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + }, + }, + { + Description: "renewed storage - greater capacity", + Modify: func(rev *types.V2FileContract) { + // add storage + rev.Capacity = SectorSize * 4 + rev.Filesize = SectorSize + }, + }, + { + Description: "renewed storage - no renter rollover", + Modify: func(rev *types.V2FileContract) { + // add storage + rev.Capacity = SectorSize + rev.Filesize = SectorSize + // transfer all the renter funds to the host + rev.HostOutput.Value, rev.RenterOutput.Value = rev.HostOutput.Value.Add(rev.RenterOutput.Value), types.ZeroCurrency + }, + }, + } + + // the actual cost to the renter and host should always be the additional allowance and collateral + // on top of the existing contract costs + additionalAllowance, additionalCollateral := types.Siacoins(20), types.Siacoins(10) + renterCost := additionalAllowance.Add(prices.ContractPrice) + hostCost := additionalCollateral + + for _, tc := range cases { + t.Run(tc.Description, func(t *testing.T) { + contract, _ := NewContract(prices, RPCFormContractParams{ + RenterPublicKey: renterKey, + RenterAddress: types.StandardAddress(renterKey), + Allowance: types.Siacoins(300), + Collateral: types.Siacoins(400), + ProofHeight: initialProofHeight, + }, hostKey, types.StandardAddress(hostKey)) + + params := RPCRefreshContractParams{ + Allowance: additionalAllowance, + Collateral: additionalCollateral, + } + tc.Modify(&contract) + + refresh, _ := RefreshContract(contract, prices, params) + tax := cs.V2FileContractTax(refresh.NewContract) + renter, host := RefreshCost(cs, prices, refresh, minerFee) + if !renter.Equals(renterCost.Add(tax).Add(minerFee)) { + t.Errorf("expected renter cost %v, got %v", renterCost, renter.Sub(tax).Sub(minerFee)) + } else if !host.Equals(hostCost) { + t.Errorf("expected host cost %v, got %v", hostCost, host) + } + + contractTotal := refresh.NewContract.HostOutput.Value.Add(refresh.NewContract.RenterOutput.Value) + totalCost := renter.Add(host).Add(refresh.HostRollover).Add(refresh.RenterRollover).Sub(tax).Sub(minerFee) + + switch { + case !contractTotal.Equals(totalCost): + t.Fatalf("expected contract sum %v, got %v", contractTotal, totalCost) + case contract.Filesize != refresh.NewContract.Filesize: + t.Fatalf("expected contract size %d, got %d", contract.Filesize, refresh.NewContract.Filesize) + case contract.Capacity != refresh.NewContract.Capacity: + t.Fatalf("expected contract capacity %d, got %d", contract.Capacity, refresh.NewContract.Capacity) + } + }) + } +} diff --git a/rhp/v4/validation.go b/rhp/v4/validation.go index d3380594..1896195f 100644 --- a/rhp/v4/validation.go +++ b/rhp/v4/validation.go @@ -3,15 +3,42 @@ package rhp import ( "errors" "fmt" + "time" "go.sia.tech/core/types" ) +// 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 +} + +// 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(hostKey types.PublicKey) error { + switch { + case at.HostKey != hostKey: + return NewRPCError(ErrorCodeBadRequest, "host key mismatch") + case time.Now().After(at.ValidUntil): + return NewRPCError(ErrorCodeBadRequest, "account token expired") + case !types.PublicKey(at.Account).VerifyHash(at.SigHash(), at.Signature): + return ErrInvalidSignature + } + return nil +} + // Validate validates a read sector request. -func (req *RPCReadSectorRequest) Validate(pk types.PublicKey) error { - if err := req.Prices.Validate(pk); err != nil { +func (req *RPCReadSectorRequest) Validate(hostKey types.PublicKey) error { + if err := req.Prices.Validate(hostKey); err != nil { return fmt.Errorf("prices are invalid: %w", err) - } else if err := req.Token.Validate(); err != nil { + } else if err := req.Token.Validate(hostKey); err != nil { return fmt.Errorf("token is invalid: %w", err) } switch { @@ -26,17 +53,13 @@ func (req *RPCReadSectorRequest) Validate(pk types.PublicKey) error { } // Validate validates a write sector request. -func (req *RPCWriteSectorRequest) Validate(pk types.PublicKey, maxDuration uint64) error { - if err := req.Prices.Validate(pk); err != nil { +func (req *RPCWriteSectorRequest) Validate(hostKey types.PublicKey) error { + if err := req.Prices.Validate(hostKey); err != nil { return fmt.Errorf("prices are invalid: %w", err) - } else if err := req.Token.Validate(); err != nil { + } else if err := req.Token.Validate(hostKey); 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: @@ -48,11 +71,11 @@ func (req *RPCWriteSectorRequest) Validate(pk types.PublicKey, maxDuration uint6 } // Validate validates a modify sectors request. Signatures are not validated. -func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxActions uint64) error { +func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileContract) 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) + } else if uint64(len(req.Indices)) > MaxSectorBatchSize { + return fmt.Errorf("removing too many sectors at once: %d > %d", len(req.Indices), MaxSectorBatchSize) } seen := make(map[uint64]bool) sectors := fc.Filesize / SectorSize @@ -68,7 +91,7 @@ func (req *RPCFreeSectorsRequest) Validate(pk types.PublicKey, fc types.V2FileCo } // Validate validates a sector roots request. Signatures are not validated. -func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileContract, maxSectors uint64) error { +func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileContract) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } @@ -79,8 +102,8 @@ func (req *RPCSectorRootsRequest) Validate(pk types.PublicKey, fc types.V2FileCo 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) + case req.Length > MaxSectorBatchSize: + return fmt.Errorf("read request range exceeds maximum sectors: %d > %d", req.Length, MaxSectorBatchSize) } return nil } @@ -126,7 +149,7 @@ func (req *RPCFormContractRequest) Validate(pk types.PublicKey, tip types.ChainI } // 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 { +func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.ChainIndex, existingSize uint64, existingProofHeight uint64, maxCollateral types.Currency, maxDuration uint64) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } @@ -148,14 +171,18 @@ func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.Chain // 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) + // collateral is risked for the entire contract duration + riskedCollateral := req.Prices.Collateral.Mul64(existingSize).Mul64(expirationHeight - req.Prices.TipHeight) + // renewals add collateral on top of the required risked collateral + totalCollateral := req.Renewal.Collateral.Add(riskedCollateral) 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 totalCollateral.Cmp(maxCollateral) > 0: + return fmt.Errorf("required collateral %v exceeds max collateral %v", totalCollateral, maxCollateral) case duration > maxDuration: return fmt.Errorf("contract duration %v exceeds max duration %v", duration, maxDuration) case req.Renewal.Allowance.Cmp(minRenterAllowance) < 0: @@ -166,7 +193,7 @@ func (req *RPCRenewContractRequest) Validate(pk types.PublicKey, tip types.Chain } // Validate validates a refresh contract request. Prices are not validated -func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, expirationHeight uint64, maxCollateral types.Currency) error { +func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, existingTotalCollateral types.Currency, expirationHeight uint64, maxCollateral types.Currency) error { if err := req.Prices.Validate(pk); err != nil { return fmt.Errorf("prices are invalid: %w", err) } @@ -184,12 +211,14 @@ func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, expirationHei // 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) + // refreshes add collateral on top of the existing collateral + totalCollateral := req.Refresh.Collateral.Add(existingTotalCollateral) 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 totalCollateral.Cmp(maxCollateral) > 0: + return fmt.Errorf("required collateral %v exceeds max collateral %v", totalCollateral, maxCollateral) case req.Refresh.Allowance.Cmp(minRenterAllowance) < 0: return fmt.Errorf("allowance %v is less than minimum allowance %v", req.Refresh.Allowance, minRenterAllowance) default: @@ -198,10 +227,10 @@ func (req *RPCRefreshContractRequest) Validate(pk types.PublicKey, expirationHei } // Validate checks that the request is valid -func (req *RPCVerifySectorRequest) Validate(pk types.PublicKey) error { - if err := req.Prices.Validate(pk); err != nil { +func (req *RPCVerifySectorRequest) Validate(hostKey types.PublicKey) error { + if err := req.Prices.Validate(hostKey); err != nil { return fmt.Errorf("prices are invalid: %w", err) - } else if err := req.Token.Validate(); err != nil { + } else if err := req.Token.Validate(hostKey); 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) @@ -210,13 +239,13 @@ func (req *RPCVerifySectorRequest) Validate(pk types.PublicKey) error { } // Validate checks that the request is valid -func (req *RPCAppendSectorsRequest) Validate(pk types.PublicKey, maxActions uint64) error { +func (req *RPCAppendSectorsRequest) Validate(pk types.PublicKey) 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) + } else if uint64(len(req.Sectors)) > MaxSectorBatchSize { + return fmt.Errorf("too many sectors to append: %d > %d", len(req.Sectors), MaxSectorBatchSize) } return nil } diff --git a/rhp/v4/validation_test.go b/rhp/v4/validation_test.go new file mode 100644 index 00000000..2bb548cd --- /dev/null +++ b/rhp/v4/validation_test.go @@ -0,0 +1,40 @@ +package rhp + +import ( + "errors" + "strings" + "testing" + "time" + + "go.sia.tech/core/types" + "lukechampine.com/frand" +) + +func TestValidateAccountToken(t *testing.T) { + hostKey := types.GeneratePrivateKey().PublicKey() + renterKey := types.GeneratePrivateKey() + account := Account(renterKey.PublicKey()) + + ac := AccountToken{ + HostKey: hostKey, + Account: account, + ValidUntil: time.Now().Add(-time.Minute), + } + + if err := ac.Validate(frand.Entropy256()); !strings.Contains(err.Error(), "host key mismatch") { + t.Fatalf("expected host key mismatch, got %v", err) + } else if err := ac.Validate(hostKey); !strings.Contains(err.Error(), "token expired") { + t.Fatalf("expected token expired, got %v", err) + } + + ac.ValidUntil = time.Now().Add(time.Minute) + if err := ac.Validate(hostKey); !errors.Is(err, ErrInvalidSignature) { + t.Fatalf("expected ErrInvalidSignature, got %v", err) + } + + ac.Signature = renterKey.SignHash(ac.SigHash()) + + if err := ac.Validate(hostKey); err != nil { + t.Fatal(err) + } +} diff --git a/types/encoding.go b/types/encoding.go index 7a1eb1d0..66868ead 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -125,6 +125,18 @@ func EncodePtr[T any, P interface { } } +// EncodePtrCast encodes a pointer to an object by casting it to V. +func EncodePtrCast[V interface { + Cast() T + EncoderTo +}, T any](e *Encoder, p *T) { + e.WriteBool(p != nil) + if p != nil { + vp := *(*V)(unsafe.Pointer(p)) + vp.EncodeTo(e) + } +} + // EncodeSlice encodes a slice of objects that implement EncoderTo. func EncodeSlice[T EncoderTo](e *Encoder, s []T) { e.WriteUint64(uint64(len(s))) @@ -262,6 +274,22 @@ func DecodePtr[T any, TP interface { } } +// DecodePtrCast decodes a pointer to an object by casting it to V. +func DecodePtrCast[T interface { + Cast() V +}, TP interface { + *T + DecoderFrom +}, V any](d *Decoder, p **V) { + tp := (**T)(unsafe.Pointer(p)) + if d.ReadBool() { + *tp = new(T) + TP(*tp).DecodeFrom(d) + } else { + *tp = nil + } +} + // DecodeSlice decodes a length-prefixed slice of type T, containing values read // from the decoder. func DecodeSlice[T any, DF interface { @@ -683,10 +711,11 @@ func (rev V2FileContractRevision) EncodeTo(e *Encoder) { // EncodeTo implements types.EncoderTo. func (ren V2FileContractRenewal) EncodeTo(e *Encoder) { - ren.FinalRevision.EncodeTo(e) - ren.NewContract.EncodeTo(e) + V2SiacoinOutput(ren.FinalRenterOutput).EncodeTo(e) + V2SiacoinOutput(ren.FinalHostOutput).EncodeTo(e) V2Currency(ren.RenterRollover).EncodeTo(e) V2Currency(ren.HostRollover).EncodeTo(e) + ren.NewContract.EncodeTo(e) ren.RenterSignature.EncodeTo(e) ren.HostSignature.EncodeTo(e) } @@ -833,7 +862,6 @@ func (txn V2TransactionSemantics) EncodeTo(e *Encoder) { renewal := *res nilSigs( &renewal.NewContract.RenterSignature, &renewal.NewContract.HostSignature, - &renewal.FinalRevision.RenterSignature, &renewal.FinalRevision.HostSignature, &renewal.RenterSignature, &renewal.HostSignature, ) fcr.Resolution = &renewal @@ -860,6 +888,14 @@ func (b V2BlockData) EncodeTo(e *Encoder) { V2TransactionsMultiproof(b.Transactions).EncodeTo(e) } +// EncodeTo implements types.EncoderTo. +func (h BlockHeader) EncodeTo(e *Encoder) { + h.ParentID.EncodeTo(e) + e.WriteUint64(h.Nonce) + e.WriteTime(h.Timestamp) + h.Commitment.EncodeTo(e) +} + // V1Block provides v1 encoding for Block. type V1Block Block @@ -1085,10 +1121,7 @@ func (txn *Transaction) DecodeFrom(d *Decoder) { // DecodeFrom implements types.DecoderFrom. func (p *SpendPolicy) DecodeFrom(d *Decoder) { - const ( - version = 1 - maxPolicies = 1024 - ) + const version = 1 const ( opInvalid = iota opAbove @@ -1100,7 +1133,6 @@ func (p *SpendPolicy) DecodeFrom(d *Decoder) { opUnlockConditions ) - var totalPolicies int var readPolicy func() (SpendPolicy, error) readPolicy = func() (SpendPolicy, error) { switch op := d.ReadUint8(); op { @@ -1119,9 +1151,6 @@ func (p *SpendPolicy) DecodeFrom(d *Decoder) { case opThreshold: n := d.ReadUint8() of := make([]SpendPolicy, d.ReadUint8()) - if totalPolicies += len(of); totalPolicies > maxPolicies { - return SpendPolicy{}, errors.New("policy is too complex") - } var err error for i := range of { if of[i], err = readPolicy(); err != nil { @@ -1243,10 +1272,11 @@ func (rev *V2FileContractRevision) DecodeFrom(d *Decoder) { // DecodeFrom implements types.DecoderFrom. func (ren *V2FileContractRenewal) DecodeFrom(d *Decoder) { - ren.FinalRevision.DecodeFrom(d) - ren.NewContract.DecodeFrom(d) + (*V2SiacoinOutput)(&ren.FinalRenterOutput).DecodeFrom(d) + (*V2SiacoinOutput)(&ren.FinalHostOutput).DecodeFrom(d) (*V2Currency)(&ren.RenterRollover).DecodeFrom(d) (*V2Currency)(&ren.HostRollover).DecodeFrom(d) + ren.NewContract.DecodeFrom(d) ren.RenterSignature.DecodeFrom(d) ren.HostSignature.DecodeFrom(d) } @@ -1337,6 +1367,14 @@ func (b *V2BlockData) DecodeFrom(d *Decoder) { (*V2TransactionsMultiproof)(&b.Transactions).DecodeFrom(d) } +// DecodeFrom implements types.DecoderFrom. +func (h *BlockHeader) DecodeFrom(d *Decoder) { + h.ParentID.DecodeFrom(d) + h.Nonce = d.ReadUint64() + h.Timestamp = d.ReadTime() + h.Commitment.DecodeFrom(d) +} + // DecodeFrom implements types.DecoderFrom. func (b *V1Block) DecodeFrom(d *Decoder) { b.ParentID.DecodeFrom(d) diff --git a/types/encoding_test.go b/types/encoding_test.go index 84e670dd..141ac2b7 100644 --- a/types/encoding_test.go +++ b/types/encoding_test.go @@ -11,6 +11,26 @@ import ( "lukechampine.com/frand" ) +func TestEncodePtrCast(t *testing.T) { + var buf bytes.Buffer + e := types.NewEncoder(&buf) + c := types.Siacoins(1) + types.EncodePtrCast[types.V1Currency](e, &c) + types.EncodePtrCast[types.V2Currency](e, &c) + types.EncodePtrCast[types.V2Currency](e, nil) + e.Flush() + var c1, c2, c3 *types.Currency + d := types.NewBufDecoder(buf.Bytes()) + types.DecodePtrCast[types.V1Currency](d, &c1) + types.DecodePtrCast[types.V2Currency](d, &c2) + types.DecodePtrCast[types.V2Currency](d, &c3) + if err := d.Err(); err != nil { + t.Fatal(err) + } else if *c1 != c || *c2 != c || c3 != nil { + t.Fatal("mismatch:", c1, c2, c3) + } +} + func TestEncodeSlice(t *testing.T) { txns := multiproofTxns(10, 10) var buf bytes.Buffer diff --git a/types/policy.go b/types/policy.go index 794dc85e..94629de2 100644 --- a/types/policy.go +++ b/types/policy.go @@ -132,8 +132,9 @@ func (p SpendPolicy) Verify(height uint64, medianTimestamp time.Time, sigHash Ha } return } - errInvalidSignature := errors.New("invalid signature") - errInvalidPreimage := errors.New("invalid preimage") + const maxPolicies = 1024 + var totalPolicies int + errOpaque := errors.New("opaque policy") var verify func(SpendPolicy) error verify = func(p SpendPolicy) error { switch p := p.Type.(type) { @@ -141,38 +142,48 @@ func (p SpendPolicy) Verify(height uint64, medianTimestamp time.Time, sigHash Ha if height >= uint64(p) { return nil } - return fmt.Errorf("height not above %v", uint64(p)) + return fmt.Errorf("height (%v) not above %v", height, uint64(p)) case PolicyTypeAfter: if medianTimestamp.After(time.Time(p)) { return nil } - return fmt.Errorf("median timestamp not after %v", time.Time(p)) + return fmt.Errorf("median timestamp (%v) not after %v", medianTimestamp, time.Time(p)) case PolicyTypePublicKey: if sig, ok := nextSig(); ok && PublicKey(p).VerifyHash(sigHash, sig) { return nil } - return errInvalidSignature + return errors.New("invalid signature") case PolicyTypeHash: if preimage, ok := nextPreimage(); ok && p == sha256.Sum256(preimage[:]) { return nil } - return errInvalidPreimage + return errors.New("invalid preimage") case PolicyTypeThreshold: - for i := 0; i < len(p.Of) && p.N > 0 && len(p.Of[i:]) >= int(p.N); i++ { - if _, ok := p.Of[i].Type.(PolicyTypeUnlockConditions); ok { + if totalPolicies += len(p.Of); totalPolicies > maxPolicies || len(p.Of) > 255 { + return errors.New("policy is too complex") + } + var satisfied uint8 + for _, sp := range p.Of { + switch sp.Type.(type) { + case PolicyTypeUnlockConditions: return errors.New("unlock conditions cannot be sub-policies") - } else if err := verify(p.Of[i]); err == errInvalidSignature || err == errInvalidPreimage { - return err // fatal; should have been opaque - } else if err == nil { - p.N-- + case PolicyTypeOpaque: + continue + default: + if satisfied == p.N { + return errors.New("threshold exceeded") + } else if err := verify(sp); err != nil { + return err // fatal; should have been opaque + } + satisfied++ } } - if p.N == 0 { + if satisfied == p.N { return nil } return errors.New("threshold not reached") case PolicyTypeOpaque: - return errors.New("opaque policy") + return errOpaque case PolicyTypeUnlockConditions: if err := verify(PolicyAbove(p.Timelock)); err != nil { return err diff --git a/types/policy_test.go b/types/policy_test.go index 1ec8179a..b3a5de6e 100644 --- a/types/policy_test.go +++ b/types/policy_test.go @@ -2,9 +2,11 @@ package types import ( "bytes" + "crypto/sha256" "encoding/json" "fmt" "reflect" + "strings" "testing" "time" @@ -35,236 +37,293 @@ func TestPolicyVerify(t *testing.T) { key := GeneratePrivateKey() pk := key.PublicKey() sigHash := Hash256{1, 2, 3} - + currentTime := CurrentTimestamp() for _, test := range []struct { - desc string - p SpendPolicy - height uint64 - sigs []Signature - valid bool + desc string + p SpendPolicy + height uint64 + sigs []Signature + preimages [][32]byte + err string }{ { - "above 0", - PolicyAbove(0), - 0, - nil, - true, + desc: "above 0", + p: PolicyAbove(0), }, { - "below 1", - PolicyAbove(1), - 0, - nil, - false, + desc: "below 1", + p: PolicyAbove(1), + err: "not above 1", }, { - "above 1", - PolicyAbove(1), - 1, - nil, - true, + desc: "above 1", + p: PolicyAbove(1), + height: 1, }, { - "no signature", - PolicyPublicKey(pk), - 1, - nil, - false, + desc: "after now", + p: PolicyAfter(currentTime), + err: "not after", }, { - "invalid signature", - PolicyPublicKey(pk), - 1, - []Signature{key.SignHash(Hash256{})}, - false, + desc: "after before", + p: PolicyAfter(currentTime.Add(-time.Second)), }, { - "valid signature", - PolicyPublicKey(pk), - 1, - []Signature{key.SignHash(sigHash)}, - true, + desc: "opaque", + p: PolicyOpaque(AnyoneCanSpend()), + err: "opaque", }, { - "valid signature, invalid height", - PolicyThreshold(2, []SpendPolicy{ + desc: "no signature", + p: PolicyPublicKey(pk), + err: "invalid signature", + }, + { + desc: "invalid signature", + p: PolicyPublicKey(pk), + sigs: []Signature{key.SignHash(Hash256{})}, + err: "invalid signature", + }, + { + desc: "valid signature", + p: PolicyPublicKey(pk), + sigs: []Signature{key.SignHash(sigHash)}, + }, + { + desc: "invalid preimage", + p: PolicyHash(sha256.Sum256([]byte{31: 1})), + preimages: [][32]byte{{31: 2}}, + err: "invalid preimage", + }, + { + desc: "valid preimage", + p: PolicyHash(sha256.Sum256([]byte{31: 1})), + preimages: [][32]byte{{31: 1}}, + }, + { + desc: "superfluous preimage", + p: PolicyHash(sha256.Sum256([]byte{31: 1})), + preimages: [][32]byte{{31: 1}, {31: 1}}, + err: "superfluous preimage(s)", + }, + { + desc: "valid signature, invalid height", + p: PolicyThreshold(2, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 1, - []Signature{key.SignHash(sigHash)}, - false, + sigs: []Signature{key.SignHash(sigHash)}, + err: "not above 10", }, { - "valid height, invalid signature", - PolicyThreshold(2, []SpendPolicy{ + desc: "valid height, invalid signature", + p: PolicyThreshold(2, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 11, - nil, - false, + height: 11, + err: "invalid signature", }, { - "valid height, valid signature", - PolicyThreshold(2, []SpendPolicy{ + desc: "valid height, valid signature", + p: PolicyThreshold(2, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash)}, - true, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "lower threshold, valid height", - PolicyThreshold(1, []SpendPolicy{ + desc: "lower threshold, valid height", + p: PolicyThreshold(1, []SpendPolicy{ PolicyAbove(10), PolicyOpaque(PolicyPublicKey(pk)), }), - 11, - nil, - true, + height: 11, }, { - "lower threshold, valid signature", - PolicyThreshold(1, []SpendPolicy{ + desc: "lower threshold, valid signature", + p: PolicyThreshold(1, []SpendPolicy{ PolicyOpaque(PolicyAbove(10)), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash)}, - true, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "exceed threshold", - PolicyThreshold(1, []SpendPolicy{ + desc: "exceed threshold", + p: PolicyThreshold(1, []SpendPolicy{ PolicyAbove(10), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash)}, - false, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold exceeded", }, { - "exceed threshold with keys", - PolicyThreshold(1, []SpendPolicy{ + desc: "exceed threshold with keys", + p: PolicyThreshold(1, []SpendPolicy{ PolicyPublicKey(pk), PolicyPublicKey(pk), }), - 11, - []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, - false, + height: 11, + sigs: []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, + err: "threshold exceeded", }, { - "lower threshold, neither valid", - PolicyThreshold(1, []SpendPolicy{ + desc: "exceed threshold with above", + p: PolicyThreshold(1, []SpendPolicy{ + PolicyAbove(10), + PolicyAfter(currentTime.Add(-time.Second)), + }), + height: 11, + err: "threshold exceeded", + }, + { + desc: "opaque above subpolicy", + p: PolicyThreshold(1, []SpendPolicy{ + PolicyOpaque(PolicyAbove(10)), + PolicyAfter(currentTime.Add(-time.Second)), + }), + }, + { + desc: "lower threshold, neither valid", + p: PolicyThreshold(1, []SpendPolicy{ PolicyOpaque(PolicyAbove(10)), PolicyOpaque(PolicyPublicKey(pk)), }), - 11, - []Signature{key.SignHash(sigHash)}, - false, + height: 11, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold not reached", + }, + { + desc: "too many subpolicies", + p: PolicyThreshold(1, make([]SpendPolicy, 256)), + err: "too complex", }, { - "unlock conditions within threshold", - PolicyThreshold(1, []SpendPolicy{ + desc: "too many cumulative subpolicies", + p: PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyThreshold(1, append([]SpendPolicy{ + PolicyAbove(0), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + }, make([]SpendPolicy, 250)...)), + err: "too complex", + }, + { + desc: "unlock conditions within threshold", + p: PolicyThreshold(1, []SpendPolicy{ {PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey()}, SignaturesRequired: 1, }}, }), - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "unlock conditions cannot be sub-policies", }, { - "unlock conditions, invalid height", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, invalid height", + p: SpendPolicy{PolicyTypeUnlockConditions{ Timelock: 10, }}, - 1, - nil, - false, + err: "not above 10", }, { - "unlock conditions, insufficient signatures", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, insufficient signatures", + p: SpendPolicy{PolicyTypeUnlockConditions{ SignaturesRequired: 1000, }}, - 1, - nil, - false, + height: 1, + sigs: nil, + err: "threshold not reached", }, { - "unlock conditions, superfluous signatures", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, superfluous signatures", + p: SpendPolicy{PolicyTypeUnlockConditions{ SignaturesRequired: 0, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "superfluous signature(s)", }, { - "unlock conditions, wrong signature algorithm", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, wrong signature algorithm", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{{ Algorithm: SpecifierEntropy, Key: nil, }}, SignaturesRequired: 1, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "entropy public key", + }, + { + desc: "unlock conditions, unknown signature algorithm", + p: SpendPolicy{PolicyTypeUnlockConditions{ + PublicKeys: []UnlockKey{{ + Algorithm: NewSpecifier("trust me bro"), + }}, + SignaturesRequired: 1, + }}, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "unlock conditions, wrong pubkey", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, wrong pubkey", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{{ Algorithm: SpecifierEd25519, Key: nil, }}, SignaturesRequired: 1, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold not reached", }, { - "unlock conditions, insufficient signatures", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, insufficient signatures", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey()}, SignaturesRequired: 2, }}, - 1, - []Signature{key.SignHash(sigHash)}, - false, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, + err: "threshold not reached", }, { - "unlock conditions, valid", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, valid", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey()}, SignaturesRequired: 1, }}, - 1, - []Signature{key.SignHash(sigHash)}, - true, + height: 1, + sigs: []Signature{key.SignHash(sigHash)}, }, { - "unlock conditions, valid with extra pubkeys", - SpendPolicy{PolicyTypeUnlockConditions{ + desc: "unlock conditions, valid with extra pubkeys", + p: SpendPolicy{PolicyTypeUnlockConditions{ PublicKeys: []UnlockKey{pk.UnlockKey(), PublicKey{1, 2, 3}.UnlockKey(), pk.UnlockKey()}, SignaturesRequired: 2, }}, - 1, - []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, - true, + height: 1, + sigs: []Signature{key.SignHash(sigHash), key.SignHash(sigHash)}, }, } { - if err := test.p.Verify(test.height, time.Time{}, sigHash, test.sigs, nil); err != nil && test.valid { + if err := test.p.Verify(test.height, currentTime, sigHash, test.sigs, test.preimages); test.err == "" && err != nil { t.Fatalf("%v: %v", test.desc, err) - } else if err == nil && !test.valid { - t.Fatal("expected error") + } else if test.err != "" && (err == nil || !strings.Contains(err.Error(), test.err)) { + t.Fatalf("%v: expected error containing %q, got %v", test.desc, test.err, err) } } } diff --git a/types/types.go b/types/types.go index 981721f9..48ab5c49 100644 --- a/types/types.go +++ b/types/types.go @@ -516,25 +516,19 @@ type V2FileContractRevision struct { } // A V2FileContractResolution closes a v2 file contract's payment channel. There -// are four ways a contract can be resolved: +// are three ways a contract can be resolved: // -// 1) The renter can finalize the contract's current state, preventing further -// revisions and immediately creating its outputs. -// -// 2) The renter and host can jointly renew the contract. The old contract is +// 1) The renter and host can jointly renew the contract. The old contract is // finalized, and a portion of its funds are "rolled over" into a new contract. +// Renewals must be submitted prior to the contract's ProofHeight. // -// 3) The host can submit a storage proof, asserting that it has faithfully -// stored the contract data for the agreed-upon duration. Typically, a storage -// proof is only required if the renter is unable or unwilling to sign a -// renewal. A storage proof can only be submitted after the contract's -// ProofHeight; this allows the renter (or host) to broadcast the -// latest contract revision prior to the proof. +// 2) If the renter is unwilling or unable to sign a renewal, the host can +// submit a storage proof, asserting that it has faithfully stored the contract +// data for the agreed-upon duration. Storage proofs must be submitted after the +// contract's ProofHeight and prior to its ExpirationHeight. // -// 4) Lastly, anyone can submit a contract expiration. An expiration can only -// be submitted after the contract's ExpirationHeight; this gives the host a -// reasonable window of time after the ProofHeight in which to submit a storage -// proof. +// 3) Lastly, anyone can submit a contract expiration. An expiration can only be +// submitted after the contract's ExpirationHeight. // // Once a contract has been resolved, it cannot be altered or resolved again. // When a contract is resolved, its RenterOutput and HostOutput are created @@ -561,10 +555,11 @@ func (*V2FileContractExpiration) isV2FileContractResolution() {} // A V2FileContractRenewal renews a file contract. type V2FileContractRenewal struct { - FinalRevision V2FileContract `json:"finalRevision"` - NewContract V2FileContract `json:"newContract"` - RenterRollover Currency `json:"renterRollover"` - HostRollover Currency `json:"hostRollover"` + FinalRenterOutput SiacoinOutput `json:"finalRenterOutput"` + FinalHostOutput SiacoinOutput `json:"finalHostOutput"` + RenterRollover Currency `json:"renterRollover"` + HostRollover Currency `json:"hostRollover"` + NewContract V2FileContract `json:"newContract"` // signatures cover above fields RenterSignature Signature `json:"renterSignature"` @@ -641,7 +636,7 @@ type SiafundElement struct { ID SiafundOutputID `json:"id"` StateElement StateElement `json:"stateElement"` SiafundOutput SiafundOutput `json:"siafundOutput"` - ClaimStart Currency `json:"claimStart"` // value of SiafundPool when element was created + ClaimStart Currency `json:"claimStart"` // value of SiafundTaxRevenue when element was created } // A FileContractElement is a record of a FileContract within the state @@ -804,7 +799,26 @@ type V2BlockData struct { Transactions []V2Transaction `json:"transactions"` } -// A Block is a set of transactions grouped under a header. +// A BlockHeader is the preimage of a Block's ID. +type BlockHeader struct { + ParentID BlockID `json:"parentID"` + Nonce uint64 `json:"nonce"` + Timestamp time.Time `json:"timestamp"` + Commitment Hash256 `json:"commitment"` +} + +// ID returns the hash of the header data. +func (bh BlockHeader) ID() BlockID { + buf := make([]byte, 32+8+8+32) + copy(buf[:32], bh.ParentID[:]) + binary.LittleEndian.PutUint64(buf[32:], bh.Nonce) + binary.LittleEndian.PutUint64(buf[40:], uint64(bh.Timestamp.Unix())) + copy(buf[48:], bh.Commitment[:]) + return BlockID(HashBytes(buf)) +} + +// A Block is a timestamped set of transactions, immutably linked to a previous +// block, secured by proof-of-work. type Block struct { ParentID BlockID `json:"parentID"` Nonce uint64 `json:"nonce"` @@ -815,12 +829,6 @@ type Block struct { V2 *V2BlockData `json:"v2,omitempty"` } -// MerkleRoot returns the Merkle root of the block's miner payouts and -// transactions. -func (b *Block) MerkleRoot() Hash256 { - return blockMerkleRoot(b.MinerPayouts, b.Transactions) -} - // V2Transactions returns the block's v2 transactions, if present. func (b *Block) V2Transactions() []V2Transaction { if b.V2 != nil { @@ -829,20 +837,26 @@ func (b *Block) V2Transactions() []V2Transaction { return nil } -// ID returns a hash that uniquely identifies a block. -func (b *Block) ID() BlockID { - buf := make([]byte, 32+8+8+32) - binary.LittleEndian.PutUint64(buf[32:], b.Nonce) - binary.LittleEndian.PutUint64(buf[40:], uint64(b.Timestamp.Unix())) - if b.V2 != nil { - copy(buf[:32], "sia/id/block|") - copy(buf[48:], b.V2.Commitment[:]) +// Header returns the block's header. +func (b *Block) Header() BlockHeader { + var commitment Hash256 + if b.V2 == nil { + // NOTE: expensive! + commitment = blockMerkleRoot(b.MinerPayouts, b.Transactions) } else { - root := b.MerkleRoot() // NOTE: expensive! - copy(buf[:32], b.ParentID[:]) - copy(buf[48:], root[:]) + commitment = b.V2.Commitment } - return BlockID(HashBytes(buf)) + return BlockHeader{ + ParentID: b.ParentID, + Nonce: b.Nonce, + Timestamp: b.Timestamp, + Commitment: commitment, + } +} + +// ID returns a hash that uniquely identifies a block. +func (b *Block) ID() BlockID { + return b.Header().ID() } func unmarshalHex(dst []byte, data []byte) error {