Skip to content

Commit e86306f

Browse files
authored
feat: Introduce whitelisted logic for prividium mode (#4190)
## What ❔ - Added `whitelisted_wallets` to the YAML configuration, allowing for flexible wallet authorization. - Updated the `Authorizer` class to handle the new `whitelisted_wallets` property, supporting both 'all' and specific wallet addresses. - Enhanced `YamlParser` to parse and validate the `whitelisted_wallets` field. - Implemented a new route in `usersRoutes` to check if a user's address is whitelisted based on the updated logic. ## Why ❔ <!-- Why are these changes done? What goal do they contribute to? What are the principles behind them? --> <!-- The `Why` has to be clear to non-Matter Labs entities running their own ZK Chain --> <!-- Example: PR templates ensure PR reviewers, observers, and future iterators are in context about the evolution of repos. --> ## Is this a breaking change? - [ ] Yes - [ ] No ## Operational changes <!-- Any config changes? Any new flags? Any changes to any scripts? --> <!-- Please add anything that non-Matter Labs entities running their own ZK Chain may need to know --> ## Checklist <!-- Check your PR fulfills the following items. --> <!-- For draft PRs check the boxes as you complete them. --> - [ ] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [ ] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. - [ ] Code has been formatted via `zkstack dev fmt` and `zkstack dev lint`.
1 parent 120fc13 commit e86306f

File tree

8 files changed

+149
-22
lines changed

8 files changed

+149
-22
lines changed

core/tests/ts-integration/src/private-rpc-permissions-editor.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ type YamlMethod = {
1010
};
1111

1212
type YamlContract = { address: string; methods: YamlMethod[] };
13-
type YamlRoot = { groups: any[]; contracts: YamlContract[] };
13+
type YamlRoot = {
14+
whitelisted_wallets?: string | string[];
15+
groups: any[];
16+
contracts: YamlContract[];
17+
};
1418
const mutex = new Mutex();
1519
/**
1620
* Adds a method with public read/write permissions to the YAML file.
@@ -62,3 +66,29 @@ export async function injectPermissionsToFile(
6266
await fs.writeFile(filePath, dumped, 'utf8');
6367
mutex.release();
6468
}
69+
70+
/**
71+
* Sets the whitelisted wallets in the YAML file.
72+
*
73+
* @param filePath - absolute or relative path to the YAML file
74+
* @param wallets - either "all" to allow all wallets, or an array of wallet addresses
75+
*/
76+
export async function setWhitelistedWallets(filePath: string, wallets: string | string[]): Promise<void> {
77+
await mutex.acquire();
78+
try {
79+
// --- load & parse ---------------------------------------------------------
80+
const raw = await fs.readFile(filePath, 'utf8');
81+
const data = yaml.load(raw) as YamlRoot;
82+
83+
if (!data) throw new Error('Invalid YAML structure');
84+
85+
// --- set whitelisted wallets ----------------------------------------------
86+
data.whitelisted_wallets = wallets;
87+
88+
// --- dump & save ----------------------------------------------------------
89+
const dumped = yaml.dump(data, { lineWidth: 120, sortKeys: false, quotingType: '"' });
90+
await fs.writeFile(filePath, dumped, 'utf8');
91+
} finally {
92+
mutex.release();
93+
}
94+
}

core/tests/ts-integration/tests/prividium.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { RetryableWallet } from '../src/retry-provider';
66
import * as zksync from 'zksync-ethers';
77
import { sleep } from 'utils';
88
import { shouldLoadConfigFromFile } from 'utils/build/file-configs';
9-
import { injectPermissionsToFile } from '../src/private-rpc-permissions-editor';
9+
import { injectPermissionsToFile, setWhitelistedWallets } from '../src/private-rpc-permissions-editor';
1010
import path from 'path';
1111
import { shouldChangeTokenBalances, shouldOnlyTakeFee } from '../src/modifiers/balance-checker';
1212
import { Token } from '../src/types';
@@ -76,6 +76,12 @@ describe('Tests for the private rpc', () => {
7676
const runCommand = `zkstack private-rpc run --verbose --chain ${chainName}`;
7777

7878
await executeCommandWithLogs(initCommand, await logsPath('private-rpc-init.log'));
79+
80+
// Set whitelisted wallets to "all" for integration tests
81+
const pathToHome = path.join(__dirname, '../../../..');
82+
const permissionsPath = path.join(pathToHome, `chains/${chainName}/configs/private-rpc-permissions.yaml`);
83+
await setWhitelistedWallets(permissionsPath, 'all');
84+
7985
executeCommandWithLogs(runCommand, await logsPath('private-rpc-run.log'));
8086

8187
await waitForHealth(rpcUrl());

private-rpc/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,33 @@ zkstack private-rpc reset-db
3030
The permissions can be modified by updating the `private-rpc-permissions.yaml` file. The exact path of this file will be
3131
printed during the init command.
3232

33+
The file has two main sections: `whitelisted_wallets` and `contracts`.
34+
35+
### Wallet Whitelisting
36+
37+
The `whitelisted_wallets` key controls which wallet addresses are allowed to connect to the RPC. This is the first layer
38+
of security. It has two modes:
39+
40+
1. **Allow all wallets**: To disable whitelisting and allow any address to connect, use the literal string `"all"`.
41+
42+
```yaml
43+
whitelisted_wallets: 'all'
44+
```
45+
46+
2. **Allow specific wallets**: To restrict access, provide a list of authorized wallet addresses. The list cannot be
47+
empty.
48+
49+
```yaml
50+
whitelisted_wallets:
51+
- '0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B'
52+
- '0x...another address...'
53+
```
54+
55+
### Contract & Method Permissions
56+
57+
The `contracts` section allows for fine-grained control over which addresses can call specific methods on specific
58+
contracts. This acts as a second layer of security after the initial wallet whitelist check.
59+
3360
## Creating access tokens
3461

3562
```bash
@@ -41,6 +68,11 @@ curl -X POST http://localhost:4041/users \
4168
}'
4269
```
4370

71+
The server will respond with a JSON object:
72+
73+
- `{"authorized":true}` if the address is allowed.
74+
- `{"authorized":false}` if the address is not on the whitelist.
75+
4476
## Using the rpc
4577

4678
```bash

private-rpc/example-permissions.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
# To allow any wallet to connect, use the literal string "all".
2+
# Example: whitelisted_wallets: "all"
3+
4+
# To restrict access to specific wallets, provide a non-empty list of addresses.
5+
whitelisted_wallets:
6+
- "0x742d35Cc6634C0532925a3b8D69C7F16F6d34d2c" # Example wallet address
7+
18
groups:
29
- name: "group1"
310
members:

private-rpc/src/permissions/authorizer.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ import { env } from '@/env';
88
export class Authorizer {
99
permissions: Map<string, AccessRule>;
1010
postReadFilters: Map<string, ResponseFilter>;
11+
whitelistedWallets: Set<Address> | 'all';
1112

1213
constructor() {
1314
this.permissions = new Map();
1415
this.postReadFilters = new Map();
16+
this.whitelistedWallets = new Set();
1517
}
1618

1719
addReadRule(address: Address, method: Hex, rule: AccessRule): void {
@@ -45,12 +47,27 @@ export class Authorizer {
4547
return this.postReadFilters.get(`${address}:${method}`) || null;
4648
}
4749

50+
isAddressWhitelisted(address: Address): boolean {
51+
if (this.whitelistedWallets === 'all') {
52+
return true;
53+
}
54+
return this.whitelistedWallets.has(address);
55+
}
56+
4857
reloadFromEnv(): Authorizer {
4958
const filePath = env.PERMISSIONS_YAML_PATH;
5059
console.log(`loading permissions from ${filePath}`);
5160
this.permissions = new Map();
5261
this.postReadFilters = new Map();
53-
new YamlParser(filePath).load_rules(this);
62+
const parser = new YamlParser(filePath);
63+
parser.load_rules(this);
64+
65+
const wallets = parser.getWhitelistedWallets();
66+
if (wallets === 'all') {
67+
this.whitelistedWallets = 'all';
68+
} else {
69+
this.whitelistedWallets = new Set(wallets);
70+
}
5471
return this;
5572
}
5673
}

private-rpc/src/permissions/yaml-parser.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import YAML from 'yaml';
1818

1919
const publicSchema = z.object({ type: z.literal('public') });
2020
const closedSchema = z.object({ type: z.literal('closed') });
21-
const groupSchema = z.object({
21+
const groupSchemaRaw = z.object({
2222
type: z.literal('group'),
2323
groups: z.array(z.string())
2424
});
@@ -28,10 +28,10 @@ const checkArgumentSchema = z.object({
2828
});
2929
const oneOfSchema = z.object({
3030
type: z.literal('oneOf'),
31-
rules: z.array(z.union([publicSchema, groupSchema, checkArgumentSchema]))
31+
rules: z.array(z.union([publicSchema, groupSchemaRaw, checkArgumentSchema]))
3232
});
3333

34-
const ruleSchema = z.union([publicSchema, closedSchema, groupSchema, checkArgumentSchema, oneOfSchema]);
34+
const ruleSchema = z.union([publicSchema, closedSchema, groupSchemaRaw, checkArgumentSchema, oneOfSchema]);
3535
type Rule = z.infer<typeof ruleSchema>;
3636

3737
const methodSchema = z.object({
@@ -42,20 +42,26 @@ const methodSchema = z.object({
4242
});
4343
type RawMethod = z.infer<typeof methodSchema>;
4444

45+
const groupSchema = z.object({
46+
name: z.string(),
47+
members: z.array(addressSchema)
48+
});
49+
type RawGroup = z.infer<typeof groupSchema>;
50+
51+
const contractSchema = z.object({
52+
address: addressSchema,
53+
methods: z.array(methodSchema)
54+
});
55+
4556
const yamlSchema = z.object({
46-
groups: z.array(
47-
z.object({
48-
name: z.string(),
49-
members: z.array(addressSchema)
50-
})
51-
),
52-
53-
contracts: z.array(
54-
z.object({
55-
address: addressSchema,
56-
methods: z.array(methodSchema)
57-
})
58-
)
57+
whitelisted_wallets: z.union([
58+
z.array(addressSchema).nonempty({
59+
message: 'whitelisted_wallets array cannot be empty. To allow all wallets, use the literal "all".'
60+
}),
61+
z.literal('all')
62+
]),
63+
groups: z.array(groupSchema),
64+
contracts: z.array(contractSchema)
5965
});
6066

6167
export class YamlParser {
@@ -66,7 +72,11 @@ export class YamlParser {
6672
const buf = readFileSync(filePath);
6773
const raw = YAML.parse(buf.toString());
6874
this.yaml = yamlSchema.parse(raw);
69-
this.groups = this.yaml.groups.map(({ name, members }) => new Group(name, members));
75+
this.groups = this.yaml.groups.map(({ name, members }: RawGroup) => new Group(name, members));
76+
}
77+
78+
getWhitelistedWallets(): Address[] | 'all' {
79+
return this.yaml.whitelisted_wallets;
7080
}
7181

7282
private extractSelector(method: RawMethod): Hex {

private-rpc/src/routes/rpc-routes.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export function rpcRoutes(app: WebServer) {
1313
maybe.expect(new HttpError('Unauthorized', 401))
1414
);
1515

16+
// Check if user is still whitelisted before processing any RPC calls
17+
if (!app.context.authorizer.isAddressWhitelisted(user.address)) {
18+
throw new HttpError('Forbidden: Address not whitelisted', 403);
19+
}
20+
1621
const handler = new RpcCallHandler(allHandlers, {
1722
currentUser: user.address,
1823
targetRpcUrl: app.context.targetRpc,

private-rpc/src/routes/users-routes.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ const createUserSchema = {
1818
export function usersRoutes(app: WebServer) {
1919
app.post('/', createUserSchema, async (req, reply) => {
2020
const { address, secret } = req.body;
21+
const { authorizer, db, createTokenSecret } = app.context;
22+
23+
if (!authorizer.isAddressWhitelisted(address)) {
24+
throw new HttpError('Forbidden: Address not whitelisted', 403);
25+
}
26+
2127
const token = nanoid(32);
2228

23-
if (secret !== app.context.createTokenSecret) {
29+
if (secret !== createTokenSecret) {
2430
throw new HttpError('forbidden', 403);
2531
}
2632

27-
await app.context.db.transaction(async (tx) => {
33+
await db.transaction(async (tx) => {
2834
await tx.delete(usersTable).where(eq(usersTable.address, address));
2935

3036
await tx.insert(usersTable).values({
@@ -35,4 +41,18 @@ export function usersRoutes(app: WebServer) {
3541

3642
return reply.send({ ok: true, token });
3743
});
44+
45+
const getUserSchema = {
46+
schema: {
47+
params: z.object({
48+
address: addressSchema
49+
})
50+
}
51+
};
52+
53+
app.get('/:address', getUserSchema, (req, reply) => {
54+
const { authorizer } = app.context;
55+
const authorized = authorizer.isAddressWhitelisted(req.params.address);
56+
return reply.send({ authorized });
57+
});
3858
}

0 commit comments

Comments
 (0)