diff --git a/consensus/update_test.go b/consensus/update_test.go index 01cccf94..948ca8f7 100644 --- a/consensus/update_test.go +++ b/consensus/update_test.go @@ -83,7 +83,11 @@ func TestApplyBlock(t *testing.T) { if err = consensus.ValidateBlock(cs, b, bs); err != nil { return } - cs, au = consensus.ApplyBlock(cs, b, bs, ancestorTimestamp(dbStore, b.ParentID, cs.AncestorDepth())) + prev := cs + cs, au = consensus.ApplyBlock(prev, b, bs, ancestorTimestamp(dbStore, b.ParentID, cs.AncestorDepth())) + dbStore.ApplyBlock(prev, au, true) + dbStore.AddBlock(b, &bs) + dbStore.AddState(cs) return } checkUpdateElements := func(au consensus.ApplyUpdate, addedSCEs, spentSCEs []types.SiacoinElement, addedSFEs, spentSFEs []types.SiafundElement) { @@ -123,6 +127,43 @@ func TestApplyBlock(t *testing.T) { t.Fatal("extraneous elements") } } + checkRevertElements := func(ru consensus.RevertUpdate, addedSCEs, spentSCEs []types.SiacoinElement, addedSFEs, spentSFEs []types.SiafundElement) { + ru.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { + sces := &addedSCEs + if spent { + sces = &spentSCEs + } + if len(*sces) == 0 { + t.Fatal("unexpected spent siacoin element") + } + sce.StateElement = types.StateElement{} + if !reflect.DeepEqual(sce, (*sces)[len(*sces)-1]) { + js1, _ := json.MarshalIndent(sce, "", " ") + js2, _ := json.MarshalIndent((*sces)[len(*sces)-1], "", " ") + t.Fatalf("siacoin element doesn't match:\n%s\nvs\n%s\n", js1, js2) + } + *sces = (*sces)[:len(*sces)-1] + }) + ru.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { + sfes := &addedSFEs + if spent { + sfes = &spentSFEs + } + if len(*sfes) == 0 { + t.Fatal("unexpected spent siafund element") + } + sfe.StateElement = types.StateElement{} + if !reflect.DeepEqual(sfe, (*sfes)[len(*sfes)-1]) { + js1, _ := json.MarshalIndent(sfe, "", " ") + js2, _ := json.MarshalIndent((*sfes)[len(*sfes)-1], "", " ") + t.Fatalf("siafund element doesn't match:\n%s\nvs\n%s\n", js1, js2) + } + *sfes = (*sfes)[:len(*sfes)-1] + }) + if len(addedSCEs)+len(spentSCEs)+len(addedSFEs)+len(spentSFEs) > 0 { + t.Fatal("extraneous elements") + } + } // block with nothing except block reward b1 := types.Block{ @@ -185,9 +226,16 @@ func TestApplyBlock(t *testing.T) { spentSFEs = []types.SiafundElement{ {SiafundOutput: giftTxn.SiafundOutputs[0]}, } + + prev := cs + bs := dbStore.SupplementTipBlock(b2) if au, err := addBlock(b2); err != nil { t.Fatal(err) } else { checkUpdateElements(au, addedSCEs, spentSCEs, addedSFEs, spentSFEs) } + + ru := consensus.RevertBlock(prev, b2, bs) + dbStore.RevertBlock(cs, ru) + checkRevertElements(ru, addedSCEs, spentSCEs, addedSFEs, spentSFEs) } diff --git a/consensus/validation_test.go b/consensus/validation_test.go index a71f6fb9..31967e5a 100644 --- a/consensus/validation_test.go +++ b/consensus/validation_test.go @@ -2,6 +2,7 @@ package consensus_test import ( "bytes" + "math" "testing" "time" @@ -141,6 +142,7 @@ func TestValidateBlock(t *testing.T) { t.Fatal(err) } + // tests with correct signatures { tests := []struct { desc string @@ -200,6 +202,16 @@ func TestValidateBlock(t *testing.T) { } }, }, + { + "overflowing siacoin outputs", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.SiacoinOutputs = []types.SiacoinOutput{ + {Address: types.VoidAddress, Value: types.MaxCurrency}, + {Address: types.VoidAddress, Value: types.MaxCurrency}, + } + }, + }, { "zero-valued SiacoinOutput", func(b *types.Block) { @@ -353,6 +365,21 @@ func TestValidateBlock(t *testing.T) { txn.FileContracts[0].Payout = txn.FileContracts[0].Payout.Sub(types.Siacoins(1)) }, }, + { + "file contract that ends after v2 hardfork", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.FileContracts[0].WindowStart = n.HardforkV2.RequireHeight + txn.FileContracts[0].WindowEnd = txn.FileContracts[0].WindowStart + 100 + }, + }, + { + "revision of nonexistent file contract", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.FileContractRevisions[0].ParentID[0] ^= 255 + }, + }, { "revision with window that starts in past", func(b *types.Block) { @@ -400,7 +427,16 @@ func TestValidateBlock(t *testing.T) { }, }, { - "double-spent input", + "conflicting revisions in same transaction", + func(b *types.Block) { + txn := &b.Transactions[0] + newRevision := txn.FileContractRevisions[0] + newRevision.RevisionNumber++ + txn.FileContractRevisions = append(txn.FileContractRevisions, newRevision) + }, + }, + { + "double-spent siacoin input", func(b *types.Block) { txn := &b.Transactions[0] txn.SiacoinInputs = append(txn.SiacoinInputs, txn.SiacoinInputs[0]) @@ -408,17 +444,23 @@ func TestValidateBlock(t *testing.T) { }, }, { - "conflicting revisions in same transaction", + "double-spent siafund input", func(b *types.Block) { txn := &b.Transactions[0] - newRevision := txn.FileContractRevisions[0] - newRevision.RevisionNumber++ - txn.FileContractRevisions = append(txn.FileContractRevisions, newRevision) + txn.SiafundInputs = append(txn.SiafundInputs, txn.SiafundInputs[0]) + txn.SiafundOutputs[0].Value = txn.SiafundOutputs[0].Value + 100 + }, + }, + { + "transaction contains a storage proof and creates new outputs", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.StorageProofs = append(txn.StorageProofs, types.StorageProof{}) }, }, } for _, test := range tests { - corruptBlock := deepCopyBlock(b) + corruptBlock := deepCopyBlock(validBlock) test.corrupt(&corruptBlock) signTxn(&corruptBlock.Transactions[0]) findBlockNonce(cs, &corruptBlock) @@ -428,16 +470,82 @@ func TestValidateBlock(t *testing.T) { } } } + + // signature test + { + tests := []struct { + desc string + corrupt func(*types.Block) + }{ + { + "siacoin input with missing signature", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.Signatures = []types.TransactionSignature{txn.Signatures[1]} + }, + }, + { + "siafund input with missing signature", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.Signatures = []types.TransactionSignature{txn.Signatures[0]} + }, + }, + { + "signature that refers to parent not in transaction", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.Signatures[0].ParentID[0] ^= 255 + }, + }, + { + "signature that refers to nonexistent public key", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.Signatures[0].PublicKeyIndex = math.MaxUint64 + }, + }, + { + "redundant signature", + func(b *types.Block) { + txn := &b.Transactions[0] + txn.Signatures = append(txn.Signatures, txn.Signatures[0]) + }, + }, + } + for _, test := range tests { + corruptBlock := deepCopyBlock(validBlock) + test.corrupt(&corruptBlock) + findBlockNonce(cs, &corruptBlock) + + if err := consensus.ValidateBlock(cs, corruptBlock, dbStore.SupplementTipBlock(corruptBlock)); err == nil { + t.Fatalf("accepted block with %v", test.desc) + } + } + } +} + +func updateProofs(cau consensus.ApplyUpdate, sces []types.SiacoinElement, sfes []types.SiafundElement, fces []types.V2FileContractElement) { + for i := range sces { + cau.UpdateElementProof(&sces[i].StateElement) + } + for i := range sfes { + cau.UpdateElementProof(&sfes[i].StateElement) + } + for i := range fces { + cau.UpdateElementProof(&fces[i].StateElement) + } } func TestValidateV2Block(t *testing.T) { n, genesisBlock := chain.TestnetZen() + n.HardforkOak.Height = 0 n.HardforkTax.Height = 0 n.HardforkFoundation.Height = 0 n.InitialTarget = types.BlockID{0xFF} n.HardforkV2.AllowHeight = 0 - n.HardforkV2.RequireHeight = 1025000 + n.HardforkV2.RequireHeight = 0 giftPrivateKey := types.GeneratePrivateKey() giftPublicKey := giftPrivateKey.PublicKey() @@ -460,6 +568,24 @@ func TestValidateV2Block(t *testing.T) { txn.FileContractRevisions[i].Revision.RenterSignature = renterPrivateKey.SignHash(cs.ContractSigHash(txn.FileContractRevisions[i].Revision)) txn.FileContractRevisions[i].Revision.HostSignature = hostPrivateKey.SignHash(cs.ContractSigHash(txn.FileContractRevisions[i].Revision)) } + for i := range txn.FileContractResolutions { + switch r := txn.FileContractResolutions[i].Resolution.(type) { + case *types.V2FileContractRenewal: + r.InitialRevision.RenterSignature = renterPrivateKey.SignHash(cs.ContractSigHash(r.InitialRevision)) + r.InitialRevision.HostSignature = hostPrivateKey.SignHash(cs.ContractSigHash(r.InitialRevision)) + r.FinalRevision.RenterSignature = renterPrivateKey.SignHash(cs.ContractSigHash(r.FinalRevision)) + r.FinalRevision.HostSignature = hostPrivateKey.SignHash(cs.ContractSigHash(r.FinalRevision)) + + r.RenterSignature = renterPrivateKey.SignHash(cs.RenewalSigHash(*r)) + r.HostSignature = hostPrivateKey.SignHash(cs.RenewalSigHash(*r)) + break + case *types.V2FileContractFinalization: + sigHash := cs.ContractSigHash(types.V2FileContract(*r)) + r.RenterSignature = renterPrivateKey.SignHash(sigHash) + r.HostSignature = hostPrivateKey.SignHash(sigHash) + break + } + } for i := range txn.SiacoinInputs { txn.SiacoinInputs[i].SatisfiedPolicy.Signatures = []types.Signature{giftPrivateKey.SignHash(cs.InputSigHash(*txn))} } @@ -473,8 +599,8 @@ func TestValidateV2Block(t *testing.T) { v1GiftFC := rhpv2.PrepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), 100, rhpv2.HostSettings{}, types.VoidAddress) v2GiftFC := types.V2FileContract{ Filesize: v1GiftFC.Filesize, - ProofHeight: 5, - ExpirationHeight: 10, + ProofHeight: 20, + ExpirationHeight: 30, RenterOutput: v1GiftFC.ValidProofOutputs[0], HostOutput: v1GiftFC.ValidProofOutputs[1], MissedHostValue: v1GiftFC.MissedProofOutputs[1].Value, @@ -482,10 +608,12 @@ func TestValidateV2Block(t *testing.T) { RenterPublicKey: renterPublicKey, HostPublicKey: hostPublicKey, } + contractCost := v2GiftFC.RenterOutput.Value.Add(v2GiftFC.HostOutput.Value).Add(n.GenesisState().V2FileContractTax(v2GiftFC)) giftTxn := types.V2Transaction{ SiacoinOutputs: []types.SiacoinOutput{ {Address: giftAddress, Value: giftAmountSC}, + {Address: giftAddress, Value: contractCost}, }, SiafundOutputs: []types.SiafundOutput{ {Address: giftAddress, Value: giftAmountSF}, @@ -498,8 +626,7 @@ func TestValidateV2Block(t *testing.T) { Transactions: []types.V2Transaction{giftTxn}, } - bs := consensus.V1BlockSupplement{Transactions: make([]consensus.V1TransactionSupplement, len(genesisBlock.Transactions))} - _, cau := consensus.ApplyBlock(n.GenesisState(), genesisBlock, bs, time.Time{}) + _, cau := consensus.ApplyBlock(n.GenesisState(), genesisBlock, consensus.V1BlockSupplement{}, time.Time{}) var sces []types.SiacoinElement cau.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { @@ -513,6 +640,8 @@ func TestValidateV2Block(t *testing.T) { cau.ForEachV2FileContractElement(func(fce types.V2FileContractElement, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { fces = append(fces, fce) }) + var cies []types.ChainIndexElement + cies = append(cies, cau.ChainIndexElement()) dbStore, tipState, err := chain.NewDBStore(chain.NewMemDB(), n, genesisBlock) if err != nil { @@ -522,7 +651,6 @@ func TestValidateV2Block(t *testing.T) { fc := v2GiftFC fc.TotalCollateral = fc.HostOutput.Value - contractCost := fc.RenterOutput.Value.Add(fc.HostOutput.Value).Add(cs.V2FileContractTax(fc)) rev1 := v2GiftFC rev1.RevisionNumber++ @@ -563,10 +691,10 @@ func TestValidateV2Block(t *testing.T) { } signTxn(cs, &b.V2.Transactions[0]) b.V2.Commitment = cs.Commitment(cs.TransactionsCommitment(b.Transactions, b.V2Transactions()), b.MinerPayouts[0].Address) + findBlockNonce(cs, &b) // initial block should be valid validBlock := deepCopyBlock(b) - findBlockNonce(cs, &validBlock) if err := consensus.ValidateBlock(cs, validBlock, dbStore.SupplementTipBlock(validBlock)); err != nil { t.Fatal(err) } @@ -576,6 +704,18 @@ func TestValidateV2Block(t *testing.T) { desc string corrupt func(*types.Block) }{ + { + "v1 transaction after v2 hardfork", + func(b *types.Block) { + b.Transactions = []types.Transaction{{}} + }, + }, + { + "block height that does not increment parent height", + func(b *types.Block) { + b.V2.Height = 0 + }, + }, { "weight that exceeds the limit", func(b *types.Block) { @@ -720,6 +860,13 @@ func TestValidateV2Block(t *testing.T) { txn.NewFoundationAddress = &addr }, }, + { + "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", func(b *types.Block) { @@ -787,12 +934,314 @@ func TestValidateV2Block(t *testing.T) { }, }, { - "conflicting revisions in same transaction", + "missed host value exceeding valid host value", func(b *types.Block) { txn := &b.V2.Transactions[0] - newRevision := txn.FileContractRevisions[0] - newRevision.Revision.RevisionNumber++ - txn.FileContractRevisions = append(txn.FileContractRevisions, newRevision) + txn.FileContracts[0].MissedHostValue = txn.FileContracts[0].HostOutput.Value.Add(types.Siacoins(1)) + }, + }, + { + "total collateral exceeding valid host value", + 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", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.SiacoinInputs[0].Parent.StateElement.ID[0] ^= 255 + }, + }, + { + "spends siafund output not in accumulator", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.SiafundInputs[0].Parent.StateElement.ID[0] ^= 255 + }, + }, + { + "superfluous siacoin spend policy preimage(s)", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.SiacoinInputs[0].SatisfiedPolicy.Preimages = [][]byte{{1}} + }, + }, + { + "superfluous siafund spend policy preimage(s)", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.SiafundInputs[0].SatisfiedPolicy.Preimages = [][]byte{{1}} + }, + }, + { + "transaction both resolves a file contract and creates new outputs", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.FileContractResolutions = append(txn.FileContractResolutions, types.V2FileContractResolution{ + Parent: fces[0], + Resolution: &types.V2FileContractExpiration{}, + }) + }, + }, + { + "attestation with an empty key", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.Attestations = append(txn.Attestations, types.Attestation{}) + }, + }, + { + "attestation with invalid signature", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.Attestations = append(txn.Attestations, types.Attestation{ + Key: "HostAnnouncement", + PublicKey: giftPublicKey, + }) + }, + }, + } + for _, test := range tests { + corruptBlock := deepCopyBlock(validBlock) + test.corrupt(&corruptBlock) + signTxn(cs, &corruptBlock.V2.Transactions[0]) + if len(corruptBlock.MinerPayouts) > 0 { + corruptBlock.V2.Commitment = cs.Commitment(cs.TransactionsCommitment(corruptBlock.Transactions, corruptBlock.V2Transactions()), corruptBlock.MinerPayouts[0].Address) + } + findBlockNonce(cs, &corruptBlock) + + if err := consensus.ValidateBlock(cs, corruptBlock, dbStore.SupplementTipBlock(corruptBlock)); err == nil { + t.Fatalf("accepted block with %v", test.desc) + } + } + } + + cs, testCau := consensus.ApplyBlock(cs, validBlock, dbStore.SupplementTipBlock(validBlock), time.Now()) + if dbStore.ApplyBlock(cs, testCau, true) != true { + t.Fatal("didn't commit block") + } + updateProofs(testCau, sces, sfes, fces) + + var testSces []types.SiacoinElement + testCau.ForEachSiacoinElement(func(sce types.SiacoinElement, spent bool) { + testSces = append(testSces, sce) + }) + var testSfes []types.SiafundElement + testCau.ForEachSiafundElement(func(sfe types.SiafundElement, spent bool) { + testSfes = append(testSfes, sfe) + }) + var testFces []types.V2FileContractElement + testCau.ForEachV2FileContractElement(func(fce types.V2FileContractElement, rev *types.V2FileContractElement, res types.V2FileContractResolutionType) { + testFces = append(testFces, fce) + }) + cies = append(cies, testCau.ChainIndexElement()) + + // mine empty blocks + blockID := validBlock.ID() + for i := uint64(0); i < v2GiftFC.ProofHeight; i++ { + b := types.Block{ + ParentID: blockID, + Timestamp: types.CurrentTimestamp(), + V2: &types.V2BlockData{ + Height: cs.Index.Height + 1, + }, + MinerPayouts: []types.SiacoinOutput{{ + Address: types.VoidAddress, + Value: cs.BlockReward(), + }}, + } + b.V2.Commitment = cs.Commitment(cs.TransactionsCommitment(b.Transactions, b.V2Transactions()), b.MinerPayouts[0].Address) + + findBlockNonce(cs, &b) + if err := consensus.ValidateBlock(cs, b, dbStore.SupplementTipBlock(b)); err != nil { + t.Fatal(err) + } + cs, cau = consensus.ApplyBlock(cs, b, dbStore.SupplementTipBlock(validBlock), time.Now()) + if dbStore.ApplyBlock(cs, cau, true) != true { + t.Fatal("didn't commit block") + } + + updateProofs(cau, sces, sfes, fces) + updateProofs(cau, testSces, testSfes, testFces) + cies = append(cies, cau.ChainIndexElement()) + + blockID = b.ID() + } + + b = types.Block{ + ParentID: blockID, + Timestamp: types.CurrentTimestamp(), + V2: &types.V2BlockData{ + Height: cs.Index.Height + 1, + Transactions: []types.V2Transaction{ + {}, + }, + }, + MinerPayouts: []types.SiacoinOutput{{ + Address: types.VoidAddress, + Value: cs.BlockReward(), + }}, + } + signTxn(cs, &b.V2.Transactions[0]) + b.V2.Commitment = cs.Commitment(cs.TransactionsCommitment(b.Transactions, b.V2Transactions()), b.MinerPayouts[0].Address) + findBlockNonce(cs, &validBlock) + + // initial block should be valid + validBlock = deepCopyBlock(b) + if err := consensus.ValidateBlock(cs, validBlock, dbStore.SupplementTipBlock(validBlock)); err != nil { + t.Fatal(err) + } + + { + tests := []struct { + desc string + corrupt func(*types.Block) + }{ + { + "double spend of non-parent siacoin output", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.SiacoinInputs = append(txn.SiacoinInputs, types.V2SiacoinInput{ + Parent: testSces[0], + SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy}, + }) + }, + }, + { + "double spend of non-parent siafund output", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.SiafundInputs = append(txn.SiafundInputs, types.V2SiafundInput{ + Parent: testSfes[0], + SatisfiedPolicy: types.SatisfiedPolicy{Policy: giftPolicy}, + }) + }, + }, + { + "revision after proof height", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + rev := testFces[0].V2FileContract + rev.RevisionNumber += 1 + txn.FileContractRevisions = []types.V2FileContractRevision{{ + Parent: testFces[0], + Revision: rev, + }} + }, + }, + { + "storage proof expiration at wrong proof height", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.FileContractResolutions = []types.V2FileContractResolution{{ + Parent: testFces[0], + Resolution: &types.V2StorageProof{ + ProofIndex: cies[len(cies)-1], + }, + }} + }, + }, + { + "file contract expiration submitted before expiration height", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + txn.FileContractResolutions = []types.V2FileContractResolution{{ + Parent: testFces[0], + Resolution: &types.V2FileContractExpiration{}, + }} + }, + }, + { + "file contract finalization that does not set maximum revision number", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + + resolution := types.V2FileContractFinalization(testFces[0].V2FileContract) + txn.FileContractResolutions = []types.V2FileContractResolution{{ + Parent: testFces[0], + Resolution: &resolution, + }} + }, + }, + { + "file contract finalization with invalid revision", + func(b *types.Block) { + txn := &b.V2.Transactions[0] + + resolution := types.V2FileContractFinalization(testFces[0].V2FileContract) + resolution.RevisionNumber = types.MaxRevisionNumber + resolution.TotalCollateral = types.ZeroCurrency + txn.FileContractResolutions = []types.V2FileContractResolution{{ + Parent: testFces[0], + Resolution: &resolution, + }} + }, + }, + { + "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, + InitialRevision: testFces[0].V2FileContract, + } + txn.FileContractResolutions = []types.V2FileContractResolution{{ + Parent: testFces[0], + Resolution: &resolution, + }} + }, + }, + { + "file contract renewal with invalid final revision", + 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 + rev.RevisionNumber = types.MaxRevisionNumber + rev.TotalCollateral = types.ZeroCurrency + resolution := types.V2FileContractRenewal{ + FinalRevision: rev, + InitialRevision: testFces[0].V2FileContract, + } + txn.FileContractResolutions = []types.V2FileContractResolution{{ + Parent: testFces[0], + Resolution: &resolution, + }} + }, + }, + { + "file contract renewal with invalid initial revision", + 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 + rev.ExpirationHeight = rev.ProofHeight + finalRev := testFces[0].V2FileContract + finalRev.RevisionNumber = types.MaxRevisionNumber + resolution := types.V2FileContractRenewal{ + FinalRevision: finalRev, + InitialRevision: rev, + } + txn.FileContractResolutions = []types.V2FileContractResolution{{ + Parent: testFces[0], + Resolution: &resolution, + }} }, }, } diff --git a/gateway/encoding.go b/gateway/encoding.go index fa8ab506..6de0e3c2 100644 --- a/gateway/encoding.go +++ b/gateway/encoding.go @@ -281,6 +281,7 @@ type RPCSendV2Blocks struct { } func (r *RPCSendV2Blocks) encodeRequest(e *types.Encoder) { + e.WritePrefix(len(r.History)) for i := range r.History { r.History[i].EncodeTo(e) } diff --git a/types/encoding.go b/types/encoding.go index c807c058..ed77b8bd 100644 --- a/types/encoding.go +++ b/types/encoding.go @@ -598,6 +598,11 @@ func (ren V2FileContractRenewal) EncodeTo(e *Encoder) { ren.HostSignature.EncodeTo(e) } +// EncodeTo implements types.EncoderTo. +func (fcf V2FileContractFinalization) EncodeTo(e *Encoder) { + V2FileContract(fcf).EncodeTo(e) +} + // EncodeTo implements types.EncoderTo. func (sp V2StorageProof) EncodeTo(e *Encoder) { sp.ProofIndex.EncodeTo(e) @@ -1173,6 +1178,11 @@ func (ren *V2FileContractRenewal) DecodeFrom(d *Decoder) { ren.HostSignature.DecodeFrom(d) } +// DecodeFrom implements types.DecoderFrom. +func (fcf V2FileContractFinalization) DecodeFrom(d *Decoder) { + (*V2FileContract)(&fcf).DecodeFrom(d) +} + // DecodeFrom implements types.DecoderFrom. func (sp *V2StorageProof) DecodeFrom(d *Decoder) { sp.ProofIndex.DecodeFrom(d)