Skip to content
This repository was archived by the owner on May 20, 2025. It is now read-only.

Commit 4f1f458

Browse files
authored
Merge pull request #423 from iotaledger/fix/chained-outputs
Fix chained outputs
2 parents ef33ea7 + 52162de commit 4f1f458

File tree

4 files changed

+158
-5
lines changed

4 files changed

+158
-5
lines changed

output.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,30 @@ func SyntacticallyValidateOutputs(outputs Outputs, funcs ...OutputsSyntacticalVa
870870
return nil
871871
}
872872

873+
func OutputsSyntacticalChainConstrainedOutputUniqueness() OutputsSyntacticalValidationFunc {
874+
chainConstrainedOutputs := make(ChainConstrainedOutputsSet)
875+
876+
return func(index int, output Output) error {
877+
chainConstrainedOutput, is := output.(ChainConstrainedOutput)
878+
if !is {
879+
return nil
880+
}
881+
882+
chainID := chainConstrainedOutput.Chain()
883+
if chainID.Empty() {
884+
// we can ignore newly minted chainConstrainedOutputs
885+
return nil
886+
}
887+
888+
if _, has := chainConstrainedOutputs[chainID]; has {
889+
return fmt.Errorf("%w: output with chainID %s already exist on the output side", ErrNonUniqueChainConstrainedOutputs, chainID.ToHex())
890+
}
891+
892+
chainConstrainedOutputs[chainID] = chainConstrainedOutput
893+
return nil
894+
}
895+
}
896+
873897
// JsonOutputSelector selects the json output implementation for the given type.
874898
func JsonOutputSelector(ty int) (JSONSerializable, error) {
875899
var obj JSONSerializable

protocol_test.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"testing"
66

7-
"github.com/stretchr/testify/assert"
87
"github.com/stretchr/testify/require"
98

109
"github.com/iotaledger/hive.go/serializer/v2"
@@ -30,19 +29,19 @@ func (test *deSerializeTest) deSerialize(t *testing.T) {
3029
require.Error(t, err, test.seriErr)
3130
return
3231
}
33-
assert.NoError(t, err)
32+
require.NoError(t, err)
3433
if src, ok := test.source.(serializer.SerializableWithSize); ok {
35-
assert.Equal(t, len(data), src.Size())
34+
require.Equal(t, len(data), src.Size())
3635
}
3736

3837
bytesRead, err := test.target.Deserialize(data, serializer.DeSeriModePerformValidation, tpkg.TestProtoParas)
3938
if test.deSeriErr != nil {
4039
require.Error(t, err, test.deSeriErr)
4140
return
4241
}
43-
assert.NoError(t, err)
42+
require.NoError(t, err)
4443
require.Len(t, data, bytesRead)
45-
assert.EqualValues(t, test.source, test.target)
44+
require.EqualValues(t, test.source, test.target)
4645
}
4746

4847
func TestProtocolParameters_DeSerialize(t *testing.T) {

transaction_essence.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ func (u *TransactionEssence) syntacticallyValidate(protoParas *ProtocolParameter
344344
OutputsSyntacticalDepositAmount(protoParas),
345345
OutputsSyntacticalExpirationAndTimelock(),
346346
OutputsSyntacticalNativeTokens(),
347+
OutputsSyntacticalChainConstrainedOutputUniqueness(),
347348
OutputsSyntacticalFoundry(),
348349
OutputsSyntacticalAlias(),
349350
OutputsSyntacticalNFT(),

transaction_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,135 @@ func TestNFTTransition(t *testing.T) {
149149
}, inputs))
150150
}
151151

