Skip to content

Commit

Permalink
Noble binary search
Browse files Browse the repository at this point in the history
  • Loading branch information
grod220 committed Oct 15, 2024
1 parent bcb4a2e commit 70a9988
Show file tree
Hide file tree
Showing 11 changed files with 1,074 additions and 215 deletions.
52 changes: 52 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,57 @@
"turbo": "^2.1.1",
"typescript": "^5.5.4",
"vitest": "1.x"
},
"pnpm": {
"overrides": {
"@penumbra-zone/bech32m": "file:///Users/gabe/Desktop/repos/web/packages/bech32m/penumbra-zone-bech32m-8.0.0.tgz",
"@penumbra-zone/client": "file:///Users/gabe/Desktop/repos/web/packages/client/penumbra-zone-client-19.0.0.tgz",
"@penumbra-zone/crypto-web": "file:///Users/gabe/Desktop/repos/web/packages/crypto/penumbra-zone-crypto-web-25.0.0.tgz",
"@penumbra-zone/getters": "file:///Users/gabe/Desktop/repos/web/packages/getters/penumbra-zone-getters-18.0.0.tgz",
"@penumbra-zone/perspective": "file:///Users/gabe/Desktop/repos/web/packages/perspective/penumbra-zone-perspective-32.0.0.tgz",
"@penumbra-zone/protobuf": "file:///Users/gabe/Desktop/repos/web/packages/protobuf/penumbra-zone-protobuf-6.1.0.tgz",
"@penumbra-zone/query": "file:///Users/gabe/Desktop/repos/web/packages/query/penumbra-zone-query-33.0.0.tgz",
"@penumbra-zone/services": "file:///Users/gabe/Desktop/repos/web/packages/services/penumbra-zone-services-36.0.0.tgz",
"@penumbra-zone/storage": "file:///Users/gabe/Desktop/repos/web/packages/storage/penumbra-zone-storage-32.0.0.tgz",
"@penumbra-zone/transport-chrome": "file:///Users/gabe/Desktop/repos/web/packages/transport-chrome/penumbra-zone-transport-chrome-8.0.1.tgz",
"@penumbra-zone/transport-dom": "file:///Users/gabe/Desktop/repos/web/packages/transport-dom/penumbra-zone-transport-dom-7.5.0.tgz",
"@penumbra-zone/types": "file:///Users/gabe/Desktop/repos/web/packages/types/penumbra-zone-types-24.0.0.tgz",
"@penumbra-zone/ui": "file:///Users/gabe/Desktop/repos/web/packages/ui/penumbra-zone-ui-10.0.2.tgz",
"@penumbra-zone/wasm": "file:///Users/gabe/Desktop/repos/web/packages/wasm/penumbra-zone-wasm-29.1.0.tgz",
"@penumbra-zone/keys": "file:///Users/gabe/Desktop/repos/web/packages/keys/penumbra-zone-keys-4.2.1.tgz"
},
"peerDependencyRules": {
"allowAny": [
"@penumbra-zone/bech32m",
"@penumbra-zone/client",
"@penumbra-zone/crypto-web",
"@penumbra-zone/getters",
"@penumbra-zone/perspective",
"@penumbra-zone/protobuf",
"@penumbra-zone/query",
"@penumbra-zone/services",
"@penumbra-zone/storage",
"@penumbra-zone/transport-chrome",
"@penumbra-zone/transport-dom",
"@penumbra-zone/types",
"@penumbra-zone/ui",
"@penumbra-zone/wasm",
"@penumbra-zone/bech32m",
"@penumbra-zone/client",
"@penumbra-zone/crypto-web",
"@penumbra-zone/getters",
"@penumbra-zone/keys",
"@penumbra-zone/perspective",
"@penumbra-zone/protobuf",
"@penumbra-zone/query",
"@penumbra-zone/services",
"@penumbra-zone/storage",
"@penumbra-zone/transport-chrome",
"@penumbra-zone/transport-dom",
"@penumbra-zone/types",
"@penumbra-zone/ui",
"@penumbra-zone/wasm"
]
}
}
}
1 change: 0 additions & 1 deletion packages/context/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export class Services implements ServicesInterface {

const viewServer = await ViewServer.initialize({
fullViewingKey,
epochDuration: sctParams.epochDuration,
getStoredTree: () => indexedDb.getStateCommitmentTree(),
idbConstants: indexedDb.constants(),
});
Expand Down
13 changes: 13 additions & 0 deletions packages/noble/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { penumbraEslintConfig } from '@repo/eslint-config';
import { config, parser } from 'typescript-eslint';

export default config({
...penumbraEslintConfig,
languageOptions: {
parser,
parserOptions: {
project: true,
tsconfigRootDir: import.meta.dirname,
},
},
});
24 changes: 24 additions & 0 deletions packages/noble/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@repo/noble",
"version": "1.0.0",
"private": true,
"license": "(MIT OR Apache-2.0)",
"type": "module",
"scripts": {
"lint": "eslint \"**/*.ts*\"",
"test": "vitest run"
},
"files": [
"src/",
"*.md"
],
"exports": {
".": "./src/client.ts"
},
"dependencies": {
"@cosmjs/stargate": "^0.32.4",
"@penumbra-zone/bech32m": "8.0.0",
"@penumbra-zone/protobuf": "6.1.0",
"@penumbra-zone/wasm": "29.1.0"
}
}
106 changes: 106 additions & 0 deletions packages/noble/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { MsgRegisterAccount } from '@penumbra-zone/protobuf/noble/forwarding/v1/tx_pb';
import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';
import { getNobleForwardingAddr } from '@penumbra-zone/wasm/keys';
import { StargateClient } from '@cosmjs/stargate';
import { Any } from '@bufbuild/protobuf';
import { Tx } from '@penumbra-zone/protobuf/cosmos/tx/v1beta1/tx_pb';
import { SignMode } from '@penumbra-zone/protobuf/cosmos/tx/signing/v1beta1/signing_pb';
import { ForwardingPubKey } from '@penumbra-zone/protobuf/noble/forwarding/v1/account_pb';
import { CosmosSdkError, isCosmosSdkErr } from './error';

