Skip to content
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

Performance increase to sign & verify signature #20

Merged
merged 6 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# Webhook
SECRET_KEY="<Ed25519 Secret-key>"
PUBLIC_KEY="<Ed25519 Public-key>"
PRIVATE_KEY=<Ed25519 Private Key>
WEBHOOK_URL=http://127.0.0.1:3000
PORT=9102

# Get Substreams API Key
# https://app.pinax.network
# https://app.streamingfast.io/
SUBSTREAMS_API_KEY="<Substreams API Key @ https://pinax.network>"
SUBSTREAMS_API_KEY=<Substreams API Token @ https://pinax.network>
SUBSTREAMS_ENDPOINT=https://eth.substreams.pinax.network:443

# Substreams package
# Substreams Package (*.spkg)
MANIFEST=https://github.com/pinax-network/substreams/releases/download/blocks-v0.1.0/blocks-v0.1.0.spkg
MODULE_NAME=map_blocks
START_BLOCK=-10
PRODUCTION_MODE=true

# Webhook (Optional)
DISABLE_PING=false
DISABLE_SIGNATURE=false
VERBOSE=true
MAXIMUM_ATTEMPTS=100
23 changes: 0 additions & 23 deletions .github/workflows/bun-build.yml

This file was deleted.

2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ docker-compose.yml
substreams-sink-webhook
bun.lockb
.vscode
examples/**/bun.lockb
examples/**/package-lock.json
81 changes: 43 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
## HTTP Server examples

- [`Bun`](/examples/bun) - https://bun.sh/
- [`Deno`](/examples/deno) - https://deno.com/runtime
- [`Express`](/examples/express) - https://expressjs.com/
- [`node:http`](/examples/node:http) - https://nodejs.org/api/http.html
- [POST request](/examples/post.http)
- [`node:http`](/examples/node) - https://nodejs.org/api/http.html
- [POST request](/examples/http)

## 📖 References

Expand All @@ -30,8 +29,8 @@ The POST message will be a JSON object with the following structure:
```http
POST http://localhost:3000 HTTP/1.1
content-type: application/json
x-signature-ed25519: 6ec208fc250059fdb0fa543e01339ee3c6967da6fc7b6bf86dcd8217fa2e130ce2e17a5258fcf9bbe415de223d00eaee2f6949ef3a44594b42e7fb1a53481802
x-signature-timestamp: 1696733583
x-signature-ed25519: 8f01c66ccda5b987c43d913290419572ea586dbef2077fa166c4a84797e1d2c76b305bc67ed43efb1fc841562620a61cb59c4d8a13de689a2e98ead19190f80c
x-signature-timestamp: 1707776632
```