152+
func TestChainConstrainedOutputUniqueness(t *testing.T) {
153+
ident1 := tpkg.RandEd25519Address()
154+
155+
inputIDs := tpkg.RandOutputIDs(1)
156+
157+
aliasAddress := iotago.AliasAddressFromOutputID(inputIDs[0])
158+
aliasID := aliasAddress.AliasID()
159+
160+
nftAddress := iotago.NFTAddressFromOutputID(inputIDs[0])
161+
nftID := nftAddress.NFTID()
162+
163+
tests := []deSerializeTest{
164+
{
165+
// we transition the same Alias twice
166+
name: "transition the same Alias twice",
167+
source: tpkg.RandTransactionWithEssence(&iotago.TransactionEssence{
168+
NetworkID: tpkg.TestNetworkID,
169+
Inputs: inputIDs.UTXOInputs(),
170+
Outputs: iotago.Outputs{
171+
&iotago.AliasOutput{
172+
Amount: OneMi,
173+
AliasID: aliasID,
174+
Conditions: iotago.UnlockConditions{
175+
&iotago.StateControllerAddressUnlockCondition{Address: ident1},
176+
&iotago.GovernorAddressUnlockCondition{Address: ident1},
177+
},
178+
Features: nil,
179+
},
180+
&iotago.AliasOutput{
181+
Amount: OneMi,
182+
AliasID: aliasID,
183+
Conditions: iotago.UnlockConditions{
184+
&iotago.StateControllerAddressUnlockCondition{Address: ident1},
185+
&iotago.GovernorAddressUnlockCondition{Address: ident1},
186+
},
187+
Features: nil,
188+
},
189+
},
190+
}),
191+
target: &iotago.Transaction{},
192+
seriErr: iotago.ErrNonUniqueChainConstrainedOutputs,
193+
deSeriErr: nil,
194+
},
195+
{
196+
// we transition the same NFT twice
197+
name: "transition the same NFT twice",
198+
source: tpkg.RandTransactionWithEssence(&iotago.TransactionEssence{
199+
NetworkID: tpkg.TestNetworkID,
200+
Inputs: inputIDs.UTXOInputs(),
201+
Outputs: iotago.Outputs{
202+
&iotago.NFTOutput{
203+
Amount: OneMi,
204+
NFTID: nftID,
205+
Conditions: iotago.UnlockConditions{
206+
&iotago.AddressUnlockCondition{Address: ident1},
207+
},
208+
Features: nil,
209+
},
210+
&iotago.NFTOutput{
211+
Amount: OneMi,
212+
NFTID: nftID,
213+
Conditions: iotago.UnlockConditions{
214+
&iotago.AddressUnlockCondition{Address: ident1},
215+
},
216+
Features: nil,
217+
},
218+
},
219+
}),
220+
target: &iotago.Transaction{},
221+
seriErr: iotago.ErrNonUniqueChainConstrainedOutputs,
222+
deSeriErr: nil,
223+
},
224+
{
225+
// we transition the same Foundry twice
226+
name: "transition the same Foundry twice",
227+
source: tpkg.RandTransactionWithEssence(&iotago.TransactionEssence{
228+
NetworkID: tpkg.TestNetworkID,
229+
Inputs: inputIDs.UTXOInputs(),
230+
Outputs: iotago.Outputs{
231+
&iotago.AliasOutput{
232+
Amount: OneMi,
233+
AliasID: aliasID,
234+
Conditions: iotago.UnlockConditions{
235+
&iotago.StateControllerAddressUnlockCondition{Address: ident1},
236+
&iotago.GovernorAddressUnlockCondition{Address: ident1},
237+
},
238+
Features: nil,
239+
},
240+
&iotago.FoundryOutput{
241+
Amount: OneMi,
242+
NativeTokens: nil,
243+
SerialNumber: 1,
244+
TokenScheme: &iotago.SimpleTokenScheme{
245+
MintedTokens: big.NewInt(50),
246+
MeltedTokens: big.NewInt(0),
247+
MaximumSupply: big.NewInt(50),
248+
},
249+
Conditions: iotago.UnlockConditions{
250+
&iotago.ImmutableAliasUnlockCondition{Address: &aliasAddress},
251+
},
252+
Features: nil,
253+
},
254+
&iotago.FoundryOutput{
255+
Amount: OneMi,
256+
NativeTokens: nil,
257+
SerialNumber: 1,
258+
TokenScheme: &iotago.SimpleTokenScheme{
259+
MintedTokens: big.NewInt(50),
260+
MeltedTokens: big.NewInt(0),
261+
MaximumSupply: big.NewInt(50),
262+
},
263+
Conditions: iotago.UnlockConditions{
264+
&iotago.ImmutableAliasUnlockCondition{Address: &aliasAddress},
265+
},
266+
Features: nil,
267+
},
268+
},
269+
}),
270+
target: &iotago.Transaction{},
271+
seriErr: iotago.ErrNonUniqueChainConstrainedOutputs,
272+
deSeriErr: nil,
273+
},
274+
}
275+
276+
for _, tt := range tests {
277+
t.Run(tt.name, tt.deSerialize)
278+
}
279+
}
280+
152281
func TestCirculatingSupplyMelting(t *testing.T) {
153282
_, ident1, ident1AddrKeys := tpkg.RandEd25519Identity()
154283
aliasIdent1 := tpkg.RandAliasAddress()

0 commit comments

Comments
 (0)