diff --git a/token/core/zkatdlog/nogh/v1/audit/auditor.go b/token/core/zkatdlog/nogh/v1/audit/auditor.go index 843e04e44d..3d70011eb6 100644 --- a/token/core/zkatdlog/nogh/v1/audit/auditor.go +++ b/token/core/zkatdlog/nogh/v1/audit/auditor.go @@ -7,7 +7,6 @@ SPDX-License-Identifier: Apache-2.0 package audit import ( - "bytes" "context" math "github.com/IBM/mathlib" @@ -122,7 +121,6 @@ func (a *Auditor) Check( inputTokens [][]*token.Token, txID driver.TokenRequestAnchor, ) error { - // TODO: inputTokens should be checked against the actions // De-obfuscate issue requests a.Logger.DebugfContext(ctx, "Get audit info for %d issues", len(tokenRequest.Issues)) outputsFromIssue, identitiesFromIssue, err := a.GetAuditInfoForIssues(tokenRequest.Issues, tokenRequestMetadata.Issues) @@ -144,7 +142,7 @@ func (a *Auditor) Check( } // De-obfuscate transfer requests a.Logger.DebugfContext(ctx, "Get audit info for %d transfers", len(tokenRequest.Transfers)) - auditableInputs, outputsFromTransfer, err := a.GetAuditInfoForTransfers(tokenRequest.Transfers, tokenRequestMetadata.Transfers, inputTokens) + auditableInputs, outputsFromTransfer, err := a.GetAuditInfoForTransfers(ctx, tokenRequest.Transfers, tokenRequestMetadata.Transfers, inputTokens) if err != nil { return errors.Wrapf(err, "failed getting audit info for transfers for [%s]", txID) } @@ -258,7 +256,7 @@ func (a *Auditor) InspectIdentity(ctx context.Context, matcher InfoMatcher, iden // If identity is provided in metadata, it must match the one in the action if len(identity.IdentityFromMeta) != 0 { // enforce equality - if !bytes.Equal(identity.IdentityFromMeta, identity.Identity) { + if !identity.IdentityFromMeta.Equal(identity.Identity) { return errors.Errorf("failed to inspect identity at index [%d]: identity does not match the identity form metadata", index) } } @@ -333,7 +331,7 @@ func (a *Auditor) GetAuditInfoForIssues(issues [][]byte, issueMetadata []*driver // GetAuditInfoForTransfers returns an array of InspectableToken for each transfer action. // It takes an array of serialized transfer actions, an array of transfer metadata and input tokens. -func (a *Auditor) GetAuditInfoForTransfers(transfers [][]byte, metadata []*driver.TransferMetadata, inputs [][]*token.Token) ([][]*InspectableToken, [][]*InspectableToken, error) { +func (a *Auditor) GetAuditInfoForTransfers(ctx context.Context, transfers [][]byte, metadata []*driver.TransferMetadata, inputs [][]*token.Token) ([][]*InspectableToken, [][]*InspectableToken, error) { if len(transfers) != len(metadata) { return nil, nil, errors.Errorf("number of transfers does not match the number of provided metadata") } @@ -363,12 +361,35 @@ func (a *Auditor) GetAuditInfoForTransfers(transfers [][]byte, metadata []*drive } } ta := &transfer.Action{} - err := ta.Deserialize(transfers[k]) - if err != nil { + if err := ta.Deserialize(transfers[k]); err != nil { return nil, nil, err } - if len(ta.Outputs) != len(transferMetadata.Outputs) { - return nil, nil, errors.Errorf("number of outputs does not match the number of output metadata [%d]!=[%d]", len(ta.Outputs), len(transferMetadata.Outputs)) + // Validate structural consistency between action and metadata (counts, signers, issuer). + if err := transferMetadata.Match(ta); err != nil { + return nil, nil, errors.Wrapf(err, "transfer at index [%d]", k) + } + // Build serialized forms and validate input tokens match the action. + actionInputSer := make([][]byte, len(ta.Inputs)) + for i, actionInput := range ta.Inputs { + if actionInput.Token == nil { + continue // upgrade-witness input: no zkatdlog commitment to compare + } + ser, err := actionInput.Token.Serialize() + if err != nil { + return nil, nil, errors.Errorf("failed serializing action input at [%d][%d]: %s", k, i, err) + } + actionInputSer[i] = ser + } + ledgerInputSer := make([][]byte, len(inputs[k])) + for i, t := range inputs[k] { + ser, err := t.Serialize() + if err != nil { + return nil, nil, errors.Errorf("failed serializing ledger input at [%d][%d]: %s", k, i, err) + } + ledgerInputSer[i] = ser + } + if err := transferMetadata.MatchInputs(actionInputSer, ledgerInputSer); err != nil { + return nil, nil, errors.Wrapf(err, "transfer at index [%d]", k) } // Process auditable outputs outputs[k] = make([]*InspectableToken, len(ta.Outputs)) @@ -376,16 +397,14 @@ func (a *Auditor) GetAuditInfoForTransfers(transfers [][]byte, metadata []*drive if ta.Outputs[i] == nil { return nil, nil, errors.Errorf("output token at index [%d] is nil", i) } - if transferMetadata.Outputs[i] == nil { return nil, nil, errors.Errorf("metadata for output token at index [%d] is nil", i) } ti := &token.Metadata{} - err = ti.Deserialize(transferMetadata.Outputs[i].OutputMetadata) + err := ti.Deserialize(transferMetadata.Outputs[i].OutputMetadata) if err != nil { return nil, nil, err } - // TODO: we need to check also how many recipients the output contains, and check them all in isolation and compatibility outputs[k][i], err = NewInspectableToken( ta.Outputs[i], transferMetadata.Outputs[i].OutputAuditInfo, @@ -396,6 +415,24 @@ func (a *Auditor) GetAuditInfoForTransfers(transfers [][]byte, metadata []*drive if err != nil { return nil, nil, err } + // Verify each receiver's audit info matches their identity for non-redeem outputs. + if !ta.Outputs[i].IsRedeem() { + if err := transferMetadata.Outputs[i].ValidateReceivers(); err != nil { + return nil, nil, errors.Wrapf(err, "output at index [%d][%d]", k, i) + } + for j, receiver := range transferMetadata.Outputs[i].Receivers { + identityToVerify := receiver.Identity + if identityToVerify.IsNone() { + identityToVerify = ta.Outputs[i].Owner + } + if err := a.InspectIdentity(ctx, a.InfoMatcher, &InspectableIdentity{ + Identity: identityToVerify, + AuditInfo: receiver.AuditInfo, + }, j); err != nil { + return nil, nil, errors.Wrapf(err, "failed inspecting receiver at index [%d][%d][%d]", k, i, j) + } + } + } } } diff --git a/token/core/zkatdlog/nogh/v1/audit/auditor_test.go b/token/core/zkatdlog/nogh/v1/audit/auditor_test.go index 76e1a10ee8..7570550bdd 100644 --- a/token/core/zkatdlog/nogh/v1/audit/auditor_test.go +++ b/token/core/zkatdlog/nogh/v1/audit/auditor_test.go @@ -128,11 +128,11 @@ func TestAuditor_Errors(t *testing.T) { // in the number of transfers, transfer metadata, or input tokens. t.Run("GetAuditInfoForTransfers length mismatch", func(t *testing.T) { _, _, auditor := setupAuditorTest(t) - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{{1}}, nil, nil) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{1}}, nil, nil) require.Error(t, err) require.Contains(t, err.Error(), "number of transfers does not match the number of provided metadata") - _, _, err = auditor.GetAuditInfoForTransfers([][]byte{{1}}, []*driver.TransferMetadata{{}}, nil) + _, _, err = auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{1}}, []*driver.TransferMetadata{{}}, nil) require.Error(t, err) require.Contains(t, err.Error(), "number of inputs does not match the number of provided metadata") }) @@ -280,7 +280,7 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) { // for a transfer is nil. t.Run("GetAuditInfoForTransfers nil input token", func(t *testing.T) { _, _, auditor := setupAuditorTest(t) - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{{}}}}, [][]*token.Token{{nil}}) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{{}}}}, [][]*token.Token{{nil}}) require.Error(t, err) require.Contains(t, err.Error(), "input[0][0] is nil") }) @@ -289,7 +289,7 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) { // metadata for a transfer input is nil. t.Run("GetAuditInfoForTransfers invalid input metadata", func(t *testing.T) { _, _, auditor := setupAuditorTest(t) - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{nil}}}, [][]*token.Token{{&token.Token{}}}) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{}}, []*driver.TransferMetadata{{Inputs: []*driver.TransferInputMetadata{nil}}}, [][]*token.Token{{&token.Token{}}}) require.Error(t, err) require.Contains(t, err.Error(), "invalid metadata for input[0][0]") }) @@ -299,7 +299,7 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) { t.Run("GetAuditInfoForTransfers transfer deserialization error", func(t *testing.T) { _, _, auditor := setupAuditorTest(t) inputs := []*driver.TransferInputMetadata{{Senders: []*driver.AuditableIdentity{{AuditInfo: []byte{1}}}}} - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{{1, 2, 3}}, []*driver.TransferMetadata{{Inputs: inputs}}, [][]*token.Token{{&token.Token{}}}) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{{1, 2, 3}}, []*driver.TransferMetadata{{Inputs: inputs}}, [][]*token.Token{{&token.Token{}}}) require.Error(t, err) require.Contains(t, err.Error(), "failed to deserialize transfer action") }) @@ -308,22 +308,22 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) { // of outputs in a transfer action does not match the number of provided output metadata. t.Run("GetAuditInfoForTransfers output count mismatch", func(t *testing.T) { _, pp, auditor := setupAuditorTest(t) - transfer, meta, _ := createTransfer(t, pp) + transfer, meta, tokens := createTransfer(t, pp) raw, _ := transfer.Serialize() meta.Outputs = meta.Outputs[:len(meta.Outputs)-1] - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}}) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) require.Error(t, err) - require.Contains(t, err.Error(), "number of outputs does not match the number of output metadata") + require.Contains(t, err.Error(), "expected [1] outputs but got [2]") }) // GetAuditInfoForTransfers nil output token tests that an error is returned when one of the outputs // in a transfer action is nil. t.Run("GetAuditInfoForTransfers nil output token", func(t *testing.T) { _, pp, auditor := setupAuditorTest(t) - transfer, meta, _ := createTransfer(t, pp) + transfer, meta, tokens := createTransfer(t, pp) transfer.Outputs[0] = nil raw, _ := transfer.Serialize() - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}}) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) require.Error(t, err) require.Contains(t, err.Error(), "output token at index [0] is nil") }) @@ -332,10 +332,10 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) { // for a transfer output is nil. t.Run("GetAuditInfoForTransfers nil output metadata", func(t *testing.T) { _, pp, auditor := setupAuditorTest(t) - transfer, meta, _ := createTransfer(t, pp) + transfer, meta, tokens := createTransfer(t, pp) meta.Outputs[0] = nil raw, _ := transfer.Serialize() - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}}) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) require.Error(t, err) require.Contains(t, err.Error(), "metadata for output token at index [0] is nil") }) @@ -344,13 +344,65 @@ func TestAuditor_GetAuditInfo_Errors(t *testing.T) { // the metadata for a transfer output cannot be deserialized. t.Run("GetAuditInfoForTransfers output metadata deserialization error", func(t *testing.T) { _, pp, auditor := setupAuditorTest(t) - transfer, meta, _ := createTransfer(t, pp) + transfer, meta, tokens := createTransfer(t, pp) meta.Outputs[0].OutputMetadata = []byte{1, 2, 3} raw, _ := transfer.Serialize() - _, _, err := auditor.GetAuditInfoForTransfers([][]byte{raw}, []*driver.TransferMetadata{meta}, [][]*token.Token{{&token.Token{}, &token.Token{}}}) + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) require.Error(t, err) require.Contains(t, err.Error(), "failed deserializing metadata") }) + + // GetAuditInfoForTransfers input token commitment mismatch tests that an error is returned when + // an input token's serialized form does not match the one embedded in the transfer action. + t.Run("GetAuditInfoForTransfers input token commitment mismatch", func(t *testing.T) { + _, pp, auditor := setupAuditorTest(t) + transfer, meta, tokens := createTransfer(t, pp) + raw, _ := transfer.Serialize() + tokens[0][0].Data = pp.PedersenGenerators[0] // tamper with commitment + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) + require.Error(t, err) + require.Contains(t, err.Error(), "does not match the transfer action") + }) + + // GetAuditInfoForTransfers input token owner mismatch tests that an error is returned when + // an input token's serialized form does not match the one embedded in the transfer action. + t.Run("GetAuditInfoForTransfers input token owner mismatch", func(t *testing.T) { + _, pp, auditor := setupAuditorTest(t) + transfer, meta, tokens := createTransfer(t, pp) + raw, _ := transfer.Serialize() + tokens[0][0].Owner = []byte("wrong-owner") // tamper with owner + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) + require.Error(t, err) + require.Contains(t, err.Error(), "does not match the transfer action") + }) + + // GetAuditInfoForTransfers no receivers for output tests that an error is returned when a + // non-redeemed output has no declared receivers. + t.Run("GetAuditInfoForTransfers no receivers for output", func(t *testing.T) { + _, pp, auditor := setupAuditorTest(t) + transfer, meta, tokens := createTransfer(t, pp) + raw, _ := transfer.Serialize() + meta.Outputs[0].Receivers = nil + _, _, err := auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) + require.Error(t, err) + require.Contains(t, err.Error(), "has no receivers") + }) + + // GetAuditInfoForTransfers receiver audit info mismatch tests that an error is returned when + // a receiver's audit info does not match the output owner. + t.Run("GetAuditInfoForTransfers receiver audit info mismatch", func(t *testing.T) { + _, pp, auditor := setupAuditorTest(t) + transfer, meta, tokens := createTransfer(t, pp) + _, differentAuditInfo := getIdemixInfo(t, "./testdata/bls12_381_bbs/idemix") + differentAuditInfoRaw, err := differentAuditInfo.Bytes() + require.NoError(t, err) + raw, err := transfer.Serialize() + require.NoError(t, err) + meta.Outputs[0].Receivers[0].AuditInfo = differentAuditInfoRaw + _, _, err = auditor.GetAuditInfoForTransfers(t.Context(), [][]byte{raw}, []*driver.TransferMetadata{meta}, tokens) + require.Error(t, err) + require.Contains(t, err.Error(), "failed inspecting receiver") + }) } // TestAuditor_Check_Errors tests error handling for the Check method, ensuring the auditor diff --git a/token/driver/match.go b/token/driver/match.go new file mode 100644 index 0000000000..695f757a70 --- /dev/null +++ b/token/driver/match.go @@ -0,0 +1,96 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package driver + +import ( + "bytes" + "slices" + + "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" +) + +// Match validates the structural consistency between this metadata and the provided issue action. +// It checks input/output counts, extra signers, and the issuer identity. +func (i *IssueMetadata) Match(action IssueAction) error { + if len(i.Inputs) != action.NumInputs() { + return errors.Errorf("expected [%d] inputs but got [%d]", len(i.Inputs), action.NumInputs()) + } + if len(i.Outputs) != action.NumOutputs() { + return errors.Errorf("expected [%d] outputs but got [%d]", len(i.Outputs), action.NumOutputs()) + } + extraSigners := action.ExtraSigners() + if len(i.ExtraSigners) != len(extraSigners) { + return errors.Errorf("expected [%d] extra signers but got [%d]", len(extraSigners), len(i.ExtraSigners)) + } + for idx, signer := range extraSigners { + if !slices.ContainsFunc(i.ExtraSigners, signer.Equal) { + return errors.Errorf("expected extra signer [%s] but got [%s]", signer, i.ExtraSigners[idx]) + } + } + if !i.Issuer.Identity.Equal(action.GetIssuer()) { + return errors.Errorf("expected issuer [%s] but got [%s]", i.Issuer.Identity, action.GetIssuer()) + } + + return nil +} + +// Match validates the structural consistency between this metadata and the provided transfer action. +// It checks input/output counts, extra signers, and the issuer identity. +func (t *TransferMetadata) Match(action TransferAction) error { + if len(t.Inputs) != action.NumInputs() { + return errors.Errorf("expected [%d] inputs but got [%d]", len(t.Inputs), action.NumInputs()) + } + if len(t.Outputs) != action.NumOutputs() { + return errors.Errorf("expected [%d] outputs but got [%d]", len(t.Outputs), action.NumOutputs()) + } + extraSigners := action.ExtraSigners() + if len(t.ExtraSigners) != len(extraSigners) { + return errors.Errorf("expected [%d] extra signers but got [%d]", len(t.ExtraSigners), len(extraSigners)) + } + for idx, signer := range extraSigners { + if !signer.Equal(t.ExtraSigners[idx]) { + return errors.Errorf("expected extra signer [%s] but got [%s]", t.ExtraSigners[idx], signer) + } + } + if !t.Issuer.Equal(action.GetIssuer()) { + return errors.Errorf("expected issuer [%s] but got [%s]", t.Issuer, action.GetIssuer().Bytes()) + } + + return nil +} + +// MatchInputs validates that the serialized action inputs match the serialized ledger tokens. +// Nil entries in serializedActionInputs are skipped (e.g., upgrade-witness inputs). +func (t *TransferMetadata) MatchInputs(serializedActionInputs [][]byte, serializedLedgerTokens [][]byte) error { + if len(serializedActionInputs) != len(serializedLedgerTokens) { + return errors.Errorf("action has [%d] inputs but [%d] tokens provided", len(serializedActionInputs), len(serializedLedgerTokens)) + } + for i, actionInput := range serializedActionInputs { + if actionInput == nil { + continue + } + if !bytes.Equal(actionInput, serializedLedgerTokens[i]) { + return errors.Errorf("input token at index [%d]: does not match the transfer action", i) + } + } + + return nil +} + +// ValidateReceivers checks that this output has at least one non-nil receiver declared. +func (t *TransferOutputMetadata) ValidateReceivers() error { + if len(t.Receivers) == 0 { + return errors.New("has no receivers") + } + for j, receiver := range t.Receivers { + if receiver == nil { + return errors.Errorf("receiver at index [%d] is nil", j) + } + } + + return nil +} diff --git a/token/driver/match_test.go b/token/driver/match_test.go new file mode 100644 index 0000000000..7cfdd50e04 --- /dev/null +++ b/token/driver/match_test.go @@ -0,0 +1,271 @@ +/* +Copyright IBM Corp. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package driver_test + +import ( + "testing" + + "github.com/hyperledger-labs/fabric-token-sdk/token/driver" + "github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIssueMetadataMatch(t *testing.T) { + makeMetadata := func() *driver.IssueMetadata { + return &driver.IssueMetadata{ + Issuer: driver.AuditableIdentity{Identity: driver.Identity("issuer")}, + Inputs: []*driver.IssueInputMetadata{{}, {}}, + Outputs: []*driver.IssueOutputMetadata{{}, {}}, + ExtraSigners: []driver.Identity{driver.Identity("signer1")}, + } + } + makeAction := func() *mock.IssueAction { + ia := &mock.IssueAction{} + ia.NumInputsReturns(2) + ia.NumOutputsReturns(2) + ia.ExtraSignersReturns([]driver.Identity{driver.Identity("signer1")}) + ia.GetIssuerReturns([]byte("issuer")) + + return ia + } + + tests := []struct { + name string + setupMeta func(*driver.IssueMetadata) + setupAct func(*mock.IssueAction) + wantErr string + }{ + { + name: "success", + }, + { + name: "input count mismatch", + setupAct: func(a *mock.IssueAction) { a.NumInputsReturns(3) }, + wantErr: "expected [2] inputs but got [3]", + }, + { + name: "output count mismatch", + setupAct: func(a *mock.IssueAction) { a.NumOutputsReturns(1) }, + wantErr: "expected [2] outputs but got [1]", + }, + { + name: "extra signer count mismatch", + setupAct: func(a *mock.IssueAction) { + a.ExtraSignersReturns([]driver.Identity{driver.Identity("s1"), driver.Identity("s2")}) + }, + wantErr: "expected [2] extra signers but got [1]", + }, + { + name: "extra signer value mismatch", + setupAct: func(a *mock.IssueAction) { + a.ExtraSignersReturns([]driver.Identity{driver.Identity("other")}) + }, + wantErr: "expected extra signer", + }, + { + name: "issuer mismatch", + setupAct: func(a *mock.IssueAction) { a.GetIssuerReturns([]byte("other")) }, + wantErr: "expected issuer", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + meta := makeMetadata() + action := makeAction() + if tc.setupMeta != nil { + tc.setupMeta(meta) + } + if tc.setupAct != nil { + tc.setupAct(action) + } + err := meta.Match(action) + if tc.wantErr == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + } + }) + } +} + +func TestTransferMetadataMatch(t *testing.T) { + makeMetadata := func() *driver.TransferMetadata { + return &driver.TransferMetadata{ + Inputs: []*driver.TransferInputMetadata{{}, {}}, + Outputs: []*driver.TransferOutputMetadata{{}, {}}, + ExtraSigners: []driver.Identity{driver.Identity("signer1")}, + Issuer: driver.Identity("issuer"), + } + } + makeAction := func() *mock.TransferAction { + ta := &mock.TransferAction{} + ta.NumInputsReturns(2) + ta.NumOutputsReturns(2) + ta.ExtraSignersReturns([]driver.Identity{driver.Identity("signer1")}) + ta.GetIssuerReturns(driver.Identity("issuer")) + + return ta + } + + tests := []struct { + name string + setupMeta func(*driver.TransferMetadata) + setupAct func(*mock.TransferAction) + wantErr string + }{ + { + name: "success", + }, + { + name: "input count mismatch", + setupAct: func(a *mock.TransferAction) { a.NumInputsReturns(3) }, + wantErr: "expected [2] inputs but got [3]", + }, + { + name: "output count mismatch", + setupAct: func(a *mock.TransferAction) { a.NumOutputsReturns(1) }, + wantErr: "expected [2] outputs but got [1]", + }, + { + name: "extra signer count mismatch", + setupAct: func(a *mock.TransferAction) { + a.ExtraSignersReturns([]driver.Identity{driver.Identity("s1"), driver.Identity("s2")}) + }, + wantErr: "expected [1] extra signers but got [2]", + }, + { + name: "extra signer value mismatch", + setupAct: func(a *mock.TransferAction) { + a.ExtraSignersReturns([]driver.Identity{driver.Identity("other")}) + }, + wantErr: "expected extra signer", + }, + { + name: "issuer mismatch", + setupAct: func(a *mock.TransferAction) { a.GetIssuerReturns(driver.Identity("other")) }, + wantErr: "expected issuer", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + meta := makeMetadata() + action := makeAction() + if tc.setupMeta != nil { + tc.setupMeta(meta) + } + if tc.setupAct != nil { + tc.setupAct(action) + } + err := meta.Match(action) + if tc.wantErr == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + } + }) + } +} + +func TestTransferMetadataMatchInputs(t *testing.T) { + tests := []struct { + name string + actionInputs [][]byte + ledgerTokens [][]byte + wantErr string + }{ + { + name: "success", + actionInputs: [][]byte{[]byte("token0"), []byte("token1")}, + ledgerTokens: [][]byte{[]byte("token0"), []byte("token1")}, + }, + { + name: "length mismatch", + actionInputs: [][]byte{[]byte("token0")}, + ledgerTokens: [][]byte{[]byte("token0"), []byte("token1")}, + wantErr: "action has [1] inputs but [2] tokens provided", + }, + { + name: "nil action input skipped", + actionInputs: [][]byte{nil, []byte("token1")}, + ledgerTokens: [][]byte{[]byte("anything"), []byte("token1")}, + }, + { + name: "byte mismatch at index", + actionInputs: [][]byte{[]byte("token0"), []byte("token1")}, + ledgerTokens: [][]byte{[]byte("token0"), []byte("different")}, + wantErr: "input token at index [1]: does not match the transfer action", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + meta := &driver.TransferMetadata{} + err := meta.MatchInputs(tc.actionInputs, tc.ledgerTokens) + if tc.wantErr == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + } + }) + } +} + +func TestTransferOutputMetadataValidateReceivers(t *testing.T) { + tests := []struct { + name string + receivers []*driver.AuditableIdentity + wantErr string + }{ + { + name: "success single receiver", + receivers: []*driver.AuditableIdentity{{Identity: driver.Identity("bob")}}, + }, + { + name: "success multiple receivers", + receivers: []*driver.AuditableIdentity{ + {Identity: driver.Identity("bob")}, + {Identity: driver.Identity("carol")}, + }, + }, + { + name: "no receivers empty slice", + wantErr: "has no receivers", + }, + { + name: "nil receivers slice", + receivers: nil, + wantErr: "has no receivers", + }, + { + name: "nil receiver at index", + receivers: []*driver.AuditableIdentity{ + {Identity: driver.Identity("bob")}, + nil, + }, + wantErr: "receiver at index [1] is nil", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + output := &driver.TransferOutputMetadata{Receivers: tc.receivers} + err := output.ValidateReceivers() + if tc.wantErr == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.wantErr) + } + }) + } +} diff --git a/token/metadata.go b/token/metadata.go index 5d3c67e2c2..9b511ac6c8 100644 --- a/token/metadata.go +++ b/token/metadata.go @@ -9,7 +9,6 @@ package token import ( "context" - "slices" "github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors" "github.com/hyperledger-labs/fabric-smart-client/platform/common/utils/collections" @@ -225,39 +224,11 @@ func (m *IssueMetadata) Match(action *IssueAction) error { if action == nil { return errors.New("can't match issue metadata to issue action: nil issue action") } - - // Validate the action's structure. if err := action.Validate(); err != nil { return errors.Wrap(err, "failed validating issue action") } - // Check that the number of inputs matches. - if len(m.Inputs) != action.NumInputs() { - return errors.Errorf("expected [%d] inputs but got [%d]", len(m.Inputs), action.NumInputs()) - } - - // Check that the number of outputs matches. - if len(m.Outputs) != action.NumOutputs() { - return errors.Errorf("expected [%d] outputs but got [%d]", len(m.Outputs), action.NumOutputs()) - } - - // Check that the extra signers are the same. - extraSigners := action.a.ExtraSigners() - if len(m.ExtraSigners) != len(extraSigners) { - return errors.Errorf("expected [%d] extra signers but got [%d]", len(extraSigners), len(m.ExtraSigners)) - } - for i, signer := range extraSigners { - if !slices.ContainsFunc(m.ExtraSigners, signer.Equal) { - return errors.Errorf("expected extra signer [%s] but got [%s]", signer, m.ExtraSigners[i]) - } - } - - // Check that the issuer identity matches. - if !m.Issuer.Identity.Equal(action.GetIssuer()) { - return errors.Errorf("expected issuer [%s] but got [%s]", m.Issuer.Identity, action.GetIssuer()) - } - - return nil + return m.IssueMetadata.Match(action.a) } // IsOutputAbsent returns true if the j-th output's metadata is absent (e.g., filtered out). @@ -280,39 +251,11 @@ func (m *TransferMetadata) Match(action *TransferAction) error { if action == nil { return errors.New("can't match transfer metadata to transfer action: nil issue action") } - - // Validate the action's structure. if err := action.Validate(); err != nil { return errors.Wrap(err, "failed validating issue action") } - // Check that the number of inputs matches. - if len(m.Inputs) != action.NumInputs() { - return errors.Errorf("expected [%d] inputs but got [%d]", len(m.Inputs), action.NumInputs()) - } - - // Check that the number of outputs matches. - if len(m.Outputs) != action.NumOutputs() { - return errors.Errorf("expected [%d] outputs but got [%d]", len(m.Outputs), action.NumOutputs()) - } - - // Check that the extra signers are the same. - extraSigners := action.ExtraSigners() - if len(m.ExtraSigners) != len(extraSigners) { - return errors.Errorf("expected [%d] extra signers but got [%d]", len(m.ExtraSigners), len(extraSigners)) - } - for i, signer := range extraSigners { - if !signer.Equal(m.ExtraSigners[i]) { - return errors.Errorf("expected extra signer [%s] but got [%s]", m.ExtraSigners[i], signer) - } - } - - // Check that the issuer identity matches, if present in the metadata. - if !m.Issuer.Equal(action.GetIssuer()) { - return errors.Errorf("expected issuer [%s] but got [%s]", m.Issuer, action.GetIssuer().Bytes()) - } - - return nil + return m.TransferMetadata.Match(action.TransferAction) } // IsOutputAbsent returns true if the j-th output's metadata is absent (e.g., filtered out).