-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Changed how the TransactionID is computed
Added a way to generate OutputID proofs from a given transaction and output index and a way to validate them given the output.
- Loading branch information
Showing
6 changed files
with
322 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
Check failure on line 29 in output_id_proof.go GitHub Actions / golangci[golangci] output_id_proof.go#L29
Raw output
|
||
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) | ||
Check failure on line 73 in output_id_proof.go GitHub Actions / golangci[golangci] output_id_proof.go#L73
Raw output
|
||
|
||
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
Oops, something went wrong.