Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Soloseng/add-combiner-db #10491

Closed
wants to merge 30 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
7bd7662
update pnpcommon README, release sdks script, combiner deps
alecps Aug 3, 2023
1d2aa5c
Update packages/phone-number-privacy/common/README.md
alecps Aug 3, 2023
2077c41
Soloseng/firebase-dep-update (#10468)
soloseng Aug 3, 2023
49e8044
Soloseng/fix-test-quota-bypass (#10471)
soloseng Aug 7, 2023
5ff2284
Merge branch 'master' into alecps/miscFixesFromRelease08-02-23
alecps Aug 7, 2023
b5cc434
Merge branch 'master' into alecps/miscFixesFromRelease08-02-23
soloseng Aug 8, 2023
120ea8e
odis crypto op metering (#10461)
alecps Aug 11, 2023
b1e14ce
Soloseng/loadtest-with-DEK (#10482)
soloseng Aug 11, 2023
ca4d72b
Merge branch 'master' into alecps/miscFixesFromRelease08-02-23
alecps Aug 11, 2023
5b1c2f0
first draft
soloseng Aug 12, 2023
02eb07c
comments
soloseng Aug 12, 2023
3186360
Merge branch 'master' into soloseng/add-combiner-db
soloseng Aug 15, 2023
7f94796
removed is null req when updating
soloseng Aug 15, 2023
1269498
running locally
soloseng Aug 15, 2023
eb132e3
switch to local `authenticateUser`
soloseng Aug 16, 2023
5876c7f
++ primary key
soloseng Aug 16, 2023
be9078f
lint
soloseng Aug 16, 2023
7660603
Revert "lint"
soloseng Aug 17, 2023
69879a3
lint fix
soloseng Aug 17, 2023
f97281b
more lint
soloseng Aug 17, 2023
1676c9a
refactor to init db on combiner startup
soloseng Aug 17, 2023
c1df45e
misc cleanup
soloseng Aug 17, 2023
41344c4
Merge branch 'master' into soloseng/add-combiner-db
soloseng Aug 17, 2023
0499449
removed user from db
soloseng Aug 18, 2023
a6cc343
fixed hanging combiner tests
soloseng Aug 18, 2023
0102b70
removed duplicate primary
soloseng Aug 19, 2023
d7db5b2
Merge branch 'master' into soloseng/add-combiner-db
soloseng Aug 19, 2023
7004a5a
++ default options to db configs
soloseng Aug 21, 2023
b0de5c1
Merge branch 'alecps/odisRelease3.0.0' into soloseng/add-combiner-db
soloseng Aug 25, 2023
4a6a46a
adapt to new achitecture
soloseng Aug 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion packages/phone-number-privacy/combiner/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,15 @@
"test": "jest --runInBand --testPathIgnorePatterns test/end-to-end",
"test:coverage": "yarn test --coverage",
"test:integration": "jest --runInBand test/integration",
"test:integration:debugdb": "VERBOSE_DB_LOGGING=true jest --runInBand test/integration",
"test:e2e": "jest test/end-to-end --verbose",
"test:e2e:staging": "CONTEXT_NAME=staging yarn test:e2e",
"test:e2e:alfajores": "CONTEXT_NAME=alfajores yarn test:e2e"
"test:e2e:alfajores": "CONTEXT_NAME=alfajores yarn test:e2e",
"db:migrate": "NODE_ENV=dev FIREBASE_CONFIG=./firebase.json ts-node ./scripts/run-migrations.ts",
"db:migrate:staging": "GCLOUD_PROJECT=celo-phone-number-privacy-stg yarn db:migrate",
"db:migrate:alfajores": "GCLOUD_PROJECT=celo-phone-number-privacy yarn db:migrate",
"db:migrate:mainnet": "GCLOUD_PROJECT=celo-pgpnp-mainnet yarn db:migrate",
"db:migrate:make": "knex --migrations-directory ./migrations migrate:make -x ts"
},
"dependencies": {
"@celo/contractkit": "^4.1.1-beta.1",
Expand Down
19 changes: 19 additions & 0 deletions packages/phone-number-privacy/combiner/scripts/run-migrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// tslint:disable: no-console
// TODO de-dupe with signer script
import { initDatabase } from '../src/database/database'
import config from '../src/config'

async function start() {
console.info('Running migrations')
await initDatabase(config, undefined, false)
}

start()
.then(() => {
console.info('Migrations complete')
process.exit(0)
})
.catch((e) => {
console.error('Migration failed', e)
process.exit(1)
})
47 changes: 47 additions & 0 deletions packages/phone-number-privacy/combiner/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
BlockchainConfig,
DB_POOL_MAX_SIZE,
DB_TIMEOUT,
FULL_NODE_TIMEOUT_IN_MS,
RETRY_COUNT,
RETRY_DELAY_IN_MS,
Expand All @@ -13,6 +15,7 @@ export function getCombinerVersion(): string {
}
export const DEV_MODE =
process.env.NODE_ENV !== 'production' || process.env.FUNCTIONS_EMULATOR === 'true'
export const VERBOSE_DB_LOGGING = toBool(process.env.VERBOSE_DB_LOGGING, false)

export const FORNO_ALFAJORES = 'https://alfajores-forno.celo-testnet.org'

Expand All @@ -25,6 +28,13 @@ export const MAX_BLOCK_DISCREPANCY_THRESHOLD = 3
export const MAX_TOTAL_QUOTA_DISCREPANCY_THRESHOLD = 5
export const MAX_QUERY_COUNT_DISCREPANCY_THRESHOLD = 5

export enum SupportedDatabase {
Postgres = 'postgres', // PostgresSQL
MySql = 'mysql', // MySQL
MsSql = 'mssql', // Microsoft SQL Server
Sqlite = 'sqlite3', // SQLite (for testing)
}

export interface OdisConfig {
serviceName: string
enabled: boolean
Expand All @@ -46,6 +56,17 @@ export interface CombinerConfig {
blockchain: BlockchainConfig
phoneNumberPrivacy: OdisConfig
domains: OdisConfig
db: {
type: SupportedDatabase
user: string
password: string
database: string
host: string
port?: number
ssl: boolean
poolMaxSize: number
timeout: number
}
}

let config: CombinerConfig
Expand Down Expand Up @@ -141,6 +162,17 @@ if (DEV_MODE) {
fullNodeRetryCount: RETRY_COUNT,
fullNodeRetryDelayMs: RETRY_DELAY_IN_MS,
},
db: {
type: SupportedDatabase.Sqlite,
user: '',
password: '',
database: 'phoneNumber+privacy',
host: 'http://localhost',
port: undefined,
ssl: true,
poolMaxSize: DB_POOL_MAX_SIZE,
timeout: DB_TIMEOUT,
},
}
} else {
const functionConfig = functions.config()
Expand Down Expand Up @@ -188,6 +220,21 @@ if (DEV_MODE) {
functionConfig.pnp.full_node_retry_delay_ms ?? RETRY_DELAY_IN_MS
),
},
db: {
type: functionConfig.db.type
? functionConfig.db.type.toLowerCase()
: SupportedDatabase.Postgres,
user: functionConfig.db.username,
password: functionConfig.db.pass,
database: functionConfig.db.name,
host: `/cloudsql/${functionConfig.db.host}`,
port: functionConfig.db.port ? Number(functionConfig.db.port) : undefined,
ssl: toBool(functionConfig.db.ssl, true),
poolMaxSize: functionConfig.db.pool_max_size
? Number(functionConfig.db.pool_max_size)
: DB_POOL_MAX_SIZE,
timeout: functionConfig.db.timeout ? Number(functionConfig.db.timeout) : DB_TIMEOUT,
},
}
}
export default config
96 changes: 96 additions & 0 deletions packages/phone-number-privacy/combiner/src/database/database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { rootLogger } from '@celo/phone-number-privacy-common'
import Logger from 'bunyan'
import knex, { Knex } from 'knex'
import { CombinerConfig, DEV_MODE, SupportedDatabase, VERBOSE_DB_LOGGING } from '../config'
import { ACCOUNTS_COLUMNS, ACCOUNTS_TABLE } from './models/account'

export async function initDatabase(
config: CombinerConfig,
migrationsPath?: string,
doTestQuery = true
): Promise<Knex> {
const logger = rootLogger(config.serviceName)
logger.info({ config: config.db }, 'Initializing database connection')
const { type, host, port, user, password, database, ssl, poolMaxSize } = config.db

let connection: any
let client: string
if (type === SupportedDatabase.Postgres) {
logger.info('Using Postgres')
client = 'pg'
connection = {
user,
password,
database,
host,
port: port ?? 5432,
ssl,
pool: { max: poolMaxSize },
}
} else if (type === SupportedDatabase.MySql) {
logger.info('Using MySql')
client = 'mysql2'
connection = {
user,
password,
database,
host,
port: port ?? 3306,
ssl,
pool: { max: poolMaxSize },
}
} else if (type === SupportedDatabase.MsSql) {
logger.info('Using MS SQL')
client = 'mssql'
connection = {
user,
password,
database,
server: host,
port: port ?? 1433,
pool: { max: poolMaxSize },
}
} else if (type === SupportedDatabase.Sqlite) {
logger.info('Using SQLite - combiner')
client = 'sqlite3'
connection = ':memory:'
} else {
throw new Error(`Unsupported database type: ${type}`)
}
const db = knex({
client,
useNullAsDefault: type === SupportedDatabase.Sqlite,
connection,
debug: DEV_MODE && VERBOSE_DB_LOGGING,
})

logger.info('Running Migrations')

await db.migrate.latest({
directory: migrationsPath ?? './dist/database/migrations',
loadExtensions: ['.js'],
})

if (doTestQuery) {
await executeTestQuery(db, logger)
}

logger.info('Database initialized successfully')
return db
}

async function executeTestQuery(db: Knex, logger: Logger) {
logger.info('Counting accounts')
const result = await db(ACCOUNTS_TABLE).count(ACCOUNTS_COLUMNS.address).first()

if (!result) {
throw new Error('No result from count, have migrations been run?')
}

const count = Object.values(result)[0]
if (count === undefined || count === null || count === '') {
throw new Error('No result from count, have migrations been run?')
}

logger.info(`Found ${count} accounts`)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Knex } from 'knex'
import { ACCOUNTS_COLUMNS, ACCOUNTS_TABLE } from '../models/account'

export async function up(knex: Knex): Promise<any> {
if (!(await knex.schema.hasTable(ACCOUNTS_TABLE))) {
return knex.schema.createTable(ACCOUNTS_TABLE, (t) => {
t.string(ACCOUNTS_COLUMNS.address).notNullable().primary()
t.dateTime(ACCOUNTS_COLUMNS.createdAt).notNullable()
t.string(ACCOUNTS_COLUMNS.dek)
t.dateTime(ACCOUNTS_COLUMNS.onChainDataLastUpdated)
})
}
return null
}

export async function down(knex: Knex): Promise<any> {
return knex.schema.dropTable(ACCOUNTS_TABLE)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const ACCOUNTS_TABLE = 'accounts'

export enum ACCOUNTS_COLUMNS {
address = 'address',
createdAt = 'created_at',
dek = 'dek',
onChainDataLastUpdated = 'onChainDataLastUpdated',
}

export class Account {
[ACCOUNTS_COLUMNS.address]: string | undefined;
[ACCOUNTS_COLUMNS.createdAt]: Date = new Date();
[ACCOUNTS_COLUMNS.dek]: string | undefined;
[ACCOUNTS_COLUMNS.onChainDataLastUpdated]: Date | null = null

constructor(address: string, dek?: string) {
this.address = address
if (dek) {
this.dek = dek
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { DB_TIMEOUT, ErrorMessage } from '@celo/phone-number-privacy-common'
import Logger from 'bunyan'
import { Knex } from 'knex'
import { Account, ACCOUNTS_COLUMNS, ACCOUNTS_TABLE } from '../models/account'

function accounts(db: Knex) {
return db<Account>(ACCOUNTS_TABLE)
}

/*
* Get DEK signer record from DB.
*/
export async function getDekSignerRecord(
db: Knex,
account: string,
logger: Logger
): Promise<string | undefined> {
try {
logger.info('getting Dek Signer Record')
const dekSignerRecord = await accounts(db)
.where(ACCOUNTS_COLUMNS.address, account)
.select(ACCOUNTS_COLUMNS.dek)
.first()
.timeout(DB_TIMEOUT)

return dekSignerRecord ? dekSignerRecord[ACCOUNTS_COLUMNS.dek] : undefined
} catch (err) {
logger.error(ErrorMessage.DATABASE_GET_FAILURE)
logger.error(err)
return undefined
}
}

export async function updateDekSignerRecord(
db: Knex,
account: string,
newDek: string,
logger: Logger,
trx: Knex.Transaction
) {
logger.info(`updating Dek Signer Record`)
if (await getAccountExist(db, account, trx)) {
await accounts(db)
.transacting(trx)
.timeout(DB_TIMEOUT)
.where(ACCOUNTS_COLUMNS.address, account)
.update({ [ACCOUNTS_COLUMNS.dek]: newDek })
await accounts(db)
.transacting(trx)
.timeout(DB_TIMEOUT)
.where(ACCOUNTS_COLUMNS.address, account)
.update({ [ACCOUNTS_COLUMNS.onChainDataLastUpdated]: new Date() })
} else {
// account does not exists
const newAccount = new Account(account, newDek)
await accounts(db).transacting(trx).timeout(DB_TIMEOUT).insert(newAccount)
}
}

export function tableWithLockForTrx(baseQuery: Knex.QueryBuilder, trx?: Knex.Transaction) {
if (trx) {
// Lock relevant database rows for the duration of the transaction
return baseQuery.transacting(trx).forUpdate()
}
return baseQuery
}

async function getAccountExist(
db: Knex,
account: string,
trx?: Knex.Transaction
): Promise<boolean> {
const accountRecord = await tableWithLockForTrx(accounts(db), trx)
.where(ACCOUNTS_COLUMNS.address, account)
.first()
.timeout(DB_TIMEOUT)

return !!accountRecord
}
13 changes: 12 additions & 1 deletion packages/phone-number-privacy/combiner/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getContractKitWithAgent } from '@celo/phone-number-privacy-common'
import * as functions from 'firebase-functions'
import { Knex } from 'knex'
import config from './config'
import { initDatabase } from './database/database'
import { startCombiner } from './server'

require('dotenv').config()
Expand All @@ -12,5 +14,14 @@ export const combiner = functions
// Defined check required for running tests vs. deployment
minInstances: functions.config().service ? Number(functions.config().service.min_instances) : 0,
})
.https.onRequest(startCombiner(config, getContractKitWithAgent(config.blockchain)))
.https.onRequest(async (req, res) => {
try {
const db: Knex = await initDatabase(config)
const app = startCombiner(db, config, getContractKitWithAgent(config.blockchain))

app(req, res)
} catch (e) {
res.status(500).send('Internal Server Error')
}
})
export * from './config'
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
authenticateUser,
CombinerEndpoint,
DataEncryptionKeyFetcher,
ErrorMessage,
Expand All @@ -19,8 +18,11 @@ import { getKeyVersionInfo, sendFailure } from '../../../common/io'
import { getCombinerVersion, OdisConfig } from '../../../config'
import { logPnpSignerResponseDiscrepancies } from '../../services/log-responses'
import { findCombinerQuotaState } from '../../services/threshold-state'
import { authenticateUser } from '../../../utils/authentication'
import { Knex } from 'knex'

export function createPnpQuotaHandler(
db: Knex,
signers: Signer[],
config: OdisConfig,
dekFetcher: DataEncryptionKeyFetcher
Expand All @@ -33,7 +35,7 @@ export function createPnpQuotaHandler(
return
}

if (!(await authenticateUser(request, logger, dekFetcher))) {
if (!(await authenticateUser(db, request, logger, dekFetcher))) {
sendFailure(WarningMessage.UNAUTHENTICATED_USER, 401, response)
return
}
Expand Down
Loading
Loading