16 "github.com/bytom/crypto/ed25519/chainkd"
17 log "github.com/sirupsen/logrus"
20 // Minimum amount of time between cache reloads. This limit applies if the platform does
21 // not support change notifications. It also applies if the keystore directory does not
22 // exist yet, the code will attempt to create a watcher at most this often.
23 const minReloadInterval = 2 * time.Second
25 type keysByFile []XPub
27 func (s keysByFile) Len() int { return len(s) }
28 func (s keysByFile) Less(i, j int) bool { return s[i].File < s[j].File }
29 func (s keysByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
31 // AmbiguousKeyError is returned when attempting to unlock
32 // an XPub for which more than one file exists.
33 type AmbiguousKeyError struct {
38 func (err *AmbiguousKeyError) Error() string {
40 for i, a := range err.Matches {
42 if i < len(err.Matches)-1 {
46 return fmt.Sprintf("multiple keys match keys (%s)", files)
49 // keyCache is a live index of all keys in the keystore.
50 type keyCache struct {
55 byPubs map[chainkd.XPub][]XPub
59 func newKeyCache(keydir string) *keyCache {
62 byPubs: make(map[chainkd.XPub][]XPub),
64 kc.watcher = newWatcher(kc)
68 func (kc *keyCache) hasKey(xpub chainkd.XPub) bool {
72 return len(kc.byPubs[xpub]) > 0
75 func (kc *keyCache) hasAlias(alias string) bool {
77 for _, xpub := range xpubs {
78 if xpub.Alias == alias {
85 func (kc *keyCache) add(newKey XPub) {
89 i := sort.Search(len(kc.all), func(i int) bool { return kc.all[i].File >= newKey.File })
90 if i < len(kc.all) && kc.all[i] == newKey {
93 // newKey is not in the cache.
94 kc.all = append(kc.all, XPub{})
95 copy(kc.all[i+1:], kc.all[i:])
97 kc.byPubs[newKey.XPub] = append(kc.byPubs[newKey.XPub], newKey)
100 func (kc *keyCache) keys() []XPub {
104 cpy := make([]XPub, len(kc.all))
109 func (kc *keyCache) maybeReload() {
113 if kc.watcher.running {
114 return // A watcher is running and will keep the cache up-to-date.
117 if kc.throttle == nil {
118 kc.throttle = time.NewTimer(0)
121 case <-kc.throttle.C:
123 return // The cache was reloaded recently.
128 kc.throttle.Reset(minReloadInterval)
131 // find returns the cached keys for alias if there is a unique match.
132 // The exact matching rules are explained by the documentation of Account.
133 // Callers must hold ac.mu.
134 func (kc *keyCache) find(xpub XPub) (XPub, error) {
135 // Limit search to xpub candidates if possible.
137 if (xpub.XPub != chainkd.XPub{}) {
138 matches = kc.byPubs[xpub.XPub]
141 // If only the basename is specified, complete the path.
142 if !strings.ContainsRune(xpub.File, filepath.Separator) {
143 xpub.File = filepath.Join(kc.keydir, xpub.File)
145 for i := range matches {
146 if matches[i].File == xpub.File {
147 return matches[i], nil
150 if (xpub.XPub == chainkd.XPub{}) {
151 return XPub{}, ErrLoadKey
154 switch len(matches) {
156 return matches[0], nil
158 return XPub{}, ErrLoadKey
160 err := &AmbiguousKeyError{Pubkey: hex.EncodeToString(xpub.XPub[:]), Matches: make([]XPub, len(matches))}
161 copy(err.Matches, matches)
166 // reload caches addresses of existing key.
167 // Callers must hold ac.mu.
168 func (kc *keyCache) reload() {
169 keys, err := kc.scan()
171 log.WithField("load keys error", err).Error("can't load keys")
175 for k := range kc.byPubs {
178 for _, k := range keys {
179 kc.byPubs[k.XPub] = append(kc.byPubs[k.XPub], k)
181 log.WithField("cache has keys:", len(kc.all)).Debug("reloaded keys")
184 func (kc *keyCache) scan() ([]XPub, error) {
185 files, err := ioutil.ReadDir(kc.keydir)
190 buf = new(bufio.Reader)
193 Alias string `json:"alias"`
194 XPub chainkd.XPub `json:"xpub"`
197 for _, fi := range files {
198 path := filepath.Join(kc.keydir, fi.Name())
200 //log.Printf("ignoring file %v", path)
201 //fmt.Printf("ignoring file %v", path)
204 fd, err := os.Open(path)
211 // Parse the address.
213 err = json.NewDecoder(buf).Decode(&keyJSON)
216 log.WithField("decode json err", err).Errorf("can't decode key %s: %v", path, err)
217 case (keyJSON.Alias == ""):
218 log.WithField("can't decode key, key path:", path).Warn("missing or void alias")
220 keys = append(keys, XPub{XPub: keyJSON.XPub, Alias: keyJSON.Alias, File: path})
227 func (kc *keyCache) delete(removed XPub) {
230 kc.all = removeKey(kc.all, removed)
231 if ba := removeKey(kc.byPubs[removed.XPub], removed); len(ba) == 0 {
232 delete(kc.byPubs, removed.XPub)
234 kc.byPubs[removed.XPub] = ba
238 func removeKey(slice []XPub, elem XPub) []XPub {
239 for i := range slice {
240 if slice[i] == elem {
241 return append(slice[:i], slice[i+1:]...)
247 func skipKeyFile(fi os.FileInfo) bool {
248 // Skip editor backups and UNIX-style hidden files.
249 if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") {
252 // Skip misc special files, directories (yes, symlinks too).
253 if fi.IsDir() || fi.Mode()&os.ModeType != 0 {
259 func (kc *keyCache) close() {
262 if kc.throttle != nil {