Skip to content

Commit

Permalink
Merge pull request #28 from SiaFoundation/fix-file-contract-bug
Browse files Browse the repository at this point in the history
Fix file contract element bug
  • Loading branch information
n8maninger authored May 15, 2024
2 parents 236e7cf + 806ba88 commit 56c7558
Show file tree
Hide file tree
Showing 3 changed files with 291 additions and 18 deletions.
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

0 comments on commit 56c7558

Please sign in to comment.