Skip to content

Commit b788def

Browse files
Merge pull request #175 from OffchainLabs/tx-filtering
Add transaction filtering support to nitro-testnode
2 parents dd3216e + a818eeb commit b788def

7 files changed

Lines changed: 340 additions & 11 deletions

File tree

docker-compose.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,37 @@ services:
480480
depends_on:
481481
- redis
482482

483+
minio:
484+
image: minio/minio:RELEASE.2025-09-07T16-13-09Z-cpuv1
485+
ports:
486+
- "127.0.0.1:9000:9000"
487+
- "127.0.0.1:9001:9001"
488+
volumes:
489+
- "minio-data:/data"
490+
environment:
491+
MINIO_ROOT_USER: minioadmin
492+
MINIO_ROOT_PASSWORD: minioadmin
493+
command: server /data --console-address ":9001"
494+
healthcheck:
495+
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
496+
interval: 5s
497+
timeout: 5s
498+
retries: 5
499+
500+
transaction-filterer:
501+
pid: host
502+
image: nitro-node-dev-testnode
503+
entrypoint: /usr/local/bin/transaction-filterer
504+
ports:
505+
- "127.0.0.1:8549:8547"
506+
volumes:
507+
- "config:/config"
508+
- "l1keystore:/home/user/l1keystore"
509+
command:
510+
- --conf.file=/config/transaction_filterer_config.json
511+
depends_on:
512+
- sequencer
513+
483514
volumes:
484515
l1data:
485516
consensus:
@@ -503,3 +534,4 @@ volumes:
503534
timeboost-auctioneer-data:
504535
contracts:
505536
contracts-local:
537+
minio-data:

scripts/accounts.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as crypto from "crypto";
55
import { runStress } from "./stress";
66
const path = require("path");
77

8-
const specialAccounts = 7;
8+
const specialAccounts = 8;
99