**body**
Expand All @@ -41,23 +40,23 @@ x-signature-timestamp: 1696733583
```json
{
"status": 200,
"cursor": "T0S2BNqDj6a8pKMA6bEXAaWwLpc_DFltXAvkKhhBj4L29XqRiMmiVjVzbU_UxPzyiRLsSV-q2tzLEih6oMZR7oLpwbA2vHI_F39_l9vm_ODoe6CjP1tJdekzCuzcN9DRWD7eYgv7c7EK6dXiMqeMM0ZkNsEjfmLn2j0EpYJWdaUVunUzlT2vdc6Ag_iU-dAQrOV0QLelxyOkUzJ-fx5cbJ6GNaPKuW51bQ==",
"cursor": "OJGbpO9ZnZcwvxW38_FO8KWwLpcyA1lrUQPgKRFL04Py8yCW35v1VTB1O0-Elami3RztQlOp2tmcHC9y9ZQFuoDrxLpj6yU-FXorwoHr_OfqLPumMQwTJ-hgWeuKYNLeWDjTagn4ersEtNGzbvLaY0UxZZUhK2G62z1VptdXJfEWuiJmyjmrIZrRhK-WoNAS_rEkQ7L1xCmhDzJ4K0dTPcSDNPKZuDR2",
"session": {
"traceId": "4ebea20349c16844d92bf6c961f627fa",
"resolvedStartBlock": 32900744
"traceId": "3cbb0a1c772a47a72995d95f4c6d2cff",
"resolvedStartBlock": 53448515
},
"clock": {
"timestamp": "2022-09-09T20:23:38.000Z",
"number": 32901090,
"id": "9058ded4fd65b4de2d772564366f1b61bc328bac7a4c4b87d73ca6ab4bae6be8"
"timestamp": "2024-02-12T22:23:51.000Z",
"number": 53448530,
"id": "f843bc26cea0cbd50b09699546a8a97de6a1727646c17a857c5d8d868fc26142"
},
"manifest": {
"substreamsEndpoint": "https://polygon.substreams.pinax.network:443",
"chain": "polygon",
"finalBlockOnly": "false",
"moduleName": "map_block_stats",
"type": "subtivity.v1.BlockStats",
"moduleHash": "6fb7bbc60685bbfc1cd209d26697639e05efdb24"
"moduleName": "map_blocks",
"type": "sf.substreams.v1.Clock",
"moduleHash": "44c506941d5f30db6cca01692624395d1ac40cd1"
},
"data": {
...
Expand All @@ -71,21 +70,18 @@ x-signature-timestamp: 1696733583
import nacl from "tweetnacl";

// ...HTTP server
const PUBLIC_KEY = "APPLICATION_PUBLIC_KEY";

// get headers and body from POST request
const rawBody = await request.text();
const signature = request.headers.get("x-signature-ed25519");
const expiry = Number(request.headers.get("x-signature-ed25519-expiry"));
const publicKey = request.headers.get("x-signature-ed25519-public-key");

if (new Date().getTime() >= expiry) return new Response("signature expired", { status: 401 });
const timestamp = request.headers.get("x-signature-timestamp");
const body = await request.text();

// validate signature using public key
const payload = JSON.stringify({ exp: expiry, id: publicKey });
const isVerified = nacl.sign.detached.verify(
Buffer.from(payload),
Buffer.from(timestamp + body),
Buffer.from(signature, "hex"),
Buffer.from(publicKey, "hex")
Buffer.from(PUBLIC_KEY, "hex")
);

if (!isVerified) {
Expand All @@ -97,18 +93,26 @@ if (!isVerified) {

```env
# Webhook
SECRET_KEY="<Ed25519 Secret-key>"
PUBLIC_KEY="<Ed25519 Public-key>"
PRIVATE_KEY=<Ed25519 Private Key>
WEBHOOK_URL=http://127.0.0.1:3000

# Substreams endpoint
SUBSTREAMS_API_TOKEN="<Substreams API Token @ https://pinax.network>"
SUBSTREAMS_ENDPOINT=https://polygon.substreams.pinax.network:443

# Substreams package
MANIFEST=https://github.com/pinax-network/subtivity-substreams/releases/download/v0.3.0/subtivity-ethereum-v0.3.0.spkg
MODULE_NAME=map_block_stats
START_BLOCK=-1
PORT=9102

# Get Substreams API Key
# https://app.pinax.network
# https://app.streamingfast.io/
SUBSTREAMS_API_KEY=<Substreams API Token @ https://pinax.network>

# Substreams Package (*.spkg)
MANIFEST=https://github.com/pinax-network/substreams/releases/download/blocks-v0.1.0/blocks-v0.1.0.spkg
MODULE_NAME=map_blocks
START_BLOCK=-10
PRODUCTION_MODE=true

