From 27625e1ed2c1de80ba95dddfb70a3c4a745a1629 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 3 Oct 2024 16:16:32 +0100 Subject: [PATCH 1/3] examples: add TLS support to basic price oracle service This commit adds basic TLS support to the example price oracle service. When connecting to the RPC price oracle, Tapd now assumes TLS. TLS certificate authentication is skipped by Tapd. --- docs/examples/basic-price-oracle/main.go | 85 ++++++++++++++++++++++-- 1 file changed, 79 insertions(+), 6 deletions(-) diff --git a/docs/examples/basic-price-oracle/main.go b/docs/examples/basic-price-oracle/main.go index 5838bea8c..c2ee54f59 100644 --- a/docs/examples/basic-price-oracle/main.go +++ b/docs/examples/basic-price-oracle/main.go @@ -7,13 +7,22 @@ package main import ( "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" "fmt" + "log" + "math/big" "net" "time" oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/credentials" ) const ( @@ -174,12 +183,76 @@ func startService(grpcServer *grpc.Server) error { return grpcServer.Serve(grpcListener) } +// Generate a self-signed TLS certificate and private key. +func generateSelfSignedCert() (tls.Certificate, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return tls.Certificate{}, err + } + + keyUsage := x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature + extKeyUsage := []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth} + template := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"basic-price-oracle"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), // Valid for 1 day + + KeyUsage: keyUsage, + ExtKeyUsage: extKeyUsage, + BasicConstraintsValid: true, + } + + certDER, err := x509.CreateCertificate( + rand.Reader, &template, &template, &privateKey.PublicKey, + privateKey, + ) + if err != nil { + return tls.Certificate{}, err + } + + privateKeyBits, err := x509.MarshalECPrivateKey(privateKey) + if err != nil { + return tls.Certificate{}, err + } + + certPEM := pem.EncodeToMemory( + &pem.Block{Type: "CERTIFICATE", Bytes: certDER}, + ) + keyPEM := pem.EncodeToMemory( + &pem.Block{Type: "EC PRIVATE KEY", Bytes: privateKeyBits}, + ) + + tlsCert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return tls.Certificate{}, err + } + + return tlsCert, nil +} + func main() { // Start the mock RPC price oracle service. - serverOpts := []grpc.ServerOption{ - grpc.Creds(insecure.NewCredentials()), + // + // Generate self-signed certificate. This allows us to use TLS for the + // gRPC server. + tlsCert, err := generateSelfSignedCert() + if err != nil { + log.Fatalf("Failed to generate TLS certificate: %v", err) + } + + // Create the gRPC server with TLS + transportCredentials := credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{tlsCert}, + }) + backendService := grpc.NewServer(grpc.Creds(transportCredentials)) + + err = startService(backendService) + if err != nil { + log.Fatalf("Start service error: %v", err) } - backendService := grpc.NewServer(serverOpts...) - _ = startService(backendService) - backendService.Stop() + + backendService.GracefulStop() } From 967415bd417dc584a1df33f2b3c3fb282a3fd899 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 7 Oct 2024 14:11:51 +0100 Subject: [PATCH 2/3] examples: add logging to basic price oracle service --- docs/examples/basic-price-oracle/go.mod | 1 + docs/examples/basic-price-oracle/go.sum | 16 ++++++++ docs/examples/basic-price-oracle/main.go | 47 +++++++++++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/docs/examples/basic-price-oracle/go.mod b/docs/examples/basic-price-oracle/go.mod index 1909f32cc..9c627e962 100644 --- a/docs/examples/basic-price-oracle/go.mod +++ b/docs/examples/basic-price-oracle/go.mod @@ -8,6 +8,7 @@ replace github.com/lightninglabs/taproot-assets => ../../../ require ( github.com/lightninglabs/taproot-assets v0.0.0 + github.com/sirupsen/logrus v1.9.2 google.golang.org/grpc v1.59.0 ) diff --git a/docs/examples/basic-price-oracle/go.sum b/docs/examples/basic-price-oracle/go.sum index ab124ac0f..0188426c3 100644 --- a/docs/examples/basic-price-oracle/go.sum +++ b/docs/examples/basic-price-oracle/go.sum @@ -1,3 +1,6 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -6,8 +9,17 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= +github.com/sirupsen/logrus v1.9.2/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= @@ -22,3 +34,7 @@ google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/docs/examples/basic-price-oracle/main.go b/docs/examples/basic-price-oracle/main.go index c2ee54f59..0147ac337 100644 --- a/docs/examples/basic-price-oracle/main.go +++ b/docs/examples/basic-price-oracle/main.go @@ -15,11 +15,15 @@ import ( "crypto/x509/pkix" "encoding/pem" "fmt" + "io" "log" "math/big" "net" + "os" "time" + "github.com/sirupsen/logrus" + oraclerpc "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -30,6 +34,30 @@ const ( serviceListenAddress = "localhost:8095" ) +// setupLogger sets up the logger to write logs to a file. +func setupLogger() { + // Create a log file. + flags := os.O_CREATE | os.O_WRONLY | os.O_APPEND + file, err := os.OpenFile("basic-price-oracle-example.log", flags, 0666) + if err != nil { + logrus.Fatalf("Failed to open log file: %v", err) + } + + // Create a multi-writer to write to both stdout and the file. + multiWriter := io.MultiWriter(os.Stdout, file) + + // Set the output of logrus to the multi-writer. + logrus.SetOutput(multiWriter) + + // Set the log level (optional). + logrus.SetLevel(logrus.DebugLevel) + + // Set the log format (optional). + logrus.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + }) +} + // RpcPriceOracleServer is a basic example RPC price oracle server. type RpcPriceOracleServer struct { oraclerpc.UnimplementedPriceOracleServer @@ -115,6 +143,8 @@ func (p *RpcPriceOracleServer) QueryRateTick(_ context.Context, // Ensure that the payment asset is BTC. We only support BTC as the // payment asset in this example. if !oraclerpc.IsAssetBtc(req.PaymentAsset) { + logrus.Infof("Payment asset is not BTC: %v", req.PaymentAsset) + return &oraclerpc.QueryRateTickResponse{ Result: &oraclerpc.QueryRateTickResponse_Error{ Error: &oraclerpc.QueryRateTickErrResponse{ @@ -127,11 +157,15 @@ func (p *RpcPriceOracleServer) QueryRateTick(_ context.Context, // Ensure that the subject asset is set. if req.SubjectAsset == nil { + logrus.Info("Subject asset is not set") return nil, fmt.Errorf("subject asset is not set") } // Ensure that the subject asset is supported. if !isSupportedSubjectAsset(req.SubjectAsset) { + logrus.Infof("Unsupported subject asset ID str: %v\n", + req.SubjectAsset) + return &oraclerpc.QueryRateTickResponse{ Result: &oraclerpc.QueryRateTickResponse_Error{ Error: &oraclerpc.QueryRateTickErrResponse{ @@ -148,16 +182,24 @@ func (p *RpcPriceOracleServer) QueryRateTick(_ context.Context, // If a rate tick hint is provided, return it as the rate tick. // In doing so, we effectively accept the rate tick proposed by // our peer. + logrus.Info("Suggested asset to BTC rate provided, " + + "returning rate as accepted rate") + rateTick.Rate = req.RateTickHint.Rate rateTick.ExpiryTimestamp = req.RateTickHint.ExpiryTimestamp } else { // If a rate tick hint is not provided, fetch a rate tick from // our internal system. + logrus.Info("Suggested asset to BTC rate not provided, " + + "querying internal system for rate") + rateTick = getRateTick( req.TransactionType, req.SubjectAssetMaxAmount, ) } + logrus.Infof("QueryRateTick returning rate: %v", rateTick.Rate) + return &oraclerpc.QueryRateTickResponse{ Result: &oraclerpc.QueryRateTickResponse_Success{ Success: &oraclerpc.QueryRateTickSuccessResponse{ @@ -171,7 +213,8 @@ func (p *RpcPriceOracleServer) QueryRateTick(_ context.Context, // shut down. func startService(grpcServer *grpc.Server) error { serviceAddr := fmt.Sprintf("rfqrpc://%s", serviceListenAddress) - println("Starting RPC price oracle service at address: ", serviceAddr) + logrus.Infof("Starting RPC price oracle service at address: %s\n", + serviceAddr) server := RpcPriceOracleServer{} oraclerpc.RegisterPriceOracleServer(grpcServer, &server) @@ -234,6 +277,8 @@ func generateSelfSignedCert() (tls.Certificate, error) { } func main() { + setupLogger() + // Start the mock RPC price oracle service. // // Generate self-signed certificate. This allows us to use TLS for the From 2d0b4a48a0ebaa78263084da30f8ba93c48987a3 Mon Sep 17 00:00:00 2001 From: ffranr Date: Mon, 7 Oct 2024 14:57:13 +0100 Subject: [PATCH 3/3] examples: make price oracle asset check more robust Compare subject asset to supported asset using both string and byte slice instead of just string. --- docs/examples/basic-price-oracle/main.go | 30 ++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/examples/basic-price-oracle/main.go b/docs/examples/basic-price-oracle/main.go index 0147ac337..920bf2c30 100644 --- a/docs/examples/basic-price-oracle/main.go +++ b/docs/examples/basic-price-oracle/main.go @@ -6,6 +6,7 @@ package main import ( + "bytes" "context" "crypto/ecdsa" "crypto/elliptic" @@ -13,6 +14,7 @@ import ( "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/hex" "encoding/pem" "fmt" "io" @@ -68,15 +70,35 @@ type RpcPriceOracleServer struct { func isSupportedSubjectAsset(subjectAsset *oraclerpc.AssetSpecifier) bool { // Ensure that the subject asset is set. if subjectAsset == nil { + logrus.Info("Subject asset is not set (nil)") return false } - // In this example we'll only support a single asset. - assetIdStr := subjectAsset.GetAssetIdStr() - supportedAssetId := "7b4336d33b019df9438e586f83c587ca00fa65602497b9" + + supportedAssetIdStr := "7b4336d33b019df9438e586f83c587ca00fa65602497b9" + "3ace193e9ce53b1a67" + supportedAssetIdBytes, err := hex.DecodeString(supportedAssetIdStr) + if err != nil { + fmt.Println("Error decoding supported asset hex string:", err) + return false + } + + // Check the subject asset bytes if set. + subjectAssetIdBytes := subjectAsset.GetAssetId() + if len(subjectAssetIdBytes) > 0 { + logrus.Infof("Subject asset ID bytes populated: %x", + supportedAssetIdBytes) + return bytes.Equal(supportedAssetIdBytes, subjectAssetIdBytes) + } + + subjectAssetIdStr := subjectAsset.GetAssetIdStr() + if len(subjectAssetIdStr) > 0 { + logrus.Infof("Subject asset ID str populated: %s", + supportedAssetIdStr) + return subjectAssetIdStr == supportedAssetIdStr + } - return assetIdStr == supportedAssetId + logrus.Infof("Subject asset ID not set") + return false } // getRateTick returns a rate tick for a given transaction type and subject