feat(consensus): validate HF5 asset operations
Co-Authored-By: Charon <charon@lethean.io>
This commit is contained in:
parent
c1b68523c6
commit
050d530b29
6 changed files with 346 additions and 9 deletions
107
consensus/tx.go
107
consensus/tx.go
|
|
@ -7,17 +7,20 @@ package consensus
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
)
|
||||
|
||||
// ValidateTransaction performs semantic validation on a regular (non-coinbase)
|
||||
// transaction. Checks are ordered to match the C++ validate_tx_semantic().
|
||||
func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error {
|
||||
hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
|
||||
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
|
||||
|
||||
// 0. Transaction version.
|
||||
if err := checkTxVersion(tx, forks, height); err != nil {
|
||||
|
|
@ -49,6 +52,11 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
|
|||
return err
|
||||
}
|
||||
|
||||
// 4a. HF5 asset operation validation inside extra.
|
||||
if err := checkAssetOperations(tx.Extra, hf5Active); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Money overflow.
|
||||
if _, err := sumInputs(tx); err != nil {
|
||||
return err
|
||||
|
|
@ -191,3 +199,102 @@ func checkKeyImages(tx *types.Transaction) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAssetOperations(extra []byte, hf5Active bool) error {
|
||||
if len(extra) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
elements, err := wire.DecodeVariantVector(extra)
|
||||
if err != nil {
|
||||
return coreerr.E("checkAssetOperations", "parse extra", ErrInvalidExtra)
|
||||
}
|
||||
|
||||
for i, elem := range elements {
|
||||
if elem.Tag != types.AssetDescriptorOperationTag {
|
||||
continue
|
||||
}
|
||||
if !hf5Active {
|
||||
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]: asset descriptor operation pre-HF5", i), ErrInvalidExtra)
|
||||
}
|
||||
|
||||
op, err := wire.DecodeAssetDescriptorOperation(elem.Data)
|
||||
if err != nil {
|
||||
return coreerr.E("checkAssetOperations", fmt.Sprintf("extra[%d]: decode asset descriptor operation", i), ErrInvalidExtra)
|
||||
}
|
||||
if err := validateAssetDescriptorOperation(op); 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@
|
|||
package consensus
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"dappco.re/go/core/blockchain/config"
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
"dappco.re/go/core/blockchain/wire"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -272,6 +274,101 @@ func TestCheckOutputs_UnsupportedTarget_Bad(t *testing.T) {
|
|||
assert.ErrorIs(t, err, ErrInvalidOutput)
|
||||
}
|
||||
|
||||
func assetDescriptorExtraBlob(ticker string, ownerZero bool) []byte {
|
||||
var buf bytes.Buffer
|
||||
enc := wire.NewEncoder(&buf)
|
||||
|
||||
enc.WriteVarint(1)
|
||||
enc.WriteUint8(types.AssetDescriptorOperationTag)
|
||||
|
||||
assetOp := bytes.Buffer{}
|
||||
opEnc := wire.NewEncoder(&assetOp)
|
||||
opEnc.WriteUint8(1) // version
|
||||
opEnc.WriteUint8(types.AssetOpRegister)
|
||||
opEnc.WriteUint8(0) // no asset id
|
||||
opEnc.WriteUint8(1) // descriptor present
|
||||
opEnc.WriteVarint(uint64(len(ticker)))
|
||||
opEnc.WriteBytes([]byte(ticker))
|
||||
opEnc.WriteVarint(7)
|
||||
opEnc.WriteBytes([]byte("Lethean"))
|
||||
opEnc.WriteUint64LE(1000000)
|
||||
opEnc.WriteUint64LE(0)
|
||||
opEnc.WriteUint8(12)
|
||||
opEnc.WriteVarint(0)
|
||||
if ownerZero {
|
||||
opEnc.WriteBytes(make([]byte, 32))
|
||||
} else {
|
||||
opEnc.WriteBytes(bytes.Repeat([]byte{0xAA}, 32))
|
||||
}
|
||||
opEnc.WriteVarint(0)
|
||||
opEnc.WriteUint64LE(0)
|
||||
opEnc.WriteUint64LE(0)
|
||||
opEnc.WriteVarint(0)
|
||||
|
||||
enc.WriteBytes(assetOp.Bytes())
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func TestValidateTransaction_AssetDescriptorOperation_Good(t *testing.T) {
|
||||
tx := &types.Transaction{
|
||||
Version: types.VersionPostHF5,
|
||||
Vin: []types.TxInput{
|
||||
types.TxInputZC{
|
||||
KeyImage: types.KeyImage{1},
|
||||
},
|
||||
},
|
||||
Vout: []types.TxOutput{
|
||||
types.TxOutputBare{
|
||||
Amount: 90,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{1}},
|
||||
},
|
||||
types.TxOutputBare{
|
||||
Amount: 1,
|
||||
Target: types.TxOutToKey{Key: types.PublicKey{2}},
|
||||
},
|
||||
},
|
||||
Extra: assetDescriptorExtraBlob("LTHN", false),
|
||||
}
|
||||
blob := make([]byte, 100)
|
||||
err := ValidateTransaction(tx, blob, config.TestnetForks, 250)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateTransaction_AssetDescriptorOperationPreHF5_Bad(t *testing.T) {
|
||||
tx := &types.Transaction{
|
||||
Version: types.VersionPreHF4,
|
||||
Vin: []types.TxInput{
|
||||
types.TxInputToKey{Amount: 100, KeyImage: types.KeyImage{1}},
|
||||
},
|
||||
Vout: []types.TxOutput{
|
||||
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
|
||||
},
|
||||
Extra: assetDescriptorExtraBlob("LTHN", false),
|
||||
}
|
||||
blob := make([]byte, 100)
|
||||
err := ValidateTransaction(tx, blob, config.MainnetForks, 5000)
|
||||
assert.ErrorIs(t, err, ErrInvalidExtra)
|
||||
}
|
||||
|
||||
func TestValidateTransaction_AssetDescriptorOperationInvalid_Bad(t *testing.T) {
|
||||
tx := &types.Transaction{
|
||||
Version: types.VersionPostHF5,
|
||||
Vin: []types.TxInput{
|
||||
types.TxInputZC{
|
||||
KeyImage: types.KeyImage{1},
|
||||
},
|
||||
},
|
||||
Vout: []types.TxOutput{
|
||||
types.TxOutputBare{Amount: 90, Target: types.TxOutToKey{Key: types.PublicKey{1}}},
|
||||
types.TxOutputBare{Amount: 1, Target: types.TxOutToKey{Key: types.PublicKey{2}}},
|
||||
},
|
||||
Extra: assetDescriptorExtraBlob("TOO-LONG", true),
|
||||
}
|
||||
blob := make([]byte, 100)
|
||||
err := ValidateTransaction(tx, blob, config.TestnetForks, 250)
|
||||
assert.ErrorIs(t, err, ErrInvalidExtra)
|
||||
}
|
||||
|
||||
// --- Key image tests for HTLC (Task 8) ---
|
||||
|
||||
func TestCheckKeyImages_HTLCDuplicate_Bad(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
package types
|
||||
|
||||
// AssetDescriptorOperationTag is the wire tag for asset_descriptor_operation
|
||||
// extra variants.
|
||||
const AssetDescriptorOperationTag uint8 = 40
|
||||
|
||||
// Asset operation types used by the HF5 asset_descriptor_operation variant.
|
||||
const (
|
||||
AssetOpRegister uint8 = 0 // deploy new asset
|
||||
|
|
|
|||
|
|
@ -540,10 +540,10 @@ const (
|
|||
tagZarcanumSig = 45 // zarcanum_sig — complex
|
||||
|
||||
// Asset operation tags (HF5 confidential assets).
|
||||
tagAssetDescriptorOperation = 40 // asset_descriptor_operation
|
||||
tagAssetOperationProof = 49 // asset_operation_proof
|
||||
tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof
|
||||
tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth
|
||||
tagAssetDescriptorOperation = types.AssetDescriptorOperationTag // asset_descriptor_operation
|
||||
tagAssetOperationProof = 49 // asset_operation_proof
|
||||
tagAssetOperationOwnershipProof = 50 // asset_operation_ownership_proof
|
||||
tagAssetOperationOwnershipProofETH = 51 // asset_operation_ownership_proof_eth
|
||||
|
||||
// Proof variant tags (proof_v).
|
||||
tagZCAssetSurjectionProof = 46 // vector<BGE_proof_s>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,23 @@ func TestReadAssetDescriptorOperation_Good(t *testing.T) {
|
|||
if !bytes.Equal(got, blob) {
|
||||
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(blob))
|
||||
}
|
||||
|
||||
op, err := DecodeAssetDescriptorOperation(blob)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeAssetDescriptorOperation failed: %v", err)
|
||||
}
|
||||
if op.Version != 1 || op.OperationType != 0 {
|
||||
t.Fatalf("unexpected operation header: %+v", op)
|
||||
}
|
||||
if op.Descriptor == nil {
|
||||
t.Fatal("expected descriptor to be present")
|
||||
}
|
||||
if op.Descriptor.Ticker != "LTHN" || op.Descriptor.FullName != "Lethean" {
|
||||
t.Fatalf("unexpected descriptor contents: %+v", op.Descriptor)
|
||||
}
|
||||
if op.Descriptor.TotalMaxSupply != 1000000 || op.Descriptor.DecimalPoint != 12 {
|
||||
t.Fatalf("unexpected descriptor values: %+v", op.Descriptor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadAssetDescriptorOperation_Bad(t *testing.T) {
|
||||
|
|
@ -136,6 +153,17 @@ func TestVariantVectorWithTag40_Good(t *testing.T) {
|
|||
if !bytes.Equal(got, raw) {
|
||||
t.Fatalf("round-trip mismatch: got %d bytes, want %d bytes", len(got), len(raw))
|
||||
}
|
||||
|
||||
elements, err := DecodeVariantVector(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("DecodeVariantVector failed: %v", err)
|
||||
}
|
||||
if len(elements) != 1 || elements[0].Tag != tagAssetDescriptorOperation {
|
||||
t.Fatalf("unexpected elements: %+v", elements)
|
||||
}
|
||||
if !bytes.Equal(elements[0].Data, innerBlob) {
|
||||
t.Fatalf("unexpected element payload length: got %d, want %d", len(elements[0].Data), len(innerBlob))
|
||||
}
|
||||
}
|
||||
|
||||
func buildAssetOperationProofBlob() []byte {
|
||||
|
|
@ -273,9 +301,9 @@ func TestV3TransactionRoundTrip_Good(t *testing.T) {
|
|||
// version = 3
|
||||
enc.WriteVarint(3)
|
||||
// vin: 1 coinbase input
|
||||
enc.WriteVarint(1) // input count
|
||||
enc.WriteVarint(1) // input count
|
||||
enc.WriteVariantTag(0) // txin_gen tag
|
||||
enc.WriteVarint(201) // height
|
||||
enc.WriteVarint(201) // height
|
||||
|
||||
// extra: variant vector with 2 elements (public_key + zarcanum_tx_data_v1)
|
||||
enc.WriteVarint(2)
|
||||
|
|
@ -289,13 +317,13 @@ func TestV3TransactionRoundTrip_Good(t *testing.T) {
|
|||
// vout: 2 Zarcanum outputs
|
||||
enc.WriteVarint(2)
|
||||
for range 2 {
|
||||
enc.WriteVariantTag(38) // OutputTypeZarcanum
|
||||
enc.WriteVariantTag(38) // OutputTypeZarcanum
|
||||
enc.WriteBytes(make([]byte, 32)) // stealth_address
|
||||
enc.WriteBytes(make([]byte, 32)) // concealing_point
|
||||
enc.WriteBytes(make([]byte, 32)) // amount_commitment
|
||||
enc.WriteBytes(make([]byte, 32)) // blinded_asset_id
|
||||
enc.WriteUint64LE(0) // encrypted_amount
|
||||
enc.WriteUint8(0) // mix_attr
|
||||
enc.WriteUint64LE(0) // encrypted_amount
|
||||
enc.WriteUint8(0) // mix_attr
|
||||
}
|
||||
|
||||
// hardfork_id = 5
|
||||
|
|
|
|||
101
wire/variant.go
Normal file
101
wire/variant.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// Copyright (c) 2017-2026 Lethean (https://lt.hn)
|
||||
//
|
||||
// Licensed under the European Union Public Licence (EUPL) version 1.2.
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
package wire
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
coreerr "dappco.re/go/core/log"
|
||||
|
||||
"dappco.re/go/core/blockchain/types"
|
||||
)
|
||||
|
||||
// VariantElement is one tagged element from a raw variant vector.
|
||||
// Data contains the raw wire bytes for the element payload, without the tag.
|
||||
type VariantElement struct {
|
||||
Tag uint8
|
||||
Data []byte
|
||||
}
|
||||
|
||||
// DecodeVariantVector decodes a raw variant vector into tagged raw elements.
|
||||
// It is useful for higher-level validation of raw transaction fields such as
|
||||
// extra, attachment, signatures, and proofs.
|
||||
func DecodeVariantVector(raw []byte) ([]VariantElement, error) {
|
||||
dec := NewDecoder(bytes.NewReader(raw))
|
||||
count := dec.ReadVarint()
|
||||
if dec.Err() != nil {
|
||||
return nil, dec.Err()
|
||||
}
|
||||
|
||||
elems := make([]VariantElement, 0, int(count))
|
||||
for i := uint64(0); i < count; i++ {
|
||||
tag := dec.ReadUint8()
|
||||
if dec.Err() != nil {
|
||||
return nil, coreerr.E("DecodeVariantVector", fmt.Sprintf("read tag %d", i), dec.Err())
|
||||
}
|
||||
|
||||
data := readVariantElementData(dec, tag)
|
||||
if dec.Err() != nil {
|
||||
return nil, coreerr.E("DecodeVariantVector", fmt.Sprintf("read element %d", i), dec.Err())
|
||||
}
|
||||
|
||||
elems = append(elems, VariantElement{Tag: tag, Data: data})
|
||||
}
|
||||
|
||||
return elems, nil
|
||||
}
|
||||
|
||||
// DecodeAssetDescriptorOperation decodes a raw asset_descriptor_operation
|
||||
// payload into its typed representation.
|
||||
func DecodeAssetDescriptorOperation(raw []byte) (types.AssetDescriptorOperation, error) {
|
||||
dec := NewDecoder(bytes.NewReader(raw))
|
||||
var op types.AssetDescriptorOperation
|
||||
|
||||
op.Version = dec.ReadUint8()
|
||||
op.OperationType = dec.ReadUint8()
|
||||
|
||||
if dec.ReadUint8() != 0 {
|
||||
assetID := dec.ReadBytes(32)
|
||||
if dec.Err() != nil {
|
||||
return types.AssetDescriptorOperation{}, coreerr.E("DecodeAssetDescriptorOperation", "read asset id", dec.Err())
|
||||
}
|
||||
copy(op.AssetID[:], assetID)
|
||||
}
|
||||
|
||||
if dec.ReadUint8() != 0 {
|
||||
desc := &types.AssetDescriptorBase{}
|
||||
desc.Ticker = decodeStringField(dec)
|
||||
desc.FullName = decodeStringField(dec)
|
||||
desc.TotalMaxSupply = dec.ReadUint64LE()
|
||||
desc.CurrentSupply = dec.ReadUint64LE()
|
||||
desc.DecimalPoint = dec.ReadUint8()
|
||||
desc.MetaInfo = decodeStringField(dec)
|
||||
ownerKey := dec.ReadBytes(32)
|
||||
if dec.Err() != nil {
|
||||
return types.AssetDescriptorOperation{}, coreerr.E("DecodeAssetDescriptorOperation", "read owner key", dec.Err())
|
||||
}
|
||||
copy(desc.OwnerKey[:], ownerKey)
|
||||
desc.Etc = readVariantVectorFixed(dec, 1)
|
||||
if dec.Err() != nil {
|
||||
return types.AssetDescriptorOperation{}, coreerr.E("DecodeAssetDescriptorOperation", "read descriptor etc", dec.Err())
|
||||
}
|
||||
op.Descriptor = desc
|
||||
}
|
||||
|
||||
op.AmountToEmit = dec.ReadUint64LE()
|
||||
op.AmountToBurn = dec.ReadUint64LE()
|
||||
op.Etc = readVariantVectorFixed(dec, 1)
|
||||
if dec.Err() != nil {
|
||||
return types.AssetDescriptorOperation{}, coreerr.E("DecodeAssetDescriptorOperation", "read trailing etc", dec.Err())
|
||||
}
|
||||
|
||||
if dec.Err() != nil {
|
||||
return types.AssetDescriptorOperation{}, coreerr.E("DecodeAssetDescriptorOperation", "decode asset descriptor operation", dec.Err())
|
||||
}
|
||||
|
||||
return op, nil
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue