diff --git a/cmd/oracled/cmd/register_oracle.go b/cmd/oracled/cmd/register_oracle.go index 8046d44..1b26c2b 100644 --- a/cmd/oracled/cmd/register_oracle.go +++ b/cmd/oracled/cmd/register_oracle.go @@ -10,7 +10,9 @@ import ( "time" "github.com/cosmos/cosmos-sdk/client/input" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/edgelesssys/ego/enclave" + oracletypes "github.com/medibloc/panacea-core/v2/x/oracle/types" "github.com/medibloc/panacea-oracle/client/flags" "github.com/medibloc/panacea-oracle/crypto" oracleevent "github.com/medibloc/panacea-oracle/event/oracle" @@ -64,22 +66,64 @@ func registerOracleCmd() *cobra.Command { defer queryClient.Close() // get oracle account from mnemonic. - _, err = panacea.NewOracleAccount(conf.OracleMnemonic, conf.OracleAccNum, conf.OracleAccIndex) + oracleAccount, err := panacea.NewOracleAccount(conf.OracleMnemonic, conf.OracleAccNum, conf.OracleAccIndex) if err != nil { return fmt.Errorf("failed to get oracle account from mnemonic: %w", err) } // generate node key and its remote report - _, nodePubKeyRemoteReport, err := generateSealedNodeKey(nodePrivKeyPath) + nodePubKey, nodePubKeyRemoteReport, err := generateSealedNodeKey(nodePrivKeyPath) if err != nil { return fmt.Errorf("failed to generate node key pair: %w", err) } report, _ := enclave.VerifyRemoteReport(nodePubKeyRemoteReport) - _ = hex.EncodeToString(report.UniqueID) + uniqueID := hex.EncodeToString(report.UniqueID) // request register oracle Tx to Panacea - // TODO: add register-oracle Tx + oracleCommissionRateStr, err := cmd.Flags().GetString(flagOracleCommissionRate) + if err != nil { + return err + } + + oracleCommissionRate, err := sdk.NewDecFromStr(oracleCommissionRateStr) + if err != nil { + return err + } + + endPoint, err := cmd.Flags().GetString(flagOracleEndpoint) + if err != nil { + return err + } + + msgRegisterOracle := oracletypes.NewMsgRegisterOracle(uniqueID, oracleAccount.GetAddress(), nodePubKey, nodePubKeyRemoteReport, trustedBlockInfo.TrustedBlockHeight, trustedBlockInfo.TrustedBlockHash, endPoint, oracleCommissionRate) + txBuilder := panacea.NewTxBuilder(queryClient) + cli, err := panacea.NewGRPCClient(conf.Panacea.GRPCAddr) + if err != nil { + return fmt.Errorf("failed to generate gRPC client: %w", err) + } + defer cli.Close() + + defaultFeeAmount, err := sdk.ParseCoinsNormalized(conf.Panacea.DefaultFeeAmount) + if err != nil { + return err + } + + txBytes, err := txBuilder.GenerateSignedTxBytes(oracleAccount.GetPrivKey(), conf.Panacea.DefaultGasLimit, defaultFeeAmount, msgRegisterOracle) + if err != nil { + return fmt.Errorf("failed to generate signed Tx bytes: %w", err) + } + + resp, err := cli.BroadcastTx(txBytes) + if err != nil { + return fmt.Errorf("failed to broadcast transaction: %w", err) + } + + if resp.TxResponse.Code != 0 { + return fmt.Errorf("register oracle transaction failed: %v", resp.TxResponse.RawLog) + } + + log.Infof("register-oracle transaction succeed. height(%v), hash(%s)", resp.TxResponse.Height, resp.TxResponse.TxHash) // subscribe approval of oracle registration and handle it client, err := rpchttp.New(conf.Panacea.RPCAddr, "/websocket") diff --git a/crypto/encrypt.go b/crypto/encrypt.go new file mode 100644 index 0000000..bf13054 --- /dev/null +++ b/crypto/encrypt.go @@ -0,0 +1,59 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "fmt" + "io" +) + +// EncryptWithAES256 encrypts data using a AES256 cryptography. +func EncryptWithAES256(secretKey, data []byte) ([]byte, error) { + if len(secretKey) != 32 { + return nil, fmt.Errorf("secret key is not for AES-256: total %d bits", 8*len(secretKey)) + } + + block, err := aes.NewCipher(secretKey) + if err != nil { + return nil, err + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, err + } + + cipherText := aesGCM.Seal(nonce, nonce, data, nil) + + return cipherText, nil +} + +// DecryptWithAES256 decrypts data using a AES256 cryptography. +func DecryptWithAES256(secretKey, ciphertext []byte) ([]byte, error) { + if len(secretKey) != 32 { + return nil, fmt.Errorf("secret key is not for AES-256: total %d bits", 8*len(secretKey)) + } + + block, err := aes.NewCipher(secretKey) + if err != nil { + return nil, err + } + + aesgcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + plainText, err := aesgcm.Open(nil, ciphertext[:aesgcm.NonceSize()], ciphertext[aesgcm.NonceSize():], nil) + if err != nil { + return nil, err + } + + return plainText, nil +} diff --git a/event/oracle/event.go b/event/oracle/event.go new file mode 100644 index 0000000..fde70d0 --- /dev/null +++ b/event/oracle/event.go @@ -0,0 +1,79 @@ +package oracle + +import ( + "bytes" + "encoding/hex" + "fmt" + + "github.com/btcsuite/btcd/btcec" + oracletypes "github.com/medibloc/panacea-core/v2/x/oracle/types" + "github.com/medibloc/panacea-oracle/crypto" + "github.com/medibloc/panacea-oracle/panacea" + "github.com/tendermint/tendermint/light/provider" +) + +func makeMsgApproveOracleRegistration(uniqueID, approverAddr, targetAddr string, oraclePrivKey, nodePubKey []byte) (*oracletypes.MsgApproveOracleRegistration, error) { + privKey, _ := crypto.PrivKeyFromBytes(oraclePrivKey) + pubKey, err := btcec.ParsePubKey(nodePubKey, btcec.S256()) + if err != nil { + return nil, err + } + + shareKey := crypto.DeriveSharedKey(privKey, pubKey, crypto.KDFSHA256) + encryptedOraclePrivKey, err := crypto.EncryptWithAES256(shareKey, oraclePrivKey) + if err != nil { + return nil, err + } + + registrationApproval := &oracletypes.ApproveOracleRegistration{ + UniqueId: uniqueID, + ApproverOracleAddress: approverAddr, + TargetOracleAddress: targetAddr, + EncryptedOraclePrivKey: encryptedOraclePrivKey, + } + + return makeMsgApproveOracleRegistrationWithSignature(registrationApproval, oraclePrivKey) +} + +func makeMsgApproveOracleRegistrationWithSignature(approveOracleRegistration *oracletypes.ApproveOracleRegistration, oraclePrivKey []byte) (*oracletypes.MsgApproveOracleRegistration, error) { + key, _ := crypto.PrivKeyFromBytes(oraclePrivKey) + + marshaledApproveOracleRegistration, err := approveOracleRegistration.Marshal() + if err != nil { + return nil, err + } + + sig, err := key.Sign(marshaledApproveOracleRegistration) + if err != nil { + return nil, err + } + + msgApproveOracleRegistration := &oracletypes.MsgApproveOracleRegistration{ + ApproveOracleRegistration: approveOracleRegistration, + Signature: sig.Serialize(), + } + + return msgApproveOracleRegistration, nil +} + +func verifyTrustedBlockInfo(queryClient panacea.QueryClient, height int64, blockHash []byte) error { + block, err := queryClient.GetLightBlock(height) + if err != nil { + switch err { + case provider.ErrLightBlockNotFound, provider.ErrHeightTooHigh: + return fmt.Errorf("not found light block. %w", err) + default: + return err + } + } + + if !bytes.Equal(block.Hash().Bytes(), blockHash) { + return fmt.Errorf("failed to verify trusted block information. height(%v), expected block hash(%s), got block hash(%s)", + height, + hex.EncodeToString(block.Hash().Bytes()), + hex.EncodeToString(blockHash), + ) + } + + return nil +} diff --git a/event/oracle/register_oracle_event.go b/event/oracle/register_oracle_event.go index ed9b92d..3fed816 100644 --- a/event/oracle/register_oracle_event.go +++ b/event/oracle/register_oracle_event.go @@ -1,7 +1,14 @@ package oracle import ( + "crypto/sha256" + "fmt" + + oracletypes "github.com/medibloc/panacea-core/v2/x/oracle/types" "github.com/medibloc/panacea-oracle/event" + "github.com/medibloc/panacea-oracle/panacea" + "github.com/medibloc/panacea-oracle/sgx" + log "github.com/sirupsen/logrus" ctypes "github.com/tendermint/tendermint/rpc/core/types" ) @@ -20,6 +27,61 @@ func (e RegisterOracleEvent) GetEventQuery() string { } func (e RegisterOracleEvent) EventHandler(event ctypes.ResultEvent) error { - // TODO: https://github.com/medibloc/panacea-oracle/issues/3 - panic("will be implemented in issue #3") + uniqueID := event.Events[oracletypes.EventTypeRegistration+"."+oracletypes.AttributeKeyUniqueID][0] + targetAddress := event.Events[oracletypes.EventTypeRegistration+"."+oracletypes.AttributeKeyOracleAddress][0] + + msgApproveOracleRegistration, err := e.verifyAndGetMsgApproveOracleRegistration(uniqueID, targetAddress) + if err != nil { + return err + } + + log.Infof("new oracle registration approval info. uniqueID(%s), approverAddress(%s), targetAddress(%s)", + msgApproveOracleRegistration.ApproveOracleRegistration.UniqueId, + msgApproveOracleRegistration.ApproveOracleRegistration.ApproverOracleAddress, + msgApproveOracleRegistration.ApproveOracleRegistration.TargetOracleAddress, + ) + + txBuilder := panacea.NewTxBuilder(e.reactor.QueryClient()) + txBytes, err := txBuilder.GenerateTxBytes(e.reactor.OracleAcc().GetPrivKey(), e.reactor.Config(), msgApproveOracleRegistration) + if err != nil { + return err + } + + txHeight, txHash, err := e.reactor.BroadcastTx(txBytes) + if err != nil { + return fmt.Errorf("failed to ApproveOracleRegistration transaction for new oracle registration: %v", err) + } else { + log.Infof("succeeded to ApproveOracleRegistration transaction for new oracle registration. height(%v), hash(%s)", txHeight, txHash) + } + + return nil +} + +func (e RegisterOracleEvent) verifyAndGetMsgApproveOracleRegistration(uniqueID, targetAddress string) (*oracletypes.MsgApproveOracleRegistration, error) { + queryClient := e.reactor.QueryClient() + approverAddress := e.reactor.OracleAcc().GetAddress() + oraclePrivKeyBz := e.reactor.OraclePrivKey().Serialize() + approverUniqueID := e.reactor.EnclaveInfo().UniqueIDHex() + + if uniqueID != approverUniqueID { + return nil, fmt.Errorf("oracle's uniqueID does not match the requested uniqueID. expected(%s) got(%s)", approverUniqueID, uniqueID) + } else { + oracleRegistration, err := queryClient.GetOracleRegistration(uniqueID, targetAddress) + log.Errorf("err while get oracleRegistration: %v", err) + + if err := verifyTrustedBlockInfo(e.reactor.QueryClient(), oracleRegistration.TrustedBlockHeight, oracleRegistration.TrustedBlockHash); err != nil { + log.Errorf("failed to verify trusted block. height(%d), hash(%s), err(%v)", oracleRegistration.TrustedBlockHeight, oracleRegistration.TrustedBlockHash, err) + return nil, err + } + + nodePubKeyHash := sha256.Sum256(oracleRegistration.NodePubKey) + + if err := sgx.VerifyRemoteReport(oracleRegistration.NodePubKeyRemoteReport, nodePubKeyHash[:], *e.reactor.EnclaveInfo()); err != nil { + log.Errorf("failed to verification report. uniqueID(%s), address(%s), err(%v)", oracleRegistration.UniqueId, oracleRegistration.OracleAddress, err) + return nil, err + } + + return makeMsgApproveOracleRegistration(approverUniqueID, approverAddress, targetAddress, oraclePrivKeyBz, oracleRegistration.NodePubKey) + } + } diff --git a/go.mod b/go.mod index 7b16c3e..9a39f2c 100644 --- a/go.mod +++ b/go.mod @@ -83,6 +83,7 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-isatty v0.0.16 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/medibloc/panacea-core/v2 v2.0.6-0.20221208062148-f30a46f80881 // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect github.com/minio/sha256-simd v0.1.1 // indirect diff --git a/go.sum b/go.sum index 8e75329..e94c796 100644 --- a/go.sum +++ b/go.sum @@ -1382,6 +1382,8 @@ github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88J github.com/mbilski/exhaustivestruct v1.2.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc= github.com/medibloc/cosmos-sdk v0.45.9-panacea.1 h1:JTprXN6z/+6UjkjQU4OfDz7z+sUpzev1s9DywmWA2Sk= github.com/medibloc/cosmos-sdk v0.45.9-panacea.1/go.mod h1:Z5M4TX7PsHNHlF/1XanI2DIpORQ+Q/st7oaeufEjnvU= +github.com/medibloc/panacea-core/v2 v2.0.6-0.20221208062148-f30a46f80881 h1:NllUbgXJzXVWpqSlfIt8NCOtcLAO5FXM/ixJbvmvIXk= +github.com/medibloc/panacea-core/v2 v2.0.6-0.20221208062148-f30a46f80881/go.mod h1:jDmkCB2vXq/Daq/XvdzF5ELGH9eT20WpCLdTB7zhMBY= github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517/go.mod h1:KQ7+USdGKfpPjXk4Ga+5XxQM4Lm4e3gAogrreFAYpOg= github.com/mgechev/revive v1.2.1/go.mod h1:+Ro3wqY4vakcYNtkBWdZC7dBg1xSB6sp054wWwmeFm0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= diff --git a/panacea/query_client.go b/panacea/query_client.go index a43239c..0db5804 100644 --- a/panacea/query_client.go +++ b/panacea/query_client.go @@ -15,6 +15,7 @@ import ( "github.com/cosmos/cosmos-sdk/std" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/ibc-go/v2/modules/core/23-commitment/types" + oracletypes "github.com/medibloc/panacea-core/v2/x/oracle/types" "github.com/medibloc/panacea-oracle/config" sgxdb "github.com/medibloc/panacea-oracle/store/sgxleveldb" log "github.com/sirupsen/logrus" @@ -34,6 +35,10 @@ import ( type QueryClient interface { Close() error GetAccount(address string) (authtypes.AccountI, error) + GetOracleRegistration(uniqueID, oracleAddr string) (*oracletypes.OracleRegistration, error) + GetLightBlock(height int64) (*tmtypes.LightBlock, error) + GetCdc() *codec.ProtoCodec + GetChainID() string } const ( @@ -219,6 +224,14 @@ func refresh(ctx context.Context, lc *light.Client, trustPeriod time.Duration, m return nil } +func (q verifiedQueryClient) GetCdc() *codec.ProtoCodec { + return q.cdc +} + +func (q verifiedQueryClient) GetChainID() string { + return q.chainID +} + // GetStoreData get data from panacea with storeKey and key, then verify queried data with light client and merkle proof. // the returned data type is ResponseQuery.value ([]byte), so recommend to convert to expected type func (q verifiedQueryClient) GetStoreData(ctx context.Context, storeKey string, key []byte) ([]byte, error) { @@ -348,28 +361,28 @@ func (q verifiedQueryClient) GetAccount(address string) (authtypes.AccountI, err return account, nil } -//func (q verifiedQueryClient) GetOracleRegistration(oracleAddr, uniqueID, pubKey string) (*oracletypes.OracleRegistration, error) { -// -// acc, err := GetAccAddressFromBech32(oracleAddr) -// if err != nil { -// return nil, err -// } -// -// key := oracletypes.GetOracleRegistrationKey(uniqueID, acc, pubKey) -// -// bz, err := q.GetStoreData(context.Background(), oracletypes.StoreKey, key) -// if err != nil { -// return nil, err -// } -// -// var oracleRegistration oracletypes.OracleRegistration -// err = q.cdc.UnmarshalLengthPrefixed(bz, &oracleRegistration) -// if err != nil { -// return nil, err -// } -// -// return &oracleRegistration, nil -//} +func (q verifiedQueryClient) GetOracleRegistration(uniqueID, oracleAddr string) (*oracletypes.OracleRegistration, error) { + + acc, err := GetAccAddressFromBech32(oracleAddr) + if err != nil { + return nil, err + } + + key := oracletypes.GetOracleRegistrationKey(uniqueID, acc) + + bz, err := q.GetStoreData(context.Background(), oracletypes.StoreKey, key) + if err != nil { + return nil, err + } + + var oracleRegistration oracletypes.OracleRegistration + err = q.cdc.UnmarshalLengthPrefixed(bz, &oracleRegistration) + if err != nil { + return nil, err + } + + return &oracleRegistration, nil +} //func (q verifiedQueryClient) GetOracleParamsPublicKey() (*btcec.PublicKey, error) { // pubKeyBase64Bz, err := q.GetStoreData(context.Background(), paramstypes.StoreKey, append(append([]byte(oracletypes.StoreKey), '/'), oracletypes.KeyOraclePublicKey...)) diff --git a/panacea/tx.go b/panacea/tx.go new file mode 100644 index 0000000..59ad5f7 --- /dev/null +++ b/panacea/tx.go @@ -0,0 +1,100 @@ +package panacea + +import ( + clienttx "github.com/cosmos/cosmos-sdk/client/tx" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/bech32" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing" + authtx "github.com/cosmos/cosmos-sdk/x/auth/tx" + "github.com/medibloc/panacea-oracle/config" +) + +type TxBuilder struct { + client QueryClient +} + +func NewTxBuilder(client QueryClient) *TxBuilder { + return &TxBuilder{ + client: client, + } +} + +// GenerateTxBytes generates transaction byte array. +func (tb TxBuilder) GenerateTxBytes(privKey cryptotypes.PrivKey, conf *config.Config, msg ...sdk.Msg) ([]byte, error) { + defaultFeeAmount, err := sdk.ParseCoinsNormalized(conf.Panacea.DefaultFeeAmount) + if err != nil { + return nil, err + } + txBytes, err := tb.GenerateSignedTxBytes(privKey, conf.Panacea.DefaultGasLimit, defaultFeeAmount, msg...) + if err != nil { + return nil, err + } + + return txBytes, nil +} + +// GenerateSignedTxBytes signs msgs using the private key and returns the signed Tx message in form of byte array. +func (tb TxBuilder) GenerateSignedTxBytes( + privateKey cryptotypes.PrivKey, + gasLimit uint64, + feeAmount sdk.Coins, + msg ...sdk.Msg, +) ([]byte, error) { + txConfig := authtx.NewTxConfig(tb.client.GetCdc(), []signing.SignMode{signing.SignMode_SIGN_MODE_DIRECT}) + txBuilder := txConfig.NewTxBuilder() + txBuilder.SetGasLimit(gasLimit) + txBuilder.SetFeeAmount(feeAmount) + + if err := txBuilder.SetMsgs(msg...); err != nil { + return nil, err + } + + signerAddress, err := bech32.ConvertAndEncode(prefix, privateKey.PubKey().Address().Bytes()) + if err != nil { + return nil, err + } + + signerAccount, err := tb.client.GetAccount(signerAddress) + if err != nil { + return nil, err + } + + sigV2 := signing.SignatureV2{ + PubKey: privateKey.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signing.SignMode_SIGN_MODE_DIRECT, + Signature: nil, + }, + Sequence: signerAccount.GetSequence(), + } + + if err := txBuilder.SetSignatures(sigV2); err != nil { + return nil, err + } + + signerData := authsigning.SignerData{ + ChainID: tb.client.GetChainID(), + AccountNumber: signerAccount.GetAccountNumber(), + Sequence: signerAccount.GetSequence(), + } + + sigV2, err = clienttx.SignWithPrivKey( + signing.SignMode_SIGN_MODE_DIRECT, + signerData, + txBuilder, + privateKey, + txConfig, + signerAccount.GetSequence(), + ) + if err != nil { + return nil, err + } + + if err := txBuilder.SetSignatures(sigV2); err != nil { + return nil, err + } + + return txConfig.TxEncoder()(txBuilder.GetTx()) +} diff --git a/server/middleware/auth_test.go b/server/middleware/auth_test.go index d3e8a48..756d7ae 100644 --- a/server/middleware/auth_test.go +++ b/server/middleware/auth_test.go @@ -9,14 +9,17 @@ import ( "time" "github.com/btcsuite/btcd/btcec" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/lestrrat-go/jwx/v2/jwa" "github.com/lestrrat-go/jwx/v2/jwt" + oracletypes "github.com/medibloc/panacea-core/v2/x/oracle/types" "github.com/medibloc/panacea-oracle/server/middleware" "github.com/stretchr/testify/require" + tmtypes "github.com/tendermint/tendermint/types" ) var ( @@ -133,6 +136,22 @@ func testHTTPRequest(t *testing.T, authorizationHeader string, statusCode int, e type mockQueryClient struct{} +func (c *mockQueryClient) GetOracleRegistration(uniqueID, oracleAddr string) (*oracletypes.OracleRegistration, error) { + return nil, nil +} + +func (c *mockQueryClient) GetLightBlock(height int64) (*tmtypes.LightBlock, error) { + return nil, nil +} + +func (c *mockQueryClient) GetCdc() *codec.ProtoCodec { + return nil +} + +func (c *mockQueryClient) GetChainID() string { + return "" +} + func (c *mockQueryClient) Close() error { return nil }