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

AVM: Set proper costs for json_ref #4096

Merged
merged 19 commits into from
Jun 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion data/transactions/logic/TEAL_opcodes.md
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ When A is a uint64, index 0 is the least significant bit. Setting bit 3 to 1 on
- Opcode: 0x5c {uint8 encoding index}
- Stack: ..., A: []byte → ..., []byte
- decode A which was base64-encoded using _encoding_ E. Fail if A is not base64 encoded with encoding E
- **Cost**: 1 + 1 per 16 bytes
- **Cost**: 1 + 1 per 16 bytes of A
- Availability: v7

`base64` Encodings:
Expand All @@ -769,6 +769,7 @@ Decodes A using the base64 encoding E. Specify the encoding with an immediate ar
- Opcode: 0x5d {string return type}
- Stack: ..., A: []byte, B: []byte → ..., any
- return key B's value from a [valid](jsonspec.md) utf-8 encoded json object A
- **Cost**: 25 + 2 per 7 bytes of A
- Availability: v7

`json_ref` Types:
Expand Down
3 changes: 2 additions & 1 deletion data/transactions/logic/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,8 @@ func OpAllCosts(opName string) []VerCost {
if !ok {
continue
}
cost := spec.OpDetails.docCost()
argLen := len(spec.Arg.Types)
cost := spec.OpDetails.docCost(argLen)
if costs == nil || cost != costs[len(costs)-1].Cost {
costs = append(costs, VerCost{v, v, cost})
} else {
Expand Down
85 changes: 46 additions & 39 deletions data/transactions/logic/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -1033,13 +1033,13 @@ func (cx *EvalContext) step() error {
return nil
}

// oneBlank is a boring stack provided to deets.Cost during checkStep. It is
// blankStack is a boring stack provided to deets.Cost during checkStep. It is
// good enough to allow Cost() to not crash. It would be incorrect to provide
// this stack if there were linear cost opcodes before backBranchEnabledVersion,
// because the static cost would be wrong. But then again, a static cost model
// wouldn't work before backBranchEnabledVersion, so such an opcode is already
// unacceptable. TestLinearOpcodes ensures.
var oneBlank = []stackValue{{Bytes: []byte{}}}
var blankStack = make([]stackValue, 5)

func (cx *EvalContext) checkStep() (int, error) {
cx.instructionStarts[cx.pc] = true
Expand All @@ -1055,7 +1055,7 @@ func (cx *EvalContext) checkStep() (int, error) {
if deets.Size != 0 && (cx.pc+deets.Size > len(cx.program)) {
return 0, fmt.Errorf("%s program ends short of immediate values", spec.Name)
}
opcost := deets.Cost(cx.program, cx.pc, oneBlank)
opcost := deets.Cost(cx.program, cx.pc, blankStack)
if opcost <= 0 {
return 0, fmt.Errorf("%s reported non-positive cost", spec.Name)
}
Expand Down Expand Up @@ -4764,56 +4764,52 @@ func opBase64Decode(cx *EvalContext) error {
cx.stack[last].Bytes = bytes
return nil
}
func hasDuplicateKeys(jsonText []byte) (bool, map[string]json.RawMessage, error) {

func isPrimitiveJSON(jsonText []byte) (bool, error) {
dec := json.NewDecoder(bytes.NewReader(jsonText))
parsed := make(map[string]json.RawMessage)
t, err := dec.Token()
if err != nil {
return false, nil, err
return false, err
}
t, ok := t.(json.Delim)
if !ok || t.(json.Delim).String() != "{" {
return false, nil, fmt.Errorf("only json object is allowed")
}
for dec.More() {
var value json.RawMessage
// get JSON key
key, err := dec.Token()
if err != nil {
return false, nil, err
}
// end of json
if key == '}' {
break
}
// decode value
err = dec.Decode(&value)
if err != nil {
return false, nil, err
}
// check for duplicates
if _, ok := parsed[key.(string)]; ok {
return true, nil, nil
}
parsed[key.(string)] = value
return true, nil
}
return false, parsed, nil
return false, nil
}

func parseJSON(jsonText []byte) (map[string]json.RawMessage, error) {
if !json.Valid(jsonText) {
// parse JSON with Algorand's standard JSON library
var parsed map[interface{}]json.RawMessage
err := protocol.DecodeJSON(jsonText, &parsed)

if err != nil {
// if the error was caused by duplicate keys
if strings.Contains(err.Error(), "cannot decode into a non-pointer value") {
return nil, fmt.Errorf("invalid json text, duplicate keys not allowed")
}

// if the error was caused by non-json object
if strings.Contains(err.Error(), "read map - expect char '{' but got char") {
return nil, fmt.Errorf("invalid json text, only json object is allowed")
}

return nil, fmt.Errorf("invalid json text")
}
// parse json text and check for duplicate keys
hasDuplicates, parsed, err := hasDuplicateKeys(jsonText)
if hasDuplicates {
return nil, fmt.Errorf("invalid json text, duplicate keys not allowed")
}
if err != nil {
return nil, fmt.Errorf("invalid json text, %v", err)

// check whether any keys are not strings
stringMap := make(map[string]json.RawMessage)
for k, v := range parsed {
key, ok := k.(string)
if !ok {
return nil, fmt.Errorf("invalid json text")
}
stringMap[key] = v
}
return parsed, nil

return stringMap, nil
}

func opJSONRef(cx *EvalContext) error {
// get json key
last := len(cx.stack) - 1
Expand All @@ -4837,6 +4833,17 @@ func opJSONRef(cx *EvalContext) error {
var stval stackValue
_, ok = parsed[key]
if !ok {
// if the key is not found, first check whether the JSON text is the null value
// by checking whether it is a primitive JSON value. Any other primitive
// (or array) would have thrown an error previously during `parseJSON`.
isPrimitive, err := isPrimitiveJSON(cx.stack[last].Bytes)
if err == nil && isPrimitive {
err = fmt.Errorf("invalid json text, only json object is allowed")
}
if err != nil {
return fmt.Errorf("error while parsing JSON text, %v", err)
}

return fmt.Errorf("key %s not found in JSON text", key)
}

Expand Down
Loading