Skip to content

Commit

Permalink
wallet: Add birthday.
Browse files Browse the repository at this point in the history
Add a birthday state to the database than can be used when creating a
new wallet to restore from a certain date or time.
  • Loading branch information
JoeGruffins committed May 8, 2024
1 parent f314a44 commit 2d9989e
Show file tree
Hide file tree
Showing 18 changed files with 4,177 additions and 3,520 deletions.
41 changes: 41 additions & 0 deletions internal/prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"unicode"

"decred.org/dcrwallet/v4/errors"
Expand Down Expand Up @@ -367,6 +369,45 @@ func Seed(reader *bufio.Reader) (seed []byte, imported bool, err error) {
}
}

// Birthday prompts for a wallet birthday. Return values may be nil with no error.
func Birthday(reader *bufio.Reader) (*time.Time, *uint32, error) {
for {
fmt.Printf("Do you have a wallet birthday we should rescan from? (enter date as YYYY-MM-DD, or a block number, or 'no') [no]: ")
reply, err := reader.ReadString('\n')
if err != nil {
return nil, nil, err
}
reply = strings.TrimSpace(reply)
switch strings.ToLower(reply) {
case "", "n", "no":
return nil, nil, nil
case "y", "yes":
continue
default:
}

// If just a uint assume this is a block number and return that.
// Ignoring errors and parsing as a birthday below.
if n, err := strconv.ParseUint(reply, 10, 32); err == nil {
birthdayBlock := uint32(n)
fmt.Printf("Using birthday block %d.\n", birthdayBlock)
return nil, &birthdayBlock, nil
}

birthday, err := time.Parse("2006-01-02", reply)
if err != nil {
fmt.Printf("Unable to parse date: %v\n", err)
continue
}
if time.Since(birthday) < time.Hour*24 {
fmt.Println("Birthday cannot be in the future or too close (one day) to the present.")
continue
}
fmt.Printf("Using birthday time %s.\n", birthday)
return &birthday, nil, nil
}
}

