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

feat(gnovm): implement overflow checking at VM level #3250

Merged
merged 21 commits into from
Jan 9, 2025

Conversation

mvertes
Copy link
Contributor

@mvertes mvertes commented Nov 29, 2024

I propose that we implement overflow checking directly in gnovm opcodes, and that gnovm always enforces overflow checking. Overflow checking becomes a capacity of the Gno language and the Gno virtual machine.

It's important for a smart contract platform to offer by default, and without user or developer effort, the strongest guarantees on numerical operations.

In that topic, Gno would be superior to the standard Go runtime which, like C and most other languages, don't address this internally beside constants (to preserve the best possible native performances), and rely on external user code.

It would also simplify the user code and avoid to use specific libraries.
For example, in gnovm/stdlibs/std/coins.go, for the Coin.Add method:

Before:

import "math/overflow"

func (c Coin) Add(other Coin) Coin {
    mustMatchDenominations(c.Denom, other.Denom)
 
    sum, ok := overflow.Add64(c.Amount, other.Amount)
    if !ok {
        panic("coin add overflow/underflow: " +
              strconv.Itoa(int(c.Amount)) + " +/- " +
              strconv.Itoa(int(other.Amount)))
    }

    c.Amount = sum
    return c
}

After:

func (c Coin) Add(other Coin) Coin {
    mustMatchDenominations(c.Denom, other.Denom)
    c.Amount += other.Amount
    return c
} 

with the same behaviour for overflow checking. Note also that the new version, is not only simpler, but also faster, because overflow checking is performed natively, and not interpreted.

Integer overflow handling is only implemented for signed integers. Unsigned integers, on purpose, just wrap around when reaching their maximum or minimum values. This is intended to support all crypto, hash and bitwise operations which may rely on that wrap around property. Division by zero is still handled both in signed and unsigned integers.

Note: from now, on security level, the use of unsigned integers for standard numeric operations should be probably considered suspicious.

Benchmark

To measure the impact of overflow, I execute the following benchmarks:

First a micro benchmark comparing an addition of 2 ints, with and without overflow:

//go:noinline
func AddNoOverflow(x, y int) int { return x + y }

func BenchmarkAddNoOverflow(b *testing.B) {
    x, y := 4, 3
    c := 0
    for range b.N {
        c = AddNoOverflow(x, y)
    }
    if c != 7 {
        b.Error("invalid result")
    }
}

func BenchmarkAddOverflow(b *testing.B) {
    x, y := 4, 3
    c := 0
    for range b.N {
        c = overflow.Addp(x, y)
    }
    if c != 7 {
        b.Error("invalid result")
    }
}

The implementation of overflow checking is taken from http://github.com/gnolang/overflow, already used in tm2.

It gives the following results:

$ go test -v- run=^# -benchmem -bench=Overflow
goos: darwin
goarch: arm64
pkg: github.com/gnolang/gno/gnovm/pkg/gnolang
cpu: Apple M1
BenchmarkAddNoOverflow
BenchmarkAddNoOverflow-8    1000000000           0.9392 ns/op          0 B/op          0 allocs/op
BenchmarkAddOverflow
BenchmarkAddOverflow-8      568881582            2.101 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/gnolang/gno/gnovm/pkg/gnolang    2.640s

Checking overflow doubles the execution time of an addition from 1 ns/op to 2 ns/op.

But at 2 ns, the total time is still an order of magnitude lower than the cost of running the VM.
The impact of overflow check doesn't even appear when benchmarking at VM level with the following:

func BenchmarkOpAdd(b *testing.B) {
    m := NewMachine("bench", nil)
    x := TypedValue{T: IntType}
    x.SetInt(4)
    y := TypedValue{T: IntType}
    y.SetInt(3)

    b.ResetTimer()

    for range b.N {
        m.PushOp(OpHalt)
        m.PushExpr(&BinaryExpr{})
        m.PushValue(x)
        m.PushValue(y)
        m.PushOp(OpAdd)
        m.Run()
    }
}

Which gives something like:

$ go test -v -benchmem -bench=OpAdd -run=^#
goos: darwin
goarch: arm64
pkg: github.com/gnolang/gno/gnovm/pkg/gnolang
cpu: Apple M1
BenchmarkOpAdd
BenchmarkOpAdd-8    16069832            74.41 ns/op      163 B/op          1 allocs/op
PASS
ok      github.com/gnolang/gno/gnovm/pkg/gnolang    1.526

Where the execution time varie from 60 ns/op to 100 ns/op for both versions of addition, with or without overflow.

Related PRs and issues

