diff --git a/go.mod b/go.mod index 53dd7373ae..f78e61a07a 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/d4l3k/messagediff v1.2.1 // indirect github.com/deckarep/golang-set v1.7.1 // indirect github.com/dgraph-io/ristretto v0.0.3 + github.com/dustin/go-humanize v1.0.0 github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 github.com/edsrzf/mmap-go v1.0.0 // indirect github.com/elastic/gosigar v0.10.5 // indirect diff --git a/go.sum b/go.sum index 77db167bb4..e26beeb3de 100644 --- a/go.sum +++ b/go.sum @@ -185,6 +185,7 @@ github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dop251/goja v0.0.0-20200219165308-d1232e640a87 h1:OMbqMXf9OAXzH1dDH82mQMrddBE8LIIwDtxeK4wE1/A= github.com/dop251/goja v0.0.0-20200219165308-d1232e640a87/go.mod h1:Mw6PkjjMXWbTj+nnj4s3QPXq1jaT0s5pC0iFD4+BOAA= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0 h1:90Ly+6UfUypEF6vvvW5rQIv9opIL8CbmW9FT20LDQoY= github.com/dustinkirkland/golang-petname v0.0.0-20191129215211-8e5a1ed0cff0/go.mod h1:V+Qd57rJe8gd4eiGzZyg4h54VLHmYVVw54iMnlAMrF8= diff --git a/nogo_config.json b/nogo_config.json index ca1a2b869b..ff7abcd8f4 100644 --- a/nogo_config.json +++ b/nogo_config.json @@ -107,7 +107,8 @@ "exclude_files": { ".*/.*_test\\.go": "Tests are OK to use weak crypto", "shared/rand/rand\\.go": "Abstracts CSPRNGs for common use", - "shared/aggregation/testing/bitlistutils.go": "Test-only package" + "shared/aggregation/testing/bitlistutils.go": "Test-only package", + "shared/petnames/names.go": "Needs deterministic randomness" } } } diff --git a/shared/petnames/BUILD.bazel b/shared/petnames/BUILD.bazel new file mode 100644 index 0000000000..89587d58d3 --- /dev/null +++ b/shared/petnames/BUILD.bazel @@ -0,0 +1,9 @@ +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["names.go"], + importpath = "github.com/prysmaticlabs/prysm/shared/petnames", + visibility = ["//visibility:public"], + deps = ["//shared/hashutil:go_default_library"], +) diff --git a/shared/petnames/names.go b/shared/petnames/names.go new file mode 100644 index 0000000000..e2b1d0c25f --- /dev/null +++ b/shared/petnames/names.go @@ -0,0 +1,26 @@ +package petnames + +import ( + "math/rand" + "strings" + + "github.com/prysmaticlabs/prysm/shared/hashutil" +) + +var ( + adjectives = []string{"able", "above", "absolute", "accepted", "accurate", "ace", "active", "actual", "adapted", "adapting", "adequate", "adjusted", "advanced", "alert", "alive", "allowed", "allowing", "amazed", "amazing", "ample", "amused", "amusing", "apparent", "apt", "arriving", "artistic", "assured", "assuring", "awaited", "awake", "aware", "balanced", "becoming", "beloved", "better", "big", "blessed", "bold", "boss", "brave", "brief", "bright", "bursting", "busy", "calm", "capable", "capital", "careful", "caring", "casual", "causal", "central", "certain", "champion", "charmed", "charming", "cheerful", "chief", "choice", "civil", "classic", "clean", "clear", "clever", "climbing", "close", "closing", "coherent", "comic", "communal", "complete", "composed", "concise", "concrete", "content", "cool", "correct", "cosmic", "crack", "creative", "credible", "crisp", "crucial", "cuddly", "cunning", "curious", "current", "cute", "daring", "darling", "dashing", "dear", "decent", "deciding", "deep", "definite", "delicate", "desired", "destined", "devoted", "direct", "discrete", "distinct", "diverse", "divine", "dominant", "driven", "driving", "dynamic", "eager", "easy", "electric", "elegant", "emerging", "eminent", "enabled", "enabling", "endless", "engaged", "engaging", "enhanced", "enjoyed", "enormous", "enough", "epic", "equal", "equipped", "eternal", "ethical", "evident", "evolved", "evolving", "exact", "excited", "exciting", "exotic", "expert", "factual", "fair", "faithful", "famous", "fancy", "fast", "feasible", "fine", "finer", "firm", "first", "fit", "fitting", "fleet", "flexible", "flowing", "fluent", "flying", "fond", "frank", "free", "fresh", "full", "fun", "funky", "funny", "game", "generous", "gentle", "genuine", "giving", "glad", "glorious", "glowing", "golden", "good", "gorgeous", "grand", "grateful", "great", "growing", "grown", "guided", "guiding", "handy", "happy", "hardy", "harmless", "healthy", "helped", "helpful", "helping", "heroic", "hip", "holy", "honest", "hopeful", "hot", "huge", "humane", "humble", "humorous", "ideal", "immense", "immortal", "immune", "improved", "in", "included", "infinite", "informed", "innocent", "inspired", "integral", "intense", "intent", "internal", "intimate", "inviting", "joint", "just", "keen", "key", "kind", "knowing", "known", "large", "lasting", "leading", "learning", "legal", "legible", "lenient", "liberal", "light", "liked", "literate", "live", "living", "logical", "loved", "loving", "loyal", "lucky", "magical", "magnetic", "main", "major", "many", "massive", "master", "mature", "maximum", "measured", "meet", "merry", "mighty", "mint", "model", "modern", "modest", "moral", "more", "moved", "moving", "musical", "mutual", "national", "native", "natural", "nearby", "neat", "needed", "neutral", "new", "next", "nice", "noble", "normal", "notable", "noted", "novel", "obliging", "on", "one", "open", "optimal", "optimum", "organic", "oriented", "outgoing", "patient", "peaceful", "perfect", "pet", "picked", "pleasant", "pleased", "pleasing", "poetic", "polished", "polite", "popular", "positive", "possible", "powerful", "precious", "precise", "premium", "prepared", "present", "pretty", "primary", "prime", "pro", "probable", "profound", "promoted", "prompt", "proper", "proud", "proven", "pumped", "pure", "quality", "quick", "quiet", "rapid", "rare", "rational", "ready", "real", "refined", "regular", "related", "relative", "relaxed", "relaxing", "relevant", "relieved", "renewed", "renewing", "resolved", "rested", "rich", "right", "robust", "romantic", "ruling", "sacred", "safe", "saved", "saving", "secure", "select", "selected", "sensible", "set", "settled", "settling", "sharing", "sharp", "shining", "simple", "sincere", "singular", "skilled", "smart", "smashing", "smiling", "smooth", "social", "solid", "sought", "sound", "special", "splendid", "square", "stable", "star", "steady", "sterling", "still", "stirred", "stirring", "striking", "strong", "stunning", "subtle", "suitable", "suited", "summary", "sunny", "super", "superb", "supreme", "sure", "sweeping", "sweet", "talented", "teaching", "tender", "thankful", "thorough", "tidy", "tight", "together", "tolerant", "top", "topical", "tops", "touched", "touching", "tough", "true", "trusted", "trusting", "trusty", "ultimate", "unbiased", "uncommon", "unified", "unique", "united", "up", "upright", "upward", "usable", "useful", "valid", "valued", "vast", "verified", "viable", "vital", "vocal", "wanted", "warm", "wealthy", "welcome", "welcomed", "well", "whole", "willing", "winning", "wired", "wise", "witty", "wondrous", "workable", "working", "worthy"} + adverbs = []string{"abnormally", "absolutely", "accurately", "actively", "actually", "adequately", "admittedly", "adversely", "allegedly", "amazingly", "annually", "apparently", "arguably", "awfully", "badly", "barely", "basically", "blatantly", "blindly", "briefly", "brightly", "broadly", "carefully", "centrally", "certainly", "cheaply", "cleanly", "clearly", "closely", "commonly", "completely", "constantly", "conversely", "correctly", "curiously", "currently", "daily", "deadly", "deeply", "definitely", "directly", "distinctly", "duly", "eagerly", "early", "easily", "eminently", "endlessly", "enormously", "entirely", "equally", "especially", "evenly", "evidently", "exactly", "explicitly", "externally", "extremely", "factually", "fairly", "finally", "firmly", "firstly", "forcibly", "formally", "formerly", "frankly", "freely", "frequently", "friendly", "fully", "generally", "gently", "genuinely", "ghastly", "gladly", "globally", "gradually", "gratefully", "greatly", "grossly", "happily", "hardly", "heartily", "heavily", "hideously", "highly", "honestly", "hopefully", "hopelessly", "horribly", "hugely", "humbly", "ideally", "illegally", "immensely", "implicitly", "incredibly", "indirectly", "infinitely", "informally", "inherently", "initially", "instantly", "intensely", "internally", "jointly", "jolly", "kindly", "largely", "lately", "legally", "lightly", "likely", "literally", "lively", "locally", "logically", "loosely", "loudly", "lovely", "luckily", "mainly", "manually", "marginally", "mentally", "merely", "mildly", "miserably", "mistakenly", "moderately", "monthly", "morally", "mostly", "multiply", "mutually", "namely", "nationally", "naturally", "nearly", "neatly", "needlessly", "newly", "nicely", "nominally", "normally", "notably", "noticeably", "obviously", "oddly", "officially", "only", "openly", "optionally", "overly", "painfully", "partially", "partly", "perfectly", "personally", "physically", "plainly", "pleasantly", "poorly", "positively", "possibly", "precisely", "preferably", "presently", "presumably", "previously", "primarily", "privately", "probably", "promptly", "properly", "publicly", "purely", "quickly", "quietly", "radically", "randomly", "rapidly", "rarely", "rationally", "readily", "really", "reasonably", "recently", "regularly", "reliably", "remarkably", "remotely", "repeatedly", "rightly", "roughly", "routinely", "sadly", "safely", "scarcely", "secondly", "secretly", "seemingly", "sensibly", "separately", "seriously", "severely", "sharply", "shortly", "similarly", "simply", "sincerely", "singularly", "slightly", "slowly", "smoothly", "socially", "solely", "specially", "steadily", "strangely", "strictly", "strongly", "subtly", "suddenly", "suitably", "supposedly", "surely", "terminally", "terribly", "thankfully", "thoroughly", "tightly", "totally", "trivially", "truly", "typically", "ultimately", "unduly", "uniformly", "uniquely", "unlikely", "urgently", "usefully", "usually", "utterly", "vaguely", "vastly", "verbally", "vertically", "vigorously", "violently", "virtually", "visually", "weekly", "wholly", "widely", "wildly", "willingly", "wrongly", "yearly"} + names = []string{"ox", "ant", "ape", "asp", "bat", "bee", "boa", "bug", "cat", "cod", "cow", "cub", "doe", "dog", "eel", "eft", "elf", "elk", "emu", "ewe", "fly", "fox", "gar", "gnu", "hen", "hog", "imp", "jay", "kid", "kit", "koi", "lab", "man", "owl", "pig", "pug", "pup", "ram", "rat", "ray", "yak", "bass", "bear", "bird", "boar", "buck", "bull", "calf", "chow", "clam", "colt", "crab", "crow", "dane", "deer", "dodo", "dory", "dove", "drum", "duck", "fawn", "fish", "flea", "foal", "fowl", "frog", "gnat", "goat", "grub", "gull", "hare", "hawk", "ibex", "joey", "kite", "kiwi", "lamb", "lark", "lion", "loon", "lynx", "mako", "mink", "mite", "mole", "moth", "mule", "mutt", "newt", "orca", "oryx", "pika", "pony", "puma", "seal", "shad", "slug", "sole", "stag", "stud", "swan", "tahr", "teal", "tick", "toad", "tuna", "wasp", "wolf", "worm", "wren", "yeti", "adder", "akita", "alien", "aphid", "bison", "boxer", "bream", "bunny", "burro", "camel", "chimp", "civet", "cobra", "coral", "corgi", "crane", "dingo", "drake", "eagle", "egret", "filly", "finch", "gator", "gecko", "ghost", "ghoul", "goose", "guppy", "heron", "hippo", "horse", "hound", "husky", "hyena", "koala", "krill", "leech", "lemur", "liger", "llama", "louse", "macaw", "midge", "molly", "moose", "moray", "mouse", "panda", "perch", "prawn", "quail", "racer", "raven", "rhino", "robin", "satyr", "shark", "sheep", "shrew", "skink", "skunk", "sloth", "snail", "snake", "snipe", "squid", "stork", "swift", "swine", "tapir", "tetra", "tiger", "troll", "trout", "viper", "wahoo", "whale", "zebra", "alpaca", "amoeba", "baboon", "badger", "beagle", "bedbug", "beetle", "bengal", "bobcat", "caiman", "cattle", "cicada", "collie", "condor", "cougar", "coyote", "dassie", "donkey", "dragon", "earwig", "falcon", "feline", "ferret", "gannet", "gibbon", "glider", "goblin", "gopher", "grouse", "guinea", "hermit", "hornet", "iguana", "impala", "insect", "jackal", "jaguar", "jennet", "kitten", "kodiak", "lizard", "locust", "maggot", "magpie", "mammal", "mantis", "marlin", "marmot", "marten", "martin", "mayfly", "minnow", "monkey", "mullet", "muskox", "ocelot", "oriole", "osprey", "oyster", "parrot", "pigeon", "piglet", "poodle", "possum", "python", "quagga", "rabbit", "raptor", "rodent", "roughy", "salmon", "sawfly", "serval", "shiner", "shrimp", "spider", "sponge", "tarpon", "thrush", "tomcat", "toucan", "turkey", "turtle", "urchin", "vervet", "walrus", "weasel", "weevil", "wombat", "anchovy", "anemone", "bluejay", "buffalo", "bulldog", "buzzard", "caribou", "catfish", "chamois", "cheetah", "chicken", "chigger", "cowbird", "crappie", "crawdad", "cricket", "dogfish", "dolphin", "firefly", "garfish", "gazelle", "gelding", "giraffe", "gobbler", "gorilla", "goshawk", "grackle", "griffon", "grizzly", "grouper", "haddock", "hagfish", "halibut", "hamster", "herring", "jackass", "javelin", "jawfish", "jaybird", "katydid", "ladybug", "lamprey", "lemming", "leopard", "lioness", "lobster", "macaque", "mallard", "mammoth", "manatee", "mastiff", "meerkat", "mollusk", "monarch", "mongrel", "monitor", "monster", "mudfish", "muskrat", "mustang", "narwhal", "oarfish", "octopus", "opossum", "ostrich", "panther", "peacock", "pegasus", "pelican", "penguin", "phoenix", "piranha", "polecat", "primate", "quetzal", "raccoon", "rattler", "redbird", "redfish", "reptile", "rooster", "sawfish", "sculpin", "seagull", "skylark", "snapper", "spaniel", "sparrow", "sunbeam", "sunbird", "sunfish", "tadpole", "termite", "terrier", "unicorn", "vulture", "wallaby", "walleye", "warthog", "whippet", "wildcat", "aardvark", "airedale", "albacore", "anteater", "antelope", "arachnid", "barnacle", "basilisk", "blowfish", "bluebird", "bluegill", "bonefish", "bullfrog", "cardinal", "chipmunk", "cockatoo", "crayfish", "dinosaur", "doberman", "duckling", "elephant", "escargot", "flamingo", "flounder", "foxhound", "glowworm", "goldfish", "grubworm", "hedgehog", "honeybee", "hookworm", "humpback", "kangaroo", "killdeer", "kingfish", "labrador", "lacewing", "ladybird", "lionfish", "longhorn", "mackerel", "malamute", "marmoset", "mamoswine", "moccasin", "mongoose", "monkfish", "mosquito", "pangolin", "parakeet", "pheasant", "pipefish", "platypus", "polliwog", "porpoise", "reindeer", "ringtail", "sailfish", "scorpion", "seahorse", "seasnail", "sheepdog", "shepherd", "silkworm", "squirrel", "stallion", "starfish", "starling", "stingray", "stinkbug", "sturgeon", "terrapin", "titmouse", "tortoise", "treefrog", "werewolf", "woodcock"} +) + +// DeterministicName returns a deterministic triple of adverb-adjective-name +// given a random seed for initialization. +func DeterministicName(seed []byte, separator string) string { + hashedValue := hashutil.FastSum64(seed) + rand.Seed(int64(hashedValue)) + adverb := adverbs[rand.Intn(len(adverbs)-1)] + adjective := adjectives[rand.Intn(len(adjectives)-1)] + name := names[rand.Intn(len(names)-1)] + petname := []string{adverb, adjective, name} + return strings.Join(petname, separator) +} diff --git a/validator/accounts/v2/BUILD.bazel b/validator/accounts/v2/BUILD.bazel index fa751f2c6b..2678565039 100644 --- a/validator/accounts/v2/BUILD.bazel +++ b/validator/accounts/v2/BUILD.bazel @@ -28,6 +28,7 @@ go_library( "//validator/keymanager/v2/derived:go_default_library", "//validator/keymanager/v2/direct:go_default_library", "//validator/keymanager/v2/remote:go_default_library", + "@com_github_dustin_go_humanize//:go_default_library", "@com_github_dustinkirkland_golang_petname//:go_default_library", "@com_github_logrusorgru_aurora//:go_default_library", "@com_github_manifoldco_promptui//:go_default_library", @@ -53,16 +54,16 @@ go_test( ], embed = [":go_default_library"], deps = [ - "//shared/bls:go_default_library", - "//shared/bytesutil:go_default_library", "//shared/testutil:go_default_library", "//shared/testutil/assert:go_default_library", "//shared/testutil/require:go_default_library", "//validator/flags:go_default_library", "//validator/keymanager/v2:go_default_library", + "//validator/keymanager/v2/derived:go_default_library", "//validator/keymanager/v2/direct:go_default_library", "//validator/keymanager/v2/remote:go_default_library", "//validator/keymanager/v2/testing:go_default_library", + "@com_github_dustin_go_humanize//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", ], diff --git a/validator/accounts/v2/iface/wallet.go b/validator/accounts/v2/iface/wallet.go index 50883fa136..bc03ce0ecc 100644 --- a/validator/accounts/v2/iface/wallet.go +++ b/validator/accounts/v2/iface/wallet.go @@ -17,6 +17,7 @@ type Wallet interface { ReadEncryptedSeedFromDisk(ctx context.Context) (io.ReadCloser, error) ReadPasswordForAccount(accountName string) (string, error) ReadFileForAccount(accountName string, fileName string) ([]byte, error) + ReadFileAtPath(ctx context.Context, filePath string, fileName string) ([]byte, error) // Write methods to persist important wallet and accounts-related files to disk. WriteFileAtPath(ctx context.Context, pathName string, fileName string, data []byte) error WriteEncryptedSeedToDisk(ctx context.Context, encoded []byte) error diff --git a/validator/accounts/v2/list.go b/validator/accounts/v2/list.go index a21b22281b..cd8c22aa24 100644 --- a/validator/accounts/v2/list.go +++ b/validator/accounts/v2/list.go @@ -7,10 +7,12 @@ import ( "strconv" "time" + "github.com/dustin/go-humanize" "github.com/logrusorgru/aurora" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/validator/flags" v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2" + "github.com/prysmaticlabs/prysm/validator/keymanager/v2/derived" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct" "github.com/urfave/cli/v2" ) @@ -39,9 +41,21 @@ func ListAccounts(cliCtx *cli.Context) error { showDepositData := cliCtx.Bool(flags.ShowDepositDataFlag.Name) switch wallet.KeymanagerKind() { case v2keymanager.Direct: - if err := listDirectKeymanagerAccounts(showDepositData, wallet, keymanager); err != nil { + km, ok := keymanager.(*direct.Keymanager) + if !ok { + log.Fatal("Could not assert keymanager interface to concrete type") + } + if err := listDirectKeymanagerAccounts(showDepositData, wallet, km); err != nil { log.Fatalf("Could not list validator accounts with direct keymanager: %v", err) } + case v2keymanager.Derived: + km, ok := keymanager.(*derived.Keymanager) + if !ok { + log.Fatal("Could not assert keymanager interface to concrete type") + } + if err := listDerivedKeymanagerAccounts(showDepositData, wallet, km); err != nil { + log.Fatalf("Could not list validator accounts with derived keymanager: %v", err) + } default: log.Fatalf("Keymanager kind %s not yet supported", wallet.KeymanagerKind().String()) } @@ -51,7 +65,7 @@ func ListAccounts(cliCtx *cli.Context) error { func listDirectKeymanagerAccounts( showDepositData bool, wallet *Wallet, - keymanager v2keymanager.IKeymanager, + keymanager *direct.Keymanager, ) error { // We initialize the wallet's keymanager. accountNames, err := wallet.AccountNames() @@ -88,12 +102,12 @@ func listDirectKeymanagerAccounts( if err != nil { return errors.Wrapf(err, "could not read file for account: %s", direct.TimestampFileName) } - unixTimestamp, err := strconv.ParseInt(string(createdAtBytes), 10, 64) + unixTimestampStr, err := strconv.ParseInt(string(createdAtBytes), 10, 64) if err != nil { return errors.Wrapf(err, "could not parse account created at timestamp: %s", createdAtBytes) } - unixTimestampStr := time.Unix(unixTimestamp, 0) - fmt.Printf("%s %v\n", au.BrightCyan("[created at]").Bold(), unixTimestampStr.String()) + unixTimestamp := time.Unix(unixTimestampStr, 0) + fmt.Printf("%s %s\n", au.BrightCyan("[created at]").Bold(), humanize.Time(unixTimestamp)) if !showDepositData { continue } @@ -117,3 +131,82 @@ func listDirectKeymanagerAccounts( fmt.Println("") return nil } + +func listDerivedKeymanagerAccounts( + showDepositData bool, + wallet *Wallet, + keymanager *derived.Keymanager, +) error { + au := aurora.NewAurora(true) + fmt.Println( + au.BrightRed("View the eth1 deposit transaction data for your accounts " + + "by running `validator accounts-v2 list --show-deposit-data"), + ) + dirPath := au.BrightCyan("(wallet dir)") + fmt.Printf("%s %s\n", dirPath, wallet.AccountsDir()) + fmt.Printf("(keymanager kind) %s\n", au.BrightGreen("derived, (HD) hierarchical-deterministic").Bold()) + fmt.Printf("(derivation format) %s\n", au.BrightGreen(keymanager.Config().DerivedPathStructure).Bold()) + ctx := context.Background() + validatingPubKeys, err := keymanager.FetchValidatingPublicKeys(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch validating public keys") + } + withdrawalPublicKeys, err := keymanager.FetchWithdrawalPublicKeys(ctx) + if err != nil { + return errors.Wrap(err, "could not fetch validating public keys") + } + nextAccountNumber := keymanager.NextAccountNumber(ctx) + currentAccountNumber := nextAccountNumber + if nextAccountNumber > 0 { + currentAccountNumber-- + } + accountNames, err := keymanager.AccountNames(ctx) + if err != nil { + return err + } + log.Info(currentAccountNumber) + for i := uint64(0); i <= currentAccountNumber; i++ { + fmt.Println("") + validatingKeyPath := fmt.Sprintf(derived.ValidatingKeyDerivationPathTemplate, i) + withdrawalKeyPath := fmt.Sprintf(derived.WithdrawalKeyDerivationPathTemplate, i) + + // Retrieve the withdrawal key account metadata. + createdAtBytes, err := wallet.ReadFileAtPath(ctx, validatingKeyPath, derived.TimestampFileName) + if err != nil { + return errors.Wrapf(err, "could not read file for account: %s", derived.TimestampFileName) + } + unixTimestampInt, err := strconv.ParseInt(string(createdAtBytes), 10, 64) + if err != nil { + return errors.Wrapf(err, "could not parse account created at timestamp: %s", createdAtBytes) + } + unixTimestamp := time.Unix(unixTimestampInt, 0) + fmt.Printf("%s | %s\n", au.BrightGreen(accountNames[i]).Bold(), humanize.Time(unixTimestamp)) + fmt.Printf("%s %#x\n", au.BrightMagenta("[withdrawal public key]").Bold(), withdrawalPublicKeys[i]) + fmt.Printf("%s %s\n", au.BrightMagenta("[derivation path]").Bold(), withdrawalKeyPath) + + // Retrieve the validating key account metadata. + fmt.Printf("%s %#x\n", au.BrightCyan("[validating public key]").Bold(), validatingPubKeys[i]) + fmt.Printf("%s %s\n", au.BrightCyan("[derivation path]").Bold(), validatingKeyPath) + + if !showDepositData { + continue + } + enc, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, derived.DepositTransactionFileName) + if err != nil { + return errors.Wrapf(err, "could not read file for account: %s", direct.DepositTransactionFileName) + } + fmt.Printf( + "%s %s\n", + "(deposit tx file)", + path.Join(wallet.AccountsDir(), withdrawalKeyPath, derived.DepositTransactionFileName), + ) + fmt.Printf(` +======================Deposit Transaction Data===================== + +%#x + +===================================================================`, enc) + fmt.Println(" ") + } + return nil +} diff --git a/validator/accounts/v2/list_test.go b/validator/accounts/v2/list_test.go index f3010596dd..f823cc069f 100644 --- a/validator/accounts/v2/list_test.go +++ b/validator/accounts/v2/list_test.go @@ -5,18 +5,17 @@ import ( "fmt" "io/ioutil" "os" - "path" "strconv" "strings" "testing" "time" - "github.com/prysmaticlabs/prysm/shared/bls" - "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/dustin/go-humanize" + "github.com/prysmaticlabs/prysm/shared/testutil/assert" "github.com/prysmaticlabs/prysm/shared/testutil/require" v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2" + "github.com/prysmaticlabs/prysm/validator/keymanager/v2/derived" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct" - mock "github.com/prysmaticlabs/prysm/validator/keymanager/v2/testing" ) func TestListAccounts_DirectKeymanager(t *testing.T) { @@ -29,42 +28,26 @@ func TestListAccounts_DirectKeymanager(t *testing.T) { KeymanagerKind: keymanagerKind, }) require.NoError(t, err) + keymanager, err := direct.NewKeymanager(ctx, wallet, direct.DefaultConfig(), true /* skip confirm */) + require.NoError(t, err) numAccounts := 5 - accountNames := make([]string, numAccounts) - pubKeys := make([][48]byte, numAccounts) depositDataForAccounts := make([][]byte, numAccounts) + accountCreationTimestamps := make([][]byte, numAccounts) for i := 0; i < numAccounts; i++ { - // Generate a new account name and write the account - // to disk using the wallet. - name, err := wallet.generateAccountName() + accountName, err := keymanager.CreateAccount(ctx, "hello world") + require.NoError(t, err) + depositData, err := wallet.ReadFileForAccount(accountName, direct.DepositTransactionFileName) require.NoError(t, err) - accountNames[i] = name - // Generate a directory for the account name and - // write its associated password to disk. - accountPath := path.Join(wallet.accountsPath, name) - require.NoError(t, os.MkdirAll(accountPath, DirectoryPermissions)) - require.NoError(t, wallet.writePasswordToFile(name, password)) - - // Write the deposit data for each account. - depositData := []byte(strconv.Itoa(i)) depositDataForAccounts[i] = depositData - require.NoError(t, wallet.WriteFileForAccount(ctx, name, direct.DepositTransactionFileName, depositData)) - - // Write the creation timestamp for the account with unix timestamp 0. - require.NoError(t, wallet.WriteFileForAccount(ctx, name, direct.TimestampFileName, []byte("0"))) - - // Create public keys for the accounts. - key := bls.RandKey() - pubKeys[i] = bytesutil.ToBytes48(key.PublicKey().Marshal()) + unixTimestamp, err := wallet.ReadFileForAccount(accountName, direct.TimestampFileName) + require.NoError(t, err) + accountCreationTimestamps[i] = unixTimestamp } rescueStdout := os.Stdout r, w, err := os.Pipe() require.NoError(t, err) os.Stdout = w - keymanager := &mock.MockKeymanager{ - PublicKeys: pubKeys, - } // We call the list direct keymanager accounts function. require.NoError(t, listDirectKeymanagerAccounts(true /* show deposit data */, wallet, keymanager)) @@ -84,6 +67,11 @@ func TestListAccounts_DirectKeymanager(t *testing.T) { t.Errorf("Did not find accounts path %s in output", wallet.accountsPath) } + accountNames, err := wallet.AccountNames() + require.NoError(t, err) + pubKeys, err := keymanager.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + for i := 0; i < numAccounts; i++ { accountName := accountNames[i] // Assert the account name is printed to stdout. @@ -104,8 +92,108 @@ func TestListAccounts_DirectKeymanager(t *testing.T) { } // Assert the account creation time is displayed - if !strings.Contains(stringOutput, fmt.Sprintf("%v", time.Unix(0, 0).String())) { - t.Error("Did not display account creation timestamp") - } + unixTimestampStr, err := strconv.ParseInt(string(accountCreationTimestamps[i]), 10, 64) + require.NoError(t, err) + unixTimestamp := time.Unix(unixTimestampStr, 0) + assert.Equal(t, strings.Contains(stringOutput, humanize.Time(unixTimestamp)), true) + } +} + +func TestListAccounts_DerivedKeymanager(t *testing.T) { + walletDir, passwordsDir := setupWalletDir(t) + keymanagerKind := v2keymanager.Derived + ctx := context.Background() + wallet, err := NewWallet(ctx, &WalletConfig{ + PasswordsDir: passwordsDir, + WalletDir: walletDir, + KeymanagerKind: keymanagerKind, + }) + require.NoError(t, err) + + password := "hello world" + seedConfig, err := derived.InitializeWalletSeedFile(ctx, password, true /* skip confirm */) + require.NoError(t, err) + + // Create a new wallet seed file and write it to disk. + seedConfigFile, err := derived.MarshalEncryptedSeedFile(ctx, seedConfig) + require.NoError(t, err) + require.NoError(t, wallet.WriteEncryptedSeedToDisk(ctx, seedConfigFile)) + + keymanager, err := derived.NewKeymanager( + ctx, + wallet, + derived.DefaultConfig(), + true, /* skip confirm */ + password, + ) + require.NoError(t, err) + numAccounts := 5 + depositDataForAccounts := make([][]byte, numAccounts) + accountCreationTimestamps := make([][]byte, numAccounts) + for i := 0; i < numAccounts; i++ { + _, err := keymanager.CreateAccount(ctx, password) + require.NoError(t, err) + withdrawalKeyPath := fmt.Sprintf(derived.WithdrawalKeyDerivationPathTemplate, i) + depositData, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, direct.DepositTransactionFileName) + require.NoError(t, err) + depositDataForAccounts[i] = depositData + unixTimestamp, err := wallet.ReadFileAtPath(ctx, withdrawalKeyPath, direct.TimestampFileName) + require.NoError(t, err) + accountCreationTimestamps[i] = unixTimestamp + } + + rescueStdout := os.Stdout + r, w, err := os.Pipe() + require.NoError(t, err) + os.Stdout = w + + // We call the list direct keymanager accounts function. + require.NoError(t, listDerivedKeymanagerAccounts(true /* show deposit data */, wallet, keymanager)) + + require.NoError(t, w.Close()) + out, err := ioutil.ReadAll(r) + require.NoError(t, err) + os.Stdout = rescueStdout + + // Assert the keymanager kind is printed to stdout. + stringOutput := string(out) + if !strings.Contains(stringOutput, wallet.KeymanagerKind().String()) { + t.Error("Did not find Keymanager kind in output") + } + + // Assert the wallet and passwords paths are in stdout. + if !strings.Contains(stringOutput, wallet.accountsPath) { + t.Errorf("Did not find accounts path %s in output", wallet.accountsPath) + } + + accountNames, err := keymanager.AccountNames(ctx) + require.NoError(t, err) + pubKeys, err := keymanager.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + + for i := 0; i < numAccounts; i++ { + accountName := accountNames[i] + // Assert the account name is printed to stdout. + if !strings.Contains(stringOutput, accountName) { + t.Errorf("Did not find account %s in output", accountName) + } + key := pubKeys[i] + depositData := depositDataForAccounts[i] + + // Assert every public key is printed to stdout. + if !strings.Contains(stringOutput, fmt.Sprintf("%#x", key)) { + t.Errorf("Did not find pubkey %#x in output", key) + } + + // Assert the deposit data for the account is printed to stdout. + if !strings.Contains(stringOutput, fmt.Sprintf("%#x", depositData)) { + t.Errorf("Did not find deposit data %#x in output", depositData) + } + + // Assert the account creation time is displayed + unixTimestampStr, err := strconv.ParseInt(string(accountCreationTimestamps[i]), 10, 64) + require.NoError(t, err) + unixTimestamp := time.Unix(unixTimestampStr, 0) + assert.Equal(t, strings.Contains(stringOutput, humanize.Time(unixTimestamp)), true) } } diff --git a/validator/accounts/v2/testing/mock.go b/validator/accounts/v2/testing/mock.go index f42e56476a..4a58ee1a95 100644 --- a/validator/accounts/v2/testing/mock.go +++ b/validator/accounts/v2/testing/mock.go @@ -101,6 +101,18 @@ func (m *Wallet) WriteFileAtPath(ctx context.Context, pathName string, fileName return nil } +// ReadFileAtPath -- +func (m *Wallet) ReadFileAtPath(ctx context.Context, pathName string, fileName string) ([]byte, error) { + m.lock.RLock() + defer m.lock.RUnlock() + for f, v := range m.Files[pathName] { + if f == fileName { + return v, nil + } + } + return nil, errors.New("file not found") +} + // ReadEncryptedSeedFromDisk -- func (m *Wallet) ReadEncryptedSeedFromDisk(ctx context.Context) (io.ReadCloser, error) { m.lock.Lock() diff --git a/validator/accounts/v2/wallet.go b/validator/accounts/v2/wallet.go index c56bfc5d99..14b04b2e50 100644 --- a/validator/accounts/v2/wallet.go +++ b/validator/accounts/v2/wallet.go @@ -103,11 +103,15 @@ func NewWallet(ctx context.Context, cfg *WalletConfig) (*Wallet, error) { // type of keymanager associated with the wallet by reading files in the wallet // path, if applicable. If a wallet does not exist, returns an appropriate error. func OpenWallet(ctx context.Context, cfg *WalletConfig) (*Wallet, error) { - walletPath := path.Join(cfg.WalletDir, cfg.KeymanagerKind.String()) + keymanagerKind, err := readKeymanagerKindFromWalletPath(cfg.WalletDir) + if err != nil { + return nil, errors.Wrap(err, "could not read keymanager kind for wallet") + } + walletPath := path.Join(cfg.WalletDir, keymanagerKind.String()) return &Wallet{ accountsPath: walletPath, passwordsDir: cfg.PasswordsDir, - keymanagerKind: cfg.KeymanagerKind, + keymanagerKind: keymanagerKind, canUnlockAccounts: cfg.CanUnlockAccounts, }, nil } @@ -246,6 +250,20 @@ func (w *Wallet) WriteFileAtPath(ctx context.Context, filePath string, fileName return nil } +// ReadFileAtPath within the wallet directory given the desired path and filename. +func (w *Wallet) ReadFileAtPath(ctx context.Context, filePath string, fileName string) ([]byte, error) { + accountPath := path.Join(w.accountsPath, filePath) + if err := os.MkdirAll(accountPath, os.ModePerm); err != nil { + return nil, errors.Wrapf(err, "could not create path: %s", accountPath) + } + fullPath := path.Join(accountPath, fileName) + rawData, err := ioutil.ReadFile(fullPath) + if err != nil { + return nil, errors.Wrapf(err, "could not read %s", filePath) + } + return rawData, nil +} + // WriteFileForAccount stores a unique file and its data under an account namespace // in the wallet's directory on-disk. Creates the file if it does not exist // and writes over it otherwise. diff --git a/validator/keymanager/v2/derived/BUILD.bazel b/validator/keymanager/v2/derived/BUILD.bazel index 17d3d1a4ed..54491ba777 100644 --- a/validator/keymanager/v2/derived/BUILD.bazel +++ b/validator/keymanager/v2/derived/BUILD.bazel @@ -13,15 +13,23 @@ go_library( "//validator:__subpackages__", ], deps = [ + "//contracts/deposit-contract:go_default_library", "//proto/validator/accounts/v2:go_default_library", "//shared/bls:go_default_library", + "//shared/bytesutil:go_default_library", + "//shared/depositutil:go_default_library", + "//shared/params:go_default_library", + "//shared/petnames:go_default_library", "//shared/rand:go_default_library", "//shared/roughtime:go_default_library", "//validator/accounts/v2/iface:go_default_library", "//validator/keymanager/v2:go_default_library", + "@com_github_ethereum_go_ethereum//core/types:go_default_library", "@com_github_google_uuid//:go_default_library", "@com_github_manifoldco_promptui//:go_default_library", "@com_github_pkg_errors//:go_default_library", + "@com_github_prysmaticlabs_ethereumapis//eth/v1alpha1:go_default_library", + "@com_github_prysmaticlabs_go_ssz//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_tyler_smith_go_bip39//:go_default_library", "@com_github_wealdtech_go_eth2_util//:go_default_library", diff --git a/validator/keymanager/v2/derived/derived.go b/validator/keymanager/v2/derived/derived.go index 6c932ff6eb..a26f60adbd 100644 --- a/validator/keymanager/v2/derived/derived.go +++ b/validator/keymanager/v2/derived/derived.go @@ -2,6 +2,7 @@ package derived import ( "context" + "encoding/hex" "encoding/json" "fmt" "io" @@ -10,10 +11,18 @@ import ( "strconv" "sync" + "github.com/ethereum/go-ethereum/core/types" "github.com/google/uuid" "github.com/pkg/errors" + ethpb "github.com/prysmaticlabs/ethereumapis/eth/v1alpha1" + "github.com/prysmaticlabs/go-ssz" + contract "github.com/prysmaticlabs/prysm/contracts/deposit-contract" validatorpb "github.com/prysmaticlabs/prysm/proto/validator/accounts/v2" "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/depositutil" + "github.com/prysmaticlabs/prysm/shared/params" + "github.com/prysmaticlabs/prysm/shared/petnames" "github.com/prysmaticlabs/prysm/shared/rand" "github.com/prysmaticlabs/prysm/shared/roughtime" "github.com/prysmaticlabs/prysm/validator/accounts/v2/iface" @@ -41,6 +50,11 @@ const ( // keys for Prysm eth2 validators. According to EIP-2334, the format is as follows: // m / purpose / coin_type / account_index / withdrawal_key / validating_key ValidatingKeyDerivationPathTemplate = "m/12381/3600/%d/0/0" + // DepositTransactionFileName for the encoded, eth1 raw deposit tx data + // for a validator account. + DepositTransactionFileName = "deposit_transaction.rlp" + // DepositDataFileName for the raw, ssz-encoded deposit data object. + DepositDataFileName = "deposit_data.ssz" ) // Config for a derived keymanager. @@ -72,7 +86,7 @@ type SeedConfig struct { // DefaultConfig for a derived keymanager implementation. func DefaultConfig() *Config { return &Config{ - DerivedPathStructure: "m / purpose / coin_type / account / withdrawal_key / validating_key", + DerivedPathStructure: "m / purpose / coin_type / account_index / withdrawal_key / validating_key", DerivedEIPNumber: EIPVersion, } } @@ -187,6 +201,26 @@ func MarshalEncryptedSeedFile(ctx context.Context, seedCfg *SeedConfig) ([]byte, return json.MarshalIndent(seedCfg, "", "\t") } +// Config -- +func (dr *Keymanager) Config() *Config { + return dr.cfg +} + +// NextAccountNumber managed by the derived keymanager. +func (dr *Keymanager) NextAccountNumber(ctx context.Context) uint64 { + return dr.seedCfg.NextAccount +} + +// AccountNames -- +func (dr *Keymanager) AccountNames(ctx context.Context) ([]string, error) { + names := make([]string, 0) + for i := uint64(0); i < dr.seedCfg.NextAccount; i++ { + withdrawalKeyPath := fmt.Sprintf(WithdrawalKeyDerivationPathTemplate, i) + names = append(names, petnames.DeterministicName([]byte(withdrawalKeyPath), "-")) + } + return names, nil +} + // CreateAccount for a derived keymanager implementation. This utilizes // the EIP-2335 keystore standard for BLS12-381 keystores. It uses the EIP-2333 and EIP-2334 // for hierarchical derivation of BLS secret keys and a common derivation path structure for @@ -230,6 +264,30 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (strin return "", errors.Wrapf(err, "could not write keystore file for account %d", dr.seedCfg.NextAccount) } + // Upon confirmation of the withdrawal key, proceed to display + // and write associated deposit data to disk. + tx, depositData, err := generateDepositTransaction(validatingKey.Marshal(), withdrawalKey.Marshal()) + if err != nil { + return "", errors.Wrap(err, "could not generate deposit transaction data") + } + + // Log the deposit transaction data to the user. + logDepositTransaction(tx) + + // We write the raw deposit transaction as an .rlp encoded file. + if err := dr.wallet.WriteFileAtPath(ctx, withdrawalKeyPath, DepositTransactionFileName, tx.Data()); err != nil { + return "", errors.Wrapf(err, "could not write for account %s: %s", withdrawalKeyPath, DepositTransactionFileName) + } + + // We write the ssz-encoded deposit data to disk as a .ssz file. + encodedDepositData, err := ssz.Marshal(depositData) + if err != nil { + return "", errors.Wrap(err, "could not marshal deposit data") + } + if err := dr.wallet.WriteFileAtPath(ctx, withdrawalKeyPath, DepositDataFileName, encodedDepositData); err != nil { + return "", errors.Wrapf(err, "could not write for account %s: %s", withdrawalKeyPath, encodedDepositData) + } + // Finally, write the account creation timestamps as a files. createdAt := roughtime.Now().Unix() createdAtStr := strconv.FormatInt(createdAt, 10) @@ -259,16 +317,55 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, password string) (strin return fmt.Sprintf("%d", newAccountNumber), nil } -// FetchValidatingPublicKeys fetches the list of public keys from the direct account keystores. -func (dr *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) { - return nil, errors.New("unimplemented") -} - // Sign signs a message using a validator key. func (dr *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (bls.Signature, error) { return nil, errors.New("unimplemented") } +// FetchValidatingPublicKeys fetches the list of validating public keys from the keymanager. +func (dr *Keymanager) FetchValidatingPublicKeys(ctx context.Context) ([][48]byte, error) { + publicKeys := make([][48]byte, 0) + for i := uint64(0); i < dr.seedCfg.NextAccount; i++ { + validatingKeyPath := fmt.Sprintf(ValidatingKeyDerivationPathTemplate, i) + validatingKeystore, err := dr.wallet.ReadFileAtPath(ctx, validatingKeyPath, KeystoreFileName) + if err != nil { + return nil, err + } + keystoreFile := &v2keymanager.Keystore{} + if err := json.Unmarshal(validatingKeystore, keystoreFile); err != nil { + return nil, errors.Wrapf(err, "could not decode keystore json for account: %s", validatingKeyPath) + } + pubKeyBytes, err := hex.DecodeString(keystoreFile.Pubkey) + if err != nil { + return nil, errors.Wrapf(err, "could not decode pubkey bytes: %#x", keystoreFile.Pubkey) + } + publicKeys = append(publicKeys, bytesutil.ToBytes48(pubKeyBytes)) + } + return publicKeys, nil +} + +// FetchWithdrawalPublicKeys fetches the list of withdrawal public keys from keymanager +func (dr *Keymanager) FetchWithdrawalPublicKeys(ctx context.Context) ([][48]byte, error) { + publicKeys := make([][48]byte, 0) + for i := uint64(0); i < dr.seedCfg.NextAccount; i++ { + withdrawalKeyPath := fmt.Sprintf(WithdrawalKeyDerivationPathTemplate, i) + withdrawalKeystore, err := dr.wallet.ReadFileAtPath(ctx, withdrawalKeyPath, KeystoreFileName) + if err != nil { + return nil, err + } + keystoreFile := &v2keymanager.Keystore{} + if err := json.Unmarshal(withdrawalKeystore, keystoreFile); err != nil { + return nil, errors.Wrapf(err, "could not decode keystore json for account: %s", withdrawalKeyPath) + } + pubKeyBytes, err := hex.DecodeString(keystoreFile.Pubkey) + if err != nil { + return nil, errors.Wrapf(err, "could not decode pubkey bytes: %#x", keystoreFile.Pubkey) + } + publicKeys = append(publicKeys, bytesutil.ToBytes48(pubKeyBytes)) + } + return publicKeys, nil +} + func (dr *Keymanager) generateKeystoreFile(privateKey []byte, publicKey []byte, password string) ([]byte, error) { encryptor := keystorev4.New() cryptoFields, err := encryptor.Encrypt(privateKey, []byte(password)) @@ -288,3 +385,52 @@ func (dr *Keymanager) generateKeystoreFile(privateKey []byte, publicKey []byte, } return json.MarshalIndent(keystoreFile, "", "\t") } + +func generateDepositTransaction( + validatingKey []byte, + withdrawalKey []byte, +) (*types.Transaction, *ethpb.Deposit_Data, error) { + validatingPrivateKey, err := bls.SecretKeyFromBytes(validatingKey) + if err != nil { + return nil, nil, err + } + withdrawalPrivateKey, err := bls.SecretKeyFromBytes(withdrawalKey) + if err != nil { + return nil, nil, err + } + depositData, depositRoot, err := depositutil.DepositInput( + validatingPrivateKey, withdrawalPrivateKey, params.BeaconConfig().MaxEffectiveBalance, + ) + if err != nil { + return nil, nil, errors.Wrap(err, "could not generate deposit input") + } + testAcc, err := contract.Setup() + if err != nil { + return nil, nil, errors.Wrap(err, "could not load deposit contract") + } + testAcc.TxOpts.GasLimit = 1000000 + + tx, err := testAcc.Contract.Deposit( + testAcc.TxOpts, + depositData.PublicKey, + depositData.WithdrawalCredentials, + depositData.Signature, + depositRoot, + ) + return tx, depositData, nil +} + +func logDepositTransaction(tx *types.Transaction) { + log.Info( + "Copy + paste the deposit data below when using the " + + "eth1 deposit contract") + fmt.Printf(` +========================Deposit Data=============================== + +%#x + +===================================================================`, tx.Data()) + fmt.Printf(` +***Enter the above deposit data into step 3 on https://prylabs.net/participate*** +`) +}