export enum NobleRegistrationResponse {
// There are no funds in the account. Send funds first and request registration again.
NeedsDeposit,
// There were funds already deposited into the address. They have been flushed and forwarded to the sent registration address.
Success,
// A successful registration+flush has already occurred for this sequence number.
AlreadyRegistered,
}

export interface NobleClientInterface {
registerAccount: (props: {
sequence: number;
accountIndex?: number;
}) => Promise<NobleRegistrationResponse>;
}

interface NobleClientProps {
endpoint: string;
channel: string;
fvk: FullViewingKey;
}

export class NobleClient implements NobleClientInterface {
private readonly channel: string;
private readonly fvk: FullViewingKey;
private readonly endpoint: string;

constructor({ endpoint, channel, fvk }: NobleClientProps) {
this.fvk = fvk;
this.channel = channel;
this.endpoint = endpoint;
}

async registerAccount({ sequence, accountIndex }: { sequence: number; accountIndex?: number }) {
const { penumbraAddr, nobleAddrBech32, nobleAddrBytes } = getNobleForwardingAddr(
sequence,
this.fvk,
this.channel,
accountIndex,
);

console.log({ penumbraAddr: bech32mAddress(penumbraAddr), nobleAddrBech32 });

const msg = new MsgRegisterAccount({
signer: nobleAddrBech32,
recipient: bech32mAddress(penumbraAddr),
channel: this.channel,
});

const pubKey = new ForwardingPubKey({ key: nobleAddrBytes });

const tx = new Tx({
body: {
messages: [
new Any({ typeUrl: '/noble.forwarding.v1.MsgRegisterAccount', value: msg.toBinary() }),
],
},
authInfo: {
signerInfos: [
{
publicKey: new Any({
typeUrl: '/noble.forwarding.v1.ForwardingPubKey',
value: pubKey.toBinary(),
}),
modeInfo: { sum: { case: 'single', value: { mode: SignMode.DIRECT } } },
},
],
fee: {
gasLimit: 200000n,
},
},
signatures: [new Uint8Array()],
});

const client = await StargateClient.connect(this.endpoint);

try {
const res = await client.broadcastTx(tx.toBinary());
console.log(res);
if (res.code !== 0) {
throw new CosmosSdkError(res.code, 'sdk', JSON.stringify(res));
}
return NobleRegistrationResponse.Success;
} catch (e) {
if (isCosmosSdkErr(e)) {
if (e.code === 9) {
return NobleRegistrationResponse.NeedsDeposit;
} else if (e.code === 19 || e.message.includes('tx already exists in cache')) {
return NobleRegistrationResponse.AlreadyRegistered;
}
}
throw e;
}
}
}
16 changes: 16 additions & 0 deletions packages/noble/src/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export class CosmosSdkError extends Error {
code: number;
codespace: string;
log: string;

constructor(code: number, codespace: string, log: string) {
super(log);
this.code = code;
this.codespace = codespace;
this.log = log;
}
}

export const isCosmosSdkErr = (e: unknown): e is CosmosSdkError => {
return e !== null && typeof e === 'object' && 'code' in e;
};
147 changes: 147 additions & 0 deletions packages/noble/src/sequence-search.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { describe, expect, it } from 'vitest';
import { NobleClientInterface, NobleRegistrationResponse } from './client';
import { getNextSequence, MAX_SEQUENCE_NUMBER } from './sequence-search';
import { generateSpendKey, getFullViewingKey } from '@penumbra-zone/wasm/keys';

const seedPhrase =
'benefit cherry cannon tooth exhibit law avocado spare tooth that amount pumpkin scene foil tape mobile shine apology add crouch situate sun business explain';
const spendKey = generateSpendKey(seedPhrase);
const fvk = getFullViewingKey(spendKey);

