diff --git a/consensus/tx.go b/consensus/tx.go index a4c8587..f12d4ff 100644 --- a/consensus/tx.go +++ b/consensus/tx.go @@ -7,7 +7,6 @@ package consensus import ( "fmt" - "unicode/utf8" coreerr "dappco.re/go/core/log" @@ -222,79 +221,10 @@ func checkAssetOperations(extra []byte, hf5Active bool) error { if err != nil { return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]: decode asset descriptor operation", i), ErrInvalidExtra) } - if err := validateAssetDescriptorOperation(op); err != nil { + if err := op.Validate(); err != nil { return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]", i), err) } } return nil } - -func validateAssetDescriptorOperation(op types.AssetDescriptorOperation) error { - switch op.Version { - case 0, 1: - default: - return coreerr.E("validateAssetDescriptorOperation", fmt.Sprintf("unsupported version %d", op.Version), ErrInvalidExtra) - } - - switch op.OperationType { - case types.AssetOpRegister: - if !op.AssetID.IsZero() { - return coreerr.E("validateAssetDescriptorOperation", "register operation must not carry asset id", ErrInvalidExtra) - } - if op.Descriptor == nil { - return coreerr.E("validateAssetDescriptorOperation", "register operation missing descriptor", ErrInvalidExtra) - } - if err := validateAssetDescriptorBase(*op.Descriptor); err != nil { - return err - } - if op.AmountToEmit != 0 || op.AmountToBurn != 0 { - return coreerr.E("validateAssetDescriptorOperation", "register operation must not include emission or burn amounts", ErrInvalidExtra) - } - case types.AssetOpEmit, types.AssetOpUpdate, types.AssetOpBurn, types.AssetOpPublicBurn: - if op.AssetID.IsZero() { - return coreerr.E("validateAssetDescriptorOperation", "operation must carry asset id", ErrInvalidExtra) - } - if op.OperationType == types.AssetOpUpdate && op.Descriptor == nil { - return coreerr.E("validateAssetDescriptorOperation", "update operation missing descriptor", ErrInvalidExtra) - } - if op.OperationType == types.AssetOpEmit && op.AmountToEmit == 0 { - return coreerr.E("validateAssetDescriptorOperation", "emit operation has zero amount", ErrInvalidExtra) - } - if (op.OperationType == types.AssetOpBurn || op.OperationType == types.AssetOpPublicBurn) && op.AmountToBurn == 0 { - return coreerr.E("validateAssetDescriptorOperation", "burn operation has zero amount", ErrInvalidExtra) - } - if op.OperationType == types.AssetOpUpdate && op.Descriptor != nil { - if err := validateAssetDescriptorBase(*op.Descriptor); err != nil { - return err - } - } - default: - return coreerr.E("validateAssetDescriptorOperation", fmt.Sprintf("unsupported operation type %d", op.OperationType), ErrInvalidExtra) - } - - return nil -} - -func validateAssetDescriptorBase(base types.AssetDescriptorBase) error { - tickerLen := utf8.RuneCountInString(base.Ticker) - fullNameLen := utf8.RuneCountInString(base.FullName) - - if base.TotalMaxSupply == 0 { - return coreerr.E("validateAssetDescriptorBase", "total max supply must be non-zero", ErrInvalidExtra) - } - if base.CurrentSupply > base.TotalMaxSupply { - return coreerr.E("validateAssetDescriptorBase", fmt.Sprintf("current supply %d exceeds max supply %d", base.CurrentSupply, base.TotalMaxSupply), ErrInvalidExtra) - } - if tickerLen == 0 || tickerLen > 6 { - return coreerr.E("validateAssetDescriptorBase", fmt.Sprintf("ticker length %d out of range", tickerLen), ErrInvalidExtra) - } - if fullNameLen == 0 || fullNameLen > 64 { - return coreerr.E("validateAssetDescriptorBase", fmt.Sprintf("full name length %d out of range", fullNameLen), ErrInvalidExtra) - } - if base.OwnerKey.IsZero() { - return coreerr.E("validateAssetDescriptorBase", "owner key must be non-zero", ErrInvalidExtra) - } - - return nil -} diff --git a/types/asset.go b/types/asset.go index dbaf6e2..f7427b8 100644 --- a/types/asset.go +++ b/types/asset.go @@ -5,6 +5,11 @@ package types +import ( + "fmt" + "unicode/utf8" +) + // AssetDescriptorOperationTag is the wire tag for asset_descriptor_operation // extra variants. const AssetDescriptorOperationTag uint8 = 40 @@ -31,6 +36,30 @@ type AssetDescriptorBase struct { Etc []byte } +// Validate checks that the base asset metadata is structurally valid. +func (base AssetDescriptorBase) Validate() error { + tickerLen := utf8.RuneCountInString(base.Ticker) + fullNameLen := utf8.RuneCountInString(base.FullName) + + if base.TotalMaxSupply == 0 { + return fmt.Errorf("types: total max supply must be non-zero") + } + if base.CurrentSupply > base.TotalMaxSupply { + return fmt.Errorf("types: current supply %d exceeds max supply %d", base.CurrentSupply, base.TotalMaxSupply) + } + if tickerLen == 0 || tickerLen > 6 { + return fmt.Errorf("types: ticker length %d out of range", tickerLen) + } + if fullNameLen == 0 || fullNameLen > 64 { + return fmt.Errorf("types: full name length %d out of range", fullNameLen) + } + if base.OwnerKey.IsZero() { + return fmt.Errorf("types: owner key must be non-zero") + } + + return nil +} + // AssetDescriptorOperation represents a deploy/emit/update/burn operation. // The wire format is parsed in wire/ as an opaque blob for round-tripping. type AssetDescriptorOperation struct { @@ -42,3 +71,65 @@ type AssetDescriptorOperation struct { AmountToBurn uint64 Etc []byte } + +// Validate checks that the operation is structurally valid for HF5 parsing. +func (op AssetDescriptorOperation) Validate() error { + switch op.Version { + case 0, 1: + default: + return fmt.Errorf("types: unsupported version %d", op.Version) + } + + switch op.OperationType { + case AssetOpRegister: + if !op.AssetID.IsZero() { + return fmt.Errorf("types: register operation must not carry asset id") + } + if op.Descriptor == nil { + return fmt.Errorf("types: register operation missing descriptor") + } + if err := op.Descriptor.Validate(); err != nil { + return err + } + if op.AmountToEmit != 0 || op.AmountToBurn != 0 { + return fmt.Errorf("types: register operation must not include emission or burn amounts") + } + case AssetOpEmit: + if op.AssetID.IsZero() { + return fmt.Errorf("types: emit operation must carry asset id") + } + if op.AmountToEmit == 0 { + return fmt.Errorf("types: emit operation has zero amount") + } + if op.Descriptor != nil { + return fmt.Errorf("types: emit operation must not carry descriptor") + } + case AssetOpUpdate: + if op.AssetID.IsZero() { + return fmt.Errorf("types: update operation must carry asset id") + } + if op.Descriptor == nil { + return fmt.Errorf("types: update operation missing descriptor") + } + if err := op.Descriptor.Validate(); err != nil { + return err + } + if op.AmountToEmit != 0 || op.AmountToBurn != 0 { + return fmt.Errorf("types: update operation must not include emission or burn amounts") + } + case AssetOpBurn, AssetOpPublicBurn: + if op.AssetID.IsZero() { + return fmt.Errorf("types: burn operation must carry asset id") + } + if op.AmountToBurn == 0 { + return fmt.Errorf("types: burn operation has zero amount") + } + if op.Descriptor != nil { + return fmt.Errorf("types: burn operation must not carry descriptor") + } + default: + return fmt.Errorf("types: unsupported operation type %d", op.OperationType) + } + + return nil +} diff --git a/types/asset_test.go b/types/asset_test.go index f538950..70fd3be 100644 --- a/types/asset_test.go +++ b/types/asset_test.go @@ -58,3 +58,83 @@ func TestAssetDescriptorTypes_Good(t *testing.T) { t.Fatalf("unexpected operation: %+v", op) } } + +func TestAssetDescriptorBaseValidate_Good(t *testing.T) { + base := AssetDescriptorBase{ + Ticker: "LTHN", + FullName: "Lethean", + TotalMaxSupply: 1000000, + CurrentSupply: 0, + DecimalPoint: 12, + MetaInfo: "{}", + OwnerKey: PublicKey{1}, + } + + if err := base.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestAssetDescriptorBaseValidate_Bad(t *testing.T) { + tests := []struct { + name string + base AssetDescriptorBase + }{ + {"zero_supply", AssetDescriptorBase{Ticker: "LTHN", FullName: "Lethean", OwnerKey: PublicKey{1}}}, + {"too_many_current", AssetDescriptorBase{Ticker: "LTHN", FullName: "Lethean", TotalMaxSupply: 10, CurrentSupply: 11, OwnerKey: PublicKey{1}}}, + {"empty_ticker", AssetDescriptorBase{FullName: "Lethean", TotalMaxSupply: 10, OwnerKey: PublicKey{1}}}, + {"empty_name", AssetDescriptorBase{Ticker: "LTHN", TotalMaxSupply: 10, OwnerKey: PublicKey{1}}}, + {"zero_owner", AssetDescriptorBase{Ticker: "LTHN", FullName: "Lethean", TotalMaxSupply: 10}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.base.Validate(); err == nil { + t.Fatal("Validate() error = nil, want error") + } + }) + } +} + +func TestAssetDescriptorOperationValidate_Good(t *testing.T) { + base := AssetDescriptorBase{ + Ticker: "LTHN", + FullName: "Lethean", + TotalMaxSupply: 1000000, + CurrentSupply: 0, + DecimalPoint: 12, + MetaInfo: "{}", + OwnerKey: PublicKey{1}, + } + + op := AssetDescriptorOperation{ + Version: 1, + OperationType: AssetOpRegister, + Descriptor: &base, + } + + if err := op.Validate(); err != nil { + t.Fatalf("Validate() error = %v", err) + } +} + +func TestAssetDescriptorOperationValidate_Bad(t *testing.T) { + tests := []struct { + name string + op AssetDescriptorOperation + }{ + {"unsupported_version", AssetDescriptorOperation{Version: 2, OperationType: AssetOpRegister}}, + {"register_missing_descriptor", AssetDescriptorOperation{Version: 1, OperationType: AssetOpRegister}}, + {"emit_zero_amount", AssetDescriptorOperation{Version: 1, OperationType: AssetOpEmit, AssetID: Hash{1}}}, + {"update_missing_descriptor", AssetDescriptorOperation{Version: 1, OperationType: AssetOpUpdate, AssetID: Hash{1}}}, + {"burn_zero_amount", AssetDescriptorOperation{Version: 1, OperationType: AssetOpBurn, AssetID: Hash{1}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.op.Validate(); err == nil { + t.Fatal("Validate() error = nil, want error") + } + }) + } +}