From bc48206353ebe5c70e3049ca889cbfac93219717 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Mon, 19 Aug 2024 15:29:00 -0400 Subject: [PATCH] noto: Add functioning states for mint Signed-off-by: Andrew Richardson --- domains/noto/build.gradle | 4 +- domains/noto/internal/noto/e2e_noto_test.go | 22 +-- domains/noto/internal/noto/noto.go | 189 ++++++++++++++++++-- domains/noto/internal/noto/replytracker.go | 122 +++++++++++++ solidity/contracts/noto/Noto.sol | 11 +- 5 files changed, 311 insertions(+), 37 deletions(-) create mode 100644 domains/noto/internal/noto/replytracker.go diff --git a/domains/noto/build.gradle b/domains/noto/build.gradle index 794057311..2763b381f 100644 --- a/domains/noto/build.gradle +++ b/domains/noto/build.gradle @@ -74,12 +74,14 @@ task test(type: Exec) { dependsOn copySolidity dependsOn startTestbed + finalizedBy stopTestbed ignoreExitValue true doLast { + String composeLogs = getProject().findProperty('composeLogs') ExecResult execResult = executionResult.get() - if (execResult.exitValue != 0) { + if (execResult.exitValue != 0 && composeLogs == "true") { startTestbed.dumpLogs("testbed") } execResult.assertNormalExitValue() diff --git a/domains/noto/internal/noto/e2e_noto_test.go b/domains/noto/internal/noto/e2e_noto_test.go index 70bedb2be..f919b2eb4 100644 --- a/domains/noto/internal/noto/e2e_noto_test.go +++ b/domains/noto/internal/noto/e2e_noto_test.go @@ -17,6 +17,7 @@ package noto import ( "context" + "fmt" "testing" "time" @@ -75,26 +76,27 @@ func TestNoto(t *testing.T) { log.L(ctx).Infof("Calling testbed_deploy") var deployResult ethtypes.Address0xHex rpcerr = rpc.CallRPC(callCtx, &deployResult, "testbed_deploy", - "noto", &NotoConstructor{Notary: notaryName}) + "noto", &NotoConstructorParams{Notary: notaryName}) if rpcerr != nil { assert.NoError(t, rpcerr.Error()) } log.L(ctx).Infof("Calling testbed_invoke") - transfer := findMethod(domain.Contract.ABI, "transfer") - assert.NotNil(t, transfer) rpcerr = rpc.CallRPC(callCtx, &boolResult, "testbed_invoke", &types.PrivateContractInvoke{ From: notaryName, To: types.EthAddress(deployResult), - Function: *transfer, - Inputs: types.RawJSON(`{ - "inputs": [], - "outputs": [], - "signature": "0x", - "data": "0x" - }`), + Function: *NotoMintABI, + Inputs: types.RawJSON(fmt.Sprintf(`{ + "to": "%s", + "amount": "100" + }`, notaryName)), }) if rpcerr != nil { assert.NoError(t, rpcerr.Error()) } + + coins, err := domain.FindCoins(ctx, "{}") + assert.NoError(t, err) + assert.Len(t, coins, 1) + assert.Equal(t, "100", coins[0].Amount) } diff --git a/domains/noto/internal/noto/noto.go b/domains/noto/internal/noto/noto.go index 8ef078677..9d269373e 100644 --- a/domains/noto/internal/noto/noto.go +++ b/domains/noto/internal/noto/noto.go @@ -60,19 +60,52 @@ type Noto struct { stream pb.KataMessageService_ListenClient stopListener context.CancelFunc done chan bool + domainID string + coinSchema *pb.StateSchema + replies *replyTracker } -type NotoConstructor struct { +type NotoConstructorParams struct { Notary string `json:"notary"` } var NotoConstructorABI = &abi.Entry{ - Type: "constructor", + Type: abi.Constructor, Inputs: abi.ParameterArray{ {Name: "notary", Type: "string"}, }, } +type NotoMintParams struct { + To string `json:"to"` + Amount string `json:"amount"` +} + +var NotoMintABI = &abi.Entry{ + Name: "mint", + Type: abi.Function, + Inputs: abi.ParameterArray{ + {Name: "to", Type: "string"}, + {Name: "amount", Type: "uint256"}, + }, +} + +var NotoTransferABI = &abi.Entry{ + Name: "transfer", + Type: abi.Function, + Inputs: abi.ParameterArray{ + {Name: "from", Type: "string"}, + {Name: "to", Type: "string"}, + {Name: "amount", Type: "uint256"}, + }, +} + +var NotoABI = &abi.ABI{ + NotoConstructorABI, + NotoMintABI, + NotoTransferABI, +} + type NotoDomainConfig struct { Notary string `json:"notary"` } @@ -81,6 +114,22 @@ var NotoDomainConfigABI = &abi.ParameterArray{ {Name: "notary", Type: "address"}, } +type NotoCoin struct { + Salt string `json:"salt"` + Owner string `json:"owner"` + Amount string `json:"amount"` +} + +var NotoCoinABI = &abi.Parameter{ + Type: "tuple", + InternalType: "struct NotoCoin", + Components: abi.ParameterArray{ + {Name: "salt", Type: "bytes32"}, + {Name: "owner", Type: "string"}, + {Name: "amount", Type: "uint256"}, + }, +} + func loadBuild(buildOutput []byte) SolidityBuild { var build SolidityBuild err := json.Unmarshal(buildOutput, &build) @@ -121,6 +170,10 @@ func New(ctx context.Context, addr string) (*Noto, error) { Factory: loadBuild(notoFactoryJSON), Contract: contract, } + d.replies = &replyTracker{ + inflight: make(map[string]*inflightRequest), + client: d.client, + } return d, d.waitForReady(ctx) } @@ -218,6 +271,12 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { return err } + inflight := d.replies.getInflight(message.CorrelationId) + if inflight != nil { + inflight.done <- message + return nil + } + switch m := body.(type) { case *pb.ConfigureDomainRequest: log.L(ctx).Infof("Received ConfigureDomainRequest") @@ -239,6 +298,10 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { if err != nil { return err } + schemaJSON, err := json.Marshal(NotoCoinABI) + if err != nil { + return err + } response := &pb.ConfigureDomainResponse{ DomainConfig: &pb.DomainConfig{ @@ -246,20 +309,21 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { FactoryContractAbiJson: string(factoryJSON), PrivateContractAbiJson: string(notoJSON), ConstructorAbiJson: string(constructorJSON), - AbiStateSchemasJson: []string{}, + AbiStateSchemasJson: []string{string(schemaJSON)}, }, } return d.sendReply(ctx, message, response) case *pb.InitDomainRequest: log.L(ctx).Infof("Received InitDomainRequest") - response := &pb.InitDomainResponse{} - return d.sendReply(ctx, message, response) + d.domainID = m.DomainUuid + d.coinSchema = m.AbiStateSchemas[0] + return d.sendReply(ctx, message, &pb.InitDomainResponse{}) case *pb.InitDeployTransactionRequest: log.L(ctx).Infof("Received InitDeployTransactionRequest") - var params NotoConstructor + var params NotoConstructorParams err := yaml.Unmarshal([]byte(m.Transaction.ConstructorParamsJson), ¶ms) if err != nil { return err @@ -279,7 +343,7 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { case *pb.PrepareDeployTransactionRequest: log.L(ctx).Infof("Received PrepareDeployTransactionRequest") - var params NotoConstructor + var params NotoConstructorParams err := yaml.Unmarshal([]byte(m.Transaction.ConstructorParamsJson), ¶ms) if err != nil { return err @@ -311,16 +375,24 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { case *pb.AssembleTransactionRequest: log.L(ctx).Infof("Received AssembleTransactionRequest") - configValues, err := NotoDomainConfigABI.DecodeABIDataCtx(ctx, m.Transaction.ContractConfig, 0) + var functionABI abi.Entry + err := json.Unmarshal([]byte(m.Transaction.FunctionAbiJson), &functionABI) if err != nil { return err } - configJSON, err := types.StandardABISerializer().SerializeJSON(configValues) + + var assembled *pb.AssembledTransaction + switch functionABI.Name { + case "mint": + assembled, err = d.assembleMint(m.Transaction.FunctionParamsJson) + default: + err = fmt.Errorf("Unsupported method: %s", functionABI.Name) + } if err != nil { return err } - var config NotoDomainConfig - err = json.Unmarshal(configJSON, &config) + + _, err = d.decodeDomainConfig(ctx, m.Transaction.ContractConfig) if err != nil { return err } @@ -328,7 +400,7 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { response := &pb.AssembleTransactionResponse{ AssemblyResult: pb.AssembleTransactionResponse_OK, - AssembledTransaction: &pb.AssembledTransaction{}, + AssembledTransaction: assembled, AttestationPlan: []*pb.AttestationRequest{ { Name: "signer", @@ -353,15 +425,30 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { case *pb.PrepareTransactionRequest: log.L(ctx).Infof("Received PrepareTransactionRequest") + inputs := make([]string, len(m.Transaction.SpentStates)) + for i, state := range m.Transaction.SpentStates { + inputs[i] = state.HashId + } + outputs := make([]string, len(m.Transaction.NewStates)) + for i, state := range m.Transaction.NewStates { + outputs[i] = state.HashId + } + + params := map[string]interface{}{ + "inputs": inputs, + "outputs": outputs, + "signature": "0x", + "data": m.Transaction.TransactionId, + } + paramsJSON, err := json.Marshal(params) + if err != nil { + return err + } + response := &pb.PrepareTransactionResponse{ Transaction: &pb.BaseLedgerTransaction{ - FunctionName: "transfer", - ParamsJson: `{ - "inputs": [], - "outputs": [], - "signature": "0x", - "data": "0x" - }`, + FunctionName: "transfer", // TODO: can we have more than one method on base ledger? + ParamsJson: string(paramsJSON), }, } return d.sendReply(ctx, message, response) @@ -375,3 +462,67 @@ func (d *Noto) handleMessage(ctx context.Context, message *pb.Message) error { return nil } } + +func (d *Noto) decodeDomainConfig(ctx context.Context, domainConfig []byte) (*NotoDomainConfig, error) { + configValues, err := NotoDomainConfigABI.DecodeABIDataCtx(ctx, domainConfig, 0) + if err != nil { + return nil, err + } + configJSON, err := types.StandardABISerializer().SerializeJSON(configValues) + if err != nil { + return nil, err + } + var config NotoDomainConfig + err = json.Unmarshal(configJSON, &config) + return &config, err +} + +func (d *Noto) assembleMint(params string) (*pb.AssembledTransaction, error) { + var functionParams NotoMintParams + err := json.Unmarshal([]byte(params), &functionParams) + if err != nil { + return nil, err + } + + newCoin := &NotoCoin{ + Salt: types.RandHex(32), + Owner: functionParams.To, + Amount: functionParams.Amount, + } + newCoinJSON, err := json.Marshal(newCoin) + if err != nil { + return nil, err + } + + return &pb.AssembledTransaction{ + NewStates: []*pb.NewState{ + { + SchemaId: d.coinSchema.Id, + StateDataJson: string(newCoinJSON), + }, + }, + }, nil +} + +func (d *Noto) FindCoins(ctx context.Context, query string) ([]*NotoCoin, error) { + req := &pb.FindAvailableStatesRequest{ + DomainUuid: d.domainID, + SchemaId: d.coinSchema.Id, + QueryJson: query, + } + + res := &pb.FindAvailableStatesResponse{} + err := requestReply(ctx, d.replies, "from-domain", *d.dest, req, &res) + if err != nil { + return nil, err + } + + coins := make([]*NotoCoin, len(res.States)) + for i, state := range res.States { + coins[i] = &NotoCoin{} + if err := json.Unmarshal([]byte(state.DataJson), &coins[i]); err != nil { + return nil, err + } + } + return coins, err +} diff --git a/domains/noto/internal/noto/replytracker.go b/domains/noto/internal/noto/replytracker.go new file mode 100644 index 000000000..19f3eff7c --- /dev/null +++ b/domains/noto/internal/noto/replytracker.go @@ -0,0 +1,122 @@ +/* + * Copyright © 2024 Kaleido, 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package noto + +import ( + "context" + "fmt" + "sync" + "time" + + "github.com/google/uuid" + "github.com/hyperledger/firefly-common/pkg/log" + pb "github.com/kaleido-io/paladin/kata/pkg/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/known/anypb" +) + +type inflightRequest struct { + req *pb.Message + queued time.Time + done chan *pb.Message +} + +type replyTracker struct { + inflight map[string]*inflightRequest + inflightLock sync.Mutex + client pb.KataMessageServiceClient +} + +func (tb *replyTracker) addInflight(ctx context.Context, msg *pb.Message) *inflightRequest { + inFlight := &inflightRequest{ + req: msg, + queued: time.Now(), + done: make(chan *pb.Message, 1), + } + log.L(ctx).Infof("--> %s [%s]", msg.Id, msg.Body.ProtoReflect().Descriptor().FullName()) + tb.inflightLock.Lock() + defer tb.inflightLock.Unlock() + tb.inflight[msg.Id] = inFlight + return inFlight +} + +func (tb *replyTracker) getInflight(correlID *string) *inflightRequest { + if correlID == nil { + return nil + } + tb.inflightLock.Lock() + defer tb.inflightLock.Unlock() + return tb.inflight[*correlID] +} + +func (tb *replyTracker) waitInFlight(ctx context.Context, inFlight *inflightRequest) (*pb.Message, error) { + select { + case <-ctx.Done(): + log.L(ctx).Errorf("