A signing sidecar that signs L1 batch submission transactions via AWS KMS. It implements
the JSON-RPC signing protocol expected by --signer.endpoint, making it compatible with
any batcher that speaks that protocol (OP, Nitro, or otherwise). The private key never
leaves KMS hardware; the binary performs one KMS Sign call per request.
See the design document for requirements and architecture rationale.
If you have Nix with flakes enabled, nix develop drops you into a shell with the correct Rust toolchain and all system dependencies (OpenSSL, pkg-config, macOS SDK frameworks) already set up:
nix develop
cargo build
cargo testWithout Nix, make sure you have a recent stable Rust toolchain installed via rustup and run the same commands directly.
# Start localstack community edition (the :latest tag requires a paid license)
docker run -d -p 4566:4566 localstack/localstack:3
# Create a secp256k1 KMS key
AWS_DEFAULT_REGION=us-east-2 AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test \
aws --endpoint-url http://localhost:4566 kms create-key \
--key-usage SIGN_VERIFY \
--key-spec ECC_SECG_P256K1
# Note the KeyId from the output, then:
export AWS_KMS_KEY_ID=<key-id>
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_DEFAULT_REGION=us-east-2
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export CHAIN_ID=11155111 # Sepolia; use your devnet's chain ID
export LISTEN_ADDR=127.0.0.1:8547
cargo runThe signer logs the derived Ethereum address at startup. Configure the OP batcher:
--signer.endpoint http://127.0.0.1:8547
--signer.address <logged address>
The binary reads standard AWS credential environment variables / instance profile. Set only:
AWS_KMS_KEY_ID=<arn-or-key-id>
AWS_REGION=us-east-2
CHAIN_ID=<your-chain-id>
LISTEN_ADDR=127.0.0.1:8547 # localhost-only; cross-host deployments are not yet supported
Optionally restrict signing to a single BatchInbox address:
ALLOWED_TO=0xYourBatchInboxAddress
With the signer running, you can call it directly with curl:
# Health check
curl -s -X POST http://127.0.0.1:8547 \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"health_status","params":[],"id":1}'
# Sign an EIP-1559 transaction (substitute the address logged at startup)
curl -s -X POST http://127.0.0.1:8547 \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"method": "eth_signTransaction",
"params": [{
"from": "0x<address-from-startup-log>",
"to": "0xff00000000000000000000000000000011155111",
"gas": "0x5208",
"maxFeePerGas": "0x3B9ACA00",
"maxPriorityFeePerGas": "0xF4240",
"value": "0x1",
"nonce": "0x0",
"chainId": "0xAA36A7"
}],
"id": 1
}'
# Sign a 32-byte digest (op-batcher's Espresso batch-auth path via `eth_sign`).
# `data` is base64 — the exact wire format op-batcher sends (Go's default
# JSON encoding of `[]byte`). It must decode to exactly 32 bytes.
# The example below is base64 of a 32-byte digest of all zeros except the last byte = 1.
curl -s -X POST http://127.0.0.1:8547 \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"method": "eth_sign",
"params": [
"0x<address-from-startup-log>",
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAE="
],
"id": 1
}'eth_signTransaction returns a 0x-prefixed RLP-encoded signed transaction ready to broadcast;
chainId must match CHAIN_ID (0xAA36A7 = 11155111 for Sepolia). eth_sign returns a 65-byte
hex signature over the supplied digest (r||s||v with v ∈ {0,1}), with no Ethereum message prefix.
The JSON files under tests/fixtures/ are committed expected outputs produced by a small Go program that uses op-geth's TransactionArgs encoding. Re-run the generator only if the wire format changes:
cd tests/fixtures/gen
go mod tidy # first time, or after go.mod changes
go run . -out ../Commit the updated .json files alongside any code change that affects TransactionArgs serialisation.
| Variable | Required | Default | Description |
|---|---|---|---|
AWS_KMS_KEY_ID |
yes | — | KMS key ID or ARN |
AWS_REGION |
yes | — | AWS region |
CHAIN_ID |
yes | — | EVM chain ID; requests with other IDs are rejected |
LISTEN_ADDR |
no | 127.0.0.1:8547 |
TCP socket the JSON-RPC server binds to |
AWS_ENDPOINT_URL |
no | — | Override KMS endpoint (localstack) |
ALLOWED_TO |
no | — | Comma-separated allowlist of to addresses |
TLS_CERT_FILE |
no | — | Path to PEM server certificate chain; enables TLS |
TLS_KEY_FILE |
no | — | Path to PEM server private key; required when TLS_CERT_FILE is set |
TLS_CLIENT_CA_FILE |
no | — | Path to PEM CA certificate for verifying client certs; enables mTLS |