Contributors' checklist...
  • Added new tests, or not needed, or not feasible
  • Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory
  • Updated the official documentation or not needed
  • No breaking changes were made, or a BREAKING CHANGE: xxx message was included in the description
  • Added references to related issues and PRs
  • Provided any useful hints for running manual tests

@mvertes mvertes added the 📦 🤖 gnovm Issues or PRs gnovm related label Nov 29, 2024
@mvertes mvertes self-assigned this Nov 29, 2024
@Gno2D2
Copy link
Collaborator

Gno2D2 commented Nov 29, 2024

🛠 PR Checks Summary

All Automated Checks passed. ✅

Manual Checks (for Reviewers):
  • IGNORE the bot requirements for this PR (force green CI check)
Read More

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

✅ Automated Checks (for Contributors):

🟢 Maintainers must be able to edit this pull request (more info)

☑️ Contributor Actions:
  1. Fix any issues flagged by automated checks.
  2. Follow the Contributor Checklist to ensure your PR is ready for review.
    • Add new tests, or document why they are unnecessary.
    • Provide clear examples/screenshots, if necessary.
    • Update documentation, if required.
    • Ensure no breaking changes, or include BREAKING CHANGE notes.
    • Link related issues/PRs, where applicable.
☑️ Reviewer Actions:
  1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.
📚 Resources:
Debug
Automated Checks
Maintainers must be able to edit this pull request (more info)

If

🟢 Condition met
└── 🟢 The pull request was created from a fork (head branch repo: mvertes/gno)

Then

🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

Manual Checks
**IGNORE** the bot requirements for this PR (force green CI check)

If

🟢 Condition met
└── 🟢 On every pull request

Can be checked by

  • Any user with comment edit permission

Copy link

codecov bot commented Nov 29, 2024

Codecov Report

Attention: Patch coverage is 87.50000% with 23 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
gnovm/pkg/gnolang/op_binary.go 89.53% 14 Missing and 4 partials ⚠️
gnovm/pkg/gnolang/op_inc_dec.go 50.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@petar-dambovaliev
Copy link
Contributor

I like this

@mvertes mvertes marked this pull request as ready for review December 8, 2024 16:04
@mvertes mvertes marked this pull request as draft December 9, 2024 08:44
@github-actions github-actions bot added the 📦 🌐 tendermint v2 Issues or PRs tm2 related label Dec 9, 2024
@ltzmaxwell ltzmaxwell self-requested a review December 9, 2024 13:40
@Kouteki Kouteki added the in focus Core team is prioritizing this work label Dec 9, 2024
@github-actions github-actions bot added the 🧾 package/realm Tag used for new Realms or Packages. label Dec 18, 2024
@mvertes mvertes marked this pull request as ready for review December 19, 2024 15:17
@kristovatlas kristovatlas self-requested a review December 19, 2024 22:33
@mvertes mvertes enabled auto-merge (squash) January 7, 2025 08:55
@zivkovicmilos zivkovicmilos added this to the 🚀 Mainnet beta launch milestone Jan 8, 2025
@mvertes mvertes merged commit 68aff64 into gnolang:master Jan 9, 2025
79 of 80 checks passed
albttx pushed a commit that referenced this pull request Jan 10, 2025
I propose that we implement overflow checking directly in gnovm opcodes,
and that gnovm always enforces overflow checking. Overflow checking
becomes a capacity of the Gno language and the Gno virtual machine.

It's important for a smart contract platform to offer by default, and
without user or developer effort, the strongest guarantees on numerical
operations.

In that topic, Gno would be superior to the standard Go runtime which,
like C and most other languages, don't address this internally beside
constants (to preserve the best possible native performances), and rely
on external user code.

It would also simplify the user code and avoid to use specific
libraries.
For example, in `gnovm/stdlibs/std/coins.go`, for the `Coin.Add` method:

Before:

```go
import "math/overflow"

func (c Coin) Add(other Coin) Coin {
    mustMatchDenominations(c.Denom, other.Denom)
 
    sum, ok := overflow.Add64(c.Amount, other.Amount)
    if !ok {
        panic("coin add overflow/underflow: " +
              strconv.Itoa(int(c.Amount)) + " +/- " +
              strconv.Itoa(int(other.Amount)))
    }

    c.Amount = sum
    return c
}
```

After:

```go
func (c Coin) Add(other Coin) Coin {
    mustMatchDenominations(c.Denom, other.Denom)
    c.Amount += other.Amount
    return c
} 
```
with the same behaviour for overflow checking. Note also that the new
version, is not only simpler, but also faster, because overflow checking
is performed natively, and not interpreted.

