Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Offline signer #54

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/ffsigner/main.go",
"args": [
"send-transaction",
"-f", "${workspaceFolder}/test/firefly.ffsigner.yaml",
"-i", "${workspaceFolder}/test/offline-tx.json"
]
}
]
}
17 changes: 13 additions & 4 deletions cmd/ffsigner.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "f", "", "config file")
rootCmd.AddCommand(versionCommand())
rootCmd.AddCommand(configCommand())
rootCmd.AddCommand(sendTransactionCommand())
}

func Execute() error {
Expand All @@ -62,7 +63,7 @@ func initConfig() {
signerconfig.Reset()
}

func run() error {
func initServer() (rpcserver.Server, error) {

initConfig()
err := config.ReadConfig("ffsigner", cfgFile)
Expand All @@ -78,7 +79,7 @@ func run() error {
// Deferred error return from reading config
if err != nil {
cancelCtx()
return i18n.WrapError(ctx, err, i18n.MsgConfigFailed)
return nil, i18n.WrapError(ctx, err, i18n.MsgConfigFailed)
}

// Setup signal handling to cancel the context, which shuts down the API Server
Expand All @@ -90,14 +91,22 @@ func run() error {
}()

if !config.GetBool(signerconfig.FileWalletEnabled) {
return i18n.NewError(ctx, signermsgs.MsgNoWalletEnabled)
return nil, i18n.NewError(ctx, signermsgs.MsgNoWalletEnabled)
}
fileWallet, err := fswallet.NewFilesystemWallet(ctx, fswallet.ReadConfig(signerconfig.FileWalletConfig))
if err != nil {
return err
return nil, err
}

server, err := rpcserver.NewServer(ctx, fileWallet)
if err == nil {
err = server.Init()
}
return server, err
}

func run() error {
server, err := initServer()
if err != nil {
return err
}
Expand Down
54 changes: 54 additions & 0 deletions cmd/send_transaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright © 2023 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// 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 (
"github.com/spf13/cobra"
)

var transactionFile string

func sendTransactionCommand() *cobra.Command {
sendTransactionCmd := &cobra.Command{
Use: "send-transaction input-file.json",
Short: "Submits a transaction from a JSON file",
Long: `The JSON input includes the following parameters:
- abi: One or more function definitions
- method: The name of the function to invoke - required if the ABI contains more than one function
- params: The input parameters, as an object, or array
As well as ethereum signing parameters
- from: The ethereum address to use to sign - must already configured in the wallet
- to: The contract address to invoke (required - as this cannot be used for contract deploy)
- nonce: The nonce for the transaction (required)
- gas: The maximum gas limit for execution of the transaction (required)
- gasPrice
- maxPriorityFeePerGas
- maxFeePerGas
- value
`,
RunE: func(cmd *cobra.Command, args []string) error {
server, err := initServer()
if err != nil {
return err
}
return server.SignTransactionFromFile(cmd.Context(), transactionFile)
},
}
sendTransactionCmd.Flags().StringVarP(&transactionFile, "input", "i", "", "input file")
_ = sendTransactionCmd.MarkFlagRequired("input")
return sendTransactionCmd
}
93 changes: 93 additions & 0 deletions internal/rpcserver/offline_signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright © 2023 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
// 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 rpcserver

import (
"context"
"encoding/json"
"os"

"github.com/hyperledger/firefly-common/pkg/i18n"
"github.com/hyperledger/firefly-common/pkg/log"
"github.com/hyperledger/firefly-signer/internal/signermsgs"
"github.com/hyperledger/firefly-signer/pkg/abi"
"github.com/hyperledger/firefly-signer/pkg/ethsigner"
"github.com/hyperledger/firefly-signer/pkg/ethtypes"
"github.com/hyperledger/firefly-signer/pkg/rpcbackend"
)

type SignTransactionInput struct {
ethsigner.Transaction
Address *ethtypes.Address0xHex `json:"address"`
ABI *abi.ABI `json:"abi"`
Method string `json:"method,omitempty"`
Params interface{} `json:"params"`
}

func (s *rpcServer) SignTransactionFromFile(ctx context.Context, filename string) error {

// Parse the input
var input SignTransactionInput
inputData, err := os.ReadFile(filename)
if err != nil {
return err
}
if err = json.Unmarshal(inputData, &input); err != nil {
return err
}

// Find the function to invoke
functions := input.ABI.Functions()
var method *abi.Entry
if input.Method == "" {
if len(functions) != 1 {
return i18n.NewError(ctx, signermsgs.MsgOfflineSignMethodCount, len(functions))
}
} else {
for _, f := range functions {
if f.String() == input.Method {
// Full signature match wins
method = f
} else if method == nil && f.Name == input.Method {
// But name is good enough (could be multiple overrides, so we don't break)
method = f
}
}
if method == nil {
return i18n.NewError(ctx, signermsgs.MsgOfflineSignMethodNotFound, input.Method)
}
}

// Generate the transaction data
paramValues, err := method.Inputs.ParseExternalDataCtx(ctx, input.Params)
if err == nil {
input.Data, err = method.EncodeCallDataCtx(ctx, paramValues)
}
if err != nil {
return err
}

// Sign the transaction
rpcRes, err := s.processEthTransaction(ctx, &rpcbackend.RPCRequest{}, &input.Transaction)
if err != nil {
return err
}
resBytes, _ := json.Marshal(rpcRes)
log.L(ctx).Infof("Submitted:\n%s", resBytes)
return nil

}
32 changes: 24 additions & 8 deletions internal/rpcserver/rpchandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ func TestSignAndSendTransactionWithNonce(t *testing.T) {
Result: fftypes.JSONAnyPtr(`"0x61ca9c99c1d752fb3bda568b8566edf33ba93585c64a970566e6dfb540a5cbc1"`),
}, nil)

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`{
Expand Down Expand Up @@ -124,7 +126,9 @@ func TestSignAndSendTransactionWithoutNonce(t *testing.T) {
Result: fftypes.JSONAnyPtr(`"0x61ca9c99c1d752fb3bda568b8566edf33ba93585c64a970566e6dfb540a5cbc1"`),
}, nil)

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`{
Expand Down Expand Up @@ -175,7 +179,9 @@ func TestServeJSONRPCFail(t *testing.T) {
},
}, fmt.Errorf("pop"))

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`
Expand Down Expand Up @@ -237,7 +243,9 @@ func TestServeJSONRPCBatchOK(t *testing.T) {
Result: fftypes.JSONAnyPtr(`"result 3"`),
}, nil)

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`[
Expand Down Expand Up @@ -312,7 +320,9 @@ func TestServeJSONRPCBatchOneFailed(t *testing.T) {
},
}, fmt.Errorf("pop"))

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`[
Expand Down Expand Up @@ -361,7 +371,9 @@ func TestServeJSONRPCBatchBadArray(t *testing.T) {
w := s.wallet.(*ethsignermocks.Wallet)
w.On("Initialize", mock.Anything).Return(nil)

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(`[`)))
Expand Down Expand Up @@ -392,7 +404,9 @@ func TestServeJSONRPCBatchEmptyData(t *testing.T) {
w := s.wallet.(*ethsignermocks.Wallet)
w.On("Initialize", mock.Anything).Return(nil)

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(``)))
Expand Down Expand Up @@ -423,7 +437,9 @@ func TestServeJSONRPCBatchBadJSON(t *testing.T) {
w := s.wallet.(*ethsignermocks.Wallet)
w.On("Initialize", mock.Anything).Return(nil)

err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

res, err := http.Post(url, "application/json", bytes.NewReader([]byte(``)))
Expand Down
9 changes: 7 additions & 2 deletions internal/rpcserver/rpcprocessor.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright © 2022 Kaleido, Inc.
// Copyright © 2023 Kaleido, Inc.
//
// SPDX-License-Identifier: Apache-2.0
//
Expand Down Expand Up @@ -72,6 +72,11 @@ func (s *rpcServer) processEthSendTransaction(ctx context.Context, rpcReq *rpcba
return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeParseError), err
}

return s.processEthTransaction(ctx, rpcReq, &txn)
}

func (s *rpcServer) processEthTransaction(ctx context.Context, rpcReq *rpcbackend.RPCRequest, txn *ethsigner.Transaction) (*rpcbackend.RPCResponse, error) {

if txn.From == nil {
err := i18n.NewError(ctx, signermsgs.MsgMissingFrom)
return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInvalidRequest), err
Expand All @@ -94,7 +99,7 @@ func (s *rpcServer) processEthSendTransaction(ctx context.Context, rpcReq *rpcba

// Sign the transaction
var hexData ethtypes.HexBytes0xPrefix
hexData, err = s.wallet.Sign(ctx, &txn, s.chainID)
hexData, err := s.wallet.Sign(ctx, txn, s.chainID)
if err != nil {
return rpcbackend.RPCErrorResponse(err, rpcReq.ID, rpcbackend.RPCCodeInternalError), err
}
Expand Down
12 changes: 7 additions & 5 deletions internal/rpcserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ type Server interface {
Start() error
Stop()
WaitStop() error
Init() error
SignTransactionFromFile(ctx context.Context, filename string) error
}

func NewServer(ctx context.Context, wallet ethsigner.Wallet) (ss Server, err error) {
Expand Down Expand Up @@ -83,7 +85,7 @@ func (s *rpcServer) runAPIServer() {
s.apiServer.ServeHTTP(s.ctx)
}

func (s *rpcServer) Start() error {
func (s *rpcServer) Init() error {
if s.chainID < 0 {
var chainID ethtypes.HexInteger
rpcErr := s.backend.CallRPC(s.ctx, &chainID, "net_version")
Expand All @@ -93,10 +95,10 @@ func (s *rpcServer) Start() error {
s.chainID = chainID.BigInt().Int64()
}

err := s.wallet.Initialize(s.ctx)
if err != nil {
return err
}
return s.wallet.Initialize(s.ctx)
}

func (s *rpcServer) Start() error {
go s.runAPIServer()
s.started = true
return nil
Expand Down
8 changes: 5 additions & 3 deletions internal/rpcserver/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ func TestStartStop(t *testing.T) {

w := s.wallet.(*ethsignermocks.Wallet)
w.On("Initialize", mock.Anything).Return(nil)
err := s.Start()
err := s.Init()
assert.NoError(t, err)
err = s.Start()
assert.NoError(t, err)

assert.Equal(t, int64(12345), s.chainID)
Expand All @@ -105,7 +107,7 @@ func TestStartFailChainID(t *testing.T) {
hi.BigInt().SetInt64(12345)
}).Return(&rpcbackend.RPCError{Message: "pop"})

err := s.Start()
err := s.Init()
assert.Regexp(t, "pop", err)

}
Expand All @@ -123,7 +125,7 @@ func TestStartFailInitialize(t *testing.T) {

w := s.wallet.(*ethsignermocks.Wallet)
w.On("Initialize", mock.Anything).Return(fmt.Errorf("pop"))
err := s.Start()
err := s.Init()
assert.Regexp(t, "pop", err)

}
Expand Down
Loading
Loading