Repository: coinbase/rosetta-cli
Pull request #386 (2023-01-27 22:28:06)

add sign tool
Fixes # .

### Motivation
Adding a sign tool to sign transactions for local testing

### Solution
<!--
What is the solution here from a high level. What are the key technical decisions and why were they made?
-->

### Open questions
<!--
(optional) Any open questions or feedback on design desired?
-->



Patch:
diff --git a/cmd/check_data.go b/cmd/check_data.go
index 026285e..535d6c7 100644
--- a/cmd/check_data.go
+++ b/cmd/check_data.go
@@ -25,7 +25,7 @@ import (
 	"github.com/coinbase/rosetta-sdk-go/fetcher"
 	"github.com/coinbase/rosetta-sdk-go/types"
 	"github.com/coinbase/rosetta-sdk-go/utils"
-	"github.com/fatih/color" 
+	"github.com/fatih/color"
 	"github.com/spf13/cobra"
 	"golang.org/x/sync/errgroup"
 )
@@ -171,7 +171,7 @@ func runCheckDataCmd(_ *cobra.Command, _ []string) error {
 
 	g, ctx := errgroup.WithContext(ctx)
 	ctx = logger.AddMetadataMapToContext(ctx, metadataMap)
-	
+
 	g.Go(func() error {
 		return dataTester.StartPeriodicLogger(ctx)
 	})
diff --git a/cmd/root.go b/cmd/root.go
index 3cca1e7..92bb8b2 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -375,6 +375,9 @@ default values.`,
 		`Verify both minimum and Coinbase spec requirements`,
 	)
 	rootCmd.AddCommand(checkSpecCmd)
+
+	// Sign command
+	rootCmd.AddCommand(signCmd)
 }
 
 func initConfig() {
@@ -451,7 +454,7 @@ func initConfig() {
 	}
 
 	if len(InfoMetaData) != 0 {
-		Config.InfoMetaData = InfoMetaData	
+		Config.InfoMetaData = InfoMetaData
 	}
 }
 
diff --git a/cmd/sign.go b/cmd/sign.go
new file mode 100644
index 0000000..4bb7cd8
--- /dev/null
+++ b/cmd/sign.go
@@ -0,0 +1,72 @@
+// Copyright 2023 Coinbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cmd
+
+import (
+	"encoding/hex"
+	"errors"
+	"fmt"
+
+	"github.com/coinbase/rosetta-sdk-go/keys"
+	"github.com/fatih/color"
+	"github.com/spf13/cobra"
+)
+
+var (
+	signCmd = &cobra.Command{
+		Use:   "sign",
+		Short: "Sign an unsigned payload with given private key",
+		Long: `Sign an unsigned payload with given private key
+				It supports Keypair specified by https://github.com/coinbase/rosetta-specifications`,
+		RunE: runSignCmd,
+	}
+)
+
+func runSignCmd(_ *cobra.Command, _ []string) error {
+	if Config.Sign == nil {
+		return errors.New("sign configuration is missing")
+	}
+
+	keyPair, err := keys.ImportPrivateKey(Config.Sign.PrivateKey, Config.Sign.PubKey.CurveType)
+	if err != nil {
+		fmt.Println(fmt.Errorf("unable to import private keys %#v", err))
+		return err
+	}
+
+	err = keyPair.IsValid()
+	if err != nil {
+		fmt.Println(fmt.Errorf("keypair invalid with err %#v", err))
+		return err
+	}
+
+	signer, err := keyPair.Signer()
+	if err != nil {
+		fmt.Println(fmt.Errorf("signer invalid with err %#v", err))
+		return err
+	}
+
+	signingPayload := Config.Sign.SigningPayload
+	signatureType := Config.Sign.SigningPayload.SignatureType
+
+	sign, err := signer.Sign(signingPayload, signatureType)
+	if err != nil {
+		fmt.Println(fmt.Errorf("unable to sign with err %#v", err))
+		return err
+	}
+
+	hexSig := hex.EncodeToString(sign.Bytes)
+	color.Blue(hexSig)
+	return nil
+}
diff --git a/configuration/types.go b/configuration/types.go
index 01763a0..8ad3a8f 100644
--- a/configuration/types.go
+++ b/configuration/types.go
@@ -464,7 +464,7 @@ type Configuration struct {
 	// InfoMetaData is a string, rosetta-cli will convert it into a map[string]string
 	// key-value are separated by ":"
 	// different key-value pairs are separated by ","
-	// an example: if users want to record "instance_name" as "1234", and "blockchain_name" as "Bitcoin", 
+	// an example: if users want to record "instance_name" as "1234", and "blockchain_name" as "Bitcoin",
 	// this field would be "instance_name:1234,blockchain_name:Bitcoin"
 	// if adding spaces before and after ":" and ",", it will be trimmed when building map
 	// " instance_name : xxxx , blockchain_name : xxxx " will be recorded same as
@@ -474,11 +474,17 @@ type Configuration struct {
 	Construction *ConstructionConfiguration `json:"construction"`
 	Data         *DataConfiguration         `json:"data"`
 	Perf         *CheckPerfConfiguration    `json:"perf"`
+	Sign         *SignConfiguration         `json:"sign"`
 }
 
-//********************//
-// Check Perf configs //
-//********************//
+// SignConfiguration configuration for signing
+type SignConfiguration struct {
+	PubKey         *types.PublicKey      `json:"pub_key"`
+	PrivateKey     string                `json:"private_key"`
+	SigningPayload *types.SigningPayload `json:"signing_payload"`
+}
+
+// CheckPerfConfiguration configuration for check perf
 type CheckPerfConfiguration struct {
 
 	// StartBlock is the starting block for running check:perf.
diff --git a/examples/configuration/default.json b/examples/configuration/default.json
index d2c0c4f..97e38a8 100644
--- a/examples/configuration/default.json
+++ b/examples/configuration/default.json
@@ -42,5 +42,6 @@
   "pruning_balance_disabled": false,
   "initial_balance_fetch_disabled": false
  },
- "perf": null
+ "perf": null,
+ "sign": null
 }
