From d1c3f09c2ee30dfddcc73021f85189e73f1f9ce7 Mon Sep 17 00:00:00 2001 From: Andrew Richardson Date: Wed, 18 Dec 2024 14:05:01 -0500 Subject: [PATCH] noto: allow locks to have any number of outcomes Also gather two separate sender signatures for transfer + lock, so that they can be emitted separately in the two blockchain events. Signed-off-by: Andrew Richardson --- domains/noto/internal/noto/e2e_noto_test.go | 55 ++++- domains/noto/internal/noto/handler_burn.go | 2 +- domains/noto/internal/noto/handler_lock.go | 188 ++++++++++++------ domains/noto/internal/noto/handler_mint.go | 2 +- .../noto/internal/noto/handler_transfer.go | 2 +- domains/noto/internal/noto/handlers.go | 30 +-- domains/noto/internal/noto/noto.go | 25 ++- domains/noto/internal/noto/states.go | 39 +--- domains/noto/pkg/types/abi.go | 13 +- .../contracts/domains/interfaces/INoto.sol | 36 ++-- .../domains/interfaces/INotoPrivate.sol | 17 +- solidity/contracts/domains/noto/Noto.sol | 43 ++-- 12 files changed, 291 insertions(+), 161 deletions(-) diff --git a/domains/noto/internal/noto/e2e_noto_test.go b/domains/noto/internal/noto/e2e_noto_test.go index 8f934ae98..d6370aa34 100644 --- a/domains/noto/internal/noto/e2e_noto_test.go +++ b/domains/noto/internal/noto/e2e_noto_test.go @@ -511,7 +511,11 @@ func TestNotoLock(t *testing.T) { To: ¬oAddress, Function: "lock", Data: toJSON(t, &types.LockParams{ - Amount: tktypes.Int64ToInt256(50), + ID: tktypes.Bytes32(tktypes.RandBytes(32)), + Amount: tktypes.Int64ToInt256(50), + Recipients: []types.LockRecipient{ + {Ref: 0, Recipient: recipient1Name}, + }, Delegate: tktypes.MustEthAddress(unlockerDelegate1Key.Verifier.Verifier), }), }, @@ -593,4 +597,53 @@ func TestNotoLock(t *testing.T) { assert.Equal(t, recipient1Key.Verifier.Verifier, coins[0].Data.Owner.String()) assert.Equal(t, int64(50), coins[1].Data.Amount.Int().Int64()) assert.Equal(t, recipient2Key.Verifier.Verifier, coins[1].Data.Owner.String()) + + log.L(ctx).Infof("Lock 50 from recipient1 (again)") + rpcerr = rpc.CallRPC(ctx, &invokeResult, "testbed_invoke", &pldapi.TransactionInput{ + TransactionBase: pldapi.TransactionBase{ + From: recipient1Name, + To: ¬oAddress, + Function: "lock", + Data: toJSON(t, &types.LockParams{ + ID: tktypes.Bytes32(tktypes.RandBytes(32)), + Amount: tktypes.Int64ToInt256(50), + Recipients: []types.LockRecipient{ + {Ref: 0, Recipient: recipient1Name}, + {Ref: 1, Recipient: recipient2Name}, + }, + Delegate: tktypes.MustEthAddress(unlockerDelegate1Key.Verifier.Verifier), + }), + }, + ABI: types.NotoABI, + }, true) + if rpcerr != nil { + require.NoError(t, rpcerr.Error()) + } + + coins = findLockedCoins(t, ctx, rpc, noto, notoAddress, nil) + require.Len(t, coins, 1) + lockedCoin = coins[0] + + log.L(ctx).Infof("Unlock from recipient1 (send 50 to recipient2)") + tx = client.ForABI(ctx, noto.contractABI). + Public(). + From(unlockerDelegate1). + To(¬oAddress). + Function("unlock"). + Inputs(&NotoUnlockParams{ + Locked: lockedCoin.ID, + Outcome: 1, + }). + Send(). + Wait(3 * time.Second) + require.NoError(t, tx.Error()) + + coins = findLockedCoins(t, ctx, rpc, noto, notoAddress, nil) + require.Len(t, coins, 0) + coins = findAvailableCoins(t, ctx, rpc, noto, notoAddress, nil) + require.Len(t, coins, 2) + assert.Equal(t, int64(50), coins[0].Data.Amount.Int().Int64()) + assert.Equal(t, recipient2Key.Verifier.Verifier, coins[0].Data.Owner.String()) + assert.Equal(t, int64(50), coins[1].Data.Amount.Int().Int64()) + assert.Equal(t, recipient2Key.Verifier.Verifier, coins[1].Data.Owner.String()) } diff --git a/domains/noto/internal/noto/handler_burn.go b/domains/noto/internal/noto/handler_burn.go index a376960b1..7e6754957 100644 --- a/domains/noto/internal/noto/handler_burn.go +++ b/domains/noto/internal/noto/handler_burn.go @@ -152,7 +152,7 @@ func (h *burnHandler) Endorse(ctx context.Context, tx *types.ParsedTransaction, } // Notary checks the signature from the sender, then submits the transaction - if err := h.noto.validateTransferSignature(ctx, tx, req, coins); err != nil { + if err := h.noto.validateTransferSignature(ctx, tx, "sender", req, coins); err != nil { return nil, err } return &prototk.EndorseTransactionResponse{ diff --git a/domains/noto/internal/noto/handler_lock.go b/domains/noto/internal/noto/handler_lock.go index 257ead195..3f0b1499e 100644 --- a/domains/noto/internal/noto/handler_lock.go +++ b/domains/noto/internal/noto/handler_lock.go @@ -37,35 +37,49 @@ type lockHandler struct { } func (h *lockHandler) ValidateParams(ctx context.Context, config *types.NotoParsedConfig, params string) (interface{}, error) { - var transferParams types.LockParams - if err := json.Unmarshal([]byte(params), &transferParams); err != nil { + var lockParams types.LockParams + if err := json.Unmarshal([]byte(params), &lockParams); err != nil { return nil, err } - if transferParams.Delegate.IsZero() { + if lockParams.ID.IsZero() { + return nil, i18n.NewError(ctx, msgs.MsgParameterRequired, "id") + } + if lockParams.Delegate.IsZero() { return nil, i18n.NewError(ctx, msgs.MsgParameterRequired, "delegate") } - if transferParams.Amount == nil || transferParams.Amount.Int().Sign() != 1 { + if lockParams.Amount == nil || lockParams.Amount.Int().Sign() != 1 { return nil, i18n.NewError(ctx, msgs.MsgParameterGreaterThanZero, "amount") } - return &transferParams, nil + return &lockParams, nil } func (h *lockHandler) Init(ctx context.Context, tx *types.ParsedTransaction, req *prototk.InitTransactionRequest) (*prototk.InitTransactionResponse, error) { + params := tx.Params.(*types.LockParams) notary := tx.DomainConfig.NotaryLookup - return &prototk.InitTransactionResponse{ - RequiredVerifiers: []*prototk.ResolveVerifierRequest{ - { - Lookup: notary, - Algorithm: algorithms.ECDSA_SECP256K1, - VerifierType: verifiers.ETH_ADDRESS, - }, - { - Lookup: tx.Transaction.From, - Algorithm: algorithms.ECDSA_SECP256K1, - VerifierType: verifiers.ETH_ADDRESS, - }, + requests := make([]*prototk.ResolveVerifierRequest, 0, len(params.Recipients)+2) + requests = append(requests, + &prototk.ResolveVerifierRequest{ + Lookup: notary, + Algorithm: algorithms.ECDSA_SECP256K1, + VerifierType: verifiers.ETH_ADDRESS, + }, + &prototk.ResolveVerifierRequest{ + Lookup: tx.Transaction.From, + Algorithm: algorithms.ECDSA_SECP256K1, + VerifierType: verifiers.ETH_ADDRESS, }, + ) + for _, recipient := range params.Recipients { + requests = append(requests, &prototk.ResolveVerifierRequest{ + Lookup: recipient.Recipient, + Algorithm: algorithms.ECDSA_SECP256K1, + VerifierType: verifiers.ETH_ADDRESS, + }) + } + + return &prototk.InitTransactionResponse{ + RequiredVerifiers: requests, }, nil } @@ -93,29 +107,38 @@ func (h *lockHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, // A single locked coin, owned by the submitter and with a value equal to the transfer amount lockedCoin := &types.NotoLockedCoin{ - ID: tktypes.Bytes32(tktypes.RandBytes(32)), + ID: params.ID, Owner: fromAddress, Amount: params.Amount, } - // A single output coin, unlocked, with original owner - // TODO: make this configurable - revertCoin := &types.NotoCoin{ - Salt: tktypes.Bytes32(tktypes.RandBytes(32)), - Owner: fromAddress, - Amount: params.Amount, - } - lockedCoinState, err := h.noto.makeNewLockedCoinState(lockedCoin, []string{notary, tx.Transaction.From}) if err != nil { return nil, err } - revertState, err := h.noto.makeNewCoinState(revertCoin, []string{notary, tx.Transaction.From}) - if err != nil { - return nil, err + + recipientCoins := make([]*types.NotoCoin, len(params.Recipients)) + recipientStates := make([]*prototk.NewState, len(params.Recipients)) + for i, recipient := range params.Recipients { + recipientAddress, err := h.noto.findEthAddressVerifier(ctx, recipient.Recipient, recipient.Recipient, req.ResolvedVerifiers) + if err != nil { + return nil, err + } + // A single output coin, unlocked, with specified owner + // TODO: make this configurable + recipientCoins[i] = &types.NotoCoin{ + Salt: tktypes.Bytes32(tktypes.RandBytes(32)), + Owner: recipientAddress, + Amount: params.Amount, + } + recipientStates[i], err = h.noto.makeNewCoinState(recipientCoins[i], []string{notary, tx.Transaction.From, recipient.Recipient}) + if err != nil { + return nil, err + } } outputCoins := []*types.NotoCoin{} - outputStates := []*prototk.NewState{lockedCoinState, revertState} + outputStates := []*prototk.NewState{lockedCoinState} + outputStates = append(outputStates, recipientStates...) if total.Cmp(params.Amount.Int()) == 1 { remainder := big.NewInt(0).Sub(total, params.Amount.Int()) @@ -127,7 +150,11 @@ func (h *lockHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, outputStates = append(outputStates, returnedStates...) } - encodedLock, err := h.noto.encodeLock(ctx, tx.ContractAddress, inputCoins, outputCoins, lockedCoin, revertCoin) + encodedTransfer, err := h.noto.encodeTransferUnmasked(ctx, tx.ContractAddress, inputCoins, outputCoins) + if err != nil { + return nil, err + } + encodedLock, err := h.noto.encodeLock(ctx, tx.ContractAddress, lockedCoin, recipientCoins) if err != nil { return nil, err } @@ -135,7 +162,16 @@ func (h *lockHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, attestation := []*prototk.AttestationRequest{ // Sender confirms the initial request with a signature { - Name: "sender", + Name: "sender_transfer", + AttestationType: prototk.AttestationType_SIGN, + Algorithm: algorithms.ECDSA_SECP256K1, + VerifierType: verifiers.ETH_ADDRESS, + Payload: encodedTransfer, + PayloadType: signpayloads.OPAQUE_TO_RSV, + Parties: []string{req.Transaction.From}, + }, + { + Name: "sender_lock", AttestationType: prototk.AttestationType_SIGN, Algorithm: algorithms.ECDSA_SECP256K1, VerifierType: verifiers.ETH_ADDRESS, @@ -165,9 +201,11 @@ func (h *lockHandler) Assemble(ctx context.Context, tx *types.ParsedTransaction, } func (h *lockHandler) Endorse(ctx context.Context, tx *types.ParsedTransaction, req *prototk.EndorseTransactionRequest) (*prototk.EndorseTransactionResponse, error) { + params := tx.Params.(*types.LockParams) + lockedState := req.Outputs[0] - revertState := req.Outputs[1] - remainderStates := req.Outputs[2:] + recipientStates := req.Outputs[1 : 1+len(params.Recipients)] + remainderStates := req.Outputs[1+len(params.Recipients):] if lockedState.SchemaId != h.noto.LockedCoinSchemaID() { return nil, i18n.NewError(ctx, msgs.MsgUnexpectedSchema, lockedState.SchemaId) @@ -177,19 +215,22 @@ func (h *lockHandler) Endorse(ctx context.Context, tx *types.ParsedTransaction, return nil, err } - if revertState.SchemaId != h.noto.CoinSchemaID() { - return nil, i18n.NewError(ctx, msgs.MsgUnexpectedSchema, revertState.SchemaId) - } - revertCoin, err := h.noto.unmarshalCoin(revertState.StateDataJson) - if err != nil { - return nil, err + recipientCoins := make([]*types.NotoCoin, len(recipientStates)) + for i, state := range recipientStates { + if state.SchemaId != h.noto.CoinSchemaID() { + return nil, i18n.NewError(ctx, msgs.MsgUnexpectedSchema, state.SchemaId) + } + recipientCoins[i], err = h.noto.unmarshalCoin(state.StateDataJson) + if err != nil { + return nil, err + } } coins, err := h.noto.gatherCoins(ctx, req.Inputs, remainderStates) if err != nil { return nil, err } - if err := h.noto.validateLockAmounts(ctx, coins, lockedCoin, revertCoin); err != nil { + if err := h.noto.validateLockAmounts(ctx, coins, lockedCoin, recipientCoins); err != nil { return nil, err } if err := h.noto.validateOwners(ctx, tx, req, coins); err != nil { @@ -203,13 +244,13 @@ func (h *lockHandler) Endorse(ctx context.Context, tx *types.ParsedTransaction, if !lockedCoin.Owner.Equals(fromAddress) { return nil, i18n.NewError(ctx, msgs.MsgStateWrongOwner, lockedState.Id, tx.Transaction.From) } - if !revertCoin.Owner.Equals(fromAddress) { - return nil, i18n.NewError(ctx, msgs.MsgStateWrongOwner, revertState.Id, tx.Transaction.From) - } if req.EndorsementRequest.Name == "notary" { - // Notary checks the signature from the sender, then submits the transaction - if err := h.noto.validateLockSignature(ctx, tx, req, coins, lockedCoin, revertCoin); err != nil { + // Notary checks the signatures from the sender, then submits the transaction + if err := h.noto.validateTransferSignature(ctx, tx, "sender_transfer", req, coins); err != nil { + return nil, err + } + if err := h.noto.validateLockSignature(ctx, tx, "sender_lock", req, lockedCoin, recipientCoins); err != nil { return nil, err } return &prototk.EndorseTransactionResponse{ @@ -228,18 +269,36 @@ func (h *lockHandler) baseLedgerTransfer(ctx context.Context, tx *types.ParsedTr inputs[i] = state.Id } - lockedOutput := req.OutputStates[0].Id - revertOutput := req.OutputStates[1].Id - remainderOutputs := make([]string, len(req.OutputStates)-2) - for i, state := range req.OutputStates[2:] { + lockedOutput, err := tktypes.ParseBytes32Ctx(ctx, req.OutputStates[0].Id) + if err != nil { + return nil, err + } + + lockOutcomes := make([]*LockOutcome, len(inputParams.Recipients)) + remainderOutputs := make([]string, len(req.OutputStates)-len(inputParams.Recipients)-1) + for i, state := range req.OutputStates[1 : 1+len(inputParams.Recipients)] { + id, err := tktypes.ParseBytes32Ctx(ctx, state.Id) + if err != nil { + return nil, err + } + lockOutcomes[i] = &LockOutcome{ + Ref: inputParams.Recipients[i].Ref, + State: id, + } + } + for i, state := range req.OutputStates[1+len(inputParams.Recipients):] { remainderOutputs[i] = state.Id } - // Include the signature from the sender + // Include the signatures 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, i18n.NewError(ctx, msgs.MsgAttestationNotFound, "sender") + transferSignature := domain.FindAttestation("sender_transfer", req.AttestationResult) + if transferSignature == nil { + return nil, i18n.NewError(ctx, msgs.MsgAttestationNotFound, "sender_transfer") + } + lockSignature := domain.FindAttestation("sender_lock", req.AttestationResult) + if lockSignature == nil { + return nil, i18n.NewError(ctx, msgs.MsgAttestationNotFound, "sender_lock") } data, err := h.noto.encodeTransactionData(ctx, req.Transaction, req.InfoStates) @@ -247,17 +306,18 @@ func (h *lockHandler) baseLedgerTransfer(ctx context.Context, tx *types.ParsedTr return nil, err } params := &NotoTransferAndLockParams{ - Inputs: inputs, - UnlockedOutputs: remainderOutputs, - LockedOutput: lockedOutput, - Lock: LockInput{ - Outcomes: []LockOutcome{ - {Ref: 0, State: revertOutput}, - }, - Delegate: inputParams.Delegate, + Transfer: NotoTransferParamsNoData{ + Inputs: inputs, + Outputs: remainderOutputs, + Signature: transferSignature.Payload, + }, + Lock: NotoLockParamsNoData{ + Locked: lockedOutput, + Outcomes: lockOutcomes, + Delegate: inputParams.Delegate, + Signature: lockSignature.Payload, }, - Signature: signature.Payload, - Data: data, + Data: data, } paramsJSON, err := json.Marshal(params) if err != nil { diff --git a/domains/noto/internal/noto/handler_mint.go b/domains/noto/internal/noto/handler_mint.go index 93a146033..b7516dc8f 100644 --- a/domains/noto/internal/noto/handler_mint.go +++ b/domains/noto/internal/noto/handler_mint.go @@ -144,7 +144,7 @@ func (h *mintHandler) Endorse(ctx context.Context, tx *types.ParsedTransaction, } // Notary checks the signature from the sender, then submits the transaction - if err := h.noto.validateTransferSignature(ctx, tx, req, coins); err != nil { + if err := h.noto.validateTransferSignature(ctx, tx, "sender", req, coins); err != nil { return nil, err } return &prototk.EndorseTransactionResponse{ diff --git a/domains/noto/internal/noto/handler_transfer.go b/domains/noto/internal/noto/handler_transfer.go index 3eb52eb28..ca232efbb 100644 --- a/domains/noto/internal/noto/handler_transfer.go +++ b/domains/noto/internal/noto/handler_transfer.go @@ -173,7 +173,7 @@ func (h *transferHandler) Endorse(ctx context.Context, tx *types.ParsedTransacti case types.NotoVariantDefault: if req.EndorsementRequest.Name == "notary" { // Notary checks the signature from the sender, then submits the transaction - if err := h.noto.validateTransferSignature(ctx, tx, req, coins); err != nil { + if err := h.noto.validateTransferSignature(ctx, tx, "sender", req, coins); err != nil { return nil, err } return &prototk.EndorseTransactionResponse{ diff --git a/domains/noto/internal/noto/handlers.go b/domains/noto/internal/noto/handlers.go index bb7b7f159..341c887e7 100644 --- a/domains/noto/internal/noto/handlers.go +++ b/domains/noto/internal/noto/handlers.go @@ -84,7 +84,7 @@ func (n *Noto) validateBurnAmounts(ctx context.Context, params *types.BurnParams } // Check that a lock produces a locked coin and a revert coin, both matching the difference between the inputs and outputs -func (n *Noto) validateLockAmounts(ctx context.Context, coins *gatheredCoins, lockedCoin *types.NotoLockedCoin, revertCoin *types.NotoCoin) error { +func (n *Noto) validateLockAmounts(ctx context.Context, coins *gatheredCoins, lockedCoin *types.NotoLockedCoin, recipientCoins []*types.NotoCoin) error { if len(coins.inCoins) == 0 { return i18n.NewError(ctx, msgs.MsgInvalidInputs, "lock", coins.inCoins) } @@ -92,20 +92,22 @@ func (n *Noto) validateLockAmounts(ctx context.Context, coins *gatheredCoins, lo if amount.Cmp(lockedCoin.Amount.Int()) != 0 { return i18n.NewError(ctx, msgs.MsgInvalidAmount, "lock", lockedCoin.Amount.Int().Text(10), amount.Text(10)) } - if lockedCoin.Amount.Int().Cmp(revertCoin.Amount.Int()) != 0 { - return i18n.NewError(ctx, msgs.MsgInvalidAmount, "lock", lockedCoin.Amount.Int().Text(10), revertCoin.Amount.Int().Text(10)) + for _, coin := range recipientCoins { + if lockedCoin.Amount.Int().Cmp(coin.Amount.Int()) != 0 { + return i18n.NewError(ctx, msgs.MsgInvalidAmount, "lock", lockedCoin.Amount.Int().Text(10), coin.Amount.Int().Text(10)) + } } return nil } // Check that the sender of a transfer provided a signature on the input transaction details -func (n *Noto) validateTransferSignature(ctx context.Context, tx *types.ParsedTransaction, req *prototk.EndorseTransactionRequest, coins *gatheredCoins) error { - signature := domain.FindAttestation("sender", req.Signatures) +func (n *Noto) validateTransferSignature(ctx context.Context, tx *types.ParsedTransaction, name string, req *prototk.EndorseTransactionRequest, coins *gatheredCoins) error { + signature := domain.FindAttestation(name, req.Signatures) if signature == nil { - return i18n.NewError(ctx, msgs.MsgAttestationNotFound, "sender") + return i18n.NewError(ctx, msgs.MsgAttestationNotFound, name) } if signature.Verifier.Lookup != tx.Transaction.From { - return i18n.NewError(ctx, msgs.MsgAttestationUnexpected, "sender", tx.Transaction.From, signature.Verifier.Lookup) + return i18n.NewError(ctx, msgs.MsgAttestationUnexpected, name, tx.Transaction.From, signature.Verifier.Lookup) } encodedTransfer, err := n.encodeTransferUnmasked(ctx, tx.ContractAddress, coins.inCoins, coins.outCoins) if err != nil { @@ -116,21 +118,21 @@ func (n *Noto) validateTransferSignature(ctx context.Context, tx *types.ParsedTr return err } if recoveredSignature.String() != signature.Verifier.Verifier { - return i18n.NewError(ctx, msgs.MsgSignatureDoesNotMatch, "sender", signature.Verifier.Verifier, recoveredSignature.String()) + return i18n.NewError(ctx, msgs.MsgSignatureDoesNotMatch, name, signature.Verifier.Verifier, recoveredSignature.String()) } return nil } // Check that the sender of a lock provided a signature on the input transaction details -func (n *Noto) validateLockSignature(ctx context.Context, tx *types.ParsedTransaction, req *prototk.EndorseTransactionRequest, coins *gatheredCoins, lockedCoin *types.NotoLockedCoin, revertCoin *types.NotoCoin) error { - signature := domain.FindAttestation("sender", req.Signatures) +func (n *Noto) validateLockSignature(ctx context.Context, tx *types.ParsedTransaction, name string, req *prototk.EndorseTransactionRequest, lockedCoin *types.NotoLockedCoin, recipientCoins []*types.NotoCoin) error { + signature := domain.FindAttestation(name, req.Signatures) if signature == nil { - return i18n.NewError(ctx, msgs.MsgAttestationNotFound, "sender") + return i18n.NewError(ctx, msgs.MsgAttestationNotFound, name) } if signature.Verifier.Lookup != tx.Transaction.From { - return i18n.NewError(ctx, msgs.MsgAttestationUnexpected, "sender", tx.Transaction.From, signature.Verifier.Lookup) + return i18n.NewError(ctx, msgs.MsgAttestationUnexpected, name, tx.Transaction.From, signature.Verifier.Lookup) } - encodedLock, err := n.encodeLock(ctx, tx.ContractAddress, coins.inCoins, coins.outCoins, lockedCoin, revertCoin) + encodedLock, err := n.encodeLock(ctx, tx.ContractAddress, lockedCoin, recipientCoins) if err != nil { return err } @@ -139,7 +141,7 @@ func (n *Noto) validateLockSignature(ctx context.Context, tx *types.ParsedTransa return err } if recoveredSignature.String() != signature.Verifier.Verifier { - return i18n.NewError(ctx, msgs.MsgSignatureDoesNotMatch, "sender", signature.Verifier.Verifier, recoveredSignature.String()) + return i18n.NewError(ctx, msgs.MsgSignatureDoesNotMatch, name, signature.Verifier.Verifier, recoveredSignature.String()) } return nil } diff --git a/domains/noto/internal/noto/noto.go b/domains/noto/internal/noto/noto.go index af36db9ae..1b9714255 100644 --- a/domains/noto/internal/noto/noto.go +++ b/domains/noto/internal/noto/noto.go @@ -87,6 +87,12 @@ type NotoTransferParams struct { Data tktypes.HexBytes `json:"data"` } +type NotoTransferParamsNoData struct { + Inputs []string `json:"inputs"` + Outputs []string `json:"outputs"` + Signature tktypes.HexBytes `json:"signature"` +} + type NotoApproveTransferParams struct { Delegate *tktypes.EthAddress `json:"delegate"` TXHash tktypes.HexBytes `json:"txhash"` @@ -94,23 +100,22 @@ type NotoApproveTransferParams struct { Data tktypes.HexBytes `json:"data"` } -type LockInput struct { - Outcomes []LockOutcome `json:"outcomes"` - Delegate *tktypes.EthAddress `json:"delegate"` +type NotoLockParamsNoData struct { + Locked tktypes.Bytes32 `json:"locked"` + Outcomes []*LockOutcome `json:"outcomes"` + Delegate *tktypes.EthAddress `json:"delegate"` + Signature tktypes.HexBytes `json:"signature"` } type LockOutcome struct { Ref tktypes.HexUint64 `json:"ref"` - State string `json:"state"` + State tktypes.Bytes32 `json:"state"` } type NotoTransferAndLockParams struct { - Inputs []string `json:"inputs"` - UnlockedOutputs []string `json:"unlockedOutputs"` - LockedOutput string `json:"lockedOutput"` - Lock LockInput `json:"lock"` - Signature tktypes.HexBytes `json:"signature"` - Data tktypes.HexBytes `json:"data"` + Transfer NotoTransferParamsNoData `json:"transfer"` + Lock NotoLockParamsNoData `json:"lock"` + Data tktypes.HexBytes `json:"data"` } type NotoDelegateLockParams struct { diff --git a/domains/noto/internal/noto/states.go b/domains/noto/internal/noto/states.go index 30b1439b8..8c849403a 100644 --- a/domains/noto/internal/noto/states.go +++ b/domains/noto/internal/noto/states.go @@ -68,10 +68,8 @@ var NotoTransferMaskedTypeSet = eip712.TypeSet{ var NotoLockTypeSet = eip712.TypeSet{ "Lock": { - {Name: "inputs", Type: "Coin[]"}, - {Name: "outputs", Type: "Coin[]"}, {Name: "lockedCoin", Type: "LockedCoin"}, - {Name: "revertCoin", Type: "Coin"}, + {Name: "recipientCoins", Type: "Coin[]"}, }, "LockedCoin": { {Name: "id", Type: "bytes32"}, @@ -268,42 +266,27 @@ func (n *Noto) encodeTransferMasked(ctx context.Context, contract *ethtypes.Addr }) } -func (n *Noto) encodeLock(ctx context.Context, contract *ethtypes.Address0xHex, inputs, outputs []*types.NotoCoin, lockedCoin *types.NotoLockedCoin, revertCoin *types.NotoCoin) (ethtypes.HexBytes0xPrefix, error) { - messageInputs := make([]any, len(inputs)) - for i, input := range inputs { - messageInputs[i] = map[string]any{ - "salt": input.Salt, - "owner": input.Owner, - "amount": input.Amount.String(), - } - } - messageOutputs := make([]any, len(outputs)) - for i, output := range outputs { - messageOutputs[i] = map[string]any{ - "salt": output.Salt, - "owner": output.Owner, - "amount": output.Amount.String(), - } - } +func (n *Noto) encodeLock(ctx context.Context, contract *ethtypes.Address0xHex, lockedCoin *types.NotoLockedCoin, recipientCoins []*types.NotoCoin) (ethtypes.HexBytes0xPrefix, error) { lockedCoinOutput := map[string]any{ "id": lockedCoin.ID, "owner": lockedCoin.Owner, "amount": lockedCoin.Amount, } - revertCoinOutput := map[string]any{ - "salt": revertCoin.Salt, - "owner": revertCoin.Owner, - "amount": revertCoin.Amount, + recipientOutputs := make([]any, len(recipientCoins)) + for i, coin := range recipientCoins { + recipientOutputs[i] = map[string]any{ + "salt": coin.Salt, + "owner": coin.Owner, + "amount": coin.Amount, + } } return eip712.EncodeTypedDataV4(ctx, &eip712.TypedData{ Types: NotoLockTypeSet, PrimaryType: "Lock", Domain: n.eip712Domain(contract), Message: map[string]interface{}{ - "inputs": messageInputs, - "outputs": messageOutputs, - "lockedCoin": lockedCoinOutput, - "revertCoin": revertCoinOutput, + "lockedCoin": lockedCoinOutput, + "recipientCoins": recipientOutputs, }, }) } diff --git a/domains/noto/pkg/types/abi.go b/domains/noto/pkg/types/abi.go index 354c89034..7a78b59c4 100644 --- a/domains/noto/pkg/types/abi.go +++ b/domains/noto/pkg/types/abi.go @@ -82,9 +82,16 @@ type ApproveParams struct { } type LockParams struct { - Amount *tktypes.HexUint256 `json:"amount"` - Delegate *tktypes.EthAddress `json:"delegate"` - Data tktypes.HexBytes `json:"data"` + ID tktypes.Bytes32 `json:"id"` + Amount *tktypes.HexUint256 `json:"amount"` + Delegate *tktypes.EthAddress `json:"delegate"` + Recipients []LockRecipient `json:"recipients"` + Data tktypes.HexBytes `json:"data"` +} + +type LockRecipient struct { + Ref tktypes.HexUint64 `json:"ref"` + Recipient string `json:"recipient"` } type ApproveExtraParams struct { diff --git a/solidity/contracts/domains/interfaces/INoto.sol b/solidity/contracts/domains/interfaces/INoto.sol index 0d4b177bb..148f079c2 100644 --- a/solidity/contracts/domains/interfaces/INoto.sol +++ b/solidity/contracts/domains/interfaces/INoto.sol @@ -24,16 +24,24 @@ interface INoto { event NotoUnlock(bytes32 locked, bytes32 output, bytes data); - struct LockInput { - LockOutcome[] outcomes; - address delegate; - } - struct LockOutcome { uint64 ref; bytes32 state; } + struct TransferParams { + bytes32[] inputs; + bytes32[] outputs; + bytes signature; + } + + struct LockParams { + bytes32 locked; + LockOutcome[] outcomes; + address delegate; + bytes signature; + } + function initialize( address notaryAddress, bytes calldata data @@ -68,11 +76,18 @@ interface INoto { function createLock( bytes32 locked, - LockInput calldata lock, + LockOutcome[] calldata outcomes, + address delegate, bytes calldata signature, bytes calldata data ) external; + function transferAndLock( + TransferParams calldata transfer, + LockParams calldata lock, + bytes calldata data + ) external; + function updateLock( bytes32 locked, LockOutcome[] calldata outcomes @@ -81,13 +96,4 @@ interface INoto { function delegateLock(bytes32 locked, address delegate) external; function unlock(bytes32 locked, uint64 outcome) external; - - function transferAndLock( - bytes32[] calldata inputs, - bytes32[] calldata unlockedOutputs, - bytes32 lockedOutput, - LockInput calldata lock, - bytes calldata signature, - bytes calldata data - ) external; } diff --git a/solidity/contracts/domains/interfaces/INotoPrivate.sol b/solidity/contracts/domains/interfaces/INotoPrivate.sol index 46d60c0dc..9a4ce2456 100644 --- a/solidity/contracts/domains/interfaces/INotoPrivate.sol +++ b/solidity/contracts/domains/interfaces/INotoPrivate.sol @@ -19,10 +19,7 @@ interface INotoPrivate { bytes calldata data ) external; - function burn( - uint256 amount, - bytes calldata data - ) external; + function burn(uint256 amount, bytes calldata data) external; function approveTransfer( StateEncoded[] calldata inputs, @@ -32,11 +29,23 @@ interface INotoPrivate { ) external; function lock( + bytes32 id, uint256 amount, address delegate, + LockRecipient[] calldata recipients, bytes calldata data ) external; + function updateLock( + bytes32 id, + LockRecipient[] calldata recipients + ) external; + + struct LockRecipient { + uint64 ref; + string recipient; + } + struct StateEncoded { bytes id; string domain; diff --git a/solidity/contracts/domains/noto/Noto.sol b/solidity/contracts/domains/noto/Noto.sol index e48e54ca1..d98946f86 100644 --- a/solidity/contracts/domains/noto/Noto.sol +++ b/solidity/contracts/domains/noto/Noto.sol @@ -286,23 +286,27 @@ contract Noto is EIP712Upgradeable, UUPSUpgradeable, INoto { /** * @dev Create a new locked state that can only be unlocked by a specific delegate. + * When the locked state is unlocked, it will be spent and replaced by exactly one new state from + * a set of pre-approved outcomes. * * @param locked new output state to generate, representing locked value - * @param lock details on the lock and any possible outcomes + * @param outcomes possible outcomes of the lock + * @param delegate the address that is authorized to unlock the lock * @param signature EIP-712 signature on the original request that spawned this transaction * @param data any additional transaction data (opaque to the blockchain) */ function createLock( bytes32 locked, - LockInput calldata lock, + LockOutcome[] calldata outcomes, + address delegate, bytes calldata signature, bytes calldata data ) public virtual override onlyNotary { LockDetail storage stored = _locks[locked]; - for (uint256 i = 0; i < lock.outcomes.length; i++) { - stored.outcomes[lock.outcomes[i].ref] = lock.outcomes[i].state; + for (uint256 i = 0; i < outcomes.length; i++) { + stored.outcomes[outcomes[i].ref] = outcomes[i].state; } - stored.delegate = lock.delegate; + stored.delegate = delegate; stored.initialized = true; stored.data = data; emit NotoLock(locked, signature, data); @@ -310,24 +314,25 @@ contract Noto is EIP712Upgradeable, UUPSUpgradeable, INoto { /** * @dev Perform a transfer and a lock simultaneously. - * - * @param inputs as per transfer() - * @param unlockedOutputs as per transfer() - * @param lockedOutput as per createLock() - * @param lock as per createLock() - * @param signature as per createLock() - * @param data as per createLock() */ function transferAndLock( - bytes32[] calldata inputs, - bytes32[] calldata unlockedOutputs, - bytes32 lockedOutput, - LockInput calldata lock, - bytes calldata signature, + TransferParams calldata transfer_, + LockParams calldata lock, bytes calldata data ) external virtual override onlyNotary { - _transfer(inputs, unlockedOutputs, signature, data); - createLock(lockedOutput, lock, signature, data); + _transfer( + transfer_.inputs, + transfer_.outputs, + transfer_.signature, + data + ); + createLock( + lock.locked, + lock.outcomes, + lock.delegate, + lock.signature, + data + ); } /**