diff --git a/persist/sqlite/consensus.go b/persist/sqlite/consensus.go index a4d358be..5a7d4d1c 100644 --- a/persist/sqlite/consensus.go +++ b/persist/sqlite/consensus.go @@ -191,7 +191,7 @@ func (s *Store) addFileContractRevisions(dbTxn txn, id int64, txn types.Transact return errors.New("addFileContractRevisions: dbID not in map") } - if _, err := stmt.Exec(id, i, dbID, dbEncode(fcr.UnlockConditions), dbEncode(fcr.UnlockHash)); err != nil { + if _, err := stmt.Exec(id, i, dbID, dbEncode(fcr.ParentID), dbEncode(fcr.UnlockConditions)); err != nil { return fmt.Errorf("addFileContractRevisions: failed to execute statement: %w", err) } @@ -371,8 +371,6 @@ func (s *Store) updateMaturedBalances(dbTxn txn, update consensusUpdate, height _, isRevert := update.(*chain.RevertUpdate) if isRevert { height++ - } else { - height-- } rows, err := dbTxn.Query(`SELECT address, value @@ -546,7 +544,8 @@ func (s *Store) addFileContractElements(dbTxn txn, bid types.BlockID, update con stmt, err := dbTxn.Prepare(`INSERT INTO file_contract_elements(block_id, contract_id, leaf_index, merkle_proof, resolved, valid, filesize, file_merkle_root, window_start, window_end, payout, unlock_hash, revision_number) VALUES (?, ?, ?, ?, FALSE, TRUE, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (contract_id, revision_number) - DO UPDATE SET resolved = ? AND valid = ?`) + DO UPDATE SET resolved = ?, valid = ? + RETURNING id;`) if err != nil { return nil, fmt.Errorf("addFileContractElements: failed to prepare file_contract_elements statement: %w", err) } @@ -554,7 +553,7 @@ func (s *Store) addFileContractElements(dbTxn txn, bid types.BlockID, update con revisionStmt, err := dbTxn.Prepare(`INSERT INTO last_contract_revision(contract_id, contract_element_id) VALUES (?, ?) - ON CONFLICT + ON CONFLICT (contract_id) DO UPDATE SET contract_element_id = ?`) if err != nil { return nil, fmt.Errorf("addFileContractElements: failed to prepare last_contract_revision statement: %w", err) @@ -572,18 +571,13 @@ func (s *Store) addFileContractElements(dbTxn txn, bid types.BlockID, update con fc = &rev.FileContract } - result, err := stmt.Exec(dbEncode(bid), dbEncode(fce.StateElement.ID), dbEncode(fce.StateElement.LeafIndex), dbEncode(fce.StateElement.MerkleProof), fc.Filesize, dbEncode(fc.FileMerkleRoot), fc.WindowStart, fc.WindowEnd, dbEncode(fc.Payout), dbEncode(fc.UnlockHash), fc.RevisionNumber, resolved, valid) + var dbID int64 + err := stmt.QueryRow(dbEncode(bid), dbEncode(fce.StateElement.ID), dbEncode(fce.StateElement.LeafIndex), dbEncode(fce.StateElement.MerkleProof), fc.Filesize, dbEncode(fc.FileMerkleRoot), fc.WindowStart, fc.WindowEnd, dbEncode(fc.Payout), dbEncode(fc.UnlockHash), fc.RevisionNumber, resolved, valid).Scan(&dbID) if err != nil { updateErr = fmt.Errorf("addFileContractElements: failed to execute file_contract_elements statement: %w", err) return } - dbID, err := result.LastInsertId() - if err != nil { - updateErr = fmt.Errorf("addFileContractElements: failed to get last insert ID: %w", err) - return - } - if _, err := revisionStmt.Exec(dbEncode(fce.StateElement.ID), dbID, dbID); err != nil { updateErr = fmt.Errorf("addFileContractElements: failed to update last revision number: %w", err) return diff --git a/persist/sqlite/consensus_test.go b/persist/sqlite/consensus_test.go index 5311f479..a86634d5 100644 --- a/persist/sqlite/consensus_test.go +++ b/persist/sqlite/consensus_test.go @@ -1,6 +1,7 @@ package sqlite_test import ( + "math/bits" "path/filepath" "reflect" "testing" @@ -14,9 +15,7 @@ import ( "go.uber.org/zap/zaptest" ) -const giftSF = 10000 - -func testV1Network(giftAddr types.Address) (*consensus.Network, types.Block) { +func testV1Network(giftAddr types.Address, sc types.Currency, sf uint64) (*consensus.Network, types.Block) { // use a modified version of Zen n, genesisBlock := chain.TestnetZen() n.InitialTarget = types.BlockID{0xFF} @@ -28,12 +27,19 @@ func testV1Network(giftAddr types.Address) (*consensus.Network, types.Block) { n.HardforkFoundation.Height = 1 n.HardforkV2.AllowHeight = 1000 n.HardforkV2.RequireHeight = 1000 - genesisBlock.Transactions = []types.Transaction{{ - SiafundOutputs: []types.SiafundOutput{{ + genesisBlock.Transactions = []types.Transaction{{}} + if sf > 0 { + genesisBlock.Transactions[0].SiafundOutputs = []types.SiafundOutput{{ Address: giftAddr, - Value: giftSF, - }}, - }} + Value: sf, + }} + } + if sc.Cmp(types.ZeroCurrency) == 1 { + genesisBlock.Transactions[0].SiacoinOutputs = []types.SiacoinOutput{{ + Address: giftAddr, + Value: sc, + }} + } return n, genesisBlock } @@ -83,6 +89,30 @@ func mineV2Block(state consensus.State, txns []types.V2Transaction, minerAddr ty return b } +func signTxn(cs consensus.State, pk types.PrivateKey, txn *types.Transaction) { + appendSig := func(key types.PrivateKey, pubkeyIndex uint64, parentID types.Hash256) { + sig := key.SignHash(cs.WholeSigHash(*txn, parentID, pubkeyIndex, 0, nil)) + txn.Signatures = append(txn.Signatures, types.TransactionSignature{ + ParentID: parentID, + CoveredFields: types.CoveredFields{WholeTransaction: true}, + PublicKeyIndex: pubkeyIndex, + Signature: sig[:], + }) + } + for i := range txn.SiacoinInputs { + appendSig(pk, 0, types.Hash256(txn.SiacoinInputs[i].ParentID)) + } + for i := range txn.SiafundInputs { + appendSig(pk, 0, types.Hash256(txn.SiafundInputs[i].ParentID)) + } +} + +func check(t *testing.T, desc string, expect, got any) { + if !reflect.DeepEqual(expect, got) { + t.Fatalf("expected %v %s, got %v", expect, desc, got) + } +} + func TestBalance(t *testing.T) { log := zaptest.NewLogger(t) dir := t.TempDir() @@ -98,7 +128,7 @@ func TestBalance(t *testing.T) { } defer bdb.Close() - network, genesisBlock := testV1Network(types.VoidAddress) + network, genesisBlock := testV1Network(types.VoidAddress, types.ZeroCurrency, 0) store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) if err != nil { @@ -117,13 +147,10 @@ func TestBalance(t *testing.T) { sc, immatureSC, sf, err := db.Balance(addr) if err != nil { t.Fatal(err) - } else if sc != expectSC { - t.Fatalf("expected %v siacoins, got %v", expectSC, sc) - } else if immatureSC != expectImmatureSC { - t.Fatalf("expected %v immature siacoins, got %v", expectImmatureSC, immatureSC) - } else if sf != expectSF { - t.Fatalf("expected %d siafunds, got %d", expectSF, sf) } + check(t, "siacoins", expectSC, sc) + check(t, "immature siacoins", expectImmatureSC, immatureSC) + check(t, "siafunds", expectSF, sf) } // Generate three addresses: addr1, addr2, addr3 @@ -137,7 +164,7 @@ func TestBalance(t *testing.T) { addr3 := types.StandardUnlockHash(pk3.PublicKey()) expectedPayout := cm.TipState().BlockReward() - maturityHeight := cm.TipState().MaturityHeight() + 1 + maturityHeight := cm.TipState().MaturityHeight() // Mine a block sending the payout to addr1 if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, addr1)}); err != nil { @@ -148,14 +175,13 @@ func TestBalance(t *testing.T) { utxos, err := db.UnspentSiacoinOutputs(addr1, 100, 0) if err != nil { t.Fatal(err) - } else if len(utxos) != 1 { - t.Fatalf("expected 1 utxo, got %d", len(utxos)) - } else if utxos[0].SiacoinOutput.Value != expectedPayout { - t.Fatalf("expected value %v, got %v", expectedPayout, utxos[0].SiacoinOutput.Value) } + check(t, "utxos", 1, len(utxos)) + check(t, "value", expectedPayout, utxos[0].SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxos[0].Source) // Mine until the payout matures - for i := cm.TipState().Index.Height; i < maturityHeight; i++ { + for i := cm.Tip().Height; i < maturityHeight; i++ { checkBalance(addr1, types.ZeroCurrency, expectedPayout, 0) if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, types.VoidAddress)}); err != nil { t.Fatal(err) @@ -177,17 +203,8 @@ func TestBalance(t *testing.T) { {Address: addr1, Value: types.Siacoins(100)}, {Address: addr2, Value: utxos[0].SiacoinOutput.Value.Sub(types.Siacoins(100))}, }, - Signatures: []types.TransactionSignature{ - { - ParentID: utxos[0].ID, - PublicKeyIndex: 0, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - }, - }, } - parentSigHash := cm.TipState().WholeSigHash(parentTxn, utxos[0].ID, 0, 0, nil) - parentSig := pk1.SignHash(parentSigHash) - parentTxn.Signatures[0].Signature = parentSig[:] + signTxn(cm.TipState(), pk1, &parentTxn) // In the same block, have addr1 send the 100 SC it still has left to // addr3 @@ -202,17 +219,8 @@ func TestBalance(t *testing.T) { SiacoinOutputs: []types.SiacoinOutput{ {Address: addr3, Value: types.Siacoins(100)}, }, - Signatures: []types.TransactionSignature{ - { - ParentID: types.Hash256(outputID), - PublicKeyIndex: 0, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - }, - }, } - sigHash := cm.TipState().WholeSigHash(txn, types.Hash256(outputID), 0, 0, nil) - sig := pk1.SignHash(sigHash) - txn.Signatures[0].Signature = sig[:] + signTxn(cm.TipState(), pk1, &txn) if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), []types.Transaction{parentTxn, txn}, types.VoidAddress)}); err != nil { t.Fatal(err) @@ -247,7 +255,8 @@ func TestSendTransactions(t *testing.T) { pk3 := types.GeneratePrivateKey() addr3 := types.StandardUnlockHash(pk3.PublicKey()) - network, genesisBlock := testV1Network(addr1) + const giftSF = 10000 + network, genesisBlock := testV1Network(addr1, types.ZeroCurrency, giftSF) store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) if err != nil { @@ -266,70 +275,52 @@ func TestSendTransactions(t *testing.T) { sc, immatureSC, sf, err := db.Balance(addr) if err != nil { t.Fatal(err) - } else if sc != expectSC { - t.Fatalf("expected %v siacoins, got %v", expectSC, sc) - } else if immatureSC != expectImmatureSC { - t.Fatalf("expected %v immature siacoins, got %v", expectImmatureSC, immatureSC) - } else if sf != expectSF { - t.Fatalf("expected %d siafunds, got %d", expectSF, sf) } + check(t, "siacoins", expectSC, sc) + check(t, "immature siacoins", expectImmatureSC, immatureSC) + check(t, "siafunds", expectSF, sf) } checkTransaction := func(expectTxn types.Transaction, gotTxn explorer.Transaction) { - if len(expectTxn.SiacoinInputs) != len(gotTxn.SiacoinInputs) { - t.Fatalf("expected %d siacoin inputs, got %d", len(expectTxn.SiacoinInputs), len(gotTxn.SiacoinInputs)) - } else if len(expectTxn.SiacoinOutputs) != len(gotTxn.SiacoinOutputs) { - t.Fatalf("expected %d siacoin outputs, got %d", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs)) - } else if len(expectTxn.SiafundInputs) != len(gotTxn.SiafundInputs) { - t.Fatalf("expected %d siafund inputs, got %d", len(expectTxn.SiafundInputs), len(gotTxn.SiafundInputs)) - } else if len(expectTxn.SiafundOutputs) != len(gotTxn.SiafundOutputs) { - t.Fatalf("expected %d siafund outputs, got %d", len(expectTxn.SiafundOutputs), len(gotTxn.SiafundOutputs)) - } + check(t, "siacoin inputs", len(expectTxn.SiacoinInputs), len(gotTxn.SiacoinInputs)) + check(t, "siacoin outputs", len(expectTxn.SiacoinOutputs), len(gotTxn.SiacoinOutputs)) + check(t, "siafund inputs", len(expectTxn.SiafundInputs), len(gotTxn.SiafundInputs)) + check(t, "siafund outputs", len(expectTxn.SiafundOutputs), len(gotTxn.SiafundOutputs)) for i := range expectTxn.SiacoinInputs { expectSci := expectTxn.SiacoinInputs[i] gotSci := gotTxn.SiacoinInputs[i] - if expectSci.ParentID != gotSci.ParentID { - t.Fatalf("expected parent ID %v, got %v", expectSci.ParentID, gotSci.ParentID) - } else if !reflect.DeepEqual(expectSci.UnlockConditions, gotSci.UnlockConditions) { - t.Fatalf("expected unlock conditions %v, got %v", expectSci.UnlockConditions, gotSci.UnlockConditions) - } + + check(t, "parent ID", expectSci.ParentID, gotSci.ParentID) + check(t, "unlock conditions", expectSci.UnlockConditions, gotSci.UnlockConditions) } for i := range expectTxn.SiacoinOutputs { expectSco := expectTxn.SiacoinOutputs[i] gotSco := gotTxn.SiacoinOutputs[i].SiacoinOutput - if expectSco.Address != gotSco.Address { - t.Fatalf("expected address %v, got %v", expectSco.Address, gotSco.Address) - } else if expectSco.Value != gotSco.Value { - t.Fatalf("expected value %v, got %v", expectSco.Value, gotSco.Value) - } else if gotTxn.SiacoinOutputs[i].Source != explorer.SourceTransaction { - t.Fatalf("expected source %v, got %v", explorer.SourceTransaction, gotTxn.SiacoinOutputs[i].Source) - } + + check(t, "address", expectSco.Address, gotSco.Address) + check(t, "value", expectSco.Value, gotSco.Value) + check(t, "source", explorer.SourceTransaction, gotTxn.SiacoinOutputs[i].Source) } for i := range expectTxn.SiafundInputs { expectSfi := expectTxn.SiafundInputs[i] gotSfi := gotTxn.SiafundInputs[i] - if expectSfi.ParentID != gotSfi.ParentID { - t.Fatalf("expected parent ID %v, got %v", expectSfi.ParentID, gotSfi.ParentID) - } else if expectSfi.ClaimAddress != gotSfi.ClaimAddress { - t.Fatalf("expected claim address %v, got %v", expectSfi.ClaimAddress, gotSfi.ClaimAddress) - } else if !reflect.DeepEqual(expectSfi.UnlockConditions, gotSfi.UnlockConditions) { - t.Fatalf("expected unlock conditions %v, got %v", expectSfi.UnlockConditions, gotSfi.UnlockConditions) - } + + check(t, "parent ID", expectSfi.ParentID, gotSfi.ParentID) + check(t, "claim address", expectSfi.ClaimAddress, gotSfi.ClaimAddress) + check(t, "unlock conditions", expectSfi.UnlockConditions, gotSfi.UnlockConditions) } for i := range expectTxn.SiafundOutputs { expectSfo := expectTxn.SiafundOutputs[i] gotSfo := gotTxn.SiafundOutputs[i].SiafundOutput - if expectSfo.Address != gotSfo.Address { - t.Fatalf("expected address %v, got %v", expectSfo.Address, gotSfo.Address) - } else if expectSfo.Value != gotSfo.Value { - t.Fatalf("expected value %v, got %v", expectSfo.Value, gotSfo.Value) - } + + check(t, "address", expectSfo.Address, gotSfo.Address) + check(t, "value", expectSfo.Value, gotSfo.Value) } } expectedPayout := cm.TipState().BlockReward() - maturityHeight := cm.TipState().MaturityHeight() + 1 + maturityHeight := cm.TipState().MaturityHeight() // Mine a block sending the payout to the addr1 if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, addr1)}); err != nil { @@ -337,7 +328,7 @@ func TestSendTransactions(t *testing.T) { } // Mine until the payout matures - for i := cm.TipState().Index.Height; i < maturityHeight; i++ { + for i := cm.Tip().Height; i < maturityHeight; i++ { if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, types.VoidAddress)}); err != nil { t.Fatal(err) } @@ -353,13 +344,10 @@ func TestSendTransactions(t *testing.T) { utxos, err := db.UnspentSiacoinOutputs(addr1, n, 0) if err != nil { t.Fatal(err) - } else if len(utxos) != 1 { - t.Fatalf("expected 1 utxo, got %d", len(utxos)) - } else if utxos[0].SiacoinOutput.Value != expectedPayout { - t.Fatalf("expected value %v, got %v", expectedPayout, utxos[0].SiacoinOutput.Value) - } else if utxos[0].Source != explorer.SourceMinerPayout { - t.Fatalf("expected source %v, got %v", explorer.SourceMinerPayout, utxos[0].Source) } + check(t, "utxos", 1, len(utxos)) + check(t, "value", expectedPayout, utxos[0].SiacoinOutput.Value) + check(t, "source", explorer.SourceMinerPayout, utxos[0].Source) sfOutputID := genesisBlock.Transactions[0].SiafundOutputID(0) scOutputID := utxos[0].ID @@ -392,30 +380,9 @@ func TestSendTransactions(t *testing.T) { {Address: addr3, Value: 2}, {Address: addr1, Value: addr1SFs}, }, - Signatures: []types.TransactionSignature{ - { - ParentID: scOutputID, - PublicKeyIndex: 0, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - }, - { - ParentID: types.Hash256(sfOutputID), - PublicKeyIndex: 0, - CoveredFields: types.CoveredFields{WholeTransaction: true}, - }, - }, } - { - parentSigHash := cm.TipState().WholeSigHash(parentTxn, scOutputID, 0, 0, nil) - parentSig := pk1.SignHash(parentSigHash) - parentTxn.Signatures[0].Signature = parentSig[:] - } - { - parentSigHash := cm.TipState().WholeSigHash(parentTxn, types.Hash256(sfOutputID), 0, 0, nil) - parentSig := pk1.SignHash(parentSigHash) - parentTxn.Signatures[1].Signature = parentSig[:] - } + signTxn(cm.TipState(), pk1, &parentTxn) scOutputID = types.Hash256(parentTxn.SiacoinOutputID(2)) sfOutputID = parentTxn.SiafundOutputID(2) @@ -434,25 +401,16 @@ func TestSendTransactions(t *testing.T) { block, err := db.Block(b.ID()) if err != nil { t.Fatal(err) - } else if len(b.Transactions) != len(block.Transactions) { - t.Fatalf("expected %d transactions, got %d", len(b.Transactions), len(block.Transactions)) - } else if b.Nonce != block.Nonce { - t.Fatalf("expected nonce %d, got %d", b.Nonce, block.Nonce) - } else if b.Timestamp != block.Timestamp { - t.Fatalf("expected timestamp %d, got %d", b.Timestamp.Unix(), block.Timestamp.Unix()) - } else if len(b.MinerPayouts) != len(block.MinerPayouts) { - t.Fatalf("expected %d miner payouts, got %d", len(b.MinerPayouts), len(block.MinerPayouts)) - } else if len(b.Transactions) != len(block.Transactions) { - t.Fatalf("expected %d transactions, got %d", len(b.Transactions), len(block.Transactions)) } + check(t, "transactions", len(b.Transactions), len(block.Transactions)) + check(t, "miner payouts", len(b.MinerPayouts), len(block.MinerPayouts)) + check(t, "nonce", b.Nonce, block.Nonce) + check(t, "timestamp", b.Timestamp, block.Timestamp) // Ensure the miner payouts in the block match for i := range b.MinerPayouts { - if b.MinerPayouts[i].Address != block.MinerPayouts[i].SiacoinOutput.Address { - t.Fatalf("expected address %v, got %v", b.MinerPayouts[i].Address, block.MinerPayouts[i].SiacoinOutput.Address) - } else if b.MinerPayouts[i].Value != block.MinerPayouts[i].SiacoinOutput.Value { - t.Fatalf("expected value %v, got %v", b.MinerPayouts[i].Value, block.MinerPayouts[i].SiacoinOutput.Value) - } + check(t, "address", b.MinerPayouts[i].Address, b.MinerPayouts[i].Address) + check(t, "value", b.MinerPayouts[i].Value, b.MinerPayouts[i].Value) } // Ensure the transactions in the block and retrieved separately match @@ -463,9 +421,8 @@ func TestSendTransactions(t *testing.T) { txns, err := db.Transactions([]types.TransactionID{b.Transactions[i].ID()}) if err != nil { t.Fatal(err) - } else if len(txns) != 1 { - t.Fatal("failed to get transaction") } + check(t, "transactions", 1, len(txns)) checkTransaction(b.Transactions[i], txns[0]) } @@ -493,27 +450,17 @@ func TestSendTransactions(t *testing.T) { t.Fatal(err) } - if e.sc != len(sc) { - t.Fatalf("expected %d siacoin utxos, got %d", e.sc, len(sc)) - } else if e.sf != len(sf) { - t.Fatalf("expected %d siafund utxos, got %d", e.sf, len(sf)) - } + check(t, "sc utxos", e.sc, len(sc)) + check(t, "sf utxos", e.sf, len(sf)) for _, sco := range sc { - if e.addr != sco.SiacoinOutput.Address { - t.Fatalf("expected address %v, got %v", e.addr, sco.SiacoinOutput.Address) - } else if e.scValue != sco.SiacoinOutput.Value { - t.Fatalf("expected value %v, got %v", e.scValue, sco.SiacoinOutput.Value) - } else if explorer.SourceTransaction != sco.Source { - t.Fatalf("expected source %v, got %v", explorer.SourceTransaction, sco.Source) - } + check(t, "address", e.addr, sco.SiacoinOutput.Address) + check(t, "value", e.scValue, sco.SiacoinOutput.Value) + check(t, "source", explorer.SourceTransaction, sco.Source) } for _, sfo := range sf { - if e.addr != sfo.SiafundOutput.Address { - t.Fatalf("expected address %v, got %v", e.addr, sfo.SiafundOutput.Address) - } else if e.sfValue != sfo.SiafundOutput.Value { - t.Fatalf("expected value %v, got %v", e.sfValue, sfo.SiafundOutput.Value) - } + check(t, "address", e.addr, sfo.SiafundOutput.Address) + check(t, "value", e.sfValue, sfo.SiafundOutput.Value) } } } @@ -534,7 +481,7 @@ func TestTip(t *testing.T) { } defer bdb.Close() - network, genesisBlock := testV1Network(types.VoidAddress) + network, genesisBlock := testV1Network(types.VoidAddress, types.ZeroCurrency, 0) store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) if err != nil { @@ -549,7 +496,7 @@ func TestTip(t *testing.T) { } const n = 100 - for i := cm.TipState().Index.Height; i < n; i++ { + for i := cm.Tip().Height; i < n; i++ { if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, types.VoidAddress)}); err != nil { t.Fatal(err) } @@ -558,9 +505,7 @@ func TestTip(t *testing.T) { if err != nil { t.Fatal(err) } - if cm.Tip() != tip { - t.Fatal("tip mismatch") - } + check(t, "tip", cm.Tip(), tip) } for i := 0; i < n; i++ { @@ -573,3 +518,241 @@ func TestTip(t *testing.T) { } } } + +// copied from rhp/v2 to avoid import cycle +func prepareContractFormation(renterPubKey types.PublicKey, hostKey types.PublicKey, renterPayout, hostCollateral types.Currency, endHeight uint64, windowSize uint64, refundAddr types.Address) types.FileContract { + taxAdjustedPayout := func(target types.Currency) types.Currency { + guess := target.Mul64(1000).Div64(961) + mod64 := func(c types.Currency, v uint64) types.Currency { + var r uint64 + if c.Hi < v { + _, r = bits.Div64(c.Hi, c.Lo, v) + } else { + _, r = bits.Div64(0, c.Hi, v) + _, r = bits.Div64(r, c.Lo, v) + } + return types.NewCurrency64(r) + } + sfc := (consensus.State{}).SiafundCount() + tm := mod64(target, sfc) + gm := mod64(guess, sfc) + if gm.Cmp(tm) < 0 { + guess = guess.Sub(types.NewCurrency64(sfc)) + } + return guess.Add(tm).Sub(gm) + } + uc := types.UnlockConditions{ + PublicKeys: []types.UnlockKey{ + renterPubKey.UnlockKey(), + hostKey.UnlockKey(), + }, + SignaturesRequired: 2, + } + hostPayout := hostCollateral + payout := taxAdjustedPayout(renterPayout.Add(hostPayout)) + return types.FileContract{ + Filesize: 0, + FileMerkleRoot: types.Hash256{}, + WindowStart: endHeight, + WindowEnd: endHeight + windowSize, + Payout: payout, + UnlockHash: types.Hash256(uc.UnlockHash()), + RevisionNumber: 0, + ValidProofOutputs: []types.SiacoinOutput{ + {Value: renterPayout, Address: refundAddr}, + {Value: hostPayout, Address: types.VoidAddress}, + }, + MissedProofOutputs: []types.SiacoinOutput{ + {Value: renterPayout, Address: refundAddr}, + {Value: hostPayout, Address: types.VoidAddress}, + {Value: types.ZeroCurrency, Address: types.VoidAddress}, + }, + } +} + +func TestFileContract(t *testing.T) { + log := zaptest.NewLogger(t) + dir := t.TempDir() + + db, err := sqlite.OpenDatabase(filepath.Join(dir, "explored.sqlite3"), log.Named("sqlite3")) + if err != nil { + t.Fatal(err) + } + defer db.Close() + + bdb, err := coreutils.OpenBoltChainDB(filepath.Join(dir, "consensus.db")) + if err != nil { + t.Fatal(err) + } + defer bdb.Close() + + pk1 := types.GeneratePrivateKey() + addr1 := types.StandardUnlockHash(pk1.PublicKey()) + + renterPrivateKey := types.GeneratePrivateKey() + renterPublicKey := renterPrivateKey.PublicKey() + + hostPrivateKey := types.GeneratePrivateKey() + hostPublicKey := hostPrivateKey.PublicKey() + + giftSC := types.Siacoins(1000) + network, genesisBlock := testV1Network(addr1, giftSC, 0) + store, genesisState, err := chain.NewDBStore(bdb, network, genesisBlock) + if err != nil { + t.Fatal(err) + } + defer store.Close() + + cm := chain.NewManager(store, genesisState) + if err := cm.AddSubscriber(db, types.ChainIndex{}); err != nil { + t.Fatal(err) + } + + scOutputID := genesisBlock.Transactions[0].SiacoinOutputID(0) + unlockConditions := types.StandardUnlockConditions(pk1.PublicKey()) + + signTxn := func(txn *types.Transaction) { + appendSig := func(key types.PrivateKey, pubkeyIndex uint64, parentID types.Hash256) { + sig := key.SignHash(cm.TipState().WholeSigHash(*txn, parentID, pubkeyIndex, 0, nil)) + txn.Signatures = append(txn.Signatures, types.TransactionSignature{ + ParentID: parentID, + CoveredFields: types.CoveredFields{WholeTransaction: true}, + PublicKeyIndex: pubkeyIndex, + Signature: sig[:], + }) + } + for i := range txn.SiacoinInputs { + appendSig(pk1, 0, types.Hash256(txn.SiacoinInputs[i].ParentID)) + } + for i := range txn.SiafundInputs { + appendSig(pk1, 0, types.Hash256(txn.SiafundInputs[i].ParentID)) + } + for i := range txn.FileContractRevisions { + appendSig(renterPrivateKey, 0, types.Hash256(txn.FileContractRevisions[i].ParentID)) + appendSig(hostPrivateKey, 1, types.Hash256(txn.FileContractRevisions[i].ParentID)) + } + } + + checkFC := func(resolved, valid bool, expected types.FileContract, got explorer.FileContract) { + check(t, "resolved state", resolved, got.Resolved) + check(t, "valid state", valid, got.Valid) + check(t, "filesize", expected.Filesize, got.Filesize) + check(t, "file merkle root", expected.FileMerkleRoot, got.FileMerkleRoot) + check(t, "window start", expected.WindowStart, got.WindowStart) + check(t, "window end", expected.WindowEnd, got.WindowEnd) + check(t, "payout", expected.Payout, got.Payout) + check(t, "unlock hash", expected.UnlockHash, got.UnlockHash) + check(t, "revision number", expected.RevisionNumber, got.RevisionNumber) + check(t, "valid proof outputs", len(expected.ValidProofOutputs), len(got.ValidProofOutputs)) + for i := range expected.ValidProofOutputs { + check(t, "valid proof output address", expected.ValidProofOutputs[i].Address, got.ValidProofOutputs[i].Address) + check(t, "valid proof output value", expected.ValidProofOutputs[i].Value, got.ValidProofOutputs[i].Value) + } + check(t, "missed proof outputs", len(expected.MissedProofOutputs), len(got.MissedProofOutputs)) + for i := range expected.MissedProofOutputs { + check(t, "missed proof output address", expected.MissedProofOutputs[i].Address, got.MissedProofOutputs[i].Address) + check(t, "missed proof output value", expected.MissedProofOutputs[i].Value, got.MissedProofOutputs[i].Value) + } + } + + windowStart := cm.Tip().Height + 10 + windowEnd := windowStart + 10 + fc := prepareContractFormation(renterPublicKey, hostPublicKey, types.Siacoins(1), types.Siacoins(1), windowStart, windowEnd, types.VoidAddress) + txn := types.Transaction{ + SiacoinInputs: []types.SiacoinInput{{ + ParentID: scOutputID, + UnlockConditions: unlockConditions, + }}, + SiacoinOutputs: []types.SiacoinOutput{{ + Address: addr1, + Value: giftSC.Sub(fc.Payout), + }}, + FileContracts: []types.FileContract{fc}, + } + fcID := txn.FileContractID(0) + signTxn(&txn) + + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), []types.Transaction{txn}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + + { + dbFCs, err := db.Contracts([]types.FileContractID{fcID}) + if err != nil { + t.Fatal(err) + } + check(t, "fcs", 1, len(dbFCs)) + checkFC(false, true, fc, dbFCs[0]) + } + + { + txns, err := db.Transactions([]types.TransactionID{txn.ID()}) + if err != nil { + t.Fatal(err) + } + check(t, "transactions", 1, len(txns)) + check(t, "file contracts", 1, len(txns[0].FileContracts)) + checkFC(false, true, fc, txns[0].FileContracts[0]) + } + + uc := types.UnlockConditions{ + PublicKeys: []types.UnlockKey{ + renterPublicKey.UnlockKey(), + hostPublicKey.UnlockKey(), + }, + SignaturesRequired: 2, + } + fc.RevisionNumber++ + reviseTxn := types.Transaction{ + FileContractRevisions: []types.FileContractRevision{{ + ParentID: fcID, + UnlockConditions: uc, + FileContract: fc, + }}, + } + signTxn(&reviseTxn) + + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), []types.Transaction{reviseTxn}, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + + // Explorer.Contracts should return latest revision + { + dbFCs, err := db.Contracts([]types.FileContractID{fcID}) + if err != nil { + t.Fatal(err) + } + check(t, "fcs", 1, len(dbFCs)) + checkFC(false, true, fc, dbFCs[0]) + } + + { + txns, err := db.Transactions([]types.TransactionID{reviseTxn.ID()}) + if err != nil { + t.Fatal(err) + } + check(t, "transactions", 1, len(txns)) + check(t, "file contracts", 1, len(txns[0].FileContractRevisions)) + + fcr := txns[0].FileContractRevisions[0] + check(t, "parent id", txn.FileContractID(0), fcr.ParentID) + check(t, "unlock conditions", uc, fcr.UnlockConditions) + + checkFC(false, true, fc, fcr.FileContract) + } + + for i := cm.Tip().Height; i < windowEnd+10; i++ { + if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), nil, types.VoidAddress)}); err != nil { + t.Fatal(err) + } + } + + { + dbFCs, err := db.Contracts([]types.FileContractID{fcID}) + if err != nil { + t.Fatal(err) + } + check(t, "fcs", 1, len(dbFCs)) + checkFC(true, false, fc, dbFCs[0]) + } +}