# Webhook (Optional)
DISABLE_PING=false
DISABLE_SIGNATURE=false
VERBOSE=true
MAXIMUM_ATTEMPTS=100
```

## Help
Expand All @@ -127,12 +131,11 @@ Options:
-s --start-block <int> Start block to stream from (defaults to -1, which means the initialBlock of the first module you are streaming) (default: "-1", env: START_BLOCK)
-t --stop-block <int> Stop block to end stream at, inclusively (env: STOP_BLOCK)
-p, --params <string...> Set a params for parameterizable modules. Can be specified multiple times. (ex: -p module1=valA -p module2=valX&valY) (default: [], env: PARAMS)
--substreams-api-token <string> API token for the substream endpoint or API key if '--auth-issue-url' is specified (default: "", env: SUBSTREAMS_API_TOKEN)
--auth-issue-url <string> URL used to issue a token (default: "https://auth.pinax.network/v1/auth/issue", env: AUTH_ISSUE_URL)
--substreams-api-key <string> API key for the Substream endpoint (env: SUBSTREAMS_API_KEY)
--delay-before-start <int> Delay (ms) before starting Substreams (default: 0, env: DELAY_BEFORE_START)
--cursor-path <string> File path or URL to cursor lock file (default: "cursor.lock", env: CURSOR_PATH)
--http-cursor-auth <string> Basic auth credentials for http cursor (ex: username:password) (env: HTTP_CURSOR_AUTH)
--production-mode <boolean> Enable production mode, allows cached substreams data if available (default: "false", env: PRODUCTION_MODE)
--production-mode <boolean> Enable production mode, allows cached Substreams data if available (default: "false", env: PRODUCTION_MODE)
--inactivity-seconds <int> If set, the sink will stop when inactive for over a certain amount of seconds (default: 300, env: INACTIVITY_SECONDS)
--hostname <string> The process will listen on this hostname for any HTTP and Prometheus metrics requests (default: "localhost", env: HOSTNAME)
--port <int> The process will listen on this port for any HTTP and Prometheus metrics requests (default: 9102, env: PORT)
Expand All @@ -142,8 +145,10 @@ Options:
--final-blocks-only <boolean> Only process blocks that have pass finality, to prevent any reorg and undo signal by staying further away from the chain HEAD (default: "false", env: FINAL_BLOCKS_ONLY)
--verbose <boolean> Enable verbose logging (default: "false", env: VERBOSE)
--webhook-url <string> Webhook URL to send POST (env: WEBHOOK_URL)
--secret-key <string> TweetNaCl Secret-key to sign POST data payload (env: SECRET_KEY)
--disable-ping Disable ping on init (default: false, env: DISABLE_PING)
--private-key <string> Ed25519 private key to sign POST data payload (env: PRIVATE_KEY)
--disable-ping <boolean> Disable ping on init (choices: "true", "false", default: false, env: DISABLE_PING)
--disable-signature <boolean> Disable Ed25519 signature (choices: "true", "false", default: false, env: DISABLE_SIGNATURE)
--maximum-attempts <number> Maximum attempts to retry POST (default: 100, env: MAXIMUM_ATTEMPTS)
-h, --help display help for command
```

Expand Down
29 changes: 16 additions & 13 deletions bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,43 @@ import { ping } from "../src/ping.js";

export interface WebhookRunOptions extends commander.RunOptions {
webhookUrl: string;
secretKey: string;
disablePing: boolean;
privateKey: string;
expiryTime: number;
maximumAttempts: number;
disablePing: string;
disableSignature: string;
}

const expirationOption = new Option("--expiry-time <number>", "Time before a transmission becomes invalid (in seconds)").env("EXPIRY_TIME").default(40)
const webhookUrlOption = new Option("--webhook-url <string>", "Webhook URL to send POST").makeOptionMandatory().env("WEBHOOK_URL");
const privateKeyOption = new Option("--private-key <string>", "Ed25519 private key to sign POST data payload").makeOptionMandatory().env("PRIVATE_KEY");

// Run Webhook Sink
const program = commander.program(pkg);
const command = commander.run(program, pkg);
command.addOption(new Option("--webhook-url <string>", "Webhook URL to send POST").makeOptionMandatory().env("WEBHOOK_URL"));
command.addOption(new Option("--secret-key <string>", "TweetNaCl Secret-key to sign POST data payload").makeOptionMandatory().env("SECRET_KEY"));
command.addOption(new Option("--disable-ping", "Disable ping on init").env("DISABLE_PING").default(false));
command.addOption(expirationOption);
command.addOption(webhookUrlOption);
command.addOption(privateKeyOption);
command.addOption(new Option("--disable-ping <boolean>", "Disable ping on init").choices(["true", "false"]).env("DISABLE_PING").default(false));
command.addOption(new Option("--disable-signature <boolean>", "Disable Ed25519 signature").choices(["true", "false"]).env("DISABLE_SIGNATURE").default(false));
command.addOption(new Option("--maximum-attempts <number>", "Maximum attempts to retry POST").env("MAXIMUM_ATTEMPTS").default(100));
command.action(action);

