Skip to content

Commit 5f9bcf2

Browse files
authored
add custom application metadata to public data on ledger (#1050)
Signed-off-by: Arne Rutjes <arne123@gmail.com> Signed-off-by: Angelo De Caro <adc@zurich.ibm.com>
1 parent 60a254d commit 5f9bcf2

File tree

11 files changed

+307
-11
lines changed

11 files changed

+307
-11
lines changed

integration/token/fungible/support.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -621,11 +621,14 @@ func TransferCashMultiActions(network *integration.Infrastructure, sender *token
621621
RecipientEID: receivers[0].Id(),
622622
TokenIDs: []*token.ID{tokenID},
623623
}
624+
625+
uniqueKey := fmt.Sprintf("%d", time.Now().UnixNano())
624626
for i := 1; i < len(amounts); i++ {
625627
transfer.TransferAction = append(transfer.TransferAction, views.TransferAction{
626-
Amount: amounts[i],
627-
Recipient: network.Identity(receivers[i].Id()),
628-
RecipientEID: receivers[i].Id(),
628+
Amount: amounts[i],
629+
Recipient: network.Identity(receivers[i].Id()),
630+
RecipientEID: receivers[i].Id(),
631+
PublicMetadata: map[string][]byte{fmt.Sprintf("%s_%d", uniqueKey, i): fmt.Appendf(nil, "val_%d", i)},
629632
})
630633
}
631634

@@ -650,6 +653,17 @@ func TransferCashMultiActions(network *integration.Infrastructure, sender *token
650653
gomega.Expect(sigma).ToNot(gomega.BeNil(), "endorsement ack sigma is nil for identity %s", identity)
651654
}
652655
gomega.Expect(len(txInfo.EndorsementAcks)).To(gomega.BeEquivalentTo(len(signers)))
656+
params := views.ListAcceptedTransactions{SenderWallet: wallet, IDs: []string{txID}}
657+
658+
txsBoxed, err := network.Client(receivers[0].Id()).CallView("acceptedTransactionHistory", common.JSONMarshall(&params))
659+
gomega.Expect(err).NotTo(gomega.HaveOccurred())
660+
var txs []*ttxdb.TransactionRecord
661+
common.JSONUnmarshal(txsBoxed.([]byte), &txs)
662+
663+
gomega.Expect(len(txs)).To(gomega.BeEquivalentTo(1), "1 tx")
664+
gomega.Expect(len(txs[0].PublicMetadata)).To(gomega.BeEquivalentTo(1), "1 public metadata")
665+
gomega.Expect(txs[0].PublicMetadata[token2.PublicMetadataPrefix+uniqueKey+"_1"]).To(gomega.BeEquivalentTo([]byte("val_1")), fmt.Sprintf("public metadata = %v", txs[0].PublicMetadata))
666+
653667
return txID
654668
}
655669

integration/token/fungible/views/history.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ type ListAcceptedTransactions struct {
103103
ActionTypes []ttxdb.ActionType
104104
Statuses []ttxdb.TxStatus
105105
TMSID *token.TMSID
106+
IDs []string
106107
}
107108