Integer overflow handling is only implemented for signed integers.
Unsigned integers, on purpose, just wrap around when reaching their
maximum or minimum values. This is intended to support all crypto, hash
and bitwise operations which may rely on that wrap around property.
Division by zero is still handled both in signed and unsigned integers.

Note: from now, on security level, the use of unsigned integers for
standard numeric operations should be probably considered suspicious.

## Benchmark

To measure the impact of overflow, I execute the following benchmarks:

First a micro benchmark comparing an addition of 2 ints, with and
without overflow:


```go
//go:noinline
func AddNoOverflow(x, y int) int { return x + y }

func BenchmarkAddNoOverflow(b *testing.B) {
    x, y := 4, 3
    c := 0
    for range b.N {
        c = AddNoOverflow(x, y)
    }
    if c != 7 {
        b.Error("invalid result")
    }
}

func BenchmarkAddOverflow(b *testing.B) {
    x, y := 4, 3
    c := 0
    for range b.N {
        c = overflow.Addp(x, y)
    }
    if c != 7 {
        b.Error("invalid result")
    }
}
```

The implementation of overflow checking is taken from
http://github.com/gnolang/overflow, already used in tm2.

It gives the following results:

```console
$ go test -v- run=^# -benchmem -bench=Overflow
goos: darwin
goarch: arm64
pkg: github.com/gnolang/gno/gnovm/pkg/gnolang
cpu: Apple M1
BenchmarkAddNoOverflow
BenchmarkAddNoOverflow-8    1000000000           0.9392 ns/op          0 B/op          0 allocs/op
BenchmarkAddOverflow
BenchmarkAddOverflow-8      568881582            2.101 ns/op           0 B/op          0 allocs/op
PASS
ok      github.com/gnolang/gno/gnovm/pkg/gnolang    2.640s
```

Checking overflow doubles the execution time of an addition from 1 ns/op
to 2 ns/op.

But at 2 ns, the total time is still an order of magnitude lower than
the cost of running the VM.
The impact of overflow check doesn't even appear when benchmarking at VM
level with the following:

```go
func BenchmarkOpAdd(b *testing.B) {
    m := NewMachine("bench", nil)
    x := TypedValue{T: IntType}
    x.SetInt(4)
    y := TypedValue{T: IntType}
    y.SetInt(3)

    b.ResetTimer()

    for range b.N {
        m.PushOp(OpHalt)
        m.PushExpr(&BinaryExpr{})
        m.PushValue(x)
        m.PushValue(y)
        m.PushOp(OpAdd)
        m.Run()
    }
}
```

Which gives something like:

```console
$ go test -v -benchmem -bench=OpAdd -run=^#
goos: darwin
goarch: arm64
pkg: github.com/gnolang/gno/gnovm/pkg/gnolang
cpu: Apple M1
BenchmarkOpAdd
BenchmarkOpAdd-8    16069832            74.41 ns/op      163 B/op          1 allocs/op
PASS
ok      github.com/gnolang/gno/gnovm/pkg/gnolang    1.526
```

Where the execution time varie from 60 ns/op to 100 ns/op for both
versions of addition, with or without overflow.

## Related PRs and issues

- PRs: 
    - #3197 
    - #3192
    - #3117
    - #2983
    - #2905 
    - #2698
- Issues: 
    - #2873
    - #1844
    - #1729


<!-- please provide a detailed description of the changes made in this
pull request. -->

<details><summary>Contributors' checklist...</summary>

- [ ] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
</details>
mvertes added a commit to mvertes/gno that referenced this pull request Jan 14, 2025
…nolang#3250)"

This reverts commit 68aff64.

This PR was merged before discussions were complete. It was finally
rejected because it does not comply with Go specification, which
stipulates that overflow on signed integers do not trigger runtime
panic. See https://go.dev/ref/spec#Integer_overflow
@mvertes mvertes deleted the gno-overflow branch January 14, 2025 08:36
thehowl pushed a commit that referenced this pull request Jan 14, 2025
…3508)

Revert #3250

This reverts commit 68aff64.

This PR was merged before discussions were complete. It was finally
rejected because it does not comply with Go specification, which
stipulates that overflow on signed integers do not trigger runtime
panic. See https://go.dev/ref/spec#Integer_overflow
@Kouteki Kouteki removed the in focus Core team is prioritizing this work label Jan 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
📦 🌐 tendermint v2 Issues or PRs tm2 related 📦 🤖 gnovm Issues or PRs gnovm related 🧾 package/realm Tag used for new Realms or Packages.
Projects
Development

Successfully merging this pull request may close these issues.

5 participants