diff --git a/go.mod b/go.mod index 2ce7739464..eea54e7582 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/shopspring/decimal v1.4.0 github.com/smartcontractkit/chain-selectors v1.0.67 github.com/smartcontractkit/chainlink-common v0.9.6-0.20251001150007-98903c79c124 + github.com/smartcontractkit/chainlink-common/keystore v0.0.0-00010101000000-000000000000 github.com/smartcontractkit/chainlink-evm/gethwrappers v0.0.0-20250827130336-5922343458be github.com/smartcontractkit/chainlink-framework/capabilities v0.0.0-20250818175541-3389ac08a563 github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250717121125-2350c82883e2 @@ -44,9 +45,9 @@ require ( go.opentelemetry.io/otel/metric v1.37.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.40.0 + golang.org/x/crypto v0.42.0 golang.org/x/exp v0.0.0-20250711185948-6ae5c78190dc - google.golang.org/protobuf v1.36.7 + google.golang.org/protobuf v1.36.9 gopkg.in/guregu/null.v4 v4.0.0 ) @@ -190,13 +191,13 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.6.0 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.42.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools v0.36.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect @@ -207,3 +208,5 @@ require ( ) replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20250528121202-292529af39df + +replace github.com/smartcontractkit/chainlink-common/keystore => ../chainlink-common/keystore diff --git a/go.sum b/go.sum index 7c4f8d02dc..4d0f5bb541 100644 --- a/go.sum +++ b/go.sum @@ -814,8 +814,8 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= -golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -856,8 +856,8 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -909,8 +909,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -936,8 +936,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1010,8 +1010,8 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1036,8 +1036,8 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1104,8 +1104,8 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1226,8 +1226,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= -google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/pkg/keysv2/evm_tx.go b/pkg/keysv2/evm_tx.go new file mode 100644 index 0000000000..ea67585148 --- /dev/null +++ b/pkg/keysv2/evm_tx.go @@ -0,0 +1,166 @@ +package keysv2 + +import ( + "context" + "fmt" + "strings" + + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" + "github.com/ethereum/go-ethereum/common" + gethtypes "github.com/ethereum/go-ethereum/core/types" + gethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/smartcontractkit/chainlink-common/keystore" +) + +const ( + EVMPrefix = "evm" + TxKeystorePrefix = "tx" +) + +// JoinKeySegments joins path-like key name segments using "/" and avoids double slashes. +// Empty segments are skipped so JoinKeySegments("EVM", "TX", "my-key") => "EVM/TX/my-key". +func JoinKeySegments(segments ...string) string { + cleaned := make([]string, 0, len(segments)) + for _, s := range segments { + s = strings.Trim(s, "/") + if s == "" { + continue + } + cleaned = append(cleaned, s) + } + return strings.Join(cleaned, "/") +} + +func GetTxKeystoreName(localName string) string { + return JoinKeySegments(EVMPrefix, TxKeystorePrefix, localName) +} + +type TxKey struct { + ks keystore.Keystore + // Fully qualified name in keystore. Use for administration. + fullName string + name string + addr common.Address +} + +type SignTxRequest struct { + ChainID *big.Int + Tx *gethtypes.Transaction +} + +type SignTxResponse struct { + Tx *gethtypes.Transaction +} + +func (k *TxKey) Name() string { + return k.name +} + +func (k *TxKey) FullName() string { + return k.fullName +} + +func (k *TxKey) Address() common.Address { + return k.addr +} + +func (k *TxKey) SignTx(ctx context.Context, req SignTxRequest) (SignTxResponse, error) { + if req.ChainID == nil { + return SignTxResponse{}, fmt.Errorf("chainID is nil") + } + signer := gethtypes.LatestSignerForChainID(req.ChainID) + h := signer.Hash(req.Tx) + signReq := keystore.SignRequest{ + KeyName: k.FullName(), + Data: h[:], + } + signResp, err := k.ks.Sign(ctx, signReq) + if err != nil { + return SignTxResponse{}, err + } + req.Tx, err = req.Tx.WithSignature(signer, signResp.Signature) + if err != nil { + return SignTxResponse{}, err + } + return SignTxResponse{Tx: req.Tx}, nil +} + +func (k *TxKey) GetTransactOpts(ctx context.Context, chainID *big.Int) (*bind.TransactOpts, error) { + if chainID == nil { + return nil, fmt.Errorf("chainID is nil") + } + return &bind.TransactOpts{ + From: k.addr, + Signer: func(address common.Address, tx *gethtypes.Transaction) (*gethtypes.Transaction, error) { + if k.Address() != address { + return nil, bind.ErrNotAuthorized + } + resp, err := k.SignTx(ctx, SignTxRequest{ + ChainID: chainID, + Tx: tx, + }) + if err != nil { + return nil, err + } + return resp.Tx, nil + }, + }, nil +} + +func CreateTxKey(ks keystore.Keystore, localName string) (*TxKey, error) { + createReq := keystore.CreateKeysRequest{ + Keys: []keystore.CreateKeyRequest{ + { + KeyName: GetTxKeystoreName(localName), + KeyType: keystore.ECDSA_S256, + }, + }, + } + resp, err := ks.CreateKeys(context.Background(), createReq) + if err != nil { + return nil, err + } + if len(resp.Keys) == 0 { + return nil, fmt.Errorf("no keys created") + } + publicKey, err := gethcrypto.UnmarshalPubkey(resp.Keys[0].KeyInfo.PublicKey) + if err != nil { + return nil, err + } + addr := gethcrypto.PubkeyToAddress(*publicKey) + return &TxKey{ + ks: ks, + name: localName, + fullName: GetTxKeystoreName(localName), + addr: addr, + }, nil +} + +func GetTxKeys(ctx context.Context, ks keystore.Keystore, names []string) ([]*TxKey, error) { + var fullNames []string + for _, name := range names { + fullNames = append(fullNames, GetTxKeystoreName(name)) + } + resp, err := ks.GetKeys(ctx, keystore.GetKeysRequest{KeyNames: fullNames}) + if err != nil { + return nil, err + } + + var keys []*TxKey + for _, key := range resp.Keys { + publicKey, err := gethcrypto.UnmarshalPubkey(key.KeyInfo.PublicKey) + if err != nil { + return nil, err + } + addr := gethcrypto.PubkeyToAddress(*publicKey) + keys = append(keys, &TxKey{ + ks: ks, + fullName: key.KeyInfo.Name, + name: key.KeyInfo.Name, + addr: addr, + }) + } + return keys, nil +} diff --git a/pkg/keysv2/evm_tx_test.go b/pkg/keysv2/evm_tx_test.go new file mode 100644 index 0000000000..bfc4888d8e --- /dev/null +++ b/pkg/keysv2/evm_tx_test.go @@ -0,0 +1,75 @@ +package keysv2_test + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient/simulated" + commonks "github.com/smartcontractkit/chainlink-common/keystore" + ksstorage "github.com/smartcontractkit/chainlink-common/keystore/storage" + evmks "github.com/smartcontractkit/chainlink-evm/pkg/keysv2" + "github.com/stretchr/testify/require" +) + +func TestTxKey(t *testing.T) { + storage := ksstorage.NewMemoryStorage() + ctx := t.Context() + ks, err := commonks.LoadKeystore(ctx, storage, commonks.EncryptionParams{ + Password: "test-password", + ScryptParams: commonks.FastScryptParams, + }) + require.NoError(t, err) + testKey, err := evmks.CreateTxKey(ks, "test-tx-key") + require.NoError(t, err) + testKey2, err := evmks.CreateTxKey(ks, "test-tx-key-2") + require.NoError(t, err) + + backend := simulated.NewBackend(types.GenesisAlloc{ + testKey.Address(): { + Balance: big.NewInt(0).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH + }, + }, simulated.WithBlockGasLimit(10e6)) + defer backend.Close() + testTransaction := types.NewTransaction( + 0, // Nonce + testKey2.Address(), // To other key + big.NewInt(1), // Value + 21000, // Gas Limit + big.NewInt(20000000000), // Gas Price + nil) + resp, err := testKey.SignTx(ctx, evmks.SignTxRequest{ + ChainID: big.NewInt(1337), // Use a test chain ID + Tx: testTransaction, + }) + require.NoError(t, err) + require.NotNil(t, resp.Tx) + require.NoError(t, backend.Client().SendTransaction(ctx, resp.Tx)) + backend.Commit() + receipt, err := backend.Client().TransactionReceipt(ctx, resp.Tx.Hash()) + require.NoError(t, err) + require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) + + endBalance, err := backend.Client().BalanceAt(ctx, testKey2.Address(), nil) + require.NoError(t, err) + require.Equal(t, endBalance, big.NewInt(1)) + + // Admin operation will invalidate the keys. + _, err = ks.DeleteKeys(ctx, commonks.DeleteKeysRequest{ + KeyNames: []string{testKey.FullName(), testKey2.FullName()}, + }) + require.NoError(t, err) + + // Empty names will return all keys. + keys, err := evmks.GetTxKeys(ctx, ks, []string{}) + require.NoError(t, err) + require.Equal(t, len(keys), 0) + + // Signing will now error. + _, err = testKey.SignTx(ctx, evmks.SignTxRequest{ + ChainID: big.NewInt(1337), // Use a test chain ID + Tx: testTransaction, + }) + require.Error(t, err) + require.ErrorIs(t, err, commonks.ErrKeyNotFound) +} diff --git a/pkg/keysv2/ocr2_keyring_test.go b/pkg/keysv2/ocr2_keyring_test.go new file mode 100644 index 0000000000..c5815be406 --- /dev/null +++ b/pkg/keysv2/ocr2_keyring_test.go @@ -0,0 +1,316 @@ +package keysv2_test + +import ( + "context" + "crypto/ed25519" + "fmt" + "testing" + "time" + + "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi/bind/v2" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + gethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient/simulated" + commonks "github.com/smartcontractkit/chainlink-common/keystore" + ksstorage "github.com/smartcontractkit/chainlink-common/keystore/storage" + logger "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-evm/pkg/keysv2" + evmks "github.com/smartcontractkit/chainlink-evm/pkg/keysv2" + "github.com/smartcontractkit/libocr/commontypes" + ocr2agg "github.com/smartcontractkit/libocr/gethwrappers2/ocr2aggregator" + "github.com/smartcontractkit/libocr/offchainreporting2/confighelper" + median "github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median" + "github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median/evmreportcodec" + libocr "github.com/smartcontractkit/libocr/offchainreporting2plus" + "github.com/smartcontractkit/libocr/offchainreporting2plus/chains/evmutil" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + ragep2ptypes "github.com/smartcontractkit/libocr/ragep2p/types" + "github.com/stretchr/testify/require" +) + +var _ ocrtypes.ContractConfigTracker = (*helper)(nil) +var _ ocrtypes.ContractTransmitter = (*helper)(nil) +var _ median.DataSource = (*helper)(nil) +var _ ocrtypes.Database = (*helper)(nil) + +type helper struct { + backend *simulated.Backend + lggr logger.Logger + ocr2agg *ocr2agg.OCR2Aggregator + opts *bind.TransactOpts +} + +func (t *helper) Observe(ctx context.Context, repts ocrtypes.ReportTimestamp) (*big.Int, error) { + t.lggr.Info("Observe", "repts", repts) + return big.NewInt(1), nil +} + +func (t *helper) Transmit( + ctx context.Context, + reportContext ocrtypes.ReportContext, + report ocrtypes.Report, + signatures []ocrtypes.AttributedOnchainSignature, +) error { + t.lggr.Info("Transmit", "report", report) + var rs [][32]byte + var ss [][32]byte + var vs [32]byte + for i, as := range signatures { + r, s, v, err := evmutil.SplitSignature(as.Signature) + if err != nil { + panic("eventTransmit(ev): error in SplitSignature") + } + rs = append(rs, r) + ss = append(ss, s) + vs[i] = v + } + t.ocr2agg.Transmit(t.opts, + evmutil.RawReportContext(reportContext), + report, rs, ss, vs) + t.backend.Commit() + return nil +} + +func (t *helper) LatestRoundRequested(ctx context.Context, _ time.Duration) (ocrtypes.ConfigDigest, uint32, uint8, error) { + t.lggr.Info("LatestRoundRequested") + res, err := t.ocr2agg.LatestConfigDigestAndEpoch(&bind.CallOpts{Context: ctx}) + return ocrtypes.ConfigDigest(res.ConfigDigest), res.Epoch, 0, err +} + +func (t *helper) LatestConfigDigestAndEpoch(ctx context.Context) (ocrtypes.ConfigDigest, uint32, error) { + t.lggr.Info("LatestConfigDigestAndEpoch") + res, err := t.ocr2agg.LatestConfigDigestAndEpoch(&bind.CallOpts{Context: ctx}) + return ocrtypes.ConfigDigest(res.ConfigDigest), res.Epoch, err +} + +func (t *helper) LatestTransmissionDetails(ctx context.Context) (ocrtypes.ConfigDigest, uint32, uint8, *big.Int, time.Time, error) { + t.lggr.Info("LatestTransmissionDetails") + res, err := t.ocr2agg.LatestTransmissionDetails(&bind.CallOpts{Context: ctx}) + return ocrtypes.ConfigDigest(res.ConfigDigest), res.Epoch, 0, nil, time.Time{}, err +} + +func (t *helper) FromAccount(ctx context.Context) (ocrtypes.Account, error) { + t.lggr.Info("FromAccount") + return ocrtypes.Account(t.opts.From.String()), nil +} + +func (t *helper) LatestBlockHeight(ctx context.Context) (uint64, error) { + header, err := t.backend.Client().HeaderByNumber(ctx, nil) + if err != nil { + return 0, err + } + return header.Number.Uint64(), err +} + +func (t *helper) LatestConfig(ctx context.Context, changedInBlock uint64) (ocrtypes.ContractConfig, error) { + t.lggr.Info("LatestConfig", "changedInBlock", changedInBlock, "ocr2agg", t.ocr2agg) + c, err := t.ocr2agg.FilterConfigSet(&bind.FilterOpts{Context: ctx, Start: uint64(changedInBlock)}) + if err != nil { + return ocrtypes.ContractConfig{}, err + } + ok := c.Next() + if !ok { + return ocrtypes.ContractConfig{}, fmt.Errorf("no config set event found") + } + t.lggr.Infof("ConfigSet %x\n", c.Event.ConfigDigest[:]) + return evmutil.ContractConfigFromConfigSetEvent(*c.Event), nil +} + +func (t *helper) LatestConfigDetails(ctx context.Context) (uint64, ocrtypes.ConfigDigest, error) { + c, err := t.ocr2agg.LatestConfigDetails(&bind.CallOpts{Context: ctx}) + if err != nil { + return 0, ocrtypes.ConfigDigest{}, err + } + t.lggr.Info("LatestConfigDetails", "c", c) + return uint64(c.BlockNumber), ocrtypes.ConfigDigest(c.ConfigDigest), nil +} + +func (t *helper) Notify() <-chan struct{} { + return nil +} + +func (t *helper) ReadState(ctx context.Context, configDigest ocrtypes.ConfigDigest) (*ocrtypes.PersistentState, error) { + return nil, nil +} + +func (t *helper) WriteState(ctx context.Context, configDigest ocrtypes.ConfigDigest, state ocrtypes.PersistentState) error { + return nil +} + +func (t *helper) StorePendingTransmission(ctx context.Context, reportTimestamp ocrtypes.ReportTimestamp, pendingTransmission ocrtypes.PendingTransmission) error { + return nil +} + +func (t *helper) DeletePendingTransmission(ctx context.Context, reportTimestamp ocrtypes.ReportTimestamp) error { + return nil +} + +func (t *helper) DeletePendingTransmissionsOlderThan(ctx context.Context, time time.Time) error { + return nil +} + +func (t *helper) PendingTransmissionsWithConfigDigest(ctx context.Context, configDigest ocrtypes.ConfigDigest) (map[ocrtypes.ReportTimestamp]ocrtypes.PendingTransmission, error) { + return nil, nil +} + +func (t *helper) ReadConfig(ctx context.Context) (*ocrtypes.ContractConfig, error) { + return nil, nil +} + +func (t *helper) WriteConfig(ctx context.Context, config ocrtypes.ContractConfig) error { + return nil +} + +func (t *helper) NewEndpoint(configDigest ocrtypes.ConfigDigest, peerIDs []string, + v2bootstrappers []commontypes.BootstrapperLocator, f int, limits ocrtypes.BinaryNetworkEndpointLimits) (commontypes.BinaryNetworkEndpoint, error) { + t.lggr.Info("NewEndpoint", "configDigest", configDigest, "peerIDs", peerIDs, "v2bootstrappers", v2bootstrappers, "f", f, "limits", limits) + return nil, nil +} + +func (t *helper) PeerID() string { + t.lggr.Info("PeerID") + return "" +} + +// TestOCR2Keyring_Integration tests the OCR2 keyrings integration +// with libocr to ensure that the keyrings can actually be used +// to sign and verify reports. +func TestOCR2Keyring_Integration(t *testing.T) { + lggr := logger.Test(t) + storage := ksstorage.NewMemoryStorage() + ctx := t.Context() + ks, err := commonks.LoadKeystore(ctx, storage, commonks.EncryptionParams{ + Password: "test-password", + ScryptParams: commonks.FastScryptParams, + }) + require.NoError(t, err) + ownerKey, err := evmks.CreateTxKey(ks, "test-tx-key") + require.NoError(t, err) + + var oracles []confighelper.OracleIdentityExtra + var offchainKeyrings []ocrtypes.OffchainKeyring + var onchainKeyrings []ocrtypes.OnchainKeyring + for i := 0; i < 4; i++ { + onchainKeyring, err := keysv2.CreateOCR2OnchainKeyring(context.Background(), ks, fmt.Sprintf("test-onchain-keyring-%d", i)) + require.NoError(t, err) + offchainKeyring, err := keysv2.CreateOCR2OffchainKeyring(context.Background(), ks, fmt.Sprintf("test-offchain-keyring-%d", i)) + require.NoError(t, err) + keys, err := ks.CreateKeys(context.Background(), commonks.CreateKeysRequest{ + Keys: []commonks.CreateKeyRequest{ + {KeyName: fmt.Sprintf("test-peer-id-%d", i), KeyType: commonks.Ed25519}, + {KeyName: fmt.Sprintf("test-transmit-key-%d", i), KeyType: commonks.ECDSA_S256}, + }, + }) + require.NoError(t, err) + require.Equal(t, 2, len(keys.Keys)) + peerID, err := ragep2ptypes.PeerIDFromPublicKey(ed25519.PublicKey(keys.Keys[0].KeyInfo.PublicKey)) + require.NoError(t, err) + transmitKey, err := gethcrypto.UnmarshalPubkey(keys.Keys[1].KeyInfo.PublicKey) + require.NoError(t, err) + transmitAccount := gethcrypto.PubkeyToAddress(*transmitKey) + require.NoError(t, err) + oracles = append(oracles, confighelper.OracleIdentityExtra{ + OracleIdentity: confighelper.OracleIdentity{ + OnchainPublicKey: onchainKeyring.PublicKey(), + OffchainPublicKey: offchainKeyring.OffchainPublicKey(), + PeerID: peerID.String(), + TransmitAccount: ocrtypes.Account(transmitAccount.String()), + }, + ConfigEncryptionPublicKey: offchainKeyring.ConfigEncryptionPublicKey(), + }) + offchainKeyrings = append(offchainKeyrings, offchainKeyring) + onchainKeyrings = append(onchainKeyrings, onchainKeyring) + } + backend := simulated.NewBackend(types.GenesisAlloc{ + ownerKey.Address(): { + Balance: big.NewInt(0).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH + }, + common.HexToAddress(string(oracles[0].OracleIdentity.TransmitAccount)): { + Balance: big.NewInt(0).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH + }, + common.HexToAddress(string(oracles[1].OracleIdentity.TransmitAccount)): { + Balance: big.NewInt(0).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH + }, + common.HexToAddress(string(oracles[2].OracleIdentity.TransmitAccount)): { + Balance: big.NewInt(0).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH + }, + common.HexToAddress(string(oracles[3].OracleIdentity.TransmitAccount)): { + Balance: big.NewInt(0).Mul(big.NewInt(10), big.NewInt(1e18)), // 10 ETH + }, + }, simulated.WithBlockGasLimit(10e6)) + defer backend.Close() + + opts, err := ownerKey.GetTransactOpts(ctx, big.NewInt(1337)) + require.NoError(t, err) + aggAddress, tx, agg, err := ocr2agg.DeployOCR2Aggregator(opts, backend.Client(), common.HexToAddress("0x0"), big.NewInt(1), big.NewInt(10), common.HexToAddress("0x0"), common.HexToAddress("0x0"), 18, "Test") + require.NoError(t, err) + backend.Commit() + receipt, err := backend.Client().TransactionReceipt(ctx, tx.Hash()) + require.NoError(t, err) + require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) + + signers, transmitters, f, onchainConfig, offchainConfigVersion, offchainConfig, err := confighelper.ContractSetConfigArgsForEthereumIntegrationTest( + oracles, 1, 1000000) + require.NoError(t, err) + onchainConfig, err = median.StandardOnchainConfigCodec{}.Encode(ctx, median.OnchainConfig{ + Min: big.NewInt(1), + Max: big.NewInt(10), + }) + require.NoError(t, err) + + tx, err = agg.SetConfig(opts, signers, transmitters, f, onchainConfig, offchainConfigVersion, offchainConfig) + require.NoError(t, err) + backend.Commit() + receipt, err = backend.Client().TransactionReceipt(ctx, tx.Hash()) + require.NoError(t, err) + require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status) + + helper := helper{backend: backend, lggr: lggr, ocr2agg: agg, opts: opts} + for i := range oracles { + oracle, err := libocr.NewOracle(libocr.OCR2OracleArgs{ + BinaryNetworkEndpointFactory: &helper, + ReportingPluginFactory: &median.NumericalMedianFactory{ + ContractTransmitter: &helper, + DataSource: &helper, + JuelsPerFeeCoinDataSource: &helper, + GasPriceSubunitsDataSource: &helper, + IncludeGasPriceSubunitsInObservation: false, + Logger: logger.NewOCRWrapper(lggr, true, func(string) {}), + OnchainConfigCodec: median.StandardOnchainConfigCodec{}, + ReportCodec: evmreportcodec.ReportCodec{}, + DeviationFunc: median.DefaultDeviationFunc, + }, + ContractConfigTracker: &helper, + ContractTransmitter: &helper, + Database: &helper, + LocalConfig: ocrtypes.LocalConfig{ + BlockchainTimeout: 10 * time.Second, + ContractConfigConfirmations: 1, + ContractConfigTrackerPollInterval: 10 * time.Second, + ContractConfigLoadTimeout: 10 * time.Second, + ContractTransmitterTransmitTimeout: 10 * time.Second, + DatabaseTimeout: 10 * time.Second, + DevelopmentMode: ocrtypes.EnableDangerousDevelopmentMode, + EnableTransmissionTelemetry: false, + MinOCR2MaxDurationQuery: 10 * time.Second, + SkipContractConfigConfirmations: false, + }, + Logger: logger.NewOCRWrapper(lggr, true, func(string) {}), + MonitoringEndpoint: nil, + MetricsRegisterer: nil, + OffchainConfigDigester: evmutil.EVMOffchainConfigDigester{ + ChainID: uint64(1337), + ContractAddress: aggAddress, + }, + OffchainKeyring: offchainKeyrings[i], + OnchainKeyring: onchainKeyrings[i], + }) + require.NoError(t, err) + err = oracle.Start() + defer oracle.Close() + } + time.Sleep(10 * time.Second) +} diff --git a/pkg/keysv2/ocr2_offchain_keyring.go b/pkg/keysv2/ocr2_offchain_keyring.go new file mode 100644 index 0000000000..8a9a0df97a --- /dev/null +++ b/pkg/keysv2/ocr2_offchain_keyring.go @@ -0,0 +1,157 @@ +package keysv2 + +import ( + "context" + "fmt" + "strings" + + "github.com/smartcontractkit/chainlink-common/keystore" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" + "golang.org/x/crypto/curve25519" +) + +const ( + OCR2OffchainSigningPrefix = "ocr2_offchain_signing" + OCR2OffchainEncryptionPrefix = "ocr2_offchain_encryption" +) + +func GetOCR2OffchainSigningKeystoreName(localName string) string { + return JoinKeySegments(OCR2OffchainSigningPrefix, localName) +} + +func GetOCR2OffchainEncryptionKeystoreName(localName string) string { + return JoinKeySegments(OCR2OffchainEncryptionPrefix, localName) +} + +func IsOCR2OffchainSigningKey(name string) bool { + return strings.HasPrefix(name, JoinKeySegments(EVMPrefix, OCR2OffchainSigningPrefix, "")) +} + +func IsOCR2OffchainEncryptionKey(name string) bool { + return strings.HasPrefix(name, JoinKeySegments(EVMPrefix, OCR2OffchainEncryptionPrefix, "")) +} + +type OCR2OffchainKeyringCreateRequest struct { + LocalName string +} + +type OCR2OffchainKeyringCreateResponse struct { + Keyring ocrtypes.OffchainKeyring +} + +type OCR2OffchainKeyringGetKeyringsRequest struct { + Names []string // Empty slice means get all OCR2 offchain keyrings +} + +type OCR2OffchainKeyringGetKeyringsResponse struct { + Keyrings []ocrtypes.OffchainKeyring +} + +// TODO: Maybe want to embed this interface and add a few other methods. +func CreateOCR2OffchainKeyring(ctx context.Context, ks keystore.Keystore, localName string) (ocrtypes.OffchainKeyring, error) { + createReq := keystore.CreateKeysRequest{ + Keys: []keystore.CreateKeyRequest{ + { + KeyName: GetOCR2OffchainSigningKeystoreName(localName), + KeyType: keystore.Ed25519, + }, + { + KeyName: GetOCR2OffchainEncryptionKeystoreName(localName), + KeyType: keystore.X25519, + }, + }, + } + resp, err := ks.CreateKeys(ctx, createReq) + if err != nil { + return nil, err + } + if len(resp.Keys) != 2 { + return nil, fmt.Errorf("expected 2 keys, got %d", len(resp.Keys)) + } + return &evmOffchainKeyring{ + ks: ks, + OffchainKey: resp.Keys[0].KeyInfo, + OffchainEncryptionKey: resp.Keys[1].KeyInfo, + }, nil +} + +// ListOCR2OffchainKeyrings lists OCR2 offchain keyrings. If no local names provided, returns all OCR2 offchain keyrings. +func ListOCR2OffchainKeyrings(ctx context.Context, ks keystore.Keystore, localNames ...string) ([]ocrtypes.OffchainKeyring, error) { + // Build names if explicitly provided + var names []string + if len(localNames) > 0 { + for _, ln := range localNames { + names = append(names, GetOCR2OffchainSigningKeystoreName(ln)) + } + } + + getReq := keystore.GetKeysRequest{KeyNames: names} + resp, err := ks.GetKeys(ctx, getReq) + if err != nil { + return nil, err + } + + var keyrings []ocrtypes.OffchainKeyring + for _, key := range resp.Keys { + if IsOCR2OffchainSigningKey(key.KeyInfo.Name) { + // Fetch the matching encryption key + encryptionKeyName := GetOCR2OffchainEncryptionKeystoreName(strings.TrimPrefix(key.KeyInfo.Name, JoinKeySegments(EVMPrefix, OCR2OffchainSigningPrefix, ""))) + getReq := keystore.GetKeysRequest{KeyNames: []string{encryptionKeyName}} + getResp, err := ks.GetKeys(context.Background(), getReq) + if err != nil { + return nil, err + } + if len(getResp.Keys) == 0 { + return nil, fmt.Errorf("encryption key not found for keyring: %s", key.KeyInfo.Name) + } + keyrings = append(keyrings, &evmOffchainKeyring{ + ks: ks, + OffchainKey: key.KeyInfo, + OffchainEncryptionKey: getResp.Keys[0].KeyInfo, + }) + } + } + return keyrings, nil +} + +var _ ocrtypes.OffchainKeyring = &evmOffchainKeyring{} + +type evmOffchainKeyring struct { + ks keystore.Keystore + OffchainKey keystore.KeyInfo + OffchainEncryptionKey keystore.KeyInfo +} + +func (k *evmOffchainKeyring) OffchainPublicKey() ocrtypes.OffchainPublicKey { + var pubKey ocrtypes.OffchainPublicKey + copy(pubKey[:], k.OffchainKey.PublicKey) + return pubKey +} + +func (k *evmOffchainKeyring) ConfigEncryptionPublicKey() ocrtypes.ConfigEncryptionPublicKey { + var pubKey ocrtypes.ConfigEncryptionPublicKey + copy(pubKey[:], k.OffchainEncryptionKey.PublicKey) + return pubKey +} + +func (k *evmOffchainKeyring) OffchainSign(msg []byte) ([]byte, error) { + signResp, err := k.ks.Sign(context.Background(), keystore.SignRequest{ + KeyName: k.OffchainKey.Name, + Data: msg, + }) + return signResp.Signature, err +} + +func (k *evmOffchainKeyring) ConfigDiffieHellman(point [curve25519.PointSize]byte) ([curve25519.PointSize]byte, error) { + resp, err := k.ks.DeriveSharedSecret(context.Background(), keystore.DeriveSharedSecretRequest{ + KeyName: k.OffchainEncryptionKey.Name, + RemotePubKey: point[:], + }) + if err != nil { + return [curve25519.PointSize]byte{}, err + } + + var sharedPoint [curve25519.PointSize]byte + copy(sharedPoint[:], resp.SharedSecret) + return sharedPoint, nil +} diff --git a/pkg/keysv2/ocr2_onchain_keyring.go b/pkg/keysv2/ocr2_onchain_keyring.go new file mode 100644 index 0000000000..b9d6bd0c2e --- /dev/null +++ b/pkg/keysv2/ocr2_onchain_keyring.go @@ -0,0 +1,139 @@ +package keysv2 + +import ( + "context" + "fmt" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + gethcrypto "github.com/ethereum/go-ethereum/crypto" + "github.com/smartcontractkit/chainlink-common/keystore" + evmutil "github.com/smartcontractkit/libocr/offchainreporting2plus/chains/evmutil" + ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" +) + +const ( + OCR2OnchainPrefix = "ocr2_onchain" +) + +func GetOCR2OnchainKeystoreName(localName string) string { + return JoinKeySegments(EVMPrefix, OCR2OnchainPrefix, localName) +} + +func IsOCR2OnchainKey(name string) bool { + return strings.HasPrefix(name, JoinKeySegments(EVMPrefix, OCR2OnchainPrefix, "")) +} + +type OCR2OnchainKeyringCreateRequest struct { + LocalName string +} + +type OCR2OnchainKeyringCreateResponse struct { + Keyring ocrtypes.OnchainKeyring +} + +type OCR2OnchainKeyringGetKeyringsRequest struct { + Names []string // Empty slice means get all OCR2 onchain keyrings +} + +type OCR2OnchainKeyringGetKeyringsResponse struct { + Keyrings []ocrtypes.OnchainKeyring +} + +// CreateOCR2OnchainKeyring creates an OCR2 onchain keyring using the base keystore and returns the handle. +func CreateOCR2OnchainKeyring(ctx context.Context, ks keystore.Keystore, localName string) (ocrtypes.OnchainKeyring, error) { + createReq := keystore.CreateKeysRequest{ + Keys: []keystore.CreateKeyRequest{ + { + KeyName: GetOCR2OnchainKeystoreName(localName), + KeyType: keystore.ECDSA_S256, + }, + }, + } + resp, err := ks.CreateKeys(ctx, createReq) + if err != nil { + return nil, err + } + if len(resp.Keys) != 1 { + return nil, fmt.Errorf("expected 1 key, got %d", len(resp.Keys)) + } + publicKey, err := gethcrypto.UnmarshalPubkey(resp.Keys[0].KeyInfo.PublicKey) + if err != nil { + return nil, err + } + addr := gethcrypto.PubkeyToAddress(*publicKey) + return &evmOnchainKeyring{ks: ks, onchainKey: resp.Keys[0].KeyInfo, addr: addr}, nil +} + +// ListOCR2OnchainKeyrings lists OCR2 onchain keyrings. If no local names provided, returns all OCR2 onchain keyrings. +func ListOCR2OnchainKeyrings(ctx context.Context, ks keystore.Keystore, localNames ...string) ([]ocrtypes.OnchainKeyring, error) { + // Build names if explicitly provided + var names []string + if len(localNames) > 0 { + for _, ln := range localNames { + names = append(names, GetOCR2OnchainKeystoreName(ln)) + } + } + + getReq := keystore.GetKeysRequest{KeyNames: names} + resp, err := ks.GetKeys(ctx, getReq) + if err != nil { + return nil, err + } + + var keyrings []ocrtypes.OnchainKeyring + for _, key := range resp.Keys { + if IsOCR2OnchainKey(key.KeyInfo.Name) { + keyrings = append(keyrings, &evmOnchainKeyring{ks: ks, onchainKey: key.KeyInfo}) + } + } + return keyrings, nil +} + +var _ ocrtypes.OnchainKeyring = &evmOnchainKeyring{} + +type evmOnchainKeyring struct { + ks keystore.Keystore + onchainKey keystore.KeyInfo + addr common.Address +} + +func (k *evmOnchainKeyring) PublicKey() ocrtypes.OnchainPublicKey { + return k.addr.Bytes() +} + +func ReportToSigData(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) []byte { + rawReportContext := evmutil.RawReportContext(reportCtx) + sigData := crypto.Keccak256(report) + sigData = append(sigData, rawReportContext[0][:]...) + sigData = append(sigData, rawReportContext[1][:]...) + sigData = append(sigData, rawReportContext[2][:]...) + return crypto.Keccak256(sigData) +} + +func (k *evmOnchainKeyring) Sign(reportCtx ocrtypes.ReportContext, report ocrtypes.Report) ([]byte, error) { + signResp, err := k.ks.Sign(context.Background(), keystore.SignRequest{ + KeyName: k.onchainKey.Name, + Data: ReportToSigData(reportCtx, report), + }) + return signResp.Signature, err +} + +func (k *evmOnchainKeyring) Verify(publicKey ocrtypes.OnchainPublicKey, reportCtx ocrtypes.ReportContext, report ocrtypes.Report, signature []byte) bool { + verifyResp, err := k.ks.Verify(context.Background(), keystore.VerifyRequest{ + KeyType: keystore.ECDSA_S256, + PublicKey: k.onchainKey.PublicKey, + Data: ReportToSigData(reportCtx, report), + Signature: signature, + }) + if err != nil { + // Log? + return false + } + return verifyResp.Valid +} + +func (k *evmOnchainKeyring) MaxSignatureLength() int { + return 65 +}