diff --git a/faucet-app/chart/Chart.yaml b/faucet-app/chart/Chart.yaml index e3c0421d4..06d10de7f 100644 --- a/faucet-app/chart/Chart.yaml +++ b/faucet-app/chart/Chart.yaml @@ -3,4 +3,4 @@ apiVersion: v2 description: Faucet App Helm chart name: faucet-app version: 1.0.0 -appVersion: "1.4.0" +appVersion: "1.4.1" diff --git a/nitronode/chart/Chart.yaml b/nitronode/chart/Chart.yaml index 6ce400eeb..b1e6b83b4 100644 --- a/nitronode/chart/Chart.yaml +++ b/nitronode/chart/Chart.yaml @@ -3,4 +3,4 @@ apiVersion: v2 description: Nitronode Helm chart name: nitronode version: 1.0.0 -appVersion: "1.4.0" +appVersion: "1.4.1" diff --git a/nitronode/config/migrations/postgres/20260618000000_make_block_hash_nullable.sql b/nitronode/config/migrations/postgres/20260618000000_make_block_hash_nullable.sql new file mode 100644 index 000000000..679285b9c --- /dev/null +++ b/nitronode/config/migrations/postgres/20260618000000_make_block_hash_nullable.sql @@ -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; diff --git a/nitronode/store/database/contract_event.go b/nitronode/store/database/contract_event.go index 5c6b8fa64..e44246045 100644 --- a/nitronode/store/database/contract_event.go +++ b/nitronode/store/database/contract_event.go @@ -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 @@ -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 } diff --git a/nitronode/store/database/contract_event_test.go b/nitronode/store/database/contract_event_test.go index 1e14e5127..f52bc9816 100644 --- a/nitronode/store/database/contract_event_test.go +++ b/nitronode/store/database/contract_event_test.go @@ -1,6 +1,7 @@ package database import ( + "strings" "testing" "github.com/layer-3/nitrolite/pkg/core" @@ -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() diff --git a/playground/chart/Chart.yaml b/playground/chart/Chart.yaml index 7a3276b7d..b5eb11cf4 100644 --- a/playground/chart/Chart.yaml +++ b/playground/chart/Chart.yaml @@ -3,4 +3,4 @@ apiVersion: v2 description: Nitrolite Playground Helm chart name: playground version: 1.0.0 -appVersion: "1.4.0" +appVersion: "1.4.1" diff --git a/sdk/mcp/mcp-publisher b/sdk/mcp/mcp-publisher new file mode 100755 index 000000000..3b6d8e473 Binary files /dev/null and b/sdk/mcp/mcp-publisher differ diff --git a/sdk/mcp/mcp-publisher.tar.gz b/sdk/mcp/mcp-publisher.tar.gz new file mode 100644 index 000000000..e48afb001 Binary files /dev/null and b/sdk/mcp/mcp-publisher.tar.gz differ diff --git a/sdk/mcp/package-lock.json b/sdk/mcp/package-lock.json index 7ed47a12f..9e1727c36 100644 --- a/sdk/mcp/package-lock.json +++ b/sdk/mcp/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yellow-org/sdk-mcp", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yellow-org/sdk-mcp", - "version": "1.4.0", + "version": "1.4.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/sdk/mcp/package.json b/sdk/mcp/package.json index d6fc6be84..40d21a231 100644 --- a/sdk/mcp/package.json +++ b/sdk/mcp/package.json @@ -1,6 +1,6 @@ { "name": "@yellow-org/sdk-mcp", - "version": "1.4.0", + "version": "1.4.1", "description": "Unified MCP server for Yellow SDK and Nitrolite protocol context for AI agents and IDEs", "type": "module", "mcpName": "io.github.layer-3/yellow-sdk-mcp", diff --git a/sdk/mcp/server.json b/sdk/mcp/server.json index 9b2379386..c414f06c1 100644 --- a/sdk/mcp/server.json +++ b/sdk/mcp/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.layer-3/yellow-sdk-mcp", "description": "MCP server exposing Yellow SDK and Nitrolite protocol reference material to AI agents and IDEs.", - "version": "1.4.0", + "version": "1.4.1", "repository": { "url": "https://github.com/layer-3/nitrolite", "source": "github" @@ -11,7 +11,7 @@ { "registryType": "npm", "identifier": "@yellow-org/sdk-mcp", - "version": "1.4.0", + "version": "1.4.1", "transport": { "type": "stdio" } diff --git a/sdk/ts-compat/package-lock.json b/sdk/ts-compat/package-lock.json index 7960cda31..2b362215e 100644 --- a/sdk/ts-compat/package-lock.json +++ b/sdk/ts-compat/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yellow-org/sdk-compat", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yellow-org/sdk-compat", - "version": "1.4.0", + "version": "1.4.1", "license": "MIT", "dependencies": { "decimal.js": "^10.4.3" @@ -34,7 +34,7 @@ }, "../ts": { "name": "@yellow-org/sdk", - "version": "1.4.0", + "version": "1.4.1", "dev": true, "license": "MIT", "dependencies": { diff --git a/sdk/ts-compat/package.json b/sdk/ts-compat/package.json index dae04c4d4..d75643565 100644 --- a/sdk/ts-compat/package.json +++ b/sdk/ts-compat/package.json @@ -1,6 +1,6 @@ { "name": "@yellow-org/sdk-compat", - "version": "1.4.0", + "version": "1.4.1", "description": "Curated migration layer preserving selected Nitrolite SDK v0.5.3 app-facing APIs over the v1 runtime.", "type": "module", "sideEffects": false, diff --git a/sdk/ts/package-lock.json b/sdk/ts/package-lock.json index 18cd9a4ee..12ccd3a03 100644 --- a/sdk/ts/package-lock.json +++ b/sdk/ts/package-lock.json @@ -1,12 +1,12 @@ { "name": "@yellow-org/sdk", - "version": "1.4.0", + "version": "1.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@yellow-org/sdk", - "version": "1.4.0", + "version": "1.4.1", "license": "MIT", "dependencies": { "abitype": "^1.2.3", diff --git a/sdk/ts/package.json b/sdk/ts/package.json index 803eed3ca..12619f908 100644 --- a/sdk/ts/package.json +++ b/sdk/ts/package.json @@ -1,6 +1,6 @@ { "name": "@yellow-org/sdk", - "version": "1.4.0", + "version": "1.4.1", "description": "The Yellow SDK empowers developers to build high-performance, scalable web3 applications using state channels. It's designed to provide near-instant transactions and significantly improved user experiences by minimizing direct blockchain interactions.", "type": "module", "main": "dist/index.js",