diff --git a/domains/noto/build.gradle b/domains/noto/build.gradle index 857ba80ab..b0f3798f8 100644 --- a/domains/noto/build.gradle +++ b/domains/noto/build.gradle @@ -53,7 +53,6 @@ task copySolidity(type: Copy) { from fileTree(configurations.contractCompile.asPath) { include 'contracts/domains/noto/NotoFactory.sol/NotoFactory.json' include 'contracts/domains/noto/Noto.sol/Noto.json' - include 'contracts/domains/noto/NotoSelfSubmitFactory.sol/NotoSelfSubmitFactory.json' include 'contracts/domains/noto/NotoSelfSubmit.sol/NotoSelfSubmit.json' } into 'internal/noto/abis' diff --git a/domains/noto/go.mod b/domains/noto/go.mod index 1144dde45..f90e3b95e 100644 --- a/domains/noto/go.mod +++ b/domains/noto/go.mod @@ -4,7 +4,6 @@ go 1.22.5 require ( github.com/go-resty/resty/v2 v2.14.0 - github.com/hyperledger/firefly-common v1.4.8 github.com/hyperledger/firefly-signer v1.1.14 github.com/kaleido-io/paladin/core v0.0.0-00010101000000-000000000000 github.com/kaleido-io/paladin/toolkit v0.0.0-00010101000000-000000000000 @@ -37,6 +36,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hyperledger-labs/zeto/go-sdk v0.0.0-20240905213624-43a614759076 // indirect + github.com/hyperledger/firefly-common v1.4.8 // indirect github.com/iden3/go-iden3-crypto v0.0.16 // indirect github.com/iden3/go-rapidsnark/prover v0.0.10 // indirect github.com/iden3/go-rapidsnark/types v0.0.2 // indirect diff --git a/domains/noto/internal/noto/e2e_noto_test.go b/domains/noto/internal/noto/e2e_noto_test.go index 19b1fb7d9..fe65593af 100644 --- a/domains/noto/internal/noto/e2e_noto_test.go +++ b/domains/noto/internal/noto/e2e_noto_test.go @@ -22,12 +22,16 @@ import ( "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-signer/pkg/abi" "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-signer/pkg/rpcbackend" + "github.com/kaleido-io/paladin/core/pkg/blockindexer" + "github.com/kaleido-io/paladin/core/pkg/ethclient" "github.com/kaleido-io/paladin/core/pkg/testbed" "github.com/kaleido-io/paladin/domains/noto/pkg/types" + "github.com/kaleido-io/paladin/toolkit/pkg/algorithms" "github.com/kaleido-io/paladin/toolkit/pkg/domain" + "github.com/kaleido-io/paladin/toolkit/pkg/log" "github.com/kaleido-io/paladin/toolkit/pkg/plugintk" "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" "github.com/stretchr/testify/assert" @@ -46,7 +50,7 @@ func toJSON(t *testing.T, v any) []byte { return result } -func mapConfig(t *testing.T, config *types.Config) (m map[string]any) { +func mapConfig(t *testing.T, config *types.DomainConfig) (m map[string]any) { configJSON, err := json.Marshal(&config) require.NoError(t, err) err = json.Unmarshal(configJSON, &m) @@ -75,22 +79,38 @@ func deployContracts(ctx context.Context, t *testing.T, contracts map[string][]b return deployed } -func newTestDomain(t *testing.T, domainName string, config *types.Config) (context.CancelFunc, *Noto, rpcbackend.Backend) { - var domain *Noto +func newNotoDomain(t *testing.T, config *types.DomainConfig) (*Noto, *testbed.TestbedDomain) { + var domain Noto + return &domain, &testbed.TestbedDomain{ + Config: mapConfig(t, config), + Plugin: plugintk.NewDomain(func(callbacks plugintk.DomainCallbacks) plugintk.DomainAPI { + domain.Callbacks = callbacks + return &domain + }), + } +} + +func newTestbed(t *testing.T, domains map[string]*testbed.TestbedDomain) (context.CancelFunc, testbed.Testbed, rpcbackend.Backend) { tb := testbed.NewTestBed() - plugin := plugintk.NewDomain(func(callbacks plugintk.DomainCallbacks) plugintk.DomainAPI { - domain = &Noto{Callbacks: callbacks} - return domain - }) - url, done, err := tb.StartForTest("../../testbed.config.yaml", map[string]*testbed.TestbedDomain{ - domainName: { - Config: mapConfig(t, config), - Plugin: plugin, - }, - }) - require.NoError(t, err) + url, done, err := tb.StartForTest("../../testbed.config.yaml", domains) + assert.NoError(t, err) rpc := rpcbackend.NewRPCClient(resty.New().SetBaseURL(url)) - return done, domain, rpc + return done, tb, rpc +} + +func functionBuilder(ctx context.Context, t *testing.T, eth ethclient.EthClient, abi abi.ABI, functionName string) ethclient.ABIFunctionRequestBuilder { + abiClient, err := eth.ABI(ctx, abi) + assert.NoError(t, err) + fn, err := abiClient.Function(ctx, functionName) + assert.NoError(t, err) + return fn.R(ctx) +} + +func waitFor(ctx context.Context, t *testing.T, tb testbed.Testbed, txHash *tktypes.Bytes32, err error) *blockindexer.IndexedTransaction { + require.NoError(t, err) + tx, err := tb.Components().BlockIndexer().WaitForTransaction(ctx, *txHash) + assert.NoError(t, err) + return tx } func TestNoto(t *testing.T) { @@ -108,15 +128,25 @@ func TestNoto(t *testing.T) { log.L(ctx).Infof("%s deployed to %s", name, address) } - done, noto, rpc := newTestDomain(t, domainName, &types.Config{ + noto, notoTestbed := newNotoDomain(t, &types.DomainConfig{ FactoryAddress: contracts["factory"], }) + done, tb, rpc := newTestbed(t, map[string]*testbed.TestbedDomain{ + domainName: notoTestbed, + }) defer done() + _, notaryKey, err := tb.Components().KeyManager().ResolveKey(ctx, notaryName, algorithms.ECDSA_SECP256K1_PLAINBYTES) + require.NoError(t, err) + _, recipient1Key, err := tb.Components().KeyManager().ResolveKey(ctx, recipient1Name, algorithms.ECDSA_SECP256K1_PLAINBYTES) + require.NoError(t, err) + log.L(ctx).Infof("Deploying an instance of Noto") var notoAddress ethtypes.Address0xHex rpcerr := rpc.CallRPC(ctx, ¬oAddress, "testbed_deploy", - domainName, &types.ConstructorParams{Notary: notaryName}) + domainName, &types.ConstructorParams{ + Notary: notaryName, + }) if rpcerr != nil { require.NoError(t, rpcerr.Error()) } @@ -142,7 +172,7 @@ func TestNoto(t *testing.T) { require.NoError(t, err) require.Len(t, coins, 1) assert.Equal(t, int64(100), coins[0].Amount.Int64()) - assert.Equal(t, notaryName, coins[0].Owner) + assert.Equal(t, notaryKey, coins[0].Owner.String()) log.L(ctx).Infof("Attempt mint from non-notary (should fail)") rpcerr = rpc.CallRPC(ctx, &boolResult, "testbed_invoke", &tktypes.PrivateContractInvoke{ @@ -192,13 +222,13 @@ func TestNoto(t *testing.T) { // This should have been spent // TODO: why does it still exist? assert.Equal(t, int64(100), coins[0].Amount.Int64()) - assert.Equal(t, notaryName, coins[0].Owner) + assert.Equal(t, notaryKey, coins[0].Owner.String()) // These are the expected coins after the transfer assert.Equal(t, int64(50), coins[1].Amount.Int64()) - assert.Equal(t, recipient1Name, coins[1].Owner) + assert.Equal(t, recipient1Key, coins[1].Owner.String()) assert.Equal(t, int64(50), coins[2].Amount.Int64()) - assert.Equal(t, notaryName, coins[2].Owner) + assert.Equal(t, notaryKey, coins[2].Owner.String()) log.L(ctx).Infof("Transfer 50 from recipient1 to recipient2") rpcerr = rpc.CallRPC(ctx, &boolResult, "testbed_invoke", &tktypes.PrivateContractInvoke{ @@ -227,23 +257,49 @@ func TestNotoSelfSubmit(t *testing.T) { log.L(ctx).Infof("Deploying Noto factory") contractSource := map[string][]byte{ - "factory": notoSelfSubmitFactoryJSON, + "factory": notoFactoryJSON, + "noto": notoSelfSubmitJSON, } contracts := deployContracts(ctx, t, contractSource) for name, address := range contracts { log.L(ctx).Infof("%s deployed to %s", name, address) } - done, noto, rpc := newTestDomain(t, domainName, &types.Config{ - FactoryAddress: contracts["factory"], - Variant: "NotoSelfSubmit", + factoryAddress, err := ethtypes.NewAddress(contracts["factory"]) + require.NoError(t, err) + + noto, notoTestbed := newNotoDomain(t, &types.DomainConfig{ + FactoryAddress: factoryAddress.String(), + }) + done, tb, rpc := newTestbed(t, map[string]*testbed.TestbedDomain{ + domainName: notoTestbed, }) defer done() + _, notaryKey, err := tb.Components().KeyManager().ResolveKey(ctx, notaryName, algorithms.ECDSA_SECP256K1_PLAINBYTES) + require.NoError(t, err) + + eth := tb.Components().EthClientFactory().HTTPClient() + notoFactory := domain.LoadBuild(notoFactoryJSON) + txHash, err := functionBuilder(ctx, t, eth, notoFactory.ABI, "registerImplementation"). + Signer(notaryName). + To(factoryAddress). + Input(map[string]any{ + "name": "selfsubmit", + "implementation": contracts["noto"], + }). + SignAndSend() + require.NoError(t, err) + waitFor(ctx, t, tb, txHash, err) + log.L(ctx).Infof("Deploying an instance of Noto") var notoAddress ethtypes.Address0xHex rpcerr := rpc.CallRPC(ctx, ¬oAddress, "testbed_deploy", - domainName, &types.ConstructorParams{Notary: notaryName}) + domainName, &types.ConstructorParams{ + Notary: notaryName, + Implementation: "selfsubmit", + }, + ) if rpcerr != nil { require.NoError(t, rpcerr.Error()) } @@ -269,7 +325,7 @@ func TestNotoSelfSubmit(t *testing.T) { require.NoError(t, err) assert.Len(t, coins, 1) assert.Equal(t, int64(100), coins[0].Amount.Int64()) - assert.Equal(t, notaryName, coins[0].Owner) + assert.Equal(t, notaryKey, coins[0].Owner.String()) log.L(ctx).Infof("Transfer 50 from notary to recipient1") rpcerr = rpc.CallRPC(ctx, &boolResult, "testbed_invoke", &tktypes.PrivateContractInvoke{ diff --git a/domains/noto/internal/noto/handler_mint.go b/domains/noto/internal/noto/handler_mint.go index c91b6104d..0425f4200 100644 --- a/domains/noto/internal/noto/handler_mint.go +++ b/domains/noto/internal/noto/handler_mint.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/kaleido-io/paladin/domains/noto/pkg/types" "github.com/kaleido-io/paladin/toolkit/pkg/algorithms" "github.com/kaleido-io/paladin/toolkit/pkg/domain" @@ -45,6 +46,8 @@ func (h *mintHandler) ValidateParams(ctx context.Context, params string) (interf } func (h *mintHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req *pb.InitTransactionRequest) (*pb.InitTransactionResponse, error) { + params := tx.Params.(*types.MintParams) + if req.Transaction.From != tx.DomainConfig.NotaryLookup { return nil, fmt.Errorf("mint can only be initiated by notary") } @@ -54,7 +57,10 @@ func (h *mintHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req Lookup: tx.DomainConfig.NotaryLookup, Algorithm: algorithms.ECDSA_SECP256K1_PLAINBYTES, }, - // TODO: should we also resolve "To" party? + { + Lookup: params.To, + Algorithm: algorithms.ECDSA_SECP256K1_PLAINBYTES, + }, }, }, nil } @@ -62,13 +68,20 @@ func (h *mintHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req func (h *mintHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, req *pb.AssembleTransactionRequest) (*pb.AssembleTransactionResponse, error) { params := tx.Params.(*types.MintParams) - notary := domain.FindVerifier(tx.DomainConfig.NotaryLookup, req.ResolvedVerifiers) + notary := domain.FindVerifier(tx.DomainConfig.NotaryLookup, algorithms.ECDSA_SECP256K1_PLAINBYTES, req.ResolvedVerifiers) if notary == nil || notary.Verifier != tx.DomainConfig.NotaryAddress { - // TODO: do we need to verify every time? return nil, fmt.Errorf("notary resolved to unexpected address") } + to := domain.FindVerifier(params.To, algorithms.ECDSA_SECP256K1_PLAINBYTES, req.ResolvedVerifiers) + if to == nil { + return nil, fmt.Errorf("error verifying recipient address") + } + toAddress, err := ethtypes.NewAddress(to.Verifier) + if err != nil { + return nil, err + } - _, outputStates, err := h.noto.prepareOutputs(params.To, params.Amount) + _, outputStates, err := h.noto.prepareOutputs(*toAddress, params.Amount) if err != nil { return nil, err } @@ -79,6 +92,8 @@ func (h *mintHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, OutputStates: outputStates, }, AttestationPlan: []*pb.AttestationRequest{ + // Notary will endorse the assembled transaction (by submitting to the ledger) + // Note no additional attestation using req.Transaction.From, because it is guaranteed to be the notary { Name: "notary", AttestationType: pb.AttestationType_ENDORSE, @@ -121,7 +136,7 @@ func (h *mintHandler) Prepare(ctx context.Context, tx *types.ParsedTransaction, params := map[string]interface{}{ "outputs": outputs, - "signature": "0x", + "signature": "0x", // no signature, because requester AND submitter are always the notary "data": req.Transaction.TransactionId, } paramsJSON, err := json.Marshal(params) diff --git a/domains/noto/internal/noto/handler_transfer.go b/domains/noto/internal/noto/handler_transfer.go index 6305cc5d8..9b60825e3 100644 --- a/domains/noto/internal/noto/handler_transfer.go +++ b/domains/noto/internal/noto/handler_transfer.go @@ -47,13 +47,22 @@ func (h *transferHandler) ValidateParams(ctx context.Context, params string) (in } func (h *transferHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req *pb.InitTransactionRequest) (*pb.InitTransactionResponse, error) { + params := tx.Params.(*types.TransferParams) + return &pb.InitTransactionResponse{ RequiredVerifiers: []*pb.ResolveVerifierRequest{ { Lookup: tx.DomainConfig.NotaryLookup, Algorithm: algorithms.ECDSA_SECP256K1_PLAINBYTES, }, - // TODO: should we also resolve "From"/"To" parties? + { + Lookup: tx.Transaction.From, + Algorithm: algorithms.ECDSA_SECP256K1_PLAINBYTES, + }, + { + Lookup: params.To, + Algorithm: algorithms.ECDSA_SECP256K1_PLAINBYTES, + }, }, }, nil } @@ -61,23 +70,38 @@ func (h *transferHandler) Init(ctx context.Context, tx *types.ParsedTransaction, func (h *transferHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, req *pb.AssembleTransactionRequest) (*pb.AssembleTransactionResponse, error) { params := tx.Params.(*types.TransferParams) - notary := domain.FindVerifier(tx.DomainConfig.NotaryLookup, req.ResolvedVerifiers) + notary := domain.FindVerifier(tx.DomainConfig.NotaryLookup, algorithms.ECDSA_SECP256K1_PLAINBYTES, req.ResolvedVerifiers) if notary == nil || notary.Verifier != tx.DomainConfig.NotaryAddress { - // TODO: do we need to verify every time? return nil, fmt.Errorf("notary resolved to unexpected address") } + from := domain.FindVerifier(tx.Transaction.From, algorithms.ECDSA_SECP256K1_PLAINBYTES, req.ResolvedVerifiers) + if from == nil { + return nil, fmt.Errorf("error verifying recipient address") + } + fromAddress, err := ethtypes.NewAddress(from.Verifier) + if err != nil { + return nil, err + } + to := domain.FindVerifier(params.To, algorithms.ECDSA_SECP256K1_PLAINBYTES, req.ResolvedVerifiers) + if to == nil { + return nil, fmt.Errorf("error verifying recipient address") + } + toAddress, err := ethtypes.NewAddress(to.Verifier) + if err != nil { + return nil, err + } - inputCoins, inputStates, total, err := h.noto.prepareInputs(ctx, tx.Transaction.From, params.Amount) + inputCoins, inputStates, total, err := h.noto.prepareInputs(ctx, *fromAddress, params.Amount) if err != nil { return nil, err } - outputCoins, outputStates, err := h.noto.prepareOutputs(params.To, params.Amount) + outputCoins, outputStates, err := h.noto.prepareOutputs(*toAddress, params.Amount) if err != nil { return nil, err } if total.Cmp(params.Amount.BigInt()) == 1 { remainder := big.NewInt(0).Sub(total, params.Amount.BigInt()) - returnedCoins, returnedStates, err := h.noto.prepareOutputs(tx.Transaction.From, ethtypes.NewHexInteger(remainder)) + returnedCoins, returnedStates, err := h.noto.prepareOutputs(*fromAddress, ethtypes.NewHexInteger(remainder)) if err != nil { return nil, err } @@ -86,8 +110,8 @@ func (h *transferHandler) Assemble(ctx context.Context, tx *types.ParsedTransact } var attestation []*pb.AttestationRequest - switch h.noto.config.Variant { - case "Noto": + switch tx.DomainConfig.Variant.String() { + case types.NotoVariantDefault: encodedTransfer, err := h.noto.encodeTransferUnmasked(ctx, tx.ContractAddress, inputCoins, outputCoins) if err != nil { return nil, err @@ -109,7 +133,7 @@ func (h *transferHandler) Assemble(ctx context.Context, tx *types.ParsedTransact Parties: []string{tx.DomainConfig.NotaryLookup}, }, } - case "NotoSelfSubmit": + case types.NotoVariantSelfSubmit: attestation = []*pb.AttestationRequest{ // Notary will endorse the assembled transaction (by providing a signature) { @@ -126,6 +150,8 @@ func (h *transferHandler) Assemble(ctx context.Context, tx *types.ParsedTransact Parties: []string{req.Transaction.From}, }, } + default: + return nil, fmt.Errorf("unknown variant: %s", tx.DomainConfig.Variant) } return &pb.AssembleTransactionResponse{ @@ -150,6 +176,9 @@ func (h *transferHandler) validateSenderSignature(ctx context.Context, tx *types if signature == nil { return fmt.Errorf("did not find 'sender' attestation") } + if signature.Verifier.Lookup != tx.Transaction.From { + return fmt.Errorf("sender attestation does not match transaction sender") + } encodedTransfer, err := h.noto.encodeTransferUnmasked(ctx, tx.ContractAddress, coins.inCoins, coins.outCoins) if err != nil { return err @@ -164,9 +193,18 @@ func (h *transferHandler) validateSenderSignature(ctx context.Context, tx *types return nil } -func (h *transferHandler) validateOwners(tx *types.ParsedTransaction, coins *gatheredCoins) error { +func (h *transferHandler) validateOwners(tx *types.ParsedTransaction, req *pb.EndorseTransactionRequest, coins *gatheredCoins) error { + from := domain.FindVerifier(tx.Transaction.From, algorithms.ECDSA_SECP256K1_PLAINBYTES, req.ResolvedVerifiers) + if from == nil { + return fmt.Errorf("error verifying recipient address") + } + fromAddress, err := ethtypes.NewAddress(from.Verifier) + if err != nil { + return err + } + for i, coin := range coins.inCoins { - if coin.Owner != tx.Transaction.From { + if coin.Owner != *fromAddress { return fmt.Errorf("state %s is not owned by %s", coins.inStates[i].Id, tx.Transaction.From) } } @@ -181,12 +219,12 @@ func (h *transferHandler) Endorse(ctx context.Context, tx *types.ParsedTransacti if err := h.validateAmounts(coins); err != nil { return nil, err } - if err := h.validateOwners(tx, coins); err != nil { + if err := h.validateOwners(tx, req, coins); err != nil { return nil, err } - switch h.noto.config.Variant { - case "Noto": + switch tx.DomainConfig.Variant.String() { + case types.NotoVariantDefault: if req.EndorsementRequest.Name == "notary" { // Notary checks the signature from the sender, then submits the transaction if err := h.validateSenderSignature(ctx, tx, req, coins); err != nil { @@ -196,7 +234,7 @@ func (h *transferHandler) Endorse(ctx context.Context, tx *types.ParsedTransacti EndorsementResult: pb.EndorseTransactionResponse_ENDORSER_SUBMIT, }, nil } - case "NotoSelfSubmit": + case types.NotoVariantSelfSubmit: if req.EndorsementRequest.Name == "notary" { // Notary provides a signature for the assembled payload (to be verified on base ledger) inputIDs := make([]interface{}, len(req.Inputs)) @@ -217,11 +255,15 @@ func (h *transferHandler) Endorse(ctx context.Context, tx *types.ParsedTransacti Payload: encodedTransfer, }, nil } else if req.EndorsementRequest.Name == "sender" { - // Sender submits the transaction - return &pb.EndorseTransactionResponse{ - EndorsementResult: pb.EndorseTransactionResponse_ENDORSER_SUBMIT, - }, nil + if req.EndorsementVerifier.Lookup == tx.Transaction.From { + // Sender submits the transaction + return &pb.EndorseTransactionResponse{ + EndorsementResult: pb.EndorseTransactionResponse_ENDORSER_SUBMIT, + }, nil + } } + default: + return nil, fmt.Errorf("unknown variant: %s", tx.DomainConfig.Variant) } return nil, fmt.Errorf("unrecognized endorsement request: %s", req.EndorsementRequest.Name) @@ -238,19 +280,22 @@ func (h *transferHandler) Prepare(ctx context.Context, tx *types.ParsedTransacti } var signature *pb.AttestationResult - switch h.noto.config.Variant { - case "Noto": - // Include the signature from the sender (informational only) + switch tx.DomainConfig.Variant.String() { + case types.NotoVariantDefault: + // Include the signature from the sender + // This is not verified on the base ledger, but can be verified by anyone with the unmasked state data signature = domain.FindAttestation("sender", req.AttestationResult) if signature == nil { return nil, fmt.Errorf("did not find 'sender' attestation") } - case "NotoSelfSubmit": + case types.NotoVariantSelfSubmit: // Include the signature from the notary (will be verified on base ledger) signature = domain.FindAttestation("notary", req.AttestationResult) if signature == nil { return nil, fmt.Errorf("did not find 'notary' attestation") } + default: + return nil, fmt.Errorf("unknown variant: %s", tx.DomainConfig.Variant) } params := map[string]interface{}{ diff --git a/domains/noto/internal/noto/noto.go b/domains/noto/internal/noto/noto.go index ac157d8ac..3ce67a56d 100644 --- a/domains/noto/internal/noto/noto.go +++ b/domains/noto/internal/noto/noto.go @@ -39,16 +39,13 @@ var notoFactoryJSON []byte // From "gradle copySolidity" //go:embed abis/Noto.json var notoJSON []byte // From "gradle copySolidity" -//go:embed abis/NotoSelfSubmitFactory.json -var notoSelfSubmitFactoryJSON []byte // From "gradle copySolidity" - //go:embed abis/NotoSelfSubmit.json var notoSelfSubmitJSON []byte // From "gradle copySolidity" type Noto struct { Callbacks plugintk.DomainCallbacks - config *types.Config + config types.DomainConfig chainID int64 domainID string coinSchema *pb.StateSchema @@ -57,9 +54,10 @@ type Noto struct { } type NotoDeployParams struct { + Name string `json:"name,omitempty"` TransactionID string `json:"transactionId"` Notary string `json:"notary"` - Data ethtypes.HexBytes0xPrefix `json:"data"` + Config ethtypes.HexBytes0xPrefix `json:"config"` } type gatheredCoins struct { @@ -72,26 +70,17 @@ type gatheredCoins struct { } func (n *Noto) ConfigureDomain(ctx context.Context, req *pb.ConfigureDomainRequest) (*pb.ConfigureDomainResponse, error) { - var config types.Config - err := json.Unmarshal([]byte(req.ConfigJson), &config) + err := json.Unmarshal([]byte(req.ConfigJson), &n.config) if err != nil { return nil, err } - n.config = &config - n.chainID = req.ChainId + factory := domain.LoadBuild(notoFactoryJSON) + contract := domain.LoadBuild(notoJSON) - switch config.Variant { - case "", "Noto": - config.Variant = "Noto" - n.factoryABI = domain.LoadBuild(notoFactoryJSON).ABI - n.contractABI = domain.LoadBuild(notoJSON).ABI - case "NotoSelfSubmit": - n.factoryABI = domain.LoadBuild(notoSelfSubmitFactoryJSON).ABI - n.contractABI = domain.LoadBuild(notoSelfSubmitJSON).ABI - default: - return nil, fmt.Errorf("unrecognized variant: %s", config.Variant) - } + n.chainID = req.ChainId + n.factoryABI = factory.ABI + n.contractABI = contract.ABI schemaJSON, err := json.Marshal(types.NotoCoinABI) if err != nil { @@ -100,7 +89,7 @@ func (n *Noto) ConfigureDomain(ctx context.Context, req *pb.ConfigureDomainReque return &pb.ConfigureDomainResponse{ DomainConfig: &pb.DomainConfig{ - FactoryContractAddress: config.FactoryAddress, + FactoryContractAddress: n.config.FactoryAddress, AbiStateSchemasJson: []string{string(schemaJSON)}, BaseLedgerSubmitConfig: &pb.BaseLedgerSubmitConfig{ SubmitMode: pb.BaseLedgerSubmitConfig_ENDORSER_SUBMISSION, @@ -131,33 +120,33 @@ func (n *Noto) InitDeploy(ctx context.Context, req *pb.InitDeployRequest) (*pb.I } func (n *Noto) PrepareDeploy(ctx context.Context, req *pb.PrepareDeployRequest) (*pb.PrepareDeployResponse, error) { - _, err := n.validateDeploy(req.Transaction) + params, err := n.validateDeploy(req.Transaction) if err != nil { return nil, err } - config := &types.DomainConfig{ - NotaryLookup: req.ResolvedVerifiers[0].Lookup, - NotaryAddress: req.ResolvedVerifiers[0].Verifier, + config := &types.NotoConfigInput_V0{ + NotaryLookup: req.ResolvedVerifiers[0].Lookup, } - configJSON, err := json.Marshal(config) - if err != nil { - return nil, err - } - data, err := types.DomainConfigABI.EncodeABIDataJSONCtx(ctx, configJSON) + configABI, err := n.encodeConfig(config) if err != nil { return nil, err } - params := &NotoDeployParams{ + deployParams := &NotoDeployParams{ + Name: params.Implementation, TransactionID: req.Transaction.TransactionId, - Notary: config.NotaryAddress, - Data: data, + Notary: req.ResolvedVerifiers[0].Verifier, + Config: configABI, } - paramsJSON, err := json.Marshal(params) + paramsJSON, err := json.Marshal(deployParams) if err != nil { return nil, err } - functionJSON, err := json.Marshal(n.factoryABI.Functions()["deploy"]) + functionName := "deploy" + if deployParams.Name != "" { + functionName = "deployImplementation" + } + functionJSON, err := json.Marshal(n.factoryABI.Functions()[functionName]) if err != nil { return nil, err } @@ -203,8 +192,27 @@ func (n *Noto) PrepareTransaction(ctx context.Context, req *pb.PrepareTransactio return handler.Prepare(ctx, tx, req) } -func (n *Noto) decodeDomainConfig(ctx context.Context, domainConfig []byte) (*types.DomainConfig, error) { - configValues, err := types.DomainConfigABI.DecodeABIDataCtx(ctx, domainConfig, 0) +func (n *Noto) encodeConfig(config *types.NotoConfigInput_V0) ([]byte, error) { + configJSON, err := json.Marshal(config) + if err != nil { + return nil, err + } + encodedConfig, err := types.NotoConfigInputABI_V0.EncodeABIDataJSON(configJSON) + if err != nil { + return nil, err + } + result := make([]byte, 0, len(types.NotoConfigID_V0)+len(encodedConfig)) + result = append(result, types.NotoConfigID_V0...) + result = append(result, encodedConfig...) + return result, nil +} + +func (n *Noto) decodeConfig(ctx context.Context, domainConfig []byte) (*types.NotoConfigOutput_V0, error) { + configSelector := ethtypes.HexBytes0xPrefix(domainConfig[0:4]) + if configSelector.String() != types.NotoConfigID_V0.String() { + return nil, fmt.Errorf("unexpected config type: %s", configSelector) + } + configValues, err := types.NotoConfigOutputABI_V0.DecodeABIDataCtx(ctx, domainConfig[4:], 0) if err != nil { return nil, err } @@ -212,7 +220,7 @@ func (n *Noto) decodeDomainConfig(ctx context.Context, domainConfig []byte) (*ty if err != nil { return nil, err } - var config types.DomainConfig + var config types.NotoConfigOutput_V0 err = json.Unmarshal(configJSON, &config) return &config, err } @@ -248,7 +256,7 @@ func (n *Noto) validateTransaction(ctx context.Context, tx *pb.TransactionSpecif return nil, nil, fmt.Errorf("unexpected signature for function '%s': expected=%s actual=%s", functionABI.Name, signature, tx.FunctionSignature) } - domainConfig, err := n.decodeDomainConfig(ctx, tx.ContractConfig) + domainConfig, err := n.decodeConfig(ctx, tx.ContractConfig) if err != nil { return nil, nil, err } @@ -284,7 +292,7 @@ func (n *Noto) parseCoinList(label string, states []*pb.EndorsableState) ([]*typ if input.SchemaId != n.coinSchema.Id { return nil, nil, nil, fmt.Errorf("unknown schema ID: %s", input.SchemaId) } - if coins[i], err = n.makeCoin(input.StateDataJson); err != nil { + if coins[i], err = n.unmarshalCoin(input.StateDataJson); err != nil { return nil, nil, nil, fmt.Errorf("invalid %s[%d] (%s): %s", label, i, input.Id, err) } refs[i] = &pb.StateRef{ @@ -323,7 +331,7 @@ func (n *Noto) FindCoins(ctx context.Context, query string) ([]*types.NotoCoin, coins := make([]*types.NotoCoin, len(states)) for i, state := range states { - if coins[i], err = n.makeCoin(state.DataJson); err != nil { + if coins[i], err = n.unmarshalCoin(state.DataJson); err != nil { return nil, err } } diff --git a/domains/noto/internal/noto/states.go b/domains/noto/internal/noto/states.go index 8634053f1..ce8cc179d 100644 --- a/domains/noto/internal/noto/states.go +++ b/domains/noto/internal/noto/states.go @@ -39,7 +39,7 @@ var NotoTransferUnmaskedTypeSet = eip712.TypeSet{ }, "Coin": { {Name: "salt", Type: "bytes32"}, - {Name: "owner", Type: "string"}, + {Name: "owner", Type: "address"}, {Name: "amount", Type: "uint256"}, }, eip712.EIP712Domain: { @@ -64,10 +64,10 @@ var NotoTransferMaskedTypeSet = eip712.TypeSet{ }, } -func (n *Noto) makeCoin(stateData string) (*types.NotoCoin, error) { - coin := &types.NotoCoin{} +func (n *Noto) unmarshalCoin(stateData string) (*types.NotoCoin, error) { + var coin types.NotoCoin err := json.Unmarshal([]byte(stateData), &coin) - return coin, err + return &coin, err } func (n *Noto) makeNewState(coin *types.NotoCoin) (*pb.NewState, error) { @@ -81,7 +81,7 @@ func (n *Noto) makeNewState(coin *types.NotoCoin) (*pb.NewState, error) { }, nil } -func (n *Noto) prepareInputs(ctx context.Context, owner string, amount *ethtypes.HexInteger) ([]*types.NotoCoin, []*pb.StateRef, *big.Int, error) { +func (n *Noto) prepareInputs(ctx context.Context, owner ethtypes.Address0xHex, amount *ethtypes.HexInteger) ([]*types.NotoCoin, []*pb.StateRef, *big.Int, error) { var lastStateTimestamp int64 total := big.NewInt(0) stateRefs := []*pb.StateRef{} @@ -95,7 +95,7 @@ func (n *Noto) prepareInputs(ctx context.Context, owner string, amount *ethtypes "sort": []string{".created"}, "eq": []map[string]string{{ "field": "owner", - "value": owner, + "value": owner.String(), }}, } if lastStateTimestamp > 0 { @@ -118,7 +118,7 @@ func (n *Noto) prepareInputs(ctx context.Context, owner string, amount *ethtypes } for _, state := range states { lastStateTimestamp = state.StoredAt - coin, err := n.makeCoin(state.DataJson) + coin, err := n.unmarshalCoin(state.DataJson) if err != nil { return nil, nil, nil, fmt.Errorf("coin %s is invalid: %s", state.Id, err) } @@ -135,7 +135,7 @@ func (n *Noto) prepareInputs(ctx context.Context, owner string, amount *ethtypes } } -func (n *Noto) prepareOutputs(owner string, amount *ethtypes.HexInteger) ([]*types.NotoCoin, []*pb.NewState, error) { +func (n *Noto) prepareOutputs(owner ethtypes.Address0xHex, amount *ethtypes.HexInteger) ([]*types.NotoCoin, []*pb.NewState, error) { // Always produce a single coin for the entire output amount // TODO: make this configurable newCoin := &types.NotoCoin{ @@ -159,6 +159,15 @@ func (n *Noto) findAvailableStates(ctx context.Context, query string) ([]*pb.Sto return res.States, nil } +func (n *Noto) eip712Domain(contract *ethtypes.Address0xHex) map[string]interface{} { + return map[string]interface{}{ + "name": EIP712DomainName, + "version": EIP712DomainVersion, + "chainId": n.chainID, + "verifyingContract": contract, + } +} + func (n *Noto) encodeTransferUnmasked(ctx context.Context, contract *ethtypes.Address0xHex, inputs, outputs []*types.NotoCoin) (ethtypes.HexBytes0xPrefix, error) { messageInputs := make([]interface{}, len(inputs)) for i, input := range inputs { @@ -179,12 +188,7 @@ func (n *Noto) encodeTransferUnmasked(ctx context.Context, contract *ethtypes.Ad return eip712.EncodeTypedDataV4(ctx, &eip712.TypedData{ Types: NotoTransferUnmaskedTypeSet, PrimaryType: "Transfer", - Domain: map[string]interface{}{ - "name": EIP712DomainName, - "version": EIP712DomainVersion, - "chainId": n.chainID, - "verifyingContract": contract, - }, + Domain: n.eip712Domain(contract), Message: map[string]interface{}{ "inputs": messageInputs, "outputs": messageOutputs, @@ -196,12 +200,7 @@ func (n *Noto) encodeTransferMasked(ctx context.Context, contract *ethtypes.Addr return eip712.EncodeTypedDataV4(ctx, &eip712.TypedData{ Types: NotoTransferMaskedTypeSet, PrimaryType: "Transfer", - Domain: map[string]interface{}{ - "name": EIP712DomainName, - "version": EIP712DomainVersion, - "chainId": n.chainID, - "verifyingContract": contract, - }, + Domain: n.eip712Domain(contract), Message: map[string]interface{}{ "inputs": inputs, "outputs": outputs, diff --git a/domains/noto/pkg/types/abi.go b/domains/noto/pkg/types/abi.go index 973e87512..8528a59d1 100644 --- a/domains/noto/pkg/types/abi.go +++ b/domains/noto/pkg/types/abi.go @@ -56,7 +56,8 @@ var NotoABI = abi.ABI{ } type ConstructorParams struct { - Notary string `json:"notary"` + Notary string `json:"notary"` + Implementation string `json:"implementation"` } type MintParams struct { diff --git a/domains/noto/pkg/types/config.go b/domains/noto/pkg/types/config.go index b5b9a43b5..6454d9f7e 100644 --- a/domains/noto/pkg/types/config.go +++ b/domains/noto/pkg/types/config.go @@ -17,23 +17,39 @@ package types import ( "github.com/hyperledger/firefly-signer/pkg/abi" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/kaleido-io/paladin/toolkit/pkg/domain" + "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" ) -type Config struct { +type DomainConfig struct { FactoryAddress string `json:"factoryAddress"` - Variant string `json:"variant"` } -type DomainConfig struct { - NotaryLookup string `json:"notaryLookup"` - NotaryAddress string `json:"notaryAddress"` +var NotoConfigID_V0 = ethtypes.MustNewHexBytes0xPrefix("0x00010000") + +type NotoConfigInput_V0 struct { + NotaryLookup string `json:"notaryLookup"` } -var DomainConfigABI = &abi.ParameterArray{ +var NotoConfigInputABI_V0 = &abi.ParameterArray{ + {Name: "notaryLookup", Type: "string"}, +} + +type NotoConfigOutput_V0 struct { + NotaryLookup string `json:"notaryLookup"` + NotaryAddress string `json:"notaryAddress"` + Variant tktypes.Bytes32 `json:"variant"` +} + +var NotoConfigOutputABI_V0 = &abi.ParameterArray{ {Name: "notaryLookup", Type: "string"}, {Name: "notaryAddress", Type: "address"}, + {Name: "variant", Type: "bytes32"}, } -type DomainHandler = domain.DomainHandler[DomainConfig] -type ParsedTransaction = domain.ParsedTransaction[DomainConfig] +type DomainHandler = domain.DomainHandler[NotoConfigOutput_V0] +type ParsedTransaction = domain.ParsedTransaction[NotoConfigOutput_V0] + +var NotoVariantDefault = "0x0000000000000000000000000000000000000000000000000000000000000000" +var NotoVariantSelfSubmit = "0x0000000000000000000000000000000000000000000000000000000000000001" diff --git a/domains/noto/pkg/types/states.go b/domains/noto/pkg/types/states.go index 736678f5d..a79e61d83 100644 --- a/domains/noto/pkg/types/states.go +++ b/domains/noto/pkg/types/states.go @@ -21,9 +21,9 @@ import ( ) type NotoCoin struct { - Salt string `json:"salt"` - Owner string `json:"owner"` - Amount *ethtypes.HexInteger `json:"amount"` + Salt string `json:"salt"` + Owner ethtypes.Address0xHex `json:"owner"` + Amount *ethtypes.HexInteger `json:"amount"` } var NotoCoinABI = &abi.Parameter{ diff --git a/domains/test/go.sum b/domains/test/go.sum index 41b0f8b4d..e619d035a 100644 --- a/domains/test/go.sum +++ b/domains/test/go.sum @@ -115,8 +115,8 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/hyperledger-labs/zeto/go-sdk v0.0.0-20240812164533-f19c3b9c5915 h1:QIiSMgnbWcOGTYPuB/psz4UG/LiCglPHn9QtuRo63jE= -github.com/hyperledger-labs/zeto/go-sdk v0.0.0-20240812164533-f19c3b9c5915/go.mod h1:5QXncuG69tskksWX4hLBsISsshM8NudzJKTIc3WUENw= +github.com/hyperledger-labs/zeto/go-sdk v0.0.0-20240905213624-43a614759076 h1:sy2RvS84JiF8hGJ2cjY6hZUm6rzpwmWxHp2WiEi7xpI= +github.com/hyperledger-labs/zeto/go-sdk v0.0.0-20240905213624-43a614759076/go.mod h1:BDsCLkMFaNl08o+jlSk9TiOuYp+AoY4HVs25aqx+RgY= github.com/hyperledger/firefly-common v1.4.8 h1:0o1Qp1c5YzQo8nbnX+gAo9SVd2tR4Z9U2t8Y4zEzyaA= github.com/hyperledger/firefly-common v1.4.8/go.mod h1:dXewcVMFNON2SvQ1UPvu64OWUt77+M3p8qy61lT1kE4= github.com/hyperledger/firefly-signer v1.1.14 h1:gSGwdBHTLPchGlmLOKk2Y2nawfMhlH2CDm2owt0lIUE= @@ -129,8 +129,8 @@ github.com/iden3/go-rapidsnark/types v0.0.2 h1:CjJSrlbWchHzuMRdxSYrEh7n/akP+Z2PL github.com/iden3/go-rapidsnark/types v0.0.2/go.mod h1:ApgcaUxKIgSRA6fAeFxK7p+lgXXfG4oA2HN5DhFlfF4= github.com/iden3/go-rapidsnark/witness/v2 v2.0.0 h1:mkY6VDfwKVJc83QGKmwVXY2LYepidPrFAxskrjr8UCs= github.com/iden3/go-rapidsnark/witness/v2 v2.0.0/go.mod h1:3JRjqUfW1hgI9hzLDO0v8z/DUkR0ZUehhYLlnIfRxnA= -github.com/iden3/go-rapidsnark/witness/wasmer v0.0.0-20230524142950-0986cf057d4e h1:lqrevdLsG1k8ieaxgUQccf3unf73m3zmkHJ3oIdld90= -github.com/iden3/go-rapidsnark/witness/wasmer v0.0.0-20230524142950-0986cf057d4e/go.mod h1:WUtPVKXrhfZHJXavwId2+8J/fKMHQ92N0MZDxt8sfEA= +github.com/iden3/go-rapidsnark/witness/wasmer v0.0.0-20240621085734-9323fbec34a3 h1:IjXECVBygAOlpH3lRvH5VLSsQIhJeZJPZ3n3Uv/piZk= +github.com/iden3/go-rapidsnark/witness/wasmer v0.0.0-20240621085734-9323fbec34a3/go.mod h1:WUtPVKXrhfZHJXavwId2+8J/fKMHQ92N0MZDxt8sfEA= github.com/iden3/wasmer-go v0.0.1 h1:TZKh8Se8B/73PvWrcu+FTU9L1k5XYAmtFbioj7l0Uog= github.com/iden3/wasmer-go v0.0.1/go.mod h1:ZnZBAO012M7o+Q1INXLRIxKQgEcH2FuwL0Iga8A4ufg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -289,8 +289,8 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -347,8 +347,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -358,8 +358,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= -golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= -golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -370,8 +370,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/domains/test/pvp_test.go b/domains/test/pvp_test.go index 928a4dfec..adeae7aa8 100644 --- a/domains/test/pvp_test.go +++ b/domains/test/pvp_test.go @@ -89,7 +89,7 @@ func toJSON(t *testing.T, v any) []byte { return result } -func mapConfig(t *testing.T, config *types.Config) (m map[string]any) { +func mapConfig(t *testing.T, config *types.DomainConfig) (m map[string]any) { configJSON, err := json.Marshal(&config) assert.NoError(t, err) err = json.Unmarshal(configJSON, &m) @@ -118,7 +118,7 @@ func deployContracts(ctx context.Context, t *testing.T, contracts map[string][]b return deployed } -func newNotoDomain(t *testing.T, config *types.Config) (*noto.Noto, *testbed.TestbedDomain) { +func newNotoDomain(t *testing.T, config *types.DomainConfig) (*noto.Noto, *testbed.TestbedDomain) { var domain noto.Noto return &domain, &testbed.TestbedDomain{ Config: mapConfig(t, config), @@ -253,7 +253,7 @@ func TestPvP(t *testing.T) { log.L(ctx).Infof("%s deployed to %s", name, address) } - _, notoTestbed := newNotoDomain(t, &types.Config{ + _, notoTestbed := newNotoDomain(t, &types.DomainConfig{ FactoryAddress: contracts["noto"], }) done, tb, rpc := newTestbed(t, map[string]*testbed.TestbedDomain{ diff --git a/domains/zeto/go.mod b/domains/zeto/go.mod index 7b305bd6b..ad89e8476 100644 --- a/domains/zeto/go.mod +++ b/domains/zeto/go.mod @@ -5,7 +5,6 @@ go 1.22.5 require ( github.com/go-resty/resty/v2 v2.14.0 github.com/hyperledger-labs/zeto/go-sdk v0.0.0-20240905213624-43a614759076 - github.com/hyperledger/firefly-common v1.4.8 github.com/hyperledger/firefly-signer v1.1.14 github.com/iden3/go-iden3-crypto v0.0.16 github.com/kaleido-io/paladin/core v0.0.0-00010101000000-000000000000 @@ -39,6 +38,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hyperledger/firefly-common v1.4.8 // indirect github.com/iden3/go-rapidsnark/prover v0.0.10 // indirect github.com/iden3/go-rapidsnark/types v0.0.2 // indirect github.com/iden3/go-rapidsnark/witness/v2 v2.0.0 // indirect diff --git a/domains/zeto/internal/zeto/e2e_zeto_test.go b/domains/zeto/internal/zeto/e2e_zeto_test.go index 6b14e82c1..9f8ebcf22 100644 --- a/domains/zeto/internal/zeto/e2e_zeto_test.go +++ b/domains/zeto/internal/zeto/e2e_zeto_test.go @@ -21,12 +21,12 @@ import ( "testing" "github.com/go-resty/resty/v2" - "github.com/hyperledger/firefly-common/pkg/log" "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-signer/pkg/rpcbackend" "github.com/kaleido-io/paladin/core/pkg/testbed" "github.com/kaleido-io/paladin/domains/zeto/pkg/types" "github.com/kaleido-io/paladin/toolkit/pkg/domain" + "github.com/kaleido-io/paladin/toolkit/pkg/log" "github.com/kaleido-io/paladin/toolkit/pkg/plugintk" "github.com/kaleido-io/paladin/toolkit/pkg/tktypes" "github.com/stretchr/testify/assert" @@ -75,22 +75,23 @@ func deployContracts(ctx context.Context, t *testing.T, contracts []map[string][ return deployed } -func newTestDomain(t *testing.T, domainName string, config *types.Config) (context.CancelFunc, *Zeto, rpcbackend.Backend) { - var domain *Zeto +func newZetoDomain(t *testing.T, config *types.Config) (*Zeto, *testbed.TestbedDomain) { + var domain Zeto + return &domain, &testbed.TestbedDomain{ + Config: mapConfig(t, config), + Plugin: plugintk.NewDomain(func(callbacks plugintk.DomainCallbacks) plugintk.DomainAPI { + domain.Callbacks = callbacks + return &domain + }), + } +} + +func newTestbed(t *testing.T, domains map[string]*testbed.TestbedDomain) (context.CancelFunc, testbed.Testbed, rpcbackend.Backend) { tb := testbed.NewTestBed() - plugin := plugintk.NewDomain(func(callbacks plugintk.DomainCallbacks) plugintk.DomainAPI { - domain = &Zeto{Callbacks: callbacks} - return domain - }) - url, done, err := tb.StartForTest("../../testbed.config.yaml", map[string]*testbed.TestbedDomain{ - domainName: { - Config: mapConfig(t, config), - Plugin: plugin, - }, - }) - require.NoError(t, err) + url, done, err := tb.StartForTest("../../testbed.config.yaml", domains) + assert.NoError(t, err) rpc := rpcbackend.NewRPCClient(resty.New().SetBaseURL(url)) - return done, domain, rpc + return done, tb, rpc } func TestZeto(t *testing.T) { @@ -116,10 +117,13 @@ func TestZeto(t *testing.T) { log.L(ctx).Infof("%s deployed to %s", name, address) } - done, zeto, rpc := newTestDomain(t, domainName, &types.Config{ + zeto, zetoTestbed := newZetoDomain(t, &types.Config{ FactoryAddress: contracts["factory"], Libraries: contracts, }) + done, _, rpc := newTestbed(t, map[string]*testbed.TestbedDomain{ + domainName: zetoTestbed, + }) defer done() log.L(ctx).Infof("Deploying an instance of Zeto") diff --git a/domains/zeto/internal/zeto/handler_mint.go b/domains/zeto/internal/zeto/handler_mint.go index 95c7f780b..a876e2b7e 100644 --- a/domains/zeto/internal/zeto/handler_mint.go +++ b/domains/zeto/internal/zeto/handler_mint.go @@ -61,7 +61,7 @@ func (h *mintHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req func (h *mintHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, req *pb.AssembleTransactionRequest) (*pb.AssembleTransactionResponse, error) { params := tx.Params.(*types.MintParams) - resolvedRecipient := domain.FindVerifier(params.To, req.ResolvedVerifiers) + resolvedRecipient := domain.FindVerifier(params.To, algorithms.ZKP_BABYJUBJUB_PLAINBYTES, req.ResolvedVerifiers) if resolvedRecipient == nil { return nil, fmt.Errorf("failed to resolve: %s", params.To) } diff --git a/domains/zeto/internal/zeto/handler_transfer.go b/domains/zeto/internal/zeto/handler_transfer.go index 47a539493..2a93c7d49 100644 --- a/domains/zeto/internal/zeto/handler_transfer.go +++ b/domains/zeto/internal/zeto/handler_transfer.go @@ -123,11 +123,11 @@ func (h *transferHandler) formatProvingRequest(inputCoins, outputCoins []*types. func (h *transferHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, req *pb.AssembleTransactionRequest) (*pb.AssembleTransactionResponse, error) { params := tx.Params.(*types.TransferParams) - resolvedSender := domain.FindVerifier(tx.Transaction.From, req.ResolvedVerifiers) + resolvedSender := domain.FindVerifier(tx.Transaction.From, algorithms.ZKP_BABYJUBJUB_PLAINBYTES, req.ResolvedVerifiers) if resolvedSender == nil { return nil, fmt.Errorf("failed to resolve: %s", tx.Transaction.From) } - resolvedRecipient := domain.FindVerifier(params.To, req.ResolvedVerifiers) + resolvedRecipient := domain.FindVerifier(params.To, algorithms.ZKP_BABYJUBJUB_PLAINBYTES, req.ResolvedVerifiers) if resolvedRecipient == nil { return nil, fmt.Errorf("failed to resolve: %s", params.To) } diff --git a/go.work.sum b/go.work.sum index f122d85f6..30f562e21 100644 --- a/go.work.sum +++ b/go.work.sum @@ -450,11 +450,13 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0= github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= +github.com/hyperledger-labs/zeto/go-sdk v0.0.0-20240812164533-f19c3b9c5915/go.mod h1:5QXncuG69tskksWX4hLBsISsshM8NudzJKTIc3WUENw= github.com/hyperledger/firefly-common v1.4.6/go.mod h1:jkErZdQmC9fsAJZQO427tURdwB9iiW+NMUZSqS3eBIE= github.com/hyperledger/firefly-signer v1.1.13/go.mod h1:pK6kivzBFSue3zpJSQpH67VasnLLbwBJOBUNv0zHbRA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/iden3/go-rapidsnark/witness/wasmer v0.0.0-20230524142950-0986cf057d4e/go.mod h1:WUtPVKXrhfZHJXavwId2+8J/fKMHQ92N0MZDxt8sfEA= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -801,6 +803,7 @@ golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -964,15 +967,18 @@ golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/solidity/contracts/domains/interfaces/INoto.sol b/solidity/contracts/domains/interfaces/INoto.sol index 938e3ee7c..47aded59f 100644 --- a/solidity/contracts/domains/interfaces/INoto.sol +++ b/solidity/contracts/domains/interfaces/INoto.sol @@ -11,6 +11,13 @@ interface INoto { event UTXOApproved(address delegate, bytes32 txhash, bytes signature); + function initialize( + bytes32 transactionId, + address domain, + address notary, + bytes memory config + ) external; + function transfer( bytes32[] memory inputs, bytes32[] memory outputs, diff --git a/solidity/contracts/domains/noto/Noto.sol b/solidity/contracts/domains/noto/Noto.sol index d16fa7dc0..7512eba50 100644 --- a/solidity/contracts/domains/noto/Noto.sol +++ b/solidity/contracts/domains/noto/Noto.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; -import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import {INoto} from "../interfaces/INoto.sol"; import {IPaladinContract_V0} from "../interfaces/IPaladinContract.sol"; @@ -23,7 +23,12 @@ import {IPaladinContract_V0} from "../interfaces/IPaladinContract.sol"; /// This allows coordination of DVP with other smart contracts, which could /// be using any model programmable via EVM (not just C-UTXO) /// -contract Noto is EIP712, INoto, IPaladinContract_V0 { +contract Noto is + EIP712Upgradeable, + UUPSUpgradeable, + INoto, + IPaladinContract_V0 +{ mapping(bytes32 => bool) private _unspent; mapping(bytes32 => ApprovalRecord) private _approvals; address _notary; @@ -33,6 +38,19 @@ contract Noto is EIP712, INoto, IPaladinContract_V0 { error NotoInvalidOutput(bytes32 id); error NotoNotNotary(address sender); error NotoInvalidDelegate(bytes32 txhash, address delegate, address sender); + error NotoUnsupportedConfigType(bytes4 configSelector); + + // Config follows the convention of a 4 byte type selector, followed by ABI encoded bytes + bytes4 public constant NotoConfigID_V0 = 0x00010000; + + struct NotoConfig_V0 { + string notaryLookup; + address notaryAddress; + bytes32 variant; + } + + bytes32 public constant NotoVariantDefault = + 0x0000000000000000000000000000000000000000000000000000000000000000; bytes32 private constant TRANSFER_TYPEHASH = keccak256("Transfer(bytes32[] inputs,bytes32[] outputs,bytes data)"); @@ -52,16 +70,56 @@ contract Noto is EIP712, INoto, IPaladinContract_V0 { _; } - constructor( + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( bytes32 transactionId, address domain, address notary, - bytes memory data - ) EIP712("noto", "0.0.1") { + bytes calldata config + ) public virtual initializer { + __EIP712_init("noto", "0.0.1"); _notary = notary; - emit PaladinNewSmartContract_V0(transactionId, domain, data); + + NotoConfig_V0 memory configOut = _decodeConfig(config); + configOut.notaryAddress = notary; + configOut.variant = NotoVariantDefault; + + emit PaladinNewSmartContract_V0( + transactionId, + domain, + _encodeConfig(configOut) + ); + } + + function _decodeConfig( + bytes calldata config + ) internal pure returns (NotoConfig_V0 memory) { + bytes4 configSelector = bytes4(config[0:4]); + if (configSelector == NotoConfigID_V0) { + NotoConfig_V0 memory configOut; + (configOut.notaryLookup) = abi.decode(config[4:], (string)); + return configOut; + } + revert NotoUnsupportedConfigType(configSelector); + } + + function _encodeConfig( + NotoConfig_V0 memory config + ) internal pure returns (bytes memory) { + bytes memory configOut = abi.encode( + config.notaryLookup, + config.notaryAddress, + config.variant + ); + return bytes.concat(NotoConfigID_V0, configOut); } + function _authorizeUpgrade(address) internal override onlyNotary {} + /// @dev query whether a TXO is currently in the unspent list /// @param id the UTXO identifier /// @return unspent true or false depending on whether the identifier is in the unspent map @@ -96,7 +154,7 @@ contract Noto is EIP712, INoto, IPaladinContract_V0 { function _approve( address delegate, bytes32 txhash, - bytes memory signature + bytes calldata signature ) internal { _approvals[txhash].delegate = delegate; emit UTXOApproved(delegate, txhash, signature); @@ -112,10 +170,10 @@ contract Noto is EIP712, INoto, IPaladinContract_V0 { * Emits a {UTXOTransfer} event. */ function approvedTransfer( - bytes32[] memory inputs, - bytes32[] memory outputs, - bytes memory signature, - bytes memory data + bytes32[] calldata inputs, + bytes32[] calldata outputs, + bytes calldata signature, + bytes calldata data ) public { bytes32 txhash = _buildTXHash(inputs, outputs, data); if (_approvals[txhash].delegate != msg.sender) { @@ -132,9 +190,9 @@ contract Noto is EIP712, INoto, IPaladinContract_V0 { } function _buildTXHash( - bytes32[] memory inputs, - bytes32[] memory outputs, - bytes memory data + bytes32[] calldata inputs, + bytes32[] calldata outputs, + bytes calldata data ) internal view returns (bytes32) { bytes32 structHash = keccak256( abi.encode( @@ -185,19 +243,19 @@ contract Noto is EIP712, INoto, IPaladinContract_V0 { } function mint( - bytes32[] memory outputs, - bytes memory signature, - bytes memory data + bytes32[] calldata outputs, + bytes calldata signature, + bytes calldata data ) external virtual onlyNotary { bytes32[] memory inputs; - _transfer(inputs, outputs, "", data); + _transfer(inputs, outputs, signature, data); } function transfer( - bytes32[] memory inputs, - bytes32[] memory outputs, - bytes memory signature, - bytes memory data + bytes32[] calldata inputs, + bytes32[] calldata outputs, + bytes calldata signature, + bytes calldata data ) external virtual onlyNotary { _transfer(inputs, outputs, signature, data); } @@ -205,7 +263,7 @@ contract Noto is EIP712, INoto, IPaladinContract_V0 { function approve( address delegate, bytes32 txhash, - bytes memory signature + bytes calldata signature ) external virtual onlyNotary { _approve(delegate, txhash, signature); } diff --git a/solidity/contracts/domains/noto/NotoFactory.sol b/solidity/contracts/domains/noto/NotoFactory.sol index f61ea9a76..679bb087b 100644 --- a/solidity/contracts/domains/noto/NotoFactory.sol +++ b/solidity/contracts/domains/noto/NotoFactory.sol @@ -1,14 +1,64 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {Clones} from "@openzeppelin/contracts/proxy/Clones.sol"; +import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import {INoto} from "../interfaces/INoto.sol"; import {Noto} from "./Noto.sol"; -contract NotoFactory { +contract NotoFactory is Ownable { + mapping(string => address) internal implementations; + + constructor() Ownable(_msgSender()) { + implementations["default"] = address(new Noto()); + } + + /** + * Deploy a default instance of Noto. + */ function deploy( bytes32 transactionId, address notary, - bytes memory data + bytes calldata config + ) external { + _deploy(implementations["default"], transactionId, notary, config); + } + + /** + * Register an additional implementation of Noto. + */ + function registerImplementation( + string calldata name, + address implementation + ) public onlyOwner { + implementations[name] = implementation; + } + + /** + * Deploy an instance of Noto by cloning a specific implementation. + */ + function deployImplementation( + string calldata name, + bytes32 transactionId, + address notary, + bytes calldata config ) external { - new Noto(transactionId, address(this), notary, data); + _deploy(implementations[name], transactionId, notary, config); + } + + function _deploy( + address implementation, + bytes32 transactionId, + address notary, + bytes calldata config + ) internal { + address instance = Clones.clone(implementation); + INoto(instance).initialize( + transactionId, + address(this), + notary, + config + ); } } diff --git a/solidity/contracts/domains/noto/NotoSelfSubmit.sol b/solidity/contracts/domains/noto/NotoSelfSubmit.sol index 190a25435..30888bb9a 100644 --- a/solidity/contracts/domains/noto/NotoSelfSubmit.sol +++ b/solidity/contracts/domains/noto/NotoSelfSubmit.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.20; -import {Noto} from "./Noto.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {Noto} from "./Noto.sol"; /** * Noto variant which allows _any_ address to submit a transfer, as long as @@ -10,18 +10,34 @@ import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; * signature is recovered and verified. */ contract NotoSelfSubmit is Noto { - constructor( + bytes32 public constant NotoVariantSelfSubmit = + 0x0000000000000000000000000000000000000000000000000000000000000001; + + function initialize( bytes32 transactionId, address domain, address notary, - bytes memory data - ) Noto(transactionId, domain, notary, data) {} + bytes calldata config + ) public override initializer { + __EIP712_init("noto", "0.0.1"); + + NotoConfig_V0 memory configOut = _decodeConfig(config); + configOut.notaryAddress = notary; + configOut.variant = NotoVariantSelfSubmit; + + _notary = notary; + emit PaladinNewSmartContract_V0( + transactionId, + domain, + _encodeConfig(configOut) + ); + } function transfer( - bytes32[] memory inputs, - bytes32[] memory outputs, - bytes memory signature, - bytes memory data + bytes32[] calldata inputs, + bytes32[] calldata outputs, + bytes calldata signature, + bytes calldata data ) external override { bytes32 txhash = _buildTXHash(inputs, outputs, data); address signer = ECDSA.recover(txhash, signature); @@ -32,7 +48,7 @@ contract NotoSelfSubmit is Noto { function approve( address delegate, bytes32 txhash, - bytes memory signature + bytes calldata signature ) external override { address signer = ECDSA.recover(txhash, signature); requireNotary(signer); diff --git a/solidity/contracts/domains/noto/NotoSelfSubmitFactory.sol b/solidity/contracts/domains/noto/NotoSelfSubmitFactory.sol deleted file mode 100644 index 39883f3a2..000000000 --- a/solidity/contracts/domains/noto/NotoSelfSubmitFactory.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.20; - -import {NotoSelfSubmit} from "./NotoSelfSubmit.sol"; - -contract NotoSelfSubmitFactory { - function deploy( - bytes32 transactionId, - address notary, - bytes memory data - ) external { - new NotoSelfSubmit(transactionId, address(this), notary, data); - } -} diff --git a/solidity/test/domains/noto/Noto.ts b/solidity/test/domains/noto/Noto.ts index 6c372a53f..ccff06cdf 100644 --- a/solidity/test/domains/noto/Noto.ts +++ b/solidity/test/domains/noto/Noto.ts @@ -1,9 +1,17 @@ import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { expect } from "chai"; import { randomBytes } from "crypto"; -import { ContractTransactionReceipt, Signer, TypedDataEncoder } from "ethers"; +import { + AbiCoder, + ContractTransactionReceipt, + Interface, + Signer, + TypedDataEncoder, +} from "ethers"; import hre, { ethers } from "hardhat"; -import { Noto } from "../../typechain-types"; +import { NotoFactory, Noto } from "../../../typechain-types"; + +export const NotoConfigID_V0 = "0x00010000"; export async function newTransferHash( noto: Noto, @@ -38,19 +46,37 @@ export function fakeTXO() { return randomBytes32(); } +export async function deployNotoInstance( + notoFactory: NotoFactory, + notoInterface: Interface, + notary: string +) { + const abi = AbiCoder.defaultAbiCoder(); + const deployTx = await notoFactory.deploy( + randomBytes32(), + notary, + NotoConfigID_V0 + abi.encode(["string"], [""]).substring(2) + ); + const deployReceipt = await deployTx.wait(); + const deployEvent = deployReceipt?.logs.find( + (l) => notoInterface.parseLog(l)?.name === "PaladinNewSmartContract_V0" + ); + expect(deployEvent).to.exist; + return deployEvent?.address ?? ""; +} + describe("Noto", function () { async function deployNotoFixture() { const [notary, other] = await ethers.getSigners(); + const NotoFactory = await ethers.getContractFactory("NotoFactory"); + const notoFactory = await NotoFactory.deploy(); const Noto = await ethers.getContractFactory("Noto"); - const noto = await Noto.deploy( - randomBytes32(), - "0xab5a1b758fdabfa31542bf50de1e1689ab64db6e", - notary.address, - "0x" + const noto = Noto.attach( + await deployNotoInstance(notoFactory, Noto.interface, notary.address) ); - return { noto, notary, other }; + return { noto: noto as Noto, notary, other }; } async function doTransfer( diff --git a/solidity/test/domains/noto/NotoSelfSubmit.ts b/solidity/test/domains/noto/NotoSelfSubmit.ts index ee013585f..aa40ef670 100644 --- a/solidity/test/domains/noto/NotoSelfSubmit.ts +++ b/solidity/test/domains/noto/NotoSelfSubmit.ts @@ -1,9 +1,9 @@ import { loadFixture } from "@nomicfoundation/hardhat-toolbox/network-helpers"; import { expect } from "chai"; -import { ContractTransactionReceipt, Signer } from "ethers"; +import { AbiCoder, ContractTransactionReceipt, Signer } from "ethers"; import hre, { ethers } from "hardhat"; import { NotoSelfSubmit } from "../../typechain-types"; -import { fakeTXO, randomBytes32 } from "./Noto"; +import { fakeTXO, NotoConfigID_V0, randomBytes32 } from "./Noto"; export async function prepareSignature( noto: NotoSelfSubmit, @@ -32,14 +32,25 @@ export async function prepareSignature( describe("NotoSelfSubmit", function () { async function deployNotoFixture() { const [notary, other] = await ethers.getSigners(); + const abi = AbiCoder.defaultAbiCoder(); + const NotoFactory = await ethers.getContractFactory("NotoFactory"); + const notoFactory = await NotoFactory.deploy(); const Noto = await ethers.getContractFactory("NotoSelfSubmit"); - const noto = await Noto.deploy( + const notoImpl = await Noto.deploy(); + await notoFactory.registerImplementation("selfsubmit", notoImpl); + const deployTx = await notoFactory.deployImplementation( + "selfsubmit", randomBytes32(), - "0xab5a1b758fdabfa31542bf50de1e1689ab64db6e", notary.address, - "0x" + NotoConfigID_V0 + abi.encode(["string"], [""]).substring(2) ); + const deployReceipt = await deployTx.wait(); + const deployEvent = deployReceipt?.logs.find( + (l) => Noto.interface.parseLog(l)?.name === "PaladinNewSmartContract_V0" + ); + expect(deployEvent).to.exist; + const noto = Noto.attach(deployEvent?.address ?? ""); return { noto, notary, other }; } diff --git a/solidity/test/shared/atom/Atom.ts b/solidity/test/shared/atom/Atom.ts index 814e553dd..81ce862e6 100644 --- a/solidity/test/shared/atom/Atom.ts +++ b/solidity/test/shared/atom/Atom.ts @@ -1,8 +1,9 @@ import { expect } from "chai"; import { ContractTransactionReceipt, ZeroAddress } from "ethers"; import { ethers } from "hardhat"; -import { Atom } from "../typechain-types"; +import { Atom, Noto } from "../typechain-types"; import { + deployNotoInstance, fakeTXO, newTransferHash, randomBytes32, @@ -12,17 +13,17 @@ describe("Atom", function () { it("atomic operation with 2 encoded calls", async function () { const [notary1, notary2, anybody1, anybody2] = await ethers.getSigners(); + const NotoFactory = await ethers.getContractFactory("NotoFactory"); + const notoFactory = await NotoFactory.deploy(); + const Noto = await ethers.getContractFactory("Noto"); const AtomFactory = await ethers.getContractFactory("AtomFactory"); const Atom = await ethers.getContractFactory("Atom"); const ERC20Simple = await ethers.getContractFactory("ERC20Simple"); // Deploy two contracts - const noto = await Noto.connect(notary1).deploy( - randomBytes32(), - anybody1.address, - notary1.address, - "0x" + const noto: Noto = Noto.attach( + await deployNotoInstance(notoFactory, Noto.interface, notary1.address) ); const erc20 = await ERC20Simple.connect(notary2).deploy("Token", "TOK"); @@ -102,16 +103,16 @@ describe("Atom", function () { it("revert propagation", async function () { const [notary1, anybody1, anybody2] = await ethers.getSigners(); + const NotoFactory = await ethers.getContractFactory("NotoFactory"); + const notoFactory = await NotoFactory.deploy(); + const Noto = await ethers.getContractFactory("Noto"); const AtomFactory = await ethers.getContractFactory("AtomFactory"); const Atom = await ethers.getContractFactory("Atom"); // Deploy noto contract - const noto = await Noto.connect(notary1).deploy( - randomBytes32(), - anybody1.address, - notary1.address, - "0x" + const noto: Noto = Noto.attach( + await deployNotoInstance(notoFactory, Noto.interface, notary1.address) ); // Fake up a delegation diff --git a/toolkit/go/pkg/domain/util.go b/toolkit/go/pkg/domain/util.go index 46b71883c..4068415a7 100644 --- a/toolkit/go/pkg/domain/util.go +++ b/toolkit/go/pkg/domain/util.go @@ -87,9 +87,9 @@ func linkBytecode(artifact SolidityBuildWithLinks, libraries map[string]string) return hex.DecodeString(strings.TrimPrefix(bytecode, "0x")) } -func FindVerifier(lookup string, verifiers []*pb.ResolvedVerifier) *pb.ResolvedVerifier { +func FindVerifier(lookup, algorithm string, verifiers []*pb.ResolvedVerifier) *pb.ResolvedVerifier { for _, verifier := range verifiers { - if verifier.Lookup == lookup { + if verifier.Lookup == lookup && verifier.Algorithm == algorithm { return verifier } }