Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- +goose Up

-- block_hash was added as CHAR(66) NOT NULL DEFAULT '', so rows predating the
-- column (and any default insert) hold a space-padded empty string rather than a
-- real hash. Drop the NOT NULL constraint and the empty default, then normalize
-- those padded empties to NULL so the reconciler's empty-hash guard recognizes
-- "no recorded hash" instead of treating it as a reorged-out block.
ALTER TABLE contract_events ALTER COLUMN block_hash DROP DEFAULT;
ALTER TABLE contract_events ALTER COLUMN block_hash DROP NOT NULL;
UPDATE contract_events SET block_hash = NULL WHERE TRIM(block_hash) = '';

-- +goose Down

UPDATE contract_events SET block_hash = '' WHERE block_hash IS NULL;
ALTER TABLE contract_events ALTER COLUMN block_hash SET DEFAULT '';
ALTER TABLE contract_events ALTER COLUMN block_hash SET NOT NULL;
4 changes: 2 additions & 2 deletions nitronode/store/database/contract_event.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func (s *DBStore) GetLatestContractEventBlockHashAndNumber(contractAddress strin
if err != nil {
return 0, "", err
}
return ev.BlockNumber, ev.BlockHash, nil
return ev.BlockNumber, strings.TrimSpace(ev.BlockHash), nil
}

// GetPreviousDistinctBlockHash returns the block_number and block_hash of the highest
Expand All @@ -104,5 +104,5 @@ func (s *DBStore) GetPreviousDistinctBlockHash(contractAddress string, blockchai
if err != nil {
return 0, "", err
}
return ev.BlockNumber, ev.BlockHash, nil
return ev.BlockNumber, strings.TrimSpace(ev.BlockHash), nil
}
52 changes: 52 additions & 0 deletions nitronode/store/database/contract_event_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package database

import (
"strings"
"testing"

"github.com/layer-3/nitrolite/pkg/core"
Expand Down Expand Up @@ -82,6 +83,57 @@ func TestGetLatestContractEventBlockNumber(t *testing.T) {
})
}

// TestGetContractEventBlockHash_TrimsPaddedEmpty is the regression guard for the
// reorg-reconciliation bug: block_hash was a CHAR(66) NOT NULL DEFAULT '' column,
// so legacy/pre-migration rows read back as a 66-space-padded string rather than "".
// The reconciler's empty-hash guard compares against "", so an untrimmed padded
// value slipped through, got fed to common.HexToHash (-> zero hash), and made every
// stored block look reorged on every chain. The getters now TrimSpace so a padded
// empty collapses to "" the guard recognizes, while real 0x… hashes pass through.
func TestGetContractEventBlockHash_TrimsPaddedEmpty(t *testing.T) {
db, cleanup := SetupTestDB(t)
defer cleanup()

store := NewDBStore(db)

contractAddress := "0x1234567890123456789012345678901234567890"
blockchainID := uint64(1)
paddedEmpty := strings.Repeat(" ", 66) // mimics CHAR(66) padding of ''
realHash := "0x" + strings.Repeat("ab", 32)

t.Run("latest padded-empty hash trims to empty string", func(t *testing.T) {
require.NoError(t, store.StoreContractEvent(core.BlockchainEvent{
ContractAddress: contractAddress, BlockchainID: blockchainID, Name: "E1",
BlockNumber: 100, BlockHash: paddedEmpty, TransactionHash: "0xaaa", LogIndex: 0,
}))

num, hash, err := store.GetLatestContractEventBlockHashAndNumber(contractAddress, blockchainID)
require.NoError(t, err)
assert.Equal(t, uint64(100), num)
assert.Equal(t, "", hash, "padded-empty block_hash must trim to \"\" so the reconciler guard fires")
})

t.Run("previous padded-empty hash trims to empty string", func(t *testing.T) {
// A newer row sits above the padded-empty one so the "below" query returns the legacy row.
require.NoError(t, store.StoreContractEvent(core.BlockchainEvent{
ContractAddress: contractAddress, BlockchainID: blockchainID, Name: "E2",
BlockNumber: 200, BlockHash: realHash, TransactionHash: "0xbbb", LogIndex: 0,
}))

num, hash, err := store.GetPreviousDistinctBlockHash(contractAddress, blockchainID, 200)
require.NoError(t, err)
assert.Equal(t, uint64(100), num)
assert.Equal(t, "", hash, "padded-empty block_hash must trim to \"\" mid-walk")
})

t.Run("real hash is preserved (not over-trimmed)", func(t *testing.T) {
num, hash, err := store.GetLatestContractEventBlockHashAndNumber(contractAddress, blockchainID)
require.NoError(t, err)
assert.Equal(t, uint64(200), num)
assert.Equal(t, realHash, hash)
})
}

func TestIsContractEventProcessed(t *testing.T) {
db, cleanup := SetupTestDB(t)
defer cleanup()
Expand Down
Loading