diff --git a/build/lint.go b/build/lint.go index 68e7b8a..525350c 100644 --- a/build/lint.go +++ b/build/lint.go @@ -51,7 +51,7 @@ func lint() { } cmd = exec.Command(filepath.Join(goBin(), "golangci-lint")) - cmd.Args = append(cmd.Args, "run", "--config", "./build/.golangci.yml") + cmd.Args = append(cmd.Args, "run", "--config", "./build/.golangci.yml", "--timeout=10m") if *v { cmd.Args = append(cmd.Args, "-v") diff --git a/go.mod b/go.mod index 1babf8f..ef7ead3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,9 @@ module github.com/scroll-tech/paymaster go 1.23.0 require ( + github.com/aws/aws-sdk-go-v2 v1.36.4 + github.com/aws/aws-sdk-go-v2/config v1.29.16 + github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 github.com/bits-and-blooms/bitset v1.20.0 github.com/ethereum/go-ethereum v1.11.6 github.com/gin-contrib/cors v1.7.5 @@ -19,6 +22,17 @@ require ( ) require ( + github.com/aws/aws-sdk-go-v2/credentials v1.17.69 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 // indirect + github.com/aws/smithy-go v1.22.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect github.com/bytedance/sonic v1.13.2 // indirect diff --git a/go.sum b/go.sum index 54953b1..96fcacc 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,31 @@ +github.com/aws/aws-sdk-go-v2 v1.36.4 h1:GySzjhVvx0ERP6eyfAbAuAXLtAda5TEy19E5q5W8I9E= +github.com/aws/aws-sdk-go-v2 v1.36.4/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= +github.com/aws/aws-sdk-go-v2/config v1.29.16 h1:XkruGnXX1nEZ+Nyo9v84TzsX+nj86icbFAeust6uo8A= +github.com/aws/aws-sdk-go-v2/config v1.29.16/go.mod h1:uCW7PNjGwZ5cOGZ5jr8vCWrYkGIhPoTNV23Q/tpHKzg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69 h1:8B8ZQboRc3uaIKjshve/XlvJ570R7BKNy3gftSbS178= +github.com/aws/aws-sdk-go-v2/credentials v1.17.69/go.mod h1:gPME6I8grR1jCqBFEGthULiolzf/Sexq/Wy42ibKK9c= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31 h1:oQWSGexYasNpYp4epLGZxxjsDo8BMBh6iNWkTXQvkwk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.31/go.mod h1:nc332eGUU+djP3vrMI6blS0woaCfHTe3KiSQUVTMRq0= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35 h1:o1v1VFfPcDVlK3ll1L5xHsaQAFdNtZ5GXnNR7SwueC4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.35/go.mod h1:rZUQNYMNG+8uZxz9FOerQJ+FceCiodXvixpeRtdESrU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35 h1:R5b82ubO2NntENm3SAm0ADME+H630HomNJdgv+yZ3xw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.35/go.mod h1:FuA+nmgMRfkzVKYDNEqQadvEMxtxl9+RLT9ribCwEMs= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16 h1:/ldKrPPXTC421bTNWrUIpq3CxwHwRI/kpc+jPUTJocM= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.16/go.mod h1:5vkf/Ws0/wgIMJDQbjI4p2op86hNW6Hie5QtebrDgT8= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0 h1:2jKyib9msVrAVn+lngwlSplG13RpUZmzVte2yDao5nc= +github.com/aws/aws-sdk-go-v2/service/kms v1.41.0/go.mod h1:RyhzxkWGcfixlkieewzpO3D4P4fTMxhIDqDZWsh0u/4= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4 h1:EU58LP8ozQDVroOEyAfcq0cGc5R/FTZjVoYJ6tvby3w= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.4/go.mod h1:CrtOgCcysxMvrCoHnvNAD7PHWclmoFG78Q2xLK0KKcs= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2 h1:XB4z0hbQtpmBnb1FQYvKaCM7UsS6Y/u8jVBwIUGeCTk= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.2/go.mod h1:hwRpqkRxnQ58J9blRDrB4IanlXCpcKmsC83EhG77upg= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21 h1:nyLjs8sYJShFYj6aiyjCBI3EcLn1udWrQTjEF+SOXB0= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.21/go.mod h1:EhdxtZ+g84MSGrSrHzZiUm9PYiZkrADNja15wtRJSJo= +github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ= +github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= diff --git a/internal/config/config.go b/internal/config/config.go index cbdfc35..c2bb60d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,6 +18,7 @@ type Config struct { RateLimiterQPS int64 `json:"rate_limiter_qps"` ChainID int64 `json:"chain_id"` SignerPrivateKey string `json:"signer_private_key"` + AWSKMSKeyID string `json:"aws_kms_key_id"` USDTAddress common.Address `json:"usdt_address"` EthereumRPCURLs []string `json:"ethereum_rpc_urls"` DBConfig database.Config `json:"db_config"` diff --git a/internal/controller/paymaster.go b/internal/controller/paymaster.go index e64251f..5670cd6 100644 --- a/internal/controller/paymaster.go +++ b/internal/controller/paymaster.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "crypto/ecdsa" + "encoding/asn1" "encoding/hex" "encoding/json" "fmt" @@ -14,9 +15,14 @@ import ( "strings" "time" + "github.com/aws/aws-sdk-go-v2/aws" + awsConfig "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/service/kms" + awsTypes "github.com/aws/aws-sdk-go-v2/service/kms/types" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/ethereum/go-ethereum/log" "github.com/gin-gonic/gin" "gorm.io/gorm" @@ -28,12 +34,21 @@ import ( const entryPointV7Address = "0x0000000071727De22E5E9d8BAf0edAc6f37da032" -var emptyAddr = common.Address{} +var ( + emptyAddr = common.Address{} + secp256k1N = crypto.S256().Params().N + secp256k1HalfN = new(big.Int).Div(secp256k1N, big.NewInt(2)) +) // PaymasterController the controller of paymaster type PaymasterController struct { - cfg *config.Config - signerKey *ecdsa.PrivateKey + cfg *config.Config + signerKey *ecdsa.PrivateKey + signerAddress common.Address + + awsKMSKeyID string + kmsClient *kms.Client + policyOrm *orm.Policy userOperationOrm *orm.UserOperation } @@ -44,24 +59,230 @@ func NewPaymasterController(cfg *config.Config, db *gorm.DB) *PaymasterControlle log.Crit("API key is required") } - privateKeyBytes, err := hex.DecodeString(cfg.SignerPrivateKey) + pc := &PaymasterController{ + cfg: cfg, + policyOrm: orm.NewPolicy(db), + userOperationOrm: orm.NewUserOperation(db), + } + + // Check mutually exclusive signer configuration + hasPrivateKey := cfg.SignerPrivateKey != "" + hasKMSKey := cfg.AWSKMSKeyID != "" + + if !hasPrivateKey && !hasKMSKey { + log.Crit("Either signer private key or AWS KMS key ID must be provided") + } + + if hasPrivateKey && hasKMSKey { + log.Crit("Cannot specify both signer private key and AWS KMS key ID") + } + + if hasPrivateKey { + // Initialize with private key + if err := pc.initializePrivateKeySigner(cfg.SignerPrivateKey); err != nil { + log.Crit("Failed to initialize private key signer", "error", err) + } + log.Info("Paymaster signer initialized with private key", "address", pc.signerAddress.Hex()) + } else { + // Initialize with AWS KMS + if err := pc.initializeKMSSigner(cfg.AWSKMSKeyID); err != nil { + log.Crit("Failed to initialize KMS signer", "error", err) + } + log.Info("Paymaster signer initialized with AWS KMS", "address", pc.signerAddress.Hex(), "key_id", cfg.AWSKMSKeyID) + } + + return pc +} + +// initializePrivateKeySigner initializes the signer with a private key +func (pc *PaymasterController) initializePrivateKeySigner(privateKeyHex string) error { + privateKeyBytes, err := hex.DecodeString(privateKeyHex) if err != nil { - log.Crit("Failed to decode private key", "error", err) + return fmt.Errorf("failed to decode private key: %w", err) } signerKey, err := crypto.ToECDSA(privateKeyBytes) if err != nil { - log.Crit("Failed to create ECDSA key from private key", "error", err) + return fmt.Errorf("failed to create ECDSA key from private key: %w", err) } - log.Info("Paymaster signer initialized", "address", crypto.PubkeyToAddress(signerKey.PublicKey).Hex()) + pc.signerKey = signerKey + pc.signerAddress = crypto.PubkeyToAddress(signerKey.PublicKey) + return nil +} - return &PaymasterController{ - cfg: cfg, - signerKey: signerKey, - policyOrm: orm.NewPolicy(db), - userOperationOrm: orm.NewUserOperation(db), +// initializeKMSSigner initializes the signer with AWS KMS +func (pc *PaymasterController) initializeKMSSigner(keyID string) error { + // Load AWS configuration + cfg, err := awsConfig.LoadDefaultConfig(context.Background()) + if err != nil { + return fmt.Errorf("failed to load AWS config: %w", err) + } + + // Create KMS client + pc.kmsClient = kms.NewFromConfig(cfg) + pc.awsKMSKeyID = keyID + + // Get public key to derive address + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + pubkey, err := pc.getPubKeyFromKMS(ctx, keyID) + if err != nil { + return fmt.Errorf("failed to get public key from KMS: %w", err) } + + pc.signerAddress = crypto.PubkeyToAddress(*pubkey) + return nil +} + +// getPubKeyFromKMS gets public key from KMS +func (pc *PaymasterController) getPubKeyFromKMS(ctx context.Context, keyID string) (*ecdsa.PublicKey, error) { + getPubKeyOutput, err := pc.kmsClient.GetPublicKey(ctx, &kms.GetPublicKeyInput{ + KeyId: aws.String(keyID), + }) + if err != nil { + return nil, fmt.Errorf("cannot get public key from KMS for KeyId=%s: %w", keyID, err) + } + + // Parse ASN.1 DER encoded public key + var asn1pubk struct { + EcPublicKeyInfo struct { + Algorithm asn1.ObjectIdentifier + Parameters asn1.ObjectIdentifier + } + PublicKey asn1.BitString + } + + _, err = asn1.Unmarshal(getPubKeyOutput.PublicKey, &asn1pubk) + if err != nil { + return nil, fmt.Errorf("cannot parse ASN.1 public key for KeyId=%s: %w", keyID, err) + } + + pubkey, err := crypto.UnmarshalPubkey(asn1pubk.PublicKey.Bytes) + if err != nil { + return nil, fmt.Errorf("cannot construct secp256k1 public key from key bytes: %w", err) + } + + return pubkey, nil +} + +// signHashWithKMS signs a hash using AWS KMS +func (pc *PaymasterController) signHashWithKMS(hash []byte) ([]byte, error) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Get the expected public key bytes for signature verification + pubkey, err := pc.getPubKeyFromKMS(ctx, pc.awsKMSKeyID) + if err != nil { + return nil, fmt.Errorf("failed to get public key: %w", err) + } + pubKeyBytes := secp256k1.S256().Marshal(pubkey.X, pubkey.Y) + + // Get R and S values from KMS signature + rBytes, sBytes, err := pc.getSignatureFromKMS(ctx, hash) + if err != nil { + return nil, fmt.Errorf("failed to get signature from KMS: %w", err) + } + + // Adjust S value according to Ethereum standard + sBigInt := new(big.Int).SetBytes(sBytes) + if sBigInt.Cmp(secp256k1HalfN) > 0 { + sBytes = new(big.Int).Sub(secp256k1N, sBigInt).Bytes() + } + + // Get Ethereum signature with correct recovery ID + signature, err := pc.getEthereumSignature(pubKeyBytes, hash, rBytes, sBytes) + if err != nil { + return nil, fmt.Errorf("failed to get Ethereum signature: %w", err) + } + + return signature, nil +} + +// getSignatureFromKMS gets R and S values from KMS +func (pc *PaymasterController) getSignatureFromKMS(ctx context.Context, hash []byte) ([]byte, []byte, error) { + signInput := &kms.SignInput{ + KeyId: aws.String(pc.awsKMSKeyID), + SigningAlgorithm: awsTypes.SigningAlgorithmSpecEcdsaSha256, + MessageType: awsTypes.MessageTypeDigest, + Message: hash, + } + + signOutput, err := pc.kmsClient.Sign(ctx, signInput) + if err != nil { + return nil, nil, fmt.Errorf("KMS sign failed: %w", err) + } + + // Parse ASN.1 signature + var sigAsn1 struct { + R asn1.RawValue + S asn1.RawValue + } + + _, err = asn1.Unmarshal(signOutput.Signature, &sigAsn1) + if err != nil { + return nil, nil, fmt.Errorf("failed to unmarshal ASN.1 signature: %w", err) + } + + return sigAsn1.R.Bytes, sigAsn1.S.Bytes, nil +} + +// getEthereumSignature creates Ethereum signature with recovery ID +func (pc *PaymasterController) getEthereumSignature(expectedPublicKeyBytes []byte, hash []byte, r []byte, s []byte) ([]byte, error) { + rsSignature := append(pc.adjustSignatureLength(r), pc.adjustSignatureLength(s)...) + + // Try recovery ID 0 + signature := append(rsSignature, []byte{0}...) + recoveredPublicKeyBytes, err := crypto.Ecrecover(hash, signature) + if err != nil { + return nil, fmt.Errorf("recovery with ID 0 failed: %w", err) + } + + if hex.EncodeToString(recoveredPublicKeyBytes) == hex.EncodeToString(expectedPublicKeyBytes) { + return signature, nil + } + + // Try recovery ID 1 + signature = append(rsSignature, []byte{1}...) + recoveredPublicKeyBytes, err = crypto.Ecrecover(hash, signature) + if err != nil { + return nil, fmt.Errorf("recovery with ID 1 failed: %w", err) + } + + if hex.EncodeToString(recoveredPublicKeyBytes) == hex.EncodeToString(expectedPublicKeyBytes) { + return signature, nil + } + + return nil, fmt.Errorf("cannot reconstruct public key from signature") +} + +// adjustSignatureLength adjusts signature component length +func (pc *PaymasterController) adjustSignatureLength(buffer []byte) []byte { + buffer = bytes.TrimLeft(buffer, "\x00") + for len(buffer) < 32 { + zeroBuf := []byte{0} + buffer = append(zeroBuf, buffer...) + } + return buffer +} + +// signHashWithPrivateKey signs a hash using the private key +func (pc *PaymasterController) signHashWithPrivateKey(hash []byte) ([]byte, error) { + signature, err := crypto.Sign(hash, pc.signerKey) + if err != nil { + return nil, fmt.Errorf("failed to sign with private key: %w", err) + } + + if len(signature) != 65 { + return nil, fmt.Errorf("invalid signature length: got %d, expected 65", len(signature)) + } + + if signature[64] == 0 || signature[64] == 1 { + signature[64] += 27 + } + + return signature, nil } // handlePaymasterMethod handles JSON-RPC requests for paymaster methods. @@ -660,6 +881,12 @@ func (pc *PaymasterController) getPaymasterDataHash(userOp *types.PaymasterUserO return crypto.Keccak256(buffer) } +func packGasLimits(high, low *big.Int) [32]byte { + combined := new(big.Int).Or(new(big.Int).Lsh(high, 128), low) + return [32]byte(common.LeftPadBytes(combined.Bytes(), 32)) +} + +// signHash signs a hash using either private key or AWS KMS func (pc *PaymasterController) signHash(hash []byte) ([]byte, error) { if len(hash) != 32 { return nil, fmt.Errorf("hash must be 32 bytes, got %d", len(hash)) @@ -669,25 +896,12 @@ func (pc *PaymasterController) signHash(hash []byte) ([]byte, error) { fullMessage := append([]byte(prefix), hash...) ethSignedHash := crypto.Keccak256(fullMessage) - signature, err := crypto.Sign(ethSignedHash, pc.signerKey) - if err != nil { - return nil, err - } - - if len(signature) != 65 { - return nil, fmt.Errorf("invalid signature length: got %d, expected 65", len(signature)) + if pc.signerKey != nil { + // Sign with private key + return pc.signHashWithPrivateKey(ethSignedHash) } - if signature[64] == 0 || signature[64] == 1 { - signature[64] += 27 - } - - return signature, nil -} - -func packGasLimits(high, low *big.Int) [32]byte { - combined := new(big.Int).Or(new(big.Int).Lsh(high, 128), low) - return [32]byte(common.LeftPadBytes(combined.Bytes(), 32)) + return pc.signHashWithKMS(ethSignedHash) } // calculatePaymasterGasLimits calculates the gas limits for paymaster operations based on the token address. diff --git a/internal/utils/version.go b/internal/utils/version.go index 36bbe02..212eb1e 100644 --- a/internal/utils/version.go +++ b/internal/utils/version.go @@ -6,7 +6,7 @@ import ( "runtime/debug" ) -var tag = "v0.0.2" +var tag = "v0.0.3" var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok {