diff --git a/consensus/tx.go b/consensus/tx.go index 17ba9ea..2f80ce5 100644 --- a/consensus/tx.go +++ b/consensus/tx.go @@ -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 } diff --git a/consensus/tx_version_test.go b/consensus/tx_version_test.go index 192d499..b7678b9 100644 --- a/consensus/tx_version_test.go +++ b/consensus/tx_version_test.go @@ -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 { diff --git a/types/asset.go b/types/asset.go new file mode 100644 index 0000000..db814a6 --- /dev/null +++ b/types/asset.go @@ -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 +} diff --git a/types/asset_test.go b/types/asset_test.go new file mode 100644 index 0000000..f538950 --- /dev/null +++ b/types/asset_test.go @@ -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) + } +}