class MockNobleClient implements NobleClientInterface {
private readonly responses = new Map<string, NobleRegistrationResponse>();

async registerAccount(props: { sequence: number; accountIndex?: number }) {
const key = this.hash(props);
const response = this.responses.get(key) ?? NobleRegistrationResponse.NeedsDeposit;
return Promise.resolve(response);
}

private hash({ sequence, accountIndex }: { sequence: number; accountIndex?: number }): string {
return `${sequence}-${accountIndex ? accountIndex : 0}`;
}

setResponse({
response,
sequence,
accountIndex,
}: {
response: NobleRegistrationResponse;
sequence: number;
accountIndex?: number;
}) {
const key = this.hash({ sequence, accountIndex });
this.responses.set(key, response);
}
}

describe('getNextSequence', () => {
it('should find the first unused sequence number when all numbers are unused', async () => {
const client = new MockNobleClient();
const seq = await getNextSequence({ client, fvk });
expect(seq).toEqual(0);
});

it('should find the next unused sequence number when some numbers are used', async () => {
const client = new MockNobleClient();
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: 0 });
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: 1 });

const seq = await getNextSequence({ client, fvk });
expect(seq).toEqual(2);
});

it('should return the next sequence number when the midpoint has a deposit waiting for registration', async () => {
const client = new MockNobleClient();
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: 0 });
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: 1 });
client.setResponse({ response: NobleRegistrationResponse.Success, sequence: 2 });

const seq = await getNextSequence({ client, fvk });
expect(seq).toEqual(3);
});

it('should handle the case when all sequence numbers are registered', async () => {
const client = new MockNobleClient();
for (let i = 0; i <= MAX_SEQUENCE_NUMBER; i++) {
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: i });
}

const seq = await getNextSequence({ client, fvk });
expect(seq).toBeGreaterThanOrEqual(0);
expect(seq).toBeLessThanOrEqual(MAX_SEQUENCE_NUMBER);
});

it('should handle a case deep in sequence', async () => {
// Set up client so that sequences 0 to 5 are registered, and 6 onwards are unused
const client = new MockNobleClient();
for (let i = 0; i <= 50_000; i++) {
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: i });
}

const seq = await getNextSequence({ client, fvk });
expect(seq).toEqual(50_001);
});

it('should handle entire sequence flush', async () => {
const client = new MockNobleClient();

// Simulate that all sequence numbers are registered except the last one
for (let i = 0; i < MAX_SEQUENCE_NUMBER; i++) {
client.setResponse({ response: NobleRegistrationResponse.Success, sequence: i });
}
client.setResponse({
response: NobleRegistrationResponse.Success,
sequence: MAX_SEQUENCE_NUMBER,
});

const seq = await getNextSequence({ client, fvk });
expect(seq).toBeGreaterThanOrEqual(0);
expect(seq).toBeLessThanOrEqual(MAX_SEQUENCE_NUMBER);
});

it('should handle incorrectly sequenced registrations', async () => {
const client = new MockNobleClient();
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: 0 });
client.setResponse({ response: NobleRegistrationResponse.Success, sequence: 1 });
client.setResponse({ response: NobleRegistrationResponse.NeedsDeposit, sequence: 2 });
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: 3 });
client.setResponse({ response: NobleRegistrationResponse.Success, sequence: 4 });
client.setResponse({ response: NobleRegistrationResponse.NeedsDeposit, sequence: 5 });

const seq = await getNextSequence({ client, fvk });

// The algorithm doesn't guarantee the earliest non-deposited, but should return at least one
expect([2, 5].includes(seq)).toBeTruthy();
});

it('should find the highest sequence number when only it is unused', async () => {
const client = new MockNobleClient();
for (let i = 0; i < MAX_SEQUENCE_NUMBER; i++) {
client.setResponse({ response: NobleRegistrationResponse.AlreadyRegistered, sequence: i });
}

const seq = await getNextSequence({ client, fvk });
expect(seq).toEqual(MAX_SEQUENCE_NUMBER);
});

it('should handle sequence numbers for different account indices', async () => {
const client = new MockNobleClient();
client.setResponse({
response: NobleRegistrationResponse.AlreadyRegistered,
sequence: 0,
accountIndex: 1,
});
client.setResponse({
response: NobleRegistrationResponse.NeedsDeposit,
sequence: 0,
accountIndex: 2,
});

const seqAccount1 = await getNextSequence({ client, fvk, accountIndex: 1 });
const seqAccount2 = await getNextSequence({ client, fvk, accountIndex: 2 });

expect(seqAccount1).toEqual(1); // Next available sequence for accountIndex: 1
expect(seqAccount2).toEqual(0); // Sequence 0 is available for accountIndex: 2
});
});
Loading

0 comments on commit 70a9988

Please sign in to comment.