// ImportedAccounts prompts for any additional account names and xpubs to
// import at wallet creation.
func ImportedAccounts(reader *bufio.Reader, params *chaincfg.Params) (names []string, xpubs []*hdkeychain.ExtendedKey, err error) {
Expand Down
15 changes: 13 additions & 2 deletions internal/rpc/jsonrpc/methods.go
Original file line number Diff line number Diff line change
Expand Up @@ -5518,7 +5518,7 @@ func (s *Server) walletInfo(ctx context.Context, icmd any) (any, error) {
_ = binary.Read(bytes.NewBuffer(voteBits.ExtendedBits[0:4]), binary.LittleEndian, &voteVersion)
voting := w.VotingEnabled()

return &types.WalletInfoResult{
wi := &types.WalletInfoResult{
DaemonConnected: connected,
SPV: spvMode,
Unlocked: unlocked,
Expand All @@ -5530,7 +5530,18 @@ func (s *Server) walletInfo(ctx context.Context, icmd any) (any, error) {
Voting: voting,
VSP: s.cfg.VSPHost,
ManualTickets: w.ManualTickets(),
}, nil
}

birthState, err := w.BirthState(ctx)
if err != nil {
log.Errorf("Failed to get birth state: %v", err)
} else if birthState != nil &&
!(birthState.SetFromTime || birthState.SetFromHeight) {
wi.BirthHash = birthState.Hash.String()
wi.BirthHeight = birthState.Height
}

return wi, nil
}

// walletIsLocked handles the walletislocked extension request by
Expand Down
2 changes: 1 addition & 1 deletion internal/rpc/jsonrpc/rpcserverhelp.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func helpDescsEnUS() map[string]string {
"validatepredcp0005cf": "validatepredcp0005cf\n\nValidate whether all stored cfilters from before DCP0005 activation are correct according to the expected hardcoded hash\n\nArguments:\nNone\n\nResult:\ntrue|false (boolean) Whether the cfilters are valid\n",
"verifymessage": "verifymessage \"address\" \"signature\" \"message\"\n\nVerify a message was signed with the associated private key of some address.\n\nArguments:\n1. address (string, required) Address used to sign message\n2. signature (string, required) The signature to verify\n3. message (string, required) The message to verify\n\nResult:\ntrue|false (boolean) Whether the message was signed with the private key of 'address'\n",
"version": "version\n\nReturns application and API versions (semver) keyed by their names\n\nArguments:\nNone\n\nResult:\n{\n \"Program or API name\": Object containing the semantic version, (object) Version objects keyed by the program or API name\n ...\n}\n",
"walletinfo": "walletinfo\n\nReturns global information about the wallet\n\nArguments:\nNone\n\nResult:\n{\n \"daemonconnected\": true|false, (boolean) Whether or not the wallet is currently connected to the daemon RPC\n \"spv\": true|false, (boolean) Whether or not wallet is syncing in SPV mode\n \"unlocked\": true|false, (boolean) Whether or not the wallet is unlocked\n \"cointype\": n, (numeric) Active coin type. Not available for watching-only wallets.\n \"txfee\": n.nnn, (numeric) Transaction fee per kB of the serialized tx size in coins\n \"votebits\": n, (numeric) Vote bits setting\n \"votebitsextended\": \"value\", (string) Extended vote bits setting\n \"voteversion\": n, (numeric) Version of votes that will be generated\n \"voting\": true|false, (boolean) Whether or not the wallet is currently voting tickets\n \"vsp\": \"value\", (string) VSP URL used when purchasing tickets\n \"manualtickets\": true|false, (boolean) Whether or not the wallet is only accepting tickets manually\n} \n",
"walletinfo": "walletinfo\n\nReturns global information about the wallet\n\nArguments:\nNone\n\nResult:\n{\n \"daemonconnected\": true|false, (boolean) Whether or not the wallet is currently connected to the daemon RPC\n \"spv\": true|false, (boolean) Whether or not wallet is syncing in SPV mode\n \"unlocked\": true|false, (boolean) Whether or not the wallet is unlocked\n \"cointype\": n, (numeric) Active coin type. Not available for watching-only wallets.\n \"txfee\": n.nnn, (numeric) Transaction fee per kB of the serialized tx size in coins\n \"votebits\": n, (numeric) Vote bits setting\n \"votebitsextended\": \"value\", (string) Extended vote bits setting\n \"voteversion\": n, (numeric) Version of votes that will be generated\n \"voting\": true|false, (boolean) Whether or not the wallet is currently voting tickets\n \"vsp\": \"value\", (string) VSP URL used when purchasing tickets\n \"manualtickets\": true|false, (boolean) Whether or not the wallet is only accepting tickets manually\n \"birthhash\": \"value\", (string) The wallet birth hash.\n \"birthheight\": n, (numeric) The wallet birth height.\n} \n",
"walletislocked": "walletislocked\n\nReturns whether or not the wallet is locked.\n\nArguments:\nNone\n\nResult:\ntrue|false (boolean) Whether the wallet is locked\n",
"walletlock": "walletlock\n\nLock the wallet.\n\nArguments:\nNone\n\nResult:\nNothing\n",
"walletpassphrase": "walletpassphrase \"passphrase\" timeout\n\nUnlock the wallet.\n\nArguments:\n1. passphrase (string, required) The wallet passphrase\n2. timeout (numeric, required) The number of seconds to wait before the wallet automatically locks. 0 leaves the wallet unlocked indefinitely.\n\nResult:\nNothing\n",
Expand Down
60 changes: 59 additions & 1 deletion internal/rpc/rpcserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -616,6 +616,28 @@ func (s *walletServer) DumpPrivateKey(ctx context.Context, req *pb.DumpPrivateKe
return &pb.DumpPrivateKeyResponse{PrivateKeyWif: key}, nil
}

func (s *walletServer) BirthBlock(ctx context.Context, req *pb.BirthBlockRequest) (
*pb.BirthBlockResponse, error) {

birthState, err := s.wallet.BirthState(ctx)
if err != nil {
return nil, status.Errorf(codes.Internal,
"failed to get birth state: %v", err)
}
if birthState == nil || birthState.SetFromTime || birthState.SetFromHeight {
errMsg := "birth block not set"
if birthState.SetFromTime || birthState.SetFromHeight {
errMsg = "birth block is pending..."
}
return nil, status.Errorf(codes.NotFound, errMsg)
}

return &pb.BirthBlockResponse{
Hash: birthState.Hash[:],
Height: birthState.Height,
}, nil
}

func (s *walletServer) ImportPrivateKey(ctx context.Context, req *pb.ImportPrivateKeyRequest) (
*pb.ImportPrivateKeyResponse, error) {

Expand Down Expand Up @@ -2809,11 +2831,47 @@ func (s *loaderServer) CreateWallet(ctx context.Context, req *pb.CreateWalletReq
return nil, status.Errorf(codes.InvalidArgument, "seed is a required parameter")
}

_, err := s.loader.CreateNewWallet(ctx, pubPassphrase, req.PrivatePassphrase, req.Seed)
if req.SetBirthTime && req.SetBirthHeight {
return nil, status.Errorf(codes.InvalidArgument, "both set birth time and height cannot be set")
}

var birthState *udb.BirthdayState
if req.SetBirthTime {
birthday := time.Unix(req.BirthTime, 0)
if time.Since(birthday) < time.Hour*24 {
return nil, status.Errorf(codes.InvalidArgument,
"birth time cannot be in the future or too close (one day) to the present")
}
birthState = &udb.BirthdayState{
SetFromTime: true,
Time: birthday,
}
}

if req.SetBirthTime {
birthState = &udb.BirthdayState{
SetFromHeight: true,
Height: req.BirthHeight,
}
}

if birthState == nil {
// Set the genesis block as the birthday.
birthState = &udb.BirthdayState{
SetFromHeight: true,
}
}

w, err := s.loader.CreateNewWallet(ctx, pubPassphrase, req.PrivatePassphrase, req.Seed)
if err != nil {
return nil, translateError(err)
}

if err := w.SetBirthState(ctx, birthState); err != nil {
return nil, status.Errorf(codes.Internal,
"unable to set birthday state: %s", err.Error())
}

return &pb.CreateWalletResponse{}, nil
}

Expand Down
2 changes: 2 additions & 0 deletions internal/rpchelp/helpdescs_en_US.go
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,8 @@ var helpDescsEnUS = map[string]string{
"walletinforesult-voting": "Whether or not the wallet is currently voting tickets",
"walletinforesult-vsp": "VSP URL used when purchasing tickets",
"walletinforesult-manualtickets": "Whether or not the wallet is only accepting tickets manually",
"walletinforesult-birthhash": "The wallet birth hash.",
"walletinforesult-birthheight": "The wallet birth height.",

// WalletIsLockedCmd help.
"walletislocked--synopsis": "Returns whether or not the wallet is locked.",
Expand Down
12 changes: 12 additions & 0 deletions rpc/api.proto
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ service WalletService {
rpc GetCFilters (GetCFiltersRequest) returns (stream GetCFiltersResponse);
rpc GetPeerInfo(GetPeerInfoRequest) returns (GetPeerInfoResponse);
rpc DumpPrivateKey (DumpPrivateKeyRequest) returns (DumpPrivateKeyResponse);
rpc BirthBlock (BirthBlockRequest) returns (BirthBlockResponse);

// Notifications
rpc TransactionNotifications (TransactionNotificationsRequest) returns (stream TransactionNotificationsResponse);
Expand Down Expand Up @@ -321,6 +322,13 @@ message DumpPrivateKeyResponse {
string private_key_wif = 1;
}

message BirthBlockRequest {
}
message BirthBlockResponse {
bytes hash = 1;
uint32 height = 2;
}

message ImportVotingAccountFromSeedRequest {
bytes seed = 1;
string name = 2;
Expand Down Expand Up @@ -752,6 +760,10 @@ message CreateWalletRequest {
bytes public_passphrase = 1;
bytes private_passphrase = 2;
bytes seed = 3;
bool set_birth_time = 4;
int64 birth_time = 5;
bool set_birth_height = 6;
uint32 birth_height = 7;
}
message CreateWalletResponse {}

Expand Down
39 changes: 39 additions & 0 deletions rpc/documentation/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ blockchain) and the private passphrase (for private keys). Since the seed is
not saved in the wallet database and clients should make their users backup the
seed, it needs to be passed as part of the request.

A wallet birthday can be set by time or height. If by time, a block before the
time will be selected during initial sync. That block hash and height can be
retrieved after initial sync with [`BirthBlock`](#BirthBlock). If not set the
genesis block is used.

After creating a wallet, the `WalletService` service begins running.

Since API version 3.0.0, creating the wallet no longer automatically
Expand All @@ -151,6 +156,19 @@ synchronizes the wallet to the consensus server if it was previously loaded.
- `bytes seed`: The BIP0032 seed used to derive all wallet keys. The length of
this field must be between 16 and 64 bytes, inclusive.

- `bool set_birth_time`: Whether to set a wallet birthday from the passed
birth_time. Cannot be used with set_birth_height.

- `int64 birth_time`: The UNIX timestamp of the desired wallet birthday. Must
be at least a day ahead of current time. set_birth_time must also be set in
order for this to be used.

- `bool set_birth_height`: Whether to set a wallet birthday from the passed
birth_height. Cannot be used with set_birth_time.

- `uint32 birth_height`: The block number of the desired wallet birthday.
set_birth_height must also be set in order for this to be used.

**Response:** `CreateWalletReponse`

**Expected errors:**
Expand All @@ -162,6 +180,9 @@ synchronizes the wallet to the consensus server if it was previously loaded.
- `InvalidArgument`: A private passphrase was not included in the request, or
the seed is of incorrect length.

- `InvalidArgument`: set_birth_time and set_birth_height are both true.

- `InvalidArgument`: birth_time is not 24 hours ahead of current time.
___

#### `CreateWatchingOnlyWallet`
Expand Down Expand Up @@ -444,6 +465,7 @@ The service provides the following methods:
- [`GetTrackedVSPTickets`](#GetTrackedVSPTickets)
- [`Address`](#Address)
- [`DumpPrivateKey`](#DumpPrivateKey)
- [`BirthBlock`](#BirthBlock)

#### `Ping`

Expand Down Expand Up @@ -2695,6 +2717,23 @@ or account must be unlocked.

- `InvalidArgument`: Watching only wallet.

#### `BirthBlock`

The `BirthBlock` method returns the wallets birthday block if set. Rescans
should generally be started from after this block.

**Request:** `BirthBlockRequest`

**Response:** `BirthBlockResponse`

- `bytes hash`: The birth block hash.

- `uint32 height`: The birth block height.

**Expected errors:**

- `NotFound`: Birth block never set or pending being found.

## `SeedService`

The `SeedService` service provides RPC clients with the ability to generate
Expand Down
2 changes: 2 additions & 0 deletions rpc/jsonrpc/types/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ type WalletInfoResult struct {
Voting bool `json:"voting"`
VSP string `json:"vsp"`
ManualTickets bool `json:"manualtickets"`
BirthHash string `json:"birthhash"`
BirthHeight uint32 `json:"birthheight"`
}

// AccountUnlockedResult models the data returned by the accountunlocked
Expand Down

0 comments on commit 2d9989e

Please sign in to comment.