\ No newline at end of file
diff --git a/examples/configuration/sign.json b/examples/configuration/sign.json
new file mode 100644
index 0000000..c79d1f8
--- /dev/null
+++ b/examples/configuration/sign.json
@@ -0,0 +1,13 @@
+{
+  "sign": {
+    "pub_key": {
+      "curve_type": "secp256k1",
+      "hex_bytes": "03c7e625aa08cad8f257d9ee2b9b7a0214f19f981afd5b498c728ad7ed6c0c3df6"
+    },
+    "private_key": "",
+    "signing_payload": {
+      "hex_bytes": "370e74254e8cbaa343af3564901456082ec7af967e45ff24ba061233b1a1b04f",
+      "signature_type": "ecdsa"
+    }
+  }
+}
\ No newline at end of file
diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go
index 5ade4f3..f18d34f 100644
--- a/pkg/logger/logger.go
+++ b/pkg/logger/logger.go
@@ -102,7 +102,7 @@ func NewLogger(
 	logReconciliation bool,
 	checkType CheckType,
 	network *types.NetworkIdentifier,
-	logMetadataMap   map[string]string,
+	logMetadataMap map[string]string,
 	fields ...zap.Field,
 ) (*Logger, error) {
 	zapLogger, err := buildZapLogger(checkType, network, fields...)
@@ -217,7 +217,7 @@ func (l *Logger) LogConstructionStatus(
 	}
 
 	statsMessage = AddMetadata(statsMessage, l.logMetadataMap)
-	
+
 	l.lastStatsMessage = statsMessage
 	color.Cyan(statsMessage)
 }
@@ -487,7 +487,7 @@ func (l *Logger) ReconcileSuccessStream(
 	)
 	reconciliationSuccessString = AddMetadata(reconciliationSuccessString, l.logMetadataMap)
 	color.Cyan(reconciliationSuccessString)
