diff --git a/ironfish/src/migrations/data/032-add-account-scanning.ts b/ironfish/src/migrations/data/032-add-account-scanning.ts new file mode 100644 index 0000000000..61f8672218 --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-scanning.ts @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { Logger } from '../../logger' +import { IDatabase, IDatabaseTransaction } from '../../storage' +import { createDB } from '../../storage/utils' +import { Database, Migration, MigrationContext } from '../migration' +import { GetStores } from './032-add-account-syncing/stores' + +export class Migration032 extends Migration { + path = __filename + database = Database.WALLET + + prepare(context: MigrationContext): IDatabase { + return createDB({ location: context.config.walletDatabasePath }) + } + + async forward( + _context: MigrationContext, + db: IDatabase, + tx: IDatabaseTransaction | undefined, + logger: Logger, + ): Promise { + const stores = GetStores(db) + + for await (const accountValue of stores.old.accounts.getAllValuesIter(tx)) { + logger.debug(` Migrating account ${accountValue.name}`) + + const migrated = { + ...accountValue, + scanningEnabled: true, + } + + await stores.new.accounts.put(accountValue.id, migrated, tx) + } + } + + async backward( + _context: MigrationContext, + db: IDatabase, + tx: IDatabaseTransaction | undefined, + ): Promise { + const stores = GetStores(db) + + for await (const accountValue of stores.new.accounts.getAllValuesIter(tx)) { + await stores.old.accounts.put(accountValue.id, accountValue, tx) + } + } +} diff --git a/ironfish/src/migrations/data/032-add-account-syncing/new/AccountValue.ts b/ironfish/src/migrations/data/032-add-account-syncing/new/AccountValue.ts new file mode 100644 index 0000000000..4b5dba6417 --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-syncing/new/AccountValue.ts @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { PUBLIC_ADDRESS_LENGTH } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' +import { HeadValue, NullableHeadValueEncoding } from './HeadValue' + +const KEY_LENGTH = 32 +export const VIEW_KEY_LENGTH = 64 +const VERSION_LENGTH = 2 + +export interface AccountValue { + version: number + id: string + name: string + spendingKey: string | null + viewKey: string + incomingViewKey: string + outgoingViewKey: string + publicAddress: string + createdAt: HeadValue | null + scanningEnabled?: boolean + multisigKeys?: { + secret: string + keyPackage: string + } + proofAuthorizingKey: string | null +} + +export class AccountValueEncoding implements IDatabaseEncoding { + serialize(value: AccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + let flags = 0 + flags |= Number(!!value.spendingKey) << 0 + flags |= Number(!!value.createdAt) << 1 + flags |= Number(!!value.multisigKeys) << 2 + flags |= Number(!!value.proofAuthorizingKey) << 3 + flags |= Number(!!value.scanningEnabled) << 4 + bw.writeU8(flags) + bw.writeU16(value.version) + bw.writeVarString(value.id, 'utf8') + bw.writeVarString(value.name, 'utf8') + + if (value.spendingKey) { + bw.writeBytes(Buffer.from(value.spendingKey, 'hex')) + } + + bw.writeBytes(Buffer.from(value.viewKey, 'hex')) + bw.writeBytes(Buffer.from(value.incomingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.outgoingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.publicAddress, 'hex')) + + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + bw.writeBytes(encoding.serialize(value.createdAt)) + } + + if (value.multisigKeys) { + bw.writeVarBytes(Buffer.from(value.multisigKeys.secret, 'hex')) + bw.writeVarBytes(Buffer.from(value.multisigKeys.keyPackage, 'hex')) + } + + if (value.proofAuthorizingKey) { + bw.writeBytes(Buffer.from(value.proofAuthorizingKey, 'hex')) + } + + return bw.render() + } + + deserialize(buffer: Buffer): AccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const version = reader.readU16() + const hasSpendingKey = flags & (1 << 0) + const hasCreatedAt = flags & (1 << 1) + const hasMultisigKeys = flags & (1 << 2) + const hasProofAuthorizingKey = flags & (1 << 3) + const scanningEnabled = Boolean(flags & (1 << 4)) + const id = reader.readVarString('utf8') + const name = reader.readVarString('utf8') + const spendingKey = hasSpendingKey ? reader.readBytes(KEY_LENGTH).toString('hex') : null + const viewKey = reader.readBytes(VIEW_KEY_LENGTH).toString('hex') + const incomingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const outgoingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const publicAddress = reader.readBytes(PUBLIC_ADDRESS_LENGTH).toString('hex') + + let createdAt = null + if (hasCreatedAt) { + const encoding = new NullableHeadValueEncoding() + createdAt = encoding.deserialize(reader.readBytes(encoding.nonNullSize)) + } + + let multisigKeys = undefined + if (hasMultisigKeys) { + multisigKeys = { + secret: reader.readVarBytes().toString('hex'), + keyPackage: reader.readVarBytes().toString('hex'), + } + } + + const proofAuthorizingKey = hasProofAuthorizingKey + ? reader.readBytes(KEY_LENGTH).toString('hex') + : null + + return { + version, + id, + name, + viewKey, + incomingViewKey, + outgoingViewKey, + spendingKey, + publicAddress, + createdAt, + scanningEnabled, + multisigKeys, + proofAuthorizingKey, + } + } + + getSize(value: AccountValue): number { + let size = 0 + size += 1 // flags + size += VERSION_LENGTH + size += bufio.sizeVarString(value.id, 'utf8') + size += bufio.sizeVarString(value.name, 'utf8') + if (value.spendingKey) { + size += KEY_LENGTH + } + size += VIEW_KEY_LENGTH + size += KEY_LENGTH + size += KEY_LENGTH + size += PUBLIC_ADDRESS_LENGTH + + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + size += encoding.nonNullSize + } + + if (value.multisigKeys) { + size += bufio.sizeVarString(value.multisigKeys.secret, 'hex') + size += bufio.sizeVarString(value.multisigKeys.keyPackage, 'hex') + } + if (value.proofAuthorizingKey) { + size += KEY_LENGTH + } + + return size + } +} diff --git a/ironfish/src/migrations/data/032-add-account-syncing/new/HeadValue.ts b/ironfish/src/migrations/data/032-add-account-syncing/new/HeadValue.ts new file mode 100644 index 0000000000..9da18b093c --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-syncing/new/HeadValue.ts @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' + +export type HeadValue = { + hash: Buffer + sequence: number +} + +export class NullableHeadValueEncoding implements IDatabaseEncoding { + readonly nonNullSize = 32 + 4 // 256-bit block hash + 32-bit integer + + serialize(value: HeadValue | null): Buffer { + const bw = bufio.write(this.getSize(value)) + + if (value) { + bw.writeHash(value.hash) + bw.writeU32(value.sequence) + } + + return bw.render() + } + + deserialize(buffer: Buffer): HeadValue | null { + const reader = bufio.read(buffer, true) + + if (reader.left()) { + const hash = reader.readHash() + const sequence = reader.readU32() + return { hash, sequence } + } + + return null + } + + getSize(value: HeadValue | null): number { + return value ? this.nonNullSize : 0 + } +} diff --git a/ironfish/src/migrations/data/032-add-account-syncing/new/index.ts b/ironfish/src/migrations/data/032-add-account-syncing/new/index.ts new file mode 100644 index 0000000000..915a484ee5 --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-syncing/new/index.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { IDatabase, IDatabaseStore, StringEncoding } from '../../../../storage' +import { AccountValue, AccountValueEncoding } from './AccountValue' + +export function GetNewStores(db: IDatabase): { + accounts: IDatabaseStore<{ key: string; value: AccountValue }> +} { + const accounts: IDatabaseStore<{ key: string; value: AccountValue }> = db.addStore( + { + name: 'a', + keyEncoding: new StringEncoding(), + valueEncoding: new AccountValueEncoding(), + }, + false, + ) + + return { accounts } +} diff --git a/ironfish/src/migrations/data/032-add-account-syncing/old/AccountValue.ts b/ironfish/src/migrations/data/032-add-account-syncing/old/AccountValue.ts new file mode 100644 index 0000000000..5d753a2b4a --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-syncing/old/AccountValue.ts @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { PUBLIC_ADDRESS_LENGTH } from '@ironfish/rust-nodejs' +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' +import { HeadValue, NullableHeadValueEncoding } from './HeadValue' + +const KEY_LENGTH = 32 +export const VIEW_KEY_LENGTH = 64 +const VERSION_LENGTH = 2 + +export interface AccountValue { + version: number + id: string + name: string + spendingKey: string | null + viewKey: string + incomingViewKey: string + outgoingViewKey: string + publicAddress: string + createdAt: HeadValue | null + multisigKeys?: { + secret: string + keyPackage: string + } + proofAuthorizingKey: string | null +} + +export class AccountValueEncoding implements IDatabaseEncoding { + serialize(value: AccountValue): Buffer { + const bw = bufio.write(this.getSize(value)) + let flags = 0 + flags |= Number(!!value.spendingKey) << 0 + flags |= Number(!!value.createdAt) << 1 + flags |= Number(!!value.multisigKeys) << 2 + flags |= Number(!!value.proofAuthorizingKey) << 3 + bw.writeU8(flags) + bw.writeU16(value.version) + bw.writeVarString(value.id, 'utf8') + bw.writeVarString(value.name, 'utf8') + if (value.spendingKey) { + bw.writeBytes(Buffer.from(value.spendingKey, 'hex')) + } + bw.writeBytes(Buffer.from(value.viewKey, 'hex')) + bw.writeBytes(Buffer.from(value.incomingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.outgoingViewKey, 'hex')) + bw.writeBytes(Buffer.from(value.publicAddress, 'hex')) + + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + bw.writeBytes(encoding.serialize(value.createdAt)) + } + + if (value.multisigKeys) { + bw.writeVarBytes(Buffer.from(value.multisigKeys.secret, 'hex')) + bw.writeVarBytes(Buffer.from(value.multisigKeys.keyPackage, 'hex')) + } + + if (value.proofAuthorizingKey) { + bw.writeBytes(Buffer.from(value.proofAuthorizingKey, 'hex')) + } + + return bw.render() + } + + deserialize(buffer: Buffer): AccountValue { + const reader = bufio.read(buffer, true) + const flags = reader.readU8() + const version = reader.readU16() + const hasSpendingKey = flags & (1 << 0) + const hasCreatedAt = flags & (1 << 1) + const hasMultisigKeys = flags & (1 << 2) + const hasProofAuthorizingKey = flags & (1 << 3) + const id = reader.readVarString('utf8') + const name = reader.readVarString('utf8') + const spendingKey = hasSpendingKey ? reader.readBytes(KEY_LENGTH).toString('hex') : null + const viewKey = reader.readBytes(VIEW_KEY_LENGTH).toString('hex') + const incomingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const outgoingViewKey = reader.readBytes(KEY_LENGTH).toString('hex') + const publicAddress = reader.readBytes(PUBLIC_ADDRESS_LENGTH).toString('hex') + + let createdAt = null + if (hasCreatedAt) { + const encoding = new NullableHeadValueEncoding() + createdAt = encoding.deserialize(reader.readBytes(encoding.nonNullSize)) + } + + let multisigKeys = undefined + if (hasMultisigKeys) { + multisigKeys = { + secret: reader.readVarBytes().toString('hex'), + keyPackage: reader.readVarBytes().toString('hex'), + } + } + + const proofAuthorizingKey = hasProofAuthorizingKey + ? reader.readBytes(KEY_LENGTH).toString('hex') + : null + + return { + version, + id, + name, + viewKey, + incomingViewKey, + outgoingViewKey, + spendingKey, + publicAddress, + createdAt, + multisigKeys, + proofAuthorizingKey, + } + } + + getSize(value: AccountValue): number { + let size = 0 + size += 1 // flags + size += VERSION_LENGTH + size += bufio.sizeVarString(value.id, 'utf8') + size += bufio.sizeVarString(value.name, 'utf8') + if (value.spendingKey) { + size += KEY_LENGTH + } + size += VIEW_KEY_LENGTH + size += KEY_LENGTH + size += KEY_LENGTH + size += PUBLIC_ADDRESS_LENGTH + if (value.createdAt) { + const encoding = new NullableHeadValueEncoding() + size += encoding.nonNullSize + } + if (value.multisigKeys) { + size += bufio.sizeVarString(value.multisigKeys.secret, 'hex') + size += bufio.sizeVarString(value.multisigKeys.keyPackage, 'hex') + } + if (value.proofAuthorizingKey) { + size += KEY_LENGTH + } + + return size + } +} diff --git a/ironfish/src/migrations/data/032-add-account-syncing/old/HeadValue.ts b/ironfish/src/migrations/data/032-add-account-syncing/old/HeadValue.ts new file mode 100644 index 0000000000..9da18b093c --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-syncing/old/HeadValue.ts @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import bufio from 'bufio' +import { IDatabaseEncoding } from '../../../../storage' + +export type HeadValue = { + hash: Buffer + sequence: number +} + +export class NullableHeadValueEncoding implements IDatabaseEncoding { + readonly nonNullSize = 32 + 4 // 256-bit block hash + 32-bit integer + + serialize(value: HeadValue | null): Buffer { + const bw = bufio.write(this.getSize(value)) + + if (value) { + bw.writeHash(value.hash) + bw.writeU32(value.sequence) + } + + return bw.render() + } + + deserialize(buffer: Buffer): HeadValue | null { + const reader = bufio.read(buffer, true) + + if (reader.left()) { + const hash = reader.readHash() + const sequence = reader.readU32() + return { hash, sequence } + } + + return null + } + + getSize(value: HeadValue | null): number { + return value ? this.nonNullSize : 0 + } +} diff --git a/ironfish/src/migrations/data/032-add-account-syncing/old/index.ts b/ironfish/src/migrations/data/032-add-account-syncing/old/index.ts new file mode 100644 index 0000000000..fc9da34b2b --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-syncing/old/index.ts @@ -0,0 +1,20 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { IDatabase, IDatabaseStore, StringEncoding } from '../../../../storage' +import { AccountValue, AccountValueEncoding } from './AccountValue' + +export function GetOldStores(db: IDatabase): { + accounts: IDatabaseStore<{ key: string; value: AccountValue }> +} { + const accounts: IDatabaseStore<{ key: string; value: AccountValue }> = db.addStore( + { + name: 'a', + keyEncoding: new StringEncoding(), + valueEncoding: new AccountValueEncoding(), + }, + false, + ) + + return { accounts } +} diff --git a/ironfish/src/migrations/data/032-add-account-syncing/stores.ts b/ironfish/src/migrations/data/032-add-account-syncing/stores.ts new file mode 100644 index 0000000000..b046b7c66f --- /dev/null +++ b/ironfish/src/migrations/data/032-add-account-syncing/stores.ts @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +import { IDatabase } from '../../../storage' +import { GetNewStores } from './new' +import { GetOldStores } from './old' + +export function GetStores(db: IDatabase): { + old: ReturnType + new: ReturnType +} { + const oldStores = GetOldStores(db) + const newStores = GetNewStores(db) + + return { old: oldStores, new: newStores } +} diff --git a/ironfish/src/migrations/data/index.ts b/ironfish/src/migrations/data/index.ts index e4bf729414..8b7bc1b25c 100644 --- a/ironfish/src/migrations/data/index.ts +++ b/ironfish/src/migrations/data/index.ts @@ -20,6 +20,7 @@ import { Migration028 } from './028-backfill-assets-owner' import { Migration029 } from './029-backfill-assets-owner-wallet' import { Migration030 } from './030-value-to-unspent-note' import { Migration031 } from './031-add-pak-to-account' +import { Migration032 } from './032-add-account-scanning' export const MIGRATIONS = [ Migration014, @@ -40,4 +41,5 @@ export const MIGRATIONS = [ Migration029, Migration030, Migration031, + Migration032, ] diff --git a/ironfish/src/wallet/walletdb/accountValue.ts b/ironfish/src/wallet/walletdb/accountValue.ts index 3839f5ae85..2ef06e86f5 100644 --- a/ironfish/src/wallet/walletdb/accountValue.ts +++ b/ironfish/src/wallet/walletdb/accountValue.ts @@ -36,10 +36,12 @@ export class AccountValueEncoding implements IDatabaseEncoding { flags |= Number(!!value.createdAt) << 1 flags |= Number(!!value.multisigKeys) << 2 flags |= Number(!!value.proofAuthorizingKey) << 3 + flags |= Number(!!value.scanningEnabled) << 4 bw.writeU8(flags) bw.writeU16(value.version) bw.writeVarString(value.id, 'utf8') bw.writeVarString(value.name, 'utf8') + if (value.spendingKey) { bw.writeBytes(Buffer.from(value.spendingKey, 'hex')) } @@ -75,6 +77,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { const hasCreatedAt = flags & (1 << 1) const hasMultisigKeys = flags & (1 << 2) const hasProofAuthorizingKey = flags & (1 << 3) + const scanningEnabled = Boolean(flags & (1 << 4)) const id = reader.readVarString('utf8') const name = reader.readVarString('utf8') const spendingKey = hasSpendingKey ? reader.readBytes(KEY_LENGTH).toString('hex') : null @@ -110,6 +113,7 @@ export class AccountValueEncoding implements IDatabaseEncoding { spendingKey, publicAddress, createdAt, + scanningEnabled, multisigKeys, proofAuthorizingKey, }