feat(consensus): enforce hf5 tx version and add asset descriptors
Some checks are pending
Security Scan / security (push) Waiting to run
Test / Test (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 18:34:49 +00:00
parent f7ee451fc4
commit d3143d3f88
4 changed files with 123 additions and 3 deletions

View file

@ -75,14 +75,16 @@ func ValidateTransaction(tx *types.Transaction, txBlob []byte, forks []config.Ha
// checkTxVersion validates that the transaction version is appropriate for the
// current hardfork era.
//
// After HF5: transaction version must be >= VersionPostHF5 (3).
// After HF5: transaction version must be exactly VersionPostHF5 (3) and the
// embedded hardfork_id must match the active hardfork version.
// Before HF5: transaction version 3 is rejected (too early).
func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint64) error {
hf5Active := config.IsHardForkActive(forks, config.HF5, height)
currentFork := config.VersionAtHeight(forks, height)
if hf5Active && tx.Version < types.VersionPostHF5 {
if hf5Active && tx.Version != types.VersionPostHF5 {
return coreerr.E("checkTxVersion",
fmt.Sprintf("version %d too low after HF5 at height %d", tx.Version, height),
fmt.Sprintf("version %d invalid after HF5 at height %d", tx.Version, height),
ErrTxVersionInvalid)
}
@ -92,6 +94,12 @@ func checkTxVersion(tx *types.Transaction, forks []config.HardFork, height uint6
ErrTxVersionInvalid)
}
if hf5Active && tx.HardforkID != currentFork {
return coreerr.E("checkTxVersion",
fmt.Sprintf("hardfork id %d does not match active fork %d at height %d", tx.HardforkID, currentFork, height),
ErrTxVersionInvalid)
}
return nil
}

View file

@ -81,8 +81,20 @@ func TestCheckTxVersion_Bad(t *testing.T) {
}{
// v2 transaction after HF5 — must be v3.
{"v2_after_hf5", validV2Tx(), config.TestnetForks, 250},
// v3 transaction after HF5 with wrong hardfork id.
{"v3_after_hf5_wrong_hardfork", func() *types.Transaction {
tx := validV3Tx()
tx.HardforkID = 4
return tx
}(), config.TestnetForks, 250},
// v3 transaction before HF5 — too early.
{"v3_before_hf5", validV3Tx(), config.TestnetForks, 150},
// future version must be rejected.
{"v4_after_hf5", func() *types.Transaction {
tx := validV3Tx()
tx.Version = types.VersionPostHF5 + 1
return tx
}(), config.TestnetForks, 250},
}
for _, tt := range tests {

40
types/asset.go Normal file
View file

@ -0,0 +1,40 @@
// 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 types
// Asset operation types used by the HF5 asset_descriptor_operation variant.
const (
AssetOpRegister uint8 = 0 // deploy new asset
AssetOpEmit uint8 = 1 // emit additional supply
AssetOpUpdate uint8 = 2 // update asset metadata
AssetOpBurn uint8 = 3 // burn supply with proof
AssetOpPublicBurn uint8 = 4 // burn supply publicly
)
// AssetDescriptorBase holds the core asset metadata referenced by
// asset_descriptor_operation extra variants.
type AssetDescriptorBase struct {
Ticker string
FullName string
TotalMaxSupply uint64
CurrentSupply uint64
DecimalPoint uint8
MetaInfo string
OwnerKey PublicKey
Etc []byte
}
// 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 {
Version uint8
OperationType uint8
Descriptor *AssetDescriptorBase
AssetID Hash
AmountToEmit uint64
AmountToBurn uint64
Etc []byte
}

60
types/asset_test.go Normal file
View file

@ -0,0 +1,60 @@
// 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 types
import "testing"
func TestAssetOperationConstants_Good(t *testing.T) {
tests := []struct {
name string
got uint8
want uint8
}{
{"register", AssetOpRegister, 0},
{"emit", AssetOpEmit, 1},
{"update", AssetOpUpdate, 2},
{"burn", AssetOpBurn, 3},
{"public_burn", AssetOpPublicBurn, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.got != tt.want {
t.Fatalf("got %d, want %d", tt.got, tt.want)
}
})
}
}
func TestAssetDescriptorTypes_Good(t *testing.T) {
base := AssetDescriptorBase{
Ticker: "LTHN",
FullName: "Lethean",
TotalMaxSupply: 1000000,
CurrentSupply: 0,
DecimalPoint: 12,
MetaInfo: "{}",
OwnerKey: PublicKey{1},
Etc: []byte{1, 2, 3},
}
op := AssetDescriptorOperation{
Version: 1,
OperationType: AssetOpRegister,
Descriptor: &base,
AssetID: Hash{1},
AmountToEmit: 100,
AmountToBurn: 10,
Etc: []byte{4, 5, 6},
}
if op.Descriptor == nil || op.Descriptor.Ticker != "LTHN" {
t.Fatalf("unexpected descriptor: %+v", op.Descriptor)
}
if op.OperationType != AssetOpRegister || op.Version != 1 {
t.Fatalf("unexpected operation: %+v", op)
}
}