Skip to content

Commit

Permalink
Merge pull request #231 from SiaFoundation/nate/add-txn-id
Browse files Browse the repository at this point in the history
Add ID field to Transaction and V2Transaction JSON encoding
  • Loading branch information
n8maninger authored Nov 14, 2024
2 parents 7b530e0 + f5479af commit 0b6d8d1
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 0 deletions.
22 changes: 22 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,17 @@ type Transaction struct {
Signatures []TransactionSignature `json:"signatures,omitempty"`
}

// MarshalJSON implements json.Marshaller.
//
// 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 // prevent recursion
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
// transaction's effects, but not incidental data such as signatures. This
// ensures that the ID will remain stable (i.e. non-malleable).
Expand Down Expand Up @@ -689,6 +700,17 @@ type V2Transaction struct {
MinerFee Currency `json:"minerFee"`
}

// MarshalJSON implements json.Marshaller.
//
// 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 // prevent recursion
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
// 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).
Expand Down
74 changes: 74 additions & 0 deletions types/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 0b6d8d1

Please sign in to comment.