-
Notifications
You must be signed in to change notification settings - Fork 615
feat(pxe): recipient-side interactive handshake registration #24514
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
base: merge-train/fairies-v5
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,23 @@ | ||
| import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants'; | ||
| import { BlockNumber } from '@aztec/foundation/branded-types'; | ||
| import { randomInt } from '@aztec/foundation/crypto/random'; | ||
| import { Fr } from '@aztec/foundation/curves/bn254'; | ||
| import { Point } from '@aztec/foundation/curves/grumpkin'; | ||
| import { KeyStore } from '@aztec/key-store'; | ||
| import { openTmpStore } from '@aztec/kv-store/lmdb-v2'; | ||
| import { AztecAddress } from '@aztec/stdlib/aztec-address'; | ||
| import type { L2TipsProvider } from '@aztec/stdlib/block'; | ||
| import type { AztecNode } from '@aztec/stdlib/interfaces/server'; | ||
| import { SiloedTag, Tag } from '@aztec/stdlib/logs'; | ||
| import { makeBlockHeader, randomPrivateLogResult } from '@aztec/stdlib/testing'; | ||
| import { deriveKeys } from '@aztec/stdlib/keys'; | ||
| import { | ||
| AppTaggingSecret, | ||
| AppTaggingSecretKind, | ||
| type LogResult, | ||
| SiloedTag, | ||
| Tag, | ||
| appSiloEcdhSharedSecretPoint, | ||
| } from '@aztec/stdlib/logs'; | ||
| import { makeBlockHeader, makeL2Tips, randomPrivateLogResult } from '@aztec/stdlib/testing'; | ||
|
|
||
| import { type MockProxy, mock } from 'jest-mock-extended'; | ||
|
|
||
|
|
@@ -320,8 +330,136 @@ describe('LogService', () => { | |
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('fetchTaggedLogs', () => { | ||
| let recipient: AztecAddress; | ||
| let sharedSecret: Point; | ||
|
|
||
| beforeEach(async () => { | ||
| contractAddress = await AztecAddress.random(); | ||
| keyStore = new KeyStore(await openTmpStore('test')); | ||
| recipientTaggingStore = new RecipientTaggingStore(await openTmpStore('test')); | ||
| taggingSecretSourcesStore = new TaggingSecretSourcesStore(await openTmpStore('test')); | ||
| addressStore = new AddressStore(await openTmpStore('test')); | ||
|
|
||
| aztecNode = mock<AztecNode>(); | ||
|
|
||
| const l2TipsProvider = mock<L2TipsProvider>(); | ||
| l2TipsProvider.getL2Tips.mockResolvedValue(makeL2Tips(0)); | ||
|
|
||
| const completeAddress = await keyStore.addAccount(await deriveKeys(Fr.random()), Fr.random()); | ||
| await addressStore.addCompleteAddress(completeAddress); | ||
| recipient = completeAddress.address; | ||
|
|
||
| sharedSecret = await Point.random(); | ||
|
|
||
| const anchorBlockHeader = makeBlockHeader(randomInt(1000), { blockNumber: BlockNumber(INITIAL_L2_BLOCK_NUM) }); | ||
|
|
||
| logService = new LogService( | ||
| aztecNode, | ||
| anchorBlockHeader, | ||
| l2TipsProvider, | ||
| keyStore, | ||
| recipientTaggingStore, | ||
| taggingSecretSourcesStore, | ||
| addressStore, | ||
| 'test', | ||
| ); | ||
| }); | ||
|
|
||
| it('scans handshake secrets under the handshake derivation for both delivery modes', async () => { | ||
| await taggingSecretSourcesStore.addSharedSecret(recipient, 'handshake', sharedSecret); | ||
| const [unconstrainedTag, constrainedTag] = await handshakeTags(sharedSecret, contractAddress); | ||
|
|
||
| const unconstrainedLog = randomPrivateLogResult({ includeEffects: true }); | ||
| const constrainedLog = randomPrivateLogResult({ includeEffects: true }); | ||
| servePrivateLogsByTag( | ||
| aztecNode, | ||
| new Map([ | ||
| [unconstrainedTag.toString(), unconstrainedLog], | ||
| [constrainedTag.toString(), constrainedLog], | ||
| ]), | ||
| ); | ||
|
|
||
| const logs = await logService.fetchTaggedLogs(contractAddress, recipient, []); | ||
|
|
||
| const txHashes = logs.map(l => l.context.txHash); | ||
| expect(txHashes).toContainEqual(unconstrainedLog.txHash); | ||
| expect(txHashes).toContainEqual(constrainedLog.txHash); | ||
| }); | ||
|
|
||
| it('does not scan arbitrary secrets under the handshake derivation', async () => { | ||
| await taggingSecretSourcesStore.addSharedSecret(recipient, 'arbitrary-secret', sharedSecret); | ||
| const [unconstrainedTag, constrainedTag] = await handshakeTags(sharedSecret, contractAddress); | ||
|
|
||
| const directionalLog = randomPrivateLogResult({ includeEffects: true }); | ||
| const handshakeStreamLog = randomPrivateLogResult({ includeEffects: true }); | ||
| servePrivateLogsByTag( | ||
| aztecNode, | ||
| new Map([ | ||
| [(await directionalTag(sharedSecret, contractAddress, recipient)).toString(), directionalLog], | ||
| [unconstrainedTag.toString(), handshakeStreamLog], | ||
| [constrainedTag.toString(), handshakeStreamLog], | ||
| ]), | ||
| ); | ||
|
|
||
| const logs = await logService.fetchTaggedLogs(contractAddress, recipient, []); | ||
|
|
||
| const txHashes = logs.map(l => l.context.txHash); | ||
| expect(txHashes).toContainEqual(directionalLog.txHash); | ||
| expect(txHashes).not.toContainEqual(handshakeStreamLog.txHash); | ||
| }); | ||
|
|
||
| it('does not scan handshake secrets under the directional derivation', async () => { | ||
| await taggingSecretSourcesStore.addSharedSecret(recipient, 'handshake', sharedSecret); | ||
| const [unconstrainedTag] = await handshakeTags(sharedSecret, contractAddress); | ||
|
|
||
| const handshakeStreamLog = randomPrivateLogResult({ includeEffects: true }); | ||
| const directionalLog = randomPrivateLogResult({ includeEffects: true }); | ||
| servePrivateLogsByTag( | ||
| aztecNode, | ||
| new Map([ | ||
| [unconstrainedTag.toString(), handshakeStreamLog], | ||
| [(await directionalTag(sharedSecret, contractAddress, recipient)).toString(), directionalLog], | ||
| ]), | ||
| ); | ||
|
|
||
| const logs = await logService.fetchTaggedLogs(contractAddress, recipient, []); | ||
|
|
||
| const txHashes = logs.map(l => l.context.txHash); | ||
| expect(txHashes).toContainEqual(handshakeStreamLog.txHash); | ||
| expect(txHashes).not.toContainEqual(directionalLog.txHash); | ||
| }); | ||
|
|
||
| async function handshakeTags(secret: Point, app: AztecAddress): Promise<SiloedTag[]> { | ||
| const appSiloedSecret = await appSiloEcdhSharedSecretPoint(secret, app); | ||
| return Promise.all( | ||
| [AppTaggingSecretKind.UNCONSTRAINED, AppTaggingSecretKind.CONSTRAINED].map(kind => | ||
| SiloedTag.compute({ extendedSecret: new AppTaggingSecret(appSiloedSecret, app, kind), index: 0 }), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like we can use |
||
| ), | ||
| ); | ||
| } | ||
|
|
||
| async function directionalTag(secret: Point, app: AztecAddress, directedTo: AztecAddress): Promise<SiloedTag> { | ||
| const directionalSecret = await AppTaggingSecret.computeDirectional(secret, app, directedTo); | ||
| return SiloedTag.compute({ extendedSecret: directionalSecret, index: 0 }); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| /** Serves one private log per matching siloed tag and an empty page for every other tag. */ | ||
| function servePrivateLogsByTag(aztecNode: MockProxy<AztecNode>, logsByTag: Map<string, LogResult>) { | ||
| aztecNode.getPrivateLogsByTags.mockImplementation(({ tags }) => | ||
| Promise.resolve( | ||
| tags.map(tagQuery => { | ||
| const tag = tagQuery instanceof SiloedTag ? tagQuery : tagQuery.tag; | ||
| const log = logsByTag.get(tag.toString()); | ||
| return log ? [log] : []; | ||
| }), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| function makeLogRetrievalRequest( | ||
| contractAddress: AztecAddress, | ||
| tag: Tag, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ import type { CompleteAddress } from '@aztec/stdlib/contract'; | |
| import type { AztecNode } from '@aztec/stdlib/interfaces/server'; | ||
| import { | ||
| AppTaggingSecret, | ||
| AppTaggingSecretKind, | ||
| type LogResult, | ||
| type PendingTaggedLog, | ||
| SiloedTag, | ||
|
|
@@ -225,10 +226,12 @@ export class LogService { | |
|
|
||
| /** | ||
| * Computes the tagging secrets PXE can enumerate for a recipient: one per known sender (via ECDH) plus any | ||
| * pre-shared secret points registered directly for the recipient, each siloed to `contractAddress` and directed to | ||
| * `recipient`. These require knowing the recipient's address preimage and keys, so returns an empty array when those | ||
| * are unavailable. App-supplied secrets (e.g. handshake-derived) are handled separately by the caller and do not go | ||
| * through here. | ||
| * pre-shared secrets registered directly for the recipient. Each registered secret is scanned under the tag | ||
| * streams its kind can back: sender-derived and arbitrary secrets are siloed to `contractAddress` and directed to | ||
| * `recipient`, while handshake secrets back the bare handshake tag domains (one per delivery mode), since a | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handshake secrets backing both handshake tag domains isn't due to "a handshake may have no announcement to discover" right? We also say "These require knowing the recipient's address preiage..." I cannot tell if "These" is referring to the sender-derived, arbitrary, or handshake secrets. This comment could use a rewording or even just a simplification. I find the code in the method to be pretty clear after just seeing "Each registered secret is scanned under the tag streams its kind can back". |
||
| * handshake may have no announcement to discover (e.g. an interactive one). These require knowing the recipient's | ||
| * address preimage and keys, so returns an empty array when those are unavailable. App-supplied secrets (e.g. | ||
| * derived from discovered handshakes) are handled separately by the caller and do not go through here. | ||
| */ | ||
| async #getPointDerivedSecrets(contractAddress: AztecAddress, recipient: AztecAddress): Promise<AppTaggingSecret[]> { | ||
| const recipientCompleteAddress = await this.addressStore.getCompleteAddress(recipient); | ||
|
|
@@ -240,11 +243,23 @@ export class LogService { | |
| } | ||
| const recipientIvsk = await this.keyStore.getMasterIncomingViewingSecretKey(recipient); | ||
|
|
||
| const points = [ | ||
| ...(await this.#getSecretsForSenders(recipientCompleteAddress, recipientIvsk)), | ||
| ...(await this.taggingSecretSourcesStore.getSharedSecretsForRecipient(recipient)), | ||
| ]; | ||
| return Promise.all(points.map(secret => AppTaggingSecret.computeDirectional(secret, contractAddress, recipient))); | ||
| const [senderPoints, registeredSecrets] = await Promise.all([ | ||
| this.#getSecretsForSenders(recipientCompleteAddress, recipientIvsk), | ||
| this.taggingSecretSourcesStore.getSharedSecretsForRecipient(recipient), | ||
| ]); | ||
| const arbitraryPoints = registeredSecrets.filter(s => s.kind === 'arbitrary-secret').map(s => s.secret); | ||
| const handshakePoints = registeredSecrets.filter(s => s.kind === 'handshake').map(s => s.secret); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: we could just iterate all |
||
|
|
||
| return Promise.all([ | ||
| ...[...senderPoints, ...arbitraryPoints].map(secret => | ||
| AppTaggingSecret.computeDirectional(secret, contractAddress, recipient), | ||
| ), | ||
| // A handshake-backed sender tags messages with the bare app-siloed secret, one tag domain per delivery mode. | ||
| ...handshakePoints.flatMap(secret => [ | ||
| AppTaggingSecret.computeAppSiloed(secret, contractAddress, AppTaggingSecretKind.UNCONSTRAINED), | ||
| AppTaggingSecret.computeAppSiloed(secret, contractAddress, AppTaggingSecretKind.CONSTRAINED), | ||
| ]), | ||
| ]); | ||
| } | ||
|
|
||
| async #getSecretsForSenders( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just noting I added related but different tests and a new helper in yarn-project/pxe/src/logs/log_service.test.ts. We could move those here or just leave the PRs as they are and handle merge conflicts once they come up