program
.command("keypair")
.description("Generate TweetNaCl keypair")
.action(() => {
const { publicKey, secretKey } = keyPair();
const { publicKey, privateKey } = keyPair();
console.log(`PUBLIC_KEY=${publicKey}`);
console.log(`SECRET_KEY=${secretKey}`);
console.log(`PRIVATE_KEY=${privateKey}`);
});

program
.command("ping")
.description("Ping Webhook URL")
.addOption(new Option("--webhook-url <string>", "Webhook URL to send POST").makeOptionMandatory().env("WEBHOOK_URL"))
.addOption(new Option("--secret-key <string>", "TweetNaCl Secret-key to sign POST data payload").makeOptionMandatory().env("SECRET_KEY"))
.addOption(expirationOption)
.addOption(webhookUrlOption)
.addOption(privateKeyOption)
.action(async (options: WebhookRunOptions) => {
logger.settings.type = "hidden";
const response = await ping(options.webhookUrl, options.secretKey, options.expiryTime);
const response = await ping(options.webhookUrl, options.privateKey);
if (response) console.log("✅ OK");
else console.log("⁉️ ERROR");
});
Expand Down
4 changes: 2 additions & 2 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
},
"formatter": {
"indentStyle": "space",
"lineWidth": 120
"lineWidth": 160
},
"linter": {
"rules": {
Expand All @@ -39,6 +39,6 @@
}
},
"organizeImports": {
"enabled": true
"enabled": false
}
}
13 changes: 1 addition & 12 deletions examples/bun/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
FROM oven/bun

WORKDIR /app

ENV PUBLIC_KEY $PUBLIC_KEY
ENV PORT $PORT

COPY bun.lockb ./
COPY package*.json ./

RUN bun install

COPY . .

RUN bun install
CMD [ "bun", "http.ts" ]
Binary file removed examples/bun/bun.lockb
Binary file not shown.
24 changes: 8 additions & 16 deletions examples/bun/http.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "dotenv/config";
import nacl from "tweetnacl";
import { ed25519 } from "@noble/curves/ed25519.js";

const PORT = process.env.PORT ?? 3000;
const PUBLIC_KEY = process.env.PUBLIC_KEY ?? "a3cb7366ee8ca77225b4d41772e270e4e831d171d1de71d91707c42e7ba82cc9";
Expand All @@ -12,28 +12,20 @@ export default {
async fetch(request) {
// get headers and body from POST request
const signature = request.headers.get("x-signature-ed25519");
const expiry = Number(request.headers.get("x-signature-ed25519-expiry"));
const publicKey = request.headers.get("x-signature-ed25519-public-key");

const timestamp = request.headers.get("x-signature-timestamp");
const body = await request.text();

if (!signature) return new Response("missing required signature in headers", { status: 400 });
if (!expiry) return new Response("missing required expiry in headers", { status: 400 });
if (!publicKey) return new Response("missing required public key in headers", { status: 400 });
if (!timestamp) return new Response("missing required timestamp in headers", { status: 400 });
if (!body) return new Response("missing body", { status: 400 });

if (new Date().getTime() >= expiry) return new Response("signature expired", { status: 401 });
if (publicKey !== PUBLIC_KEY) return new Response("unknown public key", { status: 401 });

// validate signature using public key
console.log({signature, expiry, publicKey});
const payload = JSON.stringify({ exp: expiry, id: publicKey });
const isVerified = nacl.sign.detached.verify(
Buffer.from(payload),
Buffer.from(signature, "hex"),
Buffer.from(publicKey, "hex"),
const isVerified = ed25519.verify(
signature,
Buffer.from(timestamp + body),
PUBLIC_KEY,
);
console.log({ isVerified, signature });
console.log({ isVerified, timestamp, signature });
console.log(body);

if (!isVerified) {
Expand Down
2 changes: 1 addition & 1 deletion examples/bun/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"start": "bun http.ts"
},
"dependencies": {
"tweetnacl": "latest",
"@noble/curves": "latest",
"dotenv": "latest"
}
}
10 changes: 0 additions & 10 deletions examples/deno/Dockerfile

This file was deleted.

Loading
Loading