Skip to content
Open
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
2 changes: 1 addition & 1 deletion common/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"runtime/debug"
)

var tag = "v4.7.16"
var tag = "v4.7.17"

var commit = func() string {
if info, ok := debug.ReadBuildInfo(); ok {
Expand Down
1 change: 1 addition & 0 deletions rollup/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/aws/aws-sdk-go-v2 v1.36.3
github.com/aws/aws-sdk-go-v2/config v1.29.14
github.com/aws/aws-sdk-go-v2/credentials v1.17.67
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0
github.com/consensys/gnark-crypto v0.16.0
github.com/crate-crypto/go-kzg-4844 v1.1.0
Expand Down
2 changes: 2 additions & 0 deletions rollup/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3 h1:RivOtUH3eEu6SWnUMFHKAW4MqDOzWn1vGQ3S38Y5QMg=
github.com/aws/aws-sdk-go-v2/service/kms v1.38.3/go.mod h1:cQn6tAF77Di6m4huxovNM7NVAozWTZLsDRp9t8Z/WYk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0 h1:fV4XIU5sn/x8gjRouoJpDVHj+ExJaUk4prYF+eb6qTs=
github.com/aws/aws-sdk-go-v2/service/s3 v1.80.0/go.mod h1:qbn305Je/IofWBJ4bJz/Q7pDEtnnoInw/dGt71v6rHE=
github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8=
Expand Down
18 changes: 17 additions & 1 deletion rollup/internal/config/relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,10 @@ type GasOracleConfig struct {

// SignerConfig - config of signer, contains type and config corresponding to type
type SignerConfig struct {
SignerType string `json:"signer_type"` // type of signer can be PrivateKey or RemoteSigner
SignerType string `json:"signer_type"` // type of signer can be PrivateKey, RemoteSigner or AWSKMS
PrivateKeySignerConfig *PrivateKeySignerConfig `json:"private_key_signer_config"`
RemoteSignerConfig *RemoteSignerConfig `json:"remote_signer_config"`
AWSKMSSignerConfig *AWSKMSSignerConfig `json:"aws_kms_signer_config"`
}

// PrivateKeySignerConfig - config of private signer, contains private key
Expand All @@ -138,3 +139,18 @@ type RemoteSignerConfig struct {
RemoteSignerUrl string `json:"remote_signer_url"` // remote signer url (web3signer) in case of RemoteSigner signerType
SignerAddress string `json:"signer_address"` // address of signer
}

// AWSKMSSignerConfig - config of an AWS KMS backed signer. The private key never
// leaves KMS; the service only requests signatures over transaction hashes.
type AWSKMSSignerConfig struct {
// KeyID is the KMS key id, alias or ARN of an asymmetric ECC_SECG_P256K1 / SIGN_VERIFY key.
KeyID string `json:"key_id"`
// Region is the AWS region of the key. Optional; falls back to the ambient AWS config
// (AWS_REGION, shared config, instance/IRSA role) when empty, and startup fails if
// no region resolves from either.
Region string `json:"region"`
// SignerAddress is the expected Ethereum address of the key. Required: it is validated
// against the address derived from the KMS public key at startup so a misconfigured
// key id fails fast instead of signing from an unexpected account.
SignerAddress string `json:"signer_address"`
}
21 changes: 18 additions & 3 deletions rollup/internal/controller/relayer/l2_relayer.go
Original file line number Diff line number Diff line change
Expand Up @@ -1308,11 +1308,26 @@ func addrFromSignerConfig(config *config.SignerConfig) (common.Address, error) {
}
return crypto.PubkeyToAddress(privKey.PublicKey), nil
case sender.RemoteSignerType:
if config.RemoteSignerConfig.SignerAddress == "" {
return common.Address{}, fmt.Errorf("signer address is empty")
if config.RemoteSignerConfig == nil {
return common.Address{}, fmt.Errorf("remote_signer_config is missing")
}
return common.HexToAddress(config.RemoteSignerConfig.SignerAddress), nil
return parseSignerAddress(config.RemoteSignerConfig.SignerAddress)
case sender.AWSKMSSignerType:
if config.AWSKMSSignerConfig == nil {
return common.Address{}, fmt.Errorf("aws_kms_signer_config is missing")
}
return parseSignerAddress(config.AWSKMSSignerConfig.SignerAddress)
default:
return common.Address{}, fmt.Errorf("failed to determine signer address, unknown signer type: %v", config.SignerType)
}
}

// parseSignerAddress parses a signer address configured for a signer that holds
// its key outside the process. An empty string is not a valid hex address, so
// this covers both a missing and a malformed value.
func parseSignerAddress(addr string) (common.Address, error) {
if !common.IsHexAddress(addr) {
return common.Address{}, fmt.Errorf("signer address %q is not a valid hex address", addr)
}
return common.HexToAddress(addr), nil
}
100 changes: 100 additions & 0 deletions rollup/internal/controller/sender/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Transaction signers

Each rollup sender (`gas_oracle_sender`, `commit_sender`, `finalize_sender`) is
configured with its own `*_signer_config` block in the relayer config. The
`signer_type` field selects how transactions are signed:

| `signer_type` | Key location | Blob tx (EIP-4844) support |
| ------------- | ------------ | -------------------------- |
| `PrivateKey` | In process / mounted secret | yes |
| `RemoteSigner`| web3signer (`eth_signTransaction`) | no |
| `AWSKMS` | AWS KMS (never leaves KMS) | yes |

`commit_sender` submits batches as blob transactions, so it must use
`PrivateKey` or `AWSKMS`.

## AWSKMS

The `AWSKMS` signer keeps the private key inside AWS KMS. The service only sends
the 32-byte transaction signing hash to KMS and assembles the signature locally,
so all transaction types — including blob transactions — are supported.

```json
"commit_sender_signer_config": {
"signer_type": "AWSKMS",
"aws_kms_signer_config": {
"key_id": "arn:aws:kms:us-east-1:123456789012:key/abcd-...",
"region": "us-east-1",
"signer_address": "0x1C5A77d9FA7eF466951B2F01F724BCa3A5820b63"
}
}
```

- **`key_id`** — id, alias, or ARN of an asymmetric KMS key with key spec
`ECC_SECG_P256K1` and key usage `SIGN_VERIFY`.
- **`region`** — optional; falls back to the ambient AWS config
(`AWS_REGION`, shared config, instance/IRSA role) when empty. Startup fails if
no region resolves from either.
- **`signer_address`** — **required**. The expected Ethereum address of the key.
It is validated at startup against the address derived from the KMS public key,
so a misconfigured `key_id` fails fast instead of signing from an unexpected
account.

AWS credentials are resolved from the standard AWS SDK chain (IRSA, instance
role, or environment) — never put credentials in the config file.

### Recommended IAM setup

- Use a **separate KMS key per sender** so `kms:Sign` can be scoped and audited
(via CloudTrail) per workload.
- Grant each workload only `kms:Sign` and `kms:GetPublicKey` on its key.
- KMS protects the key material, not the spending policy: keep the hot-wallet
pattern (low balances, alerting, rotation). A compromised service can still
request signatures, so consider transaction value/rate guards as a follow-up.

### Provisioning the key

There are two ways to get a key into KMS. Whichever you use, the signer never
needs the raw private key — it derives the Ethereum address from the KMS public
key (`keccak256(pubkey)[12:]`) at startup, and you put that address in
`signer_address`. To get it, take the `PublicKey` returned by
`aws kms get-public-key` (base64 SPKI DER), whose last 65 bytes are the
uncompressed point `0x04 || X || Y`, and keccak256-hash the 64 bytes after the
`0x04` prefix; the address is the last 20 bytes of that hash. Alternatively, start
the relayer with a placeholder `signer_address`: startup fails with an error that
names the address derived from the KMS key, which you then put in the config.

**Fund the address** before the sender goes live. Once configured correctly, the
relayer logs `initialized AWS KMS signer` with the key id and address, so you can
confirm which account a workload signs from.

**Option A — generate in KMS (recommended).** The private key is created inside
the KMS HSM and is non-exportable: it provably never exists outside KMS.

```bash
aws kms create-key \
--key-spec ECC_SECG_P256K1 \
--key-usage SIGN_VERIFY \
--origin AWS_KMS \
--description "scroll commit_sender"
# optional friendlier reference:
aws kms create-alias --alias-name alias/scroll-commit-sender --target-key-id <key-id>
```

Use this for new senders — fund the freshly derived address.

**Option B — import an existing private key (`Origin=EXTERNAL`).** Use this only
when you must preserve an already-funded address. KMS supports importing key
material for asymmetric keys, but the key existed in plaintext outside KMS at
least once, which is exactly the exposure the KMS signer otherwise eliminates —
so prefer Option A unless address continuity is a hard requirement. Create the
key with `--origin EXTERNAL`, then `get-parameters-for-import` /
`import-key-material` to upload the wrapped secp256k1 private key.

**Rotation.** KMS does not auto-rotate asymmetric keys, and a key's address
cannot change in place. Rotating means creating a new key (Option A), pointing
`signer_address`/`key_id` at it, and sweeping the old balance to the new address
— the old KMS key signs that sweep itself while still enabled, so no key access
is needed; disable/schedule-delete it only after the sweep confirms. Because a
non-exportable key can't leak, rotation is primarily an IAM-compromise response
rather than routine hygiene.
Loading
Loading