From 8941fc19b44986693653a2283e32f6393e139881 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 13 Nov 2024 09:55:21 -0800 Subject: [PATCH 1/3] types: add ID field to transaction JSON --- types/types.go | 66 ++++++++++++++++++++++++++++++++++++++++ types/types_test.go | 74 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+) diff --git a/types/types.go b/types/types.go index 19e5280..9c63599 100644 --- a/types/types.go +++ b/types/types.go @@ -400,6 +400,38 @@ type Transaction struct { Signatures []TransactionSignature `json:"signatures,omitempty"` } +// MarshalJSON implements json.Marshaller. +// +// json.Umarshaller is not implemented because the ID should be discarded. +func (txn Transaction) MarshalJSON() ([]byte, error) { + jsonTxn := struct { + ID TransactionID `json:"id"` + SiacoinInputs []SiacoinInput `json:"siacoinInputs,omitempty"` + SiacoinOutputs []SiacoinOutput `json:"siacoinOutputs,omitempty"` + FileContracts []FileContract `json:"fileContracts,omitempty"` + FileContractRevisions []FileContractRevision `json:"fileContractRevisions,omitempty"` + StorageProofs []StorageProof `json:"storageProofs,omitempty"` + SiafundInputs []SiafundInput `json:"siafundInputs,omitempty"` + SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` + MinerFees []Currency `json:"minerFees,omitempty"` + ArbitraryData [][]byte `json:"arbitraryData,omitempty"` + Signatures []TransactionSignature `json:"signatures,omitempty"` + }{ + ID: txn.ID(), + SiacoinInputs: txn.SiacoinInputs, + SiacoinOutputs: txn.SiacoinOutputs, + FileContracts: txn.FileContracts, + FileContractRevisions: txn.FileContractRevisions, + StorageProofs: txn.StorageProofs, + SiafundInputs: txn.SiafundInputs, + SiafundOutputs: txn.SiafundOutputs, + MinerFees: txn.MinerFees, + ArbitraryData: txn.ArbitraryData, + Signatures: txn.Signatures, + } + return json.Marshal(jsonTxn) +} + // ID returns the "semantic hash" of the transaction, covering all of the // transaction's effects, but not incidental data such as signatures. This // ensures that the ID will remain stable (i.e. non-malleable). @@ -689,6 +721,40 @@ type V2Transaction struct { MinerFee Currency `json:"minerFee"` } +// MarshalJSON implements json.Marshaller. +// +// json.Umarshaller is not implemented because the ID should be discarded. +func (txn V2Transaction) MarshalJSON() ([]byte, error) { + jsonTxn := struct { + ID TransactionID `json:"id"` + SiacoinInputs []V2SiacoinInput `json:"siacoinInputs,omitempty"` + SiacoinOutputs []SiacoinOutput `json:"siacoinOutputs,omitempty"` + SiafundInputs []V2SiafundInput `json:"siafundInputs,omitempty"` + SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` + FileContracts []V2FileContract `json:"fileContracts,omitempty"` + FileContractRevisions []V2FileContractRevision `json:"fileContractRevisions,omitempty"` + FileContractResolutions []V2FileContractResolution `json:"fileContractResolutions,omitempty"` + Attestations []Attestation `json:"attestations,omitempty"` + ArbitraryData []byte `json:"arbitraryData,omitempty"` + NewFoundationAddress *Address `json:"newFoundationAddress,omitempty"` + MinerFee Currency `json:"minerFee"` + }{ + ID: txn.ID(), + SiacoinInputs: txn.SiacoinInputs, + SiacoinOutputs: txn.SiacoinOutputs, + SiafundInputs: txn.SiafundInputs, + SiafundOutputs: txn.SiafundOutputs, + FileContracts: txn.FileContracts, + FileContractRevisions: txn.FileContractRevisions, + FileContractResolutions: txn.FileContractResolutions, + Attestations: txn.Attestations, + ArbitraryData: txn.ArbitraryData, + NewFoundationAddress: txn.NewFoundationAddress, + MinerFee: txn.MinerFee, + } + return json.Marshal(jsonTxn) +} + // ID returns the "semantic hash" of the transaction, covering all of the // transaction's effects, but not incidental data such as signatures or Merkle // proofs. This ensures that the ID will remain stable (i.e. non-malleable). diff --git a/types/types_test.go b/types/types_test.go index d6b2e65..a48ad9a 100644 --- a/types/types_test.go +++ b/types/types_test.go @@ -759,6 +759,80 @@ func TestParseCurrency(t *testing.T) { } } +func TestTransactionJSONMarshalling(t *testing.T) { + txn := Transaction{ + SiacoinOutputs: []SiacoinOutput{ + {Address: frand.Entropy256(), Value: Siacoins(uint32(frand.Uint64n(math.MaxUint32)))}, + }, + SiacoinInputs: []SiacoinInput{ + { + ParentID: frand.Entropy256(), + UnlockConditions: UnlockConditions{ + PublicKeys: []UnlockKey{ + PublicKey(frand.Entropy256()).UnlockKey(), + }, + SignaturesRequired: 1, + }, + }, + }, + } + expectedID := txn.ID() + + buf, err := json.Marshal(txn) + if err != nil { + t.Fatal(err) + } + + txnMap := make(map[string]any) + if err := json.Unmarshal(buf, &txnMap); err != nil { + t.Fatal(err) + } else if txnMap["id"] != expectedID.String() { + t.Fatalf("expected ID %q, got %q", expectedID.String(), txnMap["id"].(string)) + } + + var txn2 Transaction + if err := json.Unmarshal(buf, &txn2); err != nil { + t.Fatal(err) + } else if txn2.ID() != expectedID { + t.Fatalf("expected unmarshalled ID to be %q, got %q", expectedID, txn2.ID()) + } +} + +func TestV2TransactionJSONMarshalling(t *testing.T) { + txn := V2Transaction{ + SiacoinInputs: []V2SiacoinInput{ + { + Parent: SiacoinElement{ + ID: frand.Entropy256(), + StateElement: StateElement{ + LeafIndex: frand.Uint64n(math.MaxUint64), + }, + }, + }, + }, + } + expectedID := txn.ID() + + buf, err := json.Marshal(txn) + if err != nil { + t.Fatal(err) + } + + txnMap := make(map[string]any) + if err := json.Unmarshal(buf, &txnMap); err != nil { + t.Fatal(err) + } else if txnMap["id"] != expectedID.String() { + t.Fatalf("expected ID %q, got %q", expectedID.String(), txnMap["id"].(string)) + } + + var txn2 V2Transaction + if err := json.Unmarshal(buf, &txn2); err != nil { + t.Fatal(err) + } else if txn2.ID() != expectedID { + t.Fatalf("expected unmarshalled ID to be %q, got %q", expectedID, txn2.ID()) + } +} + func TestUnmarshalHex(t *testing.T) { for _, test := range []struct { data string From 24a1ed616720699f30c8292035795f0504d8f844 Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 13 Nov 2024 11:59:46 -0800 Subject: [PATCH 2/3] types: address comments --- types/types.go | 68 +++++++++----------------------------------------- 1 file changed, 12 insertions(+), 56 deletions(-) diff --git a/types/types.go b/types/types.go index 9c63599..5ee7c34 100644 --- a/types/types.go +++ b/types/types.go @@ -402,34 +402,13 @@ type Transaction struct { // MarshalJSON implements json.Marshaller. // -// json.Umarshaller is not implemented because the ID should be discarded. +// For convenience, the transaction's ID is also calculated and included. This field is ignored during unmarshalling. func (txn Transaction) MarshalJSON() ([]byte, error) { - jsonTxn := struct { - ID TransactionID `json:"id"` - SiacoinInputs []SiacoinInput `json:"siacoinInputs,omitempty"` - SiacoinOutputs []SiacoinOutput `json:"siacoinOutputs,omitempty"` - FileContracts []FileContract `json:"fileContracts,omitempty"` - FileContractRevisions []FileContractRevision `json:"fileContractRevisions,omitempty"` - StorageProofs []StorageProof `json:"storageProofs,omitempty"` - SiafundInputs []SiafundInput `json:"siafundInputs,omitempty"` - SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` - MinerFees []Currency `json:"minerFees,omitempty"` - ArbitraryData [][]byte `json:"arbitraryData,omitempty"` - Signatures []TransactionSignature `json:"signatures,omitempty"` - }{ - ID: txn.ID(), - SiacoinInputs: txn.SiacoinInputs, - SiacoinOutputs: txn.SiacoinOutputs, - FileContracts: txn.FileContracts, - FileContractRevisions: txn.FileContractRevisions, - StorageProofs: txn.StorageProofs, - SiafundInputs: txn.SiafundInputs, - SiafundOutputs: txn.SiafundOutputs, - MinerFees: txn.MinerFees, - ArbitraryData: txn.ArbitraryData, - Signatures: txn.Signatures, - } - return json.Marshal(jsonTxn) + type jsonTxn Transaction + return json.Marshal(struct { + ID TransactionID `json:"id"` + jsonTxn + }{txn.ID(), jsonTxn(txn)}) } // ID returns the "semantic hash" of the transaction, covering all of the @@ -723,36 +702,13 @@ type V2Transaction struct { // MarshalJSON implements json.Marshaller. // -// json.Umarshaller is not implemented because the ID should be discarded. +// For convenience, the transaction's ID is also calculated and included. This field is ignored during unmarshalling. func (txn V2Transaction) MarshalJSON() ([]byte, error) { - jsonTxn := struct { - ID TransactionID `json:"id"` - SiacoinInputs []V2SiacoinInput `json:"siacoinInputs,omitempty"` - SiacoinOutputs []SiacoinOutput `json:"siacoinOutputs,omitempty"` - SiafundInputs []V2SiafundInput `json:"siafundInputs,omitempty"` - SiafundOutputs []SiafundOutput `json:"siafundOutputs,omitempty"` - FileContracts []V2FileContract `json:"fileContracts,omitempty"` - FileContractRevisions []V2FileContractRevision `json:"fileContractRevisions,omitempty"` - FileContractResolutions []V2FileContractResolution `json:"fileContractResolutions,omitempty"` - Attestations []Attestation `json:"attestations,omitempty"` - ArbitraryData []byte `json:"arbitraryData,omitempty"` - NewFoundationAddress *Address `json:"newFoundationAddress,omitempty"` - MinerFee Currency `json:"minerFee"` - }{ - ID: txn.ID(), - SiacoinInputs: txn.SiacoinInputs, - SiacoinOutputs: txn.SiacoinOutputs, - SiafundInputs: txn.SiafundInputs, - SiafundOutputs: txn.SiafundOutputs, - FileContracts: txn.FileContracts, - FileContractRevisions: txn.FileContractRevisions, - FileContractResolutions: txn.FileContractResolutions, - Attestations: txn.Attestations, - ArbitraryData: txn.ArbitraryData, - NewFoundationAddress: txn.NewFoundationAddress, - MinerFee: txn.MinerFee, - } - return json.Marshal(jsonTxn) + type jsonTxn V2Transaction + return json.Marshal(struct { + ID TransactionID `json:"id"` + jsonTxn + }{txn.ID(), jsonTxn(txn)}) } // ID returns the "semantic hash" of the transaction, covering all of the From f5479afe1d4cc055cc6fc52584a896efc31ba22c Mon Sep 17 00:00:00 2001 From: Nate Date: Wed, 13 Nov 2024 12:01:21 -0800 Subject: [PATCH 3/3] types: add recursion comment --- types/types.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/types.go b/types/types.go index 5ee7c34..fa8c870 100644 --- a/types/types.go +++ b/types/types.go @@ -404,7 +404,7 @@ type Transaction struct { // // For convenience, the transaction's ID is also calculated and included. This field is ignored during unmarshalling. func (txn Transaction) MarshalJSON() ([]byte, error) { - type jsonTxn Transaction + type jsonTxn Transaction // prevent recursion return json.Marshal(struct { ID TransactionID `json:"id"` jsonTxn @@ -704,7 +704,7 @@ type V2Transaction struct { // // For convenience, the transaction's ID is also calculated and included. This field is ignored during unmarshalling. func (txn V2Transaction) MarshalJSON() ([]byte, error) { - type jsonTxn V2Transaction + type jsonTxn V2Transaction // prevent recursion return json.Marshal(struct { ID TransactionID `json:"id"` jsonTxn