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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ tmp-*
.hack/devnet/custom-**

cmd/statetransition-test/

ai_plans/
21 changes: 12 additions & 9 deletions config/default.config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,19 +189,22 @@ rpcProxy:
- "eth_chainId" # Network chain ID
- "net_version" # Network version

# Optional ENS resolver: resolves execution addresses to their primary ENS name.
# Optional ENS resolver: resolves execution addresses to their primary ENS name on the
# local network (via the main execution pool) and on optional remote networks.
ensResolver:
enabled: false # enable ENS name resolution & display
# optional dedicated EL RPC endpoints (usually Ethereum mainnet). If empty, an
# available client from the main execution pool is used.
endpoints: []
# - url: "https://ethereum-rpc.publicnode.com"
# name: "mainnet"
# ENS registries to query, in priority order (first verified name wins). Each is
# probed for bytecode on the target chain and skipped if not deployed.
# ENS registries on the local network (the chain this explorer indexes), in priority
# order (first verified name wins). Each is probed for bytecode and skipped while not
# deployed (probing is retried periodically, so late deployments are picked up).
registryAddresses:
- "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" # ENS registry (mainnet)
- "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" # ENS registry (canonical address)
multicallAddress: "0xcA11bde05977b3631167028862bE2a173976CA11" # Multicall3 (batches lookups when deployed)
# Additional networks to resolve ENS names on, each with dedicated EL RPC endpoints.
# registryAddresses/multicallAddress fall back to the values configured above.
remoteNetworks: []
# - name: "mainnet"
# endpoints:
# - url: "https://ethereum-rpc.publicnode.com"
refreshPositive: 24h # re-resolve interval for addresses that have a name
refreshNegative: 6h # re-resolve interval for addresses without a name
batchSize: 100 # addresses resolved per worker batch
Expand Down
54 changes: 39 additions & 15 deletions db/ens_names.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ import (
"github.com/jmoiron/sqlx"
)