1010
async function writeAccounts() {
1111
for (let i = 0; i < specialAccounts; i++) {
@@ -50,6 +50,9 @@ export function namedAccount(
5050
if (name == "auctioneer") {
5151
return specialAccount(6);
5252
}
53+
if (name == "filterer") {
54+
return specialAccount(7);
55+
}
5356
if (name.startsWith("user_")) {
5457
return new ethers.Wallet(
5558
ethers.utils.sha256(ethers.utils.toUtf8Bytes(name))
@@ -89,7 +92,7 @@ export function namedAddress(
8992
export const namedAccountHelpString =
9093
"Valid account names:\n" +
9194
" funnel | sequencer | validator | l2owner\n" +
92-
" | auctioneer - known keys used by l2\n" +
95+
" | auctioneer | filterer - known keys used by l2\n" +
9396
" l3owner | l3sequencer - known keys used by l3\n" +
9497
" user_[Alphanumeric] - key will be generated from username\n" +
9598
" threaduser_[Alphanumeric] - same as user_[Alphanumeric]_thread_[thread-id]\n" +

scripts/config.ts

Lines changed: 226 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
11
import * as fs from 'fs';
2+
import * as crypto from 'crypto';
23
import * as consts from './consts'
34
import { ethers } from "ethers";
45
import { namedAddress } from './accounts'
6+
import { S3Client, PutObjectCommand, CreateBucketCommand, HeadBucketCommand } from "@aws-sdk/client-s3";
57

68
const path = require("path");
79

10+
const S3_CONFIG = {
11+
endpoint: "http://minio:9000",
12+
region: "us-east-1",
13+
credentials: {
14+
accessKeyId: "minioadmin",
15+
secretAccessKey: "minioadmin",
16+
},
17+
forcePathStyle: true,
18+
};
19+
20+
const S3_BUCKET = "tx-filtering";
21+
const S3_OBJECT_KEY = "address-hashes.json";
22+
823
function writePrysmConfig(argv: any) {
924
const prysm = `
1025
CONFIG_NAME: interop
@@ -184,6 +199,27 @@ function getChainInfo(): ChainInfo {
184199
return chainInfo;
185200
}
186201

202+
function applyTxFilteringConfig(config: any) {
203+
config.execution["address-filter"] = {
204+
"enable": true,
205+
"s3": {
206+
"access-key": "minioadmin",
207+
"secret-key": "minioadmin",
208+
"region": "us-east-1",
209+
"endpoint": "http://minio:9000",
210+
"bucket": "tx-filtering",
211+
"object-key": "address-hashes.json"
212+
},
213+
"poll-interval": "30s"
214+
};
215+
config.execution["transaction-filterer-rpc-client"] = {
216+
"url": "http://transaction-filterer:8547"
217+
};
218+
config["init"] = {
219+
"transaction-filtering-enabled": true
220+
};
221+
}
222+
187223
function writeConfigs(argv: any) {
188224
const valJwtSecret = path.join(consts.configpath, "val_jwt.hex")
189225
const chainInfoFile = path.join(consts.configpath, "l2_chain_info.json")
@@ -315,6 +351,9 @@ function writeConfigs(argv: any) {
315351
if (argv.anytrust) {
316352
simpleConfig.node["data-availability"]["rpc-aggregator"].enable = true
317353
}
354+
if (argv.txfiltering) {
355+
applyTxFilteringConfig(simpleConfig);
356+
}
318357
fs.writeFileSync(path.join(consts.configpath, "sequencer_config.json"), JSON.stringify(simpleConfig))
319358
} else {
320359
let validatorConfig = JSON.parse(baseConfJSON)
@@ -338,6 +377,9 @@ function writeConfigs(argv: any) {
338377
"redis-url": argv.redisUrl
339378
};
340379
}
380+
if (argv.txfiltering) {
381+
applyTxFilteringConfig(sequencerConfig);
382+
}
341383
fs.writeFileSync(path.join(consts.configpath, "sequencer_config.json"), JSON.stringify(sequencerConfig))
342384

343385
let posterConfig = JSON.parse(baseConfJSON)
@@ -418,7 +460,7 @@ function writeL2ChainConfig(argv: any) {
418460
"EnableArbOS": true,
419461
"AllowDebugPrecompiles": true,
420462
"DataAvailabilityCommittee": argv.anytrust,
421-
"InitialArbOSVersion": 40,
463+
"InitialArbOSVersion": argv.txfiltering ? 60 : 40,
422464
"InitialChainOwner": argv.l2owner,
423465
"GenesisBlockNum": 0
424466
}
@@ -658,6 +700,11 @@ export const writeConfigCommand = {
658700
describe: "run sequencer in timeboost mode",
659701
default: false
660702
},
703+
txfiltering: {
704+
boolean: true,
705+
describe: "enable transaction filtering mode",
706+
default: false
707+
},
661708
},
662709
handler: (argv: any) => {
663710
writeConfigs(argv)
@@ -688,6 +735,11 @@ export const writeL2ChainConfigCommand = {
688735
boolean: true,
689736
describe: "enable anytrust in chainconfig",
690737
default: false
738+
},
739+
txfiltering: {
740+
boolean: true,
741+
describe: "enable transaction filtering (requires ArbOS version 60)",
742+
default: false
691743
}
692744
},
693745
handler: (argv: any) => {
@@ -754,3 +806,176 @@ export const writeL2ReferenceDAConfigCommand = {
754806
writeL2ReferenceDAConfig(argv)
755807
}
756808
}
809+
810+
function writeTransactionFiltererConfig() {
811+
const config = {
812+
"chain-id": 412346,
813+
"sequencer": {
814+
"url": "http://sequencer:8547"
815+
},
816+
"wallet": {
817+
"account": namedAddress("filterer"),
818+
"password": consts.l1passphrase,
819+
"pathname": consts.l1keystore
820+
},
821+
"http": {
822+
"addr": "0.0.0.0",
823+
"port": 8547
824+
}
825+
};
826+
fs.writeFileSync(path.join(consts.configpath, "transaction_filterer_config.json"), JSON.stringify(config));
827+
}
828+
829+
export const writeTransactionFiltererConfigCommand = {
830+
command: "write-tx-filterer-config",
831+
describe: "writes transaction-filterer service config file",
832+
handler: () => {
833+
writeTransactionFiltererConfig()
834+
}
835+
}
836+
837+
export const initTxFilteringMinioCommand = {
838+
command: "init-tx-filtering-minio",
839+
describe: "initializes MinIO bucket and empty address hash list",
840+
handler: async () => {
841+
const salt = crypto.randomBytes(32).toString('hex');
842+
const initialAddressList = {
843+
"salt": salt,
844+
"hashing_scheme": "Sha256",
845+
"address_hashes": []
846+
};
847+
fs.writeFileSync(path.join(consts.configpath, "initial_address_hashes.json"), JSON.stringify(initialAddressList, null, 2));
848+
fs.writeFileSync(path.join(consts.configpath, "tx_filtering_salt.hex"), salt);
849+
850+
const s3Client = new S3Client(S3_CONFIG);
851+
852+
try {
853+
await s3Client.send(new HeadBucketCommand({ Bucket: S3_BUCKET }));
854+
console.log("Bucket already exists:", S3_BUCKET);
855+
} catch (err: any) {
856+
if (err.name === "NotFound" || err.$metadata?.httpStatusCode === 404) {
857+
await s3Client.send(new CreateBucketCommand({ Bucket: S3_BUCKET }));
858+
console.log("Created bucket:", S3_BUCKET);
859+
} else {
860+
throw err;
861+
}
862+
}
863+
864+
await uploadFilteredAddressesToMinio();
865+
console.log("Initialized tx-filtering bucket with empty address list.");
866+
}
867+
}
868+
869+
function computeAddressHash(address: string, salt: string): string {
870+
const normalizedAddress = address.toLowerCase();
871+
const data = salt + normalizedAddress.replace('0x', '');
872+
const hash = crypto.createHash('sha256').update(Buffer.from(data, 'hex')).digest('hex');
873+
return hash;
874+
}
875+
876+
async function uploadFilteredAddressesToMinio() {
877+
console.log("Uploading address list to MinIO...");
878+
const s3Client = new S3Client(S3_CONFIG);
879+
880+
const addressListPath = path.join(consts.configpath, "initial_address_hashes.json");
881+
const content = fs.readFileSync(addressListPath).toString();
882+
883+
await s3Client.send(new PutObjectCommand({
884+
Bucket: S3_BUCKET,
885+
Key: S3_OBJECT_KEY,
886+
Body: content,
887+
ContentType: "application/json",
888+
}));
889+
890+
console.log("Upload complete.");
891+
}
892+
893+
export const hashAddressCommand = {
894+
command: "hash-address",
895+
describe: "computes SHA256 hash for an address with salt",
896+
builder: {
897+
address: {
898+
string: true,
899+
describe: "address to hash",
900+
demandOption: true
901+
},
902+
},
903+
handler: (argv: any) => {
904+
const saltPath = path.join(consts.configpath, "tx_filtering_salt.hex");
905+
if (!fs.existsSync(saltPath)) {
906+
console.error("Salt file not found. Run init-tx-filtering-minio first.");
907+
process.exit(1);
908+
}
909+
const salt = fs.readFileSync(saltPath).toString().trim();
910+
const hash = computeAddressHash(argv.address, salt);
911+
console.log(hash);
912+
}
913+
}
914+
915+
export const addFilteredAddressCommand = {
916+
command: "add-filtered-address",
917+
describe: "adds an address hash to the S3 filter list",
918+
builder: {
919+
address: {
920+
string: true,
921+
describe: "address to add to filter list",
922+
demandOption: true
923+
},
924+
},
925+
handler: async (argv: any) => {
926+
const saltPath = path.join(consts.configpath, "tx_filtering_salt.hex");
927+
if (!fs.existsSync(saltPath)) {
928+
console.error("Salt file not found. Run init-tx-filtering-minio first.");
929+
process.exit(1);
930+
}
931+
const salt = fs.readFileSync(saltPath).toString().trim();
932+
const hash = computeAddressHash(argv.address, salt);
933+
934+
const addressListPath = path.join(consts.configpath, "initial_address_hashes.json");
935+
const addressList = JSON.parse(fs.readFileSync(addressListPath).toString());
936+
937+
const exists = addressList.address_hashes.some((entry: any) => entry.hash === hash);
938+
if (!exists) {
939+
addressList.address_hashes.push({ hash: hash });
940+
fs.writeFileSync(addressListPath, JSON.stringify(addressList, null, 2));
941+
console.log("Added address hash:", hash);
942+
await uploadFilteredAddressesToMinio();
943+
} else {
944+
console.log("Address hash already in list:", hash);
945+
}
946+
}
947+
}
948+
949+
export const removeFilteredAddressCommand = {
950+
command: "remove-filtered-address",
951+
describe: "removes an address hash from the S3 filter list",
952+
builder: {
953+
address: {
954+
string: true,
955+
describe: "address to remove from filter list",
956+
demandOption: true
957+
},
958+
},
959+
handler: async (argv: any) => {
960+
const saltPath = path.join(consts.configpath, "tx_filtering_salt.hex");
961+
if (!fs.existsSync(saltPath)) {
962+
console.error("Salt file not found. Run init-tx-filtering-minio first.");
963+
process.exit(1);
964+
}
965+
const salt = fs.readFileSync(saltPath).toString().trim();
966+
const hash = computeAddressHash(argv.address, salt);
967+
968+
const addressListPath = path.join(consts.configpath, "initial_address_hashes.json");
969+
const addressList = JSON.parse(fs.readFileSync(addressListPath).toString());
970+
971+
const index = addressList.address_hashes.findIndex((entry: any) => entry.hash === hash);
972+
if (index > -1) {
973+
addressList.address_hashes.splice(index, 1);
974+
fs.writeFileSync(addressListPath, JSON.stringify(addressList, null, 2));
975+
console.log("Removed address hash:", hash);
976+
await uploadFilteredAddressesToMinio();
977+
} else {
978+
console.log("Address hash not in list:", hash);
979+
}
980+
}
981+
}

scripts/ethcommands.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,3 +808,23 @@ export const waitForSyncCommand = {
808808
} while (syncStatus !== false)
809809
},
810810
};
811+
812+
export const grantFiltererRoleCommand = {
813+
command: "grant-filterer-role",
814+
describe: "grants TransactionFilterer role to the filterer account",
815+
handler: async (argv: any) => {
816+
argv.provider = new ethers.providers.WebSocketProvider(argv.l2url);
817+
818+
const arbOwnerIface = new ethers.utils.Interface([
819+
"function addTransactionFilterer(address filterer) external"
820+
]);
821+
822+
argv.data = arbOwnerIface.encodeFunctionData("addTransactionFilterer", [namedAddress("filterer")]);
823+
argv.from = "l2owner";
824+
argv.to = "address_" + ARB_OWNER;
825+
argv.ethamount = "0";
826+
827+
await runStress(argv, sendTransaction);
828+
argv.provider.destroy();
829+
}
830+
};

0 commit comments

Comments
 (0)