Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chain: add Transaction JSON Marshal Unmarshal #1665

Merged
merged 11 commits into from
Nov 5, 2024

Conversation

najeal
Copy link
Contributor

@najeal najeal commented Oct 16, 2024

Close #1633

Few notes:

  • After a Marshal -> Unmarshal, the Transaction looses the Base data and internals like bytes, size, stateKeys.

  • The tests require the file to be part of chain_test package instead of chain, otherwise it ends up with import cycle (seems to come from auth package).
    So the created test is outside the chain package and Transaction.id is unreachable. As a workaround I created a SetID function for testing purpose but it is not ideal. One other solution can be to make the id a public field.

There are also multiple options to avoid import cycle while testing in chain package:

  1. Move Auth Rule Action Object Marshaler AuthFactory AuthbatchVerifier in a separate dedicated package.
  2. Mock the objects coming from auth package used in the tests.

@@ -162,6 +163,8 @@ func (t *Transaction) Size() int { return t.size }

func (t *Transaction) ID() ids.ID { return t.id }

func (t *Transaction) SetID(id ids.ID) { t.id = id }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid exporting a function that overrides the ID of the transaction since it should always match the contents of the transaction

Comment on lines 367 to 370
ID ids.ID `json:"id"`
Actions []byte `json:"actions"`
Auth []byte `json:"auth"`
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include Base here?

return nil, err
}

hexActions, err := codec.Bytes(actionsPacker.Bytes()).MarshalText()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to call MarshalText() on this. We can just pass in the codec.Bytes type directly and this will be handled in json.Marshal

