diff --git a/cmd/misc/main.go b/cmd/misc/main.go index e163f50940..56c4eac521 100644 --- a/cmd/misc/main.go +++ b/cmd/misc/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" "context" + "database/sql" "encoding/json" "eth2-exporter/cmd/misc/commands" "eth2-exporter/db" @@ -22,9 +23,11 @@ import ( "time" "github.com/coocood/freecache" + "github.com/ethereum/go-ethereum/common" _ "github.com/jackc/pgx/v5/stdlib" "github.com/pkg/errors" utilMath "github.com/protolambda/zrnt/eth2/util/math" + go_ens "github.com/wealdtech/go-ens/v3" "golang.org/x/sync/errgroup" "flag" @@ -35,6 +38,7 @@ import ( var opts = struct { Command string User uint64 + Addresses string TargetVersion int64 StartEpoch uint64 EndEpoch uint64 @@ -75,6 +79,7 @@ func main() { flag.Uint64Var(&opts.BatchSize, "data.batchSize", 1000, "Batch size") flag.StringVar(&opts.Transformers, "transformers", "", "Comma separated list of transformers used by the eth1 indexer") flag.StringVar(&opts.ValidatorNameRanges, "validator-name-ranges", "https://config.dencun-devnet-8.ethpandaops.io/api/v1/nodes/validator-ranges", "url to or json of validator-ranges (format must be: {'ranges':{'X-Y':'name'}})") + flag.StringVar(&opts.Addresses, "addresses", "", "Comma separated list of addresses that should be processed by the command") flag.StringVar(&opts.Columns, "columns", "", "Comma separated list of columns that should be affected by the command") dryRun := flag.String("dry-run", "true", "if 'false' it deletes all rows starting with the key, per default it only logs the rows that would be deleted, but does not really delete them") versionFlag := flag.Bool("version", false, "Show version and exit") @@ -376,6 +381,10 @@ func main() { case "partition-validator-stats": statsPartitionCommand.Config.DryRun = opts.DryRun err = statsPartitionCommand.StartStatsPartitionCommand() + case "fix-ens": + err = fixEns(erigonClient) + case "fix-ens-addresses": + err = fixEnsAddresses(erigonClient) default: utils.LogFatal(nil, fmt.Sprintf("unknown command %s", opts.Command), 0) } @@ -387,6 +396,245 @@ func main() { } } +func fixEns(erigonClient *rpc.ErigonClient) error { + logrus.Infof("command: fix-ens") + addrs := []struct { + Address []byte `db:"address"` + EnsName string `db:"ens_name"` + }{} + err := db.WriterDb.Select(&addrs, `select address, ens_name from ens where is_primary_name = true`) + if err != nil { + return err + } + + logrus.Infof("found %v ens entries", len(addrs)) + + g := new(errgroup.Group) + g.SetLimit(10) // limit load on the node + + batchSize := 100 + total := len(addrs) + for i := 0; i < total; i += batchSize { + to := i + batchSize + if to > total { + to = total + } + batch := addrs[i:to] + + logrus.Infof("processing batch %v-%v / %v", i, to, total) + for _, addr := range batch { + addr := addr + g.Go(func() error { + ensAddr, err := go_ens.Resolve(erigonClient.GetNativeClient(), addr.EnsName) + if err != nil { + if err.Error() == "unregistered name" || + err.Error() == "no address" || + err.Error() == "no resolver" || + err.Error() == "abi: attempting to unmarshall an empty string while arguments are expected" || + strings.Contains(err.Error(), "execution reverted") || + err.Error() == "invalid jump destination" { + logrus.WithFields(logrus.Fields{"addr": fmt.Sprintf("%#x", addr.Address), "name": addr.EnsName, "reason": fmt.Sprintf("failed resolve: %v", err.Error())}).Warnf("deleting ens entry") + if !opts.DryRun { + _, err = db.WriterDb.Exec(`delete from ens where address = $1 and ens_name = $2`, addr.Address, addr.EnsName) + if err != nil { + return err + } + } + return nil + } + return err + } + + dbAddr := common.BytesToAddress(addr.Address) + if dbAddr.Cmp(ensAddr) != 0 { + logrus.WithFields(logrus.Fields{"addr": fmt.Sprintf("%#x", addr.Address), "name": addr.EnsName, "reason": fmt.Sprintf("dbAddr != resolved ensAddr: %#x != %#x", addr.Address, ensAddr.Bytes())}).Warnf("deleting ens entry") + if !opts.DryRun { + _, err = db.WriterDb.Exec(`delete from ens where address = $1 and ens_name = $2`, addr.Address, addr.EnsName) + if err != nil { + return err + } + } + } + + reverseName, err := go_ens.ReverseResolve(erigonClient.GetNativeClient(), dbAddr) + if err != nil { + if err.Error() == "not a resolver" || err.Error() == "no resolution" { + logrus.WithFields(logrus.Fields{"addr": fmt.Sprintf("%#x", addr.Address), "name": addr.EnsName, "reason": fmt.Sprintf("failed reverse-resolve: %v", err.Error())}).Warnf("updating ens entry: is_primary_name = false") + if !opts.DryRun { + _, err = db.WriterDb.Exec(`update ens set is_primary_name = false where address = $1 and ens_name = $2`, addr.Address, addr.EnsName) + if err != nil { + return err + } + } + return nil + } + return err + } + + if reverseName != addr.EnsName { + logrus.WithFields(logrus.Fields{"addr": fmt.Sprintf("%#x", addr.Address), "name": addr.EnsName, "reason": fmt.Sprintf("resolved != reverseResolved: %v != %v", addr.EnsName, reverseName)}).Warnf("updating ens entry: is_primary_name = false") + if !opts.DryRun { + _, err = db.WriterDb.Exec(`update ens set is_primary_name = false where address = $1 and ens_name = $2`, addr.Address, addr.EnsName) + if err != nil { + return err + } + } + } + + return nil + }) + } + + err = g.Wait() + if err != nil { + return err + } + time.Sleep(time.Millisecond * 100) + } + return nil +} + +func fixEnsAddresses(erigonClient *rpc.ErigonClient) error { + logrus.WithFields(logrus.Fields{"dry": opts.DryRun}).Infof("command: fix-ens-addresses") + if opts.Addresses == "" { + return errors.New("no addresses specified") + } + + type DbEntry struct { + NameHash []byte `db:"name_hash"` + EnsName string `db:"ens_name"` + Address []byte `db:"address"` + IsPrimaryName bool `db:"is_primary_name"` + ValidTo time.Time `db:"valid_to"` + } + + for _, addrHex := range strings.Split(opts.Addresses, ",") { + if !common.IsHexAddress(addrHex) { + return fmt.Errorf("invalid address: %v", addrHex) + } + + addr := common.HexToAddress(addrHex) + + dbEntry := &DbEntry{} + err := db.WriterDb.Get(dbEntry, `select name_hash, ens_name, address, is_primary_name, valid_to from ens where address = $1`, addr.Bytes()) + if err != nil && err != sql.ErrNoRows { + return fmt.Errorf("error getting ens entry for addr [%v]: %w", addr.Hex(), err) + } + if err == sql.ErrNoRows { + dbEntry = nil + } + + name, err := go_ens.ReverseResolve(erigonClient.GetNativeClient(), addr) + if err != nil { + if err.Error() == "not a resolver" || + err.Error() == "no resolution" { + logrus.WithFields(logrus.Fields{"addr": addr.Hex()}).Warnf("error reverse-resolving name: %v", err) + if dbEntry != nil { + logrus.WithFields(logrus.Fields{"addr": fmt.Sprintf("%v", addr.Hex()), "reason": fmt.Sprintf("error reverse-resolving name: %v", err)}).Warnf("deleting ens entry") + if !opts.DryRun { + _, err = db.WriterDb.Exec(`delete from ens where address = $1`, addr.Bytes()) + if err != nil { + return fmt.Errorf("error deleting ens entry: %w", err) + } + } + } + continue + } else { + return fmt.Errorf("error go_ens.ReverseResolve for addr %v: %w", addr.Hex(), err) + } + } + + if !strings.HasSuffix(name, ".eth") { + logrus.Infof("need to add .eth to %v for addr %v", name, addr.Hex()) + name = name + ".eth" + } + + resolvedAddr, err := go_ens.Resolve(erigonClient.GetNativeClient(), name) + if err != nil { + if err.Error() == "unregistered name" || + err.Error() == "no address" || + err.Error() == "no resolver" || + err.Error() == "abi: attempting to unmarshall an empty string while arguments are expected" || + strings.Contains(err.Error(), "execution reverted") || + err.Error() == "invalid jump destination" { + if dbEntry != nil { + logrus.WithFields(logrus.Fields{"addr": fmt.Sprintf("%v", addr.Hex()), "reason": fmt.Sprintf("error resolving name: %v", err)}).Warnf("deleting ens entry") + if !opts.DryRun { + _, err = db.WriterDb.Exec(`delete from ens where address = $1`, addr.Bytes()) + if err != nil { + return fmt.Errorf("error deleting ens entry: %w", err) + } + } + } + } else { + return fmt.Errorf("error go_ens.Resolve(%v) for addr %v: %w", name, addr.Hex(), err) + } + } + + if !bytes.Equal(resolvedAddr.Bytes(), addr.Bytes()) { + logrus.WithFields(logrus.Fields{"addr": fmt.Sprintf("%v", addr.Hex()), "reason": fmt.Sprintf("addr != resolvedAddr: %v != %v", addr.Hex(), resolvedAddr.Hex())}).Warnf("deleting ens entry") + if !opts.DryRun { + _, err = db.WriterDb.Exec(`delete from ens where address = $1`, addr.Bytes()) + if err != nil { + return fmt.Errorf("error deleting ens entry: %w", err) + } + } + } + + nameHash, err := go_ens.NameHash(name) + if err != nil { + return fmt.Errorf("error go_ens.NameHash(%v) for addr %v: %w", name, addr.Hex(), err) + } + parts := strings.Split(name, ".") + mainName := strings.Join(parts[len(parts)-2:], ".") + ensName, err := go_ens.NewName(erigonClient.GetNativeClient(), mainName) + if err != nil { + return fmt.Errorf("error could not create name via go_ens.NewName for [%v]: %w", name, err) + } + expires, err := ensName.Expires() + if err != nil { + return fmt.Errorf("error could not get ens expire date for [%v]: %w", name, err) + } + + if dbEntry == nil || dbEntry.EnsName != name || !bytes.Equal(dbEntry.NameHash, nameHash[:]) || !bytes.Equal(dbEntry.Address, resolvedAddr.Bytes()) || dbEntry.ValidTo != expires { + logFields := logrus.Fields{"resolvedAddr": resolvedAddr, "addr": addr.Hex(), "name": name, "nameHash": fmt.Sprintf("%#x", nameHash), "expires": expires} + if dbEntry == nil { + logFields["db"] = "nil" + logrus.WithFields(logFields).Warnf("adding ens entry") + } else { + logFields["db.name"] = dbEntry.EnsName + logFields["db.nameHash"] = fmt.Sprintf("%#x", dbEntry.NameHash) + logFields["db.addr"] = fmt.Sprintf("%#x", dbEntry.Address) + logFields["db.expire"] = dbEntry.ValidTo + logrus.WithFields(logFields).Warnf("updating ens entry") + } + + if !opts.DryRun { + _, err = db.WriterDb.Exec(` + INSERT INTO ens ( + name_hash, + ens_name, + address, + is_primary_name, + valid_to) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT + (name_hash) + DO UPDATE SET + ens_name = excluded.ens_name, + address = excluded.address, + is_primary_name = excluded.is_primary_name, + valid_to = excluded.valid_to`, + nameHash[:], name, addr.Bytes(), true, expires) + if err != nil { + return fmt.Errorf("error writing ens data for addr [%v]: %w", addr.Hex(), err) + } + } + } + } + return nil +} + func fixExecTransactionsCount() error { startBlockNumber := uint64(opts.StartBlock) endBlockNumber := uint64(opts.EndBlock) diff --git a/db/ens.go b/db/ens.go index 78ed3fad47..9932b18164 100644 --- a/db/ens.go +++ b/db/ens.go @@ -512,20 +512,21 @@ func validateEnsName(client *ethclient.Client, name string, alreadyChecked *EnsC addr, err := go_ens.Resolve(client, name) if err != nil { utils.LogError(err, "error, could not resolve name", 0, map[string]interface{}{"name": name}) - /* if err.Error() == "unregistered name" || - err.Error() == "no address" || - err.Error() == "no resolver" || - err.Error() == "abi: attempting to unmarshall an empty string while arguments are expected" || - strings.Contains(err.Error(), "execution reverted") || - err.Error() == "invalid jump destination" { */ - // the given name is not available anymore or resolving it did not work properly => we can remove it from the db (if it is there) - err = removeEnsName(client, name) - if err != nil { - return fmt.Errorf("error removing ens name [%v]: %w", name, err) + if err.Error() == "unregistered name" || + err.Error() == "no address" || + err.Error() == "no resolver" || + err.Error() == "abi: attempting to unmarshall an empty string while arguments are expected" || + strings.Contains(err.Error(), "execution reverted") || + err.Error() == "invalid jump destination" || + err.Error() == "invalid opcode: INVALID" { + // the given name is not available anymore or resolving it did not work properly => we can remove it from the db (if it is there) + err = removeEnsName(client, name) + if err != nil { + return fmt.Errorf("error removing ens name after resolve failed [%v]: %w", name, err) + } + return nil } - return nil - // } - // return fmt.Errorf("error could not resolve name [%v]: %w", name, err) + return fmt.Errorf("error could not resolve name [%v]: %w", name, err) } // we need to get the main domain to get the expiration date @@ -545,13 +546,11 @@ func validateEnsName(client *ethclient.Client, name string, alreadyChecked *EnsC reverseName, err := go_ens.ReverseResolve(client, addr) if err != nil { if err.Error() == "not a resolver" || err.Error() == "no resolution" { - logger.Warnf("reverse resolving address [%v] resulted in a skippable error [%s], skipping it", addr, err.Error()) - return nil + logger.Warnf("reverse resolving address [%v] for name [%v] resulted in an error [%s], marking entry as not primary", addr, name, err.Error()) + } else { + return fmt.Errorf("error could not reverse resolve address [%v]: %w", addr, err) } - - return fmt.Errorf("error could not reverse resolve address [%v]: %w", addr, err) } - if reverseName == name { isPrimary = true } diff --git a/handlers/ens.go b/handlers/ens.go index 9505c7bd5c..76a60dd0c3 100644 --- a/handlers/ens.go +++ b/handlers/ens.go @@ -47,19 +47,26 @@ func GetEnsDomain(search string) (*types.EnsDomainResponse, error) { var returnError error if utils.IsValidEnsDomain(search) { - data.Domain = search - cacheKey := fmt.Sprintf("%d:ens:address:%v", utils.Config.Chain.ClConfig.DepositChainID, search) if address, err := cache.TieredCache.GetStringWithLocalTimeout(cacheKey, time.Minute); err == nil && len(address) > 0 { data.Address = address return data, nil } + address, err := db.GetAddressForEnsName(search) if err != nil { + data.Domain = search return data, err // We want to return the data if it was a valid domain even if there was an error getting the address from bigtable. A valid domain might be enough for the caller. } data.Address = address.Hex() + + name, err := db.GetEnsNameForAddress(*address) + if err != nil { + return data, err // We want to return the data if it was a valid address even if there was an error getting the domain from bigtable. A valid address might be enough for the caller. + } + data.Domain = *name + err = cache.TieredCache.SetString(cacheKey, data.Address, time.Minute) if err != nil { logger.Errorf("error caching ens address: %v", err)