Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix file contract element bug #28

Merged
merged 3 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 58 additions & 17 deletions persist/sqlite/consensus.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,7 +556,7 @@ func deleteBlock(tx *txn, bid types.BlockID) error {
return err
}

func addFileContractElements(tx *txn, bid types.BlockID, fces []explorer.FileContractUpdate) (map[explorer.DBFileContract]int64, error) {
func addFileContractElements(tx *txn, b types.Block, fces []explorer.FileContractUpdate) (map[explorer.DBFileContract]int64, error) {
stmt, err := tx.Prepare(`INSERT INTO file_contract_elements(block_id, contract_id, leaf_index, resolved, valid, filesize, file_merkle_root, window_start, window_end, payout, unlock_hash, revision_number)
VALUES (?, ?, ?, FALSE, TRUE, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (contract_id, revision_number)
Expand All @@ -575,28 +575,69 @@ func addFileContractElements(tx *txn, bid types.BlockID, fces []explorer.FileCon
return nil, fmt.Errorf("addFileContractElements: failed to prepare last_contract_revision statement: %w", err)
}

var updateErr error
fcDBIds := make(map[explorer.DBFileContract]int64)
for _, update := range fces {
fce := update.FileContractElement

fc := &fce.FileContract
if update.Revision != nil {
fc = &update.Revision.FileContract
}

addFC := func(fcID types.FileContractID, leafIndex uint64, fc types.FileContract, resolved, valid, lastRevision bool) error {
var dbID int64
err := stmt.QueryRow(encode(bid), encode(fce.StateElement.ID), encode(fce.StateElement.LeafIndex), fc.Filesize, encode(fc.FileMerkleRoot), fc.WindowStart, fc.WindowEnd, encode(fc.Payout), encode(fc.UnlockHash), fc.RevisionNumber, update.Resolved, update.Valid, encode(fce.StateElement.LeafIndex)).Scan(&dbID)
err := stmt.QueryRow(encode(b.ID()), encode(fcID), encode(leafIndex), fc.Filesize, encode(fc.FileMerkleRoot), fc.WindowStart, fc.WindowEnd, encode(fc.Payout), encode(fc.UnlockHash), fc.RevisionNumber, resolved, valid, encode(leafIndex)).Scan(&dbID)
if err != nil {
return nil, fmt.Errorf("addFileContractElements: failed to execute file_contract_elements statement: %w", err)
return fmt.Errorf("failed to execute file_contract_elements statement: %w", err)
}

if _, err := revisionStmt.Exec(encode(fce.StateElement.ID), dbID, dbID); err != nil {
return nil, fmt.Errorf("addFileContractElements: failed to update last revision number: %w", err)
// only update if it's the most recent revision which will come from
// running ForEachFileContractElement on the update
if lastRevision {
if _, err := revisionStmt.Exec(encode(fcID), dbID, dbID); err != nil {
return fmt.Errorf("failed to update last revision number: %w", err)
}
}

fcDBIds[explorer.DBFileContract{ID: fcID, RevisionNumber: fc.RevisionNumber}] = dbID
return nil
}

var updateErr error
for _, update := range fces {
fce := &update.FileContractElement
if update.Revision != nil {
fce = update.Revision
}

if err := addFC(
types.FileContractID(fce.StateElement.ID),
fce.StateElement.LeafIndex,
fce.FileContract,
update.Resolved,
update.Valid,
true,
); err != nil {
return nil, fmt.Errorf("addFileContractElements: %w", err)
}
}
for _, txn := range b.Transactions {
for j, fc := range txn.FileContracts {
fcID := txn.FileContractID(j)
dbFC := explorer.DBFileContract{ID: txn.FileContractID(j), RevisionNumber: fc.RevisionNumber}
if _, exists := fcDBIds[dbFC]; exists {
continue
}

if err := addFC(fcID, 0, fc, false, true, false); err != nil {
return nil, fmt.Errorf("addFileContractElements: %w", err)
}
}
for _, fcr := range txn.FileContractRevisions {
fc := fcr.FileContract
dbFC := explorer.DBFileContract{ID: fcr.ParentID, RevisionNumber: fc.RevisionNumber}
if _, exists := fcDBIds[dbFC]; exists {
continue
}

fcDBIds[explorer.DBFileContract{ID: types.FileContractID(fce.StateElement.ID), RevisionNumber: fc.RevisionNumber}] = dbID
if err := addFC(fcr.ParentID, 0, fc, false, true, false); err != nil {
return nil, fmt.Errorf("addFileContractElements: %w", err)
}
}
}

return fcDBIds, updateErr
}

Expand Down Expand Up @@ -629,7 +670,7 @@ func (ut *updateTx) ApplyIndex(state explorer.UpdateState) error {
return fmt.Errorf("ApplyIndex: failed to update balances: %w", err)
}

fcDBIds, err := addFileContractElements(ut.tx, state.Block.ID(), state.FileContractElements)
fcDBIds, err := addFileContractElements(ut.tx, state.Block, state.FileContractElements)
if err != nil {
return fmt.Errorf("v: failed to add file contracts: %w", err)
}
Expand Down Expand Up @@ -664,7 +705,7 @@ func (ut *updateTx) RevertIndex(state explorer.UpdateState) error {
return fmt.Errorf("RevertIndex: failed to update siafund output state: %w", err)
} else if err := updateBalances(ut.tx, state.Index.Height, state.SpentSiacoinElements, state.NewSiacoinElements, state.SpentSiafundElements, state.NewSiafundElements); err != nil {
return fmt.Errorf("RevertIndex: failed to update balances: %w", err)
} else if _, err := addFileContractElements(ut.tx, state.Block.ID(), state.FileContractElements); err != nil {
} else if _, err := addFileContractElements(ut.tx, state.Block, state.FileContractElements); err != nil {
return fmt.Errorf("RevertIndex: failed to update file contract state: %w", err)
} else if err := deleteBlock(ut.tx, state.Block.ID()); err != nil {
return fmt.Errorf("RevertIndex: failed to delete block: %w", err)
Expand Down
232 changes: 232 additions & 0 deletions persist/sqlite/consensus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -861,6 +861,238 @@ func TestFileContract(t *testing.T) {
}
}

func TestEphemeralFileContract(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)
}

cm := chain.NewManager(store, genesisState)

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(revision, 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)

// See core/types.FileContractRevision
// Essentially, a revision cannot change the total payout, so this value
// is replaced with a sentinel value of types.MaxCurrency in revisions
// if it is decoded.
if !revision {
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)

uc := types.UnlockConditions{
PublicKeys: []types.UnlockKey{
renterPublicKey.UnlockKey(),
hostPublicKey.UnlockKey(),
},
SignaturesRequired: 2,
}
revisedFC1 := fc
revisedFC1.RevisionNumber++
reviseTxn1 := types.Transaction{
FileContractRevisions: []types.FileContractRevision{{
ParentID: fcID,
UnlockConditions: uc,
FileContract: revisedFC1,
}},
}
signTxn(&reviseTxn1)

// Create a contract and revise it in the same block
if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), []types.Transaction{txn, reviseTxn1}, types.VoidAddress)}); err != nil {
t.Fatal(err)
}
syncDB(t, db, cm)

// 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(true, false, true, revisedFC1, 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(true, false, true, fc, txns[0].FileContracts[0])
}

{
txns, err := db.Transactions([]types.TransactionID{reviseTxn1.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(true, false, true, revisedFC1, fcr.FileContract)
}

revisedFC2 := revisedFC1
revisedFC2.RevisionNumber++
reviseTxn2 := types.Transaction{
FileContractRevisions: []types.FileContractRevision{{
ParentID: fcID,
UnlockConditions: uc,
FileContract: revisedFC2,
}},
}
signTxn(&reviseTxn2)

revisedFC3 := revisedFC2
revisedFC3.RevisionNumber++
reviseTxn3 := types.Transaction{
FileContractRevisions: []types.FileContractRevision{{
ParentID: fcID,
UnlockConditions: uc,
FileContract: revisedFC3,
}},
}
signTxn(&reviseTxn3)

// Two more revisions of the same contract in the next block
if err := cm.AddBlocks([]types.Block{mineBlock(cm.TipState(), []types.Transaction{reviseTxn2, reviseTxn3}, types.VoidAddress)}); err != nil {
t.Fatal(err)
}
syncDB(t, db, cm)

// 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(true, false, true, revisedFC3, dbFCs[0])
}

{
txns, err := db.Transactions([]types.TransactionID{reviseTxn2.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(true, false, true, revisedFC2, fcr.FileContract)
}

{
txns, err := db.Transactions([]types.TransactionID{reviseTxn3.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(true, false, true, revisedFC3, fcr.FileContract)
}
}

func TestRevertTip(t *testing.T) {
log := zaptest.NewLogger(t)
dir := t.TempDir()
Expand Down
2 changes: 1 addition & 1 deletion persist/sqlite/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ CREATE TABLE transaction_file_contract_revisions (
transaction_id INTEGER REFERENCES transactions(id) ON DELETE CASCADE NOT NULL,
transaction_order INTEGER NOT NULL,
contract_id INTEGER REFERENCES file_contract_elements(id) ON DELETE CASCADE NOT NULL,
parent_id BLOB UNIQUE NOT NULL,
parent_id BLOB NOT NULL,
unlock_conditions BLOB NOT NULL,
UNIQUE(transaction_id, transaction_order)
);
Expand Down
Loading