-
Notifications
You must be signed in to change notification settings - Fork 81
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
*: add invocations to applicationlog #3569
base: master
Are you sure you want to change the base?
Conversation
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.
A good prototype, but I have several design questions that we should solve before review.
@roman-khimov, I think we need some third opinion on these topics. |
How about |
It leads to new execution context creation, thus it's a valid part of invocation tree. But is this information useful in practice? Dynamic invocations are identified by hash160 of the loaded script, as a result user can't get this script because he knows only its hash. But still we may include dynamic invocations into the resulting Invocations tree with some special field like |
f4e91f5
to
dfd5c9b
Compare
Picking this up again. I rebased the branch onto latest master and processed some of the feedback. In particular
Note; It was unclear to me based on #3569 (comment) if I should have made it a tree or keep it flat. I kept it flat for now. If the feature is enabled the applicationlog output looks as follows "invocations": [
{
"contract_hash": "0xd2a4cff31913016155e38e474a2c06d08be276cf",
"method": "transfer",
"arguments": {
"type": "Array",
"value": [
{
"type": "ByteString",
"value": "krOcd6pg8ptXwXPO2Rfxf9Mhpus="
},
{
"type": "ByteString",
"value": "AZelPVEEY0csq+FRLl/HJ9cW+Qs="
},
{
"type": "Integer",
"value": "1000000000000"
},
{
"type": "Any"
}
]
},
"arguments_count": 4,
"is_valid": true
}
] and in disabled state it returns "invocations": [] I'm looking for feedback on the above before taking care of covering |
@AnnaShaleva can this PR also get some review love please |
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.
I don't see any limits for the overall size of saved data. While the feature is optional we still need to protect node from abuse.
ping |
I feel there is a lot of resistance against this PR, but I can't really tell why. Other PRs I opened (later than this one, like #3674 and #3677) were swiftly reviewed (multiple times). This one however takes 2-3 weeks per update to get another response despite multiple pings. Can any of you @AnnaShaleva @roman-khimov elaborate please? If I understand where this resistance is coming from I can perhaps do something about it. |
@ixje, zero resistance. Sorry, it's just that there are too many things to handle at once. @AnnaShaleva will get back to it soon. |
@ixje sorry for the delay. Actually, when we don't need #3569 (comment) and #3569 (comment) to be fixed, there are only a couple of non-critical issues left to be fixed, so the PR is almost ready and looks good. |
@AnnaShaleva some RPC server tests and I've spend some time trying to understand why if invocLen := len(aer.Invocations); invocLen > 0 {
w.WriteVarUint(uint64(invocLen))
for i := range aer.Invocations {
aer.Invocations[i].EncodeBinaryWithContext(w, sc)
}
} Putting a breakpoint in |
} | ||
params, err := stackitem.FromJSONWithTypes(aux.Arguments) | ||
if err != nil { | ||
return 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.
You need to handle the case of invalid type ([]byte(fmt.Sprintf("error: %v", err))
set in the marshaller). Try to unmarshal aux.Arguments
into string and continue execution flow if unmarshalling is successful (exactly like for state.Execution
unmarshalling. Let's add Encode -> Decode -> marshal JSON -> unmarshal JSON unittest for ConstructIvocation
structure for the case when argumentsBytes is 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.
I'm starting to wonder if this
if err != nil {
item = []byte(fmt.Sprintf(`"error: %v"`, err))
}
in MarhshallJSON()
is ever not nil
. I'm sure I initially copied it from how NotificationEvent
is working. But looking at it now the stackitem.Deserialize(ci.argumentsBytes)
call above it uses a deserialization context with allowInvalid: false
therefore FromJSONWithTypes
should never fail. That logic does not exist for the NotificationEvent
MarshallJSON()
function.
-edit-
The testcases prove this is unreachable
pkg/core/interop/contract/call.go
Outdated
truncated = false | ||
argBytes []byte | ||
) | ||
if argBytes, err = ic.DAO.GetItemCtx().Serialize(stackitem.NewArray(args), false); err != 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.
I've found that argBytes
gets modified by the time EncodeBinaryWithContext
of ContractInvocation
is called (when persisting to storage). Specifically, the type changes from an Array
to a Struct
.
If I replace ic.DAO.GetItemCtx()
with stackitem.NewSerializationContext()
the content of argBytes
is preserved. I'm not sure what the implications of this are but wanted to point this out as I find this somewhat worrisome. @AnnaShaleva @roman-khimov
I found this out by a type assertion failure in MarshallJSON()
of ContractInvocation
, specifically at this line
item, err = stackitem.ToJSONWithTypes(si.(*stackitem.Array))
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's an unexpected behaviour, let me check.
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.
I found this specifically happens when trying to read the following T5 log (it's in block 2872 iirc so you don't have to sync much for it)
curl --location 'http://127.0.0.1:20332' \
--header 'Content-Type: text/plain' \
--data '{
"jsonrpc": "2.0",
"method": "getapplicationlog",
"params": ["0x2f7b0f78df942212334c0b6c03239a1afc4518aeb4461cb5b77b31aaa9e0c1a5"],
"id": 1
}
'
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.
Ref. #3569 (comment).
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3569 +/- ##
==========================================
- Coverage 83.20% 83.02% -0.18%
==========================================
Files 334 336 +2
Lines 46488 47005 +517
==========================================
+ Hits 38681 39028 +347
- Misses 6234 6378 +144
- Partials 1573 1599 +26 ☔ View full report in Codecov by Sentry. |
// MarshalJSON implements the json.Marshaler interface. | ||
func (ci ContractInvocation) MarshalJSON() ([]byte, error) { | ||
var item []byte | ||
if ci.argumentsBytes != 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.
if ci.Arguments == nil && ci.argumentsBytes != nil
ci.Hash = aux.Hash | ||
ci.ArgumentsCount = aux.ArgumentsCount | ||
ci.Truncated = aux.Truncated | ||
ci.argumentsBytes = argsBytes |
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.
Add ci.Arguments = aux.Arguments
.
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.
I explicitly removed this because otherwise MarshalJSON
and UnmarshalJSON
are not symmetrical. Is that really what we want?
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.
Yes. Because the common usage scenario is constructing response to getapplicationlog
which means: decode binary from the DB, marshal it to JSON o nthe server side, then unmarshal from JSON on the client side. Client needs only ci.Arguments
, it's not interested in ci.argumentsBytes
, so I consider it useless to spend time serializing ci.Arguments
in UnmarshalJSON
.
pkg/core/state/notification_event.go
Outdated
@@ -120,6 +130,9 @@ func (aer *AppExecResult) DecodeBinary(r *io.BinReader) { | |||
aer.Stack = arr | |||
r.ReadArray(&aer.Events) | |||
aer.FaultException = r.ReadString() | |||
if aer.VMState&0x80 != 0 { |
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.
And reuse this constant here.
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.
👀
neo-go/pkg/encoding/bigint/bigint.go
Line 31 in 673b26f
isNeg := data[size-1]&0x80 != 0 |
Line 57 in 673b26f
if buf[l-1]&0x80 != 0 { |
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.
I'm not a fan of this solution, it's harder to maintain. And I treat the case of VM state as kind of special, because originally it's not designed to store additional information in the MSB (and strictly speaking, it can even be any other free bit).
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.
VM and bigint cases are about negative/MSB, it's more obvious from the purpose of this code (especially in the bigint case). This 0x80 however is pure magic and it can be 0x20 or whatever else. Compatibility kludge in its best, we need to remember it. If we're to ever change the DB scheme this will be removed for sure.
ci.Hash = aux.Hash | ||
ci.ArgumentsCount = aux.ArgumentsCount | ||
ci.Truncated = aux.Truncated | ||
ci.argumentsBytes = argsBytes |
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.
Yes. Because the common usage scenario is constructing response to getapplicationlog
which means: decode binary from the DB, marshal it to JSON o nthe server side, then unmarshal from JSON on the client side. Client needs only ci.Arguments
, it's not interested in ci.argumentsBytes
, so I consider it useless to spend time serializing ci.Arguments
in UnmarshalJSON
.
pkg/core/state/notification_event.go
Outdated
const ( | ||
MostSignificantBit = 0x80 | ||
ClearMostSignificantBitMask = MostSignificantBit ^ 0xFF |
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.
Make them unexported, add documentation.
func TestContractInvocation_JSON_InvalidArguments(t *testing.T) { | ||
ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, false) | ||
|
||
out, err := json.Marshal(&ci) | ||
require.NoError(t, err) | ||
|
||
var ci2 ContractInvocation | ||
err = json.Unmarshal(out, &ci2) | ||
require.NoError(t, err) | ||
require.Equal(t, ci, &ci2) | ||
} | ||
|
||
func TestContractInvocation_JSON_ValidArguments(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.
Merge these two tests into one TestContractInvocation_MarshalUnmarshalJSON
, create two sub-tests (t.Run
), use testserdes.MarshalUnmarshalJSON
for non-error case.
func TestContractInvocation_EncodeDecode_InvalidArguments(t *testing.T) { | ||
ci := NewContractInvocation(util.Uint160{}, "fakeMethodCall", nil, 1, false) | ||
|
||
w := io.NewBufBinWriter() | ||
ci.EncodeBinary(w.BinWriter) | ||
require.NoError(t, w.Err) | ||
|
||
ciReader := io.NewBinReaderFromBuf(w.Bytes()) | ||
ci2 := ContractInvocation{} | ||
ci2.DecodeBinary(ciReader) | ||
require.Equal(t, ci, &ci2) | ||
} | ||
|
||
func TestContractInvocation_EncodeDecode_ValidArguments(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.
Ditto for these two, and we have testserdes.EncodeDecodeBinary
.
} | ||
|
||
// contractInvocationAux is an auxiliary struct for ContractInvocation JSON marshalling. | ||
type contractInvocationAux struct { |
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.
And move this declaration to the top of the file, place it under ContractInvocation
declaration. Usually all strucrtures are declared in the start of the file, then methods/functions follow.
) | ||
|
||
// NewContractInvocation returns a new ContractInvocation. | ||
func NewContractInvocation(hash util.Uint160, method string, argBytes []byte, argCnt uint32, truncated bool) *ContractInvocation { |
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.
Move it under types declaration.
if b := r.ReadVarBytes(); len(b) > 0 { | ||
ci.argumentsBytes = b | ||
} |
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.
OK, then I have even better suggestion: in EncodeBinary
write ci.Truncated
firstly, then write ci.Arguments
only if !ci.Truncated
. In DecodeBinary
read ci.Truncated
firstly, then use ci.argumentsBytes = r.ReadVarBytes()
only if !ci.Truncated
.
truncated bool | ||
argBytes []byte | ||
) | ||
if argBytes, err = stackitem.NewSerializationContext().Serialize(stackitem.NewArray(args), false); err != 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.
Regarding this: #3569 (comment)
This behaviour is likely caused by the behaviour of (e Element) Array()
: args
is an underlying content of either VM Array
or VM Struct
:
Lines 96 to 101 in 673b26f
func (e Element) Array() []stackitem.Item { | |
switch t := e.value.(type) { | |
case *stackitem.Array: | |
return t.Value().([]stackitem.Item) | |
case *stackitem.Struct: | |
return t.Value().([]stackitem.Item) |
When serializing it, you enforce
args
to be wrapped into array irrespectively of the original type. However, if the old serialization context is used, the information about slice type is stored in the context, hence we've got a struct. The correct behaviour is to preserve the original type of arguments. To solve this problem we need:
argsItm := ic.VM.Estack().Pop()
args := argsItm.Array()
...
ic.DAO.GetItemCtx().Serialize(argsItm, false); err != 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 still fails after changing to this
argsItm := ic.VM.Estack().Pop()
args := argsItm.Array()
I tried the old one with the
ic.DAO.GetItemCtx().Serialize(stackitem.NewArray(args), false)
and with
ic.DAO.GetItemCtx().Serialize(argsItem.Itm(), false)
In both cases the underlying type changes from Array
to Struct
Everything is almost done, so once finished, please, format commits according to https://github.com/nspcc-dev/.github/blob/master/git.md#commits and add a proper signoff to the commit messages. |
I'll wait with the squashing and signoff until everything is really approved. I thought I was close a couple of times before and then the next review requests changes over parts that 'passed' the previous reviews. |
Problem
neo-project/neo#3386
Solution
Implement as extension. Moved the discussion from Dora's backend PR to here
To do
Do we want to limit the stack item depth (think
MaxJSONDepth
) or are we content with just limiting the total stack arguments?