16 log "github.com/sirupsen/logrus"
18 "github.com/bytom/bytom/crypto/ed25519/chainkd"
21 // Minimum amount of time between cache reloads. This limit applies if the platform does
22 // not support change notifications. It also applies if the keystore directory does not
23 // exist yet, the code will attempt to create a watcher at most this often.
24 const minReloadInterval = 2 * time.Second
26 type keysByFile []XPub
28 func (s keysByFile) Len() int { return len(s) }
29 func (s keysByFile) Less(i, j int) bool { return s[i].File < s[j].File }
30 func (s keysByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
32 // AmbiguousKeyError is returned when attempting to unlock
33 // an XPub for which more than one file exists.
34 type AmbiguousKeyError struct {
39 func (err *AmbiguousKeyError) Error() string {
41 for i, a := range err.Matches {
43 if i < len(err.Matches)-1 {
47 return fmt.Sprintf("multiple keys match keys (%s)", files)
50 // keyCache is a live index of all keys in the keystore.
51 type keyCache struct {
56 byPubs map[chainkd.XPub][]XPub
60 func newKeyCache(keydir string) *keyCache {
63 byPubs: make(map[chainkd.XPub][]XPub),
65 kc.watcher = newWatcher(kc)
69 func (kc *keyCache) hasKey(xpub chainkd.XPub) bool {
73 return len(kc.byPubs[xpub]) > 0
76 func (kc *keyCache) hasAlias(alias string) bool {
78 for _, xpub := range xpubs {
79 if xpub.Alias == alias {
86 func (kc *keyCache) add(newKey XPub) {
90 i := sort.Search(len(kc.all), func(i int) bool { return kc.all[i].File >= newKey.File })
91 if i < len(kc.all) && kc.all[i] == newKey {
94 // newKey is not in the cache.
95 kc.all = append(kc.all, XPub{})
96 copy(kc.all[i+1:], kc.all[i:])
98 kc.byPubs[newKey.XPub] = append(kc.byPubs[newKey.XPub], newKey)
101 func (kc *keyCache) keys() []XPub {
105 cpy := make([]XPub, len(kc.all))
110 func (kc *keyCache) maybeReload() {
114 if kc.watcher.running {
115 return // A watcher is running and will keep the cache up-to-date.
118 if kc.throttle == nil {
119 kc.throttle = time.NewTimer(0)
122 case <-kc.throttle.C:
124 return // The cache was reloaded recently.
129 kc.throttle.Reset(minReloadInterval)
132 // find returns the cached keys for alias if there is a unique match.
133 // The exact matching rules are explained by the documentation of Account.
134 // Callers must hold ac.mu.
135 func (kc *keyCache) find(xpub XPub) (XPub, error) {
136 // Limit search to xpub candidates if possible.
138 if (xpub.XPub != chainkd.XPub{}) {
139 matches = kc.byPubs[xpub.XPub]
142 // If only the basename is specified, complete the path.
143 if !strings.ContainsRune(xpub.File, filepath.Separator) {
144 xpub.File = filepath.Join(kc.keydir, xpub.File)
146 for i := range matches {
147 if matches[i].File == xpub.File {
148 return matches[i], nil
151 if (xpub.XPub == chainkd.XPub{}) {
152 return XPub{}, ErrLoadKey
155 switch len(matches) {
157 return matches[0], nil
159 return XPub{}, ErrLoadKey
161 err := &AmbiguousKeyError{Pubkey: hex.EncodeToString(xpub.XPub[:]), Matches: make([]XPub, len(matches))}
162 copy(err.Matches, matches)
167 // reload caches addresses of existing key.
168 // Callers must hold ac.mu.
169 func (kc *keyCache) reload() {
170 keys, err := kc.scan()
172 log.WithFields(log.Fields{"module": logModule, "load keys error": err}).Error("can't load keys")
176 for k := range kc.byPubs {
179 for _, k := range keys {
180 kc.byPubs[k.XPub] = append(kc.byPubs[k.XPub], k)
182 log.WithFields(log.Fields{"module": logModule, "cache has keys:": len(kc.all)}).Debug("reloaded keys")
185 func (kc *keyCache) scan() ([]XPub, error) {
186 files, err := ioutil.ReadDir(kc.keydir)
191 buf = new(bufio.Reader)
194 Alias string `json:"alias"`
195 XPub chainkd.XPub `json:"xpub"`
198 for _, fi := range files {
199 path := filepath.Join(kc.keydir, fi.Name())
203 fd, err := os.Open(path)
208 // Parse the address.
210 err = json.NewDecoder(buf).Decode(&keyJSON)
213 log.WithFields(log.Fields{"module": logModule, "decode json err": err}).Errorf("can't decode key %s: %v", path, err)
215 case (keyJSON.Alias == ""):
216 log.WithFields(log.Fields{"module": logModule, "can't decode key, key path:": path}).Warn("missing or void alias")
218 keys = append(keys, XPub{XPub: keyJSON.XPub, Alias: keyJSON.Alias, File: path})
225 func (kc *keyCache) delete(removed XPub) {
228 kc.all = removeKey(kc.all, removed)
229 if ba := removeKey(kc.byPubs[removed.XPub], removed); len(ba) == 0 {
230 delete(kc.byPubs, removed.XPub)
232 kc.byPubs[removed.XPub] = ba
236 func removeKey(slice []XPub, elem XPub) []XPub {
237 for i := range slice {
238 if slice[i] == elem {
239 return append(slice[:i], slice[i+1:]...)
245 func skipKeyFile(fi os.FileInfo) bool {
246 // Skip editor backups and UNIX-style hidden files.
247 if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") {
250 // Skip misc special files, directories (yes, symlinks too).
251 if fi.IsDir() || fi.Mode()&os.ModeType != 0 {
257 func (kc *keyCache) close() {
260 if kc.throttle != nil {