diff --git a/api_v3.go b/api_v3.go index 347529e0e..fda2b3099 100644 --- a/api_v3.go +++ b/api_v3.go @@ -9,6 +9,7 @@ import ( "github.com/iotaledger/hive.go/ierrors" "github.com/iotaledger/hive.go/serializer/v2" "github.com/iotaledger/hive.go/serializer/v2/serix" + "github.com/iotaledger/iota.go/v4/merklehasher" ) const ( @@ -629,6 +630,10 @@ func V3API(protoParams ProtocolParameters) API { )) } + { + merklehasher.RegisterSerixRules[*APIByter[TxEssenceOutput]](api) + } + return v3 } diff --git a/go.mod b/go.mod index 2e4fb70c3..65389a050 100644 --- a/go.mod +++ b/go.mod @@ -5,15 +5,16 @@ go 1.21 require ( github.com/eclipse/paho.mqtt.golang v1.4.3 github.com/ethereum/go-ethereum v1.12.2 - github.com/iotaledger/hive.go/constraints v0.0.0-20231005142627-86973b2edb3b - github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231005142627-86973b2edb3b - github.com/iotaledger/hive.go/crypto v0.0.0-20231005142627-86973b2edb3b - github.com/iotaledger/hive.go/ierrors v0.0.0-20231005142627-86973b2edb3b - github.com/iotaledger/hive.go/lo v0.0.0-20230929122509-67f34bfed40d + github.com/iotaledger/hive.go/constraints v0.0.0-20231010113711-a208cf7170ab + github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231010113711-a208cf7170ab + github.com/iotaledger/hive.go/crypto v0.0.0-20231010113711-a208cf7170ab + github.com/iotaledger/hive.go/ierrors v0.0.0-20231010113711-a208cf7170ab + github.com/iotaledger/hive.go/lo v0.0.0-20231010113711-a208cf7170ab github.com/iotaledger/hive.go/runtime v0.0.0-20230929122509-67f34bfed40d - github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231005142627-86973b2edb3b - github.com/iotaledger/hive.go/stringify v0.0.0-20231005142627-86973b2edb3b + github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231010113711-a208cf7170ab + github.com/iotaledger/hive.go/stringify v0.0.0-20231010113711-a208cf7170ab github.com/pasztorpisti/qs v0.0.0-20171216220353-8d6c33ee906c + github.com/samber/lo v1.38.1 github.com/stretchr/testify v1.8.4 golang.org/x/crypto v0.12.0 gopkg.in/h2non/gock.v1 v1.1.2 @@ -31,6 +32,7 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/exp v0.0.0-20230810033253-352e893a4cad // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.11.0 // indirect diff --git a/go.sum b/go.sum index e06d4a86a..252158a7c 100644 --- a/go.sum +++ b/go.sum @@ -23,22 +23,22 @@ github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= -github.com/iotaledger/hive.go/constraints v0.0.0-20231005142627-86973b2edb3b h1:8FdKoB755PjCL1aHTi+2rPt9lCUS4B2wg2fNKykw5dc= -github.com/iotaledger/hive.go/constraints v0.0.0-20231005142627-86973b2edb3b/go.mod h1:dOBOM2s4se3HcWefPe8sQLUalGXJ8yVXw58oK8jke3s= -github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231005142627-86973b2edb3b h1:gQ+Wqg8h/LRpgX3CsmaOZYdMIBVL4fAPIrZbmA6Wn3Q= -github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231005142627-86973b2edb3b/go.mod h1:jn3TNmiNRIiQm/rS4VD+7wFHI2+UXABHvCA3PbQxBqI= -github.com/iotaledger/hive.go/crypto v0.0.0-20231005142627-86973b2edb3b h1:F91AhJfeVN/XdvzUObUXa5mA38VezBZ9GHhdfQw4fVg= -github.com/iotaledger/hive.go/crypto v0.0.0-20231005142627-86973b2edb3b/go.mod h1:jP68na941d9uq7RtnA8aQ/FtIGRGz/51cU4uXrInQFU= -github.com/iotaledger/hive.go/ierrors v0.0.0-20231005142627-86973b2edb3b h1:D07ocbgOyj2d/AGjrIuAxIaQC5SQDndiKp9UaaQwLas= -github.com/iotaledger/hive.go/ierrors v0.0.0-20231005142627-86973b2edb3b/go.mod h1:HcE8B5lP96enc/OALTb2/rIIi+yOLouRoHOKRclKmC8= -github.com/iotaledger/hive.go/lo v0.0.0-20230929122509-67f34bfed40d h1:qNmg1DUvge8zPvygQEoulQjLG7gFzWKqPMJ3r7ZESm0= -github.com/iotaledger/hive.go/lo v0.0.0-20230929122509-67f34bfed40d/go.mod h1:4oKCdMEhHMLCudBz79kuvJmgSY/DhfVePNIyJhew/80= +github.com/iotaledger/hive.go/constraints v0.0.0-20231010113711-a208cf7170ab h1:2g7lZLh5lsYjubQiYSDSxNqUVtEtUeIZoF7AOtiBSQ4= +github.com/iotaledger/hive.go/constraints v0.0.0-20231010113711-a208cf7170ab/go.mod h1:dOBOM2s4se3HcWefPe8sQLUalGXJ8yVXw58oK8jke3s= +github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231010113711-a208cf7170ab h1:joeVmUK6MMExsbAJbugVGgn0KtYX2JM1pWgn2uFsLD4= +github.com/iotaledger/hive.go/core v1.0.0-rc.3.0.20231010113711-a208cf7170ab/go.mod h1:jn3TNmiNRIiQm/rS4VD+7wFHI2+UXABHvCA3PbQxBqI= +github.com/iotaledger/hive.go/crypto v0.0.0-20231010113711-a208cf7170ab h1:vEjCHMTSAP42VU1DtZE+O42z7QPC8tI7gG+xpnpkQjE= +github.com/iotaledger/hive.go/crypto v0.0.0-20231010113711-a208cf7170ab/go.mod h1:jP68na941d9uq7RtnA8aQ/FtIGRGz/51cU4uXrInQFU= +github.com/iotaledger/hive.go/ierrors v0.0.0-20231010113711-a208cf7170ab h1:KqyZkt5bYYAo5yO5gLkjGa4s9fcEuse5A7KT9d8MIKg= +github.com/iotaledger/hive.go/ierrors v0.0.0-20231010113711-a208cf7170ab/go.mod h1:HcE8B5lP96enc/OALTb2/rIIi+yOLouRoHOKRclKmC8= +github.com/iotaledger/hive.go/lo v0.0.0-20231010113711-a208cf7170ab h1:m0p/CepaNrSUAtPHy+HIdzgXLwKIGUSr2AHqRAe0Ru0= +github.com/iotaledger/hive.go/lo v0.0.0-20231010113711-a208cf7170ab/go.mod h1:4oKCdMEhHMLCudBz79kuvJmgSY/DhfVePNIyJhew/80= github.com/iotaledger/hive.go/runtime v0.0.0-20230929122509-67f34bfed40d h1:mn2Gax95UuUpuzEi4osLk+1IBjv5q56LwcxF/lAxk38= github.com/iotaledger/hive.go/runtime v0.0.0-20230929122509-67f34bfed40d/go.mod h1:fXVyQ1MAwxe/EmjAnG8WcQqbzGk9EW/FsJ/n16H/f/w= -github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231005142627-86973b2edb3b h1:6anjtWbaCszD5h43psnE8lsgIM0etpjr62ZlN59t0H8= -github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231005142627-86973b2edb3b/go.mod h1:IJgaaxbgKCsNat18jlJJEAxCY2oVYR3F30B+M4vJ89I= -github.com/iotaledger/hive.go/stringify v0.0.0-20231005142627-86973b2edb3b h1:cc9VsDzLxPAaC8fj96EA1bJxbmrEZndJV+3SmG+HHOs= -github.com/iotaledger/hive.go/stringify v0.0.0-20231005142627-86973b2edb3b/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= +github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231010113711-a208cf7170ab h1:7hB97d+zP0vWIDZlT0jsnaY2nXnHxQlNYqZ9qs1xPYE= +github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1.0.20231010113711-a208cf7170ab/go.mod h1:IJgaaxbgKCsNat18jlJJEAxCY2oVYR3F30B+M4vJ89I= +github.com/iotaledger/hive.go/stringify v0.0.0-20231010113711-a208cf7170ab h1:udy0r5O3nlSRYOcXJeIieVSNQt4r0IrXzvn+OeUV5/I= +github.com/iotaledger/hive.go/stringify v0.0.0-20231010113711-a208cf7170ab/go.mod h1:FTo/UWzNYgnQ082GI9QVM9HFDERqf9rw9RivNpqrnTs= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -53,10 +53,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= +golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= diff --git a/output_id_proof.go b/output_id_proof.go new file mode 100644 index 000000000..9e0f12bd6 --- /dev/null +++ b/output_id_proof.go @@ -0,0 +1,118 @@ +package iotago + +import ( + "context" + "crypto" + + "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/lo" + "github.com/iotaledger/iota.go/v4/merklehasher" +) + +type OutputIDProof struct { + API API + Slot SlotIndex `serix:"0,mapKey=slot"` + OutputIndex uint16 `serix:"1,mapKey=outputIndex"` + TransactionCommitment Identifier `serix:"2,mapKey=transactionCommitment"` + OutputCommitmentProof *merklehasher.Proof[*APIByter[TxEssenceOutput]] `serix:"3,mapKey=outputCommitmentProof"` +} + +func OutputIDProofForOutputAtIndex(tx *Transaction, index uint16) (*OutputIDProof, error) { + if tx.API == nil { + return nil, ierrors.New("API not set") + } + + if int(index) >= len(tx.Outputs) { + return nil, ierrors.Errorf("index %d out of bounds len=%d", index, len(tx.Outputs)) + } + + outputHasher := merklehasher.NewHasher[*APIByter[TxEssenceOutput]](crypto.BLAKE2b_256) + wrappedOutputs := lo.Map(tx.Outputs, APIByterFactory[TxEssenceOutput](tx.API)) + + proof, err := outputHasher.ComputeProofForIndex(wrappedOutputs, int(index)) + if err != nil { + return nil, ierrors.Wrapf(err, "failed to compute proof for index %d", index) + } + + transactionCommitment, err := tx.TransactionCommitment() + if err != nil { + return nil, err + } + + return &OutputIDProof{ + API: tx.API, + Slot: tx.CreationSlot, + OutputIndex: index, + TransactionCommitment: transactionCommitment, + OutputCommitmentProof: proof, + }, nil +} + +func OutputIDProofFromBytes(api API) func([]byte) (*OutputIDProof, int, error) { + return func(b []byte) (proof *OutputIDProof, consumedBytes int, err error) { + proof = new(OutputIDProof) + consumedBytes, err = api.Decode(b, proof) + + return proof, consumedBytes, err + } +} + +func (p *OutputIDProof) Bytes() ([]byte, error) { + return p.API.Encode(p) +} + +func (p *OutputIDProof) SetDeserializationContext(ctx context.Context) { + p.API = APIFromContext(ctx) +} + +func (p *OutputIDProof) OutputID(output Output) (OutputID, error) { + if p.API == nil { + return EmptyOutputID, ierrors.New("API not set") + } + + outputHasher := merklehasher.NewHasher[*APIByter[TxEssenceOutput]](crypto.BLAKE2b_256) + + contains, err := p.OutputCommitmentProof.ContainsValue(APIByterFactory[TxEssenceOutput](p.API)(output), outputHasher) + if err != nil { + return EmptyOutputID, ierrors.Wrapf(err, "failed to check if proof contains output") + } + + // The proof does not contain a hash of the output + if !contains { + return EmptyOutputID, ierrors.Errorf("proof does not contain the given output") + } + + // Hash the proof to get the root + outputCommitment := Identifier(p.OutputCommitmentProof.Hash(outputHasher)) + + // Compute the output ID from the contents of the proof + utxoInput := &UTXOInput{ + TransactionID: TransactionIDFromTransactionCommitmentAndOutputCommitment(p.Slot, p.TransactionCommitment, outputCommitment), + TransactionOutputIndex: p.OutputIndex, + } + computedOutputID := utxoInput.OutputID() + + return computedOutputID, nil +} + +type APIByter[T any] struct { + API API + Value T `serix:"0"` +} + +func APIByterFactory[T any](api API) func(value T) *APIByter[T] { + return func(value T) *APIByter[T] { + return &APIByter[T]{ + API: api, + Value: value, + } + } +} + +func (a *APIByter[T]) SetDeserializationContext(ctx context.Context) { + a.API = APIFromContext(ctx) +} + +func (a *APIByter[T]) Bytes() ([]byte, error) { + return a.API.Encode(a.Value) +} diff --git a/output_id_proof_test.go b/output_id_proof_test.go new file mode 100644 index 000000000..945a4a453 --- /dev/null +++ b/output_id_proof_test.go @@ -0,0 +1,138 @@ +package iotago_test + +import ( + "fmt" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + iotago "github.com/iotaledger/iota.go/v4" + "github.com/iotaledger/iota.go/v4/tpkg" +) + +type outputIDProofTest struct { + name string + tx *iotago.Transaction +} + +func TestOutputIDProof(t *testing.T) { + ident1 := tpkg.RandEd25519Address() + + inputIDs := tpkg.RandOutputIDs(1) + + tests := []outputIDProofTest{ + { + name: "single output", + tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + CreationSlot: tpkg.RandSlot(), + NetworkID: tpkg.TestNetworkID, + Inputs: inputIDs.UTXOInputs(), + Capabilities: iotago.TransactionCapabilitiesBitMask{}, + }, + Outputs: lo.RepeatBy(1, func(_ int) iotago.TxEssenceOutput { + return &iotago.BasicOutput{ + Amount: OneMi, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + } + }), + }, + }, + { + name: "two outputs", + tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + CreationSlot: tpkg.RandSlot(), + NetworkID: tpkg.TestNetworkID, + Inputs: inputIDs.UTXOInputs(), + Capabilities: iotago.TransactionCapabilitiesBitMask{}, + }, + Outputs: lo.RepeatBy(2, func(_ int) iotago.TxEssenceOutput { + return &iotago.BasicOutput{ + Amount: OneMi, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + } + }), + }, + }, + { + name: "three outputs", + tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + CreationSlot: tpkg.RandSlot(), + NetworkID: tpkg.TestNetworkID, + Inputs: inputIDs.UTXOInputs(), + Capabilities: iotago.TransactionCapabilitiesBitMask{}, + }, + Outputs: lo.RepeatBy(3, func(_ int) iotago.TxEssenceOutput { + return &iotago.BasicOutput{ + Amount: OneMi, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + } + }), + }, + }, + { + name: "max outputs", + tx: &iotago.Transaction{ + API: tpkg.TestAPI, + TransactionEssence: &iotago.TransactionEssence{ + CreationSlot: tpkg.RandSlot(), + NetworkID: tpkg.TestNetworkID, + Inputs: inputIDs.UTXOInputs(), + Capabilities: iotago.TransactionCapabilitiesBitMask{}, + }, + Outputs: lo.RepeatBy(iotago.MaxOutputsCount, func(_ int) iotago.TxEssenceOutput { + return &iotago.BasicOutput{ + Amount: OneMi, + Conditions: iotago.BasicOutputUnlockConditions{ + &iotago.AddressUnlockCondition{Address: ident1}, + }, + } + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, tt.testOutputs) + } +} + +func (p *outputIDProofTest) testOutputs(t *testing.T) { + outputSet, err := p.tx.OutputsSet() + require.NoError(t, err) + + for outputID, output := range outputSet { + proof, err := iotago.OutputIDProofForOutputAtIndex(p.tx, outputID.Index()) + require.NoError(t, err) + + serializedProof, err := proof.Bytes() + require.NoError(t, err) + + jsonEncoded, err := tpkg.TestAPI.JSONEncode(proof) + require.NoError(t, err) + fmt.Println(string(jsonEncoded)) + + deserializedProof, consumedBytes, err := iotago.OutputIDProofFromBytes(tpkg.TestAPI)(serializedProof) + require.NoError(t, err) + require.Equal(t, len(serializedProof), consumedBytes) + + require.Equal(t, proof, deserializedProof) + + computedOutputID, err := deserializedProof.OutputID(output) + require.NoError(t, err) + + require.Equal(t, outputID, computedOutputID) + } +} diff --git a/transaction.go b/transaction.go index 4abbd72c7..12a8f11d7 100644 --- a/transaction.go +++ b/transaction.go @@ -2,12 +2,15 @@ package iotago import ( "context" + "crypto" "golang.org/x/crypto/blake2b" "github.com/iotaledger/hive.go/ierrors" + "github.com/iotaledger/hive.go/lo" "github.com/iotaledger/hive.go/serializer/v2/byteutils" "github.com/iotaledger/hive.go/stringify" + "github.com/iotaledger/iota.go/v4/merklehasher" ) const ( @@ -63,17 +66,44 @@ type Transaction struct { // ID returns the TransactionID created without the signatures. func (t *Transaction) ID() (TransactionID, error) { + transactionCommitment, err := t.TransactionCommitment() + if err != nil { + return EmptyTransactionID, ierrors.Errorf("can't compute transaction commitment: %w", err) + } + + outputCommitment, err := t.OutputCommitment() + if err != nil { + return TransactionID{}, ierrors.Errorf("can't compute output commitment: %w", err) + } + + return TransactionIDFromTransactionCommitmentAndOutputCommitment(t.CreationSlot, transactionCommitment, outputCommitment), nil +} + +func TransactionIDFromTransactionCommitmentAndOutputCommitment(slot SlotIndex, transactionCommitment Identifier, outputCommitment Identifier) TransactionID { + return TransactionIDRepresentingData(slot, byteutils.ConcatBytes(transactionCommitment[:], outputCommitment[:])) +} + +// TransactionCommitment returns the transaction commitment hashing the transaction essence. +func (t *Transaction) TransactionCommitment() (Identifier, error) { essenceBytes, err := t.API.Encode(t.TransactionEssence) if err != nil { - return TransactionID{}, ierrors.Errorf("can't compute essence bytes: %w", err) + return EmptyIdentifier, ierrors.Errorf("can't compute essence bytes: %w", err) } - outputBytes, err := t.API.Encode(t.Outputs) + return IdentifierFromData(essenceBytes), nil +} + +// OutputCommitment returns the output commitment which is the root of the merkle tree of the outputs. +func (t *Transaction) OutputCommitment() (Identifier, error) { + outputHasher := merklehasher.NewHasher[*APIByter[TxEssenceOutput]](crypto.BLAKE2b_256) + wrappedOutputs := lo.Map(t.Outputs, APIByterFactory[TxEssenceOutput](t.API)) + + root, err := outputHasher.HashValues(wrappedOutputs) if err != nil { - return TransactionID{}, ierrors.Errorf("can't compute unlock bytes: %w", err) + return EmptyIdentifier, err } - return TransactionIDRepresentingData(t.CreationSlot, byteutils.ConcatBytes(essenceBytes, outputBytes)), nil + return Identifier(root), nil } func (t *Transaction) SetDeserializationContext(ctx context.Context) {