refactor(ax): centralise asset validation
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
8802b94ee5
commit
95edac1d15
3 changed files with 172 additions and 71 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue