domain.go raw

   1  // Package domain is the wallet's own state and the operations a person can ask of it:
   2  // create a wallet from a seed, restore one from words, unlock it, derive addresses, and walk
   3  // a chain into it.
   4  //
   5  // It is deliberately free of transport and of the browser: a chain is an interface, the store
   6  // is the package, and both are satisfied by different things in a test and in a worker. That
   7  // is what lets the setup flow be tested without a page, a relay, or a node.
   8  package domain
   9  
  10  import (
  11  	"encoding/binary"
  12  	"errors"
  13  	"fmt"
  14  	"sort"
  15  	"strconv"
  16  	"sync"
  17  
  18  	"git.smesh.lol/nostrino/pkg/block"
  19  	"git.smesh.lol/nostrino/pkg/chaindata"
  20  	"git.smesh.lol/tinywallet/pkg/mnemonic"
  21  	"git.smesh.lol/tinywallet/pkg/store"
  22  	"git.smesh.lol/tinywallet/pkg/utxo"
  23  	"git.smesh.lol/tinywallet/pkg/wallet"
  24  )
  25  
  26  // Errors the setup flow maps onto what a person is told.
  27  var (
  28  	// ErrNoChain is a chain this domain does not drive.
  29  	ErrNoChain = errors.New("no such chain")
  30  	// ErrHasWallet is a setup attempt over a wallet that already exists.
  31  	ErrHasWallet = errors.New("a wallet already exists for this chain")
  32  	// ErrNoWallet is an unlock or a sync before a wallet was created.
  33  	ErrNoWallet = errors.New("no wallet for this chain")
  34  	// ErrLocked is a derivation asked of a locked wallet.
  35  	ErrLocked = errors.New("the wallet is locked")
  36  	// ErrSyncInProgress is a second sync of one chain.
  37  	ErrSyncInProgress = errors.New("a sync is already running for this chain")
  38  )
  39  
  40  // DefaultLookahead is how far past the last used address a sync looks.
  41  //
  42  // A wallet must not stop looking because nobody asked for a new address: the gap limit is
  43  // what lets a restored seed find funds that were received after the last address the person
  44  // ever saw. A hundred is the usual compromise between a rescan that finds everything and one
  45  // that downloads filters for addresses nobody will use.
  46  const DefaultLookahead uint32 = 100
  47  
  48  // Chain is what the domain needs from a light client: where it is, advance it, and ask which
  49  // of these scripts it saw. *chaindata.Client satisfies it, and so does a fake.
  50  type Chain interface {
  51  	// Position reports the verified tip and the filter tip.
  52  	Position() (chaindata.Position, error)
  53  	// Sync advances the chain from a height and returns where it stopped.
  54  	Sync(from uint32) (chaindata.Position, error)
  55  	// Match returns the heights whose filters mention any of the scripts.
  56  	Match(scripts [][]byte) ([]uint32, error)
  57  	// Block returns the raw block at a height, which is what turns a match into a coin.
  58  	Block(height uint32) ([]byte, error)
  59  }
  60  
  61  // Link is one chain the domain drives: its derivation profile and its client.
  62  type Link struct {
  63  	// Profile is the wallet profile the chain derives from.
  64  	Profile wallet.Profile
  65  	// Client is the light client for this chain.
  66  	Client Chain
  67  }
  68  
  69  // Domain is every wallet the process holds and the store they share.
  70  type Domain struct {
  71  	mu sync.Mutex
  72  
  73  	store *store.Cache
  74  	links map[string]Link
  75  	// unlocked holds the derived wallets. The seed phrase itself is not kept: it is opened
  76  	// from the vault, used to derive, and dropped, so a locked domain has one secret copy at
  77  	// rest and none in memory.
  78  	unlocked map[string]*wallet.Wallet
  79  	// utxos is the coin table a verified block fills.
  80  	utxos *utxo.Table
  81  	// syncing refuses a second concurrent sync of one chain. Two syncs of one chain are two
  82  	// questions about one tip, and a light client answers them by interrupting itself.
  83  	syncing map[string]bool
  84  	// rounds is the vault's PBKDF2 work factor. It is a field so a test can seal and unlock
  85  	// without spending the real cost; New sets the shipped value.
  86  	rounds int
  87  }
  88  
  89  // New builds a domain over a store.
  90  func New(cache *store.Cache) (*Domain, error) {
  91  	if cache == nil {
  92  		return nil, errors.New("domain: a domain needs a store")
  93  	}
  94  
  95  	table, err := utxo.Open(cache)
  96  
  97  	if err != nil {
  98  		return nil, err
  99  	}
 100  
 101  	return &Domain{
 102  		store:    cache,
 103  		utxos:    table,
 104  		links:    map[string]Link{},
 105  		unlocked: map[string]*wallet.Wallet{},
 106  		syncing:  map[string]bool{},
 107  		rounds:   vaultRounds,
 108  	}, nil
 109  }
 110  
 111  // Add registers a chain.
 112  //
 113  // The name is the wallet's own chain name ("btc", "blake"), not the network profile: the two
 114  // chains share a network in this build, and the wallet keeps them apart by account.
 115  func (d *Domain) Add(name string, link Link) error {
 116  	if err := checkName(name); err != nil {
 117  		return err
 118  	}
 119  
 120  	if link.Client == nil {
 121  		return fmt.Errorf("domain: chain %q has no client", name)
 122  	}
 123  
 124  	if link.Profile.Name == "" {
 125  		return fmt.Errorf("domain: chain %q has no profile", name)
 126  	}
 127  
 128  	d.mu.Lock()
 129  	defer d.mu.Unlock()
 130  
 131  	if _, ok := d.links[name]; ok {
 132  		return fmt.Errorf("domain: chain %q is already added", name)
 133  	}
 134  
 135  	d.links[name] = link
 136  
 137  	return nil
 138  }
 139  
 140  // Names lists the registered chains in a stable order.
 141  func (d *Domain) Names() []string {
 142  	d.mu.Lock()
 143  	defer d.mu.Unlock()
 144  
 145  	names := make([]string, 0, len(d.links))
 146  
 147  	for name := range d.links {
 148  		names = append(names, name)
 149  	}
 150  
 151  	sort.Strings(names)
 152  
 153  	return names
 154  }
 155  
 156  // link returns a registered chain or ErrNoChain.
 157  func (d *Domain) link(name string) (Link, error) {
 158  	d.mu.Lock()
 159  	defer d.mu.Unlock()
 160  
 161  	link, ok := d.links[name]
 162  
 163  	if !ok {
 164  		return Link{}, fmt.Errorf("domain: %q: %w", name, ErrNoChain)
 165  	}
 166  
 167  	return link, nil
 168  }
 169  
 170  // Created is what the setup flow hands back: the words to write down (only for a new wallet)
 171  // and the first address, so the person can see that something real happened.
 172  type Created struct {
 173  	// Chain is the chain the wallet was made for.
 174  	Chain string
 175  	// Mnemonic is the phrase to write down. It is empty on a restore, where the person
 176  	// already has it.
 177  	Mnemonic string
 178  	// Address is the first receive address.
 179  	Address wallet.Address
 180  }
 181  
 182  // Create makes a new wallet for a chain from fresh words.
 183  //
 184  // The phrase is returned exactly once, to be shown and then forgotten: it is not stored in
 185  // cleartext anywhere, and the sealed copy is only readable with the passphrase.
 186  func (d *Domain) Create(name string, words int, passphrase string) (Created, error) {
 187  	phrase, err := mnemonic.New(words)
 188  
 189  	if err != nil {
 190  		return Created{}, fmt.Errorf("domain: %w", err)
 191  	}
 192  
 193  	created, err := d.restore(name, phrase, passphrase)
 194  
 195  	if err != nil {
 196  		return Created{}, err
 197  	}
 198  
 199  	created.Mnemonic = phrase
 200  
 201  	return created, nil
 202  }
 203  
 204  // Restore recreates a wallet from words the person already has.
 205  func (d *Domain) Restore(name, phrase, passphrase string) (Created, error) {
 206  	return d.restore(name, phrase, passphrase)
 207  }
 208  
 209  // restore is the body Create and Restore share.
 210  func (d *Domain) restore(name, phrase, passphrase string) (Created, error) {
 211  	link, err := d.link(name)
 212  
 213  	if err != nil {
 214  		return Created{}, err
 215  	}
 216  
 217  	if err := mnemonic.Validate(phrase); err != nil {
 218  		return Created{}, fmt.Errorf("domain: %w", err)
 219  	}
 220  
 221  	if _, exists := d.store.Get(vaultKey(name)); exists {
 222  		return Created{}, fmt.Errorf("domain: %q: %w", name, ErrHasWallet)
 223  	}
 224  
 225  	sealed, err := seal(passphrase, phrase, d.rounds)
 226  
 227  	if err != nil {
 228  		return Created{}, err
 229  	}
 230  
 231  	derived, err := derive(phrase, link.Profile)
 232  
 233  	if err != nil {
 234  		return Created{}, err
 235  	}
 236  
 237  	first, err := derived.Address(false, 0)
 238  
 239  	if err != nil {
 240  		return Created{}, err
 241  	}
 242  
 243  	if err := d.store.Put(vaultKey(name), sealed); err != nil {
 244  		return Created{}, err
 245  	}
 246  
 247  	meta := Meta{Account: link.Profile.Account, Receive: 1}
 248  
 249  	if err := d.store.Put(metaKey(name), PutMeta(meta)); err != nil {
 250  		return Created{}, err
 251  	}
 252  
 253  	d.mu.Lock()
 254  	d.unlocked[name] = derived
 255  	d.mu.Unlock()
 256  
 257  	return Created{Chain: name, Address: first}, nil
 258  }
 259  
 260  // Unlock opens the vault and derives the wallet.
 261  //
 262  // The phrase is opened, used, and dropped: what stays in memory is the account key, which is
 263  // what addresses and, later, signatures come from.
 264  func (d *Domain) Unlock(name, passphrase string) error {
 265  	link, err := d.link(name)
 266  
 267  	if err != nil {
 268  		return err
 269  	}
 270  
 271  	sealed, ok := d.store.Get(vaultKey(name))
 272  
 273  	if !ok {
 274  		return fmt.Errorf("domain: %q: %w", name, ErrNoWallet)
 275  	}
 276  
 277  	phrase, err := open(passphrase, sealed, d.rounds)
 278  
 279  	if err != nil {
 280  		return err
 281  	}
 282  
 283  	derived, err := derive(phrase, link.Profile)
 284  
 285  	if err != nil {
 286  		return err
 287  	}
 288  
 289  	d.mu.Lock()
 290  	d.unlocked[name] = derived
 291  	d.mu.Unlock()
 292  
 293  	return nil
 294  }
 295  
 296  // Lock forgets the derived wallet for a chain.
 297  func (d *Domain) Lock(name string) {
 298  	d.mu.Lock()
 299  	delete(d.unlocked, name)
 300  	d.mu.Unlock()
 301  }
 302  
 303  // LockAll forgets every derived wallet.
 304  func (d *Domain) LockAll() {
 305  	d.mu.Lock()
 306  	d.unlocked = map[string]*wallet.Wallet{}
 307  	d.mu.Unlock()
 308  }
 309  
 310  // Unlocked reports whether a chain's wallet is unlocked.
 311  func (d *Domain) Unlocked(name string) bool {
 312  	d.mu.Lock()
 313  	defer d.mu.Unlock()
 314  
 315  	_, ok := d.unlocked[name]
 316  
 317  	return ok
 318  }
 319  
 320  // walletOf returns the unlocked wallet of a chain.
 321  func (d *Domain) walletOf(name string) (*wallet.Wallet, error) {
 322  	if _, err := d.link(name); err != nil {
 323  		return nil, err
 324  	}
 325  
 326  	d.mu.Lock()
 327  	derived, ok := d.unlocked[name]
 328  	d.mu.Unlock()
 329  
 330  	if ok {
 331  		return derived, nil
 332  	}
 333  
 334  	// A chain with no vault is not locked, it is unset: the two are different things to tell
 335  	// a person, so they are different errors.
 336  	if _, hasWallet := d.store.Get(vaultKey(name)); !hasWallet {
 337  		return nil, fmt.Errorf("domain: %q: %w", name, ErrNoWallet)
 338  	}
 339  
 340  	return nil, fmt.Errorf("domain: %q: %w", name, ErrLocked)
 341  }
 342  
 343  // Addresses derives a run of addresses and moves the chain's cursor past them.
 344  //
 345  // Moving the cursor is what makes the lookahead useful: the next sync looks a hundred past
 346  // the last address a person was shown, not past the last one they used.
 347  func (d *Domain) Addresses(name string, change bool, from, count uint32) ([]wallet.Address, error) {
 348  	if count == 0 {
 349  		return nil, nil
 350  	}
 351  
 352  	derived, err := d.walletOf(name)
 353  
 354  	if err != nil {
 355  		return nil, err
 356  	}
 357  
 358  	addresses, err := derived.Addresses(change, from, count)
 359  
 360  	if err != nil {
 361  		return nil, err
 362  	}
 363  
 364  	meta, err := d.meta(name)
 365  
 366  	if err != nil {
 367  		return nil, err
 368  	}
 369  
 370  	high := from + count
 371  
 372  	if change && high > meta.Change {
 373  		meta.Change = high
 374  
 375  		if err := d.store.Put(metaKey(name), PutMeta(meta)); err != nil {
 376  			return nil, err
 377  		}
 378  	}
 379  
 380  	if !change && high > meta.Receive {
 381  		meta.Receive = high
 382  
 383  		if err := d.store.Put(metaKey(name), PutMeta(meta)); err != nil {
 384  			return nil, err
 385  		}
 386  	}
 387  
 388  	return addresses, nil
 389  }
 390  
 391  // Status is what a page needs to draw the wallet's state.
 392  type Status struct {
 393  	// Chain is the chain name.
 394  	Chain string
 395  	// Profile is the derivation profile.
 396  	Profile wallet.Profile
 397  	// HasWallet reports whether a vault exists.
 398  	HasWallet bool
 399  	// Unlocked reports whether the account is in memory.
 400  	Unlocked bool
 401  	// Position is where the chain said it was when last asked.
 402  	Position chaindata.Position
 403  	// Receive and Change are how many addresses of each branch were handed out.
 404  	Receive uint32
 405  	Change  uint32
 406  	// Matches is how many heights the chain matched and the store recorded.
 407  	Matches int
 408  }
 409  
 410  // Status reports a chain's state without asking the chain anything.
 411  func (d *Domain) Status(name string) (Status, error) {
 412  	link, err := d.link(name)
 413  
 414  	if err != nil {
 415  		return Status{}, err
 416  	}
 417  
 418  	status := Status{Chain: name, Profile: link.Profile}
 419  
 420  	_, status.HasWallet = d.store.Get(vaultKey(name))
 421  
 422  	status.Unlocked = d.Unlocked(name)
 423  
 424  	if meta, err := d.meta(name); err == nil {
 425  		status.Receive = meta.Receive
 426  		status.Change = meta.Change
 427  	} else if !errors.Is(err, ErrNoWallet) {
 428  		return Status{}, err
 429  	}
 430  
 431  	if raw, ok := d.store.Get(positionKey(name)); ok {
 432  		if position, err := chaindata.ReadPosition(raw); err == nil {
 433  			status.Position = position
 434  		}
 435  	}
 436  
 437  	status.Matches = len(d.store.Keys(matchPrefix(name)))
 438  
 439  	return status, nil
 440  }
 441  
 442  // Report is what a sync did.
 443  type Report struct {
 444  	// Chain is the chain name.
 445  	Chain string
 446  	// From is the height the sync was asked to start at.
 447  	From uint32
 448  	// Position is where the chain says it ended up.
 449  	Position chaindata.Position
 450  	// Scripts is how many scripts were sent to the matcher.
 451  	Scripts int
 452  	// Matched is every height the chain matched.
 453  	Matched []uint32
 454  	// New is the part of Matched the store had not seen before.
 455  	New []uint32
 456  }
 457  
 458  // Sync walks the chain forward and records the heights that mention the wallet.
 459  //
 460  // A wallet that already has a filter tip resumes from it, which is what makes a reload cheap:
 461  // the store is the memory of what was verified, and the light client is asked for the rest.
 462  // Nothing is proven twice.
 463  func (d *Domain) Sync(name string, lookahead uint32) (Report, error) {
 464  	link, err := d.link(name)
 465  
 466  	if err != nil {
 467  		return Report{}, err
 468  	}
 469  
 470  	derived, err := d.walletOf(name)
 471  
 472  	if err != nil {
 473  		return Report{}, err
 474  	}
 475  
 476  	if lookahead == 0 {
 477  		lookahead = DefaultLookahead
 478  	}
 479  
 480  	d.mu.Lock()
 481  
 482  	if d.syncing[name] {
 483  		d.mu.Unlock()
 484  
 485  		return Report{}, fmt.Errorf("domain: %q: %w", name, ErrSyncInProgress)
 486  	}
 487  
 488  	d.syncing[name] = true
 489  	d.mu.Unlock()
 490  
 491  	defer func() {
 492  		d.mu.Lock()
 493  		d.syncing[name] = false
 494  		d.mu.Unlock()
 495  	}()
 496  
 497  	meta, err := d.meta(name)
 498  
 499  	if err != nil {
 500  		return Report{}, err
 501  	}
 502  
 503  	from := uint32(0)
 504  
 505  	if raw, ok := d.store.Get(positionKey(name)); ok {
 506  		if stored, err := chaindata.ReadPosition(raw); err == nil && stored.Filters > 0 {
 507  			from = stored.Filters + 1
 508  		}
 509  	}
 510  
 511  	position, err := link.Client.Sync(from)
 512  
 513  	if err != nil {
 514  		return Report{}, fmt.Errorf("domain: %q: syncing: %w", name, err)
 515  	}
 516  
 517  	report := Report{Chain: name, From: from, Position: position}
 518  
 519  	scripts, err := scriptsFor(derived, meta, lookahead)
 520  
 521  	if err != nil {
 522  		return Report{}, err
 523  	}
 524  
 525  	report.Scripts = len(scripts)
 526  
 527  	matched, err := link.Client.Match(scripts)
 528  
 529  	if err != nil {
 530  		return Report{}, fmt.Errorf("domain: %q: matching: %w", name, err)
 531  	}
 532  
 533  	for _, height := range matched {
 534  		report.Matched = append(report.Matched, height)
 535  
 536  		key := matchKey(name, height)
 537  
 538  		if _, scanned := d.store.Get(key); scanned {
 539  			// The height was matched and its block was scanned before. A second fetch would
 540  			// be a second download for a fact the store already holds.
 541  			continue
 542  		}
 543  
 544  		// A match is a claim about a block; the coins are in the block. The block is fetched
 545  		// and verified before the marker is stored, so a failure here leaves the height to be
 546  		// retried by the next sync instead of recording a scan that never happened.
 547  		raw, err := link.Client.Block(height)
 548  
 549  		if err != nil {
 550  			return Report{}, fmt.Errorf("domain: %q: fetching block %d: %w", name, height, err)
 551  		}
 552  
 553  		if _, err := d.scanBlock(derived, raw, height); err != nil {
 554  			return Report{}, fmt.Errorf("domain: %q: block %d: %w", name, height, err)
 555  		}
 556  
 557  		if err := d.store.Put(key, nil); err != nil {
 558  			return Report{}, err
 559  		}
 560  
 561  		report.New = append(report.New, height)
 562  	}
 563  
 564  	if err := d.store.Put(positionKey(name), chaindata.PutPosition(nil, position)); err != nil {
 565  		return Report{}, err
 566  	}
 567  
 568  	// One flush for the whole sync: the markers, the coins and the position land together, so a
 569  	// crash cannot leave a height marked as scanned with the coins it produced missing.
 570  	if err := d.store.Flush(); err != nil {
 571  		return report, fmt.Errorf("domain: %q: %w", name, err)
 572  	}
 573  
 574  	return report, nil
 575  }
 576  
 577  // Matches lists the recorded heights, ascending.
 578  func (d *Domain) Matches(name string) []uint32 {
 579  	keys := d.store.Keys(matchPrefix(name))
 580  	out := make([]uint32, 0, len(keys))
 581  
 582  	for _, key := range keys {
 583  		if height, ok := parseMatchKey(key); ok {
 584  			out = append(out, height)
 585  		}
 586  	}
 587  
 588  	return out
 589  }
 590  
 591  // meta reads a chain's cursor.
 592  func (d *Domain) meta(name string) (Meta, error) {
 593  	raw, ok := d.store.Get(metaKey(name))
 594  
 595  	if !ok {
 596  		return Meta{}, fmt.Errorf("domain: %q: %w", name, ErrNoWallet)
 597  	}
 598  
 599  	return ReadMeta(raw)
 600  }
 601  
 602  // scriptsFor derives the scripts a match asks about: both branches, from zero to the cursor
 603  // plus the lookahead.
 604  func scriptsFor(derived *wallet.Wallet, meta Meta, lookahead uint32) ([][]byte, error) {
 605  	scripts := make([][]byte, 0, (meta.Receive+meta.Change+2*lookahead)*2)
 606  
 607  	for _, branch := range []struct {
 608  		change bool
 609  		count  uint32
 610  	}{
 611  		{change: false, count: meta.Receive + lookahead},
 612  		{change: true, count: meta.Change + lookahead},
 613  	} {
 614  		addresses, err := derived.Addresses(branch.change, 0, branch.count)
 615  
 616  		if err != nil {
 617  			return nil, err
 618  		}
 619  
 620  		for _, address := range addresses {
 621  			scripts = append(scripts, address.Script())
 622  		}
 623  	}
 624  
 625  	return scripts, nil
 626  }
 627  
 628  // derive turns a phrase into a chain's wallet.
 629  func derive(phrase string, profile wallet.Profile) (*wallet.Wallet, error) {
 630  	seed, err := mnemonic.Seed(phrase, "")
 631  
 632  	if err != nil {
 633  		return nil, fmt.Errorf("domain: %w", err)
 634  	}
 635  
 636  	return wallet.New(seed, profile)
 637  }
 638  
 639  // Meta is a chain's address cursor and account.
 640  type Meta struct {
 641  	// Account is the BIP32 account the wallet derives under.
 642  	Account uint32
 643  	// Receive is how many receive addresses were handed out.
 644  	Receive uint32
 645  	// Change is how many change addresses were handed out.
 646  	Change uint32
 647  }
 648  
 649  // metaLen is the encoded size of a Meta: version, account, receive, change.
 650  const metaLen = 1 + 4 + 4 + 4
 651  
 652  // metaVersion is the encoding version.
 653  const metaVersion uint8 = 1
 654  
 655  // PutMeta encodes a cursor.
 656  func PutMeta(meta Meta) []byte {
 657  	out := make([]byte, 0, metaLen)
 658  	out = append(out, metaVersion)
 659  	out = binary.LittleEndian.AppendUint32(out, meta.Account)
 660  	out = binary.LittleEndian.AppendUint32(out, meta.Receive)
 661  
 662  	return binary.LittleEndian.AppendUint32(out, meta.Change)
 663  }
 664  
 665  // ReadMeta decodes a cursor.
 666  func ReadMeta(raw []byte) (Meta, error) {
 667  	if len(raw) != metaLen {
 668  		return Meta{}, fmt.Errorf("domain: a cursor is %d bytes, not %d", metaLen, len(raw))
 669  	}
 670  
 671  	if raw[0] != metaVersion {
 672  		return Meta{}, fmt.Errorf("domain: cursor version %d", raw[0])
 673  	}
 674  
 675  	return Meta{
 676  		Account: binary.LittleEndian.Uint32(raw[1:5]),
 677  		Receive: binary.LittleEndian.Uint32(raw[5:9]),
 678  		Change:  binary.LittleEndian.Uint32(raw[9:13]),
 679  	}, nil
 680  }
 681  
 682  // Storage keys. The prefixes are what make a chain's documents a range.
 683  func vaultKey(name string) string { return "wallet/" + name + "/vault" }
 684  
 685  func metaKey(name string) string { return "wallet/" + name + "/meta" }
 686  
 687  func positionKey(name string) string { return "chain/" + name + "/position" }
 688  
 689  func matchPrefix(name string) string { return "match/" + name + "/" }
 690  
 691  // matchKey names one matched height. The height is zero-padded so the keys sort in chain
 692  // order, which is how a later layer walks them.
 693  func matchKey(name string, height uint32) string {
 694  	return matchPrefix(name) + pad(height)
 695  }
 696  
 697  // parseMatchKey reads the height out of a match key.
 698  func parseMatchKey(key string) (uint32, bool) {
 699  	index := len(key) - 10
 700  
 701  	if index <= 0 || len(key) < 10 {
 702  		return 0, false
 703  	}
 704  
 705  	height, err := strconv.ParseUint(key[index:], 10, 32)
 706  
 707  	if err != nil {
 708  		return 0, false
 709  	}
 710  
 711  	return uint32(height), true
 712  }
 713  
 714  // pad renders a height as ten digits.
 715  func pad(height uint32) string {
 716  	text := strconv.FormatUint(uint64(height), 10)
 717  
 718  	for len(text) < 10 {
 719  		text = "0" + text
 720  	}
 721  
 722  	return text
 723  }
 724  
 725  // checkName refuses a chain name that would make an ambiguous key or an unreadable log.
 726  func checkName(name string) error {
 727  	if name == "" {
 728  		return errors.New("domain: a chain needs a name")
 729  	}
 730  
 731  	if len(name) > 32 {
 732  		return fmt.Errorf("domain: the chain name %q is too long", name)
 733  	}
 734  
 735  	for index := 0; index < len(name); index++ {
 736  		character := name[index]
 737  
 738  		switch {
 739  		case character >= 'a' && character <= 'z':
 740  		case character >= '0' && character <= '9':
 741  		case character == '-' || character == '_':
 742  		default:
 743  			return fmt.Errorf("domain: the chain name %q must be lower-case letters, digits, '-' or '_'", name)
 744  		}
 745  	}
 746  
 747  	return nil
 748  }
 749  
 750  // ScanBlock adds the outputs of one verified block that pay this wallet.
 751  //
 752  // The block is checked before anything is read from it: a gateway can hand over any bytes it
 753  // likes, and the merkle root is the only thing that ties them to the header the wallet already
 754  // trusts. What comes out is a coin per matching output, with the height it was confirmed at.
 755  func (d *Domain) ScanBlock(name string, raw []byte, height uint32) (int, error) {
 756  	derived, err := d.walletOf(name)
 757  
 758  	if err != nil {
 759  		return 0, err
 760  	}
 761  
 762  	added, err := d.scanBlock(derived, raw, height)
 763  
 764  	return added, err
 765  }
 766  
 767  // scanBlock verifies one block and adds the outputs that pay this wallet.
 768  func (d *Domain) scanBlock(derived *wallet.Wallet, raw []byte, height uint32) (int, error) {
 769  	parsed, err := block.Parse(raw)
 770  
 771  	if err != nil {
 772  		return 0, err
 773  	}
 774  
 775  	if err := parsed.VerifyMerkle(); err != nil {
 776  		return 0, err
 777  	}
 778  
 779  	meta, err := d.meta(derived.Profile().Name)
 780  
 781  	if err != nil {
 782  		return 0, err
 783  	}
 784  
 785  	scripts, err := scriptsFor(derived, meta, DefaultLookahead)
 786  
 787  	if err != nil {
 788  		return 0, err
 789  	}
 790  
 791  	matches := parsed.Match(scripts)
 792  	added := 0
 793  
 794  	for _, match := range matches {
 795  		if err := d.utxos.Add(utxo.Coin{
 796  			TxID: match.TxID, Vout: match.Vout, Value: match.Value, Height: height,
 797  		}); err != nil {
 798  			return added, err
 799  		}
 800  
 801  		added++
 802  	}
 803  
 804  	return added, nil
 805  }
 806  
 807  // Unspent lists the coins the wallet can spend, newest first.
 808  func (d *Domain) Unspent() ([]utxo.Coin, error) {
 809  	if d.utxos == nil {
 810  		return nil, errors.New("domain: no coin table")
 811  	}
 812  
 813  	return d.utxos.Unspent()
 814  }
 815