From 7d87d662eae91920496b6fd7dbc72f6a9fb1ebc0 Mon Sep 17 00:00:00 2001 From: Lorenz Herzberger <64837895+LaurentMontBlanc@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:02:28 +0200 Subject: [PATCH] feat: add shamir-coordinator-client for issuer wallet protection (#413) * feat: add shamir-coordinator-client for issuer wallet protection --------- Signed-off-by: Lorenz Herzberger --- clients/shamir_coordinator_client.go | 165 ++++++++++++++++++ config/config.go | 3 + .../mocks/shamir_coordinator_client_mock.go | 95 ++++++++++ testutil/network/loader.go | 13 +- util/elementsd_connector.go | 36 ---- util/elementsd_connector_test.go | 16 -- .../keeper/msg_server_distribution_request.go | 11 +- .../msg_server_reissue_rddl_proposal.go | 5 +- 8 files changed, 284 insertions(+), 60 deletions(-) create mode 100644 clients/shamir_coordinator_client.go create mode 100644 testutil/mocks/shamir_coordinator_client_mock.go diff --git a/clients/shamir_coordinator_client.go b/clients/shamir_coordinator_client.go new file mode 100644 index 0000000..e79b87c --- /dev/null +++ b/clients/shamir_coordinator_client.go @@ -0,0 +1,165 @@ +package clients + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + + "github.com/planetmint/planetmint-go/config" +) + +// TODO: revert to actual rddl-claim-service client after CosmosSDK upgrade to v0.50.x +// see https://github.com/planetmint/planetmint-go/issues/384 + +var ShamirCoordinatorServiceClient IShamirCoordinatorClient + +func lazyLoadShamirCoordinatorClient() IShamirCoordinatorClient { + if ShamirCoordinatorServiceClient != nil { + return ShamirCoordinatorServiceClient + } + cfg := config.GetConfig() + ShamirCoordinatorServiceClient = NewShamirCoordinatorClient(cfg.IssuerHost, &http.Client{}) + return ShamirCoordinatorServiceClient +} + +func SendTokens(ctx context.Context, recipient string, amount string, asset string) (txID string, err error) { + client := lazyLoadShamirCoordinatorClient() + res, err := client.SendTokens(ctx, recipient, amount, asset) + if err != nil { + return + } + return res.TxID, nil +} + +func ReIssueAsset(ctx context.Context, asset string, amount string) (txID string, err error) { + client := lazyLoadShamirCoordinatorClient() + res, err := client.ReIssueAsset(ctx, asset, amount) + if err != nil { + return + } + return res.TxID, nil +} + +type IShamirCoordinatorClient interface { + GetMnemonics(ctx context.Context) (res MnemonicsResponse, err error) + PostMnemonics(ctx context.Context, secret string) (err error) + SendTokens(ctx context.Context, recipient string, amount string, asset string) (res SendTokensResponse, err error) + ReIssueAsset(ctx context.Context, asset string, amount string) (res ReIssueResponse, err error) +} + +type SendTokensRequest struct { + Recipient string `binding:"required" json:"recipient"` + Amount string `binding:"required" json:"amount"` + Asset string `binding:"required" json:"asset"` +} + +type SendTokensResponse struct { + TxID string `binding:"required" json:"tx-id"` +} + +type ReIssueRequest struct { + Asset string `binding:"required" json:"asset"` + Amount string `binding:"required" json:"amount"` +} + +type ReIssueResponse struct { + TxID string `binding:"required" json:"tx-id"` +} + +type MnemonicsResponse struct { + Mnemonics []string `binding:"required" json:"mnemonics"` + Seed string `binding:"required" json:"seed"` +} + +type ShamirCoordinatorClient struct { + baseURL string + client *http.Client +} + +func NewShamirCoordinatorClient(baseURL string, client *http.Client) *ShamirCoordinatorClient { + if client == nil { + client = &http.Client{} + } + return &ShamirCoordinatorClient{ + baseURL: baseURL, + client: client, + } +} + +func (scc *ShamirCoordinatorClient) GetMnemonics(ctx context.Context) (res MnemonicsResponse, err error) { + err = scc.doRequest(ctx, http.MethodGet, scc.baseURL+"/mnemonics", nil, &res) + return +} + +func (scc *ShamirCoordinatorClient) PostMnemonics(ctx context.Context, secret string) (err error) { + err = scc.doRequest(ctx, http.MethodPost, scc.baseURL+"/mnemonics/"+url.PathEscape(secret), nil, nil) + return +} + +func (scc *ShamirCoordinatorClient) SendTokens(ctx context.Context, recipient string, amount string, asset string) (res SendTokensResponse, err error) { + requestBody := SendTokensRequest{ + Recipient: recipient, + Amount: amount, + Asset: asset, + } + err = scc.doRequest(ctx, http.MethodPost, scc.baseURL+"/send", &requestBody, &res) + return +} + +func (scc *ShamirCoordinatorClient) ReIssueAsset(ctx context.Context, asset string, amount string) (res ReIssueResponse, err error) { + requestBody := ReIssueRequest{ + Asset: asset, + Amount: amount, + } + err = scc.doRequest(ctx, http.MethodPost, scc.baseURL+"/reissue", &requestBody, &res) + return +} + +func (scc *ShamirCoordinatorClient) doRequest(ctx context.Context, method, url string, body interface{}, response interface{}) (err error) { + var bodyReader io.Reader + if body != nil { + bodyBytes, err := json.Marshal(body) + if err != nil { + return err + } + bodyReader = bytes.NewBuffer(bodyBytes) + } + + req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) + if err != nil { + return err + } + + if body != nil { + req.Header.Set("Content-Type", "application/json") + } + + resp, err := scc.client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + return &sccHTTPError{StatusCode: resp.StatusCode, Msg: strings.Join(resp.Header["Error"], "\n")} + } + + if response != nil { + return json.NewDecoder(resp.Body).Decode(response) + } + + return +} + +type sccHTTPError struct { + StatusCode int + Msg string +} + +func (e *sccHTTPError) Error() string { + return http.StatusText(e.StatusCode) + ": " + e.Msg +} diff --git a/config/config.go b/config/config.go index 9ba7e60..6786fb6 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,7 @@ mqtt-user = "{{ .PlmntConfig.MqttUser }}" mqtt-password = "{{ .PlmntConfig.MqttPassword }}" claim-host = "{{ .PlmntConfig.ClaimHost }}" mqtt-tls = {{ .PlmntConfig.MqttTLS }} +issuer-host = "{{ .PlmntConfig.IssuerHost }}" ` // Config defines Planetmint's top level configuration @@ -43,6 +44,7 @@ type Config struct { MqttPassword string `json:"mqtt-password" mapstructure:"mqtt-password"` ClaimHost string `json:"claim-host" mapstructure:"claim-host"` MqttTLS bool `json:"mqtt-tls" mapstructure:"mqtt-tls"` + IssuerHost string `json:"issuer-host" mapstructure:"issuer-host"` } // cosmos-sdk wide global singleton @@ -68,6 +70,7 @@ func DefaultConfig() *Config { MqttPassword: "password", ClaimHost: "https://testnet-p2r.rddl.io", MqttTLS: true, + IssuerHost: "https://testnet-issuer.rddl.io", } } diff --git a/testutil/mocks/shamir_coordinator_client_mock.go b/testutil/mocks/shamir_coordinator_client_mock.go new file mode 100644 index 0000000..57dc84f --- /dev/null +++ b/testutil/mocks/shamir_coordinator_client_mock.go @@ -0,0 +1,95 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./clients/shamir_coordinator_client.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" + clients "github.com/planetmint/planetmint-go/clients" +) + +// MockIShamirCoordinatorClient is a mock of IShamirCoordinatorClient interface. +type MockIShamirCoordinatorClient struct { + ctrl *gomock.Controller + recorder *MockIShamirCoordinatorClientMockRecorder +} + +// MockIShamirCoordinatorClientMockRecorder is the mock recorder for MockIShamirCoordinatorClient. +type MockIShamirCoordinatorClientMockRecorder struct { + mock *MockIShamirCoordinatorClient +} + +// NewMockIShamirCoordinatorClient creates a new mock instance. +func NewMockIShamirCoordinatorClient(ctrl *gomock.Controller) *MockIShamirCoordinatorClient { + mock := &MockIShamirCoordinatorClient{ctrl: ctrl} + mock.recorder = &MockIShamirCoordinatorClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIShamirCoordinatorClient) EXPECT() *MockIShamirCoordinatorClientMockRecorder { + return m.recorder +} + +// GetMnemonics mocks base method. +func (m *MockIShamirCoordinatorClient) GetMnemonics(ctx context.Context) (clients.MnemonicsResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetMnemonics", ctx) + ret0, _ := ret[0].(clients.MnemonicsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMnemonics indicates an expected call of GetMnemonics. +func (mr *MockIShamirCoordinatorClientMockRecorder) GetMnemonics(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMnemonics", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).GetMnemonics), ctx) +} + +// PostMnemonics mocks base method. +func (m *MockIShamirCoordinatorClient) PostMnemonics(ctx context.Context, secret string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostMnemonics", ctx, secret) + ret0, _ := ret[0].(error) + return ret0 +} + +// PostMnemonics indicates an expected call of PostMnemonics. +func (mr *MockIShamirCoordinatorClientMockRecorder) PostMnemonics(ctx, secret interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostMnemonics", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).PostMnemonics), ctx, secret) +} + +// ReIssueAsset mocks base method. +func (m *MockIShamirCoordinatorClient) ReIssueAsset(ctx context.Context, asset, amount string) (clients.ReIssueResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReIssueAsset", ctx, asset, amount) + ret0, _ := ret[0].(clients.ReIssueResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReIssueAsset indicates an expected call of ReIssueAsset. +func (mr *MockIShamirCoordinatorClientMockRecorder) ReIssueAsset(ctx, asset, amount interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReIssueAsset", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).ReIssueAsset), ctx, asset, amount) +} + +// SendTokens mocks base method. +func (m *MockIShamirCoordinatorClient) SendTokens(ctx context.Context, recipient, amount, asset string) (clients.SendTokensResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendTokens", ctx, recipient, amount, asset) + ret0, _ := ret[0].(clients.SendTokensResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SendTokens indicates an expected call of SendTokens. +func (mr *MockIShamirCoordinatorClientMockRecorder) SendTokens(ctx, recipient, amount, asset interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendTokens", reflect.TypeOf((*MockIShamirCoordinatorClient)(nil).SendTokens), ctx, recipient, amount, asset) +} diff --git a/testutil/network/loader.go b/testutil/network/loader.go index d9695e9..a4db58b 100644 --- a/testutil/network/loader.go +++ b/testutil/network/loader.go @@ -19,7 +19,7 @@ import ( "github.com/planetmint/planetmint-go/clients" "github.com/planetmint/planetmint-go/monitor" monitormocks "github.com/planetmint/planetmint-go/monitor/mocks" - claimmocks "github.com/planetmint/planetmint-go/testutil/mocks" + clientmocks "github.com/planetmint/planetmint-go/testutil/mocks" "github.com/planetmint/planetmint-go/testutil/sample" "github.com/planetmint/planetmint-go/util" "github.com/planetmint/planetmint-go/util/mocks" @@ -49,12 +49,21 @@ func Load(t *testing.T, configs ...Config) *Network { elements.Client = &elementsmocks.MockClient{} util.RegisterAssetServiceHTTPClient = &mocks.MockClient{} ctrl := gomock.NewController(t) - claimMock := claimmocks.NewMockIRCClient(ctrl) + claimMock := clientmocks.NewMockIRCClient(ctrl) claimMock.EXPECT().PostClaim(gomock.Any(), gomock.Any()).AnyTimes().Return(clients.PostClaimResponse{ TxID: "0000000000000000000000000000000000000000000000000000000000000000", }, nil) clients.ClaimServiceClient = claimMock + shamirMock := clientmocks.NewMockIShamirCoordinatorClient(ctrl) + shamirMock.EXPECT().SendTokens(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(clients.SendTokensResponse{ + TxID: "7add40beb27df701e02ee85089c5bc0021bc813823fedb5f1dcb5debda7f3da9", + }, nil) + shamirMock.EXPECT().ReIssueAsset(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().Return(clients.ReIssueResponse{ + TxID: "7add40beb27df701e02ee85089c5bc0021bc813823fedb5f1dcb5debda7f3da9", + }, nil) + clients.ShamirCoordinatorServiceClient = shamirMock + // enable application logger in tests appLogger := util.GetAppLogger() appLogger.SetTestingLogger(t) diff --git a/util/elementsd_connector.go b/util/elementsd_connector.go index c10931f..6fd5c61 100644 --- a/util/elementsd_connector.go +++ b/util/elementsd_connector.go @@ -4,7 +4,6 @@ import ( "crypto/sha256" "encoding/json" "fmt" - "strings" "sync" "github.com/planetmint/planetmint-go/config" @@ -18,41 +17,6 @@ var ( elementsSyncAccess sync.Mutex ) -func ReissueAsset(reissueTx string) (txID string, err error) { - conf := config.GetConfig() - url := conf.GetRPCURL() - cmdArgs := strings.Split(reissueTx, " ") - elementsSyncAccess.Lock() - defer elementsSyncAccess.Unlock() - result, err := elements.ReissueAsset(url, []string{`"` + cmdArgs[1] + `"`, cmdArgs[2]}) - if err != nil { - return - } - txID = result.TxID - return -} - -func DistributeAsset(address string, amount string, reissuanceAsset string) (txID string, err error) { - conf := config.GetConfig() - url := conf.GetRPCURL() - - elementsSyncAccess.Lock() - defer elementsSyncAccess.Unlock() - txID, err = elements.SendToAddress(url, []string{ - `"` + address + `"`, - `"` + amount + `"`, - `""`, - `""`, - "false", - "true", - "null", - `"unset"`, - "false", - `"` + reissuanceAsset + `"`, - }) - return -} - func IssueNFTAsset(name string, machineAddress string, domain string) (assetID string, contract string, hexTx string, err error) { conf := config.GetConfig() url := conf.GetRPCURL() diff --git a/util/elementsd_connector_test.go b/util/elementsd_connector_test.go index 0a53789..7919071 100644 --- a/util/elementsd_connector_test.go +++ b/util/elementsd_connector_test.go @@ -15,22 +15,6 @@ import ( "github.com/stretchr/testify/assert" ) -func TestReissueAsset(t *testing.T) { - elements.Client = &elementsmocks.MockClient{} - _, err := util.ReissueAsset("reissueasset 06c20c8de513527f1ae6c901f74a05126525ac2d7e89306f4a7fd5ec4e674403 900.000") - assert.NoError(t, err) -} - -func TestDistributeAsset(t *testing.T) { - elements.Client = &elementsmocks.MockClient{} - - _, err := util.DistributeAsset( - "tlq1qqt5078sef4aqls29c3j3pwfmukgjug70t37x26gwyhzpdxmtmjmphar88fwsl9qcm559jevve772prhtuyf9xkxdtrhvuce6a", - "20", - "06c20c8de513527f1ae6c901f74a05126525ac2d7e89306f4a7fd5ec4e674403") - assert.NoError(t, err) -} - func TestIssueNFTAsset(t *testing.T) { elements.Client = &elementsmocks.MockClient{} diff --git a/x/dao/keeper/msg_server_distribution_request.go b/x/dao/keeper/msg_server_distribution_request.go index 61c4e5b..65d83b8 100644 --- a/x/dao/keeper/msg_server_distribution_request.go +++ b/x/dao/keeper/msg_server_distribution_request.go @@ -5,6 +5,7 @@ import ( errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/planetmint/planetmint-go/clients" "github.com/planetmint/planetmint-go/errormsg" "github.com/planetmint/planetmint-go/util" "github.com/planetmint/planetmint-go/x/dao/types" @@ -42,23 +43,23 @@ func (k msgServer) DistributionRequest(goCtx context.Context, msg *types.MsgDist reissuanceAsset := k.GetParams(ctx).ReissuanceAsset util.GetAppLogger().Info(ctx, distributionRequestTag+"entering asset distribution mode") // issue 5 distributions: - earlyInvestorTx, err := util.DistributeAsset(msg.Distribution.EarlyInvAddr, msg.Distribution.EarlyInvAmount, reissuanceAsset) + earlyInvestorTx, err := clients.SendTokens(goCtx, msg.Distribution.EarlyInvAddr, msg.Distribution.EarlyInvAmount, reissuanceAsset) if err != nil { util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to early investors: "+err.Error()) } - investorTx, err := util.DistributeAsset(msg.Distribution.InvestorAddr, msg.Distribution.InvestorAmount, reissuanceAsset) + investorTx, err := clients.SendTokens(goCtx, msg.Distribution.InvestorAddr, msg.Distribution.InvestorAmount, reissuanceAsset) if err != nil { util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to investors: "+err.Error()) } - strategicTx, err := util.DistributeAsset(msg.Distribution.StrategicAddr, msg.Distribution.StrategicAmount, reissuanceAsset) + strategicTx, err := clients.SendTokens(goCtx, msg.Distribution.StrategicAddr, msg.Distribution.StrategicAmount, reissuanceAsset) if err != nil { util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to strategic investments: "+err.Error()) } - popTx, err := util.DistributeAsset(msg.Distribution.PopAddr, msg.Distribution.PopAmount, reissuanceAsset) + popTx, err := clients.SendTokens(goCtx, msg.Distribution.PopAddr, msg.Distribution.PopAmount, reissuanceAsset) if err != nil { util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to PoP: "+err.Error()) } - daoTx, err := util.DistributeAsset(msg.Distribution.DaoAddr, msg.Distribution.DaoAmount, reissuanceAsset) + daoTx, err := clients.SendTokens(goCtx, msg.Distribution.DaoAddr, msg.Distribution.DaoAmount, reissuanceAsset) if err != nil { util.GetAppLogger().Error(ctx, distributionRequestTag+"could not distribute asset to DAO: "+err.Error()) } diff --git a/x/dao/keeper/msg_server_reissue_rddl_proposal.go b/x/dao/keeper/msg_server_reissue_rddl_proposal.go index 6f0a1f1..5107e6c 100644 --- a/x/dao/keeper/msg_server_reissue_rddl_proposal.go +++ b/x/dao/keeper/msg_server_reissue_rddl_proposal.go @@ -2,9 +2,11 @@ package keeper import ( "context" + "strings" errorsmod "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/planetmint/planetmint-go/clients" "github.com/planetmint/planetmint-go/errormsg" "github.com/planetmint/planetmint-go/util" "github.com/planetmint/planetmint-go/x/dao/types" @@ -41,7 +43,8 @@ func (k msgServer) ReissueRDDLProposal(goCtx context.Context, msg *types.MsgReis } util.GetAppLogger().Info(ctx, reissueTag+"asset: "+msg.GetCommand()) - txID, err := util.ReissueAsset(msg.Command) + cmdArgs := strings.Split(msg.Command, " ") + txID, err := clients.ReIssueAsset(goCtx, cmdArgs[1], cmdArgs[2]) if err != nil { util.GetAppLogger().Error(ctx, reissueTag+"asset reissuance failed: "+err.Error()) }