Comment on lines 368 to 369
Actions []byte `json:"actions"`
Auth []byte `json:"auth"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These types should be codec.Bytes so that marshalling them to hex is handled by json.Marshal

Comment on lines 404 to 408
type txJSON struct {
ID ids.ID `json:"id"`
Actions []byte `json:"actions"`
Auth []byte `json:"auth"`
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move the type definition outside of these functions so we don't need to duplicate it?

Comment on lines 416 to 425
var actionsBytes, authBytes codec.Bytes
err = actionsBytes.UnmarshalText(tx.Actions)
if err != nil {
return fmt.Errorf("%w: cannot unmarshal actions text", err)
}
err = authBytes.UnmarshalText(tx.Auth)
if err != nil {
return fmt.Errorf("%w: cannot unmarshal auth text", err)
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment for removing UnmarshalText in favor of using codec.Bytes in the txJSON type

Comment on lines 443 to 448
t.id = tx.ID
t.TransactionData = TransactionData{
Actions: actions,
}
t.Auth = auth
return nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than trusting the received txID (which could be a mismatch), we should re-marshal the received tx based on the fields (Base, Actions, and Auth) and unmarshal it to populate the txID.

Ideally we can restrict the number of places where we construct a transaction, so that it's clear all of the required fields are always populated.

Right now the only place we create Transaction is in Sign. We could create a new function NewTx(base, actions, auth) that we call within Sign to keep it to a single place where we construct a signed transaction.

Comment on lines 85 to 107
tx := &chain.Transaction{
TransactionData: chain.TransactionData{
Actions: []chain.Action{
&mockTransferAction{
To: codec.Address{1, 2, 3, 4},
Value: 4,
Memo: []byte("hello"),
},
&mockTransferAction{
To: codec.Address{4, 5, 6, 7},
Value: 123,
Memo: []byte("world"),
},
&action2{
A: 2,
B: 4,
},
},
},
Auth: &auth.ED25519{
Signer: pk.PublicKey(),
},
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use NewTxData and Sign to produce the signed transaction? This makes sure all fields are populated (currently missing the signature in the ED25519 auth)

Copy link
Contributor Author

@najeal najeal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

few remarks.

chain/transaction.go Show resolved Hide resolved
chain/transaction.go Show resolved Hide resolved
require.NoError(txout.Auth.Verify(context.Background(), prevUnsignedBytes))
require.NoError(txout.Auth.Verify(context.Background(), unsignedBytes))
}

// TestMarshalUnmarshal roughly validates that a transaction packs and unpacks correctly
func TestMarshalUnmarshal(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaronbuchwald it looks like this test is incomplete. No unmarshal is done to verify we get back the same transaction data than the original one. I may open a issue to complete it in another PR?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, feel free to open a PR directly (no need for an issue)

chain/transaction_test.go Outdated Show resolved Hide resolved
chain/transaction_test.go Outdated Show resolved Hide resolved
func TestJSONMarshalUnmarshal(t *testing.T) {
require := require.New(t)

txdata := chain.TransactionData{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: camel case to txData

separate from this PR, but we should be consistent with the variable name throughout this file as well

err = txout.UnmarshalJSON(b, parser)
require.NoError(err)
// cannot check direct Equal between signedTx and txout
// because of the Auth codec.Address that will be different after the unmarshaling.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is auth different after unmarshalling?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private field addr is not Marshalled, looking at the address() function is shows:

func (d *ED25519) address() codec.Address {
	if d.addr == codec.EmptyAddress {
		d.addr = NewED25519Address(d.Signer)
	}
	return d.addr
}

I see the same logic for BLS, ED25519, SECP256R1.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The field by field check seems a bit brittle. Could we reduce this to a single check that the transaction unmarshalled from JSON produces the identical bytes as the original tx?


parser := chaintest.NewParser(nil, actionCodec, authCodec, nil)

var txout chain.Transaction
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: rename to txFromJSON

Comment on lines 133 to 145
require.Equal(signedTx.Base, txout.Base)
require.Equal(signedTx.Actions, txout.Actions)
require.Equal(signedTx.ID(), txout.ID())
require.Equal(signedTx.Bytes(), txout.Bytes())
require.Equal(signedTx.Size(), txout.Size())
// verify txout Auth is able to verify unsigned bytes of both original tx and unmarshaled tx.
prevUnsignedBytes, err := signedTx.UnsignedBytes()
require.NoError(err)
unsignedBytes, err := txout.UnsignedBytes()
require.NoError(err)
require.NoError(txout.Auth.Verify(context.Background(), prevUnsignedBytes))
require.NoError(txout.Auth.Verify(context.Background(), unsignedBytes))
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than perform every check one by one, could we do a single require.Equal(signedTx, txFromJSON) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatly, I can't because of the Auth.addr being different. I thought it was expected behavior.
The way I verified Auth correctness, I checked it can verify both the (prev/new)unsignedBytes.

chain/transaction.go Outdated Show resolved Hide resolved
ID ids.ID `json:"id"`
Actions codec.Bytes `json:"actions"`
Auth codec.Bytes `json:"auth"`
Base codec.Bytes `json:"base"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we keep Base as the original type since we should be able to encode/decode this directly as JSON with no issues?

Comment on lines 405 to 407
if t.Base != nil {
t.Base.Marshal(basePacker)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for this check? Base should never be nil.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should not, but as it is a pointer I prefered to check it instead of risking a panic 🙂 but anyway we don't need to use the codec as you suggested in another comment 👍

Comment on lines 166 to 175
p := codec.NewWriter(0, consts.NetworkSizeLimit)
if _, err := tx.UnsignedBytes(); err != nil {
return nil, err
}
if err := tx.Marshal(p); err != nil {
return nil, err
}
if err := p.Err(); err != nil {
return nil, err
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move creating the codec to directly before it's used and use the unsigned bytes to calculate how much memory to pre-allocate for the transaction (same as before this should be len(unsignedBytes) + consts.ByteLen + auth.Size) )

@aaronbuchwald aaronbuchwald enabled auto-merge (squash) November 5, 2024 22:19
@aaronbuchwald aaronbuchwald merged commit 3a6c0c2 into ava-labs:main Nov 5, 2024
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add Tx JSON Marshal Support
2 participants