108109
type ListAcceptedTransactionsView struct {
@@ -119,6 +120,7 @@ func (p *ListAcceptedTransactionsView) Call(context view.Context) (interface{},
119120
To: p.To,
120121
ActionTypes: p.ActionTypes,
121122
Statuses: p.Statuses,
123+
IDs: p.IDs,
122124
}, pagination.None())
123125
if err != nil {
124126
return nil, errors.Wrapf(err, "failed querying transactions")

integration/token/fungible/views/transfer.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ type TransferAction struct {
2929
Recipient view.Identity
3030
// RecipientEID is the expected enrolment id of the recipient
3131
RecipientEID string
32+
// PublicMetadata is stored with the transaction
33+
PublicMetadata map[string][]byte
3234
}
3335

3436
// Transfer contains the input information for a transfer
@@ -66,8 +68,8 @@ type Transfer struct {
6668
TMSID *token2.TMSID
6769
// NotAnonymous true if the transaction must be anonymous, false otherwise
6870
NotAnonymous bool
69-
// Metadata contains application metadata to append to the transaction
70-
Metadata map[string][]byte
71+
// ApplicationMetadata contains application metadata to append to the transaction
72+
ApplicationMetadata map[string][]byte
7173
}
7274

7375
type TransferView struct {
@@ -120,7 +122,7 @@ func (t *TransferView) Call(context view.Context) (txID interface{}, err error)
120122
assert.NoError(err, "failed creating transaction")
121123

122124
// append metadata, if any
123-
for k, v := range t.Metadata {
125+
for k, v := range t.ApplicationMetadata {
124126
tx.SetApplicationMetadata(k, v)
125127
}
126128

@@ -151,12 +153,17 @@ func (t *TransferView) Call(context view.Context) (txID interface{}, err error)
151153
// add additional transfers
152154
logger.DebugfContext(context.Context(), "Append additional actions")
153155
for i, action := range t.TransferAction {
156+
opts := []token2.TransferOption{token2.WithTokenIDs(t.TokenIDs...)}
157+
for k, v := range action.PublicMetadata {
158+
opts = append(opts, token2.WithPublicTransferMetadata(k, v))
159+
}
160+
154161
err = tx.Transfer(
155162
senderWallet,
156163
t.Type,
157164
[]uint64{action.Amount},
158165
[]view.Identity{additionalRecipients[i]},
159-
token2.WithTokenIDs(t.TokenIDs...),
166+
opts...,
160167
)
161168
assert.NoError(err, "failed adding transfer action [%d:%s]", action.Amount, action.Recipient)
162169
}
@@ -581,7 +588,7 @@ func (t *MaliciousTransferView) Call(context view.Context) (txID interface{}, er
581588
assert.NoError(err, "failed creating transaction")
582589

583590
// append metadata, if any
584-
for k, v := range t.Metadata {
591+
for k, v := range t.ApplicationMetadata {
585592
tx.SetApplicationMetadata(k, v)
586593
}
587594

token/core/common/meta/metadata.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const (
1515
TransferMetadataPrefix = "TransferMetadataPrefix"
1616
// IssueMetadataPrefix is the prefix for the metadata of an issue action
1717
IssueMetadataPrefix = "IssueMetadataPrefix"
18+
// PublicMetadataPrefix is the prefix for the metadata that will be published on the ledger without further validation
19+
PublicMetadataPrefix = "pub."
1820
)
1921

2022
// TransferActionMetadata extracts the transfer metadata from the passed attributes and
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"strings"
11+
12+
"github.com/hyperledger-labs/fabric-token-sdk/token/core/common/meta"
13+
"github.com/hyperledger-labs/fabric-token-sdk/token/driver"
14+
)
15+
16+
// IssueApplicationDataValidate accepts any metadata in the "pub" namespace.
17+
// This gives the user of the Token SDK the option to attach public data to the token transaction.
18+
func IssueApplicationDataValidate[P driver.PublicParameters, T any, TA driver.TransferAction, IA driver.IssueAction, DS driver.Deserializer](ctx *Context[P, T, TA, IA, DS]) error {
19+
for key := range ctx.IssueAction.GetMetadata() {
20+
if strings.HasPrefix(key, meta.PublicMetadataPrefix) {
21+
ctx.CountMetadataKey(key)
22+
}
23+
}
24+
return nil
25+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"testing"
11+
12+
"github.com/hyperledger-labs/fabric-token-sdk/token/core/common/meta"
13+
"github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestIssueApplicationDataValidate(t *testing.T) {
18+
tests := []struct {
19+
name string
20+
err bool
21+
errMsg string
22+
context func() (*TestContext, TestCheck)
23+
}{
24+
{
25+
name: "no metadata",
26+
err: false,
27+
context: func() (*TestContext, TestCheck) {
28+
pp := &mock.PublicParameters{}
29+
pp.AuditorsReturns(nil)
30+
action := &mock.IssueAction{}
31+
action.GetMetadataReturns(nil)
32+
ctx := &TestContext{
33+
PP: pp,
34+
IssueAction: action,
35+
}
36+
return ctx, func() bool {
37+
return len(ctx.MetadataCounter) == 0
38+
}
39+
},
40+
},
41+
{
42+
name: "one metadata without the selected prefix",
43+
err: false,
44+
context: func() (*TestContext, TestCheck) {
45+
pp := &mock.PublicParameters{}
46+
pp.AuditorsReturns(nil)
47+
action := &mock.IssueAction{}
48+
action.GetMetadataReturns(map[string][]byte{
49+
"key1": []byte("value1"),
50+
})
51+
ctx := &TestContext{
52+
PP: pp,
53+
IssueAction: action,
54+
}
55+
return ctx, func() bool {
56+
return len(ctx.MetadataCounter) == 0
57+
}
58+
},
59+
},
60+
{
61+
name: "one metadata with the selected prefix",
62+
err: false,
63+
context: func() (*TestContext, TestCheck) {
64+
pp := &mock.PublicParameters{}
65+
pp.AuditorsReturns(nil)
66+
action := &mock.IssueAction{}
67+
k := meta.PublicMetadataPrefix + "key1"
68+
action.GetMetadataReturns(map[string][]byte{
69+
k: []byte("value1"),
70+
"another key": []byte("value2"),
71+
})
72+
ctx := &TestContext{
73+
PP: pp,
74+
IssueAction: action,
75+
MetadataCounter: map[MetadataCounterID]int{},
76+
}
77+
return ctx, func() bool {
78+
return len(ctx.MetadataCounter) == 1 && ctx.MetadataCounter[k] == 1
79+
}
80+
},
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
ctx, check := tt.context()
87+
err := IssueApplicationDataValidate(ctx)
88+
if tt.err {
89+
assert.Error(t, err)
90+
assert.EqualError(t, err, tt.errMsg)
91+
} else {
92+
assert.NoError(t, err)
93+
}
94+
if check != nil {
95+
assert.True(t, check())
96+
}
97+
})
98+
}
99+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"strings"
11+
12+
"github.com/hyperledger-labs/fabric-token-sdk/token/core/common/meta"
13+
"github.com/hyperledger-labs/fabric-token-sdk/token/driver"
14+
)
15+
16+
// TransferApplicationDataValidate accepts any metadata in the "pub" namespace.
17+
// This gives the user of the Token SDK the option to attach public data to the token transaction.
18+
func TransferApplicationDataValidate[P driver.PublicParameters, T any, TA driver.TransferAction, IA driver.IssueAction, DS driver.Deserializer](ctx *Context[P, T, TA, IA, DS]) error {
19+
for key := range ctx.TransferAction.GetMetadata() {
20+
if strings.HasPrefix(key, meta.PublicMetadataPrefix) {
21+
ctx.CountMetadataKey(key)
22+
}
23+
}
24+
return nil
25+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package common
8+
9+
import (
10+
"testing"
11+
12+
"github.com/hyperledger-labs/fabric-token-sdk/token/core/common/meta"
13+
"github.com/hyperledger-labs/fabric-token-sdk/token/driver/mock"
14+
"github.com/stretchr/testify/assert"
15+
)
16+
17+
func TestTransferApplicationDataValidate(t *testing.T) {
18+
tests := []struct {
19+
name string
20+
err bool
21+
errMsg string
22+
context func() (*TestContext, TestCheck)
23+
}{
24+
{
25+
name: "no metadata",
26+
err: false,
27+
context: func() (*TestContext, TestCheck) {
28+
pp := &mock.PublicParameters{}
29+
pp.AuditorsReturns(nil)
30+
action := &mock.TransferAction{}
31+
action.GetMetadataReturns(nil)
32+
ctx := &TestContext{
33+
PP: pp,
34+
TransferAction: action,
35+
}
36+
return ctx, func() bool {
37+
return len(ctx.MetadataCounter) == 0
38+
}
39+
},
40+
},
41+
{
42+
name: "one metadata without the selected prefix",
43+
err: false,
44+
context: func() (*TestContext, TestCheck) {
45+
pp := &mock.PublicParameters{}
46+
pp.AuditorsReturns(nil)
47+
action := &mock.TransferAction{}
48+
action.GetMetadataReturns(map[string][]byte{
49+
"key1": []byte("value1"),
50+
})
51+
ctx := &TestContext{
52+
PP: pp,
53+
TransferAction: action,
54+
}
55+
return ctx, func() bool {
56+
return len(ctx.MetadataCounter) == 0
57+
}
58+
},
59+
},
60+
{
61+
name: "one metadata with the selected prefix",
62+
err: false,
63+
context: func() (*TestContext, TestCheck) {
64+
pp := &mock.PublicParameters{}
65+
pp.AuditorsReturns(nil)
66+
action := &mock.TransferAction{}
67+
k := meta.PublicMetadataPrefix + "key1"
68+
action.GetMetadataReturns(map[string][]byte{
69+
k: []byte("value1"),
70+
"another key": []byte("value2"),
71+
})
72+
ctx := &TestContext{
73+
PP: pp,
74+
TransferAction: action,
75+
MetadataCounter: map[MetadataCounterID]int{},
76+
}
77+
return ctx, func() bool {
78+
return len(ctx.MetadataCounter) == 1 && ctx.MetadataCounter[k] == 1
79+
}
80+
},
81+
},
82+
}
83+
84+
for _, tt := range tests {
85+
t.Run(tt.name, func(t *testing.T) {
86+
ctx, check := tt.context()
87+
err := TransferApplicationDataValidate(ctx)
88+
if tt.err {
89+
assert.Error(t, err)
90+
assert.EqualError(t, err, tt.errMsg)
91+
} else {
92+
assert.NoError(t, err)
93+
}
94+
if check != nil {
95+
assert.True(t, check())
96+
}
97+
})
98+
}
99+
}

token/core/fabtoken/v1/validator/validator.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,13 @@ func NewValidator(logger logging.Logger, pp *setup.PublicParams, deserializer dr
5454
TransferSignatureValidate,
5555
TransferBalanceValidate,
5656
TransferHTLCValidate,
57+
common.TransferApplicationDataValidate[*setup.PublicParams, *actions.Output, *actions.TransferAction, *actions.IssueAction, driver.Deserializer],
5758
}
5859
transferValidators = append(transferValidators, extraValidators...)
5960

6061
issueValidators := []ValidateIssueFunc{
6162
IssueValidate,
63+
common.IssueApplicationDataValidate[*setup.PublicParams, *actions.Output, *actions.TransferAction, *actions.IssueAction, driver.Deserializer],
6264
}
6365

6466
auditingValidators := []ValidateAuditingFunc{

0 commit comments

Comments
 (0)