diff --git a/packages/phone-number-privacy/combiner/package.json b/packages/phone-number-privacy/combiner/package.json
index 4fb952bb45..a49c65a337 100644
--- a/packages/phone-number-privacy/combiner/package.json
+++ b/packages/phone-number-privacy/combiner/package.json
@@ -48,6 +48,7 @@
"firebase-admin": "^11.10.1",
"firebase-functions": "^4.4.1",
"knex": "^2.1.0",
+ "lru-cache": "^10.0.1",
"node-fetch": "^2.6.9",
"pg": "^8.2.1",
"uuid": "^7.0.3"
diff --git a/packages/phone-number-privacy/combiner/src/common/tracing-utils.ts b/packages/phone-number-privacy/combiner/src/common/tracing-utils.ts
new file mode 100644
index 0000000000..f989c86e56
--- /dev/null
+++ b/packages/phone-number-privacy/combiner/src/common/tracing-utils.ts
@@ -0,0 +1,21 @@
+import opentelemetry, { SpanStatusCode } from '@opentelemetry/api'
+
+const tracer = opentelemetry.trace.getTracer('combiner-tracer')
+
+export function traceAsyncFunction(traceName: string, fn: () => Promise): Promise {
+ return tracer.startActiveSpan(traceName, async (span) => {
+ try {
+ const res = await fn()
+ span.setStatus({ code: SpanStatusCode.OK })
+ return res
+ } catch (err: any) {
+ span.setStatus({
+ code: SpanStatusCode.ERROR,
+ message: err instanceof Error ? err.message : undefined,
+ })
+ throw err
+ } finally {
+ span.end()
+ }
+ })
+}
diff --git a/packages/phone-number-privacy/combiner/src/config.ts b/packages/phone-number-privacy/combiner/src/config.ts
index 091817d2c2..bbdcbd517a 100644
--- a/packages/phone-number-privacy/combiner/src/config.ts
+++ b/packages/phone-number-privacy/combiner/src/config.ts
@@ -24,9 +24,11 @@ import {
pnpFullNodeTimeoutMs,
pnpKeysCurrentVersion,
pnpKeysVersions,
+ pnpMockDek,
pnpOdisServicesSigners,
pnpOdisServicesTimeoutMilliseconds,
pnpServiceName,
+ pnpShouldMockAccountService,
serviceNameConfig,
} from './utils/firebase-configs'
@@ -61,6 +63,8 @@ export interface OdisConfig {
fullNodeTimeoutMs: number
fullNodeRetryCount: number
fullNodeRetryDelayMs: number
+ shouldMockAccountService?: boolean
+ mockDek?: string
}
export interface CombinerConfig {
@@ -185,6 +189,8 @@ if (DEV_MODE) {
fullNodeTimeoutMs: pnpFullNodeTimeoutMs.value(),
fullNodeRetryCount: pnpFullNodeRetryCount.value(),
fullNodeRetryDelayMs: pnpFullNodeDelaysMs.value(),
+ shouldMockAccountService: pnpShouldMockAccountService.value(),
+ mockDek: pnpMockDek.value(),
},
domains: {
serviceName: domainServiceName.value(),
diff --git a/packages/phone-number-privacy/combiner/src/pnp/endpoints/quota/action.ts b/packages/phone-number-privacy/combiner/src/pnp/endpoints/quota/action.ts
index 64f31e66ba..8e87562dbf 100644
--- a/packages/phone-number-privacy/combiner/src/pnp/endpoints/quota/action.ts
+++ b/packages/phone-number-privacy/combiner/src/pnp/endpoints/quota/action.ts
@@ -1,8 +1,8 @@
import {
authenticateUser,
CombinerEndpoint,
- DataEncryptionKeyFetcher,
ErrorMessage,
+ ErrorType,
getSignerEndpoint,
hasValidAccountParam,
isBodyReasonablySized,
@@ -16,13 +16,14 @@ import { Signer, thresholdCallToSigners } from '../../../common/combine'
import { errorResult, ResultHandler } from '../../../common/handlers'
import { getKeyVersionInfo } from '../../../common/io'
import { getCombinerVersion, OdisConfig } from '../../../config'
+import { AccountService } from '../../services/account-services'
import { logPnpSignerResponseDiscrepancies } from '../../services/log-responses'
import { findCombinerQuotaState } from '../../services/threshold-state'
export function pnpQuota(
signers: Signer[],
config: OdisConfig,
- dekFetcher: DataEncryptionKeyFetcher
+ accountService: AccountService
): ResultHandler {
return async (request, response) => {
const logger = response.locals.logger
@@ -31,7 +32,9 @@ export function pnpQuota(
return errorResult(400, WarningMessage.INVALID_INPUT)
}
- if (!(await authenticateUser(request, logger, dekFetcher))) {
+ const warnings: ErrorType[] = []
+
+ if (!(await authenticateUser(request, logger, accountService.getAccount, warnings))) {
return errorResult(401, WarningMessage.UNAUTHENTICATED_USER)
}
@@ -47,7 +50,7 @@ export function pnpQuota(
responseSchema: PnpQuotaResponseSchema,
shouldCheckKeyVersion: false,
})
- const warnings = logPnpSignerResponseDiscrepancies(logger, signerResponses)
+ warnings.push(...logPnpSignerResponseDiscrepancies(logger, signerResponses))
const { threshold } = keyVersionInfo
diff --git a/packages/phone-number-privacy/combiner/src/pnp/endpoints/sign/action.ts b/packages/phone-number-privacy/combiner/src/pnp/endpoints/sign/action.ts
index 72c97b3a3a..7eba782fdc 100644
--- a/packages/phone-number-privacy/combiner/src/pnp/endpoints/sign/action.ts
+++ b/packages/phone-number-privacy/combiner/src/pnp/endpoints/sign/action.ts
@@ -1,7 +1,6 @@
import {
authenticateUser,
CombinerEndpoint,
- DataEncryptionKeyFetcher,
ErrorMessage,
ErrorType,
getSignerEndpoint,
@@ -21,13 +20,14 @@ import { BLSCryptographyClient } from '../../../common/crypto-clients/bls-crypto
import { errorResult, ResultHandler } from '../../../common/handlers'
import { getKeyVersionInfo, requestHasSupportedKeyVersion } from '../../../common/io'
import { getCombinerVersion, OdisConfig } from '../../../config'
+import { AccountService } from '../../services/account-services'
import { logPnpSignerResponseDiscrepancies } from '../../services/log-responses'
import { findCombinerQuotaState } from '../../services/threshold-state'
export function pnpSign(
signers: Signer[],
config: OdisConfig,
- dekFetcher: DataEncryptionKeyFetcher
+ accountService: AccountService
): ResultHandler {
return async (request, response) => {
const logger = response.locals.logger
@@ -39,7 +39,9 @@ export function pnpSign(
return errorResult(400, WarningMessage.INVALID_KEY_VERSION_REQUEST)
}
- if (!(await authenticateUser(request, logger, dekFetcher))) {
+ const warnings: ErrorType[] = []
+
+ if (!(await authenticateUser(request, logger, accountService.getAccount, warnings))) {
return errorResult(401, WarningMessage.UNAUTHENTICATED_USER)
}
const keyVersionInfo = getKeyVersionInfo(request, config, logger)
@@ -80,7 +82,7 @@ export function pnpSign(
processResult
)
- const warnings = logPnpSignerResponseDiscrepancies(logger, signerResponses)
+ warnings.push(...logPnpSignerResponseDiscrepancies(logger, signerResponses))
if (crypto.hasSufficientSignatures()) {
try {
diff --git a/packages/phone-number-privacy/combiner/src/pnp/services/account-services.ts b/packages/phone-number-privacy/combiner/src/pnp/services/account-services.ts
new file mode 100644
index 0000000000..e1ef6a11c5
--- /dev/null
+++ b/packages/phone-number-privacy/combiner/src/pnp/services/account-services.ts
@@ -0,0 +1,90 @@
+import { ContractKit } from '@celo/contractkit'
+import {
+ ErrorMessage,
+ FULL_NODE_TIMEOUT_IN_MS,
+ getDataEncryptionKey,
+ RETRY_COUNT,
+ RETRY_DELAY_IN_MS,
+} from '@celo/phone-number-privacy-common'
+import Logger from 'bunyan'
+import { LRUCache } from 'lru-cache'
+import { OdisError, wrapError } from '../../common/error'
+import { traceAsyncFunction } from '../../common/tracing-utils'
+
+export interface AccountService {
+ getAccount(address: string): Promise
+}
+
+export interface ContractKitAccountServiceOptions {
+ fullNodeTimeoutMs: number
+ fullNodeRetryCount: number
+ fullNodeRetryDelayMs: number
+}
+
+export class CachingAccountService implements AccountService {
+ private cache: LRUCache
+ constructor(baseService: AccountService) {
+ this.cache = new LRUCache({
+ max: 500,
+ ttl: 5 * 1000, // 5 seconds
+ allowStale: true,
+ fetchMethod: async (address: string) => {
+ return await baseService.getAccount(address)
+ },
+ })
+ }
+
+ getAccount = (address: string): Promise => {
+ return traceAsyncFunction('CachingAccountService - getAccount', async () => {
+ const dek = await this.cache.fetch(address)
+
+ if (dek === undefined) {
+ // TODO decide which error ot use here
+ throw new OdisError(ErrorMessage.FAILURE_TO_GET_DEK)
+ }
+ return dek
+ })
+ }
+}
+
+// tslint:disable-next-line:max-classes-per-file
+export class ContractKitAccountService implements AccountService {
+ constructor(
+ private readonly logger: Logger,
+ private readonly kit: ContractKit,
+ private readonly opts: ContractKitAccountServiceOptions = {
+ fullNodeTimeoutMs: FULL_NODE_TIMEOUT_IN_MS,
+ fullNodeRetryCount: RETRY_COUNT,
+ fullNodeRetryDelayMs: RETRY_DELAY_IN_MS,
+ }
+ ) {}
+
+ async getAccount(address: string): Promise {
+ return traceAsyncFunction('ContractKitAccountService - getAccount', async () => {
+ return wrapError(
+ getDataEncryptionKey(
+ address,
+ this.kit,
+ this.logger,
+ this.opts.fullNodeTimeoutMs,
+ this.opts.fullNodeRetryCount,
+ this.opts.fullNodeRetryDelayMs
+ ).catch((err) => {
+ // TODO could clean this up...quick fix since we weren't incrementing blockchain error counter
+ this.logger.error({ err, address }, 'failed to get on-chain dek for account')
+ throw err
+ }),
+ ErrorMessage.FAILURE_TO_GET_DEK
+ )
+ })
+ }
+}
+
+// tslint:disable-next-line:max-classes-per-file
+export class MockAccountService implements AccountService {
+ constructor(private readonly mockDek: string) {}
+
+ async getAccount(_address: string): Promise {
+ return this.mockDek
+ }
+}
diff --git a/packages/phone-number-privacy/combiner/src/pnp/services/log-responses.ts b/packages/phone-number-privacy/combiner/src/pnp/services/log-responses.ts
index 89bd98c268..864d69bac2 100644
--- a/packages/phone-number-privacy/combiner/src/pnp/services/log-responses.ts
+++ b/packages/phone-number-privacy/combiner/src/pnp/services/log-responses.ts
@@ -1,4 +1,5 @@
import {
+ ErrorType,
PnpQuotaRequest,
SignMessageRequest,
WarningMessage,
@@ -13,8 +14,8 @@ import {
export function logPnpSignerResponseDiscrepancies(
logger: Logger,
responses: Array>
-): string[] {
- const warnings: string[] = []
+): ErrorType[] {
+ const warnings: ErrorType[] = []
// TODO responses should all already be successes due to CombineAction receiveSuccess
// https://github.com/celo-org/celo-monorepo/issues/9826
diff --git a/packages/phone-number-privacy/combiner/src/server.ts b/packages/phone-number-privacy/combiner/src/server.ts
index 2b191f6122..5e67f74266 100644
--- a/packages/phone-number-privacy/combiner/src/server.ts
+++ b/packages/phone-number-privacy/combiner/src/server.ts
@@ -4,7 +4,6 @@ import {
getContractKitWithAgent,
KEY_VERSION_HEADER,
loggerMiddleware,
- newContractKitFetcher,
OdisRequest,
rootLogger,
} from '@celo/phone-number-privacy-common'
@@ -26,6 +25,11 @@ import { domainQuota } from './domain/endpoints/quota/action'
import { domainSign } from './domain/endpoints/sign/action'
import { pnpQuota } from './pnp/endpoints/quota/action'
import { pnpSign } from './pnp/endpoints/sign/action'
+import {
+ CachingAccountService,
+ ContractKitAccountService,
+ MockAccountService,
+} from './pnp/services/account-services'
require('events').EventEmitter.defaultMaxListeners = 15
@@ -62,13 +66,15 @@ export function startCombiner(config: CombinerConfig, kit?: ContractKit) {
})
})
- const dekFetcher = newContractKitFetcher(
- kit,
- logger,
- config.phoneNumberPrivacy.fullNodeTimeoutMs,
- config.phoneNumberPrivacy.fullNodeRetryCount,
- config.phoneNumberPrivacy.fullNodeRetryDelayMs
- )
+ const baseAccountService = config.phoneNumberPrivacy.shouldMockAccountService
+ ? new MockAccountService(config.phoneNumberPrivacy.mockDek!)
+ : new ContractKitAccountService(logger, kit, {
+ fullNodeTimeoutMs: config.phoneNumberPrivacy.fullNodeTimeoutMs,
+ fullNodeRetryCount: config.phoneNumberPrivacy.fullNodeRetryCount,
+ fullNodeRetryDelayMs: config.phoneNumberPrivacy.fullNodeRetryDelayMs,
+ })
+
+ const accountService = new CachingAccountService(baseAccountService)
const pnpSigners: Signer[] = JSON.parse(config.phoneNumberPrivacy.odisServices.signers)
const domainSigners: Signer[] = JSON.parse(config.domains.odisServices.signers)
@@ -80,7 +86,7 @@ export function startCombiner(config: CombinerConfig, kit?: ContractKit) {
createHandler(
phoneNumberPrivacy.odisServices.timeoutMilliSeconds,
phoneNumberPrivacy.enabled,
- pnpQuota(pnpSigners, config.phoneNumberPrivacy, dekFetcher)
+ pnpQuota(pnpSigners, config.phoneNumberPrivacy, accountService)
)
)
app.post(
@@ -88,7 +94,7 @@ export function startCombiner(config: CombinerConfig, kit?: ContractKit) {
createHandler(
phoneNumberPrivacy.odisServices.timeoutMilliSeconds,
phoneNumberPrivacy.enabled,
- pnpSign(pnpSigners, config.phoneNumberPrivacy, dekFetcher)
+ pnpSign(pnpSigners, config.phoneNumberPrivacy, accountService)
)
)
app.post(
diff --git a/packages/phone-number-privacy/combiner/src/utils/firebase-configs.ts b/packages/phone-number-privacy/combiner/src/utils/firebase-configs.ts
index 4a76f8b595..6be74bfa4f 100644
--- a/packages/phone-number-privacy/combiner/src/utils/firebase-configs.ts
+++ b/packages/phone-number-privacy/combiner/src/utils/firebase-configs.ts
@@ -41,6 +41,8 @@ export const pnpFullNodeRetryCount = defineInt('PNP_FULL_NODE_RETRY_COUNT', {
export const pnpFullNodeDelaysMs = defineInt('PNP_FULL_NODE_DELAY_MS', {
default: RETRY_DELAY_IN_MS,
})
+export const pnpShouldMockAccountService = defineBoolean('PNP_SHOULD_MOCK_ACCOUNT_SERVICE')
+export const pnpMockDek = defineString('PNP_MOCK_DECK')
// Domains
export const domainServiceName = defineString('DOMAIN_SERVICE_NAME', {