Skip to content

Commit ebbde41

Browse files
author
Nitesh
committed
feat: add comprehensive input validation for token operations (#1534)
- Add TokenValidator package with validation functions - Implement custom error types with error codes - Add metadata validation (key presence, 10KB size limit) - Add token type validation - Add index-specific error messages for zero values - Add owners/values length mismatch validation - Add unit tests for all validation scenarios Signed-off-by: Nitesh <nitesh@example.com>
1 parent c05df30 commit ebbde41

4 files changed

Lines changed: 534 additions & 4 deletions

File tree

token/request.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,11 @@ func (r *Request) Issue(ctx context.Context, wallet *IssuerWallet, receiver Iden
312312
return nil, errors.WithMessagef(err, "failed compiling options [%v]", opts)
313313
}
314314

315+
// Validate metadata size
316+
if err := validateMetadata(opt.Attributes); err != nil {
317+
return nil, errors.Wrap(err, "invalid metadata")
318+
}
319+
315320
// Compute Issue
316321
action, metaRaw, err := r.TokenService.tms.IssueService().Issue(
317322
ctx,
@@ -343,15 +348,40 @@ func (r *Request) Issue(ctx context.Context, wallet *IssuerWallet, receiver Iden
343348
// In other words, owners[0] will receives values[0], and so on.
344349
// Additional options can be passed to customize the action.
345350
func (r *Request) Transfer(ctx context.Context, wallet *OwnerWallet, typ token.Type, values []uint64, owners []Identity, opts ...TransferOption) (*TransferAction, error) {
346-
for _, v := range values {
351+
// Validate token type
352+
if typ == "" {
353+
return nil, errors.Errorf("type is empty")
354+
}
355+
356+
// Validate values
357+
for i, v := range values {
347358
if v == 0 {
348-
return nil, errors.Errorf("value is zero")
359+
return nil, errors.Errorf("value at index %d is zero", i)
360+
}
361+
}
362+
363+
// Validate owners match values length
364+
if len(owners) != len(values) {
365+
return nil, errors.Errorf("number of owners [%d] does not match number of values [%d]", len(owners), len(values))
366+
}
367+
368+
// Validate all owners are defined
369+
for i, owner := range owners {
370+
if owner.IsNone() {
371+
return nil, errors.Errorf("owner at index %d is not defined", i)
349372
}
350373
}
374+
351375
opt, err := CompileTransferOptions(opts...)
352376
if err != nil {
353377
return nil, errors.WithMessagef(err, "failed compiling options [%v]", opts)
354378
}
379+
380+
// Validate metadata size
381+
if err := validateMetadata(opt.Attributes); err != nil {
382+
return nil, errors.Wrap(err, "invalid metadata")
383+
}
384+
355385
tokenIDs, outputTokens, err := r.prepareTransfer(ctx, false, wallet, typ, values, owners, opt)
356386
if err != nil {
357387
return nil, errors.Wrap(err, "failed preparing transfer")
@@ -397,10 +427,26 @@ func (r *Request) Transfer(ctx context.Context, wallet *OwnerWallet, typ token.T
397427
// The action redeems tokens of the passed type for a total amount matching the passed value.
398428
// Additional options can be passed to customize the action.
399429
func (r *Request) Redeem(ctx context.Context, wallet *OwnerWallet, typ token.Type, value uint64, opts ...TransferOption) (*TransferAction, error) {
430+
// Validate token type
431+
if typ == "" {
432+
return nil, errors.Errorf("type is empty")
433+
}
434+
435+
// Validate value
436+
if value == 0 {
437+
return nil, errors.Errorf("value is zero")
438+
}
439+
400440
opt, err := CompileTransferOptions(opts...)
401441
if err != nil {
402442
return nil, errors.WithMessagef(err, "failed compiling options [%v]", opts)
403443
}
444+
445+
// Validate metadata size
446+
if err := validateMetadata(opt.Attributes); err != nil {
447+
return nil, errors.Wrap(err, "invalid metadata")
448+
}
449+
404450
tokenIDs, outputTokens, err := r.prepareTransfer(ctx, true, wallet, typ, []uint64{value}, []Identity{nil}, opt)
405451
if err != nil {
406452
return nil, errors.Wrap(err, "failed preparing transfer")
@@ -1558,3 +1604,26 @@ func (r *Request) cleanupInputIDs(ds []*token.ID) []*token.ID {
15581604

15591605
return newSlice
15601606
}
1607+
1608+
// validateMetadata validates metadata fields for size and key constraints
1609+
func validateMetadata(metadata map[interface{}]interface{}) error {
1610+
if metadata == nil {
1611+
return nil
1612+
}
1613+
1614+
for key, value := range metadata {
1615+
keyStr, isString := key.(string)
1616+
if key == nil || (isString && keyStr == "") {
1617+
return errors.Errorf("metadata key cannot be empty")
1618+
}
1619+
1620+
// Check size for byte slice values
1621+
if bytes, ok := value.([]byte); ok {
1622+
if len(bytes) > 10*1024 { // 10KB limit
1623+
return errors.Errorf("metadata value for key [%v] exceeds maximum size of 10KB", key)
1624+
}
1625+
}
1626+
}
1627+
1628+
return nil
1629+
}

token/request_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -590,15 +590,15 @@ func TestRequest_Transfer(t *testing.T) {
590590
wallet := &OwnerWallet{}
591591
_, err := req.Transfer(ctx, wallet, "USD", []uint64{0, 100}, []Identity{Identity("receiver1"), Identity("receiver2")})
592592
require.Error(t, err)
593-
assert.Contains(t, err.Error(), "value is zero")
593+
assert.Contains(t, err.Error(), "value at index 0 is zero")
594594
})
595595

596596
t.Run("multiple zero values", func(t *testing.T) {
597597
req := NewRequest(nil, "test-anchor")
598598
wallet := &OwnerWallet{}
599599
_, err := req.Transfer(ctx, wallet, "USD", []uint64{100, 0}, []Identity{Identity("receiver1"), Identity("receiver2")})
600600
require.Error(t, err)
601-
assert.Contains(t, err.Error(), "value is zero")
601+
assert.Contains(t, err.Error(), "value at index 1 is zero")
602602
})
603603
}
604604

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*
2+
Copyright IBM Corp. All Rights Reserved.
3+
4+
SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
package validation
8+
9+
import (
10+
"github.com/hyperledger-labs/fabric-smart-client/pkg/utils/errors"
11+
)
12+
13+
// Error codes for validation failures
14+
const (
15+
// Amount validation errors
16+
ErrCodeInvalidAmount = "invalid-amount"
17+
ErrCodeNegativeAmount = "negative-amount"
18+
ErrCodeZeroAmount = "zero-amount"
19+
ErrCodeAmountOverflow = "amount-overflow"
20+
ErrCodeAmountExceedsMax = "amount-exceeds-max"
21+
22+
// Address validation errors
23+
ErrCodeInvalidAddress = "invalid-address"
24+
ErrCodeEmptyAddress = "empty-address"
25+
ErrCodeMalformedAddress = "malformed-address"
26+
27+
// Metadata validation errors
28+
ErrCodeInvalidMetadata = "invalid-metadata"
29+
ErrCodeMetadataTooLarge = "metadata-too-large"
30+
ErrCodeMetadataTypeMismatch = "metadata-type-mismatch"
31+
32+
// Token type validation errors
33+
ErrCodeInvalidTokenType = "invalid-token-type"
34+
ErrCodeEmptyTokenType = "empty-token-type"
35+
ErrCodeUnknownTokenType = "unknown-token-type"
36+
37+
// General validation errors
38+
ErrCodeInvalidInput = "invalid-input"
39+
ErrCodeEmptyWallet = "empty-wallet"
40+
)
41+
42+
// MaxMetadataSize is the maximum size of metadata in bytes
43+
const MaxMetadataSize = 1024 * 10 // 10KB
44+
45+
// MaxAddressLength is the maximum length of an address
46+
const MaxAddressLength = 256
47+
48+
// InvalidAmountError indicates a token amount validation failure
49+
type InvalidAmountError struct {
50+
Code string
51+
Message string
52+
Value interface{}
53+
}
54+
55+
func (e *InvalidAmountError) Error() string {
56+
return errors.Errorf("%s: %s", e.Code, e.Message).Error()
57+
}
58+
59+
// InvalidAddressError indicates an address validation failure
60+
type InvalidAddressError struct {
61+
Code string
62+
Message string
63+
Address interface{}
64+
}
65+
66+
func (e *InvalidAddressError) Error() string {
67+
return errors.Errorf("%s: %s", e.Code, e.Message).Error()
68+
}
69+
70+
// InvalidMetadataError indicates a metadata validation failure
71+
type InvalidMetadataError struct {
72+
Code string
73+
Message string
74+
Key string
75+
}
76+
77+
func (e *InvalidMetadataError) Error() string {
78+
return errors.Errorf("%s: %s", e.Code, e.Message).Error()
79+
}
80+
81+
// InvalidTokenTypeError indicates a token type validation failure
82+
type InvalidTokenTypeError struct {
83+
Code string
84+
Message string
85+
Type string
86+
}
87+
88+
func (e *InvalidTokenTypeError) Error() string {
89+
return errors.Errorf("%s: %s", e.Code, e.Message).Error()
90+
}
91+
92+
// ValidationError is a generic validation error with a code
93+
type ValidationError struct {
94+
Code string
95+
Message string
96+
}
97+
98+
func (e *ValidationError) Error() string {
99+
return errors.Errorf("%s: %s", e.Code, e.Message).Error()
100+
}
101+
102+
// NewInvalidAmountError creates a new InvalidAmountError
103+
func NewInvalidAmountError(code, message string, value interface{}) *InvalidAmountError {
104+
return &InvalidAmountError{Code: code, Message: message, Value: value}
105+
}
106+
107+
// NewInvalidAddressError creates a new InvalidAddressError
108+
func NewInvalidAddressError(code, message string, address interface{}) *InvalidAddressError {
109+
return &InvalidAddressError{Code: code, Message: message, Address: address}
110+
}
111+
112+
// NewInvalidMetadataError creates a new InvalidMetadataError
113+
func NewInvalidMetadataError(code, message, key string) *InvalidMetadataError {
114+
return &InvalidMetadataError{Code: code, Message: message, Key: key}
115+
}
116+
117+
// NewInvalidTokenTypeError creates a new InvalidTokenTypeError
118+
func NewInvalidTokenTypeError(code, message, tokenType string) *InvalidTokenTypeError {
119+
return &InvalidTokenTypeError{Code: code, Message: message, Type: tokenType}
120+
}
121+
122+
// NewValidationError creates a new ValidationError
123+
func NewValidationError(code, message string) *ValidationError {
124+
return &ValidationError{Code: code, Message: message}
125+
}
126+
127+
// ValidateAmount validates a token amount value
128+
func ValidateAmount(value uint64, maxValue uint64) error {
129+
if value == 0 {
130+
return NewInvalidAmountError(ErrCodeZeroAmount, "token amount must be greater than zero", value)
131+
}
132+
133+
if maxValue > 0 && value > maxValue {
134+
return NewInvalidAmountError(ErrCodeAmountExceedsMax, "token amount exceeds maximum allowed value", value)
135+
}
136+
137+
return nil
138+
}
139+
140+
// ValidateAddress validates a recipient address
141+
func ValidateAddress(address []byte) error {
142+
if len(address) == 0 {
143+
return NewInvalidAddressError(ErrCodeEmptyAddress, "address cannot be empty", nil)
144+
}
145+
146+
if len(address) > MaxAddressLength {
147+
return NewInvalidAddressError(ErrCodeMalformedAddress, "address exceeds maximum length", len(address))
148+
}
149+
150+
return nil
151+
}
152+
153+
// ValidateTokenType validates a token type
154+
func ValidateTokenType(tokenType string) error {
155+
if tokenType == "" {
156+
return NewInvalidTokenTypeError(ErrCodeEmptyTokenType, "token type cannot be empty", tokenType)
157+
}
158+
159+
return nil
160+
}
161+
162+
// ValidateMetadata validates metadata fields
163+
func ValidateMetadata(metadata map[string]interface{}) error {
164+
if metadata == nil {
165+
return nil
166+
}
167+
168+
for key, value := range metadata {
169+
if len(key) == 0 {
170+
return NewInvalidMetadataError(ErrCodeInvalidMetadata, "metadata key cannot be empty", key)
171+
}
172+
173+
// Check size for byte slice values
174+
if bytes, ok := value.([]byte); ok {
175+
if len(bytes) > MaxMetadataSize {
176+
return NewInvalidMetadataError(ErrCodeMetadataTooLarge, "metadata value exceeds maximum size", key)
177+
}
178+
}
179+
}
180+
181+
return nil
182+
}
183+
184+
// ValidateTransferValues validates transfer values and owners
185+
func ValidateTransferValues(values []uint64, owners [][]byte, maxValue uint64) error {
186+
if len(values) == 0 {
187+
return NewValidationError(ErrCodeInvalidInput, "values cannot be empty")
188+
}
189+
190+
if len(owners) == 0 {
191+
return NewValidationError(ErrCodeInvalidInput, "owners cannot be empty")
192+
}
193+
194+
if len(values) != len(owners) {
195+
return NewValidationError(ErrCodeInvalidInput, "values and owners must have the same length")
196+
}
197+
198+
for i, v := range values {
199+
if err := ValidateAmount(v, maxValue); err != nil {
200+
return errors.Wrapf(err, "value at index %d", i)
201+
}
202+
}
203+
204+
for i, o := range owners {
205+
if err := ValidateAddress(o); err != nil {
206+
return errors.Wrapf(err, "owner at index %d", i)
207+
}
208+
}
209+
210+
return nil
211+
}
212+
213+
// ValidateRedeemValue validates a redeem value
214+
func ValidateRedeemValue(value uint64, maxValue uint64) error {
215+
return ValidateAmount(value, maxValue)
216+
}

0 commit comments

Comments
 (0)