diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 75089db33..ceb161068 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -6,7 +6,7 @@ on: jobs: golangci-lint: name: GolangCI-Lint - runs-on: ubuntu-latest + runs-on: self-hosted steps: - name: Check out code into the Go module directory uses: actions/checkout@v3 diff --git a/.github/workflows/test_and_benchmark.yml b/.github/workflows/test_and_benchmark.yml index 0225f0353..17fef2469 100644 --- a/.github/workflows/test_and_benchmark.yml +++ b/.github/workflows/test_and_benchmark.yml @@ -5,17 +5,20 @@ jobs: strategy: matrix: go-version: [ "1.21.x" ] - platform: [ ubuntu-latest ] - runs-on: ${{ matrix.platform }} + platform: [ self-hosted ] + runs-on: ${{ matrix.platform }} steps: - name: Install Go if: success() uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} + - name: Checkout code uses: actions/checkout@v3 + - name: Run tests run: go test -v -covermode=count ./... + - name: Run Benchmarks run: go test -bench=. ./... \ No newline at end of file diff --git a/address_test.go b/address_test.go index dfceb0d85..14c26578b 100644 --- a/address_test.go +++ b/address_test.go @@ -462,7 +462,7 @@ func runOutputsSyntacticalValidationTest(t *testing.T, testAPI iotago.API, test func TestRestrictedAddressSyntacticalValidation(t *testing.T) { - defaultAmount := OneMi + defaultAmount := OneIOTA tests := []*outputsSyntacticalValidationTest{ // ok - Valid address types nested inside of a RestrictedAddress @@ -588,7 +588,7 @@ func TestRestrictedAddressSyntacticalValidation(t *testing.T) { func TestMultiAddressSyntacticalValidation(t *testing.T) { - defaultAmount := OneMi + defaultAmount := OneIOTA tests := []*outputsSyntacticalValidationTest{ // fail - threshold > cumulativeWeight diff --git a/api_test.go b/api_test.go index ae9e778d9..61eb48a58 100644 --- a/api_test.go +++ b/api_test.go @@ -13,7 +13,7 @@ import ( ) const ( - OneMi iotago.BaseToken = 1_000_000 + OneIOTA iotago.BaseToken = 1_000_000 ) type deSerializeTest struct { diff --git a/api_v3.go b/api_v3.go index 3da545efa..ebc056868 100644 --- a/api_v3.go +++ b/api_v3.go @@ -32,8 +32,8 @@ func disallowImplicitAccountCreationAddress(address Address) error { var ( basicOutputV3UnlockCondArrRules = &serix.ArrayRules{ - Min: 1, - Max: 4, + Min: 1, // Min: AddressUnlockCondition + Max: 4, // Max: AddressUnlockCondition, StorageDepositReturnUnlockCondition, TimelockUnlockCondition, ExpirationUnlockCondition MustOccur: serializer.TypePrefixes{ uint32(UnlockConditionAddress): struct{}{}, }, @@ -42,18 +42,18 @@ var ( serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } basicOutputV3FeatBlocksArrRules = &serix.ArrayRules{ - Min: 0, - Max: 4, + Min: 0, // Min: - + Max: 4, // Max: SenderFeature, MetadataFeature, TagFeature, NativeTokenFeature ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } accountOutputV3UnlockCondArrRules = &serix.ArrayRules{ - Min: 2, Max: 2, + Min: 1, // Min: AddressUnlockCondition + Max: 1, // Max: AddressUnlockCondition MustOccur: serializer.TypePrefixes{ - uint32(UnlockConditionStateControllerAddress): struct{}{}, - uint32(UnlockConditionGovernorAddress): struct{}{}, + uint32(UnlockConditionAddress): struct{}{}, }, ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | @@ -61,23 +61,52 @@ var ( } accountOutputV3FeatBlocksArrRules = &serix.ArrayRules{ - Min: 0, - Max: 4, + Min: 0, // Min: - + Max: 4, // Max: SenderFeature, MetadataFeature, BlockIssuerFeature, StakingFeature ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } accountOutputV3ImmFeatBlocksArrRules = &serix.ArrayRules{ - Min: 0, - Max: 2, + Min: 0, // Min: - + Max: 2, // Max: IssuerFeature, MetadataFeature + ValidationMode: serializer.ArrayValidationModeNoDuplicates | + serializer.ArrayValidationModeLexicalOrdering | + serializer.ArrayValidationModeAtMostOneOfEachTypeByte, + } + + anchorOutputV3UnlockCondArrRules = &serix.ArrayRules{ + Min: 2, // Min: StateControllerAddressUnlockCondition, GovernorAddressUnlockCondition + Max: 2, // Max: StateControllerAddressUnlockCondition, GovernorAddressUnlockCondition + MustOccur: serializer.TypePrefixes{ + uint32(UnlockConditionStateControllerAddress): struct{}{}, + uint32(UnlockConditionGovernorAddress): struct{}{}, + }, + ValidationMode: serializer.ArrayValidationModeNoDuplicates | + serializer.ArrayValidationModeLexicalOrdering | + serializer.ArrayValidationModeAtMostOneOfEachTypeByte, + } + + anchorOutputV3FeatBlocksArrRules = &serix.ArrayRules{ + Min: 0, // Min: - + Max: 2, // Max: SenderFeature, MetadataFeature + ValidationMode: serializer.ArrayValidationModeNoDuplicates | + serializer.ArrayValidationModeLexicalOrdering | + serializer.ArrayValidationModeAtMostOneOfEachTypeByte, + } + + anchorOutputV3ImmFeatBlocksArrRules = &serix.ArrayRules{ + Min: 0, // Min: - + Max: 2, // Max: IssuerFeature, MetadataFeature ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } foundryOutputV3UnlockCondArrRules = &serix.ArrayRules{ - Min: 1, Max: 1, + Min: 1, // Min: ImmutableAccountUnlockCondition + Max: 1, // Max: ImmutableAccountUnlockCondition MustOccur: serializer.TypePrefixes{ uint32(UnlockConditionImmutableAccount): struct{}{}, }, @@ -87,21 +116,24 @@ var ( } foundryOutputV3FeatBlocksArrRules = &serix.ArrayRules{ - Min: 0, Max: 2, + Min: 0, // Min: - + Max: 2, // Max: MetadataFeature, NativeTokenFeature ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } foundryOutputV3ImmFeatBlocksArrRules = &serix.ArrayRules{ - Min: 0, Max: 1, + Min: 0, // Min: - + Max: 1, // Max: MetadataFeature ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } nftOutputV3UnlockCondArrRules = &serix.ArrayRules{ - Min: 1, Max: 4, + Min: 1, // Min: AddressUnlockCondition + Max: 4, // Max: AddressUnlockCondition, StorageDepositReturnUnlockCondition, TimelockUnlockCondition, ExpirationUnlockCondition MustOccur: serializer.TypePrefixes{ uint32(UnlockConditionAddress): struct{}{}, }, @@ -111,23 +143,24 @@ var ( } nftOutputV3FeatBlocksArrRules = &serix.ArrayRules{ - Min: 0, - Max: 3, + Min: 0, // Min: - + Max: 3, // Max: SenderFeature, MetadataFeature, TagFeature ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } nftOutputV3ImmFeatBlocksArrRules = &serix.ArrayRules{ - Min: 0, - Max: 2, + Min: 0, // Min: - + Max: 2, // Max: IssuerFeature, MetadataFeature ValidationMode: serializer.ArrayValidationModeNoDuplicates | serializer.ArrayValidationModeLexicalOrdering | serializer.ArrayValidationModeAtMostOneOfEachTypeByte, } delegationOutputV3UnlockCondArrRules = &serix.ArrayRules{ - Min: 1, Max: 1, + Min: 1, // Min: AddressUnlockCondition + Max: 1, // Max: AddressUnlockCondition MustOccur: serializer.TypePrefixes{ uint32(UnlockConditionAddress): struct{}{}, }, @@ -163,7 +196,8 @@ var ( } txV3UnlocksArrRules = &serix.ArrayRules{ - Min: 1, Max: MaxInputsCount, + Min: 1, + Max: MaxInputsCount, } blockIDsArrRules = &serix.ArrayRules{ @@ -430,8 +464,7 @@ func V3API(protoParams ProtocolParameters) API { serix.TypeSettings{}.WithLengthPrefixType(serix.LengthPrefixTypeAsByte).WithArrayRules(accountOutputV3UnlockCondArrRules), )) - must(api.RegisterInterfaceObjects((*accountOutputUnlockCondition)(nil), (*StateControllerAddressUnlockCondition)(nil))) - must(api.RegisterInterfaceObjects((*accountOutputUnlockCondition)(nil), (*GovernorAddressUnlockCondition)(nil))) + must(api.RegisterInterfaceObjects((*accountOutputUnlockCondition)(nil), (*AddressUnlockCondition)(nil))) must(api.RegisterTypeSettings(AccountOutputFeatures{}, serix.TypeSettings{}.WithLengthPrefixType(serix.LengthPrefixTypeAsByte).WithArrayRules(accountOutputV3FeatBlocksArrRules), @@ -450,6 +483,34 @@ func V3API(protoParams ProtocolParameters) API { must(api.RegisterInterfaceObjects((*accountOutputImmFeature)(nil), (*MetadataFeature)(nil))) } + { + must(api.RegisterTypeSettings(AnchorOutput{}, serix.TypeSettings{}.WithObjectType(uint8(OutputAnchor)))) + must(api.RegisterValidators(AnchorOutput{}, nil, func(ctx context.Context, anchor AnchorOutput) error { + return anchor.syntacticallyValidate() + })) + + must(api.RegisterTypeSettings(AnchorOutputUnlockConditions{}, + serix.TypeSettings{}.WithLengthPrefixType(serix.LengthPrefixTypeAsByte).WithArrayRules(anchorOutputV3UnlockCondArrRules), + )) + + must(api.RegisterInterfaceObjects((*anchorOutputUnlockCondition)(nil), (*StateControllerAddressUnlockCondition)(nil))) + must(api.RegisterInterfaceObjects((*anchorOutputUnlockCondition)(nil), (*GovernorAddressUnlockCondition)(nil))) + + must(api.RegisterTypeSettings(AnchorOutputFeatures{}, + serix.TypeSettings{}.WithLengthPrefixType(serix.LengthPrefixTypeAsByte).WithArrayRules(anchorOutputV3FeatBlocksArrRules), + )) + + must(api.RegisterInterfaceObjects((*anchorOutputFeature)(nil), (*SenderFeature)(nil))) + must(api.RegisterInterfaceObjects((*anchorOutputFeature)(nil), (*MetadataFeature)(nil))) + + must(api.RegisterTypeSettings(AnchorOutputImmFeatures{}, + serix.TypeSettings{}.WithLengthPrefixType(serix.LengthPrefixTypeAsByte).WithArrayRules(anchorOutputV3ImmFeatBlocksArrRules), + )) + + must(api.RegisterInterfaceObjects((*anchorOutputImmFeature)(nil), (*IssuerFeature)(nil))) + must(api.RegisterInterfaceObjects((*anchorOutputImmFeature)(nil), (*MetadataFeature)(nil))) + } + { must(api.RegisterTypeSettings(FoundryOutput{}, serix.TypeSettings{}.WithObjectType(uint8(OutputFoundry))), @@ -569,6 +630,7 @@ func V3API(protoParams ProtocolParameters) API { must(api.RegisterInterfaceObjects((*TxEssenceOutput)(nil), (*BasicOutput)(nil))) must(api.RegisterInterfaceObjects((*TxEssenceOutput)(nil), (*AccountOutput)(nil))) + must(api.RegisterInterfaceObjects((*TxEssenceOutput)(nil), (*AnchorOutput)(nil))) must(api.RegisterInterfaceObjects((*TxEssenceOutput)(nil), (*DelegationOutput)(nil))) must(api.RegisterInterfaceObjects((*TxEssenceOutput)(nil), (*FoundryOutput)(nil))) must(api.RegisterInterfaceObjects((*TxEssenceOutput)(nil), (*NFTOutput)(nil))) diff --git a/builder/output_builder_account.go b/builder/output_builder_account.go index 268c57ad5..b72f39376 100644 --- a/builder/output_builder_account.go +++ b/builder/output_builder_account.go @@ -5,18 +5,15 @@ import ( iotago "github.com/iotaledger/iota.go/v4" ) -// NewAccountOutputBuilder creates a new AccountOutputBuilder with the required state controller/governor addresses and base token amount. -func NewAccountOutputBuilder(stateCtrl iotago.Address, govAddr iotago.Address, amount iotago.BaseToken) *AccountOutputBuilder { +// NewAccountOutputBuilder creates a new AccountOutputBuilder with the address and base token amount. +func NewAccountOutputBuilder(targetAddr iotago.Address, amount iotago.BaseToken) *AccountOutputBuilder { return &AccountOutputBuilder{output: &iotago.AccountOutput{ Amount: amount, Mana: 0, AccountID: iotago.EmptyAccountID, - StateIndex: 0, - StateMetadata: []byte{}, FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: govAddr}, + &iotago.AddressUnlockCondition{Address: targetAddr}, }, Features: iotago.AccountOutputFeatures{}, ImmutableFeatures: iotago.AccountOutputImmFeatures{}, @@ -34,16 +31,13 @@ func NewAccountOutputBuilderFromPrevious(previous *iotago.AccountOutput) *Accoun // AccountOutputBuilder builds an iotago.AccountOutput. type AccountOutputBuilder struct { - prev *iotago.AccountOutput - output *iotago.AccountOutput - stateCtrlReq bool - govCtrlReq bool + prev *iotago.AccountOutput + output *iotago.AccountOutput } // Amount sets the base token amount of the output. func (builder *AccountOutputBuilder) Amount(amount iotago.BaseToken) *AccountOutputBuilder { builder.output.Amount = amount - builder.stateCtrlReq = true return builder } @@ -63,34 +57,16 @@ func (builder *AccountOutputBuilder) AccountID(accountID iotago.AccountID) *Acco return builder } -// StateMetadata sets the state metadata of the output. -func (builder *AccountOutputBuilder) StateMetadata(data []byte) *AccountOutputBuilder { - builder.output.StateMetadata = data - builder.stateCtrlReq = true - - return builder -} - // FoundriesToGenerate bumps the output's foundry counter by the amount of foundries to generate. func (builder *AccountOutputBuilder) FoundriesToGenerate(count uint32) *AccountOutputBuilder { builder.output.FoundryCounter += count - builder.stateCtrlReq = true - - return builder -} - -// StateController sets the iotago.StateControllerAddressUnlockCondition of the output. -func (builder *AccountOutputBuilder) StateController(stateCtrl iotago.Address) *AccountOutputBuilder { - builder.output.Conditions.Upsert(&iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}) - builder.govCtrlReq = true return builder } -// Governor sets the iotago.GovernorAddressUnlockCondition of the output. -func (builder *AccountOutputBuilder) Governor(governor iotago.Address) *AccountOutputBuilder { - builder.output.Conditions.Upsert(&iotago.GovernorAddressUnlockCondition{Address: governor}) - builder.govCtrlReq = true +// Address sets/modifies an iotago.AddressUnlockCondition on the output. +func (builder *AccountOutputBuilder) Address(addr iotago.Address) *AccountOutputBuilder { + builder.output.Conditions.Upsert(&iotago.AddressUnlockCondition{Address: addr}) return builder } @@ -108,7 +84,6 @@ func (builder *AccountOutputBuilder) Staking(amount iotago.BaseToken, fixedCost StartEpoch: startEpoch, EndEpoch: endEpoch, }) - builder.govCtrlReq = true return builder } @@ -119,7 +94,6 @@ func (builder *AccountOutputBuilder) BlockIssuer(keys iotago.BlockIssuerKeys, ex BlockIssuerKeys: keys, ExpirySlot: expirySlot, }) - builder.govCtrlReq = true return builder } @@ -127,7 +101,6 @@ func (builder *AccountOutputBuilder) BlockIssuer(keys iotago.BlockIssuerKeys, ex // Sender sets/modifies an iotago.SenderFeature as a mutable feature on the output. func (builder *AccountOutputBuilder) Sender(senderAddr iotago.Address) *AccountOutputBuilder { builder.output.Features.Upsert(&iotago.SenderFeature{Address: senderAddr}) - builder.govCtrlReq = true return builder } @@ -143,7 +116,6 @@ func (builder *AccountOutputBuilder) ImmutableSender(senderAddr iotago.Address) // Metadata sets/modifies an iotago.MetadataFeature on the output. func (builder *AccountOutputBuilder) Metadata(data []byte) *AccountOutputBuilder { builder.output.Features.Upsert(&iotago.MetadataFeature{Data: data}) - builder.govCtrlReq = true return builder } @@ -158,14 +130,7 @@ func (builder *AccountOutputBuilder) ImmutableMetadata(data []byte) *AccountOutp // Build builds the iotago.AccountOutput. func (builder *AccountOutputBuilder) Build() (*iotago.AccountOutput, error) { - if builder.prev != nil && builder.govCtrlReq && builder.stateCtrlReq { - return nil, ierrors.New("builder calls require both state and governor transitions which is not possible") - } - if builder.prev != nil { - if builder.stateCtrlReq { - builder.output.StateIndex++ - } if !builder.prev.ImmutableFeatures.Equal(builder.output.ImmutableFeatures) { return nil, ierrors.New("immutable features are not allowed to be changed") } @@ -188,72 +153,10 @@ func (builder *AccountOutputBuilder) MustBuild() *iotago.AccountOutput { return output } -type accountStateTransition struct { - builder *AccountOutputBuilder -} - -// StateTransition narrows the builder functions to the ones available for an account state transition. -// -//nolint:revive -func (builder *AccountOutputBuilder) StateTransition() *accountStateTransition { - return &accountStateTransition{builder: builder} -} - -// Amount sets the base token amount of the output. -func (trans *accountStateTransition) Amount(amount iotago.BaseToken) *accountStateTransition { - return trans.builder.Amount(amount).StateTransition() -} - -// Mana sets the mana of the output. -func (trans *accountStateTransition) Mana(mana iotago.Mana) *accountStateTransition { - return trans.builder.Mana(mana).StateTransition() -} - -// StateMetadata sets the state metadata of the output. -func (trans *accountStateTransition) StateMetadata(data []byte) *accountStateTransition { - return trans.builder.StateMetadata(data).StateTransition() -} - -// FoundriesToGenerate bumps the output's foundry counter by the amount of foundries to generate. -func (trans *accountStateTransition) FoundriesToGenerate(count uint32) *accountStateTransition { - return trans.builder.FoundriesToGenerate(count).StateTransition() -} - -// Sender sets/modifies an iotago.SenderFeature as a mutable feature on the output. -func (trans *accountStateTransition) Sender(senderAddr iotago.Address) *accountStateTransition { - return trans.builder.Sender(senderAddr).StateTransition() -} - -// Builder returns the AccountOutputBuilder. -func (trans *accountStateTransition) Builder() *AccountOutputBuilder { - return trans.builder -} - -type accountGovernanceTransition struct { - builder *AccountOutputBuilder -} - -// GovernanceTransition narrows the builder functions to the ones available for an account governance transition. -// -//nolint:revive -func (builder *AccountOutputBuilder) GovernanceTransition() *accountGovernanceTransition { - return &accountGovernanceTransition{builder: builder} -} - -// StateController sets the iotago.StateControllerAddressUnlockCondition of the output. -func (trans *accountGovernanceTransition) StateController(stateCtrl iotago.Address) *accountGovernanceTransition { - return trans.builder.StateController(stateCtrl).GovernanceTransition() -} - -// Governor sets the iotago.GovernorAddressUnlockCondition of the output. -func (trans *accountGovernanceTransition) Governor(governor iotago.Address) *accountGovernanceTransition { - return trans.builder.Governor(governor).GovernanceTransition() -} - // BlockIssuerTransition narrows the builder functions to the ones available for an iotago.BlockIssuerFeature transition. // If BlockIssuerFeature does not exist, it creates and sets an empty feature. -func (trans *accountGovernanceTransition) BlockIssuerTransition() *blockIssuerTransition { - blockIssuerFeature := trans.builder.output.FeatureSet().BlockIssuer() +func (builder *AccountOutputBuilder) BlockIssuerTransition() *BlockIssuerTransition { + blockIssuerFeature := builder.output.FeatureSet().BlockIssuer() if blockIssuerFeature == nil { blockIssuerFeature = &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -261,21 +164,16 @@ func (trans *accountGovernanceTransition) BlockIssuerTransition() *blockIssuerTr } } - return &blockIssuerTransition{ + return &BlockIssuerTransition{ feature: blockIssuerFeature, - builder: trans.builder, + builder: builder, } } -// BlockIssuer sets/modifies an iotago.BlockIssuerFeature as a mutable feature on the output. -func (trans *accountGovernanceTransition) BlockIssuer(keys iotago.BlockIssuerKeys, expirySlot iotago.SlotIndex) *accountGovernanceTransition { - return trans.builder.BlockIssuer(keys, expirySlot).GovernanceTransition() -} - // StakingTransition narrows the builder functions to the ones available for an iotago.StakingFeature transition. // If StakingFeature does not exist, it creates and sets an empty feature. -func (trans *accountGovernanceTransition) StakingTransition() *stakingTransition { - stakingFeature := trans.builder.output.FeatureSet().Staking() +func (builder *AccountOutputBuilder) StakingTransition() *StakingTransition { + stakingFeature := builder.output.FeatureSet().Staking() if stakingFeature == nil { stakingFeature = &iotago.StakingFeature{ StakedAmount: 0, @@ -285,40 +183,19 @@ func (trans *accountGovernanceTransition) StakingTransition() *stakingTransition } } - return &stakingTransition{ + return &StakingTransition{ feature: stakingFeature, - builder: trans.builder, + builder: builder, } - } -// Staking sets/modifies an iotago.StakingFeature as a mutable feature on the output. -func (trans *accountGovernanceTransition) Staking(amount iotago.BaseToken, fixedCost iotago.Mana, startEpoch iotago.EpochIndex, optEndEpoch ...iotago.EpochIndex) *accountGovernanceTransition { - return trans.builder.Staking(amount, fixedCost, startEpoch, optEndEpoch...).GovernanceTransition() -} - -// Sender sets/modifies an iotago.SenderFeature as a mutable feature on the output. -func (trans *accountGovernanceTransition) Sender(senderAddr iotago.Address) *accountGovernanceTransition { - return trans.builder.Sender(senderAddr).GovernanceTransition() -} - -// Metadata sets/modifies an iotago.MetadataFeature as a mutable feature on the output. -func (trans *accountGovernanceTransition) Metadata(data []byte) *accountGovernanceTransition { - return trans.builder.Metadata(data).GovernanceTransition() -} - -// Builder returns the AccountOutputBuilder. -func (trans *accountGovernanceTransition) Builder() *AccountOutputBuilder { - return trans.builder -} - -type blockIssuerTransition struct { +type BlockIssuerTransition struct { feature *iotago.BlockIssuerFeature builder *AccountOutputBuilder } // AddKeys adds the keys of the BlockIssuerFeature. -func (trans *blockIssuerTransition) AddKeys(keys ...iotago.BlockIssuerKey) *blockIssuerTransition { +func (trans *BlockIssuerTransition) AddKeys(keys ...iotago.BlockIssuerKey) *BlockIssuerTransition { for _, key := range keys { blockIssuerKey := key trans.feature.BlockIssuerKeys.Add(blockIssuerKey) @@ -328,75 +205,65 @@ func (trans *blockIssuerTransition) AddKeys(keys ...iotago.BlockIssuerKey) *bloc } // RemoveKey deletes the key of the iotago.BlockIssuerFeature. -func (trans *blockIssuerTransition) RemoveKey(keyToDelete iotago.BlockIssuerKey) *blockIssuerTransition { +func (trans *BlockIssuerTransition) RemoveKey(keyToDelete iotago.BlockIssuerKey) *BlockIssuerTransition { trans.feature.BlockIssuerKeys.Remove(keyToDelete) return trans } // Keys sets the keys of the iotago.BlockIssuerFeature. -func (trans *blockIssuerTransition) Keys(keys iotago.BlockIssuerKeys) *blockIssuerTransition { +func (trans *BlockIssuerTransition) Keys(keys iotago.BlockIssuerKeys) *BlockIssuerTransition { trans.feature.BlockIssuerKeys = keys return trans } // ExpirySlot sets the ExpirySlot of iotago.BlockIssuerFeature. -func (trans *blockIssuerTransition) ExpirySlot(slot iotago.SlotIndex) *blockIssuerTransition { +func (trans *BlockIssuerTransition) ExpirySlot(slot iotago.SlotIndex) *BlockIssuerTransition { trans.feature.ExpirySlot = slot return trans } -// GovernanceTransition returns the accountGovernanceTransition. -func (trans *blockIssuerTransition) GovernanceTransition() *accountGovernanceTransition { - return trans.builder.GovernanceTransition() -} - // Builder returns the AccountOutputBuilder. -func (trans *blockIssuerTransition) Builder() *AccountOutputBuilder { +func (trans *BlockIssuerTransition) Builder() *AccountOutputBuilder { return trans.builder } -type stakingTransition struct { +type StakingTransition struct { feature *iotago.StakingFeature builder *AccountOutputBuilder } // StakedAmount sets the StakedAmount of iotago.StakingFeature. -func (trans *stakingTransition) StakedAmount(amount iotago.BaseToken) *stakingTransition { +func (trans *StakingTransition) StakedAmount(amount iotago.BaseToken) *StakingTransition { trans.feature.StakedAmount = amount return trans } // FixedCost sets the FixedCost of iotago.StakingFeature. -func (trans *stakingTransition) FixedCost(fixedCost iotago.Mana) *stakingTransition { +func (trans *StakingTransition) FixedCost(fixedCost iotago.Mana) *StakingTransition { trans.feature.FixedCost = fixedCost return trans } // StartEpoch sets the StartEpoch of iotago.StakingFeature. -func (trans *stakingTransition) StartEpoch(epoch iotago.EpochIndex) *stakingTransition { +func (trans *StakingTransition) StartEpoch(epoch iotago.EpochIndex) *StakingTransition { trans.feature.StartEpoch = epoch return trans } // EndEpoch sets the EndEpoch of iotago.StakingFeature. -func (trans *stakingTransition) EndEpoch(epoch iotago.EpochIndex) *stakingTransition { +func (trans *StakingTransition) EndEpoch(epoch iotago.EpochIndex) *StakingTransition { trans.feature.EndEpoch = epoch return trans } -// GovernanceTransition returns the accountGovernanceTransition. -func (trans *stakingTransition) GovernanceTransition() *accountGovernanceTransition { - return trans.builder.GovernanceTransition() -} - // Builder returns the AccountOutputBuilder. -func (trans *stakingTransition) Builder() *AccountOutputBuilder { +func (trans *StakingTransition) Builder() *AccountOutputBuilder { return trans.builder } diff --git a/builder/output_builder_anchor.go b/builder/output_builder_anchor.go new file mode 100644 index 000000000..64ad31252 --- /dev/null +++ b/builder/output_builder_anchor.go @@ -0,0 +1,223 @@ +package builder + +import ( + "github.com/iotaledger/hive.go/ierrors" + iotago "github.com/iotaledger/iota.go/v4" +) + +// NewAnchorOutputBuilder creates a new AnchorOutputBuilder with the required state controller/governor addresses and base token amount. +func NewAnchorOutputBuilder(stateCtrl iotago.Address, govAddr iotago.Address, amount iotago.BaseToken) *AnchorOutputBuilder { + return &AnchorOutputBuilder{output: &iotago.AnchorOutput{ + Amount: amount, + Mana: 0, + AnchorID: iotago.EmptyAnchorID, + StateIndex: 0, + StateMetadata: []byte{}, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, + &iotago.GovernorAddressUnlockCondition{Address: govAddr}, + }, + Features: iotago.AnchorOutputFeatures{}, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{}, + }} +} + +// NewAnchorOutputBuilderFromPrevious creates a new AnchorOutputBuilder starting from a copy of the previous iotago.AnchorOutput. +func NewAnchorOutputBuilderFromPrevious(previous *iotago.AnchorOutput) *AnchorOutputBuilder { + return &AnchorOutputBuilder{ + prev: previous, + //nolint:forcetypeassert // we can safely assume that this is an AnchorOutput + output: previous.Clone().(*iotago.AnchorOutput), + } +} + +// AnchorOutputBuilder builds an iotago.AnchorOutput. +type AnchorOutputBuilder struct { + prev *iotago.AnchorOutput + output *iotago.AnchorOutput + stateCtrlReq bool + govCtrlReq bool +} + +// Amount sets the base token amount of the output. +func (builder *AnchorOutputBuilder) Amount(amount iotago.BaseToken) *AnchorOutputBuilder { + builder.output.Amount = amount + builder.stateCtrlReq = true + + return builder +} + +// Mana sets the mana of the output. +func (builder *AnchorOutputBuilder) Mana(mana iotago.Mana) *AnchorOutputBuilder { + builder.output.Mana = mana + + return builder +} + +// AnchorID sets the iotago.AnchorID of this output. +// Do not call this function if the underlying iotago.AnchorOutput is not new. +func (builder *AnchorOutputBuilder) AnchorID(anchorID iotago.AnchorID) *AnchorOutputBuilder { + builder.output.AnchorID = anchorID + + return builder +} + +// StateMetadata sets the state metadata of the output. +func (builder *AnchorOutputBuilder) StateMetadata(data []byte) *AnchorOutputBuilder { + builder.output.StateMetadata = data + builder.stateCtrlReq = true + + return builder +} + +// StateController sets the iotago.StateControllerAddressUnlockCondition of the output. +func (builder *AnchorOutputBuilder) StateController(stateCtrl iotago.Address) *AnchorOutputBuilder { + builder.output.Conditions.Upsert(&iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}) + builder.govCtrlReq = true + + return builder +} + +// Governor sets the iotago.GovernorAddressUnlockCondition of the output. +func (builder *AnchorOutputBuilder) Governor(governor iotago.Address) *AnchorOutputBuilder { + builder.output.Conditions.Upsert(&iotago.GovernorAddressUnlockCondition{Address: governor}) + builder.govCtrlReq = true + + return builder +} + +// Sender sets/modifies an iotago.SenderFeature as a mutable feature on the output. +func (builder *AnchorOutputBuilder) Sender(senderAddr iotago.Address) *AnchorOutputBuilder { + builder.output.Features.Upsert(&iotago.SenderFeature{Address: senderAddr}) + builder.govCtrlReq = true + + return builder +} + +// ImmutableSender sets/modifies an iotago.SenderFeature as an immutable feature on the output. +// Only call this function on a new iotago.AnchorOutput. +func (builder *AnchorOutputBuilder) ImmutableSender(senderAddr iotago.Address) *AnchorOutputBuilder { + builder.output.ImmutableFeatures.Upsert(&iotago.SenderFeature{Address: senderAddr}) + + return builder +} + +// Metadata sets/modifies an iotago.MetadataFeature on the output. +func (builder *AnchorOutputBuilder) Metadata(data []byte) *AnchorOutputBuilder { + builder.output.Features.Upsert(&iotago.MetadataFeature{Data: data}) + builder.govCtrlReq = true + + return builder +} + +// ImmutableMetadata sets/modifies an iotago.MetadataFeature as an immutable feature on the output. +// Only call this function on a new iotago.AnchorOutput. +func (builder *AnchorOutputBuilder) ImmutableMetadata(data []byte) *AnchorOutputBuilder { + builder.output.ImmutableFeatures.Upsert(&iotago.MetadataFeature{Data: data}) + + return builder +} + +// Build builds the iotago.AnchorOutput. +func (builder *AnchorOutputBuilder) Build() (*iotago.AnchorOutput, error) { + if builder.prev != nil && builder.govCtrlReq && builder.stateCtrlReq { + return nil, ierrors.New("builder calls require both state and governor transitions which is not possible") + } + + if builder.prev != nil { + if builder.stateCtrlReq { + builder.output.StateIndex++ + } + if !builder.prev.ImmutableFeatures.Equal(builder.output.ImmutableFeatures) { + return nil, ierrors.New("immutable features are not allowed to be changed") + } + } + + builder.output.Conditions.Sort() + builder.output.Features.Sort() + builder.output.ImmutableFeatures.Sort() + + return builder.output, nil +} + +// MustBuild works like Build() but panics if an error is encountered. +func (builder *AnchorOutputBuilder) MustBuild() *iotago.AnchorOutput { + output, err := builder.Build() + if err != nil { + panic(err) + } + + return output +} + +type anchorStateTransition struct { + builder *AnchorOutputBuilder +} + +// StateTransition narrows the builder functions to the ones available for an anchor state transition. +// +//nolint:revive +func (builder *AnchorOutputBuilder) StateTransition() *anchorStateTransition { + return &anchorStateTransition{builder: builder} +} + +// Amount sets the base token amount of the output. +func (trans *anchorStateTransition) Amount(amount iotago.BaseToken) *anchorStateTransition { + return trans.builder.Amount(amount).StateTransition() +} + +// Mana sets the mana of the output. +func (trans *anchorStateTransition) Mana(mana iotago.Mana) *anchorStateTransition { + return trans.builder.Mana(mana).StateTransition() +} + +// StateMetadata sets the state metadata of the output. +func (trans *anchorStateTransition) StateMetadata(data []byte) *anchorStateTransition { + return trans.builder.StateMetadata(data).StateTransition() +} + +// Sender sets/modifies an iotago.SenderFeature as a mutable feature on the output. +func (trans *anchorStateTransition) Sender(senderAddr iotago.Address) *anchorStateTransition { + return trans.builder.Sender(senderAddr).StateTransition() +} + +// Builder returns the AnchorOutputBuilder. +func (trans *anchorStateTransition) Builder() *AnchorOutputBuilder { + return trans.builder +} + +type anchorGovernanceTransition struct { + builder *AnchorOutputBuilder +} + +// GovernanceTransition narrows the builder functions to the ones available for an anchor governance transition. +// +//nolint:revive +func (builder *AnchorOutputBuilder) GovernanceTransition() *anchorGovernanceTransition { + return &anchorGovernanceTransition{builder: builder} +} + +// StateController sets the iotago.StateControllerAddressUnlockCondition of the output. +func (trans *anchorGovernanceTransition) StateController(stateCtrl iotago.Address) *anchorGovernanceTransition { + return trans.builder.StateController(stateCtrl).GovernanceTransition() +} + +// Governor sets the iotago.GovernorAddressUnlockCondition of the output. +func (trans *anchorGovernanceTransition) Governor(governor iotago.Address) *anchorGovernanceTransition { + return trans.builder.Governor(governor).GovernanceTransition() +} + +// Sender sets/modifies an iotago.SenderFeature as a mutable feature on the output. +func (trans *anchorGovernanceTransition) Sender(senderAddr iotago.Address) *anchorGovernanceTransition { + return trans.builder.Sender(senderAddr).GovernanceTransition() +} + +// Metadata sets/modifies an iotago.MetadataFeature as a mutable feature on the output. +func (trans *anchorGovernanceTransition) Metadata(data []byte) *anchorGovernanceTransition { + return trans.builder.Metadata(data).GovernanceTransition() +} + +// Builder returns the AnchorOutputBuilder. +func (trans *anchorGovernanceTransition) Builder() *AnchorOutputBuilder { + return trans.builder +} diff --git a/builder/output_builder_test.go b/builder/output_builder_test.go index efdea8d61..12a4394fb 100644 --- a/builder/output_builder_test.go +++ b/builder/output_builder_test.go @@ -48,8 +48,7 @@ func TestBasicOutputBuilder(t *testing.T) { func TestAccountOutputBuilder(t *testing.T) { var ( - stateCtrl = tpkg.RandEd25519Address() - gov = tpkg.RandEd25519Address() + addr = tpkg.RandEd25519Address() amount iotago.BaseToken = 1337 metadata = []byte("123456") immMetadata = []byte("654321") @@ -62,9 +61,8 @@ func TestAccountOutputBuilder(t *testing.T) { newBlockIssuerKey2 = iotago.Ed25519PublicKeyBlockIssuerKeyFromPublicKey(tpkg.Rand32ByteArray()) ) - accountOutput, err := builder.NewAccountOutputBuilder(stateCtrl, gov, amount). + accountOutput, err := builder.NewAccountOutputBuilder(addr, amount). Metadata(metadata). - StateMetadata(metadata). Staking(amount, 1, 1000). BlockIssuer(iotago.NewBlockIssuerKeys(blockIssuerKey1, blockIssuerKey2, blockIssuerKey3), 100000). ImmutableMetadata(immMetadata). @@ -77,12 +75,9 @@ func TestAccountOutputBuilder(t *testing.T) { expected := &iotago.AccountOutput{ Amount: 1337, - StateIndex: 0, - StateMetadata: metadata, FoundryCounter: 5, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: gov}, + &iotago.AddressUnlockCondition{Address: addr}, }, Features: iotago.AccountOutputFeatures{ &iotago.MetadataFeature{Data: metadata}, @@ -108,19 +103,19 @@ func TestAccountOutputBuilder(t *testing.T) { //nolint:forcetypeassert // we can safely assume that this is an AccountOutput expectedCpy := expected.Clone().(*iotago.AccountOutput) expectedCpy.Amount = newAmount - expectedCpy.StateIndex++ - updatedOutput, err := builder.NewAccountOutputBuilderFromPrevious(accountOutput).StateTransition(). - Amount(newAmount).Builder().Build() + + updatedOutput, err := builder.NewAccountOutputBuilderFromPrevious(accountOutput). + Amount(newAmount).Build() require.NoError(t, err) require.Equal(t, expectedCpy, updatedOutput) - updatedFeatures, err := builder.NewAccountOutputBuilderFromPrevious(accountOutput).GovernanceTransition(). + updatedFeatures, err := builder.NewAccountOutputBuilderFromPrevious(accountOutput). BlockIssuerTransition(). AddKeys(newBlockIssuerKey2, newBlockIssuerKey1). RemoveKey(blockIssuerKey3). RemoveKey(blockIssuerKey1). ExpirySlot(1500). - GovernanceTransition(). + Builder(). StakingTransition(). EndEpoch(2000). Builder().Build() @@ -130,12 +125,9 @@ func TestAccountOutputBuilder(t *testing.T) { expectedFeatures := &iotago.AccountOutput{ Amount: 1337, - StateIndex: 0, - StateMetadata: metadata, FoundryCounter: 5, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: gov}, + &iotago.AddressUnlockCondition{Address: addr}, }, Features: iotago.AccountOutputFeatures{ &iotago.MetadataFeature{Data: metadata}, @@ -158,6 +150,80 @@ func TestAccountOutputBuilder(t *testing.T) { require.True(t, expectedFeatures.Equal(updatedFeatures), "features should be equal") } +func TestAnchorOutputBuilder(t *testing.T) { + var ( + stateCtrl = tpkg.RandEd25519Address() + stateCtrlNew = tpkg.RandEd25519Address() + gov = tpkg.RandEd25519Address() + amount iotago.BaseToken = 1337 + metadata = []byte("123456") + immMetadata = []byte("654321") + immSender = tpkg.RandEd25519Address() + ) + + anchorOutput, err := builder.NewAnchorOutputBuilder(stateCtrl, gov, amount). + Metadata(metadata). + StateMetadata(metadata). + ImmutableMetadata(immMetadata). + ImmutableSender(immSender). + Build() + require.NoError(t, err) + + expected := &iotago.AnchorOutput{ + Amount: amount, + StateIndex: 0, + StateMetadata: metadata, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, + &iotago.GovernorAddressUnlockCondition{Address: gov}, + }, + Features: iotago.AnchorOutputFeatures{ + &iotago.MetadataFeature{Data: metadata}, + }, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.SenderFeature{Address: immSender}, + &iotago.MetadataFeature{Data: immMetadata}, + }, + } + require.True(t, expected.Equal(anchorOutput), "anchor output should be equal") + + const newAmount iotago.BaseToken = 7331 + //nolint:forcetypeassert // we can safely assume that this is an AnchorOutput + expectedCpy := expected.Clone().(*iotago.AnchorOutput) + expectedCpy.Amount = newAmount + expectedCpy.StateIndex++ + expectedCpy.StateMetadata = []byte("newState") + updatedOutput, err := builder.NewAnchorOutputBuilderFromPrevious(anchorOutput).StateTransition(). + Amount(newAmount). + StateMetadata([]byte("newState")). + Builder().Build() + require.NoError(t, err) + require.Equal(t, expectedCpy, updatedOutput) + require.True(t, expectedCpy.Equal(updatedOutput), "outputs should be equal") + + updatedOutput2, err := builder.NewAnchorOutputBuilderFromPrevious(anchorOutput).GovernanceTransition(). + StateController(stateCtrlNew).Builder().Build() + require.NoError(t, err) + + expectedOutput2 := &iotago.AnchorOutput{ + Amount: amount, + StateIndex: 0, + StateMetadata: metadata, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: stateCtrlNew}, + &iotago.GovernorAddressUnlockCondition{Address: gov}, + }, + Features: iotago.AnchorOutputFeatures{ + &iotago.MetadataFeature{Data: metadata}, + }, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.SenderFeature{Address: immSender}, + &iotago.MetadataFeature{Data: immMetadata}, + }, + } + require.True(t, expectedOutput2.Equal(updatedOutput2), "outputs should be equal") +} + func TestDelegationOutputBuilder(t *testing.T) { var ( address = tpkg.RandEd25519Address() diff --git a/builder/transaction_builder_test.go b/builder/transaction_builder_test.go index 2d2869c56..3c417bcbb 100644 --- a/builder/transaction_builder_test.go +++ b/builder/transaction_builder_test.go @@ -27,6 +27,7 @@ func TestTransactionBuilder(t *testing.T) { } tests := []*test{ + // ok - 1 input/output func() *test { inputUTXO1 := &iotago.UTXOInput{TransactionID: tpkg.Rand36ByteArray(), TransactionOutputIndex: 0} input := tpkg.RandBasicOutput(iotago.AddressEd25519) @@ -45,6 +46,8 @@ func TestTransactionBuilder(t *testing.T) { builder: bdl, } }(), + + // ok - mix basic+chain outputs func() *test { var ( inputID1 = &iotago.UTXOInput{TransactionID: tpkg.Rand36ByteArray(), TransactionOutputIndex: 0} @@ -71,8 +74,7 @@ func TestTransactionBuilder(t *testing.T) { Amount: 1000, AccountID: tpkg.Rand32ByteArray(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: nftOutput.ChainID().ToAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: nftOutput.ChainID().ToAddress()}, + &iotago.AddressUnlockCondition{Address: nftOutput.ChainID().ToAddress()}, }, } @@ -100,6 +102,8 @@ func TestTransactionBuilder(t *testing.T) { builder: bdl, } }(), + + // ok - with tagged data payload func() *test { inputUTXO1 := &iotago.UTXOInput{TransactionID: tpkg.Rand36ByteArray(), TransactionOutputIndex: 0} @@ -119,6 +123,8 @@ func TestTransactionBuilder(t *testing.T) { builder: bdl, } }(), + + // err - missing address keys (wrong address) func() *test { inputUTXO1 := &iotago.UTXOInput{TransactionID: tpkg.Rand36ByteArray(), TransactionOutputIndex: 0} @@ -144,6 +150,8 @@ func TestTransactionBuilder(t *testing.T) { buildErr: iotago.ErrAddressKeysNotMapped, } }(), + + // err - missing address keys (no keys given at all) func() *test { inputUTXO1 := &iotago.UTXOInput{TransactionID: tpkg.Rand36ByteArray(), TransactionOutputIndex: 0} diff --git a/gen/identifier_gen.go b/gen/identifier_gen.go index 2126661de..11afeb151 100644 --- a/gen/identifier_gen.go +++ b/gen/identifier_gen.go @@ -4,3 +4,4 @@ package gen //go:generate go run github.com/iotaledger/hive.go/codegen/features/cmd@13da292 identifier.tmpl ../identifier.gen.go Identifier i "" "" //go:generate go run github.com/iotaledger/hive.go/codegen/features/cmd@13da292 identifier.tmpl ../identifier_account.gen.go AccountID a "chainid" "AddressType=AccountAddress" +//go:generate go run github.com/iotaledger/hive.go/codegen/features/cmd@13da292 identifier.tmpl ../identifier_anchor.gen.go AnchorID a "chainid" "AddressType=AnchorAddress" diff --git a/output.go b/output.go index a2dd9be1c..86053e237 100644 --- a/output.go +++ b/output.go @@ -63,6 +63,8 @@ const ( OutputBasic OutputType = iota // OutputAccount denotes an AccountOutput. OutputAccount + // OutputAnchor denotes an AnchorOuptut. + OutputAnchor // OutputFoundry denotes a FoundryOutput. OutputFoundry // OutputNFT denotes an NFTOutput. @@ -82,6 +84,7 @@ func (outputType OutputType) String() string { var outputNames = [OutputDelegation + 1]string{ "BasicOutput", "AccountOutput", + "AnchorOutput", "FoundryOutput", "NFTOutput", "DelegationOutput", @@ -414,8 +417,8 @@ func OutputsSyntacticalExpirationAndTimelock() OutputsSyntacticalValidationFunc } // OutputsSyntacticalAccount returns an OutputsSyntacticalValidationFunc which checks that AccountOutput(s)': -// - StateIndex/FoundryCounter are zero if the AccountID is zeroed -// - StateController and GovernanceController must be different from AccountAddress derived from AccountID +// - FoundryCounter is zero if the AccountID is zeroed +// - Address must be different from AccountAddress derived from AccountID func OutputsSyntacticalAccount() OutputsSyntacticalValidationFunc { return func(index int, output Output) error { accountOutput, is := output.(*AccountOutput) @@ -424,22 +427,47 @@ func OutputsSyntacticalAccount() OutputsSyntacticalValidationFunc { } if accountOutput.AccountEmpty() { - switch { - case accountOutput.StateIndex != 0: - return ierrors.Wrapf(ErrAccountOutputNonEmptyState, "output %d, state index not zero", index) - case accountOutput.FoundryCounter != 0: + if accountOutput.FoundryCounter != 0 { return ierrors.Wrapf(ErrAccountOutputNonEmptyState, "output %d, foundry counter not zero", index) } + // can not be cyclic when the AccountOutput is new return nil } - outputAccountAddr := AccountAddress(accountOutput.AccountID) - if stateCtrlAddr, ok := accountOutput.StateController().(*AccountAddress); ok && outputAccountAddr == *stateCtrlAddr { - return ierrors.Wrapf(ErrAccountOutputCyclicAddress, "output %d, AccountID=StateController", index) + if addr, ok := accountOutput.Ident().(*AccountAddress); ok && AccountAddress(accountOutput.AccountID) == *addr { + return ierrors.Wrapf(ErrAccountOutputCyclicAddress, "output %d", index) + } + + return nil + } +} + +// OutputsSyntacticalAnchor returns an OutputsSyntacticalValidationFunc which checks that AnchorOutput(s)': +// - StateIndex is zero if the AnchorID is zeroed +// - StateController and GovernanceController must be different from AnchorAddress derived from AnchorID +func OutputsSyntacticalAnchor() OutputsSyntacticalValidationFunc { + return func(index int, output Output) error { + anchorOutput, is := output.(*AnchorOutput) + if !is { + return nil + } + + if anchorOutput.AnchorEmpty() { + if anchorOutput.StateIndex != 0 { + return ierrors.Wrapf(ErrAnchorOutputNonEmptyState, "output %d, state index not zero", index) + } + + // can not be cyclic when the AnchorOutput is new + return nil + } + + outputAnchorAddr := AnchorAddress(anchorOutput.AnchorID) + if stateCtrlAddr, ok := anchorOutput.StateController().(*AnchorAddress); ok && outputAnchorAddr == *stateCtrlAddr { + return ierrors.Wrapf(ErrAnchorOutputCyclicAddress, "output %d, AnchorID=StateController", index) } - if govCtrlAddr, ok := accountOutput.GovernorAddress().(*AccountAddress); ok && outputAccountAddr == *govCtrlAddr { - return ierrors.Wrapf(ErrAccountOutputCyclicAddress, "output %d, AccountID=GovernanceController", index) + if govCtrlAddr, ok := anchorOutput.GovernorAddress().(*AnchorAddress); ok && outputAnchorAddr == *govCtrlAddr { + return ierrors.Wrapf(ErrAnchorOutputCyclicAddress, "output %d, AnchorID=GovernanceController", index) } return nil diff --git a/output_account.go b/output_account.go index b641c8706..7f2050887 100644 --- a/output_account.go +++ b/output_account.go @@ -1,8 +1,6 @@ package iotago import ( - "bytes" - "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/serializer/v2" ) @@ -12,8 +10,6 @@ var ( ErrNonUniqueAccountOutputs = ierrors.New("non unique accounts within outputs") // ErrInvalidAccountStateTransition gets returned when an account is doing an invalid state transition. ErrInvalidAccountStateTransition = ierrors.New("invalid account state transition") - // ErrInvalidAccountGovernanceTransition gets returned when an account is doing an invalid governance transition. - ErrInvalidAccountGovernanceTransition = ierrors.New("invalid account governance transition") // ErrInvalidBlockIssuerTransition gets returned when an account tries to transition block issuer expiry too soon. ErrInvalidBlockIssuerTransition = ierrors.New("invalid block issuer transition") // ErrAccountLocked gets returned when an account has negative block issuance credits. @@ -109,27 +105,15 @@ type AccountOutput struct { // The stored mana held by the output. Mana Mana `serix:"1,mapKey=mana"` // The identifier for this account. - AccountID AccountID `serix:"3,mapKey=accountId"` - // The index of the state. - StateIndex uint32 `serix:"4,mapKey=stateIndex"` - // The state of the account which can only be mutated by the state controller. - StateMetadata []byte `serix:"5,lengthPrefixType=uint16,mapKey=stateMetadata,omitempty,maxLen=8192"` + AccountID AccountID `serix:"2,mapKey=accountId"` // The counter that denotes the number of foundries created by this account. - FoundryCounter uint32 `serix:"6,mapKey=foundryCounter"` + FoundryCounter uint32 `serix:"3,mapKey=foundryCounter"` // The unlock conditions on this output. - Conditions AccountOutputUnlockConditions `serix:"7,mapKey=unlockConditions,omitempty"` + Conditions AccountOutputUnlockConditions `serix:"4,mapKey=unlockConditions,omitempty"` // The features on the output. - Features AccountOutputFeatures `serix:"8,mapKey=features,omitempty"` + Features AccountOutputFeatures `serix:"5,mapKey=features,omitempty"` // The immutable feature on the output. - ImmutableFeatures AccountOutputImmFeatures `serix:"9,mapKey=immutableFeatures,omitempty"` -} - -func (a *AccountOutput) GovernorAddress() Address { - return a.Conditions.MustSet().GovernorAddress().Address -} - -func (a *AccountOutput) StateController() Address { - return a.Conditions.MustSet().StateControllerAddress().Address + ImmutableFeatures AccountOutputImmFeatures `serix:"6,mapKey=immutableFeatures,omitempty"` } func (a *AccountOutput) Clone() Output { @@ -137,8 +121,6 @@ func (a *AccountOutput) Clone() Output { Amount: a.Amount, Mana: a.Mana, AccountID: a.AccountID, - StateIndex: a.StateIndex, - StateMetadata: append([]byte(nil), a.StateMetadata...), FoundryCounter: a.FoundryCounter, Conditions: a.Conditions.Clone(), Features: a.Features.Clone(), @@ -164,14 +146,6 @@ func (a *AccountOutput) Equal(other Output) bool { return false } - if a.StateIndex != otherOutput.StateIndex { - return false - } - - if !bytes.Equal(a.StateMetadata, otherOutput.StateMetadata) { - return false - } - if a.FoundryCounter != otherOutput.FoundryCounter { return false } @@ -191,8 +165,9 @@ func (a *AccountOutput) Equal(other Output) bool { return true } -func (a *AccountOutput) UnlockableBy(ident Address, next TransDepIdentOutput, pastBoundedSlotIndex SlotIndex, futureBoundedSlotIndex SlotIndex) (bool, error) { - return outputUnlockableBy(a, next, ident, pastBoundedSlotIndex, futureBoundedSlotIndex) +func (a *AccountOutput) UnlockableBy(ident Address, pastBoundedSlotIndex SlotIndex, futureBoundedSlotIndex SlotIndex) bool { + ok, _ := outputUnlockableBy(a, nil, ident, pastBoundedSlotIndex, futureBoundedSlotIndex) + return ok } func (a *AccountOutput) StorageScore(storageScoreStruct *StorageScoreStructure, _ StorageScoreFunc) StorageScore { @@ -205,10 +180,7 @@ func (a *AccountOutput) StorageScore(storageScoreStruct *StorageScoreStructure, func (a *AccountOutput) syntacticallyValidate() error { // Address should never be nil. - stateControllerAddress := a.Conditions.MustSet().StateControllerAddress().Address - governorAddress := a.Conditions.MustSet().GovernorAddress().Address - - if (stateControllerAddress.Type() == AddressImplicitAccountCreation) || (governorAddress.Type() == AddressImplicitAccountCreation) { + if a.Conditions.MustSet().Address().Address.Type() == AddressImplicitAccountCreation { return ErrImplicitAccountCreationAddressInInvalidOutput } @@ -234,23 +206,8 @@ func (a *AccountOutput) WorkScore(workScoreParameters *WorkScoreParameters) (Wor return workScoreConditions.Add(workScoreFeatures, workScoreImmutableFeatures) } -func (a *AccountOutput) Ident(nextState TransDepIdentOutput) (Address, error) { - // if there isn't a next state, then only the governance address can destroy the account - if nextState == nil { - return a.GovernorAddress(), nil - } - otherAccountOutput, isAccountOutput := nextState.(*AccountOutput) - if !isAccountOutput { - return nil, ierrors.Wrapf(ErrTransDepIdentOutputNextInvalid, "expected AccountOutput but got %s for ident computation", nextState.Type()) - } - switch { - case a.StateIndex == otherAccountOutput.StateIndex: - return a.GovernorAddress(), nil - case a.StateIndex+1 == otherAccountOutput.StateIndex: - return a.StateController(), nil - default: - return nil, ierrors.Wrap(ErrTransDepIdentOutputNextInvalid, "can not compute right ident for account output as state index delta is invalid") - } +func (a *AccountOutput) Ident() Address { + return a.Conditions.MustSet().Address().Address } func (a *AccountOutput) ChainID() ChainID { @@ -298,10 +255,6 @@ func (a *AccountOutput) Size() int { BaseTokenSize + ManaSize + AccountIDLength + - // StateIndex - serializer.UInt32ByteSize + - serializer.UInt16ByteSize + - len(a.StateMetadata) + // FoundryCounter serializer.UInt32ByteSize + a.Conditions.Size() + diff --git a/output_anchor.go b/output_anchor.go new file mode 100644 index 000000000..32936c91e --- /dev/null +++ b/output_anchor.go @@ -0,0 +1,287 @@ +package iotago + +import ( + "bytes" + + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/serializer/v2" +) + +var ( + // ErrNonUniqueAnchorOutputs gets returned when multiple AnchorOutputs(s) with the same AnchorID exist within sets. + ErrNonUniqueAnchorOutputs = ierrors.New("non unique anchors within outputs") + // ErrInvalidAnchorStateTransition gets returned when an anchor is doing an invalid state transition. + ErrInvalidAnchorStateTransition = ierrors.New("invalid anchor state transition") + // ErrInvalidAnchorGovernanceTransition gets returned when an anchor is doing an invalid governance transition. + ErrInvalidAnchorGovernanceTransition = ierrors.New("invalid anchor governance transition") + // ErrAnchorMissing gets returned when an anchor is missing. + ErrAnchorMissing = ierrors.New("anchor is missing") +) + +// AnchorOutputs is a slice of AnchorOutput(s). +type AnchorOutputs []*AnchorOutput + +// Every checks whether every element passes f. +// Returns either -1 if all elements passed f or the index of the first element which didn't. +func (outputs AnchorOutputs) Every(f func(output *AnchorOutput) bool) int { + for i, output := range outputs { + if !f(output) { + return i + } + } + + return -1 +} + +// AnchorOutputsSet is a set of AnchorOutput(s). +type AnchorOutputsSet map[AnchorID]*AnchorOutput + +// Includes checks whether all anchors included in other exist in this set. +func (set AnchorOutputsSet) Includes(other AnchorOutputsSet) error { + for anchorID := range other { + if _, has := set[anchorID]; !has { + return ierrors.Wrapf(ErrAnchorMissing, "%s missing in source", anchorID.ToHex()) + } + } + + return nil +} + +// EveryTuple runs f for every key which exists in both this set and other. +func (set AnchorOutputsSet) EveryTuple(other AnchorOutputsSet, f func(in *AnchorOutput, out *AnchorOutput) error) error { + for k, v := range set { + v2, has := other[k] + if !has { + continue + } + if err := f(v, v2); err != nil { + return err + } + } + + return nil +} + +// Merge merges other with this set in a new set. +// Returns an error if an anchor isn't unique across both sets. +func (set AnchorOutputsSet) Merge(other AnchorOutputsSet) (AnchorOutputsSet, error) { + newSet := make(AnchorOutputsSet) + for k, v := range set { + newSet[k] = v + } + for k, v := range other { + if _, has := newSet[k]; has { + return nil, ierrors.Wrapf(ErrNonUniqueAnchorOutputs, "anchor %s exists in both sets", k.ToHex()) + } + newSet[k] = v + } + + return newSet, nil +} + +type ( + anchorOutputUnlockCondition interface{ UnlockCondition } + anchorOutputFeature interface{ Feature } + anchorOutputImmFeature interface{ Feature } + AnchorOutputUnlockConditions = UnlockConditions[anchorOutputUnlockCondition] + AnchorOutputFeatures = Features[anchorOutputFeature] + AnchorOutputImmFeatures = Features[anchorOutputImmFeature] +) + +// AnchorOutput is an output type which represents an anchor. +type AnchorOutput struct { + // The amount of IOTA tokens held by the output. + Amount BaseToken `serix:"0,mapKey=amount"` + // The stored mana held by the output. + Mana Mana `serix:"1,mapKey=mana"` + // The identifier for this anchor. + AnchorID AnchorID `serix:"2,mapKey=anchorId"` + // The index of the state. + StateIndex uint32 `serix:"3,mapKey=stateIndex"` + // The state of the anchor which can only be mutated by the state controller. + StateMetadata []byte `serix:"4,lengthPrefixType=uint16,mapKey=stateMetadata,omitempty,maxLen=8192"` + // The unlock conditions on this output. + Conditions AnchorOutputUnlockConditions `serix:"5,mapKey=unlockConditions,omitempty"` + // The features on the output. + Features AnchorOutputFeatures `serix:"6,mapKey=features,omitempty"` + // The immutable feature on the output. + ImmutableFeatures AnchorOutputImmFeatures `serix:"7,mapKey=immutableFeatures,omitempty"` +} + +func (a *AnchorOutput) GovernorAddress() Address { + return a.Conditions.MustSet().GovernorAddress().Address +} + +func (a *AnchorOutput) StateController() Address { + return a.Conditions.MustSet().StateControllerAddress().Address +} + +func (a *AnchorOutput) Clone() Output { + return &AnchorOutput{ + Amount: a.Amount, + Mana: a.Mana, + AnchorID: a.AnchorID, + StateIndex: a.StateIndex, + StateMetadata: append([]byte(nil), a.StateMetadata...), + Conditions: a.Conditions.Clone(), + Features: a.Features.Clone(), + ImmutableFeatures: a.ImmutableFeatures.Clone(), + } +} + +func (a *AnchorOutput) Equal(other Output) bool { + otherOutput, isSameType := other.(*AnchorOutput) + if !isSameType { + return false + } + + if a.Amount != otherOutput.Amount { + return false + } + + if a.Mana != otherOutput.Mana { + return false + } + + if a.AnchorID != otherOutput.AnchorID { + return false + } + + if a.StateIndex != otherOutput.StateIndex { + return false + } + + if !bytes.Equal(a.StateMetadata, otherOutput.StateMetadata) { + return false + } + + if !a.Conditions.Equal(otherOutput.Conditions) { + return false + } + + if !a.Features.Equal(otherOutput.Features) { + return false + } + + if !a.ImmutableFeatures.Equal(otherOutput.ImmutableFeatures) { + return false + } + + return true +} + +func (a *AnchorOutput) UnlockableBy(ident Address, next TransDepIdentOutput, pastBoundedSlotIndex SlotIndex, futureBoundedSlotIndex SlotIndex) (bool, error) { + return outputUnlockableBy(a, next, ident, pastBoundedSlotIndex, futureBoundedSlotIndex) +} + +func (a *AnchorOutput) StorageScore(storageScoreStruct *StorageScoreStructure, _ StorageScoreFunc) StorageScore { + return storageScoreStruct.OffsetOutput + + storageScoreStruct.FactorData().Multiply(StorageScore(a.Size())) + + a.Conditions.StorageScore(storageScoreStruct, nil) + + a.Features.StorageScore(storageScoreStruct, nil) + + a.ImmutableFeatures.StorageScore(storageScoreStruct, nil) +} + +func (a *AnchorOutput) syntacticallyValidate() error { + // Address should never be nil. + stateControllerAddress := a.Conditions.MustSet().StateControllerAddress().Address + governorAddress := a.Conditions.MustSet().GovernorAddress().Address + + if (stateControllerAddress.Type() == AddressImplicitAccountCreation) || (governorAddress.Type() == AddressImplicitAccountCreation) { + return ErrImplicitAccountCreationAddressInInvalidOutput + } + + return nil +} + +func (a *AnchorOutput) WorkScore(workScoreParameters *WorkScoreParameters) (WorkScore, error) { + workScoreConditions, err := a.Conditions.WorkScore(workScoreParameters) + if err != nil { + return 0, err + } + + workScoreFeatures, err := a.Features.WorkScore(workScoreParameters) + if err != nil { + return 0, err + } + + workScoreImmutableFeatures, err := a.ImmutableFeatures.WorkScore(workScoreParameters) + if err != nil { + return 0, err + } + + return workScoreConditions.Add(workScoreFeatures, workScoreImmutableFeatures) +} + +func (a *AnchorOutput) Ident(nextState TransDepIdentOutput) (Address, error) { + // if there isn't a next state, then only the governance address can destroy the anchor + if nextState == nil { + return a.GovernorAddress(), nil + } + otherAnchorOutput, isAnchorOutput := nextState.(*AnchorOutput) + if !isAnchorOutput { + return nil, ierrors.Wrapf(ErrTransDepIdentOutputNextInvalid, "expected AnchorOutput but got %s for ident computation", nextState.Type()) + } + switch { + case a.StateIndex == otherAnchorOutput.StateIndex: + return a.GovernorAddress(), nil + case a.StateIndex+1 == otherAnchorOutput.StateIndex: + return a.StateController(), nil + default: + return nil, ierrors.Wrap(ErrTransDepIdentOutputNextInvalid, "can not compute right ident for anchor output as state index delta is invalid") + } +} + +func (a *AnchorOutput) ChainID() ChainID { + return a.AnchorID +} + +func (a *AnchorOutput) AnchorEmpty() bool { + return a.AnchorID == EmptyAnchorID +} + +func (a *AnchorOutput) FeatureSet() FeatureSet { + return a.Features.MustSet() +} + +func (a *AnchorOutput) UnlockConditionSet() UnlockConditionSet { + return a.Conditions.MustSet() +} + +func (a *AnchorOutput) ImmutableFeatureSet() FeatureSet { + return a.ImmutableFeatures.MustSet() +} + +func (a *AnchorOutput) BaseTokenAmount() BaseToken { + return a.Amount +} + +func (a *AnchorOutput) StoredMana() Mana { + return a.Mana +} + +func (a *AnchorOutput) Target() (Address, error) { + addr := new(AnchorAddress) + copy(addr[:], a.AnchorID[:]) + + return addr, nil +} + +func (a *AnchorOutput) Type() OutputType { + return OutputAnchor +} + +func (a *AnchorOutput) Size() int { + // OutputType + return serializer.OneByte + + BaseTokenSize + + ManaSize + + AnchorIDLength + + // StateIndex + serializer.UInt32ByteSize + + serializer.UInt16ByteSize + + len(a.StateMetadata) + + a.Conditions.Size() + + a.Features.Size() + + a.ImmutableFeatures.Size() +} diff --git a/output_id_proof_test.go b/output_id_proof_test.go index 1b2883e6a..f73cc66b9 100644 --- a/output_id_proof_test.go +++ b/output_id_proof_test.go @@ -34,7 +34,7 @@ func TestOutputIDProof(t *testing.T) { }, Outputs: lo.RepeatBy(1, func(_ int) iotago.TxEssenceOutput { return &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, @@ -54,7 +54,7 @@ func TestOutputIDProof(t *testing.T) { }, Outputs: lo.RepeatBy(2, func(_ int) iotago.TxEssenceOutput { return &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, @@ -74,7 +74,7 @@ func TestOutputIDProof(t *testing.T) { }, Outputs: lo.RepeatBy(3, func(_ int) iotago.TxEssenceOutput { return &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, @@ -94,7 +94,7 @@ func TestOutputIDProof(t *testing.T) { }, Outputs: lo.RepeatBy(iotago.MaxOutputsCount, func(_ int) iotago.TxEssenceOutput { return &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, diff --git a/output_test.go b/output_test.go index ed98baaa7..05ef26d1a 100644 --- a/output_test.go +++ b/output_test.go @@ -16,10 +16,12 @@ func TestOutputTypeString(t *testing.T) { outputType iotago.OutputType outputTypeString string }{ - {iotago.OutputNFT, "NFTOutput"}, {iotago.OutputBasic, "BasicOutput"}, {iotago.OutputAccount, "AccountOutput"}, + {iotago.OutputAnchor, "AnchorOutput"}, {iotago.OutputFoundry, "FoundryOutput"}, + {iotago.OutputNFT, "NFTOutput"}, + {iotago.OutputDelegation, "DelegationOutput"}, } for _, tt := range tests { require.Equal(t, tt.outputType.String(), tt.outputTypeString) @@ -62,12 +64,9 @@ func TestOutputsDeSerialize(t *testing.T) { Amount: 1337, Mana: 500, AccountID: tpkg.RandAccountAddress().AccountID(), - StateIndex: 10, - StateMetadata: []byte("hello world"), FoundryCounter: 1337, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{ &iotago.SenderFeature{Address: tpkg.RandEd25519Address()}, @@ -79,6 +78,28 @@ func TestOutputsDeSerialize(t *testing.T) { }, target: &iotago.AccountOutput{}, }, + { + name: "ok - AnchorOutput", + source: &iotago.AnchorOutput{ + Amount: 1337, + Mana: 500, + AnchorID: tpkg.RandAnchorAddress().AnchorID(), + StateIndex: 10, + StateMetadata: []byte("hello world"), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + }, + Features: iotago.AnchorOutputFeatures{ + &iotago.SenderFeature{Address: tpkg.RandEd25519Address()}, + &iotago.MetadataFeature{Data: tpkg.RandBytes(100)}, + }, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.IssuerFeature{Address: tpkg.RandEd25519Address()}, + }, + }, + target: &iotago.AnchorOutput{}, + }, { name: "ok - FoundryOutput", source: &iotago.FoundryOutput{ @@ -145,51 +166,61 @@ func TestOutputsDeSerialize(t *testing.T) { target: &iotago.DelegationOutput{}, }, { - name: "fail - Delegation Output contains Implicit Account Creation Address", - source: &iotago.DelegationOutput{ - Amount: 1337, - DelegatedAmount: 1337, - DelegationID: tpkg.Rand32ByteArray(), - ValidatorAddress: tpkg.RandAccountAddress(), - StartEpoch: iotago.EpochIndex(32), - EndEpoch: iotago.EpochIndex(37), - Conditions: iotago.DelegationOutputUnlockConditions{ + name: "fail - Account Output contains Implicit Account Creation Address", + source: &iotago.AccountOutput{ + Conditions: iotago.AccountOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, }, }, - target: &iotago.DelegationOutput{}, + target: &iotago.AccountOutput{}, seriErr: iotago.ErrImplicitAccountCreationAddressInInvalidOutput, }, { - name: "fail - NFT Output contains Implicit Account Creation Address", - source: &iotago.NFTOutput{ - Conditions: iotago.NFTOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, + name: "fail - Anchor Output contains Implicit Account Creation Address as State Controller", + source: &iotago.AnchorOutput{ + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, }, - target: &iotago.NFTOutput{}, + target: &iotago.AnchorOutput{}, seriErr: iotago.ErrImplicitAccountCreationAddressInInvalidOutput, }, { - name: "fail - Account Output contains Implicit Account Creation Address as State Controller", - source: &iotago.AccountOutput{ - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + name: "fail - Anchor Output contains Implicit Account Creation Address as Governor", + source: &iotago.AnchorOutput{ + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, }, }, - target: &iotago.AccountOutput{}, + target: &iotago.AnchorOutput{}, seriErr: iotago.ErrImplicitAccountCreationAddressInInvalidOutput, }, { - name: "fail - Account Output contains Implicit Account Creation Address as Governor", - source: &iotago.AccountOutput{ - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, + name: "fail - NFT Output contains Implicit Account Creation Address", + source: &iotago.NFTOutput{ + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, }, }, - target: &iotago.AccountOutput{}, + target: &iotago.NFTOutput{}, + seriErr: iotago.ErrImplicitAccountCreationAddressInInvalidOutput, + }, + { + name: "fail - Delegation Output contains Implicit Account Creation Address", + source: &iotago.DelegationOutput{ + Amount: 1337, + DelegatedAmount: 1337, + DelegationID: tpkg.Rand32ByteArray(), + ValidatorAddress: tpkg.RandAccountAddress(), + StartEpoch: iotago.EpochIndex(32), + EndEpoch: iotago.EpochIndex(37), + Conditions: iotago.DelegationOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: tpkg.RandImplicitAccountCreationAddress()}, + }, + }, + target: &iotago.DelegationOutput{}, seriErr: iotago.ErrImplicitAccountCreationAddressInInvalidOutput, }, { @@ -286,13 +317,13 @@ func TestOutputsSyntacticalDepositAmount(t *testing.T) { protoParams: nonZeroCostParams, outputs: iotago.Outputs[iotago.Output]{ &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, &iotago.StorageDepositReturnUnlockCondition{ ReturnAddress: tpkg.RandAccountAddress(), // off by one from the deposit - Amount: OneMi + 1, + Amount: OneIOTA + 1, }, }, Mana: 500, @@ -525,13 +556,11 @@ func TestOutputsSyntacticalAccount(t *testing.T) { name: "ok - empty state", outputs: iotago.Outputs[iotago.Output]{ &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: iotago.AccountID{}, - StateIndex: 0, FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, }, }, }, @@ -541,92 +570,156 @@ func TestOutputsSyntacticalAccount(t *testing.T) { name: "ok - non empty state", outputs: iotago.Outputs[iotago.Output]{ &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: tpkg.Rand32ByteArray(), - StateIndex: 10, FoundryCounter: 1337, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, }, }, }, wantErr: nil, }, - { - name: "fail - state index non zero on empty account ID", - outputs: iotago.Outputs[iotago.Output]{ - &iotago.AccountOutput{ - Amount: OneMi, - AccountID: iotago.AccountID{}, - StateIndex: 1, - FoundryCounter: 0, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, - }, - }, - }, - wantErr: iotago.ErrAccountOutputNonEmptyState, - }, { name: "fail - foundry counter non zero on empty account ID", outputs: iotago.Outputs[iotago.Output]{ &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: iotago.AccountID{}, - StateIndex: 0, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandAccountAddress()}, }, }, }, wantErr: iotago.ErrAccountOutputNonEmptyState, }, { - name: "fail - cyclic state controller", + name: "fail - cyclic", outputs: iotago.Outputs[iotago.Output]{ func() *iotago.AccountOutput { accountID := iotago.AccountID(tpkg.Rand32ByteArray()) return &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: accountID, - StateIndex: 10, FoundryCounter: 1337, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: accountID.ToAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, + &iotago.AddressUnlockCondition{Address: accountID.ToAddress()}, }, } }(), }, wantErr: iotago.ErrAccountOutputCyclicAddress, }, + } + valFunc := iotago.OutputsSyntacticalAccount() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + var runErr error + for index, output := range tt.outputs { + if err := valFunc(index, output); err != nil { + runErr = err + } + } + require.ErrorIs(t, runErr, tt.wantErr) + }) + }) + } +} + +func TestOutputsSyntacticalAnchor(t *testing.T) { + tests := []struct { + name string + outputs iotago.Outputs[iotago.Output] + wantErr error + }{ + { + name: "ok - empty state", + outputs: iotago.Outputs[iotago.Output]{ + &iotago.AnchorOutput{ + Amount: OneIOTA, + AnchorID: iotago.AnchorID{}, + StateIndex: 0, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + }, + }, + }, + wantErr: nil, + }, + { + name: "ok - non empty state", + outputs: iotago.Outputs[iotago.Output]{ + &iotago.AnchorOutput{ + Amount: OneIOTA, + AnchorID: tpkg.Rand32ByteArray(), + StateIndex: 10, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + }, + }, + }, + wantErr: nil, + }, + { + name: "fail - state index non zero on empty anchor ID", + outputs: iotago.Outputs[iotago.Output]{ + &iotago.AnchorOutput{ + Amount: OneIOTA, + AnchorID: iotago.AnchorID{}, + StateIndex: 1, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + }, + }, + }, + wantErr: iotago.ErrAnchorOutputNonEmptyState, + }, + { + name: "fail - cyclic state controller", + outputs: iotago.Outputs[iotago.Output]{ + func() *iotago.AnchorOutput { + anchorID := iotago.AnchorID(tpkg.Rand32ByteArray()) + + return &iotago.AnchorOutput{ + Amount: OneIOTA, + AnchorID: anchorID, + StateIndex: 10, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: anchorID.ToAddress()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + }, + } + }(), + }, + wantErr: iotago.ErrAnchorOutputCyclicAddress, + }, { name: "fail - cyclic governance controller", outputs: iotago.Outputs[iotago.Output]{ - func() *iotago.AccountOutput { - accountID := iotago.AccountID(tpkg.Rand32ByteArray()) + func() *iotago.AnchorOutput { + anchorID := iotago.AnchorID(tpkg.Rand32ByteArray()) - return &iotago.AccountOutput{ - Amount: OneMi, - AccountID: accountID, - StateIndex: 10, - FoundryCounter: 1337, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAccountAddress()}, - &iotago.GovernorAddressUnlockCondition{Address: accountID.ToAddress()}, + return &iotago.AnchorOutput{ + Amount: OneIOTA, + AnchorID: anchorID, + StateIndex: 10, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandAnchorAddress()}, + &iotago.GovernorAddressUnlockCondition{Address: anchorID.ToAddress()}, }, } }(), }, - wantErr: iotago.ErrAccountOutputCyclicAddress, + wantErr: iotago.ErrAnchorOutputCyclicAddress, }, } - valFunc := iotago.OutputsSyntacticalAccount() + valFunc := iotago.OutputsSyntacticalAnchor() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) { @@ -770,7 +863,7 @@ func TestOutputsSyntacticalNFT(t *testing.T) { name: "ok", outputs: iotago.Outputs[iotago.Output]{ &iotago.NFTOutput{ - Amount: OneMi, + Amount: OneIOTA, NFTID: iotago.NFTID{}, Conditions: iotago.NFTOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, @@ -785,7 +878,7 @@ func TestOutputsSyntacticalNFT(t *testing.T) { nftID := iotago.NFTID(tpkg.Rand32ByteArray()) return &iotago.NFTOutput{ - Amount: OneMi, + Amount: OneIOTA, NFTID: nftID, Conditions: iotago.NFTOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: nftID.ToAddress()}, @@ -824,8 +917,8 @@ func TestOutputsSyntacticaDelegation(t *testing.T) { name: "ok", outputs: iotago.Outputs[iotago.Output]{ &iotago.DelegationOutput{ - Amount: OneMi, - DelegatedAmount: OneMi, + Amount: OneIOTA, + DelegatedAmount: OneIOTA, DelegationID: iotago.EmptyDelegationID(), ValidatorAddress: tpkg.RandAccountAddress(), Conditions: iotago.DelegationOutputUnlockConditions{ @@ -838,7 +931,7 @@ func TestOutputsSyntacticaDelegation(t *testing.T) { name: "fail - validator id zeroed", outputs: iotago.Outputs[iotago.Output]{ &iotago.DelegationOutput{ - Amount: OneMi, + Amount: OneIOTA, DelegationID: iotago.EmptyDelegationID(), ValidatorAddress: &emptyAccountAddress, Conditions: iotago.DelegationOutputUnlockConditions{ @@ -881,7 +974,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "can unlock - target is source (no unlock conditions)", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: receiverIdent}, }, @@ -897,7 +990,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "can not unlock - target is not source (no timelocks or expiration unlock conditions)", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, @@ -914,7 +1007,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "expiration - receiver ident can unlock", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: receiverIdent}, &iotago.ExpirationUnlockCondition{ @@ -936,7 +1029,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "expiration - receiver ident can not unlock", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: receiverIdent}, &iotago.ExpirationUnlockCondition{ @@ -958,7 +1051,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "expiration - return ident can unlock", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: receiverIdent}, &iotago.ExpirationUnlockCondition{ @@ -980,7 +1073,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "expiration - return ident can not unlock", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: receiverIdent}, &iotago.ExpirationUnlockCondition{ @@ -1001,7 +1094,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "timelock - expired timelock is unlockable", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: receiverIdent}, &iotago.TimelockUnlockCondition{Slot: 15}, @@ -1019,7 +1112,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { return &test{ name: "timelock - non-expired timelock is not unlockable", output: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: receiverIdent}, &iotago.TimelockUnlockCondition{Slot: 16}, @@ -1043,7 +1136,7 @@ func TestTransIndepIdentOutput_UnlockableBy(t *testing.T) { } } -func TestAccountOutput_UnlockableBy(t *testing.T) { +func TestAnchorOutput_UnlockableBy(t *testing.T) { type test struct { name string current iotago.TransDepIdentOutput @@ -1063,18 +1156,18 @@ func TestAccountOutput_UnlockableBy(t *testing.T) { return &test{ name: "state ctrl can unlock - state index increase", - current: &iotago.AccountOutput{ - Amount: OneMi, + current: &iotago.AnchorOutput{ + Amount: OneIOTA, StateIndex: 0, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: govCtrl}, }, }, - next: &iotago.AccountOutput{ - Amount: OneMi, + next: &iotago.AnchorOutput{ + Amount: OneIOTA, StateIndex: 1, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: govCtrl}, }, @@ -1092,18 +1185,18 @@ func TestAccountOutput_UnlockableBy(t *testing.T) { return &test{ name: "state ctrl can not unlock - state index same", - current: &iotago.AccountOutput{ - Amount: OneMi, + current: &iotago.AnchorOutput{ + Amount: OneIOTA, StateIndex: 0, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: govCtrl}, }, }, - next: &iotago.AccountOutput{ - Amount: OneMi, + next: &iotago.AnchorOutput{ + Amount: OneIOTA, StateIndex: 0, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: govCtrl}, }, @@ -1122,10 +1215,10 @@ func TestAccountOutput_UnlockableBy(t *testing.T) { return &test{ name: "state ctrl can not unlock - transition destroy", - current: &iotago.AccountOutput{ - Amount: OneMi, + current: &iotago.AnchorOutput{ + Amount: OneIOTA, StateIndex: 0, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: govCtrl}, }, diff --git a/storagescore.go b/storagescore.go index cdf9338a8..30f2463f3 100644 --- a/storagescore.go +++ b/storagescore.go @@ -91,14 +91,9 @@ func NewStorageScoreStructure(storageScoreParameters *StorageScoreParameters) *S Amount: 0, Mana: 0, AccountID: EmptyAccountID, - StateIndex: 0, - StateMetadata: []byte{}, FoundryCounter: 0, Conditions: AccountOutputUnlockConditions{ - &GovernorAddressUnlockCondition{ - Address: &Ed25519Address{}, - }, - &StateControllerAddressUnlockCondition{ + &AddressUnlockCondition{ Address: &Ed25519Address{}, }, }, diff --git a/transaction.go b/transaction.go index e3612ccfb..af3b1db34 100644 --- a/transaction.go +++ b/transaction.go @@ -34,8 +34,12 @@ var ( ErrMultipleInputCommitments = ierrors.New("there are multiple commitment inputs") // ErrAccountOutputNonEmptyState gets returned if an AccountOutput with zeroed AccountID contains state (counters non-zero etc.). ErrAccountOutputNonEmptyState = ierrors.New("account output is not empty state") - // ErrAccountOutputCyclicAddress gets returned if an AccountOutput's AccountID results into the same address as the State/Governance controller. - ErrAccountOutputCyclicAddress = ierrors.New("account output's AccountID corresponds to state and/or governance controller") + // ErrAccountOutputCyclicAddress gets returned if an AccountOutput's AccountID results into the same address as the address field within the output. + ErrAccountOutputCyclicAddress = ierrors.New("account output's ID corresponds to address field") + // ErrAnchorOutputNonEmptyState gets returned if an AnchorOutput with zeroed AnchorID contains state (counters non-zero etc.). + ErrAnchorOutputNonEmptyState = ierrors.New("anchor output is not empty state") + // ErrAnchorOutputCyclicAddress gets returned if an AnchorOutput's AnchorID results into the same address as the State/Governance controller. + ErrAnchorOutputCyclicAddress = ierrors.New("anchor output's AnchorID corresponds to state and/or governance controller") // ErrNFTOutputCyclicAddress gets returned if an NFTOutput's NFTID results into the same address as the address field within the output. ErrNFTOutputCyclicAddress = ierrors.New("NFT output's ID corresponds to address field") // ErrDelegationValidatorAddressZeroed gets returned if a Delegation Output's Validator address is zeroed out. @@ -259,6 +263,7 @@ func (t *Transaction) syntacticallyValidate(api API) error { OutputsSyntacticalChainConstrainedOutputUniqueness(), OutputsSyntacticalFoundry(), OutputsSyntacticalAccount(), + OutputsSyntacticalAnchor(), OutputsSyntacticalNFT(), OutputsSyntacticalDelegation(), ) diff --git a/transaction_test.go b/transaction_test.go index 5d6f70010..9a0e5afb1 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -31,6 +31,9 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { accountAddress := iotago.AccountAddressFromOutputID(inputIDs[0]) accountID := accountAddress.AccountID() + anchorAddress := iotago.AnchorAddressFromOutputID(inputIDs[0]) + anchorID := anchorAddress.AnchorID() + nftAddress := iotago.NFTAddressFromOutputID(inputIDs[0]) nftID := nftAddress.NFTID() @@ -50,18 +53,54 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: accountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, Features: nil, }, &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: accountID, Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + Features: nil, + }, + }, + }), + target: &iotago.SignedTransaction{}, + seriErr: iotago.ErrNonUniqueChainOutputs, + deSeriErr: nil, + }, + { + // we transition the same Anchor twice + name: "transition the same Anchor twice", + source: tpkg.RandSignedTransactionWithTransaction(tpkg.TestAPI, + &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + NetworkID: tpkg.TestNetworkID, + ContextInputs: iotago.TxEssenceContextInputs{}, + Inputs: inputIDs.UTXOInputs(), + Allotments: iotago.Allotments{}, + Capabilities: iotago.TransactionCapabilitiesBitMask{}, + }, + Outputs: iotago.TxEssenceOutputs{ + &iotago.AnchorOutput{ + Amount: OneIOTA, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + Features: nil, + }, + &iotago.AnchorOutput{ + Amount: OneIOTA, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident1}, &iotago.GovernorAddressUnlockCondition{Address: ident1}, }, @@ -86,7 +125,7 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.NFTOutput{ - Amount: OneMi, + Amount: OneIOTA, NFTID: nftID, Conditions: iotago.NFTOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, @@ -94,7 +133,7 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { Features: nil, }, &iotago.NFTOutput{ - Amount: OneMi, + Amount: OneIOTA, NFTID: nftID, Conditions: iotago.NFTOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, @@ -120,16 +159,15 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: accountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, Features: nil, }, &iotago.FoundryOutput{ - Amount: OneMi, + Amount: OneIOTA, SerialNumber: 1, TokenScheme: &iotago.SimpleTokenScheme{ MintedTokens: big.NewInt(50), @@ -142,7 +180,7 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { Features: nil, }, &iotago.FoundryOutput{ - Amount: OneMi, + Amount: OneIOTA, SerialNumber: 1, TokenScheme: &iotago.SimpleTokenScheme{ MintedTokens: big.NewInt(50), diff --git a/vm/nova/stvf_test.go b/vm/nova/stvf_test.go index 3d14656f0..0fdafa07d 100644 --- a/vm/nova/stvf_test.go +++ b/vm/nova/stvf_test.go @@ -38,8 +38,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { exampleIssuer := tpkg.RandEd25519Address() exampleAccountID := tpkg.RandAccountAddress().AccountID() - exampleStateCtrl := tpkg.RandEd25519Address() - exampleGovCtrl := tpkg.RandEd25519Address() + exampleAddress := tpkg.RandEd25519Address() exampleExistingFoundryOutput := &iotago.FoundryOutput{ Amount: 100, @@ -65,7 +64,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { } exampleBIC := map[iotago.AccountID]iotago.BlockIssuanceCredits{ - exampleAccountID: 100, + exampleAccountID: 1000, } type test struct { @@ -85,8 +84,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, ImmutableFeatures: iotago.AccountOutputImmFeatures{ &iotago.IssuerFeature{Address: exampleIssuer}, @@ -115,8 +113,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, ImmutableFeatures: iotago.AccountOutputImmFeatures{ &iotago.IssuerFeature{Address: exampleIssuer}, @@ -156,8 +153,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, ImmutableFeatures: iotago.AccountOutputImmFeatures{ &iotago.IssuerFeature{Address: exampleIssuer}, @@ -197,8 +193,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, ImmutableFeatures: iotago.AccountOutputImmFeatures{ &iotago.IssuerFeature{Address: exampleIssuer}, @@ -238,8 +233,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -280,8 +274,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -322,8 +315,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -364,8 +356,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -406,8 +397,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -446,12 +436,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 50, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -465,12 +453,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 51, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -508,12 +494,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 50, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ exampleBlockIssuerFeature, @@ -521,12 +505,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 51, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -562,12 +544,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 50, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ exampleBlockIssuerFeature, @@ -575,12 +555,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 51, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -617,12 +595,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 50, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ exampleBlockIssuerFeature, @@ -630,14 +606,11 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 51, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, - StateMetadata: []byte("1337"), Features: iotago.AccountOutputFeatures{ exampleBlockIssuerFeature, }, @@ -671,12 +644,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 50, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -690,12 +661,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 51, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ exampleBlockIssuerFeature, @@ -728,12 +697,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 50, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -747,12 +714,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 51, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -791,12 +756,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 50, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -810,12 +773,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 51, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -854,12 +815,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(1000, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -876,12 +835,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -914,127 +871,15 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, wantErr: iotago.ErrInvalidStakingBlockIssuerRequired, }, - { - name: "fail - account removes staking feature in governance transition", - input: &vm.ChainOutputWithIDs{ - OutputID: tpkg.RandOutputIDWithCreationSlot(1000, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, - Features: iotago.AccountOutputFeatures{ - &iotago.StakingFeature{ - StakedAmount: 50, - FixedCost: 5, - StartEpoch: currentEpoch, - EndEpoch: iotago.MaxEpochIndex, - }, - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 990, - }, - }, - }, - }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, - Features: iotago.AccountOutputFeatures{}, - }, - transType: iotago.ChainTransitionTypeStateChange, - svCtx: &vm.Params{ - API: tpkg.TestAPI, - WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: currentSlot, - }, - UnlockedIdents: vm.UnlockedIdentities{ - exampleIssuer.Key(): {UnlockedAt: 0}, - }, - Tx: &iotago.Transaction{ - API: tpkg.TestAPI, - TransactionEssence: &iotago.TransactionEssence{ - CreationSlot: currentSlot, - Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), - }, - }, - BIC: exampleBIC, - }, - }, - wantErr: iotago.ErrInvalidAccountGovernanceTransition, - }, - { - name: "fail - account removes block issuer feature in staking transition", - input: &vm.ChainOutputWithIDs{ - OutputID: tpkg.RandOutputIDWithCreationSlot(1000, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, - Features: iotago.AccountOutputFeatures{ - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 990, - }, - }, - }, - }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 2, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, - Features: iotago.AccountOutputFeatures{}, - }, - transType: iotago.ChainTransitionTypeStateChange, - svCtx: &vm.Params{ - API: tpkg.TestAPI, - WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: currentSlot, - }, - UnlockedIdents: vm.UnlockedIdentities{ - exampleIssuer.Key(): {UnlockedAt: 0}, - }, - Tx: &iotago.Transaction{ - API: tpkg.TestAPI, - TransactionEssence: &iotago.TransactionEssence{ - CreationSlot: currentSlot, - Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), - }, - }, - BIC: exampleBIC, - }, - }, - wantErr: iotago.ErrInvalidAccountStateTransition, - }, { name: "fail - expired staking feature removed without specifying reward input", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(1000, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -1048,12 +893,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 2, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ exampleBlockIssuerFeature, @@ -1086,12 +929,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(1000, 0), Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -1105,12 +946,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 2, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -1150,12 +989,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { OutputID: tpkg.RandOutputIDWithCreationSlot(1000, 0), ChainID: exampleAccountID, Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 1, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -1169,12 +1006,10 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 2, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -1219,8 +1054,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: tpkg.RandAccountAddress().AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, }, }, @@ -1248,8 +1082,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1290,8 +1123,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1321,7 +1153,6 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, wantErr: iotago.ErrBlockIssuanceCreditInputRequired, }, - { name: "fail - non-expired block issuer destroy transition", input: &vm.ChainOutputWithIDs{ @@ -1330,8 +1161,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: tpkg.RandAccountAddress().AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1369,8 +1199,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1412,8 +1241,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1421,16 +1249,13 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { ExpirySlot: 1000, }, }, - StateIndex: 10, }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 10, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{}, }, @@ -1465,8 +1290,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1474,16 +1298,13 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { ExpirySlot: 1000, }, }, - StateIndex: 10, }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 10, + Amount: 100, + AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{}, }, @@ -1511,30 +1332,28 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { wantErr: nil, }, { - name: "ok - gov transition", + name: "ok - state transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), Output: &iotago.AccountOutput{ Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, - StateIndex: 10, + FoundryCounter: 5, }, }, next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 10, - // mutating controllers + Amount: 200, + AccountID: exampleAccountID, + // mutating owner Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, + FoundryCounter: 7, Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: exampleGovCtrl}, + &iotago.SenderFeature{Address: exampleAddress}, &iotago.MetadataFeature{Data: []byte("1337")}, &iotago.BlockIssuerFeature{ BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), @@ -1547,59 +1366,12 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ UnlockedIdents: vm.UnlockedIdentities{ - exampleGovCtrl.Key(): {UnlockedAt: 0}, + exampleAddress.Key(): {UnlockedAt: 0}, }, Commitment: &iotago.Commitment{ Slot: 990, }, BIC: exampleBIC, - Tx: &iotago.Transaction{ - API: tpkg.TestAPI, - TransactionEssence: &iotago.TransactionEssence{ - CreationSlot: 900, - Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), - }, - }, - }, - }, - wantErr: nil, - }, - { - name: "ok - state transition", - input: &vm.ChainOutputWithIDs{ - OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, - StateIndex: 10, - FoundryCounter: 5, - }, - }, - next: &iotago.AccountOutput{ - Amount: 200, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, - StateIndex: 11, - StateMetadata: []byte("1337"), - FoundryCounter: 7, - Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: exampleStateCtrl}, - }, - }, - transType: iotago.ChainTransitionTypeStateChange, - svCtx: &vm.Params{ - API: tpkg.TestAPI, - WorkingSet: &vm.WorkingSet{ - UnlockedIdents: vm.UnlockedIdentities{ - exampleStateCtrl.Key(): {UnlockedAt: 0}, - }, InChains: map[iotago.ChainID]*vm.ChainOutputWithIDs{ // serial number 5 exampleExistingFoundryOutputFoundryID: { @@ -1611,6 +1383,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Tx: &iotago.Transaction{ API: tpkg.TestAPI, TransactionEssence: &iotago.TransactionEssence{ + CreationSlot: 900, Inputs: nil, Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything())}, Outputs: iotago.TxEssenceOutputs{ @@ -1644,8 +1417,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1653,19 +1425,16 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { ExpirySlot: 1000, }, }, - StateIndex: 10, }, }, next: &iotago.AccountOutput{ Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, - StateIndex: 10, Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: exampleStateCtrl}, + &iotago.SenderFeature{Address: exampleAddress}, &iotago.BlockIssuerFeature{ BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), ExpirySlot: 1000, @@ -1680,7 +1449,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Slot: 990, }, UnlockedIdents: vm.UnlockedIdentities{ - exampleStateCtrl.Key(): {UnlockedAt: 0}, + exampleAddress.Key(): {UnlockedAt: 0}, }, BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ exampleAccountID: 10, @@ -1705,8 +1474,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, ImmutableFeatures: iotago.AccountOutputImmFeatures{ &iotago.BlockIssuerFeature{ @@ -1714,18 +1482,14 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { ExpirySlot: 900, }, }, - StateIndex: 10, }, }, next: &iotago.AccountOutput{ Amount: 200, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, - StateIndex: 11, - StateMetadata: []byte("1337"), ImmutableFeatures: iotago.AccountOutputImmFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), @@ -1738,7 +1502,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ UnlockedIdents: vm.UnlockedIdentities{ - exampleStateCtrl.Key(): {UnlockedAt: 0}, + exampleAddress.Key(): {UnlockedAt: 0}, }, Tx: &iotago.Transaction{ TransactionEssence: &iotago.TransactionEssence{ @@ -1757,8 +1521,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ @@ -1766,388 +1529,716 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { ExpirySlot: 900, }, }, - StateIndex: 10, }, }, next: &iotago.AccountOutput{ Amount: 100, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, + &iotago.AddressUnlockCondition{Address: exampleAddress}, }, - StateIndex: 10, Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: exampleStateCtrl}, + &iotago.SenderFeature{Address: exampleAddress}, + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 999, + }, + }, + }, + transType: iotago.ChainTransitionTypeStateChange, + svCtx: &vm.Params{ + API: tpkg.TestAPI, + WorkingSet: &vm.WorkingSet{ + Commitment: &iotago.Commitment{ + Slot: 990, + }, + UnlockedIdents: vm.UnlockedIdentities{ + exampleAddress.Key(): {UnlockedAt: 0}, + }, + BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ + exampleAccountID: 10, + }, + Tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + Inputs: nil, + CreationSlot: 990, + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), + }, + }, + }, + }, + wantErr: iotago.ErrInvalidBlockIssuerTransition, + }, + { + name: "fail - update expired block issuer feature with extending expiration to the past before MCA", + input: &vm.ChainOutputWithIDs{ + OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), + Output: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1100, + }, + }, + }, + }, + next: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.SenderFeature{Address: exampleAddress}, &iotago.BlockIssuerFeature{ BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), ExpirySlot: 999, }, }, }, - transType: iotago.ChainTransitionTypeStateChange, + transType: iotago.ChainTransitionTypeStateChange, + svCtx: &vm.Params{ + API: tpkg.TestAPI, + WorkingSet: &vm.WorkingSet{ + Commitment: &iotago.Commitment{ + Slot: 990, + }, + UnlockedIdents: vm.UnlockedIdentities{ + exampleAddress.Key(): {UnlockedAt: 0}, + }, + BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ + exampleAccountID: 10, + }, + Tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + Inputs: nil, + CreationSlot: 990, + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), + }, + }, + }, + }, + wantErr: iotago.ErrInvalidBlockIssuerTransition, + }, + { + name: "fail - update block issuer account with negative BIC", + input: &vm.ChainOutputWithIDs{ + OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), + Output: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1000, + }, + }, + }, + }, + next: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.MetadataFeature{Data: []byte("1337")}, + &iotago.SenderFeature{Address: exampleAddress}, + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1000, + }, + }, + }, + transType: iotago.ChainTransitionTypeStateChange, + svCtx: &vm.Params{ + API: tpkg.TestAPI, + WorkingSet: &vm.WorkingSet{ + Commitment: &iotago.Commitment{ + Slot: 900, + }, + UnlockedIdents: vm.UnlockedIdentities{ + exampleAddress.Key(): {UnlockedAt: 0}, + }, + BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ + exampleAccountID: -1, + }, + Tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + Inputs: nil, + CreationSlot: 900, + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), + }, + }, + }, + }, + wantErr: iotago.ErrAccountLocked, + }, + { + name: "fail - update block issuer account without BIC provided", + input: &vm.ChainOutputWithIDs{ + OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), + Output: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1000, + }, + }, + }, + }, + next: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.SenderFeature{Address: exampleAddress}, + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1000, + }, + }, + }, + transType: iotago.ChainTransitionTypeStateChange, + svCtx: &vm.Params{ + API: tpkg.TestAPI, + WorkingSet: &vm.WorkingSet{ + Commitment: &iotago.Commitment{ + Slot: 900, + }, + UnlockedIdents: vm.UnlockedIdentities{ + exampleAddress.Key(): {UnlockedAt: 0}, + }, + + Tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + CreationSlot: 900, + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), + }, + }, + }, + }, + wantErr: iotago.ErrBlockIssuanceCreditInputRequired, + }, + { + name: "ok - update block issuer feature expiration to earlier slot", + input: &vm.ChainOutputWithIDs{ + OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), + Output: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1000, + }, + }, + }, + }, + next: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.MetadataFeature{Data: []byte("1337")}, + &iotago.SenderFeature{Address: exampleAddress}, + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 999, + }, + }, + }, + transType: iotago.ChainTransitionTypeStateChange, + svCtx: &vm.Params{ + API: tpkg.TestAPI, + WorkingSet: &vm.WorkingSet{ + Commitment: &iotago.Commitment{ + Slot: 900, + }, + UnlockedIdents: vm.UnlockedIdentities{ + exampleAddress.Key(): {UnlockedAt: 0}, + }, + BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ + exampleAccountID: 10, + }, + Tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + CreationSlot: 900, + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "ok - non-expired block issuer replace key", + input: &vm.ChainOutputWithIDs{ + OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), + Output: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1000, + }, + }, + FoundryCounter: 5, + }, + }, + next: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + FoundryCounter: 5, + Features: iotago.AccountOutputFeatures{ + &iotago.SenderFeature{Address: exampleAddress}, + &iotago.BlockIssuerFeature{ + BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), + ExpirySlot: 1000, + }, + }, + }, + transType: iotago.ChainTransitionTypeStateChange, + svCtx: &vm.Params{ + API: tpkg.TestAPI, + WorkingSet: &vm.WorkingSet{ + Commitment: &iotago.Commitment{ + Slot: 0, + }, + UnlockedIdents: vm.UnlockedIdentities{ + exampleAddress.Key(): {UnlockedAt: 0}, + }, + InChains: map[iotago.ChainID]*vm.ChainOutputWithIDs{ + // serial number 5 + exampleExistingFoundryOutputFoundryID: { + ChainID: exampleExistingFoundryOutputFoundryID, + OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), + Output: exampleExistingFoundryOutput, + }, + }, + BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ + exampleAccountID: 10, + }, + Tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + Inputs: nil, + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything())}, + Outputs: iotago.TxEssenceOutputs{ + &iotago.FoundryOutput{ + Amount: 100, + SerialNumber: 6, + TokenScheme: &iotago.SimpleTokenScheme{}, + Conditions: iotago.FoundryOutputUnlockConditions{ + &iotago.ImmutableAccountUnlockCondition{Address: exampleAccountID.ToAddress().(*iotago.AccountAddress)}, + }, + }, + &iotago.FoundryOutput{ + Amount: 100, + SerialNumber: 7, + TokenScheme: &iotago.SimpleTokenScheme{}, + Conditions: iotago.FoundryOutputUnlockConditions{ + &iotago.ImmutableAccountUnlockCondition{Address: exampleAccountID.ToAddress().(*iotago.AccountAddress)}, + }, + }, + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "fail - state transition", + input: &vm.ChainOutputWithIDs{ + OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), + Output: &iotago.AccountOutput{ + Amount: 100, + AccountID: exampleAccountID, + FoundryCounter: 5, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: exampleAddress}, + }, + ImmutableFeatures: iotago.AccountOutputImmFeatures{ + &iotago.IssuerFeature{Address: exampleIssuer}, + }, + }, + }, + nextMut: map[string]fieldMutations{ + "foundry_counter_lower_than_current": { + "FoundryCounter": uint32(4), + }, + "foundries_not_created": { + "FoundryCounter": uint32(7), + }, + }, + transType: iotago.ChainTransitionTypeStateChange, + svCtx: &vm.Params{ + API: tpkg.TestAPI, + WorkingSet: &vm.WorkingSet{ + UnlockedIdents: vm.UnlockedIdentities{}, + InChains: vm.ChainInputSet{}, + Tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), + }, + }, + }, + }, + wantErr: iotago.ErrInvalidAccountStateTransition, + }, + } + + for _, tt2 := range tests { + tt := tt2 + + if tt.nextMut != nil { + for mutName, muts := range tt.nextMut { + t.Run(fmt.Sprintf("%s_%s", tt.name, mutName), func(t *testing.T) { + cpy := copyObject(t, tt.input.Output, muts).(*iotago.AccountOutput) + + if tt.input != nil { + // create the working set for the test + if tt.svCtx.WorkingSet.UTXOInputsSet == nil { + tt.svCtx.WorkingSet.UTXOInputsSet = make(vm.InputSet) + } + + tt.svCtx.WorkingSet.UTXOInputsSet[tt.input.OutputID] = tt.input.Output + } + + err := novaVM.ChainSTVF(tt.svCtx, tt.transType, tt.input, cpy) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } + continue + } + + t.Run(tt.name, func(t *testing.T) { + if tt.input != nil { + // create the working set for the test + if tt.svCtx.WorkingSet.UTXOInputsSet == nil { + tt.svCtx.WorkingSet.UTXOInputsSet = make(vm.InputSet) + } + + tt.svCtx.WorkingSet.UTXOInputsSet[tt.input.OutputID] = tt.input.Output + } + + err := novaVM.ChainSTVF(tt.svCtx, tt.transType, tt.input, tt.next) + if tt.wantErr != nil { + require.ErrorIs(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestAnchorOutput_ValidateStateTransition(t *testing.T) { + exampleIssuer := tpkg.RandEd25519Address() + exampleAnchorID := tpkg.RandAnchorAddress().AnchorID() + + exampleStateCtrl := tpkg.RandEd25519Address() + exampleGovCtrl := tpkg.RandEd25519Address() + + type test struct { + name string + input *vm.ChainOutputWithIDs + next *iotago.AnchorOutput + nextMut map[string]fieldMutations + transType iotago.ChainTransitionType + svCtx *vm.Params + wantErr error + } + + tests := []*test{ + { + name: "ok - genesis transition", + next: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: iotago.AnchorID{}, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + }, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.IssuerFeature{Address: exampleIssuer}, + }, + }, + input: nil, + transType: iotago.ChainTransitionTypeGenesis, svCtx: &vm.Params{ API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: 990, - }, UnlockedIdents: vm.UnlockedIdentities{ - exampleStateCtrl.Key(): {UnlockedAt: 0}, - }, - BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ - exampleAccountID: 10, + exampleIssuer.Key(): {UnlockedAt: 0}, }, Tx: &iotago.Transaction{ - API: tpkg.TestAPI, TransactionEssence: &iotago.TransactionEssence{ - Inputs: nil, - CreationSlot: 990, Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), }, }, }, }, - wantErr: iotago.ErrInvalidBlockIssuerTransition, + wantErr: nil, }, { - name: "fail - update expired block issuer feature with extending expiration to the past before MCA", + name: "ok - destroy transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + Output: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: tpkg.RandAnchorAddress().AnchorID(), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - Features: iotago.AccountOutputFeatures{ - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1100, - }, - }, - StateIndex: 10, - }, - }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, - StateIndex: 10, - Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: exampleStateCtrl}, - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 999, - }, }, }, - transType: iotago.ChainTransitionTypeStateChange, + next: nil, + transType: iotago.ChainTransitionTypeDestroy, svCtx: &vm.Params{ API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: 990, - }, - UnlockedIdents: vm.UnlockedIdentities{ - exampleStateCtrl.Key(): {UnlockedAt: 0}, - }, - BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ - exampleAccountID: 10, - }, + UnlockedIdents: vm.UnlockedIdentities{}, Tx: &iotago.Transaction{ API: tpkg.TestAPI, TransactionEssence: &iotago.TransactionEssence{ - Inputs: nil, - CreationSlot: 990, Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), }, }, }, }, - wantErr: iotago.ErrInvalidBlockIssuerTransition, + wantErr: nil, }, { - name: "fail - update block issuer account with negative BIC", + name: "ok - gov transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + Output: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: exampleAnchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - Features: iotago.AccountOutputFeatures{ - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1000, - }, - }, StateIndex: 10, }, }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, - &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, - }, + next: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: exampleAnchorID, StateIndex: 10, - Features: iotago.AccountOutputFeatures{ + // mutating controllers + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + }, + Features: iotago.AnchorOutputFeatures{ + &iotago.SenderFeature{Address: exampleGovCtrl}, &iotago.MetadataFeature{Data: []byte("1337")}, - &iotago.SenderFeature{Address: exampleStateCtrl}, - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1000, - }, }, }, transType: iotago.ChainTransitionTypeStateChange, svCtx: &vm.Params{ API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: 900, - }, UnlockedIdents: vm.UnlockedIdentities{ - exampleStateCtrl.Key(): {UnlockedAt: 0}, + exampleGovCtrl.Key(): {UnlockedAt: 0}, }, - BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ - exampleAccountID: -1, + Commitment: &iotago.Commitment{ + Slot: 990, }, Tx: &iotago.Transaction{ API: tpkg.TestAPI, TransactionEssence: &iotago.TransactionEssence{ - Inputs: nil, CreationSlot: 900, Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), }, }, }, }, - wantErr: iotago.ErrAccountLocked, + wantErr: nil, }, { - name: "fail - update block issuer account without BIC provided", + name: "ok - state transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + Output: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: exampleAnchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - Features: iotago.AccountOutputFeatures{ - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1000, - }, - }, StateIndex: 10, }, }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + next: &iotago.AnchorOutput{ + Amount: 200, + AnchorID: exampleAnchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - StateIndex: 10, - Features: iotago.AccountOutputFeatures{ + StateIndex: 11, + StateMetadata: []byte("1337"), + Features: iotago.AnchorOutputFeatures{ &iotago.SenderFeature{Address: exampleStateCtrl}, - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1000, - }, }, }, transType: iotago.ChainTransitionTypeStateChange, svCtx: &vm.Params{ API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: 900, - }, UnlockedIdents: vm.UnlockedIdentities{ exampleStateCtrl.Key(): {UnlockedAt: 0}, }, - Tx: &iotago.Transaction{ API: tpkg.TestAPI, TransactionEssence: &iotago.TransactionEssence{ - CreationSlot: 900, + Inputs: nil, Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), }, }, }, }, - wantErr: iotago.ErrBlockIssuanceCreditInputRequired, + wantErr: nil, }, { - name: "ok - update block issuer feature expiration to earlier slot", + name: "fail - update anchor immutable features in gov transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + Output: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: exampleAnchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - Features: iotago.AccountOutputFeatures{ - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1000, - }, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("1337")}, }, StateIndex: 10, }, }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + next: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: exampleAnchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, StateIndex: 10, - Features: iotago.AccountOutputFeatures{ - &iotago.MetadataFeature{Data: []byte("1337")}, - &iotago.SenderFeature{Address: exampleStateCtrl}, - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 999, - }, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("1338")}, }, }, transType: iotago.ChainTransitionTypeStateChange, svCtx: &vm.Params{ API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: 900, - }, UnlockedIdents: vm.UnlockedIdentities{ exampleStateCtrl.Key(): {UnlockedAt: 0}, }, - BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ - exampleAccountID: 10, - }, Tx: &iotago.Transaction{ - API: tpkg.TestAPI, TransactionEssence: &iotago.TransactionEssence{ - CreationSlot: 900, Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), }, }, }, }, - wantErr: nil, + wantErr: iotago.ErrInvalidAnchorGovernanceTransition, }, { - name: "ok - non-expired block issuer replace key", + name: "fail - update anchor immutable features in state transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + Output: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: exampleAnchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - Features: iotago.AccountOutputFeatures{ - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1000, - }, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("1337")}, }, - StateIndex: 10, - FoundryCounter: 5, + StateIndex: 10, }, }, - next: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - Conditions: iotago.AccountOutputUnlockConditions{ + next: &iotago.AnchorOutput{ + Amount: 200, + AnchorID: exampleAnchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - StateIndex: 10, - FoundryCounter: 5, - Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: exampleStateCtrl}, - &iotago.BlockIssuerFeature{ - BlockIssuerKeys: tpkg.RandomBlockIsssuerKeysEd25519(1), - ExpirySlot: 1000, - }, + StateIndex: 11, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("1338")}, }, }, transType: iotago.ChainTransitionTypeStateChange, svCtx: &vm.Params{ API: tpkg.TestAPI, WorkingSet: &vm.WorkingSet{ - Commitment: &iotago.Commitment{ - Slot: 0, - }, UnlockedIdents: vm.UnlockedIdentities{ exampleStateCtrl.Key(): {UnlockedAt: 0}, }, - InChains: map[iotago.ChainID]*vm.ChainOutputWithIDs{ - // serial number 5 - exampleExistingFoundryOutputFoundryID: { - ChainID: exampleExistingFoundryOutputFoundryID, - OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: exampleExistingFoundryOutput, - }, - }, - BIC: map[iotago.AccountID]iotago.BlockIssuanceCredits{ - exampleAccountID: 10, - }, Tx: &iotago.Transaction{ - API: tpkg.TestAPI, TransactionEssence: &iotago.TransactionEssence{ - Inputs: nil, - Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything())}, - Outputs: iotago.TxEssenceOutputs{ - &iotago.FoundryOutput{ - Amount: 100, - SerialNumber: 6, - TokenScheme: &iotago.SimpleTokenScheme{}, - Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: exampleAccountID.ToAddress().(*iotago.AccountAddress)}, - }, - }, - &iotago.FoundryOutput{ - Amount: 100, - SerialNumber: 7, - TokenScheme: &iotago.SimpleTokenScheme{}, - Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: exampleAccountID.ToAddress().(*iotago.AccountAddress)}, - }, - }, + Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), }, }, }, }, - wantErr: nil, + wantErr: iotago.ErrInvalidAnchorStateTransition, }, { name: "fail - gov transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ + Output: &iotago.AnchorOutput{ Amount: 100, - AccountID: exampleAccountID, + AnchorID: exampleAnchorID, StateIndex: 10, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, @@ -2160,9 +2251,6 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { "state_metadata": { "StateMetadata": []byte("7331"), }, - "foundry_counter": { - "FoundryCounter": uint32(1337), - }, }, transType: iotago.ChainTransitionTypeStateChange, svCtx: &vm.Params{ @@ -2176,22 +2264,21 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, }, - wantErr: iotago.ErrInvalidAccountGovernanceTransition, + wantErr: iotago.ErrInvalidAnchorGovernanceTransition, }, { name: "fail - state transition", input: &vm.ChainOutputWithIDs{ OutputID: tpkg.RandOutputIDWithCreationSlot(0, 0), - Output: &iotago.AccountOutput{ - Amount: 100, - AccountID: exampleAccountID, - StateIndex: 10, - FoundryCounter: 5, - Conditions: iotago.AccountOutputUnlockConditions{ + Output: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: exampleAnchorID, + StateIndex: 10, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, - ImmutableFeatures: iotago.AccountOutputImmFeatures{ + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ &iotago.IssuerFeature{Address: exampleIssuer}, }, }, @@ -2199,35 +2286,27 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { nextMut: map[string]fieldMutations{ "state_controller": { "StateIndex": uint32(11), - "Conditions": iotago.AccountOutputUnlockConditions{ + "Conditions": iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, &iotago.GovernorAddressUnlockCondition{Address: exampleGovCtrl}, }, }, "governance_controller": { "StateIndex": uint32(11), - "Conditions": iotago.AccountOutputUnlockConditions{ + "Conditions": iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: exampleStateCtrl}, &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, }, - "foundry_counter_lower_than_current": { - "FoundryCounter": uint32(4), - "StateIndex": uint32(11), - }, "state_index_lower": { "StateIndex": uint32(4), }, "state_index_bigger_more_than_1": { "StateIndex": uint32(7), }, - "foundries_not_created": { - "StateIndex": uint32(11), - "FoundryCounter": uint32(7), - }, "metadata_feature_added": { "StateIndex": uint32(11), - "Features": iotago.AccountOutputFeatures{ + "Features": iotago.AnchorOutputFeatures{ &iotago.MetadataFeature{Data: []byte("foo")}, }, }, @@ -2246,7 +2325,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { }, }, }, - wantErr: iotago.ErrInvalidAccountStateTransition, + wantErr: iotago.ErrInvalidAnchorStateTransition, }, } @@ -2256,7 +2335,7 @@ func TestAccountOutput_ValidateStateTransition(t *testing.T) { if tt.nextMut != nil { for mutName, muts := range tt.nextMut { t.Run(fmt.Sprintf("%s_%s", tt.name, mutName), func(t *testing.T) { - cpy := copyObject(t, tt.input.Output, muts).(*iotago.AccountOutput) + cpy := copyObject(t, tt.input.Output, muts).(*iotago.AnchorOutput) if tt.input != nil { // create the working set for the test @@ -3562,8 +3641,7 @@ func TestImplicitAccountOutput_ValidateStateTransition(t *testing.T) { Amount: exampleAmount, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{ exampleBlockIssuerFeature, @@ -3611,8 +3689,7 @@ func TestImplicitAccountOutput_ValidateStateTransition(t *testing.T) { Amount: exampleAmount, AccountID: exampleAccountID, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - &iotago.GovernorAddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, }, Features: iotago.AccountOutputFeatures{}, }, diff --git a/vm/nova/vm.go b/vm/nova/vm.go index 06d047bb7..98bd20a5f 100644 --- a/vm/nova/vm.go +++ b/vm/nova/vm.go @@ -163,6 +163,16 @@ func (novaVM *virtualMachine) ChainSTVF(vmParams *vm.Params, transType iotago.Ch return implicitAccountSTVF(vmParams, castedInput, input.OutputID, nextAccount, transType) + case *iotago.AnchorOutput: + var nextAnchor *iotago.AnchorOutput + if next != nil { + if nextAnchor, ok = next.(*iotago.AnchorOutput); !ok { + return ierrors.New("can only state transition to another anchor output") + } + } + + return anchorSTVF(vmParams, input, transType, nextAnchor) + case *iotago.FoundryOutput: var nextFoundry *iotago.FoundryOutput if next != nil { @@ -226,11 +236,7 @@ func implicitAccountSTVF(vmParams *vm.Params, implicitAccount *vm.ImplicitAccoun } // For output AccountOutput(s) with non-zeroed AccountID, there must be a corresponding input AccountOutput where either its -// AccountID is zeroed and StateIndex and FoundryCounter are zero or an input AccountOutput with the same AccountID. -// -// On account state transitions: The StateIndex must be incremented by 1 and Only Amount, NativeTokens, StateIndex, StateMetadata and FoundryCounter can be mutated. -// -// On account governance transition: Only StateController (must be mutated), GovernanceController and the MetadataBlock can be mutated. +// AccountID is zeroed and FoundryCounter is zero or an input AccountOutput with the same AccountID. func accountSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, transType iotago.ChainTransitionType, next *iotago.AccountOutput) error { switch transType { case iotago.ChainTransitionTypeGenesis: @@ -301,97 +307,15 @@ func accountStateChangeValid(vmParams *vm.Params, input *vm.ChainOutputWithIDs, } } - if current.StateIndex == next.StateIndex { - return accountGovernanceSTVF(vmParams, input, next) - } - - return accountStateSTVF(vmParams, input, next) -} - -func accountGovernanceSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, next *iotago.AccountOutput) error { - //nolint:forcetypeassert // we can safely assume that this is an AccountOutput - current := input.Output.(*iotago.AccountOutput) - - switch { - case current.Amount != next.Amount: - return ierrors.Wrapf(iotago.ErrInvalidAccountGovernanceTransition, "amount changed, in %d / out %d ", current.Amount, next.Amount) - case current.StateIndex != next.StateIndex: - return ierrors.Wrapf(iotago.ErrInvalidAccountGovernanceTransition, "state index changed, in %d / out %d", current.StateIndex, next.StateIndex) - case !bytes.Equal(current.StateMetadata, next.StateMetadata): - return ierrors.Wrapf(iotago.ErrInvalidAccountGovernanceTransition, "state metadata changed, in %v / out %v", current.StateMetadata, next.StateMetadata) - case current.FoundryCounter != next.FoundryCounter: - return ierrors.Wrapf(iotago.ErrInvalidAccountGovernanceTransition, "foundry counter changed, in %d / out %d", current.FoundryCounter, next.FoundryCounter) - } - - // staking feature cannot change during account governance transition - if err := iotago.FeatureUnchanged(iotago.FeatureStaking, current.Features.MustSet(), next.Features.MustSet()); err != nil { - return ierrors.Join(iotago.ErrInvalidAccountGovernanceTransition, err) - } - - if current.FeatureSet().Staking() != nil && next.FeatureSet().BlockIssuer() == nil { - return ierrors.Wrapf(iotago.ErrInvalidAccountGovernanceTransition, "%w", iotago.ErrInvalidStakingBlockIssuerRequired) - } - - return accountBlockIssuerSTVF(vmParams, input, input.Output.FeatureSet().BlockIssuer(), next) -} - -func accountStateSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, next *iotago.AccountOutput) error { - //nolint:forcetypeassert // we can safely assume that this is an AccountOutput - current := input.Output.(*iotago.AccountOutput) - switch { - case !current.StateController().Equal(next.StateController()): - return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "state controller changed, in %v / out %v", current.StateController(), next.StateController()) - case !current.GovernorAddress().Equal(next.GovernorAddress()): - return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "governance controller changed, in %v / out %v", current.GovernorAddress(), next.GovernorAddress()) - case current.FoundryCounter > next.FoundryCounter: - return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "foundry counter of next state is less than previous, in %d / out %d", current.FoundryCounter, next.FoundryCounter) - case current.StateIndex+1 != next.StateIndex: - return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "state index %d on the input side but %d on the output side", current.StateIndex, next.StateIndex) - } - - if err := iotago.FeatureUnchanged(iotago.FeatureMetadata, current.Features.MustSet(), next.Features.MustSet()); err != nil { - return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "%w", err) - } - - // block issuer feature cannot change during account state transition - if err := iotago.FeatureUnchanged(iotago.FeatureBlockIssuer, current.Features.MustSet(), next.Features.MustSet()); err != nil { - return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "%w", err) - } - if err := accountStakingSTVF(vmParams, input.ChainID, current, next); err != nil { return err } - // check that for a foundry counter change, X amount of foundries were actually created - if current.FoundryCounter == next.FoundryCounter { - return nil - } - - var seenNewFoundriesOfAccount uint32 - for _, output := range vmParams.WorkingSet.Tx.Outputs { - foundryOutput, is := output.(*iotago.FoundryOutput) - if !is { - continue - } - - if _, notNew := vmParams.WorkingSet.InChains[foundryOutput.MustFoundryID()]; notNew { - continue - } - - //nolint:forcetypeassert // we can safely assume that this is an AccountAddress - foundryAccountID := foundryOutput.Ident().(*iotago.AccountAddress).ChainID() - if !foundryAccountID.Matches(next.AccountID) { - continue - } - seenNewFoundriesOfAccount++ - } - - expectedNewFoundriesCount := next.FoundryCounter - current.FoundryCounter - if expectedNewFoundriesCount != seenNewFoundriesOfAccount { - return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "%d new foundries were created but the account output's foundry counter changed by %d", seenNewFoundriesOfAccount, expectedNewFoundriesCount) + if err := accountFoundryCounterSTVF(vmParams, current, next); err != nil { + return err } - return nil + return accountBlockIssuerSTVF(vmParams, input, input.Output.FeatureSet().BlockIssuer(), next) } // If an account output has a block issuer feature, the following conditions for its transition must be checked. @@ -401,23 +325,24 @@ func accountStateSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, next *i func accountBlockIssuerSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, currentBlockIssuerFeat *iotago.BlockIssuerFeature, next *iotago.AccountOutput) error { current := input.Output nextBlockIssuerFeat := next.FeatureSet().BlockIssuer() + // if the account has no block issuer feature. if currentBlockIssuerFeat == nil && nextBlockIssuerFeat == nil { return nil } + if vmParams.WorkingSet.Commitment == nil { + return ierrors.Wrap(iotago.ErrInvalidBlockIssuerTransition, "block issuer feature validation requires a commitment input") + } + // else if the account has negative bic, this is invalid. // new block issuers may not have a bic registered yet. if bic, exists := vmParams.WorkingSet.BIC[next.AccountID]; exists { if bic < 0 { - return ierrors.Wrap(iotago.ErrInvalidBlockIssuerTransition, "negative block issuer credit") + return ierrors.Wrapf(iotago.ErrInvalidBlockIssuerTransition, "negative block issuer credit: %d", bic) } } else { - return ierrors.Wrap(iotago.ErrInvalidBlockIssuerTransition, "no BIC provided for block issuer") - } - - if vmParams.WorkingSet.Commitment == nil { - return ierrors.Wrap(iotago.ErrInvalidBlockIssuerTransition, "block issuer feature validation requires a commitment input") + return ierrors.Wrap(iotago.ErrInvalidBlockIssuerTransition, "no BIC provided for block issuer feature validation") } commitmentInputSlot := vmParams.WorkingSet.Commitment.Slot @@ -426,10 +351,10 @@ func accountBlockIssuerSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, c if currentBlockIssuerFeat != nil && currentBlockIssuerFeat.ExpirySlot >= commitmentInputSlot { // if the block issuer feature has not expired, it can not be removed. if nextBlockIssuerFeat == nil { - return ierrors.Wrap(iotago.ErrInvalidBlockIssuerTransition, "cannot remove block issuer feature until it expires") + return ierrors.Wrapf(iotago.ErrInvalidBlockIssuerTransition, "cannot remove block issuer feature until it expires (current slot: %d, expiry slot: %d)", commitmentInputSlot, currentBlockIssuerFeat.ExpirySlot) } if nextBlockIssuerFeat.ExpirySlot != currentBlockIssuerFeat.ExpirySlot && nextBlockIssuerFeat.ExpirySlot < pastBoundedSlot { - return ierrors.Wrap(iotago.ErrInvalidBlockIssuerTransition, "block issuer feature expiry set too soon") + return ierrors.Wrapf(iotago.ErrInvalidBlockIssuerTransition, "block issuer feature expiry set too soon (is %d, must be >= %d)", nextBlockIssuerFeat.ExpirySlot, pastBoundedSlot) } } else if nextBlockIssuerFeat != nil { // The block issuer feature was newly added, @@ -454,7 +379,7 @@ func accountBlockIssuerSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, c } manaIn, err = safemath.SafeSub(manaIn, manaStoredAccount) if err != nil { - return ierrors.Wrapf(err, "account %s stored mana in exceeds total remaining mana in", next.AccountID) + return ierrors.Wrapf(err, "account %s stored mana in exceeds total remaining mana in, manaStoredAccountIn: %d, manaIn: %d", next.AccountID, manaStoredAccount, manaIn) } // AccountInPotential - the potential mana from the input side of the account in question @@ -463,37 +388,43 @@ func accountBlockIssuerSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, c if err != nil { return err } + excessBaseTokensAccount, err := safemath.SafeSub(current.BaseTokenAmount(), minDeposit) if err != nil { + // an error means there was an underflow, which means the account has less than the minimum deposit excessBaseTokensAccount = 0 } + manaPotentialAccount, err := manaDecayProvider.ManaGenerationWithDecay(excessBaseTokensAccount, input.OutputID.CreationSlot(), vmParams.WorkingSet.Tx.CreationSlot) if err != nil { return ierrors.Wrapf(err, "account %s potential mana calculation failed", next.AccountID) } + manaIn, err = safemath.SafeSub(manaIn, manaPotentialAccount) if err != nil { - return ierrors.Wrapf(err, "account %s potential mana in exceeds total remaining mana in", next.AccountID) + return ierrors.Wrapf(err, "account %s potential mana in exceeds total remaining mana in, manaPotentialAccountIn: %d, manaIn: %d", next.AccountID, manaPotentialAccount, manaIn) } + // AccountOutStored - stored Mana on the output side of the account in question manaOut, err = safemath.SafeSub(manaOut, next.Mana) if err != nil { - return ierrors.Wrapf(err, "account %s stored mana out exceeds total remaining mana out", next.AccountID) + return ierrors.Wrapf(err, "account %s stored mana out exceeds total remaining mana out, storedManaOut: %d, manaOut: %d", next.AccountID, next.Mana, manaOut) } + // AccountOutAllotted - allotments to the account in question accountOutAllotted := vmParams.WorkingSet.Tx.Allotments.Get(next.AccountID) manaOut, err = safemath.SafeSub(manaOut, accountOutAllotted) if err != nil { - return ierrors.Wrapf(err, "account %s allotment exceeds total remaining mana out", next.AccountID) + return ierrors.Wrapf(err, "account %s allotment exceeds total remaining mana out, accountAllotted: %d, manaOut: %d", next.AccountID, accountOutAllotted, manaOut) } // AccountOutLocked - outputs with manalock conditions minManalockedSlot := pastBoundedSlot + vmParams.API.ProtocolParameters().MaxCommittableAge() - for _, output := range vmParams.WorkingSet.Tx.Outputs { + for outputIndex, output := range vmParams.WorkingSet.Tx.Outputs { if output.UnlockConditionSet().HasManalockCondition(next.AccountID, minManalockedSlot) { manaOut, err = safemath.SafeSub(manaOut, output.StoredMana()) if err != nil { - return ierrors.Wrapf(err, "account %s manalocked output mana exceeds total remaining mana out", next.AccountID) + return ierrors.Wrapf(err, "account %s manalocked output mana exceeds total remaining mana out, outputIndex: %d, outputStoredMana: %d, manaOut: %d", next.AccountID, outputIndex, output.StoredMana(), manaOut) } } } @@ -512,6 +443,10 @@ func accountStakingSTVF(vmParams *vm.Params, chainID iotago.ChainID, current *io _, isClaiming := vmParams.WorkingSet.Rewards[chainID] if currentStakingFeat != nil { + if next.FeatureSet().BlockIssuer() == nil { + return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "%w", iotago.ErrInvalidStakingBlockIssuerRequired) + } + commitment := vmParams.WorkingSet.Commitment if commitment == nil { return ierrors.Join(iotago.ErrInvalidStakingTransition, iotago.ErrInvalidStakingCommitmentInput) @@ -531,9 +466,7 @@ func accountStakingSTVF(vmParams *vm.Params, chainID iotago.ChainID, current *io ) } - return accountStakingExpiredValidation( - vmParams, next, currentStakingFeat, nextStakingFeat, isClaiming, - ) + return accountStakingExpiredValidation(vmParams, next, currentStakingFeat, nextStakingFeat, isClaiming) } else if nextStakingFeat != nil { return accountStakingGenesisValidation(vmParams, next, nextStakingFeat) } @@ -637,6 +570,43 @@ func accountStakingExpiredValidation( return nil } +func accountFoundryCounterSTVF(vmParams *vm.Params, current *iotago.AccountOutput, next *iotago.AccountOutput) error { + if current.FoundryCounter > next.FoundryCounter { + return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "foundry counter of next state is less than previous, in %d / out %d", current.FoundryCounter, next.FoundryCounter) + } + + // check that for a foundry counter change, X amount of foundries were actually created + if current.FoundryCounter == next.FoundryCounter { + return nil + } + + var seenNewFoundriesOfAccount uint32 + for _, output := range vmParams.WorkingSet.Tx.Outputs { + foundryOutput, is := output.(*iotago.FoundryOutput) + if !is { + continue + } + + if _, notNew := vmParams.WorkingSet.InChains[foundryOutput.MustFoundryID()]; notNew { + continue + } + + //nolint:forcetypeassert // we can safely assume that this is an AccountAddress + foundryAccountID := foundryOutput.Ident().(*iotago.AccountAddress).ChainID() + if !foundryAccountID.Matches(next.AccountID) { + continue + } + seenNewFoundriesOfAccount++ + } + + expectedNewFoundriesCount := next.FoundryCounter - current.FoundryCounter + if expectedNewFoundriesCount != seenNewFoundriesOfAccount { + return ierrors.Wrapf(iotago.ErrInvalidAccountStateTransition, "%d new foundries were created but the account output's foundry counter changed by %d", seenNewFoundriesOfAccount, expectedNewFoundriesCount) + } + + return nil +} + func accountDestructionValid(vmParams *vm.Params, input *vm.ChainOutputWithIDs) error { if vmParams.WorkingSet.Tx.Capabilities.CannotDestroyAccountOutputs() { return ierrors.Join(iotago.ErrInvalidAccountStateTransition, iotago.ErrTxCapabilitiesAccountDestructionNotAllowed) @@ -689,6 +659,111 @@ func accountDestructionValid(vmParams *vm.Params, input *vm.ChainOutputWithIDs) return nil } +// For output AnchorOutput(s) with non-zeroed AnchorID, there must be a corresponding input AnchorOutput where either its +// AnchorID is zeroed and StateIndex is zero or an input AnchorOutput with the same AnchorID. +// +// On anchor state transitions: The StateIndex must be incremented by 1 and Only Amount, StateIndex and StateMetadata can be mutated. +// +// On anchor governance transition: Only StateController (must be mutated), GovernanceController and the MetadataBlock can be mutated. +func anchorSTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, transType iotago.ChainTransitionType, next *iotago.AnchorOutput) error { + switch transType { + case iotago.ChainTransitionTypeGenesis: + if err := anchorGenesisValid(vmParams, next, true); err != nil { + return ierrors.Wrapf(err, " anchor %s", next.AnchorID) + } + case iotago.ChainTransitionTypeStateChange: + if err := anchorStateChangeValid(input, next); err != nil { + //nolint:forcetypeassert // we can safely assume that this is an AnchorOutput + a := input.Output.(*iotago.AnchorOutput) + + return ierrors.Wrapf(err, "anchor %s", a.AnchorID) + } + case iotago.ChainTransitionTypeDestroy: + if err := anchorDestructionValid(vmParams); err != nil { + //nolint:forcetypeassert // we can safely assume that this is an AnchorOutput + a := input.Output.(*iotago.AnchorOutput) + + return ierrors.Wrapf(err, "anchor %s", a.AnchorID) + } + default: + panic("unknown chain transition type in AnchorOutput") + } + + return nil +} + +func anchorGenesisValid(vmParams *vm.Params, current *iotago.AnchorOutput, anchorIDMustBeZeroed bool) error { + if anchorIDMustBeZeroed && !current.AnchorID.Empty() { + return ierrors.Wrap(iotago.ErrInvalidAnchorStateTransition, "AnchorOutput's ID is not zeroed even though it is new") + } + + return vm.IsIssuerOnOutputUnlocked(current, vmParams.WorkingSet.UnlockedIdents) +} + +func anchorStateChangeValid(input *vm.ChainOutputWithIDs, next *iotago.AnchorOutput) error { + //nolint:forcetypeassert // we can safely assume that this is an AnchorOutput + current := input.Output.(*iotago.AnchorOutput) + + isGovTransition := current.StateIndex == next.StateIndex + if !current.ImmutableFeatures.Equal(next.ImmutableFeatures) { + err := iotago.ErrInvalidAnchorStateTransition + if isGovTransition { + err = iotago.ErrInvalidAnchorGovernanceTransition + } + + return ierrors.Wrapf(err, "old state %s, next state %s", current.ImmutableFeatures, next.ImmutableFeatures) + } + + if isGovTransition { + return anchorGovernanceSTVF(input, next) + } + + return anchorStateSTVF(input, next) +} + +func anchorGovernanceSTVF(input *vm.ChainOutputWithIDs, next *iotago.AnchorOutput) error { + //nolint:forcetypeassert // we can safely assume that this is an AnchorOutput + current := input.Output.(*iotago.AnchorOutput) + + switch { + case current.Amount != next.Amount: + return ierrors.Wrapf(iotago.ErrInvalidAnchorGovernanceTransition, "amount changed, in %d / out %d ", current.Amount, next.Amount) + case current.StateIndex != next.StateIndex: + return ierrors.Wrapf(iotago.ErrInvalidAnchorGovernanceTransition, "state index changed, in %d / out %d", current.StateIndex, next.StateIndex) + case !bytes.Equal(current.StateMetadata, next.StateMetadata): + return ierrors.Wrapf(iotago.ErrInvalidAnchorGovernanceTransition, "state metadata changed, in %v / out %v", current.StateMetadata, next.StateMetadata) + } + + return nil +} + +func anchorStateSTVF(input *vm.ChainOutputWithIDs, next *iotago.AnchorOutput) error { + //nolint:forcetypeassert // we can safely assume that this is an AnchorOutput + current := input.Output.(*iotago.AnchorOutput) + switch { + case !current.StateController().Equal(next.StateController()): + return ierrors.Wrapf(iotago.ErrInvalidAnchorStateTransition, "state controller changed, in %v / out %v", current.StateController(), next.StateController()) + case !current.GovernorAddress().Equal(next.GovernorAddress()): + return ierrors.Wrapf(iotago.ErrInvalidAnchorStateTransition, "governance controller changed, in %v / out %v", current.GovernorAddress(), next.GovernorAddress()) + case current.StateIndex+1 != next.StateIndex: + return ierrors.Wrapf(iotago.ErrInvalidAnchorStateTransition, "state index %d on the input side but %d on the output side", current.StateIndex, next.StateIndex) + } + + if err := iotago.FeatureUnchanged(iotago.FeatureMetadata, current.Features.MustSet(), next.Features.MustSet()); err != nil { + return ierrors.Wrapf(iotago.ErrInvalidAnchorStateTransition, "%w", err) + } + + return nil +} + +func anchorDestructionValid(vmParams *vm.Params) error { + if vmParams.WorkingSet.Tx.Capabilities.CannotDestroyAnchorOutputs() { + return ierrors.Join(iotago.ErrInvalidAnchorStateTransition, iotago.ErrTxCapabilitiesAnchorDestructionNotAllowed) + } + + return nil +} + func foundrySTVF(vmParams *vm.Params, input *vm.ChainOutputWithIDs, transType iotago.ChainTransitionType, next *iotago.FoundryOutput) error { inSums := vmParams.WorkingSet.InNativeTokens outSums := vmParams.WorkingSet.OutNativeTokens diff --git a/vm/nova/vm_test.go b/vm/nova/vm_test.go index 73c74cfb5..42ed0d955 100644 --- a/vm/nova/vm_test.go +++ b/vm/nova/vm_test.go @@ -21,7 +21,7 @@ import ( ) const ( - OneMi = 1_000_000 + OneIOTA iotago.BaseToken = 1_000_000 betaPerYear float64 = 1 / 3.0 slotsPerEpochExponent = 13 @@ -64,7 +64,7 @@ func TestNFTTransition(t *testing.T) { inputIDs := tpkg.RandOutputIDs(1) inputs := vm.InputSet{ inputIDs[0]: &iotago.NFTOutput{ - Amount: OneMi, + Amount: OneIOTA, NFTID: iotago.NFTID{}, Conditions: iotago.NFTOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, @@ -84,7 +84,7 @@ func TestNFTTransition(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.NFTOutput{ - Amount: OneMi, + Amount: OneIOTA, NFTID: nftID, Conditions: iotago.NFTOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, @@ -115,25 +115,22 @@ func TestCirculatingSupplyMelting(t *testing.T) { inputIDs := tpkg.RandOutputIDs(3) inputs := vm.InputSet{ inputIDs[0]: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, }, inputIDs[1]: &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: accountIdent1.AccountID(), - StateIndex: 1, - StateMetadata: nil, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, Features: nil, }, inputIDs[2]: &iotago.FoundryOutput{ - Amount: OneMi, + Amount: OneIOTA, SerialNumber: 1, TokenScheme: &iotago.SimpleTokenScheme{ MintedTokens: big.NewInt(50), @@ -162,19 +159,16 @@ func TestCirculatingSupplyMelting(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: OneMi, + Amount: OneIOTA, AccountID: accountIdent1.AccountID(), - StateIndex: 2, - StateMetadata: nil, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, Features: nil, }, &iotago.FoundryOutput{ - Amount: 2 * OneMi, + Amount: 2 * OneIOTA, SerialNumber: 1, TokenScheme: &iotago.SimpleTokenScheme{ MintedTokens: big.NewInt(50), @@ -214,6 +208,7 @@ func TestNovaTransactionExecution(t *testing.T) { wantErr error } tests := []*test{ + // ok func() *test { var ( _, ident1, ident1AddrKeys = tpkg.RandEd25519Identity() @@ -224,10 +219,10 @@ func TestNovaTransactionExecution(t *testing.T) { ) var ( - defaultAmount iotago.BaseToken = OneMi - storageDepositReturn iotago.BaseToken = OneMi / 2 - nativeTokenTransfer1 = tpkg.RandNativeTokenFeature() - nativeTokenTransfer2 = tpkg.RandNativeTokenFeature() + defaultAmount = OneIOTA + storageDepositReturn = OneIOTA / 2 + nativeTokenTransfer1 = tpkg.RandNativeTokenFeature() + nativeTokenTransfer2 = tpkg.RandNativeTokenFeature() ) var ( @@ -235,15 +230,33 @@ func TestNovaTransactionExecution(t *testing.T) { nft2ID = tpkg.Rand32ByteArray() ) - inputIDs := tpkg.RandOutputIDs(16) + inputIDs := tpkg.RandOutputIDs(18) + + account1InputID := inputIDs[6] + account2InputID := inputIDs[7] + + account1AccountID := iotago.AccountIDFromOutputID(account1InputID) + account1AccountAddress := account1AccountID.ToAddress().(*iotago.AccountAddress) + + foundry1InputID := inputIDs[11] + foundry2InputID := inputIDs[12] + foundry3InputID := inputIDs[13] + foundry4InputID := inputIDs[14] + + nft1InputID := inputIDs[15] inputs := vm.InputSet{ + // basic output with no features [defaultAmount] (owned by ident1) + // => output 0: change ownership to ident5 inputIDs[0]: &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, }, + + // basic output with native token feature - nativeTokenTransfer1 [defaultAmount] (owned by ident2) + // => output 1: change ownership to ident3 inputIDs[1]: &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -253,6 +266,9 @@ func TestNovaTransactionExecution(t *testing.T) { nativeTokenTransfer1, }, }, + + // basic output with native token feature - nativeTokenTransfer2 [defaultAmount] (owned by ident2) + // => output 2: change ownership to ident4 inputIDs[2]: &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -262,6 +278,9 @@ func TestNovaTransactionExecution(t *testing.T) { nativeTokenTransfer2, }, }, + + // basic output with expiration unlock condition - slot: 500, return: ident1 [defaultAmount] (originally owned by ident2 => creation slot 750 => owned by ident1) + // => output 3: remove expiration unlock condition inputIDs[3]: &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -272,6 +291,9 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, }, + + // basic output with timelock unlock condition - slot: 500 [defaultAmount] (owned by ident2 => creation slot 750 => can be unlocked) + // => output 4: remove timelock unlock condition inputIDs[4]: &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -281,6 +303,13 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, }, + + // basic output [defaultAmount + storageDepositReturn] (owned by ident2 => creation slot 750 => can be unlocked, owned by ident2) + // storage deposit return unlock condition - return: ident1 + // timelock unlock condition - slot 500 + // expiration unlock condition - slot: 900, return: ident1 + // => output 5: storageDepositReturn to ident1 + // => output 14: defaultAmount inputIDs[5]: &iotago.BasicOutput{ Amount: defaultAmount + storageDepositReturn, Conditions: iotago.BasicOutputUnlockConditions{ @@ -298,43 +327,76 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, }, - inputIDs[6]: &iotago.AccountOutput{ + + // account output with no features - foundry counter 5 [defaultAmount] (owned by ident3) => going to be transitioned + // => output 6: output transition (foundry counter 5 => 6, added metadata) + account1InputID: &iotago.AccountOutput{ + Amount: defaultAmount, + AccountID: iotago.AccountID{}, + FoundryCounter: 5, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident3}, + }, + Features: nil, + }, + + // account output with no features [defaultAmount] (owned by ident3) => going to be destroyed + // => output 7: destroyed and new account output created + account2InputID: &iotago.AccountOutput{ Amount: defaultAmount, AccountID: iotago.AccountID{}, - StateIndex: 0, - StateMetadata: []byte("gov transitioning"), FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident3}, + }, + Features: nil, + }, + + // anchor output with no features - state index 0 [defaultAmount] (owned by - state: ident3, gov: ident4) => going to be governance transitioned + // => output 8: governance transition (added metadata) + inputIDs[8]: &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: iotago.AnchorID{}, + StateIndex: 0, + StateMetadata: []byte("gov transitioning"), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident3}, &iotago.GovernorAddressUnlockCondition{Address: ident4}, }, Features: nil, }, - inputIDs[7]: &iotago.AccountOutput{ - Amount: defaultAmount + defaultAmount, // to fund also the new account output - AccountID: iotago.AccountID{}, - StateIndex: 5, - StateMetadata: []byte("current state"), - FoundryCounter: 5, - Conditions: iotago.AccountOutputUnlockConditions{ + + // anchor output with no features - state index 5 [defaultAmount] (owned by - state: ident3, gov: ident4) => going to be state transitioned + // => output 9: state transition (state index 5 => 6, changed state metadata) + inputIDs[9]: &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: iotago.AnchorID{}, + StateIndex: 5, + StateMetadata: []byte("state transitioning"), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident3}, &iotago.GovernorAddressUnlockCondition{Address: ident4}, }, Features: nil, }, - inputIDs[8]: &iotago.AccountOutput{ - Amount: defaultAmount, - AccountID: iotago.AccountID{}, - StateIndex: 0, - StateMetadata: []byte("going to be destroyed"), - FoundryCounter: 0, - Conditions: iotago.AccountOutputUnlockConditions{ + + // anchor output with no features - state index 0 [defaultAmount] (owned by - state: ident3, gov: ident3) => going to be destroyed + // => output 10: destroyed and new anchor output created + inputIDs[10]: &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: iotago.AnchorID{}, + StateIndex: 0, + StateMetadata: []byte("going to be destroyed"), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident3}, &iotago.GovernorAddressUnlockCondition{Address: ident3}, }, Features: nil, }, - inputIDs[9]: &iotago.FoundryOutput{ + + // foundry output - serialNumber: 1, minted: 100, melted: 0, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // => output 11: mint 100 new tokens + foundry1InputID: &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 1, TokenScheme: &iotago.SimpleTokenScheme{ @@ -343,11 +405,15 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetUint64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: nil, }, - inputIDs[10]: &iotago.FoundryOutput{ + + // foundry output - serialNumber: 2, minted: 100, melted: 0, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // - native token balance later updated to 100 (still on input side) + // => output 12: melt 50 tokens + foundry2InputID: &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 2, TokenScheme: &iotago.SimpleTokenScheme{ @@ -356,13 +422,16 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetUint64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: iotago.FoundryOutputFeatures{ // native token feature added later }, }, - inputIDs[11]: &iotago.FoundryOutput{ + + // foundry output - serialNumber: 3, minted: 100, melted: 0, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // => output 13: add metadata + foundry3InputID: &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 3, TokenScheme: &iotago.SimpleTokenScheme{ @@ -371,11 +440,15 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetUint64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: nil, }, - inputIDs[12]: &iotago.FoundryOutput{ + + // foundry output - serialNumber: 4, minted: 100, melted: 0, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // - native token balance later updated to 50 (still on input side) + // => output 15: foundry destroyed + foundry4InputID: &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 4, TokenScheme: &iotago.SimpleTokenScheme{ @@ -384,11 +457,14 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetUint64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: nil, }, - inputIDs[13]: &iotago.NFTOutput{ + + // NFT output with issuer (ident3) and immutable metadata feature [defaultAmount] (owned by ident3) => going to be transferred to ident4 + // => output 16: transfer to ident4 + nft1InputID: &iotago.NFTOutput{ Amount: defaultAmount, NFTID: nft1ID, Conditions: iotago.NFTOutputUnlockConditions{ @@ -401,7 +477,10 @@ func TestNovaTransactionExecution(t *testing.T) { &iotago.MetadataFeature{Data: []byte("transfer to 4")}, }, }, - inputIDs[14]: &iotago.NFTOutput{ + + // NFT output with immutable features [defaultAmount] (owned by ident4) => going to be destroyed + // => output 17: destroyed and new NFT output created + inputIDs[16]: &iotago.NFTOutput{ Amount: defaultAmount, NFTID: nft2ID, Conditions: iotago.NFTOutputUnlockConditions{ @@ -414,7 +493,10 @@ func TestNovaTransactionExecution(t *testing.T) { &iotago.MetadataFeature{Data: []byte("going to be destroyed")}, }, }, - inputIDs[15]: &iotago.BasicOutput{ + + // basic output with no features [defaultAmount] (owned by nft1ID) + // => output 18: change ownership to ident5 + inputIDs[17]: &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: iotago.NFTID(nft1ID).ToAddress()}, @@ -422,10 +504,22 @@ func TestNovaTransactionExecution(t *testing.T) { }, } - foundry1Ident3NativeTokenID := inputs[inputIDs[9]].(*iotago.FoundryOutput).MustNativeTokenID() - foundry2Ident3NativeTokenID := inputs[inputIDs[10]].(*iotago.FoundryOutput).MustNativeTokenID() - foundry4Ident3NativeTokenID := inputs[inputIDs[12]].(*iotago.FoundryOutput).MustNativeTokenID() + foundry1Ident3NativeTokenID := inputs[foundry1InputID].(*iotago.FoundryOutput).MustNativeTokenID() + foundry2Ident3NativeTokenID := inputs[foundry2InputID].(*iotago.FoundryOutput).MustNativeTokenID() + foundry4Ident3NativeTokenID := inputs[foundry3InputID].(*iotago.FoundryOutput).MustNativeTokenID() + + inputs[foundry2InputID].(*iotago.FoundryOutput).Features.Upsert(&iotago.NativeTokenFeature{ + ID: foundry2Ident3NativeTokenID, + Amount: big.NewInt(100), + }) + + inputs[foundry4InputID].(*iotago.FoundryOutput).Features.Upsert(&iotago.NativeTokenFeature{ + ID: foundry4Ident3NativeTokenID, + Amount: big.NewInt(50), + }) + // new foundry output - serialNumber: 6, minted: 100, melted: 0, max: 1000 (owned by account1AccountAddress) + // - native token balance 100 newFoundryWithInitialSupply := &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 6, @@ -435,7 +529,7 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetInt64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: nil, } @@ -445,16 +539,6 @@ func TestNovaTransactionExecution(t *testing.T) { Amount: big.NewInt(100), }) - inputs[inputIDs[10]].(*iotago.FoundryOutput).Features.Upsert(&iotago.NativeTokenFeature{ - ID: foundry2Ident3NativeTokenID, - Amount: big.NewInt(100), - }) - - inputs[inputIDs[12]].(*iotago.FoundryOutput).Features.Upsert(&iotago.NativeTokenFeature{ - ID: foundry4Ident3NativeTokenID, - Amount: big.NewInt(50), - }) - creationSlot := iotago.SlotIndex(750) transaction := &iotago.Transaction{ API: testAPI, @@ -464,12 +548,17 @@ func TestNovaTransactionExecution(t *testing.T) { Capabilities: iotago.TransactionCapabilitiesBitMaskWithCapabilities(iotago.WithTransactionCanDoAnything()), }, Outputs: iotago.TxEssenceOutputs{ + // basic output [defaultAmount] (owned by ident5) + // => input 0 &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident5}, }, }, + + // basic output with native token feature - nativeTokenTransfer1 [defaultAmount] (owned by ident3) + // => input 1 &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -479,6 +568,9 @@ func TestNovaTransactionExecution(t *testing.T) { nativeTokenTransfer1, }, }, + + // basic output with native token feature - nativeTokenTransfer2 [defaultAmount] (owned by ident4) + // => input 2 &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -488,64 +580,109 @@ func TestNovaTransactionExecution(t *testing.T) { nativeTokenTransfer2, }, }, + + // basic output [defaultAmount] (owned by ident2) + // => input 3 &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident2}, }, }, + + // basic output [defaultAmount] (owned by ident2) + // => input 4 &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident2}, }, }, + + // basic output [storageDepositReturn] (owned by ident1) + // => input 5 &iotago.BasicOutput{ Amount: storageDepositReturn, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, }, + + // transitioned account output [defaultAmount] (owned by ident3) + // => input 6 &iotago.AccountOutput{ Amount: defaultAmount, - AccountID: iotago.AccountID{}, - StateIndex: 0, - StateMetadata: []byte("a new account output"), - FoundryCounter: 0, + AccountID: account1AccountID, + FoundryCounter: 6, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident3}, - &iotago.GovernorAddressUnlockCondition{Address: ident4}, + &iotago.AddressUnlockCondition{Address: ident3}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.MetadataFeature{Data: []byte("transitioned")}, }, - Features: nil, }, + + // new account output [defaultAmount] (owned by ident3) + // => input 7 &iotago.AccountOutput{ Amount: defaultAmount, - AccountID: iotago.AccountIDFromOutputID(inputIDs[6]), - StateIndex: 0, - StateMetadata: []byte("gov transitioning"), + AccountID: iotago.AccountID{}, FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident3}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.MetadataFeature{Data: []byte("new")}, + }, + }, + + // governance transitioned anchor output [defaultAmount] (owned by - state: ident3, gov: ident4) + // => input 8 + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: iotago.AnchorIDFromOutputID(inputIDs[6]), + StateIndex: 0, + StateMetadata: []byte("gov transitioning"), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident3}, &iotago.GovernorAddressUnlockCondition{Address: ident4}, }, - Features: iotago.AccountOutputFeatures{ + Features: iotago.AnchorOutputFeatures{ &iotago.MetadataFeature{Data: []byte("the gov mutation on this output")}, }, }, - &iotago.AccountOutput{ - Amount: defaultAmount, - AccountID: iotago.AccountIDFromOutputID(inputIDs[7]), - StateIndex: 6, - StateMetadata: []byte("next state"), - FoundryCounter: 6, - Conditions: iotago.AccountOutputUnlockConditions{ + + // state transitioned anchor output [defaultAmount] (owned by - state: ident3, gov: ident4) + // => input 9 + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: iotago.AnchorIDFromOutputID(inputIDs[7]), + StateIndex: 6, + StateMetadata: []byte("next state"), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident3}, &iotago.GovernorAddressUnlockCondition{Address: ident4}, }, Features: nil, }, - // new foundry - newFoundryWithInitialSupply, + + // new anchor output [defaultAmount] (owned by - state: ident3, gov: ident4) + // => input 10 + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: iotago.AnchorID{}, + StateIndex: 0, + StateMetadata: []byte("a new anchor output"), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident3}, + &iotago.GovernorAddressUnlockCondition{Address: ident4}, + }, + Features: nil, + }, + + // foundry output - serialNumber: 1, minted: 200, melted: 0, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // - native token balance 100 (freshly minted) + // => input 11 &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 1, @@ -555,7 +692,7 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetInt64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: iotago.FoundryOutputFeatures{ &iotago.NativeTokenFeature{ @@ -564,6 +701,10 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, }, + + // foundry output - serialNumber: 2, minted: 100, melted: 50, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // - native token balance 50 (melted 50) + // => input 12 &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 2, @@ -573,7 +714,7 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetInt64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: iotago.FoundryOutputFeatures{ &iotago.NativeTokenFeature{ @@ -582,6 +723,9 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, }, + + // foundry output - serialNumber: 3, minted: 100, melted: 0, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // => input 13 &iotago.FoundryOutput{ Amount: defaultAmount, SerialNumber: 3, @@ -591,30 +735,29 @@ func TestNovaTransactionExecution(t *testing.T) { MaximumSupply: new(big.Int).SetInt64(1000), }, Conditions: iotago.FoundryOutputUnlockConditions{ - &iotago.ImmutableAccountUnlockCondition{Address: iotago.AccountIDFromOutputID(inputIDs[7]).ToAddress().(*iotago.AccountAddress)}, + &iotago.ImmutableAccountUnlockCondition{Address: account1AccountAddress}, }, Features: iotago.FoundryOutputFeatures{ &iotago.MetadataFeature{Data: []byte("interesting metadata")}, }, }, - // from foundry 4 ident 3 destruction remainder + + // foundry output - serialNumber: 6, minted: 100, melted: 0, max: 1000 [defaultAmount] (owned by account1AccountAddress) + // - native token balance 100 + // => input 5 + newFoundryWithInitialSupply, + + // basic output [defaultAmount] (owned by ident3) + // => input 14 (foundry 4 destruction remainder) &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident3}, }, }, - &iotago.NFTOutput{ - Amount: defaultAmount, - NFTID: iotago.NFTID{}, - Conditions: iotago.NFTOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: ident4}, - }, - Features: nil, - ImmutableFeatures: iotago.NFTOutputImmFeatures{ - &iotago.MetadataFeature{Data: []byte("immutable metadata")}, - }, - }, + + // NFT output transitioned and changed ownership [defaultAmount] (owned by ident4) + // => input 15 &iotago.NFTOutput{ Amount: defaultAmount, NFTID: nft1ID, @@ -628,14 +771,23 @@ func TestNovaTransactionExecution(t *testing.T) { &iotago.MetadataFeature{Data: []byte("transfer to 4")}, }, }, - // from NFT ident 4 destruction remainder - &iotago.BasicOutput{ + + // new NFT output [defaultAmount] (owned by ident4) + // => input 16 + &iotago.NFTOutput{ Amount: defaultAmount, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: ident3}, + NFTID: iotago.NFTID{}, + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident4}, + }, + Features: nil, + ImmutableFeatures: iotago.NFTOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("immutable metadata")}, }, }, - // from NFT 1 to ident 5 + + // basic output [defaultAmount] (owned by ident5) + // => input 17 &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -662,35 +814,39 @@ func TestNovaTransactionExecution(t *testing.T) { Transaction: transaction, Unlocks: iotago.Unlocks{ // basic - &iotago.SignatureUnlock{Signature: sigs[0]}, - &iotago.SignatureUnlock{Signature: sigs[1]}, - &iotago.ReferenceUnlock{Reference: 1}, - &iotago.ReferenceUnlock{Reference: 0}, - &iotago.ReferenceUnlock{Reference: 1}, - &iotago.ReferenceUnlock{Reference: 1}, + &iotago.SignatureUnlock{Signature: sigs[0]}, // basic output (owned by ident1) => (ident1 == Reference 0) + &iotago.SignatureUnlock{Signature: sigs[1]}, // basic output (owned by ident2) => (ident2 == Reference 1) + &iotago.ReferenceUnlock{Reference: 1}, // basic output (owned by ident2) + &iotago.ReferenceUnlock{Reference: 0}, // basic output (owned by ident1) + &iotago.ReferenceUnlock{Reference: 1}, // basic output (owned by ident2) + &iotago.ReferenceUnlock{Reference: 1}, // basic output (owned by ident2) // account - &iotago.SignatureUnlock{Signature: sigs[3]}, - &iotago.SignatureUnlock{Signature: sigs[2]}, - &iotago.ReferenceUnlock{Reference: 7}, + &iotago.SignatureUnlock{Signature: sigs[2]}, // account output (owned by ident3) => (ident3 == Reference 6) + &iotago.ReferenceUnlock{Reference: 6}, // account output (owned by ident3) + // anchor + &iotago.SignatureUnlock{Signature: sigs[3]}, // anchor output (owned by state: ident3, gov: ident4) => governance transitioned => (ident4 == Reference 8) + &iotago.ReferenceUnlock{Reference: 6}, // anchor output (owned by state: ident3, gov: ident4) => state transitioned + &iotago.ReferenceUnlock{Reference: 6}, // anchor output (owned by state: ident3, gov: ident3) => governance transitioned // foundries - &iotago.AccountUnlock{Reference: 7}, - &iotago.AccountUnlock{Reference: 7}, - &iotago.AccountUnlock{Reference: 7}, - &iotago.AccountUnlock{Reference: 7}, + &iotago.AccountUnlock{Reference: 6}, // foundry output (owned by account1AccountAddress) + &iotago.AccountUnlock{Reference: 6}, // foundry output (owned by account1AccountAddress) + &iotago.AccountUnlock{Reference: 6}, // foundry output (owned by account1AccountAddress) + &iotago.AccountUnlock{Reference: 6}, // foundry output (owned by account1AccountAddress) // nfts - &iotago.ReferenceUnlock{Reference: 7}, - &iotago.ReferenceUnlock{Reference: 6}, - &iotago.NFTUnlock{Reference: 13}, + &iotago.ReferenceUnlock{Reference: 6}, // NFT output (owned by ident3) + &iotago.ReferenceUnlock{Reference: 8}, // NFT output (owned by ident4) + &iotago.NFTUnlock{Reference: 15}, // basic output (owned by nft1ID) }, }, wantErr: nil, } }(), + + // fail - changed immutable account address unlock func() *test { accountAddr1 := tpkg.RandAccountAddress() _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() - _, ident2, _ := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) inFoundry := &iotago.FoundryOutput{ @@ -713,12 +869,10 @@ func TestNovaTransactionExecution(t *testing.T) { inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, inputIDs[1]: inFoundry, @@ -732,12 +886,10 @@ func TestNovaTransactionExecution(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: 100, - StateIndex: 1, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, outFoundry, @@ -768,19 +920,19 @@ func TestNovaTransactionExecution(t *testing.T) { wantErr: iotago.ErrNativeTokenSumUnbalanced, } }(), + + // ok - modify block issuer account func() *test { accountAddr1 := tpkg.RandAccountAddress() - _, ident1, _ := tpkg.RandEd25519Identity() - _, ident2, ident2AddressKeys := tpkg.RandEd25519Identity() + _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -788,8 +940,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, } @@ -808,9 +959,8 @@ func TestNovaTransactionExecution(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -818,8 +968,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, }, @@ -833,7 +982,7 @@ func TestNovaTransactionExecution(t *testing.T) { Slot: 110, } - sigs, err := transaction.Sign(ident2AddressKeys) + sigs, err := transaction.Sign(ident1AddressKeys) require.NoError(t, err) return &test{ @@ -852,19 +1001,19 @@ func TestNovaTransactionExecution(t *testing.T) { wantErr: nil, } }(), + + // ok - set block issuer expiry to max value func() *test { accountAddr1 := tpkg.RandAccountAddress() - _, ident1, _ := tpkg.RandEd25519Identity() - _, ident2, ident2AddressKeys := tpkg.RandEd25519Identity() + _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -872,8 +1021,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, } @@ -892,9 +1040,8 @@ func TestNovaTransactionExecution(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -902,8 +1049,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, }, @@ -917,7 +1063,7 @@ func TestNovaTransactionExecution(t *testing.T) { Slot: 110, } - sigs, err := transaction.Sign(ident2AddressKeys) + sigs, err := transaction.Sign(ident1AddressKeys) require.NoError(t, err) return &test{ @@ -936,6 +1082,8 @@ func TestNovaTransactionExecution(t *testing.T) { wantErr: nil, } }(), + + // fail - destroy block issuer account with expiry at slot with max value func() *test { accountAddr1 := tpkg.RandAccountAddress() @@ -945,9 +1093,8 @@ func TestNovaTransactionExecution(t *testing.T) { inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -955,8 +1102,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, } @@ -1011,6 +1157,8 @@ func TestNovaTransactionExecution(t *testing.T) { wantErr: iotago.ErrInvalidBlockIssuerTransition, } }(), + + // ok - destroy block issuer account func() *test { accountAddr1 := tpkg.RandAccountAddress() @@ -1020,9 +1168,8 @@ func TestNovaTransactionExecution(t *testing.T) { inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -1030,8 +1177,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, } @@ -1085,6 +1231,8 @@ func TestNovaTransactionExecution(t *testing.T) { wantErr: nil, } }(), + + // fail - destroy block issuer account without supplying BIC func() *test { accountAddr1 := tpkg.RandAccountAddress() @@ -1094,9 +1242,8 @@ func TestNovaTransactionExecution(t *testing.T) { inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -1104,8 +1251,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, } @@ -1150,6 +1296,8 @@ func TestNovaTransactionExecution(t *testing.T) { wantErr: iotago.ErrBlockIssuanceCreditInputRequired, } }(), + + // fail - modify block issuer without supplying BIC func() *test { accountAddr1 := tpkg.RandAccountAddress() @@ -1159,9 +1307,8 @@ func TestNovaTransactionExecution(t *testing.T) { inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -1169,8 +1316,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, } @@ -1184,9 +1330,8 @@ func TestNovaTransactionExecution(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{ BlockIssuerKeys: iotago.NewBlockIssuerKeys(), @@ -1194,8 +1339,7 @@ func TestNovaTransactionExecution(t *testing.T) { }, }, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, }, @@ -1356,7 +1500,7 @@ func runNovaTransactionExecutionTest(t *testing.T, test *txExecTest) { func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { - var defaultAmount iotago.BaseToken = OneMi + defaultAmount := OneIOTA tests := []*txExecTest{ // ok - restricted ed25519 address unlock @@ -1399,7 +1543,7 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { return &txExecTest{ name: "ok - restricted account address unlock", txBuilder: &txBuilder{ - ed25519AddrCnt: 2, + ed25519AddrCnt: 1, addressesFunc: func(ed25519Addresses []iotago.Address) []iotago.Address { accountAddress := tpkg.RandAccountAddress() return []iotago.Address{ @@ -1416,12 +1560,9 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { &iotago.AccountOutput{ Amount: defaultAmount, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 1, - StateMetadata: []byte("current state"), FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: nil, }, @@ -1436,16 +1577,12 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { }, outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { return iotago.TxEssenceOutputs{ - // the account unlock needs to be a state transition (governor doesn't work for account reference unlocks) &iotago.AccountOutput{ Amount: defaultAmount, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 2, - StateMetadata: []byte("next state"), FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: nil, }, @@ -1459,7 +1596,7 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { }, unlocksFunc: func(sigs []iotago.Signature, testAddresses []iotago.Address) iotago.Unlocks { return iotago.Unlocks{ - &iotago.SignatureUnlock{Signature: sigs[0]}, // account state controller unlock + &iotago.SignatureUnlock{Signature: sigs[0]}, // account unlock &iotago.AccountUnlock{Reference: 0}, } }, @@ -1468,39 +1605,37 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { } }(), - // ok - restricted NFT unlock + // ok - restricted anchor address unlock func() *txExecTest { return &txExecTest{ - name: "ok - restricted NFT unlock", + name: "ok - restricted anchor address unlock", txBuilder: &txBuilder{ ed25519AddrCnt: 2, addressesFunc: func(ed25519Addresses []iotago.Address) []iotago.Address { - nftAddress := tpkg.RandNFTAddress() + anchorAddress := tpkg.RandAnchorAddress() return []iotago.Address{ - nftAddress, + anchorAddress, &iotago.RestrictedAddress{ - Address: nftAddress, + Address: anchorAddress, AllowedCapabilities: iotago.AddressCapabilitiesBitMask{}, }, } }, inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { return []iotago.Output{ - // we add an output with a Ed25519 address to be able to check the NFT Unlock in the RestrictedAddress - &iotago.NFTOutput{ - Amount: defaultAmount, - NFTID: testAddresses[0].(*iotago.NFTAddress).NFTID(), - Conditions: iotago.NFTOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, - }, - Features: iotago.NFTOutputFeatures{ - &iotago.IssuerFeature{Address: ed25519Addresses[1]}, - }, - ImmutableFeatures: iotago.NFTOutputImmFeatures{ - &iotago.MetadataFeature{Data: []byte("immutable")}, + // we add an output with a Ed25519 address to be able to check the AnchorUnlock in the RestrictedAddress + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: testAddresses[0].(*iotago.AnchorAddress).AnchorID(), + StateIndex: 1, + StateMetadata: []byte("current state"), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, }, + Features: nil, }, - // owned by restricted NFT address + // owned by restricted anchor address &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -1511,14 +1646,88 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { }, outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { return iotago.TxEssenceOutputs{ - &iotago.NFTOutput{ - Amount: defaultAmount, - NFTID: testAddresses[0].(*iotago.NFTAddress).NFTID(), - Conditions: iotago.NFTOutputUnlockConditions{ + // the anchor unlock needs to be a state transition (governor doesn't work for anchor reference unlocks) + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: testAddresses[0].(*iotago.AnchorAddress).AnchorID(), + StateIndex: 2, + StateMetadata: []byte("next state"), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, + }, + Features: nil, + }, + &iotago.BasicOutput{ + Amount: totalInputAmount - defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, - Features: iotago.NFTOutputFeatures{ - &iotago.IssuerFeature{Address: ed25519Addresses[1]}, + }, + } + }, + unlocksFunc: func(sigs []iotago.Signature, testAddresses []iotago.Address) iotago.Unlocks { + return iotago.Unlocks{ + &iotago.SignatureUnlock{Signature: sigs[0]}, // anchor state controller unlock + &iotago.AnchorUnlock{Reference: 0}, + } + }, + }, + wantErr: nil, + } + }(), + + // ok - restricted NFT unlock + func() *txExecTest { + return &txExecTest{ + name: "ok - restricted NFT unlock", + txBuilder: &txBuilder{ + ed25519AddrCnt: 2, + addressesFunc: func(ed25519Addresses []iotago.Address) []iotago.Address { + nftAddress := tpkg.RandNFTAddress() + return []iotago.Address{ + nftAddress, + &iotago.RestrictedAddress{ + Address: nftAddress, + AllowedCapabilities: iotago.AddressCapabilitiesBitMask{}, + }, + } + }, + inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { + return []iotago.Output{ + // we add an output with a Ed25519 address to be able to check the NFT Unlock in the RestrictedAddress + &iotago.NFTOutput{ + Amount: defaultAmount, + NFTID: testAddresses[0].(*iotago.NFTAddress).NFTID(), + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + Features: iotago.NFTOutputFeatures{ + &iotago.IssuerFeature{Address: ed25519Addresses[1]}, + }, + ImmutableFeatures: iotago.NFTOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("immutable")}, + }, + }, + // owned by restricted NFT address + &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: testAddresses[1]}, + }, + }, + } + }, + outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { + return iotago.TxEssenceOutputs{ + &iotago.NFTOutput{ + Amount: defaultAmount, + NFTID: testAddresses[0].(*iotago.NFTAddress).NFTID(), + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + Features: iotago.NFTOutputFeatures{ + &iotago.IssuerFeature{Address: ed25519Addresses[1]}, &iotago.MetadataFeature{Data: []byte("some new metadata")}, }, ImmutableFeatures: iotago.NFTOutputImmFeatures{ @@ -1543,6 +1752,99 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { wantErr: nil, } }(), + + // ok - restricted multi address unlock + func() *txExecTest { + return &txExecTest{ + name: "ok - restricted multi address unlock", + txBuilder: &txBuilder{ + ed25519AddrCnt: 1, + addressesFunc: func(ed25519Addresses []iotago.Address) []iotago.Address { + nftAddress := tpkg.RandNFTAddress() + return []iotago.Address{ + nftAddress, + &iotago.RestrictedAddress{ + Address: &iotago.MultiAddress{ + Addresses: []*iotago.AddressWithWeight{ + { + Address: ed25519Addresses[0], + Weight: 1, + }, + { + Address: nftAddress, + Weight: 1, + }, + }, + Threshold: 2, + }, + AllowedCapabilities: iotago.AddressCapabilitiesBitMask{}, + }, + } + }, + inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { + return []iotago.Output{ + // we add an output with a Ed25519 address to be able to check the NFT Unlock in the RestrictedAddress multi address + &iotago.NFTOutput{ + Amount: defaultAmount, + NFTID: testAddresses[0].(*iotago.NFTAddress).NFTID(), + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + Features: iotago.NFTOutputFeatures{ + &iotago.IssuerFeature{Address: ed25519Addresses[0]}, + }, + ImmutableFeatures: iotago.NFTOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("immutable")}, + }, + }, + // owned by restricted multi address + &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: testAddresses[1]}, + }, + }, + } + }, + outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { + return iotago.TxEssenceOutputs{ + &iotago.NFTOutput{ + Amount: defaultAmount, + NFTID: testAddresses[0].(*iotago.NFTAddress).NFTID(), + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + Features: iotago.NFTOutputFeatures{ + &iotago.IssuerFeature{Address: ed25519Addresses[0]}, + &iotago.MetadataFeature{Data: []byte("some new metadata")}, + }, + ImmutableFeatures: iotago.NFTOutputImmFeatures{ + &iotago.MetadataFeature{Data: []byte("immutable")}, + }, + }, + &iotago.BasicOutput{ + Amount: totalInputAmount - defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + }, + } + }, + unlocksFunc: func(sigs []iotago.Signature, testAddresses []iotago.Address) iotago.Unlocks { + return iotago.Unlocks{ + &iotago.SignatureUnlock{Signature: sigs[0]}, // NFT unlock + &iotago.MultiUnlock{ + Unlocks: []iotago.Unlock{ + &iotago.ReferenceUnlock{Reference: 0}, + &iotago.NFTUnlock{Reference: 0}, + }, + }, + } + }, + }, + wantErr: nil, + } + }(), } for _, tt := range tests { runNovaTransactionExecutionTest(t, tt) @@ -1551,7 +1853,7 @@ func TestNovaTransactionExecution_RestrictedAddress(t *testing.T) { func TestNovaTransactionExecution_MultiAddress(t *testing.T) { - var defaultAmount iotago.BaseToken = OneMi + defaultAmount := OneIOTA tests := []*txExecTest{ // ok - threshold == cumulativeWeight (threshold reached) @@ -1931,10 +2233,10 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { } }(), - // ok - Account unlock (state transition) + // ok - Account unlock func() *txExecTest { return &txExecTest{ - name: "ok - Account unlock (state transition)", + name: "ok - Account unlock", txBuilder: &txBuilder{ ed25519AddrCnt: 2, addressesFunc: func(ed25519Addresses []iotago.Address) []iotago.Address { @@ -1963,12 +2265,9 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { &iotago.AccountOutput{ Amount: defaultAmount, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 1, - StateMetadata: []byte("current state"), FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: nil, }, @@ -1989,16 +2288,12 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { }, outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { return iotago.TxEssenceOutputs{ - // the account unlock needs to be a state transition (governor doesn't work for account reference unlocks) &iotago.AccountOutput{ Amount: defaultAmount, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 2, - StateMetadata: []byte("next state"), FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: nil, }, @@ -2058,7 +2353,7 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { } return iotago.Unlocks{ - &iotago.SignatureUnlock{Signature: sigs[0]}, // account state controller unlock + &iotago.SignatureUnlock{Signature: sigs[0]}, // account unlock &iotago.SignatureUnlock{Signature: sigs[1]}, // basic output unlock multiUnlock, } @@ -2068,17 +2363,152 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { } }(), - // fail - Account unlock (governance transition) + // ok - Anchor unlock (state transition) func() *txExecTest { return &txExecTest{ - name: "fail - Account unlock (governance transition)", + name: "ok - Anchor unlock (state transition)", txBuilder: &txBuilder{ ed25519AddrCnt: 2, addressesFunc: func(ed25519Addresses []iotago.Address) []iotago.Address { - accountAddress := tpkg.RandAccountAddress() + anchorAddress := tpkg.RandAnchorAddress() return []iotago.Address{ - accountAddress, - // ed25519 address + account address + anchorAddress, + // ed25519 address + anchor address + &iotago.MultiAddress{ + Addresses: []*iotago.AddressWithWeight{ + { + Address: ed25519Addresses[1], + Weight: 1, + }, + { + Address: anchorAddress, + Weight: 1, + }, + }, + Threshold: 2, + }, + } + }, + inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { + return []iotago.Output{ + // we add an output with a Ed25519 address to be able to check the AnchorUnlock in the MultiAddress + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: testAddresses[0].(*iotago.AnchorAddress).AnchorID(), + StateIndex: 1, + StateMetadata: []byte("current state"), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, + }, + Features: nil, + }, + &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[1]}, + }, + }, + // owned by ed25519 address + anchor address + &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: testAddresses[1]}, + }, + }, + } + }, + outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { + return iotago.TxEssenceOutputs{ + // the anchor unlock needs to be a state transition (governor doesn't work for anchor reference unlocks) + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: testAddresses[0].(*iotago.AnchorAddress).AnchorID(), + StateIndex: 2, + StateMetadata: []byte("next state"), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, + }, + Features: nil, + }, + &iotago.BasicOutput{ + Amount: totalInputAmount - defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + }, + } + }, + unlocksFunc: func(sigs []iotago.Signature, testAddresses []iotago.Address) iotago.Unlocks { + // this is a bit complicated in the test, because the addresses are generated randomly, + // but the MultiAddresses get sorted lexically, so we have to find out the correct order in the MultiUnlock. + + anchorAddress := testAddresses[0] + multiAddress := testAddresses[1].(*iotago.MultiAddress) + + // sort the addresses in the multi like the serializer will do + slices.SortFunc(multiAddress.Addresses, func(a *iotago.AddressWithWeight, b *iotago.AddressWithWeight) int { + return bytes.Compare(a.Address.ID(), b.Address.ID()) + }) + + // search the index of the anchor address in the multi address + foundAnchorAddressIndex := -1 + for idx, address := range multiAddress.Addresses { + if address.Address.Equal(anchorAddress) { + foundAnchorAddressIndex = idx + break + } + } + + var multiUnlock *iotago.MultiUnlock + + switch foundAnchorAddressIndex { + case -1: + require.FailNow(t, "anchor address not found in multi address") + + case 0: + multiUnlock = &iotago.MultiUnlock{ + Unlocks: []iotago.Unlock{ + &iotago.AnchorUnlock{Reference: 0}, + &iotago.ReferenceUnlock{Reference: 1}, + }, + } + + case 1: + multiUnlock = &iotago.MultiUnlock{ + Unlocks: []iotago.Unlock{ + &iotago.ReferenceUnlock{Reference: 1}, + &iotago.AnchorUnlock{Reference: 0}, + }, + } + + default: + require.FailNow(t, "unknown anchor address index found in multi address") + } + + return iotago.Unlocks{ + &iotago.SignatureUnlock{Signature: sigs[0]}, // anchor state controller unlock + &iotago.SignatureUnlock{Signature: sigs[1]}, // basic output unlock + multiUnlock, + } + }, + }, + wantErr: nil, + } + }(), + + // fail - Anchor unlock (governance transition) + func() *txExecTest { + return &txExecTest{ + name: "fail - Anchor unlock (governance transition)", + txBuilder: &txBuilder{ + ed25519AddrCnt: 2, + addressesFunc: func(ed25519Addresses []iotago.Address) []iotago.Address { + anchorAddress := tpkg.RandAnchorAddress() + return []iotago.Address{ + anchorAddress, + // ed25519 address + anchor address &iotago.MultiAddress{ Addresses: []*iotago.AddressWithWeight{ { @@ -2086,7 +2516,7 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { Weight: 1, }, { - Address: accountAddress, + Address: anchorAddress, Weight: 1, }, }, @@ -2096,14 +2526,13 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { }, inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { return []iotago.Output{ - // we add an output with a Ed25519 address to be able to check the AccountUnlock in the MultiAddress - &iotago.AccountOutput{ - Amount: defaultAmount, - AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 1, - StateMetadata: []byte("governance transition"), - FoundryCounter: 0, - Conditions: iotago.AccountOutputUnlockConditions{ + // we add an output with a Ed25519 address to be able to check the AnchorUnlock in the MultiAddress + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: testAddresses[0].(*iotago.AnchorAddress).AnchorID(), + StateIndex: 1, + StateMetadata: []byte("governance transition"), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, }, @@ -2115,7 +2544,7 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, }, - // owned by ed25519 address + account address + // owned by ed25519 address + anchor address &iotago.BasicOutput{ Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ @@ -2126,14 +2555,13 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { }, outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { return iotago.TxEssenceOutputs{ - // the account unlock needs to be a state transition (governor doesn't work for account reference unlocks) - &iotago.AccountOutput{ - Amount: defaultAmount, - AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 1, - StateMetadata: []byte("governance transition"), - FoundryCounter: 0, - Conditions: iotago.AccountOutputUnlockConditions{ + // the anchor unlock needs to be a state transition (governor doesn't work for anchor reference unlocks) + &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: testAddresses[0].(*iotago.AnchorAddress).AnchorID(), + StateIndex: 1, + StateMetadata: []byte("governance transition"), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[1]}, }, @@ -2151,7 +2579,7 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { // this is a bit complicated in the test, because the addresses are generated randomly, // but the MultiAddresses get sorted lexically, so we have to find out the correct order in the MultiUnlock. - accountAddress := testAddresses[0] + anchorAddress := testAddresses[0] multiAddress := testAddresses[1].(*iotago.MultiAddress) // sort the addresses in the multi like the serializer will do @@ -2159,25 +2587,25 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { return bytes.Compare(a.Address.ID(), b.Address.ID()) }) - // search the index of the account address in the multi address - foundAccountAddressIndex := -1 + // search the index of the anchor address in the multi address + foundAnchorAddressIndex := -1 for idx, address := range multiAddress.Addresses { - if address.Address.Equal(accountAddress) { - foundAccountAddressIndex = idx + if address.Address.Equal(anchorAddress) { + foundAnchorAddressIndex = idx break } } var multiUnlock *iotago.MultiUnlock - switch foundAccountAddressIndex { + switch foundAnchorAddressIndex { case -1: - require.FailNow(t, "account address not found in multi address") + require.FailNow(t, "anchor address not found in multi address") case 0: multiUnlock = &iotago.MultiUnlock{ Unlocks: []iotago.Unlock{ - &iotago.AccountUnlock{Reference: 0}, + &iotago.AnchorUnlock{Reference: 0}, &iotago.ReferenceUnlock{Reference: 1}, }, } @@ -2186,16 +2614,16 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { multiUnlock = &iotago.MultiUnlock{ Unlocks: []iotago.Unlock{ &iotago.ReferenceUnlock{Reference: 1}, - &iotago.AccountUnlock{Reference: 0}, + &iotago.AnchorUnlock{Reference: 0}, }, } default: - require.FailNow(t, "unknown account address index found in multi address") + require.FailNow(t, "unknown anchor address index found in multi address") } return iotago.Unlocks{ - &iotago.SignatureUnlock{Signature: sigs[1]}, // account governor unlock + &iotago.SignatureUnlock{Signature: sigs[1]}, // anchor governor unlock &iotago.SignatureUnlock{Signature: sigs[0]}, // basic output unlock multiUnlock, } @@ -2515,7 +2943,7 @@ func TestNovaTransactionExecution_MultiAddress(t *testing.T) { func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { - var defaultAmount iotago.BaseToken = OneMi + defaultAmount := OneIOTA // builds a transaction that burns native tokens burnNativeTokenTxBuilder := &txBuilder{ @@ -2571,12 +2999,9 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { Amount: defaultAmount, Mana: 0, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 0, - StateMetadata: []byte{}, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: iotago.AccountOutputFeatures{}, ImmutableFeatures: iotago.AccountOutputImmFeatures{}, @@ -2620,12 +3045,9 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { Amount: totalInputAmount - defaultAmount, Mana: totalInputMana, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 1, - StateMetadata: []byte{}, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: iotago.AccountOutputFeatures{}, ImmutableFeatures: iotago.AccountOutputImmFeatures{}, @@ -2681,12 +3103,9 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { Amount: defaultAmount, Mana: 0, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 0, - StateMetadata: []byte{}, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: iotago.AccountOutputFeatures{}, ImmutableFeatures: iotago.AccountOutputImmFeatures{}, @@ -2727,12 +3146,9 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { Amount: totalInputAmount - defaultAmount, Mana: totalInputMana, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 1, - StateMetadata: []byte{}, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: iotago.AccountOutputFeatures{}, ImmutableFeatures: iotago.AccountOutputImmFeatures{}, @@ -2759,20 +3175,52 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { unlocksFunc: func(sigs []iotago.Signature, testAddresses []iotago.Address) iotago.Unlocks { return iotago.Unlocks{ &iotago.SignatureUnlock{Signature: sigs[0]}, - &iotago.AccountUnlock{Reference: 0}, - &iotago.ReferenceUnlock{Reference: 0}, + &iotago.AccountUnlock{Reference: 0}, + &iotago.ReferenceUnlock{Reference: 0}, + } + }, + } + + // builds a transaction that burns mana + burnManaTxBuilder := &txBuilder{ + ed25519AddrCnt: 1, + inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { + return []iotago.Output{ + &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + }, + } + }, + outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { + return iotago.TxEssenceOutputs{ + &iotago.BasicOutput{ + Amount: totalInputAmount, + // burn mana + Mana: totalInputMana - 10, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, + }, + }, + } + }, + unlocksFunc: func(sigs []iotago.Signature, testAddresses []iotago.Address) iotago.Unlocks { + return iotago.Unlocks{ + &iotago.SignatureUnlock{Signature: sigs[0]}, } }, } - // builds a transaction that burns mana - burnManaTxBuilder := &txBuilder{ + // builds a transaction that destroys an account + destroyAccountTxBuilder := &txBuilder{ ed25519AddrCnt: 1, inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { return []iotago.Output{ - &iotago.BasicOutput{ + &iotago.AccountOutput{ Amount: defaultAmount, - Conditions: iotago.BasicOutputUnlockConditions{ + Conditions: iotago.AccountOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, }, @@ -2780,10 +3228,10 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { }, outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { return iotago.TxEssenceOutputs{ + // destroy the account output &iotago.BasicOutput{ Amount: totalInputAmount, - // burn mana - Mana: totalInputMana - 10, + Mana: totalInputMana, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, @@ -2797,14 +3245,14 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { }, } - // builds a transaction that destroys an account - destroyAccountTxBuilder := &txBuilder{ + // builds a transaction that destroys an anchor + destroyAnchorTxBuilder := &txBuilder{ ed25519AddrCnt: 1, inputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address) []iotago.Output { return []iotago.Output{ - &iotago.AccountOutput{ + &iotago.AnchorOutput{ Amount: defaultAmount, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[0]}, &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, }, @@ -2813,7 +3261,7 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { }, outputsFunc: func(ed25519Addresses []iotago.Address, testAddresses []iotago.Address, totalInputAmount iotago.BaseToken, totalInputMana iotago.Mana) iotago.TxEssenceOutputs { return iotago.TxEssenceOutputs{ - // destroy the account output + // destroy the anchor output &iotago.BasicOutput{ Amount: totalInputAmount, Mana: totalInputMana, @@ -2845,12 +3293,9 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { Amount: defaultAmount, Mana: 0, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 0, - StateMetadata: []byte{}, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: iotago.AccountOutputFeatures{}, ImmutableFeatures: iotago.AccountOutputImmFeatures{}, @@ -2880,12 +3325,9 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { Amount: totalInputAmount, Mana: totalInputMana, AccountID: testAddresses[0].(*iotago.AccountAddress).AccountID(), - StateIndex: 1, - StateMetadata: []byte{}, FoundryCounter: 1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ed25519Addresses[0]}, - &iotago.GovernorAddressUnlockCondition{Address: ed25519Addresses[0]}, + &iotago.AddressUnlockCondition{Address: ed25519Addresses[0]}, }, Features: iotago.AccountOutputFeatures{}, ImmutableFeatures: iotago.AccountOutputImmFeatures{}, @@ -3078,6 +3520,35 @@ func TestNovaTransactionExecution_TxCapabilities(t *testing.T) { } }(), + // ok - destroy anchor (destruction enabled) + func() *txExecTest { + return &txExecTest{ + name: "ok - destroy anchor (destruction enabled)", + txBuilder: destroyAnchorTxBuilder, + txPreSignHook: func(t *iotago.Transaction) { + t.Capabilities = iotago.TransactionCapabilitiesBitMaskWithCapabilities( + iotago.WithTransactionCanDestroyAnchorOutputs(true), + ) + }, + wantErr: nil, + } + }(), + + // fail - destroy anchor (destruction disabled) + func() *txExecTest { + return &txExecTest{ + name: "fail - destroy anchor (destruction disabled)", + txBuilder: destroyAnchorTxBuilder, + txPreSignHook: func(t *iotago.Transaction) { + t.Capabilities = iotago.TransactionCapabilitiesBitMaskWithCapabilities( + iotago.WithTransactionCanDoAnything(), + iotago.WithTransactionCanDestroyAnchorOutputs(false), + ) + }, + wantErr: iotago.ErrTxCapabilitiesAnchorDestructionNotAllowed, + } + }(), + // ok - destroy foundry (destruction enabled) func() *txExecTest { return &txExecTest{ @@ -3152,72 +3623,85 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr error } tests := []*test{ + // ok func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, ident2AddrKeys := tpkg.RandEd25519Identity() - inputIDs := tpkg.RandOutputIDs(8) + inputIDs := tpkg.RandOutputIDs(12) + accountIdent1 := iotago.AccountAddressFromOutputID(inputIDs[1]) + anchorIdent1 := iotago.AnchorAddressFromOutputID(inputIDs[2]) + nftIdent1 := tpkg.RandNFTAddress() + nftIdent2 := tpkg.RandNFTAddress() + + defaultAmount := OneIOTA inputs := vm.InputSet{ + // basic output to create a signature unlock (owned by ident1) inputIDs[0]: &iotago.BasicOutput{ - Amount: 100, + Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, }, - inputIDs[1]: &iotago.AccountOutput{ - Amount: 100, - AccountID: iotago.AccountID{}, // empty on purpose as validation should resolve - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + // basic output unlockable by sender as expired (owned by ident2) + inputIDs[1]: &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + &iotago.ExpirationUnlockCondition{ + ReturnAddress: ident2, + Slot: 5, + }, }, }, + // basic output not unlockable by sender as not expired (owned by ident1) inputIDs[2]: &iotago.BasicOutput{ - Amount: 100, + Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: accountIdent1}, + &iotago.AddressUnlockCondition{Address: ident1}, + &iotago.ExpirationUnlockCondition{ + ReturnAddress: ident2, + Slot: 30, + }, }, }, - inputIDs[3]: &iotago.NFTOutput{ - Amount: 100, - NFTID: nftIdent1.NFTID(), - Conditions: iotago.NFTOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: accountIdent1}, + + // account output that ownes the following outputs (owned by ident1) + inputIDs[3]: &iotago.AccountOutput{ + Amount: defaultAmount, + AccountID: iotago.AccountID{}, // empty on purpose as validation should resolve + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, }, }, + // basic output (owned by accountIdent1) inputIDs[4]: &iotago.BasicOutput{ - Amount: 100, + Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: nftIdent1}, + &iotago.AddressUnlockCondition{Address: accountIdent1}, }, }, - // unlockable by sender as expired - inputIDs[5]: &iotago.BasicOutput{ - Amount: 100, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: ident1}, - &iotago.ExpirationUnlockCondition{ - ReturnAddress: ident2, - Slot: 5, - }, + // NFT output (owned by accountIdent1) + inputIDs[5]: &iotago.NFTOutput{ + Amount: defaultAmount, + NFTID: nftIdent1.NFTID(), + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: accountIdent1}, }, }, - // not unlockable by sender as not expired + // basic output (owned by nftIdent1) inputIDs[6]: &iotago.BasicOutput{ - Amount: 100, + Amount: defaultAmount, Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: ident1}, - &iotago.ExpirationUnlockCondition{ - ReturnAddress: ident2, - Slot: 30, - }, + &iotago.AddressUnlockCondition{Address: nftIdent1}, }, }, + // foundry output (owned by accountIdent1) inputIDs[7]: &iotago.FoundryOutput{ - Amount: 100, + Amount: defaultAmount, SerialNumber: 0, TokenScheme: &iotago.SimpleTokenScheme{ MintedTokens: new(big.Int).SetInt64(100), @@ -3228,6 +3712,38 @@ func TestTxSemanticInputUnlocks(t *testing.T) { &iotago.ImmutableAccountUnlockCondition{Address: accountIdent1}, }, }, + + // anchor output that ownes the following outputs (owned by ident1) + inputIDs[8]: &iotago.AnchorOutput{ + Amount: defaultAmount, + AnchorID: iotago.AnchorID{}, // empty on purpose as validation should resolve + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + }, + // basic output (owned by anchorIdent1) + inputIDs[9]: &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: anchorIdent1}, + }, + }, + // NFT output (owned by anchorIdent1) + inputIDs[10]: &iotago.NFTOutput{ + Amount: defaultAmount, + NFTID: nftIdent2.NFTID(), + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: anchorIdent1}, + }, + }, + // basic output (owned by nftIdent2) + inputIDs[11]: &iotago.BasicOutput{ + Amount: defaultAmount, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: nftIdent2}, + }, + }, } creationSlot := iotago.SlotIndex(10) @@ -3240,10 +3756,17 @@ func TestTxSemanticInputUnlocks(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: 100, - AccountID: accountIdent1.AccountID(), - StateIndex: 1, + Amount: defaultAmount / 2, + AccountID: accountIdent1.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + }, + &iotago.AnchorOutput{ + Amount: defaultAmount / 2, + AnchorID: anchorIdent1.AnchorID(), + StateIndex: 1, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident1}, &iotago.GovernorAddressUnlockCondition{Address: ident1}, }, @@ -3269,19 +3792,25 @@ func TestTxSemanticInputUnlocks(t *testing.T) { API: testAPI, Transaction: transaction, Unlocks: iotago.Unlocks{ - &iotago.SignatureUnlock{Signature: sigs[0]}, - &iotago.ReferenceUnlock{Reference: 0}, - &iotago.AccountUnlock{Reference: 1}, - &iotago.AccountUnlock{Reference: 1}, - &iotago.NFTUnlock{Reference: 3}, - &iotago.SignatureUnlock{Signature: sigs[1]}, - &iotago.ReferenceUnlock{Reference: 0}, - &iotago.AccountUnlock{Reference: 1}, + &iotago.SignatureUnlock{Signature: sigs[0]}, // basic output (owned by ident1) + &iotago.SignatureUnlock{Signature: sigs[1]}, // basic output (owned by ident2) + &iotago.ReferenceUnlock{Reference: 0}, // basic output (owned by ident1) + &iotago.ReferenceUnlock{Reference: 0}, // account output (owned by ident1) + &iotago.AccountUnlock{Reference: 3}, // basic output (owned by accountIdent1) + &iotago.AccountUnlock{Reference: 3}, // NFT output (owned by accountIdent1) + &iotago.NFTUnlock{Reference: 5}, // basic output (owned by nftIdent1) + &iotago.AccountUnlock{Reference: 3}, // foundry output (owned by accountIdent1) + &iotago.ReferenceUnlock{Reference: 0}, // anchor output (owned by ident1) + &iotago.AnchorUnlock{Reference: 8}, // basic output (owned by anchorIdent1) + &iotago.AnchorUnlock{Reference: 8}, // NFT output (owned by anchorIdent1) + &iotago.NFTUnlock{Reference: 10}, // basic output (owned by nftIdent2 }, }, wantErr: nil, } }(), + + // fail - invalid signature func() *test { ident1Sk, ident1, _ := tpkg.RandEd25519Identity() _, _, ident2AddrKeys := tpkg.RandEd25519Identity() @@ -3322,6 +3851,8 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrEd25519SignatureInvalid, } }(), + + // fail - should contain reference unlock func() *test { _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) @@ -3365,6 +3896,8 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrInvalidInputUnlock, } }(), + + // fail - should contain account unlock func() *test { _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) @@ -3375,8 +3908,7 @@ func TestTxSemanticInputUnlocks(t *testing.T) { Amount: 100, AccountID: iotago.AccountID{}, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, inputIDs[1]: &iotago.BasicOutput{ @@ -3411,6 +3943,56 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrInvalidInputUnlock, } }(), + + // fail - should contain anchor unlock + func() *test { + _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() + inputIDs := tpkg.RandOutputIDs(2) + + anchorIdent1 := iotago.AnchorAddressFromOutputID(inputIDs[0]) + inputs := vm.InputSet{ + inputIDs[0]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: iotago.AnchorID{}, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + }, + inputIDs[1]: &iotago.BasicOutput{ + Amount: 100, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: anchorIdent1}, + }, + }, + } + + transaction := &iotago.Transaction{API: testAPI, TransactionEssence: &iotago.TransactionEssence{ + Inputs: inputIDs.UTXOInputs(), + }} + + sigs, err := transaction.Sign(ident1AddressKeys) + require.NoError(t, err) + + return &test{ + name: "fail - should contain anchor unlock", + vmParams: &vm.Params{ + API: testAPI, + }, + resolvedInputs: vm.ResolvedInputs{InputSet: inputs}, + tx: &iotago.SignedTransaction{ + API: testAPI, + Transaction: transaction, + Unlocks: iotago.Unlocks{ + &iotago.SignatureUnlock{Signature: sigs[0]}, + &iotago.ReferenceUnlock{Reference: 0}, + }, + }, + wantErr: iotago.ErrInvalidInputUnlock, + } + }(), + + // fail - should contain NFT unlock func() *test { _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) @@ -3456,6 +4038,8 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrInvalidInputUnlock, } }(), + + // fail - circular NFT unlock func() *test { inputIDs := tpkg.RandOutputIDs(2) @@ -3501,6 +4085,8 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrInvalidInputUnlock, } }(), + + // fail - sender can not unlock yet func() *test { _, ident1, _ := tpkg.RandEd25519Identity() _, ident2, ident2AddressKeys := tpkg.RandEd25519Identity() @@ -3546,6 +4132,8 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrExpirationConditionUnlockFailed, } }(), + + // fail - receiver can not unlock anymore func() *test { _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() _, ident2, _ := tpkg.RandEd25519Identity() @@ -3591,6 +4179,8 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrEd25519PubKeyAndAddrMismatch, } }(), + + // fail - referencing other account unlocked by source account func() *test { _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(3) @@ -3607,8 +4197,7 @@ func TestTxSemanticInputUnlocks(t *testing.T) { Amount: 100, AccountID: accountAddr1.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, // owned by account1 @@ -3616,8 +4205,7 @@ func TestTxSemanticInputUnlocks(t *testing.T) { Amount: 100, AccountID: accountAddr2.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: accountAddr1}, - &iotago.GovernorAddressUnlockCondition{Address: accountAddr1}, + &iotago.AddressUnlockCondition{Address: accountAddr1}, }, }, // owned by account1 @@ -3625,8 +4213,7 @@ func TestTxSemanticInputUnlocks(t *testing.T) { Amount: 100, AccountID: accountAddr3.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: accountAddr1}, - &iotago.GovernorAddressUnlockCondition{Address: accountAddr1}, + &iotago.AddressUnlockCondition{Address: accountAddr1}, }, }, } @@ -3657,18 +4244,88 @@ func TestTxSemanticInputUnlocks(t *testing.T) { wantErr: iotago.ErrInvalidInputUnlock, } }(), + + // fail - referencing other anchor unlocked by source anchor + func() *test { + _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() + inputIDs := tpkg.RandOutputIDs(3) + + var ( + anchorAddr1 = tpkg.RandAnchorAddress() + anchorAddr2 = tpkg.RandAnchorAddress() + anchorAddr3 = tpkg.RandAnchorAddress() + ) + + inputs := vm.InputSet{ + // owned by ident1 + inputIDs[0]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorAddr1.AnchorID(), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + }, + // owned by anchor1 + inputIDs[1]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorAddr2.AnchorID(), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: anchorAddr1}, + &iotago.GovernorAddressUnlockCondition{Address: anchorAddr1}, + }, + }, + // owned by anchor1 + inputIDs[2]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorAddr3.AnchorID(), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: anchorAddr1}, + &iotago.GovernorAddressUnlockCondition{Address: anchorAddr1}, + }, + }, + } + + transaction := &iotago.Transaction{API: testAPI, TransactionEssence: &iotago.TransactionEssence{ + Inputs: inputIDs.UTXOInputs(), + }} + + sigs, err := transaction.Sign(ident1AddressKeys) + require.NoError(t, err) + + return &test{ + name: "fail - referencing other anchor unlocked by source anchor", + vmParams: &vm.Params{ + API: testAPI, + }, + resolvedInputs: vm.ResolvedInputs{InputSet: inputs}, + tx: &iotago.SignedTransaction{ + API: testAPI, + Transaction: transaction, + Unlocks: iotago.Unlocks{ + &iotago.SignatureUnlock{Signature: sigs[0]}, + &iotago.AnchorUnlock{Reference: 0}, + // error, should be 0, because anchor3 is unlocked by anchor1, not anchor2 + &iotago.AnchorUnlock{Reference: 1}, + }, + }, + wantErr: iotago.ErrInvalidInputUnlock, + } + }(), + + // fail - anchor output not state transitioning func() *test { _, ident1, _ := tpkg.RandEd25519Identity() _, ident2, ident2AddressKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) - accountAddr1 := tpkg.RandAccountAddress() + anchorAddr1 := tpkg.RandAnchorAddress() inputs := vm.InputSet{ - inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - AccountID: accountAddr1.AccountID(), - Conditions: iotago.AccountOutputUnlockConditions{ + inputIDs[0]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorAddr1.AnchorID(), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident1}, &iotago.GovernorAddressUnlockCondition{Address: ident2}, }, @@ -3676,7 +4333,7 @@ func TestTxSemanticInputUnlocks(t *testing.T) { inputIDs[1]: &iotago.BasicOutput{ Amount: 100, Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: accountAddr1}, + &iotago.AddressUnlockCondition{Address: anchorAddr1}, }, }, } @@ -3687,10 +4344,10 @@ func TestTxSemanticInputUnlocks(t *testing.T) { Inputs: inputIDs.UTXOInputs(), }, Outputs: iotago.TxEssenceOutputs{ - &iotago.AccountOutput{ - Amount: 100, - AccountID: accountAddr1.AccountID(), - Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorAddr1.AnchorID(), + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: ident1}, &iotago.GovernorAddressUnlockCondition{Address: ident2}, }, @@ -3702,7 +4359,7 @@ func TestTxSemanticInputUnlocks(t *testing.T) { require.NoError(t, err) return &test{ - name: "fail - account output not state transitioning", + name: "fail - anchor output not state transitioning", vmParams: &vm.Params{ API: testAPI, }, @@ -3712,17 +4369,18 @@ func TestTxSemanticInputUnlocks(t *testing.T) { Transaction: transaction, Unlocks: iotago.Unlocks{ &iotago.SignatureUnlock{Signature: sigs[0]}, - &iotago.AccountUnlock{Reference: 0}, + &iotago.AnchorUnlock{Reference: 0}, }, }, wantErr: iotago.ErrInvalidInputUnlock, } }(), + + // fail - wrong unlock for foundry func() *test { accountAddr1 := tpkg.RandAccountAddress() _, ident1, ident1AddressKeys := tpkg.RandEd25519Identity() - _, ident2, _ := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) foundryOutput := &iotago.FoundryOutput{ @@ -3740,12 +4398,10 @@ func TestTxSemanticInputUnlocks(t *testing.T) { inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - StateIndex: 0, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, inputIDs[1]: foundryOutput, @@ -3758,12 +4414,10 @@ func TestTxSemanticInputUnlocks(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: 100, - StateIndex: 1, - AccountID: accountAddr1.AccountID(), + Amount: 100, + AccountID: accountAddr1.AccountID(), Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident2}, + &iotago.AddressUnlockCondition{Address: ident1}, }, }, foundryOutput, @@ -3815,6 +4469,7 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr error } tests := []*test{ + // ok func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, ident2AddrKeys := tpkg.RandEd25519Identity() @@ -3908,6 +4563,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: nil, } }(), + + // ok - more storage deposit returned via more outputs func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, _ := tpkg.RandEd25519Identity() @@ -3974,6 +4631,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: nil, } }(), + + // fail - unbalanced, more on output than input func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) @@ -4021,6 +4680,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: iotago.ErrInputOutputSumMismatch, } }(), + + // fail - unbalanced, more on input than output func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) @@ -4068,6 +4729,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: iotago.ErrInputOutputSumMismatch, } }(), + + // fail - return not fulfilled func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, _ := tpkg.RandEd25519Identity() @@ -4131,6 +4794,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: iotago.ErrReturnAmountNotFulFilled, } }(), + + // fail - storage deposit return not basic output func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, _ := tpkg.RandEd25519Identity() @@ -4188,6 +4853,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: iotago.ErrReturnAmountNotFulFilled, } }(), + + // fail - storage deposit return has additional unlocks func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, _ := tpkg.RandEd25519Identity() @@ -4249,6 +4916,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: iotago.ErrReturnAmountNotFulFilled, } }(), + + // fail - storage deposit return has feature func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, _ := tpkg.RandEd25519Identity() @@ -4309,6 +4978,8 @@ func TestTxSemanticDeposit(t *testing.T) { wantErr: iotago.ErrReturnAmountNotFulFilled, } }(), + + // fail - storage deposit return has native tokens func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, ident2, _ := tpkg.RandEd25519Identity() @@ -4432,6 +5103,7 @@ func TestTxSemanticNativeTokens(t *testing.T) { wantErr error } tests := []*test{ + // ok func() *test { inputIDs := tpkg.RandOutputIDs(2) @@ -4500,6 +5172,8 @@ func TestTxSemanticNativeTokens(t *testing.T) { wantErr: nil, } }(), + + // ok - consolidate native token (same type) func() *test { inputIDs := tpkg.RandOutputIDs(iotago.MaxInputsCount) nativeToken := tpkg.RandNativeTokenFeature() @@ -4555,6 +5229,8 @@ func TestTxSemanticNativeTokens(t *testing.T) { wantErr: nil, } }(), + + // ok - most possible tokens in a tx func() *test { inputIDs := tpkg.RandOutputIDs(iotago.MaxInputsCount) @@ -4611,6 +5287,8 @@ func TestTxSemanticNativeTokens(t *testing.T) { wantErr: nil, } }(), + + // fail - unbalanced on output func() *test { inputIDs := tpkg.RandOutputIDs(1) @@ -4664,6 +5342,8 @@ func TestTxSemanticNativeTokens(t *testing.T) { wantErr: iotago.ErrNativeTokenSumUnbalanced, } }(), + + // fail - unbalanced with unrelated foundry in term of new output tokens func() *test { inputIDs := tpkg.RandOutputIDs(3) @@ -4762,9 +5442,12 @@ func TestTxSemanticOutputsSender(t *testing.T) { wantErr error } tests := []*test{ + // ok func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() - inputIDs := tpkg.RandOutputIDs(2) + inputIDs := tpkg.RandOutputIDs(4) + accountAddr := tpkg.RandAccountAddress() + anchorAddr := tpkg.RandAnchorAddress() nftAddr := tpkg.RandNFTAddress() inputs := vm.InputSet{ @@ -4774,7 +5457,22 @@ func TestTxSemanticOutputsSender(t *testing.T) { &iotago.AddressUnlockCondition{Address: ident1}, }, }, - inputIDs[1]: &iotago.NFTOutput{ + inputIDs[1]: &iotago.AccountOutput{ + Amount: 100, + AccountID: accountAddr.AccountID(), + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + }, + inputIDs[2]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorAddr.AnchorID(), + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + }, + inputIDs[3]: &iotago.NFTOutput{ Amount: 100, NFTID: nftAddr.NFTID(), Conditions: iotago.NFTOutputUnlockConditions{ @@ -4788,66 +5486,52 @@ func TestTxSemanticOutputsSender(t *testing.T) { TransactionEssence: &iotago.TransactionEssence{ Inputs: inputIDs.UTXOInputs(), }, - Outputs: iotago.TxEssenceOutputs{ - // sender is an Ed25519 address - &iotago.BasicOutput{ - Amount: 1337, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - }, - Features: iotago.BasicOutputFeatures{ - &iotago.SenderFeature{Address: ident1}, - }, - }, - &iotago.AccountOutput{ - Amount: 1337, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, - }, - Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: ident1}, - }, - }, - &iotago.NFTOutput{ - Amount: 1337, - Conditions: iotago.NFTOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - }, - Features: iotago.NFTOutputFeatures{ - &iotago.SenderFeature{Address: ident1}, - }, - }, - // sender is an NFT address - &iotago.BasicOutput{ - Amount: 1337, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - }, - Features: iotago.BasicOutputFeatures{ - &iotago.SenderFeature{Address: nftAddr}, - }, - }, - &iotago.AccountOutput{ - Amount: 1337, - Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident1}, - &iotago.GovernorAddressUnlockCondition{Address: ident1}, - }, - Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: nftAddr}, - }, - }, - &iotago.NFTOutput{ - Amount: 1337, - Conditions: iotago.NFTOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, - }, - Features: iotago.NFTOutputFeatures{ - &iotago.SenderFeature{Address: nftAddr}, - }, - }, - }, + Outputs: func() iotago.TxEssenceOutputs { + outputs := make(iotago.TxEssenceOutputs, 0) + for _, sender := range []iotago.Address{ident1, accountAddr, anchorAddr, nftAddr} { + outputs = append(outputs, &iotago.BasicOutput{ + Amount: 1337, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + }, + Features: iotago.BasicOutputFeatures{ + &iotago.SenderFeature{Address: sender}, + }, + }) + + outputs = append(outputs, &iotago.AccountOutput{ + Amount: 1337, + Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + Features: iotago.AccountOutputFeatures{ + &iotago.SenderFeature{Address: sender}, + }, + }) + + outputs = append(outputs, &iotago.AnchorOutput{ + Amount: 1337, + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + Features: iotago.AnchorOutputFeatures{ + &iotago.SenderFeature{Address: sender}, + }, + }) + + outputs = append(outputs, &iotago.NFTOutput{ + Amount: 1337, + Conditions: iotago.NFTOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: tpkg.RandEd25519Address()}, + }, + Features: iotago.NFTOutputFeatures{ + &iotago.SenderFeature{Address: sender}, + }, + }) + } + return outputs + }(), } sigs, err := transaction.Sign(ident1AddrKeys) require.NoError(t, err) @@ -4864,11 +5548,15 @@ func TestTxSemanticOutputsSender(t *testing.T) { Unlocks: iotago.Unlocks{ &iotago.SignatureUnlock{Signature: sigs[0]}, &iotago.ReferenceUnlock{Reference: 0}, + &iotago.ReferenceUnlock{Reference: 0}, + &iotago.ReferenceUnlock{Reference: 0}, }, }, wantErr: nil, } }(), + + // fail - sender not unlocked func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) @@ -4918,18 +5606,20 @@ func TestTxSemanticOutputsSender(t *testing.T) { wantErr: iotago.ErrSenderFeatureNotUnlocked, } }(), + + // fail - sender not unlocked due to governance transition func() *test { _, stateController, _ := tpkg.RandEd25519Identity() _, governor, governorAddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) - accountAddr := tpkg.RandAccountAddress() - accountID := accountAddr.AccountID() + anchorAddr := tpkg.RandAnchorAddress() + anchorID := anchorAddr.AnchorID() inputs := vm.InputSet{ - inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + inputIDs[0]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -4942,15 +5632,15 @@ func TestTxSemanticOutputsSender(t *testing.T) { Inputs: inputIDs.UTXOInputs(), }, Outputs: iotago.TxEssenceOutputs{ - &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, - Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: accountAddr}, + Features: iotago.AnchorOutputFeatures{ + &iotago.SenderFeature{Address: anchorAddr}, }, }, }, @@ -4974,20 +5664,22 @@ func TestTxSemanticOutputsSender(t *testing.T) { wantErr: iotago.ErrSenderFeatureNotUnlocked, } }(), + + // ok - anchor addr unlocked with state transition func() *test { _, stateController, stateControllerAddrKeys := tpkg.RandEd25519Identity() _, governor, _ := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) - accountAddr := tpkg.RandAccountAddress() - accountID := accountAddr.AccountID() + anchorAddr := tpkg.RandAnchorAddress() + anchorID := anchorAddr.AnchorID() currentStateIndex := uint32(1) inputs := vm.InputSet{ - inputIDs[0]: &iotago.AccountOutput{ + inputIDs[0]: &iotago.AnchorOutput{ Amount: 100, - AccountID: accountID, + AnchorID: anchorID, StateIndex: currentStateIndex, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5000,16 +5692,16 @@ func TestTxSemanticOutputsSender(t *testing.T) { Inputs: inputIDs.UTXOInputs(), }, Outputs: iotago.TxEssenceOutputs{ - &iotago.AccountOutput{ + &iotago.AnchorOutput{ Amount: 100, - AccountID: accountID, + AnchorID: anchorID, StateIndex: currentStateIndex + 1, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, - Features: iotago.AccountOutputFeatures{ - &iotago.SenderFeature{Address: accountAddr}, + Features: iotago.AnchorOutputFeatures{ + &iotago.SenderFeature{Address: anchorAddr}, }, }, }, @@ -5018,7 +5710,7 @@ func TestTxSemanticOutputsSender(t *testing.T) { require.NoError(t, err) return &test{ - name: "ok - account addr unlocked with state transition", + name: "ok - anchor addr unlocked with state transition", vmParams: &vm.Params{ API: testAPI, }, @@ -5033,18 +5725,20 @@ func TestTxSemanticOutputsSender(t *testing.T) { wantErr: nil, } }(), + + // ok - sender is governor address func() *test { _, stateController, _ := tpkg.RandEd25519Identity() _, governor, governorAddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) - accountAddr := tpkg.RandAccountAddress() - accountID := accountAddr.AccountID() + anchorAddr := tpkg.RandAnchorAddress() + anchorID := anchorAddr.AnchorID() inputs := vm.InputSet{ - inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + inputIDs[0]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5057,14 +5751,14 @@ func TestTxSemanticOutputsSender(t *testing.T) { Inputs: inputIDs.UTXOInputs(), }, Outputs: iotago.TxEssenceOutputs{ - &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, - Features: iotago.AccountOutputFeatures{ + Features: iotago.AnchorOutputFeatures{ &iotago.SenderFeature{Address: governor}, }, }, @@ -5089,6 +5783,8 @@ func TestTxSemanticOutputsSender(t *testing.T) { wantErr: nil, } }(), + + // ok - multi address in sender feature func() *test { _, ident1, ident1Keys := tpkg.RandEd25519Identity() _, ident2, ident2Keys := tpkg.RandEd25519Identity() @@ -5165,6 +5861,8 @@ func TestTxSemanticOutputsSender(t *testing.T) { wantErr: nil, } }(), + + // ok - restricted multi address in sender and issuer feature func() *test { _, ident1, ident1Keys := tpkg.RandEd25519Identity() _, ident2, ident2Keys := tpkg.RandEd25519Identity() @@ -5274,19 +5972,20 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { wantErr error } tests := []*test{ + // fail - issuer not unlocked due to governance transition func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, stateController, _ := tpkg.RandEd25519Identity() _, governor, governorAddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) - accountAddr := tpkg.RandAccountAddress() - accountID := accountAddr.AccountID() + anchorAddr := tpkg.RandAnchorAddress() + anchorID := anchorAddr.AnchorID() inputs := vm.InputSet{ - inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + inputIDs[0]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5305,10 +6004,10 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { Inputs: inputIDs.UTXOInputs(), }, Outputs: iotago.TxEssenceOutputs{ - &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5319,7 +6018,7 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { &iotago.AddressUnlockCondition{Address: ident1}, }, ImmutableFeatures: iotago.NFTOutputImmFeatures{ - &iotago.IssuerFeature{Address: accountAddr}, + &iotago.IssuerFeature{Address: anchorAddr}, }, }, }, @@ -5344,24 +6043,26 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { wantErr: iotago.ErrIssuerFeatureNotUnlocked, } }(), + + // ok - issuer unlocked with state transition func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, stateController, stateControllerAddrKeys := tpkg.RandEd25519Identity() _, governor, _ := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) - accountAddr := tpkg.RandAccountAddress() - accountID := accountAddr.AccountID() + anchorAddr := tpkg.RandAnchorAddress() + anchorID := anchorAddr.AnchorID() currentStateIndex := uint32(1) nftAddr := tpkg.RandNFTAddress() inputs := vm.InputSet{ - // possible issuers: accountAddr, stateController, nftAddr, ident1 - inputIDs[0]: &iotago.AccountOutput{ + // possible issuers: anchorAddr, stateController, nftAddr, ident1 + inputIDs[0]: &iotago.AnchorOutput{ Amount: 100, - AccountID: accountID, + AnchorID: anchorID, StateIndex: currentStateIndex, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5381,12 +6082,12 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { Inputs: inputIDs.UTXOInputs(), }, Outputs: iotago.TxEssenceOutputs{ - // transitioned account + nft - &iotago.AccountOutput{ + // transitioned anchor + nft + &iotago.AnchorOutput{ Amount: 100, - AccountID: accountID, + AnchorID: anchorID, StateIndex: currentStateIndex + 1, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5398,24 +6099,24 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { &iotago.AddressUnlockCondition{Address: ident1}, }, }, - // issuer is accountAddr + // issuer is anchorAddr &iotago.NFTOutput{ Amount: 100, Conditions: iotago.NFTOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, }, ImmutableFeatures: iotago.NFTOutputImmFeatures{ - &iotago.IssuerFeature{Address: accountAddr}, + &iotago.IssuerFeature{Address: anchorAddr}, }, }, - &iotago.AccountOutput{ + &iotago.AnchorOutput{ Amount: 100, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, - ImmutableFeatures: iotago.AccountOutputImmFeatures{ - &iotago.IssuerFeature{Address: accountAddr}, + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ + &iotago.IssuerFeature{Address: anchorAddr}, }, }, // issuer is stateController @@ -5428,13 +6129,13 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { &iotago.IssuerFeature{Address: stateController}, }, }, - &iotago.AccountOutput{ + &iotago.AnchorOutput{ Amount: 100, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, - ImmutableFeatures: iotago.AccountOutputImmFeatures{ + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ &iotago.IssuerFeature{Address: stateController}, }, }, @@ -5448,13 +6149,13 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { &iotago.IssuerFeature{Address: nftAddr}, }, }, - &iotago.AccountOutput{ + &iotago.AnchorOutput{ Amount: 100, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, - ImmutableFeatures: iotago.AccountOutputImmFeatures{ + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ &iotago.IssuerFeature{Address: nftAddr}, }, }, @@ -5468,13 +6169,13 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { &iotago.IssuerFeature{Address: ident1}, }, }, - &iotago.AccountOutput{ + &iotago.AnchorOutput{ Amount: 100, - Conditions: iotago.AccountOutputUnlockConditions{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, - ImmutableFeatures: iotago.AccountOutputImmFeatures{ + ImmutableFeatures: iotago.AnchorOutputImmFeatures{ &iotago.IssuerFeature{Address: ident1}, }, }, @@ -5500,19 +6201,21 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { wantErr: nil, } }(), + + // ok - issuer is the governor func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() _, stateController, _ := tpkg.RandEd25519Identity() _, governor, governorAddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(2) - accountAddr := tpkg.RandAccountAddress() - accountID := accountAddr.AccountID() + anchorAddr := tpkg.RandAnchorAddress() + anchorID := anchorAddr.AnchorID() inputs := vm.InputSet{ - inputIDs[0]: &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + inputIDs[0]: &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5531,10 +6234,10 @@ func TestTxSemanticOutputsIssuer(t *testing.T) { Inputs: inputIDs.UTXOInputs(), }, Outputs: iotago.TxEssenceOutputs{ - &iotago.AccountOutput{ - Amount: 100, - AccountID: accountID, - Conditions: iotago.AccountOutputUnlockConditions{ + &iotago.AnchorOutput{ + Amount: 100, + AnchorID: anchorID, + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.StateControllerAddressUnlockCondition{Address: stateController}, &iotago.GovernorAddressUnlockCondition{Address: governor}, }, @@ -5593,6 +6296,7 @@ func TestTxSemanticTimelocks(t *testing.T) { wantErr error } tests := []*test{ + // ok func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) @@ -5634,6 +6338,8 @@ func TestTxSemanticTimelocks(t *testing.T) { wantErr: nil, } }(), + + // fail - timelock not expired func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) @@ -5651,48 +6357,6 @@ func TestTxSemanticTimelocks(t *testing.T) { } creationSlot := iotago.SlotIndex(10) - transaction := &iotago.Transaction{API: testAPI, TransactionEssence: &iotago.TransactionEssence{Inputs: inputIDs.UTXOInputs(), CreationSlot: 10}} - sigs, err := transaction.Sign(ident1AddrKeys) - require.NoError(t, err) - - return &test{ - name: "fail - timelock not expired", - vmParams: &vm.Params{ - API: testAPI, - }, - resolvedInputs: vm.ResolvedInputs{ - InputSet: inputs, - CommitmentInput: &iotago.Commitment{ - Slot: creationSlot, - }, - }, - tx: &iotago.SignedTransaction{ - API: testAPI, - Transaction: transaction, - Unlocks: iotago.Unlocks{ - &iotago.SignatureUnlock{Signature: sigs[0]}, - }, - }, - wantErr: iotago.ErrTimelockNotExpired, - } - }(), - func() *test { - _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() - inputIDs := tpkg.RandOutputIDs(1) - - inputs := vm.InputSet{ - inputIDs[0]: &iotago.BasicOutput{ - Amount: 100, - Conditions: iotago.BasicOutputUnlockConditions{ - &iotago.AddressUnlockCondition{Address: ident1}, - &iotago.TimelockUnlockCondition{ - Slot: 1337, - }, - }, - }, - } - - creationSlot := iotago.SlotIndex(666) transaction := &iotago.Transaction{API: testAPI, TransactionEssence: &iotago.TransactionEssence{Inputs: inputIDs.UTXOInputs(), CreationSlot: creationSlot}} sigs, err := transaction.Sign(ident1AddrKeys) require.NoError(t, err) @@ -5718,6 +6382,8 @@ func TestTxSemanticTimelocks(t *testing.T) { wantErr: iotago.ErrTimelockNotExpired, } }(), + + // fail - no commitment input for timelock func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDs(1) @@ -5781,13 +6447,14 @@ func TestTxSemanticMana(t *testing.T) { wantErr error } tests := []*test{ + // ok - stored Mana only without allotment" func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDsWithCreationSlot(10, 1) inputs := vm.InputSet{ inputIDs[0]: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Mana: iotago.MaxMana, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, @@ -5803,7 +6470,7 @@ func TestTxSemanticMana(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Mana: func() iotago.Mana { var creationSlot iotago.SlotIndex = 10 targetSlot := 10 + 100*testProtoParams.ParamEpochDurationInSlots() @@ -5847,13 +6514,15 @@ func TestTxSemanticMana(t *testing.T) { wantErr: nil, } }(), + + // ok - stored and allotted func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDsWithCreationSlot(10, 1) inputs := vm.InputSet{ inputIDs[0]: &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Mana: iotago.MaxMana, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident1}, @@ -5872,7 +6541,7 @@ func TestTxSemanticMana(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.BasicOutput{ - Amount: OneMi, + Amount: OneIOTA, Mana: func() iotago.Mana { var createdSlot iotago.SlotIndex = 10 targetSlot := 10 + 100*testProtoParams.ParamEpochDurationInSlots() @@ -5917,6 +6586,8 @@ func TestTxSemanticMana(t *testing.T) { wantErr: nil, } }(), + + // fail - input created after tx func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDsWithCreationSlot(20, 1) @@ -5966,6 +6637,8 @@ func TestTxSemanticMana(t *testing.T) { wantErr: iotago.ErrInputCreationAfterTxCreation, } }(), + + // ok - input created in same slot as tx func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDsWithCreationSlot(15, 1) @@ -6015,6 +6688,8 @@ func TestTxSemanticMana(t *testing.T) { wantErr: nil, } }(), + + // fail - mana overflow on the input side sum func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDsWithCreationSlot(15, 2) @@ -6072,6 +6747,8 @@ func TestTxSemanticMana(t *testing.T) { wantErr: iotago.ErrManaOverflow, } }(), + + // fail - mana overflow on the output side sum func() *test { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() inputIDs := tpkg.RandOutputIDsWithCreationSlot(15, 1) @@ -6153,15 +6830,12 @@ func TestManaRewardsClaimingStaking(t *testing.T) { inputIDs := tpkg.RandOutputIDs(1) inputs := vm.InputSet{ inputIDs[0]: &iotago.AccountOutput{ - Amount: OneMi * 10, + Amount: OneIOTA * 10, AccountID: accountIdent.AccountID(), - StateIndex: 1, - StateMetadata: nil, Mana: 0, FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident}, - &iotago.GovernorAddressUnlockCondition{Address: ident}, + &iotago.AddressUnlockCondition{Address: ident}, }, Features: iotago.AccountOutputFeatures{ &iotago.StakingFeature{ @@ -6181,19 +6855,16 @@ func TestManaRewardsClaimingStaking(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.AccountOutput{ - Amount: OneMi * 5, + Amount: OneIOTA * 5, AccountID: accountIdent.AccountID(), - StateIndex: 2, - StateMetadata: nil, FoundryCounter: 0, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: ident}, - &iotago.GovernorAddressUnlockCondition{Address: ident}, + &iotago.AddressUnlockCondition{Address: ident}, }, Features: nil, }, &iotago.BasicOutput{ - Amount: OneMi * 5, + Amount: OneIOTA * 5, Mana: manaRewardAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: accountIdent}, @@ -6236,8 +6907,8 @@ func TestManaRewardsClaimingDelegation(t *testing.T) { inputIDs := tpkg.RandOutputIDs(1) inputs := vm.InputSet{ inputIDs[0]: &iotago.DelegationOutput{ - Amount: OneMi * 10, - DelegatedAmount: OneMi * 10, + Amount: OneIOTA * 10, + DelegatedAmount: OneIOTA * 10, DelegationID: iotago.EmptyDelegationID(), ValidatorAddress: &iotago.AccountAddress{}, StartEpoch: currentEpoch, @@ -6258,7 +6929,7 @@ func TestManaRewardsClaimingDelegation(t *testing.T) { }, Outputs: iotago.TxEssenceOutputs{ &iotago.BasicOutput{ - Amount: OneMi * 10, + Amount: OneIOTA * 10, Mana: manaRewardAmount, Conditions: iotago.BasicOutputUnlockConditions{ &iotago.AddressUnlockCondition{Address: ident}, @@ -6448,21 +7119,21 @@ func TestTxSemanticAddressRestrictions(t *testing.T) { createTestOutput: func(address iotago.Address) iotago.Output { return &iotago.AccountOutput{ Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{Address: address}, + &iotago.AddressUnlockCondition{Address: address}, }, } }, createTestParameters: []func() testParameters{ func() testParameters { return testParameters{ - name: "ok - Account Output Address in State Controller UC in Account Output", + name: "ok - Account Output Address in Account Output", address: iotago.RestrictedAddressWithCapabilities(addr, iotago.WithAddressCanReceiveAccountOutputs(true)), wantErr: nil, } }, func() testParameters { return testParameters{ - name: "fail - Non Account Output Address in State Controller UC in Account Output", + name: "fail - Non Account Output Address in Account Output", address: iotago.RestrictedAddressWithCapabilities(addr), wantErr: iotago.ErrAddressCannotReceiveAccountOutput, } @@ -6471,8 +7142,33 @@ func TestTxSemanticAddressRestrictions(t *testing.T) { }, { createTestOutput: func(address iotago.Address) iotago.Output { - return &iotago.AccountOutput{ - Conditions: iotago.AccountOutputUnlockConditions{ + return &iotago.AnchorOutput{ + Conditions: iotago.AnchorOutputUnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: address}, + }, + } + }, + createTestParameters: []func() testParameters{ + func() testParameters { + return testParameters{ + name: "ok - Anchor Output Address in State Controller UC in Anchor Output", + address: iotago.RestrictedAddressWithCapabilities(addr, iotago.WithAddressCanReceiveAnchorOutputs(true)), + wantErr: nil, + } + }, + func() testParameters { + return testParameters{ + name: "fail - Non Anchor Output Address in State Controller UC in Anchor Output", + address: iotago.RestrictedAddressWithCapabilities(addr), + wantErr: iotago.ErrAddressCannotReceiveAnchorOutput, + } + }, + }, + }, + { + createTestOutput: func(address iotago.Address) iotago.Output { + return &iotago.AnchorOutput{ + Conditions: iotago.AnchorOutputUnlockConditions{ &iotago.GovernorAddressUnlockCondition{Address: address}, }, } @@ -6480,16 +7176,16 @@ func TestTxSemanticAddressRestrictions(t *testing.T) { createTestParameters: []func() testParameters{ func() testParameters { return testParameters{ - name: "ok - Account Output Address in Governor UC in Account Output", - address: iotago.RestrictedAddressWithCapabilities(addr, iotago.WithAddressCanReceiveAccountOutputs(true)), + name: "ok - Anchor Output Address in Governor UC in Anchor Output", + address: iotago.RestrictedAddressWithCapabilities(addr, iotago.WithAddressCanReceiveAnchorOutputs(true)), wantErr: nil, } }, func() testParameters { return testParameters{ - name: "fail - Non Account Output Address in Governor UC in Account Output", + name: "fail - Non Anchor Output Address in Governor UC in Anchor Output", address: iotago.RestrictedAddressWithCapabilities(addr), - wantErr: iotago.ErrAddressCannotReceiveAccountOutput, + wantErr: iotago.ErrAddressCannotReceiveAnchorOutput, } }, }, @@ -6763,10 +7459,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { Mana: exampleMana, AccountID: accountID1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: edIdent, - }, - &iotago.GovernorAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: edIdent, }, }, @@ -6812,10 +7505,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { Mana: exampleMana, AccountID: accountID1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: edIdent, - }, - &iotago.GovernorAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: edIdent, }, }, @@ -6885,10 +7575,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { Mana: 0, AccountID: accountID1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: edIdent, - }, - &iotago.GovernorAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: edIdent, }, }, @@ -6944,10 +7631,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { Mana: 0, AccountID: accountID1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: edIdent, - }, - &iotago.GovernorAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: edIdent, }, }, @@ -7003,10 +7687,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { Mana: 0, AccountID: accountID1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: edIdent, - }, - &iotago.GovernorAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: edIdent, }, }, @@ -7024,10 +7705,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { Mana: 0, AccountID: accountID2, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: edIdent, - }, - &iotago.GovernorAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: edIdent, }, }, @@ -7071,10 +7749,7 @@ func TestTxSemanticImplicitAccountCreationAndTransition(t *testing.T) { Mana: exampleMana / 2, AccountID: accountID1, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: edIdent, - }, - &iotago.GovernorAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: edIdent, }, }, @@ -7163,10 +7838,7 @@ func TestTxSyntacticImplicitAccountMinDeposit(t *testing.T) { convertedAccount := &iotago.AccountOutput{ Amount: implicitAccount.Amount, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.GovernorAddressUnlockCondition{ - Address: &iotago.Ed25519Address{}, - }, - &iotago.StateControllerAddressUnlockCondition{ + &iotago.AddressUnlockCondition{ Address: &iotago.Ed25519Address{}, }, }, diff --git a/vm/vm.go b/vm/vm.go index 87da8caa5..fbd143175 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -377,16 +377,16 @@ func ValidateUnlocks(signedTransaction *iotago.SignedTransaction, resolvedInputs chainID = chainID.(iotago.UTXOIDChainID).FromOutputID(signedTransaction.Transaction.TransactionEssence.Inputs[inputIndex].(*iotago.UTXOInput).OutputID()) } - // for account outputs which are not state transitioning, we do not add it to the set of unlocked chains - if currentAccount, ok := chainConstrOutput.(*iotago.AccountOutput); ok { + // for anchor outputs which are not state transitioning, we do not add it to the set of unlocked chains + if currentAnchor, ok := chainConstrOutput.(*iotago.AnchorOutput); ok { next, hasNextState := outChains[chainID] if !hasNextState { continue } - // note that isAccount should never be false in practice, + // note that isAnchor should never be false in practice, // but we add it anyway as an additional safeguard - nextAccount, isAccount := next.(*iotago.AccountOutput) - if !isAccount || (currentAccount.StateIndex+1 != nextAccount.StateIndex) { + nextAnchor, isAnchor := next.(*iotago.AnchorOutput) + if !isAnchor || (currentAnchor.StateIndex+1 != nextAnchor.StateIndex) { continue } } @@ -428,7 +428,7 @@ func identToUnlock(transaction *iotago.Transaction, input iotago.Output, inputIn return in.Ident(nextTransDepIdentOutput) default: - panic("unknown ident output type in semantic unlocks") + panic(fmt.Sprintf("unknown ident output type in semantic unlocks: %T", in)) } } @@ -795,6 +795,10 @@ func checkAddressRestrictions(output iotago.TxEssenceOutput, address iotago.Addr return iotago.ErrAddressCannotReceiveAccountOutput } + if addrWithCapabilities.CannotReceiveAnchorOutputs() && output.Type() == iotago.OutputAnchor { + return iotago.ErrAddressCannotReceiveAnchorOutput + } + if addrWithCapabilities.CannotReceiveNFTOutputs() && output.Type() == iotago.OutputNFT { return iotago.ErrAddressCannotReceiveNFTOutput } diff --git a/workscore_test.go b/workscore_test.go index 19dda7a3d..2b99bd327 100644 --- a/workscore_test.go +++ b/workscore_test.go @@ -32,12 +32,7 @@ func TestTransactionEssenceWorkScore(t *testing.T) { output2 := &iotago.AccountOutput{ Amount: 1_000_000, Conditions: iotago.AccountOutputUnlockConditions{ - &iotago.StateControllerAddressUnlockCondition{ - Address: addr, - }, - &iotago.GovernorAddressUnlockCondition{ - Address: addr, - }, + &iotago.AddressUnlockCondition{addr}, }, Features: iotago.AccountOutputFeatures{ &iotago.BlockIssuerFeature{