// GetEnsNamesByAddresses retrieves persisted ENS lookups (positive and negative)
// for multiple addresses in a single query. Returns a map from address (hex string)
// to the stored entry for efficient lookup.
func GetEnsNamesByAddresses(ctx context.Context, addresses [][]byte) (map[string]*dbtypes.EnsName, error) {
// GetEnsNamesByAddresses retrieves persisted ENS lookups (positive and negative, all
// networks) for multiple addresses in a single query. Returns a map from address
// (hex string) to the stored per-network entries for efficient lookup.
func GetEnsNamesByAddresses(ctx context.Context, addresses [][]byte) (map[string][]*dbtypes.EnsName, error) {
if len(addresses) == 0 {
return make(map[string]*dbtypes.EnsName), nil
return make(map[string][]*dbtypes.EnsName), nil
}

var sql strings.Builder
args := make([]any, len(addresses))

fmt.Fprint(&sql, "SELECT address, name, resolved_time FROM ens_names WHERE address IN (")
fmt.Fprint(&sql, "SELECT address, network, name, resolved_time FROM ens_names WHERE address IN (")
for i, addr := range addresses {
args[i] = addr
}
Expand All @@ -34,39 +34,63 @@ func GetEnsNamesByAddresses(ctx context.Context, addresses [][]byte) (map[string
return nil, err
}

result := make(map[string]*dbtypes.EnsName, len(names))
result := make(map[string][]*dbtypes.EnsName, len(names))
for _, name := range names {
result[byteSliceMapKey(name.Address)] = name
key := byteSliceMapKey(name.Address)
result[key] = append(result[key], name)
}
return result, nil
}

// GetEnsNamesByPrefix retrieves persisted positive ENS lookups whose name starts with
// the given prefix (case-sensitive; stored names are lowercase), across all networks.
func GetEnsNamesByPrefix(ctx context.Context, prefix string, limit int) ([]*dbtypes.EnsName, error) {
if prefix == "" || limit <= 0 {
return nil, nil
}

escaped := strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(prefix)

names := []*dbtypes.EnsName{}
err := ReaderDb.SelectContext(ctx, &names, `
SELECT address, network, name, resolved_time
FROM ens_names
WHERE name LIKE $1 ESCAPE '\' AND name != ''
ORDER BY name ASC
LIMIT $2`, escaped+"%", limit)
if err != nil {
logger.Errorf("Error while fetching ens names by prefix: %v", err)
return nil, err
}
return names, nil
}

// InsertEnsNames upserts a batch of ENS lookups (positive and negative), refreshing
// the name and resolved_time for addresses that already exist.
// the name and resolved_time for (address, network) pairs that already exist.
func InsertEnsNames(ctx context.Context, dbTx *sqlx.Tx, names []*dbtypes.EnsName) error {
if len(names) == 0 {
return nil
}

var sql strings.Builder
args := make([]any, 0, len(names)*3)
args := make([]any, 0, len(names)*4)

fmt.Fprint(&sql, EngineQuery(map[dbtypes.DBEngineType]string{
dbtypes.DBEnginePgsql: "INSERT INTO ens_names (address, name, resolved_time) VALUES ",
dbtypes.DBEngineSqlite: "INSERT OR REPLACE INTO ens_names (address, name, resolved_time) VALUES ",
dbtypes.DBEnginePgsql: "INSERT INTO ens_names (address, network, name, resolved_time) VALUES ",
dbtypes.DBEngineSqlite: "INSERT OR REPLACE INTO ens_names (address, network, name, resolved_time) VALUES ",
}))

for i, name := range names {
if i > 0 {
fmt.Fprint(&sql, ", ")
}
argIdx := len(args) + 1
fmt.Fprintf(&sql, "($%d, $%d, $%d)", argIdx, argIdx+1, argIdx+2)
args = append(args, name.Address, name.Name, name.ResolvedTime)
fmt.Fprintf(&sql, "($%d, $%d, $%d, $%d)", argIdx, argIdx+1, argIdx+2, argIdx+3)
args = append(args, name.Address, name.Network, name.Name, name.ResolvedTime)
}

fmt.Fprint(&sql, EngineQuery(map[dbtypes.DBEngineType]string{
dbtypes.DBEnginePgsql: " ON CONFLICT (address) DO UPDATE SET name = excluded.name, resolved_time = excluded.resolved_time",
dbtypes.DBEnginePgsql: " ON CONFLICT (address, network) DO UPDATE SET name = excluded.name, resolved_time = excluded.resolved_time",
}))

_, err := dbTx.ExecContext(ctx, sql.String(), args...)
Expand Down
16 changes: 16 additions & 0 deletions db/schema/pgsql/20260814000000_ens-names-network.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE public."ens_names" ADD COLUMN "network" TEXT NOT NULL DEFAULT '';
ALTER TABLE public."ens_names" DROP CONSTRAINT "ens_names_pkey";
ALTER TABLE public."ens_names" ADD CONSTRAINT "ens_names_pkey" PRIMARY KEY ("address", "network");
CREATE INDEX IF NOT EXISTS "ens_names_name_idx" ON public."ens_names" ("name") WHERE "name" != '';
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS "ens_names_name_idx";
DELETE FROM public."ens_names" WHERE "network" != '';
ALTER TABLE public."ens_names" DROP CONSTRAINT "ens_names_pkey";
ALTER TABLE public."ens_names" ADD CONSTRAINT "ens_names_pkey" PRIMARY KEY ("address");
ALTER TABLE public."ens_names" DROP COLUMN "network";
-- +goose StatementEnd
46 changes: 46 additions & 0 deletions db/schema/sqlite/20260814000000_ens-names-network.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE "ens_names_new" (
address BLOB NOT NULL,
network TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
resolved_time INTEGER NOT NULL DEFAULT 0,
CONSTRAINT ens_names_pkey PRIMARY KEY (address, network)
);
-- +goose StatementEnd
-- +goose StatementBegin
INSERT INTO "ens_names_new" (address, network, name, resolved_time)
SELECT address, '', name, resolved_time FROM "ens_names";
-- +goose StatementEnd
-- +goose StatementBegin
DROP TABLE "ens_names";
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE "ens_names_new" RENAME TO "ens_names";
-- +goose StatementEnd
-- +goose StatementBegin
CREATE INDEX IF NOT EXISTS "ens_names_name_idx" ON "ens_names" ("name") WHERE "name" != '';
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP INDEX IF EXISTS "ens_names_name_idx";
-- +goose StatementEnd
-- +goose StatementBegin
CREATE TABLE "ens_names_old" (
address BLOB NOT NULL,
name TEXT NOT NULL DEFAULT '',
resolved_time INTEGER NOT NULL DEFAULT 0,
CONSTRAINT ens_names_pkey PRIMARY KEY (address)
);
-- +goose StatementEnd
-- +goose StatementBegin
INSERT INTO "ens_names_old" (address, name, resolved_time)
SELECT address, name, resolved_time FROM "ens_names" WHERE network = '';
-- +goose StatementEnd
-- +goose StatementBegin
DROP TABLE "ens_names";
-- +goose StatementEnd
-- +goose StatementBegin
ALTER TABLE "ens_names_old" RENAME TO "ens_names";
-- +goose StatementEnd
6 changes: 4 additions & 2 deletions dbtypes/dbtypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -677,10 +677,12 @@ type ElAccount struct {
LastBlockUid uint64 `db:"last_block_uid"`
}

// EnsName holds a resolved primary ENS name for an execution address.
// An empty Name is a persisted negative result (address has no primary name).
// EnsName holds a resolved primary ENS name for an execution address on one network.
// An empty Network is the local network (the chain this explorer indexes); an empty
// Name is a persisted negative result (address has no primary name on that network).
type EnsName struct {
Address []byte `db:"address"`
Network string `db:"network"`
Name string `db:"name"`
ResolvedTime int64 `db:"resolved_time"`
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ require (
github.com/rs/zerolog v1.35.1
github.com/sirupsen/logrus v1.10.0
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d
Expand Down Expand Up @@ -154,7 +155,6 @@ require (
github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/swaggo/files v1.0.1 // indirect
github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e // indirect
github.com/tinylib/msgp v1.6.1 // indirect
Expand Down
7 changes: 5 additions & 2 deletions handlers/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,11 @@ func buildAddressPageData(ctx context.Context, addressBytes []byte, tabView stri
ensNames := resolveEnsNames(ctx, ensAddrs)
pageData.SetEnsNames(ensNames)
// The page's own address is rendered full (not as a swappable link), so surface its
// name explicitly for the header to show alongside the address.
pageData.AddressEnsName = ensNames[strings.ToLower(common.BytesToAddress(pageData.Address).Hex())]
// names explicitly for the header/info rows to show alongside the address.
pageData.AddressEnsNames = ensNames[strings.ToLower(common.BytesToAddress(pageData.Address).Hex())]
if len(pageData.AddressEnsNames) > 0 {
pageData.AddressEnsName = pageData.AddressEnsNames[0].Name
}

return pageData, 2 * time.Minute
}
Expand Down
23 changes: 18 additions & 5 deletions handlers/pageData.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/ethpandaops/dora/services"
"github.com/ethpandaops/dora/types"
"github.com/ethpandaops/dora/types/models"
"github.com/ethpandaops/dora/utils"
)

Expand Down Expand Up @@ -59,6 +60,7 @@ func InitPageData(w http.ResponseWriter, r *http.Request, active, path, title st
MainMenuItems: createMenuItems(active),
ApiEnabled: utils.Config.Api.Enabled && !utils.Config.Api.RequireAuth,
ExecutionIndexerEnabled: utils.Config.ExecutionIndexer.Enabled,
EnsSearchEnabled: ensSearchEnabled(),
}

chainState := services.GlobalBeaconService.GetChainState()
Expand Down Expand Up @@ -395,18 +397,29 @@ func handleTemplateError(w http.ResponseWriter, r *http.Request, fileIdentifier
return err
}

// resolveEnsNames resolves the primary ENS names for the given execution addresses via
// the ENS resolver service. It returns an address->name map (empty when the resolver is
// disabled) and enqueues unresolved/stale addresses for asynchronous resolution.
// resolveEnsNames resolves the ENS names for the given execution addresses on all
// configured networks via the ENS resolver service. It returns a map from lowercase
// 0x-hex address to the per-network names (display name first; empty map when the
// resolver is disabled) and enqueues unresolved/stale addresses for asynchronous
// resolution.
//
// Handlers call this once per page build with all addresses shown on the page; the
// result is stored on the page model (models.EnsNameData) and rendered client-side.
func resolveEnsNames(ctx context.Context, addrs [][]byte) map[string]string {
func resolveEnsNames(ctx context.Context, addrs [][]byte) map[string][]models.EnsNameEntry {
ensResolver := services.GlobalBeaconService.GetEnsResolver()
if ensResolver == nil {
return nil
}
return ensResolver.ResolveNames(ctx, addrs)
resolved := ensResolver.ResolveNames(ctx, addrs)
names := make(map[string][]models.EnsNameEntry, len(resolved))
for addr, entries := range resolved {
list := make([]models.EnsNameEntry, 0, len(entries))
for _, entry := range entries {
list = append(list, models.EnsNameEntry{Name: entry.Name, Network: entry.Network, Local: entry.Local})
}
names[addr] = list
}
return names
}

// appendEnsHexAddrs appends the 20-byte form of 0x-hex address strings to dst, skipping
Expand Down
Loading
Loading