Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .github/workflows/ci-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,28 @@ jobs:
TRAVIS_PULL_REQUEST_BRANCH: ${{ github.head_ref }}
TRAVIS_BRANCH: ${{ github.ref_name }}

mesh-checker:
name: Run Mesh API integration tests (amd64)
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- *checkout-harmony
- name: Download and extract harmony binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8.0.1
with:
name: harmony-binaries-amd64
path: go/src/github.com/harmony-one/harmony/bin
- *prepare-harmony-binaries

- name: Run Mesh API checker
run: bash ./test/localnet.sh mesh
working-directory: go/src/github.com/harmony-one/harmony
env:
HARMONY_BINARY_BUILDER: "true"
HARMONY_TEST_REF: 6afe7cdc1ecdb920d1c9a19d1b1ca912d3a590ab
LOCALNET_ARCH: amd64

pyhmy-checker:
name: Run pyhmy tests (${{ matrix.arch }})
needs: build
Expand Down
24 changes: 15 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ SHELL := bash
EPOCH_TO_WAIT ?=5
EXTRA_NODES_FILE ?="./test/configs/local-extra-nodes.txt"

.PHONY: all help libs exe race trace-pointer debug debug-ext debug-kill test test-all test-go test-integration test-rpc test-rpc-attach test-rosetta test-rosetta-attach test-pyhmy test-api test-api-attach linux_static deb_init deb_build deb debpub_dev debpub_prod rpm_init rpm_build rpm rpmpub_dev rpmpub_prod clean distclean docker go-vet go-test docker build_localnet_validator protofiles travis_go_checker travis_rpc_checker travis_rosetta_checker debug-start-log debug-stop-log debug-restart-log debug-delete-log
.PHONY: all help libs exe race trace-pointer debug debug-ext debug-kill test test-all test-go test-integration test-rpc test-rpc-attach test-mesh test-mesh-attach test-rosetta test-rosetta-attach test-pyhmy test-api test-api-attach linux_static deb_init deb_build deb debpub_dev debpub_prod rpm_init rpm_build rpm rpmpub_dev rpmpub_prod clean distclean docker go-vet go-test docker build_localnet_validator protofiles travis_go_checker travis_rpc_checker travis_rosetta_checker debug-start-log debug-stop-log debug-restart-log debug-delete-log

all: libs
bash ./scripts/go_executable_build.sh -S
Expand All @@ -29,12 +29,14 @@ help:
@echo "test - run native Go checks and unit tests"
@echo "test-all - run native Go checks and all localnet integration suites"
@echo "test-go - run the go test (with go lint, fmt, imports, mod, and generate checks)"
@echo "test-integration - run RPC, Rosetta, and pyhmy localnet tests"
@echo "test-integration - run RPC, Mesh API, and pyhmy localnet tests"
@echo "test-rpc - run the rpc tests"
@echo "test-pyhmy - run the pyhmy tests"
@echo "test-rpc-attach - attach onto the rpc testing docker container for inspection"
@echo "test-rosetta - run the rosetta tests"
@echo "test-rosetta-attach - attach onto the rosetta testing docker container for inspection"
@echo "test-mesh - run the Mesh API tests"
@echo "test-mesh-attach - attach onto the Mesh API testing docker container for inspection"
@echo "test-rosetta - alias for test-mesh"
@echo "test-rosetta-attach - alias for test-mesh-attach"
@echo "linux_static - static build the harmony binary & bootnode along with the MCL & BLS libs (for linux)"
@echo "linux_static_quick - static build the harmony binary & bootnode more quickly without recompiling dependencies (for linux)"
@echo "linux_static_cross_build - cross-compile static Linux binaries for Harmony and Bootnode from macOS"
Expand Down Expand Up @@ -153,7 +155,7 @@ test-go:
bash ./test/go.sh

test-integration:
bash ./test/localnet.sh rpc rosetta pyhmy
bash ./test/localnet.sh rpc mesh pyhmy

test-rpc:
bash ./test/rpc.sh run
Expand All @@ -164,11 +166,15 @@ test-pyhmy:
test-rpc-attach:
bash ./test/rpc.sh attach

