From 85f1629b49f10888ca3c705e35877166fe729fce Mon Sep 17 00:00:00 2001 From: muXxer Date: Tue, 7 Mar 2023 02:14:48 +0100 Subject: [PATCH 1/2] Add syntactic check for chained outputs --- output.go | 24 +++++++++++++ protocol_test.go | 9 +++-- transaction_essence.go | 1 + transaction_test.go | 80 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 5 deletions(-) diff --git a/output.go b/output.go index 371382c31..c022dbc16 100644 --- a/output.go +++ b/output.go @@ -870,6 +870,30 @@ func SyntacticallyValidateOutputs(outputs Outputs, funcs ...OutputsSyntacticalVa return nil } +func OutputsSyntacticalChainConstrainedOutputUniqueness() OutputsSyntacticalValidationFunc { + chainConstrainedOutputs := make(ChainConstrainedOutputsSet) + + return func(index int, output Output) error { + chainConstrainedOutput, is := output.(ChainConstrainedOutput) + if !is { + return nil + } + + chainID := chainConstrainedOutput.Chain() + if chainID.Empty() { + // we can ignore newly minted chainConstrainedOutputs + return nil + } + + if _, has := chainConstrainedOutputs[chainID]; has { + return fmt.Errorf("%w: output with chainID %s already exist on the output side", ErrNonUniqueChainConstrainedOutputs, chainID.ToHex()) + } + + chainConstrainedOutputs[chainID] = chainConstrainedOutput + return nil + } +} + // JsonOutputSelector selects the json output implementation for the given type. func JsonOutputSelector(ty int) (JSONSerializable, error) { var obj JSONSerializable diff --git a/protocol_test.go b/protocol_test.go index 758e011bf..31bca5dbc 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -4,7 +4,6 @@ import ( "encoding/json" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/iotaledger/hive.go/serializer/v2" @@ -30,9 +29,9 @@ func (test *deSerializeTest) deSerialize(t *testing.T) { require.Error(t, err, test.seriErr) return } - assert.NoError(t, err) + require.NoError(t, err) if src, ok := test.source.(serializer.SerializableWithSize); ok { - assert.Equal(t, len(data), src.Size()) + require.Equal(t, len(data), src.Size()) } bytesRead, err := test.target.Deserialize(data, serializer.DeSeriModePerformValidation, tpkg.TestProtoParas) @@ -40,9 +39,9 @@ func (test *deSerializeTest) deSerialize(t *testing.T) { require.Error(t, err, test.deSeriErr) return } - assert.NoError(t, err) + require.NoError(t, err) require.Len(t, data, bytesRead) - assert.EqualValues(t, test.source, test.target) + require.EqualValues(t, test.source, test.target) } func TestProtocolParameters_DeSerialize(t *testing.T) { diff --git a/transaction_essence.go b/transaction_essence.go index 2ea3382df..3f1cc8851 100644 --- a/transaction_essence.go +++ b/transaction_essence.go @@ -344,6 +344,7 @@ func (u *TransactionEssence) syntacticallyValidate(protoParas *ProtocolParameter OutputsSyntacticalDepositAmount(protoParas), OutputsSyntacticalExpirationAndTimelock(), OutputsSyntacticalNativeTokens(), + OutputsSyntacticalChainConstrainedOutputUniqueness(), OutputsSyntacticalFoundry(), OutputsSyntacticalAlias(), OutputsSyntacticalNFT(), diff --git a/transaction_test.go b/transaction_test.go index 3f6bc776e..3e1e3bc38 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -149,6 +149,86 @@ func TestNFTTransition(t *testing.T) { }, inputs)) } +func TestChainConstrainedOutputUniqueness(t *testing.T) { + ident1 := tpkg.RandEd25519Address() + + inputIDs := tpkg.RandOutputIDs(1) + + aliasAddress := iotago.AliasAddressFromOutputID(inputIDs[0]) + aliasID := aliasAddress.AliasID() + nftAddress := iotago.NFTAddressFromOutputID(inputIDs[0]) + nftID := nftAddress.NFTID() + + // TODO: add a foundy testcase + + tests := []deSerializeTest{ + { + // we transition the same Alias twice + name: "transition the same Alias twice", + source: tpkg.RandTransactionWithEssence(&iotago.TransactionEssence{ + NetworkID: tpkg.TestNetworkID, + Inputs: inputIDs.UTXOInputs(), + Outputs: iotago.Outputs{ + &iotago.AliasOutput{ + Amount: OneMi, + AliasID: aliasID, + Conditions: iotago.UnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + Features: nil, + }, + &iotago.AliasOutput{ + Amount: OneMi, + AliasID: aliasID, + Conditions: iotago.UnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + Features: nil, + }, + }, + }), + target: &iotago.Transaction{}, + seriErr: iotago.ErrNonUniqueChainConstrainedOutputs, + deSeriErr: nil, + }, + { + // we transition the same NFT twice + name: "transition the same NFT twice", + source: tpkg.RandTransactionWithEssence(&iotago.TransactionEssence{ + NetworkID: tpkg.TestNetworkID, + Inputs: inputIDs.UTXOInputs(), + Outputs: iotago.Outputs{ + &iotago.NFTOutput{ + Amount: OneMi, + NFTID: nftID, + Conditions: iotago.UnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + Features: nil, + }, + &iotago.NFTOutput{ + Amount: OneMi, + NFTID: nftID, + Conditions: iotago.UnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + Features: nil, + }, + }, + }), + target: &iotago.Transaction{}, + seriErr: iotago.ErrNonUniqueChainConstrainedOutputs, + deSeriErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, tt.deSerialize) + } +} + func TestCirculatingSupplyMelting(t *testing.T) { _, ident1, ident1AddrKeys := tpkg.RandEd25519Identity() aliasIdent1 := tpkg.RandAliasAddress() From 52162dec1a37f811188654b9e8af51fb67fba126 Mon Sep 17 00:00:00 2001 From: Alexander Sporn Date: Tue, 7 Mar 2023 08:21:31 +0100 Subject: [PATCH 2/2] Added foundry test case --- transaction_test.go | 53 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/transaction_test.go b/transaction_test.go index 3e1e3bc38..9ed26c956 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -156,11 +156,10 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { aliasAddress := iotago.AliasAddressFromOutputID(inputIDs[0]) aliasID := aliasAddress.AliasID() + nftAddress := iotago.NFTAddressFromOutputID(inputIDs[0]) nftID := nftAddress.NFTID() - // TODO: add a foundy testcase - tests := []deSerializeTest{ { // we transition the same Alias twice @@ -222,6 +221,56 @@ func TestChainConstrainedOutputUniqueness(t *testing.T) { seriErr: iotago.ErrNonUniqueChainConstrainedOutputs, deSeriErr: nil, }, + { + // we transition the same Foundry twice + name: "transition the same Foundry twice", + source: tpkg.RandTransactionWithEssence(&iotago.TransactionEssence{ + NetworkID: tpkg.TestNetworkID, + Inputs: inputIDs.UTXOInputs(), + Outputs: iotago.Outputs{ + &iotago.AliasOutput{ + Amount: OneMi, + AliasID: aliasID, + Conditions: iotago.UnlockConditions{ + &iotago.StateControllerAddressUnlockCondition{Address: ident1}, + &iotago.GovernorAddressUnlockCondition{Address: ident1}, + }, + Features: nil, + }, + &iotago.FoundryOutput{ + Amount: OneMi, + NativeTokens: nil, + SerialNumber: 1, + TokenScheme: &iotago.SimpleTokenScheme{ + MintedTokens: big.NewInt(50), + MeltedTokens: big.NewInt(0), + MaximumSupply: big.NewInt(50), + }, + Conditions: iotago.UnlockConditions{ + &iotago.ImmutableAliasUnlockCondition{Address: &aliasAddress}, + }, + Features: nil, + }, + &iotago.FoundryOutput{ + Amount: OneMi, + NativeTokens: nil, + SerialNumber: 1, + TokenScheme: &iotago.SimpleTokenScheme{ + MintedTokens: big.NewInt(50), + MeltedTokens: big.NewInt(0), + MaximumSupply: big.NewInt(50), + }, + Conditions: iotago.UnlockConditions{ + &iotago.ImmutableAliasUnlockCondition{Address: &aliasAddress}, + }, + Features: nil, + }, + }, + }), + target: &iotago.Transaction{}, + seriErr: iotago.ErrNonUniqueChainConstrainedOutputs, + deSeriErr: nil, + }, } for _, tt := range tests {