-	
+
 	_, err = f.WriteString(reconciliationSuccessString)
 	if err != nil {
 		err = fmt.Errorf("failed to write reconciliation success string %s: %w", reconciliationSuccessString, err)
@@ -601,14 +601,14 @@ func (l *Logger) Fatal(msg string, fields ...zap.Field) {
 }
 
 // return a string of metadata
-func (l *Logger) GetMetadata() string{
+func (l *Logger) GetMetadata() string {
 	metadatMap := l.logMetadataMap
 	metadata := ConvertMapToString(metadatMap)
 	return metadata
 }
 
 // return a map of metadatMap
-func (l *Logger) GetMetadataMap() map[string]string{
+func (l *Logger) GetMetadataMap() map[string]string {
 	metadatMap := l.logMetadataMap
 	return metadatMap
 }
@@ -683,7 +683,7 @@ func ConvertStringToMap(metadata string) map[string]string {
 	pairs := strings.Split(metadata, ",")
 	for _, pair := range pairs {
 		kv := strings.Split(pair, ":")
-		if(len(kv) != 2) {
+		if len(kv) != 2 {
 			log := fmt.Sprintf("the %s from %s could be transfer to key value pair", pair, metadata)
 			color.Yellow(log)
 		} else {
@@ -694,7 +694,7 @@ func ConvertStringToMap(metadata string) map[string]string {
 }
 
 // add requesrUUID to metadataMap
-func AddRequestUUIDToMap(metadataMap map[string]string, requestUUID string) map[string]string{
+func AddRequestUUIDToMap(metadataMap map[string]string, requestUUID string) map[string]string {
 	if len(requestUUID) > 0 {
 		metadataMap["RequestID"] = requestUUID
 	}
@@ -711,5 +711,5 @@ func ConvertMapToString(metadataMap map[string]string) string {
 			}
 		}
 	}
-	return metadata;
+	return metadata
 }
diff --git a/pkg/processor/balance_storage_helper.go b/pkg/processor/balance_storage_helper.go
index 6f929b4..704ec6a 100644
--- a/pkg/processor/balance_storage_helper.go
+++ b/pkg/processor/balance_storage_helper.go
@@ -65,7 +65,7 @@ func NewBalanceStorageHelper(
 	// Pre-process exemptAccounts on initialization
 	// to provide fast lookup while syncing.
 	for _, account := range exemptAccounts {
-		// if users do not specify Currency, we add the address 
+		// if users do not specify Currency, we add the address
 		// by this, all the Currencies in this address will be skipped
 		if account.Currency == nil {
 			exemptMap[account.Account.Address] = struct{}{}
@@ -150,12 +150,12 @@ func (h *BalanceStorageHelper) ExemptFunc() parser.ExemptOperation {
 				return true
 			}
 		}
-		// if exemptAccounts have the Account address means all the 
+		// if exemptAccounts have the Account address means all the
 		// currencies in this Account address need to be skipped
 		_, existsAddress := h.exemptAccounts[op.Account.Address]
 		if existsAddress {
 			return existsAddress
-		} 
+		}
 
 		thisAcct := types.Hash(&types.AccountCurrency{
 			Account:  op.Account,


Test patch:
diff --git a/cmd/utils_asserter_configuration_test.go b/cmd/utils_asserter_configuration_test.go
index ce6f3ca..d7844e9 100644
--- a/cmd/utils_asserter_configuration_test.go
+++ b/cmd/utils_asserter_configuration_test.go
@@ -50,18 +50,18 @@ var (
 
 	allowedErrors = []*types.Error{
 		{
-			Code: 4,
-			Message: "Block not found",
+			Code:      4,
+			Message:   "Block not found",
 			Retriable: false,
 		},
 		{
-			Code: 0,
-			Message: "Endpoint not implemented",
+			Code:      0,
+			Message:   "Endpoint not implemented",
 			Retriable: false,
 		},
 		{
-			Code: 3,
-			Message: "Bitcoind error",
+			Code:      3,
+			Message:   "Bitcoind error",
 			Retriable: false,
 		},
 	}
@@ -93,18 +93,18 @@ func TestSortArrayFields(t *testing.T) {
 	}, clientConfiguration.AllowedOperationStatuses)
 	assert.Equal([]*types.Error{
 		{
-			Code: 0,
-			Message: "Endpoint not implemented",
+			Code:      0,
+			Message:   "Endpoint not implemented",
 			Retriable: false,
 		},
 		{
-			Code: 3,
-			Message: "Bitcoind error",
+			Code:      3,
+			Message:   "Bitcoind error",
 			Retriable: false,
 		},
 		{
-			Code: 4,
-			Message: "Block not found",
+			Code:      4,
+			Message:   "Block not found",
 			Retriable: false,
 		},
 	}, clientConfiguration.AllowedErrors)
diff --git a/cmd/validate_asserter_config_test.go b/cmd/validate_asserter_config_test.go
index e48bf9c..2a64605 100644
--- a/cmd/validate_asserter_config_test.go
+++ b/cmd/validate_asserter_config_test.go
@@ -17,8 +17,8 @@ package cmd
 import (
 	"github.com/coinbase/rosetta-sdk-go/asserter"
 	"github.com/coinbase/rosetta-sdk-go/types"
-	"testing"
 	"github.com/stretchr/testify/assert"
+	"testing"
 )
 
 func TestMatch(t *testing.T) {
@@ -104,10 +104,10 @@ func generateNetworkAllowAndAsserterConfiguration() (
 ) {
 	var tsi int64 = 5
 	allow := &types.Allow{
-		OperationStatuses:       generateOperationStatuses(),
-		OperationTypes:          generateOperationTypes(),
-		Errors:                  generateErrors(),
-		TimestampStartIndex:     &tsi,
+		OperationStatuses:   generateOperationStatuses(),
+		OperationTypes:      generateOperationTypes(),
+		Errors:              generateErrors(),
+		TimestampStartIndex: &tsi,
 	}
 	config := &asserter.Configuration{
 		AllowedOperationStatuses:   generateOperationStatuses(),
@@ -127,7 +127,7 @@ func generateOperationStatuses() []*types.OperationStatus {
 	return []*types.OperationStatus{
 		{
 			Successful: true,
-			Status: "status0",
+			Status:     "status0",
 		},
 		{
 			// Successful: false
@@ -139,11 +139,11 @@ func generateOperationStatuses() []*types.OperationStatus {
 func generateErrors() []*types.Error {
 	return []*types.Error{
 		{
-			Code: 1,
+			Code:    1,
 			Message: "message1",
 		},
 		{
-			Code: 2,
+			Code:    2,
 			Message: "message2",
 		},
 	}
diff --git a/pkg/tester/data.go b/pkg/tester/data.go
index 0487fa6..68afd89 100644
--- a/pkg/tester/data.go
+++ b/pkg/tester/data.go
@@ -78,7 +78,7 @@ const (
 
 	//MaxTableSize unit is MB
 	MaxValueLogFileSize = int64(2048)
-	
+
 	// empty requestUUID
 	EmptyRequestUUID = ""
 )
@@ -1106,7 +1106,7 @@ func (t *DataTester) HandleErr(err error, sigListeners *[]context.CancelFunc) er
 	}
 
 	return t.FindMissingOps(
-		ctx, 
+		ctx,
 		err,
 		sigListeners,
 	)