test-rosetta:
bash ./test/rosetta.sh run
test-mesh:
bash ./test/mesh.sh run

test-rosetta-attach:
bash ./test/rosetta.sh attach
test-mesh-attach:
bash ./test/mesh.sh attach

test-rosetta: test-mesh

test-rosetta-attach: test-mesh-attach

linux_static:
bash ./scripts/go_executable_build.sh -s
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ Note that all test Docker containers bind several ports to the host machine for
* `9798` - Shard 1 Rosetta (for an explorer)
* `9899` - Shard 0 WS for an explorer
* `9898` - Shard 1 WS for an explorer
> This allows you to use curl, hmy CLI, postman, rosetta-cli, etc... on your host machine to play with or probe the localnet that was used for the test.
> This allows you to use curl, hmy CLI, Postman, the Mesh CLI, etc. on your host machine to play with or probe the localnet that was used for the test.

### Go tests
To run this test, do:
Expand Down Expand Up @@ -192,18 +192,18 @@ make test-rpc-attach
> the current block height of localnet. Reference the documentation for the CLI [here](https://docs.harmony.one/home/wallets/harmony-cli)
> for more details & commands.

### Rosetta tests
### Mesh API tests
To run this test, do:
```bash
make test-rosetta
make test-mesh
```
This test starts a localnet (within the Docker container), **ensures it reaches a consensus**, and runs the Construction & Data API checks using the [rosetta-cli](https://github.com/coinbase/rosetta-cli).
This test starts a localnet (within the Docker container), **ensures it reaches a consensus**, and runs the Construction & Data API checks using the [Coinbase Mesh CLI](https://github.com/coinbase/mesh-cli). The pinned v0.10.4 release still installs its official binary as `rosetta-cli`.
This test also acts as a preliminary integration test (more thorough tests are done on the testnets).
> The config for this test can be found [here](https://github.com/harmony-one/harmony-test/blob/master/localnet/configs/localnet_rosetta_test_s0.json) & [here](https://github.com/harmony-one/harmony-test/blob/master/localnet/configs/localnet_rosetta_test_s1.json)

Similar to the RPC tests, if you wish to debug further with the localnet after the tests are done, open a new shell and run:
```bash
make test-rosetta-attach
make test-mesh-attach
```

## License
Expand Down
2 changes: 2 additions & 0 deletions hmy/tracer.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,8 @@ func (hmy *Harmony) traceTx(ctx context.Context, message core.Message, txctx *tr
case *native.ParityBlockTracer:
rs, err := tracer.GetParityResult()
return rs, err
case *native.RosettaBlockTracer:
return tracer.GetRosettaResult()

case tracers.Tracer:
return tracer.GetResult()
Expand Down
131 changes: 131 additions & 0 deletions hmy/tracers/native/rosetta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package native

import (
"encoding/json"
"math/big"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/hmy/tracers"
)

func init() {
register("RosettaBlockTracer", newRosettaTracer)
}

func newRosettaTracer(_ *tracers.Context, _ json.RawMessage) (tracers.Tracer, error) {
return &RosettaBlockTracer{ParityBlockTracer: &ParityBlockTracer{}}, nil
}

// RosettaBlockTracer converts Parity-style call traces and Harmony staking
// balance movements into the operation format consumed by the Rosetta service.
type RosettaBlockTracer struct {
*ParityBlockTracer

logs []*tracers.RosettaLogItem
frameStart []int
}

func (rbt *RosettaBlockTracer) CaptureStart(env *vm.EVM, from common.Address, to common.Address, create bool, input []byte, gas uint64, value *big.Int) {
rbt.logs = nil
rbt.frameStart = []int{0}
rbt.ParityBlockTracer.CaptureStart(env, from, to, create, input, gas, value)
}

func (rbt *RosettaBlockTracer) CaptureEnd(output []byte, gasUsed uint64, duration time.Duration, err error) {
rbt.markCurrentFrame(err)
rbt.ParityBlockTracer.CaptureEnd(output, gasUsed, duration, err)
}

func (rbt *RosettaBlockTracer) CaptureEnter(_ vm.OpCode, _ common.Address, _ common.Address, _ []byte, _ uint64, _ *big.Int) {
rbt.frameStart = append(rbt.frameStart, len(rbt.logs))
}

func (rbt *RosettaBlockTracer) CaptureExit(_ []byte, _ uint64, err error) {
rbt.markCurrentFrame(err)
}
Comment on lines +41 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Nested CREATE corrupts frame tracking

When a contract calls a staking precompile, subsequently executes CREATE or CREATE2, and then reverts while an ancestor catches the failure, the unmatched creation frame causes CaptureExit to pop the wrong synthetic-log boundary. The reverted staking movement remains marked successful and is emitted as a successful Rosetta operation, producing incorrect transaction data and potential reconciliation failures.


func (rbt *RosettaBlockTracer) markCurrentFrame(err error) {
if len(rbt.frameStart) == 0 {
return
}
last := len(rbt.frameStart) - 1
start := rbt.frameStart[last]
rbt.frameStart = rbt.frameStart[:last]
if err == nil {
return
}
for _, log := range rbt.logs[start:] {
log.IsSuccess = false
log.Reverted = true
}
}

func (rbt *RosettaBlockTracer) formatAction(depth []int, parentErr error, ac *action) *tracers.RosettaLogItem {
value := ac.value
if value == nil {
value = new(big.Int)
}
return &tracers.RosettaLogItem{
IsSuccess: ac.err == nil,
Reverted: parentErr != nil || ac.err != nil,
OP: ac.op,
Depth: depth,
From: &vm.RosettaLogAddressItem{Account: &ac.from},
To: &vm.RosettaLogAddressItem{Account: &ac.to},
Value: value,
}
}

// AddRosettaLog records Harmony-specific balance movements that are not EVM
// calls, such as staking operations.
func (rbt *RosettaBlockTracer) AddRosettaLog(op vm.OpCode, from, to *vm.RosettaLogAddressItem, value *big.Int) {
if value == nil {
value = new(big.Int)
} else {
value = new(big.Int).Set(value)
}
rbt.logs = append(rbt.logs, &tracers.RosettaLogItem{
IsSuccess: true,
OP: op,
Depth: []int{},
From: from,
To: to,
Value: value,
})
}

// GetRosettaResult returns the typed trace used internally by the Rosetta API.
func (rbt *RosettaBlockTracer) GetRosettaResult() ([]*tracers.RosettaLogItem, error) {
results := make([]*tracers.RosettaLogItem, 0, len(rbt.logs))
if rbt.cur != nil {
root := &rbt.cur.action
var finalize func(*action, error, []int)
finalize = func(ac *action, parentErr error, depth []int) {
results = append(results, rbt.formatAction(depth, parentErr, ac))
nextErr := parentErr
if ac.err != nil {
nextErr = ac.err
}
for i, subAction := range ac.subCalls {
subDepth := append(append([]int(nil), depth...), i)
finalize(subAction, nextErr, subDepth)
}
}
for i, subAction := range root.subCalls {
finalize(subAction, root.err, []int{i})
}
}
return append(results, rbt.logs...), nil
}

// GetResult implements tracers.Tracer. Rosetta callers use GetRosettaResult to
// retain the typed internal representation.
func (rbt *RosettaBlockTracer) GetResult() (json.RawMessage, error) {
result, err := rbt.GetRosettaResult()
if err != nil {
return nil, err
}
return json.Marshal(result)
}
108 changes: 108 additions & 0 deletions hmy/tracers/native/rosetta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package native_test

import (
"errors"
"math/big"
"reflect"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/harmony-one/harmony/core/vm"
"github.com/harmony-one/harmony/hmy/tracers"
"github.com/harmony-one/harmony/hmy/tracers/native"
)

func TestRosettaBlockTracerRegistered(t *testing.T) {
tracer, err := tracers.New("RosettaBlockTracer", new(tracers.Context), nil)
if err != nil {
t.Fatalf("RosettaBlockTracer lookup failed: %v", err)
}
rosettaTracer, ok := tracer.(*native.RosettaBlockTracer)
if !ok {
t.Fatalf("unexpected RosettaBlockTracer type: %T", tracer)
}
if _, ok := tracer.(vm.RosettaTracer); !ok {
t.Fatalf("RosettaBlockTracer does not implement vm.RosettaTracer: %T", tracer)
}

from := common.HexToAddress("0x1")
to := common.HexToAddress("0x2")
value := big.NewInt(3)
rosettaTracer.AddRosettaLog(
vm.CALL,
&vm.RosettaLogAddressItem{Account: &from},
&vm.RosettaLogAddressItem{Account: &to},
value,
)
result, err := rosettaTracer.GetRosettaResult()
if err != nil {
t.Fatalf("RosettaBlockTracer result failed: %v", err)
}
if len(result) != 1 {
t.Fatalf("unexpected result length: got %d, want 1", len(result))
}
want := &tracers.RosettaLogItem{
IsSuccess: true,
OP: vm.CALL,
Depth: []int{},
From: &vm.RosettaLogAddressItem{Account: &from},
To: &vm.RosettaLogAddressItem{Account: &to},
Value: value,
}
if !reflect.DeepEqual(result[0], want) {
t.Fatalf("unexpected result: got %#v, want %#v", result[0], want)
}
}

func TestRosettaBlockTracerRevertsSyntheticLogsWithTransaction(t *testing.T) {
genericTracer, err := tracers.New("RosettaBlockTracer", new(tracers.Context), nil)
if err != nil {
t.Fatalf("RosettaBlockTracer lookup failed: %v", err)
}
tracer := genericTracer.(*native.RosettaBlockTracer)
tracer.CaptureEnter(vm.CALL, common.Address{}, common.Address{}, nil, 0, new(big.Int))
tracer.CaptureEnter(vm.CALL, common.Address{}, common.Address{}, nil, 0, new(big.Int))
tracer.AddRosettaLog(vm.CALL, nil, nil, big.NewInt(1))
tracer.CaptureExit(nil, 0, nil)
tracer.CaptureExit(nil, 0, errors.New("execution reverted"))

result, err := tracer.GetRosettaResult()
if err != nil {
t.Fatalf("RosettaBlockTracer result failed: %v", err)
}
if len(result) != 1 {
t.Fatalf("unexpected result length: got %d, want 1", len(result))
}
if result[0].IsSuccess || !result[0].Reverted {
t.Fatalf("synthetic log did not inherit transaction revert: %#v", result[0])
}
}

func TestRosettaBlockTracerLimitsRevertToCurrentFrame(t *testing.T) {
genericTracer, err := tracers.New("RosettaBlockTracer", new(tracers.Context), nil)
if err != nil {
t.Fatalf("RosettaBlockTracer lookup failed: %v", err)
}
tracer := genericTracer.(*native.RosettaBlockTracer)

tracer.CaptureEnter(vm.CALL, common.Address{}, common.Address{}, nil, 0, new(big.Int))
tracer.AddRosettaLog(vm.CALL, nil, nil, big.NewInt(1))
tracer.CaptureExit(nil, 0, errors.New("execution reverted"))
tracer.CaptureEnter(vm.CALL, common.Address{}, common.Address{}, nil, 0, new(big.Int))
tracer.AddRosettaLog(vm.CALL, nil, nil, big.NewInt(2))
tracer.CaptureExit(nil, 0, nil)

result, err := tracer.GetRosettaResult()
if err != nil {
t.Fatalf("RosettaBlockTracer result failed: %v", err)
}
if len(result) != 2 {
t.Fatalf("unexpected result length: got %d, want 2", len(result))
}
if result[0].IsSuccess || !result[0].Reverted {
t.Fatalf("failed frame log was not reverted: %#v", result[0])
}
if !result[1].IsSuccess || result[1].Reverted {
t.Fatalf("successful sibling log inherited revert: %#v", result[1])
}
}
2 changes: 1 addition & 1 deletion test/all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
bash "$DIR/kill_node.sh"

bash "$DIR/go.sh"
bash "$DIR/localnet.sh" rpc rosetta pyhmy
bash "$DIR/localnet.sh" rpc mesh pyhmy
Loading
Loading