-
Notifications
You must be signed in to change notification settings - Fork 111
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
Conversation
chain/transaction.go
Outdated
@@ -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 } |
There was a problem hiding this comment.
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
chain/transaction.go
Outdated
ID ids.ID `json:"id"` | ||
Actions []byte `json:"actions"` | ||
Auth []byte `json:"auth"` | ||
} |
There was a problem hiding this comment.
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?
chain/transaction.go
Outdated
return nil, err | ||
} | ||
|
||
hexActions, err := codec.Bytes(actionsPacker.Bytes()).MarshalText() |
There was a problem hiding this comment.
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
chain/transaction.go
Outdated
Actions []byte `json:"actions"` | ||
Auth []byte `json:"auth"` |
There was a problem hiding this comment.
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
chain/transaction.go
Outdated
type txJSON struct { | ||
ID ids.ID `json:"id"` | ||
Actions []byte `json:"actions"` | ||
Auth []byte `json:"auth"` | ||
} |
There was a problem hiding this comment.
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?
chain/transaction.go
Outdated
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) | ||
} | ||
|
There was a problem hiding this comment.
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
chain/transaction.go
Outdated
t.id = tx.ID | ||
t.TransactionData = TransactionData{ | ||
Actions: actions, | ||
} | ||
t.Auth = auth | ||
return nil |
There was a problem hiding this comment.
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.
chain/transaction_test.go
Outdated
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(), | ||
}, | ||
} |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
few remarks.
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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
func TestJSONMarshalUnmarshal(t *testing.T) { | ||
require := require.New(t) | ||
|
||
txdata := chain.TransactionData{ |
There was a problem hiding this comment.
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
chain/transaction_test.go
Outdated
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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
chain/transaction_test.go
Outdated
|
||
parser := chaintest.NewParser(nil, actionCodec, authCodec, nil) | ||
|
||
var txout chain.Transaction |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: rename to txFromJSON
chain/transaction_test.go
Outdated
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)) | ||
} |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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
ID ids.ID `json:"id"` | ||
Actions codec.Bytes `json:"actions"` | ||
Auth codec.Bytes `json:"auth"` | ||
Base codec.Bytes `json:"base"` |
There was a problem hiding this comment.
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?
chain/transaction.go
Outdated
if t.Base != nil { | ||
t.Base.Marshal(basePacker) | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👍
Co-authored-by: aaronbuchwald <[email protected]> Signed-off-by: nathan haim <[email protected]>
chain/transaction.go
Outdated
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 | ||
} |
There was a problem hiding this comment.
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)
)
Close #1633
Few notes:
After a Marshal -> Unmarshal, the Transaction looses the
Base
data and internals likebytes
,size
,stateKeys
.The tests require the file to be part of
chain_test
package instead ofchain
, otherwise it ends up with import cycle (seems to come fromauth
package).So the created test is outside the
chain
package andTransaction.id
is unreachable. As a workaround I created aSetID
function for testing purpose but it is not ideal. One other solution can be to make theid
a public field.There are also multiple options to avoid import cycle while testing in
chain
package:Auth
Rule
Action
Object
Marshaler
AuthFactory
AuthbatchVerifier
in a separate dedicated package.auth
package used in the tests.