diff --git a/.github/matic-cli-config.yml b/.github/matic-cli-config.yml index 7233eb0497..8c31e17dc6 100644 --- a/.github/matic-cli-config.yml +++ b/.github/matic-cli-config.yml @@ -10,8 +10,12 @@ blockNumber: - '0' blockTime: - '2' -numOfValidators: 3 -numOfNonValidators: 0 +numOfBorValidators: 3 +numOfBorSentries: 0 +numOfBorArchiveNodes: 0 +numOfErigonValidators: 0 +numOfErigonSentries: 0 +numOfErigonArchiveNodes: 0 ethURL: http://ganache:9545 ethHostUser: ubuntu devnetType: docker @@ -19,4 +23,3 @@ borDockerBuildContext: "../../bor" heimdallDockerBuildContext: "https://github.com/maticnetwork/heimdall.git#develop" sprintSizeBlockNumber: - '0' -numOfArchiveNodes: 0 diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c467800e58..7b58af0bc0 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -23,6 +23,8 @@ In case this PR includes changes that must be applied only to a subset of nodes, - [ ] I have added at least 2 reviewer or the whole pos-v1 team - [ ] I have added sufficient documentation in code - [ ] I will be resolving comments - if any - by pushing each fix in a separate commit and linking the commit hash in the comment reply +- [ ] Created a task in Jira and informed the team for implementation in Erigon client (if applicable) +- [ ] Includes RPC methods changes, and the Notion documentation has been updated # Cross repository changes diff --git a/.github/workflows/packager.yml b/.github/workflows/packager.yml index aa14a3ce22..f2aef42485 100644 --- a/.github/workflows/packager.yml +++ b/.github/workflows/packager.yml @@ -47,7 +47,7 @@ jobs: run: cp -rp packaging/deb/bor packaging/deb/bor-${{ env.GIT_TAG }}-${{ env.ARCH }} env: ARCH: amd64 - + - name: Running package build run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.GIT_TAG }}-${{ env.ARCH }} env: @@ -61,116 +61,6 @@ jobs: - name: Copying systemd file run: cp -rp packaging/templates/systemd/bor.service packaging/deb/bor/lib/systemd/system/bor.service - - name: Prepping ${{ env.NETWORK }} ${{ env.NODE }} bootnode for ${{ env.ARCH }} - run: cp -rp packaging/deb/bor packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: amd64 - NODE: bootnode - NETWORK: any - - name: Putting toml for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/testnet-v4/sentry/sentry/bor/config.toml packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/var/lib/bor/ - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mumbai - - name: Copying the preinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/preinst packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/preinst - env: - ARCH: amd64 - NODE: bootnode - NETWORK: any - - name: Copying the postinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postinst.profile packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postinst - env: - ARCH: amd64 - NODE: bootnode - NETWORK: any - - name: Copying the prerm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/prerm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/prerm - env: - ARCH: amd64 - NODE: bootnode - NETWORK: any - - name: Copying the postrm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postrm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postrm - env: - ARCH: amd64 - NODE: bootnode - NETWORK: any - - name: Copying systemd file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/systemd/bor_bootnode.service packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/lib/systemd/system/ - env: - ARCH: amd64 - NODE: bootnode - NETWORK: any - - name: Copying profile control file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/control.profile.amd64 packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/control - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mumbai - - name: Running package build for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mumbai - - - name: Prepping ${{ env.NETWORK }} ${{ env.NODE }} bootnode for ${{ env.ARCH }} - run: cp -rp packaging/deb/bor packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Putting toml for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/mainnet-v1/sentry/sentry/bor/config.toml packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/var/lib/bor/ - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the preinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/preinst packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/preinst - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the postinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postinst.profile packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postinst - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the prerm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/prerm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/prerm - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the postrm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postrm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postrm - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Copying systemd file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/systemd/bor_bootnode.service packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/lib/systemd/system/ - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Copying profile control file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/control.profile.amd64 packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/control - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Running package build for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: amd64 - NODE: bootnode - NETWORK: mainnet - - name: Prepping ${{ env.NETWORK }} ${{ env.NODE }} node for ${{ env.ARCH }} run: cp -rp packaging/deb/bor packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} env: @@ -476,7 +366,7 @@ jobs: ARCH: amd64 NODE: archive NETWORK: mainnet - - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile + - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} env: ARCH: amd64 @@ -526,116 +416,6 @@ jobs: - name: Updating the control file to use with the arm64 profile run: cp -rp packaging/templates/package_scripts/control.profile.arm64 packaging/deb/bor/DEBIAN/control - - name: Prepping ${{ env.NETWORK }} ${{ env.NODE }} bootnode for ${{ env.ARCH }} - run: cp -rp packaging/deb/bor packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Putting toml for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/testnet-v4/sentry/sentry/bor/config.toml packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/var/lib/bor/ - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Copying the preinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/preinst packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/preinst - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Copying the postinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postinst.profile packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postinst - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Copying the prerm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/prerm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/prerm - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Copying the postrm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postrm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postrm - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Copying systemd file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/systemd/bor_bootnode.service packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/lib/systemd/system/ - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Copying profile control file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/control.profile.amd64 packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/control - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - name: Running package build for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mumbai - - - name: Prepping ${{ env.NETWORK }} ${{ env.NODE }} bootnode for ${{ env.ARCH }} - run: cp -rp packaging/deb/bor packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Putting toml for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/mainnet-v1/sentry/sentry/bor/config.toml packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/var/lib/bor/ - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the preinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/preinst packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/preinst - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the postinst for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postinst.profile packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postinst - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the prerm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/prerm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/prerm - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Copying the postrm for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/postrm packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/postrm - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Copying systemd file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/systemd/bor_bootnode.service packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/lib/systemd/system/ - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Copying profile control file for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: cp -rp packaging/templates/package_scripts/control.profile.amd64 packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }}/DEBIAN/control - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Running package build for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} - run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} - env: - ARCH: arm64 - NODE: bootnode - NETWORK: mainnet - - name: Setting up bor for ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} run: cp -rp packaging/deb/bor packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} env: @@ -739,7 +519,7 @@ jobs: ARCH: arm64 NODE: sentry NETWORK: mainnet - - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile + - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} env: ARCH: arm64 @@ -849,13 +629,13 @@ jobs: ARCH: arm64 NODE: validator NETWORK: mainnet - - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile + - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} env: ARCH: arm64 NODE: validator NETWORK: mainnet - + - name: Updating the control file to use with the arm64 profile run: cp -rp packaging/templates/package_scripts/control.profile.arm64 packaging/deb/bor/DEBIAN/control @@ -895,7 +675,7 @@ jobs: ARCH: arm64 NODE: archive NETWORK: mumbai - - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile + - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} env: ARCH: arm64 @@ -938,7 +718,7 @@ jobs: ARCH: arm64 NODE: archive NETWORK: mainnet - - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile + - name: Building bor ${{ env.NODE }} on ${{ env.NETWORK }} on ${{ env.ARCH }} profile run: dpkg-deb --build --root-owner-group packaging/deb/bor-${{ env.NETWORK }}-${{ env.NODE }}-config_${{ env.GIT_TAG }}-${{ env.ARCH }} env: ARCH: arm64 diff --git a/.github/workflows/security-ci.yml b/.github/workflows/security-ci.yml index 61f4d7ddbb..1d88a8b91a 100644 --- a/.github/workflows/security-ci.yml +++ b/.github/workflows/security-ci.yml @@ -49,7 +49,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Running govulncheck - uses: Templum/govulncheck-action@v0.0.8 + uses: Templum/govulncheck-action@v0.10.1 continue-on-error: true env: DEBUG: "true" diff --git a/builder/files/config.toml b/builder/files/config.toml index f2ed492279..2e758bc8ee 100644 --- a/builder/files/config.toml +++ b/builder/files/config.toml @@ -71,6 +71,7 @@ syncmode = "full" # etherbase = "VALIDATOR ADDRESS" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true # [jsonrpc] @@ -128,7 +129,7 @@ syncmode = "full" metrics = true # expensive = false # prometheus-addr = "127.0.0.1:7071" - # opencollector-endpoint = "127.0.0.1:4317" + # opencollector-endpoint = "" # [telemetry.influx] # influxdb = false # endpoint = "" @@ -171,6 +172,10 @@ syncmode = "full" # period = 0 # gaslimit = 11500000 +# [parallelevm] + # enable = true + # procs = 8 + # [pprof] # pprof = false # port = 6060 diff --git a/builder/files/genesis-testnet-v4.json b/builder/files/genesis-testnet-v4.json index fe066411a3..3fc61e2f64 100644 --- a/builder/files/genesis-testnet-v4.json +++ b/builder/files/genesis-testnet-v4.json @@ -16,6 +16,11 @@ "bor": { "jaipurBlock": 22770000, "delhiBlock": 29638656, + "parallelUniverseBlock": 0, + "indoreBlock": 36877056, + "stateSyncConfirmationDelay": { + "36877056": 128 + }, "period": { "0": 2, "25275000": 5, diff --git a/consensus/bor/api/caller.go b/consensus/bor/api/caller.go index d5fe259c97..49c06b3f73 100644 --- a/consensus/bor/api/caller.go +++ b/consensus/bor/api/caller.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/rpc" ) @@ -11,4 +12,5 @@ import ( //go:generate mockgen -destination=./caller_mock.go -package=api . Caller type Caller interface { Call(ctx context.Context, args ethapi.TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *ethapi.StateOverride) (hexutil.Bytes, error) + CallWithState(ctx context.Context, args ethapi.TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, state *state.StateDB, overrides *ethapi.StateOverride) (hexutil.Bytes, error) } diff --git a/consensus/bor/api/caller_mock.go b/consensus/bor/api/caller_mock.go index 940c99d178..e734d6c899 100644 --- a/consensus/bor/api/caller_mock.go +++ b/consensus/bor/api/caller_mock.go @@ -9,6 +9,7 @@ import ( reflect "reflect" hexutil "github.com/ethereum/go-ethereum/common/hexutil" + state "github.com/ethereum/go-ethereum/core/state" ethapi "github.com/ethereum/go-ethereum/internal/ethapi" rpc "github.com/ethereum/go-ethereum/rpc" gomock "github.com/golang/mock/gomock" @@ -51,3 +52,18 @@ func (mr *MockCallerMockRecorder) Call(arg0, arg1, arg2, arg3 interface{}) *gomo mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Call", reflect.TypeOf((*MockCaller)(nil).Call), arg0, arg1, arg2, arg3) } + +// CallWithState mocks base method. +func (m *MockCaller) CallWithState(arg0 context.Context, arg1 ethapi.TransactionArgs, arg2 rpc.BlockNumberOrHash, arg3 *state.StateDB, arg4 *ethapi.StateOverride) (hexutil.Bytes, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CallWithState", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(hexutil.Bytes) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CallWithState indicates an expected call of CallWithState. +func (mr *MockCallerMockRecorder) CallWithState(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CallWithState", reflect.TypeOf((*MockCaller)(nil).CallWithState), arg0, arg1, arg2, arg3, arg4) +} diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index d63b60fa58..aa35dc2fbc 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1179,22 +1179,42 @@ func (c *Bor) CommitStates( fetchStart := time.Now() number := header.Number.Uint64() - _lastStateID, err := c.GenesisContractsClient.LastStateId(number - 1) - if err != nil { - return nil, err + var ( + lastStateIDBig *big.Int + from uint64 + to time.Time + err error + ) + + if c.config.IsIndore(header.Number) { + // Fetch the LastStateId from contract via current state instance + lastStateIDBig, err = c.GenesisContractsClient.LastStateId(state.Copy(), number-1, header.ParentHash) + if err != nil { + return nil, err + } + + stateSyncDelay := c.config.CalculateStateSyncDelay(number) + to = time.Unix(int64(header.Time-stateSyncDelay), 0) + } else { + lastStateIDBig, err = c.GenesisContractsClient.LastStateId(nil, number-1, header.ParentHash) + if err != nil { + return nil, err + } + + to = time.Unix(int64(chain.Chain.GetHeaderByNumber(number-c.config.CalculateSprint(number)).Time), 0) } - to := time.Unix(int64(chain.Chain.GetHeaderByNumber(number-c.config.CalculateSprint(number)).Time), 0) - lastStateID := _lastStateID.Uint64() + lastStateID := lastStateIDBig.Uint64() + from = lastStateID + 1 log.Info( "Fetching state updates from Heimdall", - "fromID", lastStateID+1, + "fromID", from, "to", to.Format(time.RFC3339)) - eventRecords, err := c.HeimdallClient.StateSyncEvents(ctx, lastStateID+1, to.Unix()) + eventRecords, err := c.HeimdallClient.StateSyncEvents(ctx, from, to.Unix()) if err != nil { - log.Error("Error occurred when fetching state sync events", "stateID", lastStateID+1, "error", err) + log.Error("Error occurred when fetching state sync events", "fromID", from, "to", to.Unix(), "err", err) } if c.config.OverrideStateSyncRecords != nil { @@ -1217,7 +1237,7 @@ func (c *Bor) CommitStates( } if err = validateEventRecord(eventRecord, number, to, lastStateID, chainID); err != nil { - log.Error("while validating event record", "block", number, "to", to, "stateID", lastStateID, "error", err.Error()) + log.Error("while validating event record", "block", number, "to", to, "stateID", lastStateID+1, "error", err.Error()) break } diff --git a/consensus/bor/contract/client.go b/consensus/bor/contract/client.go index 9e9e1392dd..fa8ad215f2 100644 --- a/consensus/bor/contract/client.go +++ b/consensus/bor/contract/client.go @@ -100,8 +100,8 @@ func (gc *GenesisContractsClient) CommitState( return gasUsed, nil } -func (gc *GenesisContractsClient) LastStateId(snapshotNumber uint64) (*big.Int, error) { - blockNr := rpc.BlockNumber(snapshotNumber) +func (gc *GenesisContractsClient) LastStateId(state *state.StateDB, number uint64, hash common.Hash) (*big.Int, error) { + blockNr := rpc.BlockNumber(number) const method = "lastStateId" @@ -116,11 +116,13 @@ func (gc *GenesisContractsClient) LastStateId(snapshotNumber uint64) (*big.Int, toAddress := common.HexToAddress(gc.StateReceiverContract) gas := (hexutil.Uint64)(uint64(math.MaxUint64 / 2)) - result, err := gc.ethAPI.Call(context.Background(), ethapi.TransactionArgs{ + // Do a call with state so that we can fetch the last state ID from a given (incoming) + // state instead of local(canonical) chain. + result, err := gc.ethAPI.CallWithState(context.Background(), ethapi.TransactionArgs{ Gas: &gas, To: &toAddress, Data: &msgData, - }, rpc.BlockNumberOrHash{BlockNumber: &blockNr}, nil) + }, rpc.BlockNumberOrHash{BlockNumber: &blockNr, BlockHash: &hash}, state, nil) if err != nil { return nil, err } diff --git a/consensus/bor/genesis.go b/consensus/bor/genesis.go index 33de53f9ba..9519b18847 100644 --- a/consensus/bor/genesis.go +++ b/consensus/bor/genesis.go @@ -3,6 +3,7 @@ package bor import ( "math/big" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/bor/clerk" "github.com/ethereum/go-ethereum/consensus/bor/statefull" "github.com/ethereum/go-ethereum/core/state" @@ -12,5 +13,5 @@ import ( //go:generate mockgen -destination=./genesis_contract_mock.go -package=bor . GenesisContract type GenesisContract interface { CommitState(event *clerk.EventRecordWithTime, state *state.StateDB, header *types.Header, chCtx statefull.ChainContext) (uint64, error) - LastStateId(snapshotNumber uint64) (*big.Int, error) + LastStateId(state *state.StateDB, number uint64, hash common.Hash) (*big.Int, error) } diff --git a/consensus/bor/genesis_contract_mock.go b/consensus/bor/genesis_contract_mock.go index dfe9390509..0296cbe905 100644 --- a/consensus/bor/genesis_contract_mock.go +++ b/consensus/bor/genesis_contract_mock.go @@ -8,6 +8,7 @@ import ( big "math/big" reflect "reflect" + common "github.com/ethereum/go-ethereum/common" clerk "github.com/ethereum/go-ethereum/consensus/bor/clerk" statefull "github.com/ethereum/go-ethereum/consensus/bor/statefull" state "github.com/ethereum/go-ethereum/core/state" @@ -54,16 +55,16 @@ func (mr *MockGenesisContractMockRecorder) CommitState(arg0, arg1, arg2, arg3 in } // LastStateId mocks base method. -func (m *MockGenesisContract) LastStateId(arg0 uint64) (*big.Int, error) { +func (m *MockGenesisContract) LastStateId(arg0 *state.StateDB, arg1 uint64, arg2 common.Hash) (*big.Int, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "LastStateId", arg0) + ret := m.ctrl.Call(m, "LastStateId", arg0, arg1, arg2) ret0, _ := ret[0].(*big.Int) ret1, _ := ret[1].(error) return ret0, ret1 } // LastStateId indicates an expected call of LastStateId. -func (mr *MockGenesisContractMockRecorder) LastStateId(arg0 interface{}) *gomock.Call { +func (mr *MockGenesisContractMockRecorder) LastStateId(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastStateId", reflect.TypeOf((*MockGenesisContract)(nil).LastStateId), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastStateId", reflect.TypeOf((*MockGenesisContract)(nil).LastStateId), arg0, arg1, arg2) } diff --git a/consensus/bor/span_mock.go b/consensus/bor/span_mock.go index 910e81716c..099807161c 100644 --- a/consensus/bor/span_mock.go +++ b/consensus/bor/span_mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: consensus/bor/span.go +// Source: github.com/ethereum/go-ethereum/consensus/bor (interfaces: Spanner) // Package bor is a generated GoMock package. package bor @@ -42,60 +42,60 @@ func (m *MockSpanner) EXPECT() *MockSpannerMockRecorder { } // CommitSpan mocks base method. -func (m *MockSpanner) CommitSpan(ctx context.Context, heimdallSpan span.HeimdallSpan, state *state.StateDB, header *types.Header, chainContext core.ChainContext) error { +func (m *MockSpanner) CommitSpan(arg0 context.Context, arg1 span.HeimdallSpan, arg2 *state.StateDB, arg3 *types.Header, arg4 core.ChainContext) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CommitSpan", ctx, heimdallSpan, state, header, chainContext) + ret := m.ctrl.Call(m, "CommitSpan", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(error) return ret0 } // CommitSpan indicates an expected call of CommitSpan. -func (mr *MockSpannerMockRecorder) CommitSpan(ctx, heimdallSpan, state, header, chainContext interface{}) *gomock.Call { +func (mr *MockSpannerMockRecorder) CommitSpan(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitSpan", reflect.TypeOf((*MockSpanner)(nil).CommitSpan), ctx, heimdallSpan, state, header, chainContext) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CommitSpan", reflect.TypeOf((*MockSpanner)(nil).CommitSpan), arg0, arg1, arg2, arg3, arg4) } // GetCurrentSpan mocks base method. -func (m *MockSpanner) GetCurrentSpan(ctx context.Context, headerHash common.Hash) (*span.Span, error) { +func (m *MockSpanner) GetCurrentSpan(arg0 context.Context, arg1 common.Hash) (*span.Span, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCurrentSpan", ctx, headerHash) + ret := m.ctrl.Call(m, "GetCurrentSpan", arg0, arg1) ret0, _ := ret[0].(*span.Span) ret1, _ := ret[1].(error) return ret0, ret1 } // GetCurrentSpan indicates an expected call of GetCurrentSpan. -func (mr *MockSpannerMockRecorder) GetCurrentSpan(ctx, headerHash interface{}) *gomock.Call { +func (mr *MockSpannerMockRecorder) GetCurrentSpan(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentSpan", reflect.TypeOf((*MockSpanner)(nil).GetCurrentSpan), ctx, headerHash) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentSpan", reflect.TypeOf((*MockSpanner)(nil).GetCurrentSpan), arg0, arg1) } // GetCurrentValidatorsByBlockNrOrHash mocks base method. -func (m *MockSpanner) GetCurrentValidatorsByBlockNrOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash, blockNumber uint64) ([]*valset.Validator, error) { +func (m *MockSpanner) GetCurrentValidatorsByBlockNrOrHash(arg0 context.Context, arg1 rpc.BlockNumberOrHash, arg2 uint64) ([]*valset.Validator, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCurrentValidatorsByBlockNrOrHash", ctx, blockNrOrHash, blockNumber) + ret := m.ctrl.Call(m, "GetCurrentValidatorsByBlockNrOrHash", arg0, arg1, arg2) ret0, _ := ret[0].([]*valset.Validator) ret1, _ := ret[1].(error) return ret0, ret1 } // GetCurrentValidatorsByBlockNrOrHash indicates an expected call of GetCurrentValidatorsByBlockNrOrHash. -func (mr *MockSpannerMockRecorder) GetCurrentValidatorsByBlockNrOrHash(ctx, blockNrOrHash, blockNumber interface{}) *gomock.Call { +func (mr *MockSpannerMockRecorder) GetCurrentValidatorsByBlockNrOrHash(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentValidatorsByBlockNrOrHash", reflect.TypeOf((*MockSpanner)(nil).GetCurrentValidatorsByBlockNrOrHash), ctx, blockNrOrHash, blockNumber) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentValidatorsByBlockNrOrHash", reflect.TypeOf((*MockSpanner)(nil).GetCurrentValidatorsByBlockNrOrHash), arg0, arg1, arg2) } // GetCurrentValidatorsByHash mocks base method. -func (m *MockSpanner) GetCurrentValidatorsByHash(ctx context.Context, headerHash common.Hash, blockNumber uint64) ([]*valset.Validator, error) { +func (m *MockSpanner) GetCurrentValidatorsByHash(arg0 context.Context, arg1 common.Hash, arg2 uint64) ([]*valset.Validator, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetCurrentValidatorsByHash", ctx, headerHash, blockNumber) + ret := m.ctrl.Call(m, "GetCurrentValidatorsByHash", arg0, arg1, arg2) ret0, _ := ret[0].([]*valset.Validator) ret1, _ := ret[1].(error) return ret0, ret1 } // GetCurrentValidatorsByHash indicates an expected call of GetCurrentValidatorsByHash. -func (mr *MockSpannerMockRecorder) GetCurrentValidatorsByHash(ctx, headerHash, blockNumber interface{}) *gomock.Call { +func (mr *MockSpannerMockRecorder) GetCurrentValidatorsByHash(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentValidatorsByHash", reflect.TypeOf((*MockSpanner)(nil).GetCurrentValidatorsByHash), ctx, headerHash, blockNumber) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetCurrentValidatorsByHash", reflect.TypeOf((*MockSpanner)(nil).GetCurrentValidatorsByHash), arg0, arg1, arg2) } diff --git a/core/blockchain.go b/core/blockchain.go index dd71db0b23..44b0fdee00 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -44,6 +44,7 @@ import ( "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/common/tracing" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" @@ -84,10 +85,12 @@ var ( blockImportTimer = metrics.NewRegisteredMeter("chain/imports", nil) triedbCommitTimer = metrics.NewRegisteredTimer("chain/triedb/commits", nil) - blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil) - blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil) - blockExecutionTimer = metrics.NewRegisteredTimer("chain/execution", nil) - blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil) + blockInsertTimer = metrics.NewRegisteredTimer("chain/inserts", nil) + blockValidationTimer = metrics.NewRegisteredTimer("chain/validation", nil) + blockExecutionTimer = metrics.NewRegisteredTimer("chain/execution", nil) + blockWriteTimer = metrics.NewRegisteredTimer("chain/write", nil) + blockExecutionParallelCounter = metrics.NewRegisteredCounter("chain/execution/parallel", nil) + blockExecutionSerialCounter = metrics.NewRegisteredCounter("chain/execution/serial", nil) blockReorgMeter = metrics.NewRegisteredMeter("chain/reorg/executes", nil) blockReorgAddMeter = metrics.NewRegisteredMeter("chain/reorg/add", nil) @@ -230,12 +233,13 @@ type BlockChain struct { stopping atomic.Bool // false if chain is running, true when stopped procInterrupt atomic.Bool // interrupt signaler for block processing - engine consensus.Engine - validator Validator // Block and state validator interface - prefetcher Prefetcher - processor Processor // Block transaction processor interface - forker *ForkChoice - vmConfig vm.Config + engine consensus.Engine + validator Validator // Block and state validator interface + prefetcher Prefetcher + processor Processor // Block transaction processor interface + parallelProcessor Processor // Parallel block transaction processor interface + forker *ForkChoice + vmConfig vm.Config // Bor related changes borReceiptsCache *lru.Cache[common.Hash, *types.Receipt] // Cache for the most recent bor receipt receipts per block @@ -467,6 +471,94 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, genesis *Genesis return bc, nil } +// NewParallelBlockChain , similar to NewBlockChain, creates a new blockchain object, but with a parallel state processor +// TODO marcello check this func +func NewParallelBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *params.ChainConfig, engine consensus.Engine, vmConfig vm.Config, shouldPreserve func(header *types.Header) bool, txLookupLimit *uint64, checker ethereum.ChainValidator) (*BlockChain, error) { + bc, err := NewBlockChain(db, cacheConfig, chainConfig, engine, vmConfig, shouldPreserve, txLookupLimit, checker) + + if err != nil { + return nil, err + } + + bc.parallelProcessor = NewParallelStateProcessor(chainConfig, bc, engine) + + return bc, nil +} + +func (bc *BlockChain) ProcessBlock(block *types.Block, parent *types.Header) (types.Receipts, []*types.Log, uint64, *state.StateDB, error) { + // Process the block using processor and parallelProcessor at the same time, take the one which finishes first, cancel the other, and return the result + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + type Result struct { + receipts types.Receipts + logs []*types.Log + usedGas uint64 + err error + statedb *state.StateDB + counter metrics.Counter + } + + resultChan := make(chan Result, 2) + + processorCount := 0 + + if bc.parallelProcessor != nil { + parallelStatedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) + if err != nil { + return nil, nil, 0, nil, err + } + + processorCount++ + + go func() { + parallelStatedb.StartPrefetcher("chain") + receipts, logs, usedGas, err := bc.parallelProcessor.Process(block, parallelStatedb, bc.vmConfig, ctx) + resultChan <- Result{receipts, logs, usedGas, err, parallelStatedb, blockExecutionParallelCounter} + }() + } + + if bc.processor != nil { + statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) + if err != nil { + return nil, nil, 0, nil, err + } + + processorCount++ + + go func() { + statedb.StartPrefetcher("chain") + receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig, ctx) + resultChan <- Result{receipts, logs, usedGas, err, statedb, blockExecutionSerialCounter} + }() + } + + result := <-resultChan + + if _, ok := result.err.(blockstm.ParallelExecFailedError); ok { + log.Warn("Parallel state processor failed", "err", result.err) + + // If the parallel processor failed, we will fallback to the serial processor if enabled + if processorCount == 2 { + result.statedb.StopPrefetcher() + result = <-resultChan + processorCount-- + } + } + + result.counter.Inc(1) + + // Make sure we are not leaking any prefetchers + if processorCount == 2 { + go func() { + second_result := <-resultChan + second_result.statedb.StopPrefetcher() + }() + } + + return result.receipts, result.logs, result.usedGas, result.statedb, result.err +} + // empty returns an indicator whether the blockchain is empty. // Note, it's a special case that we connect a non-empty ancient // database with an empty node, so that we can plugin the ancient @@ -1933,7 +2025,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool) // Process block using the parent state as reference point pstart := time.Now() - receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) + receipts, logs, usedGas, statedb, err := bc.ProcessBlock(block, parent) + activeState = statedb if err != nil { bc.reportBlock(block, receipts, err) followupInterrupt.Store(true) @@ -2769,7 +2862,6 @@ func (bc *BlockChain) InsertHeaderChain(chain []*types.Header, checkFreq int) (i return 0, errChainStopped } defer bc.chainmu.Unlock() - // TODO marcello check headerchain method _, err := bc.hc.InsertHeaderChain(chain, start, bc.forker) return 0, err } diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 246ff951e8..af2c445adb 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -32,7 +32,6 @@ import ( "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core/rawdb" - "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/crypto" @@ -158,11 +157,9 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { } return err } - statedb, err := state.New(blockchain.GetBlockByHash(block.ParentHash()).Root(), blockchain.stateCache, nil) - if err != nil { - return err - } - receipts, _, usedGas, err := blockchain.processor.Process(block, statedb, vm.Config{}) + + receipts, _, usedGas, statedb, err := blockchain.ProcessBlock(block, blockchain.GetBlockByHash(block.ParentHash()).Header()) + if err != nil { blockchain.reportBlock(block, receipts, err) return err @@ -182,6 +179,26 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { return nil } +func TestParallelBlockChainImport(t *testing.T) { + t.Parallel() + + db, _, blockchain, err := newCanonical(ethash.NewFaker(), 10, true) + blockchain.parallelProcessor = NewParallelStateProcessor(blockchain.chainConfig, blockchain, blockchain.engine) + + if err != nil { + t.Fatalf("failed to make new canonical chain: %v", err) + } + + defer blockchain.Stop() + + block := blockchain.GetBlockByHash(blockchain.CurrentBlock().Hash()) + blockChainB := makeFakeNonEmptyBlockChain(block, 5, ethash.NewFaker(), db, forkSeed, 5) + + if err := testBlockChainImport(blockChainB, blockchain); err == nil { + t.Fatalf("expected error for bad tx") + } +} + // testHeaderChainImport tries to process a chain of header, writing them into // the database if successful. func testHeaderChainImport(chain []*types.Header, blockchain *BlockChain) error { @@ -2027,7 +2044,6 @@ func testSideImport(t *testing.T, numCanonBlocksInSidechain, blocksBetweenCommon fork, _ := GenerateChain(gspec.Config, parent, engine, genDb, 2*int(DefaultCacheConfig.TriesInMemory), func(i int, b *BlockGen) { b.SetCoinbase(common.Address{2}) if int(b.header.Number.Uint64()) >= mergeBlock { - // TODO marcello SetPoS everywhere b.SetPoS() } }) diff --git a/core/blockstm/dag.go b/core/blockstm/dag.go new file mode 100644 index 0000000000..47bd0685a3 --- /dev/null +++ b/core/blockstm/dag.go @@ -0,0 +1,201 @@ +package blockstm + +import ( + "fmt" + "strings" + "time" + + "github.com/heimdalr/dag" + + "github.com/ethereum/go-ethereum/log" +) + +type DAG struct { + *dag.DAG +} + +type TxDep struct { + Index int + ReadList []ReadDescriptor + FullWriteList [][]WriteDescriptor +} + +func HasReadDep(txFrom TxnOutput, txTo TxnInput) bool { + reads := make(map[Key]bool) + + for _, v := range txTo { + reads[v.Path] = true + } + + for _, rd := range txFrom { + if _, ok := reads[rd.Path]; ok { + return true + } + } + + return false +} + +func BuildDAG(deps TxnInputOutput) (d DAG) { + d = DAG{dag.NewDAG()} + ids := make(map[int]string) + + for i := len(deps.inputs) - 1; i > 0; i-- { + txTo := deps.inputs[i] + + var txToId string + + if _, ok := ids[i]; ok { + txToId = ids[i] + } else { + txToId, _ = d.AddVertex(i) + ids[i] = txToId + } + + for j := i - 1; j >= 0; j-- { + txFrom := deps.allOutputs[j] + + if HasReadDep(txFrom, txTo) { + var txFromId string + if _, ok := ids[j]; ok { + txFromId = ids[j] + } else { + txFromId, _ = d.AddVertex(j) + ids[j] = txFromId + } + + err := d.AddEdge(txFromId, txToId) + if err != nil { + log.Warn("Failed to add edge", "from", txFromId, "to", txToId, "err", err) + } + } + } + } + + return +} + +func depsHelper(dependencies map[int]map[int]bool, txFrom TxnOutput, txTo TxnInput, i int, j int) map[int]map[int]bool { + if HasReadDep(txFrom, txTo) { + dependencies[i][j] = true + + for k := range dependencies[i] { + _, foundDep := dependencies[j][k] + + if foundDep { + delete(dependencies[i], k) + } + } + } + + return dependencies +} + +func UpdateDeps(deps map[int]map[int]bool, t TxDep) map[int]map[int]bool { + txTo := t.ReadList + + deps[t.Index] = map[int]bool{} + + for j := 0; j <= t.Index-1; j++ { + txFrom := t.FullWriteList[j] + + deps = depsHelper(deps, txFrom, txTo, t.Index, j) + } + + return deps +} + +func GetDep(deps TxnInputOutput) map[int]map[int]bool { + newDependencies := map[int]map[int]bool{} + + for i := 1; i < len(deps.inputs); i++ { + txTo := deps.inputs[i] + + newDependencies[i] = map[int]bool{} + + for j := 0; j <= i-1; j++ { + txFrom := deps.allOutputs[j] + + newDependencies = depsHelper(newDependencies, txFrom, txTo, i, j) + } + } + + return newDependencies +} + +// Find the longest execution path in the DAG +func (d DAG) LongestPath(stats map[int]ExecutionStat) ([]int, uint64) { + prev := make(map[int]int, len(d.GetVertices())) + + for i := 0; i < len(d.GetVertices()); i++ { + prev[i] = -1 + } + + pathWeights := make(map[int]uint64, len(d.GetVertices())) + + maxPath := 0 + maxPathWeight := uint64(0) + + idxToId := make(map[int]string, len(d.GetVertices())) + + for k, i := range d.GetVertices() { + idxToId[i.(int)] = k + } + + for i := 0; i < len(idxToId); i++ { + parents, _ := d.GetParents(idxToId[i]) + + if len(parents) > 0 { + for _, p := range parents { + weight := pathWeights[p.(int)] + stats[i].End - stats[i].Start + if weight > pathWeights[i] { + pathWeights[i] = weight + prev[i] = p.(int) + } + } + } else { + pathWeights[i] = stats[i].End - stats[i].Start + } + + if pathWeights[i] > maxPathWeight { + maxPath = i + maxPathWeight = pathWeights[i] + } + } + + path := make([]int, 0) + for i := maxPath; i != -1; i = prev[i] { + path = append(path, i) + } + + // Reverse the path so the transactions are in the ascending order + for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 { + path[i], path[j] = path[j], path[i] + } + + return path, maxPathWeight +} + +func (d DAG) Report(stats map[int]ExecutionStat, out func(string)) { + longestPath, weight := d.LongestPath(stats) + + serialWeight := uint64(0) + + for i := 0; i < len(d.GetVertices()); i++ { + serialWeight += stats[i].End - stats[i].Start + } + + makeStrs := func(ints []int) (ret []string) { + for _, v := range ints { + ret = append(ret, fmt.Sprint(v)) + } + + return + } + + out("Longest execution path:") + out(fmt.Sprintf("(%v) %v", len(longestPath), strings.Join(makeStrs(longestPath), "->"))) + + out(fmt.Sprintf("Longest path ideal execution time: %v of %v (serial total), %v%%", time.Duration(weight), + time.Duration(serialWeight), fmt.Sprintf("%.1f", float64(weight)*100.0/float64(serialWeight)))) +} diff --git a/core/blockstm/executor.go b/core/blockstm/executor.go new file mode 100644 index 0000000000..7ce99f9492 --- /dev/null +++ b/core/blockstm/executor.go @@ -0,0 +1,641 @@ +package blockstm + +import ( + "container/heap" + "context" + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +type ExecResult struct { + err error + ver Version + txIn TxnInput + txOut TxnOutput + txAllOut TxnOutput +} + +type ExecTask interface { + Execute(mvh *MVHashMap, incarnation int) error + MVReadList() []ReadDescriptor + MVWriteList() []WriteDescriptor + MVFullWriteList() []WriteDescriptor + Hash() common.Hash + Sender() common.Address + Settle() + Dependencies() []int +} + +type ExecVersionView struct { + ver Version + et ExecTask + mvh *MVHashMap + sender common.Address +} + +var NumSpeculativeProcs int = 8 + +func SetProcs(specProcs int) { + NumSpeculativeProcs = specProcs +} + +func (ev *ExecVersionView) Execute() (er ExecResult) { + er.ver = ev.ver + if er.err = ev.et.Execute(ev.mvh, ev.ver.Incarnation); er.err != nil { + return + } + + er.txIn = ev.et.MVReadList() + er.txOut = ev.et.MVWriteList() + er.txAllOut = ev.et.MVFullWriteList() + + return +} + +type ErrExecAbortError struct { + Dependency int + OriginError error +} + +func (e ErrExecAbortError) Error() string { + if e.Dependency >= 0 { + return fmt.Sprintf("Execution aborted due to dependency %d", e.Dependency) + } else { + return "Execution aborted" + } +} + +type ParallelExecFailedError struct { + Msg string +} + +func (e ParallelExecFailedError) Error() string { + return e.Msg +} + +type IntHeap []int + +func (h IntHeap) Len() int { return len(h) } +func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } +func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] } + +func (h *IntHeap) Push(x any) { + // Push and Pop use pointer receivers because they modify the slice's length, + // not just its contents. + *h = append(*h, x.(int)) +} + +func (h *IntHeap) Pop() any { + old := *h + n := len(old) + x := old[n-1] + *h = old[0 : n-1] + + return x +} + +type SafeQueue interface { + Push(v int, d interface{}) + Pop() interface{} + Len() int +} + +type SafeFIFOQueue struct { + c chan interface{} +} + +func NewSafeFIFOQueue(capacity int) *SafeFIFOQueue { + return &SafeFIFOQueue{ + c: make(chan interface{}, capacity), + } +} + +func (q *SafeFIFOQueue) Push(v int, d interface{}) { + q.c <- d +} + +func (q *SafeFIFOQueue) Pop() interface{} { + return <-q.c +} + +func (q *SafeFIFOQueue) Len() int { + return len(q.c) +} + +// A thread safe priority queue +type SafePriorityQueue struct { + m sync.Mutex + queue *IntHeap + data map[int]interface{} +} + +func NewSafePriorityQueue(capacity int) *SafePriorityQueue { + q := make(IntHeap, 0, capacity) + + return &SafePriorityQueue{ + m: sync.Mutex{}, + queue: &q, + data: make(map[int]interface{}, capacity), + } +} + +func (pq *SafePriorityQueue) Push(v int, d interface{}) { + pq.m.Lock() + + heap.Push(pq.queue, v) + pq.data[v] = d + + pq.m.Unlock() +} + +func (pq *SafePriorityQueue) Pop() interface{} { + pq.m.Lock() + defer pq.m.Unlock() + + v := heap.Pop(pq.queue).(int) + + return pq.data[v] +} + +func (pq *SafePriorityQueue) Len() int { + return pq.queue.Len() +} + +type ParallelExecutionResult struct { + TxIO *TxnInputOutput + Stats *map[int]ExecutionStat + Deps *DAG + AllDeps map[int]map[int]bool +} + +const numGoProcs = 1 + +type ParallelExecutor struct { + tasks []ExecTask + + // Stores the execution statistics for the last incarnation of each task + stats map[int]ExecutionStat + + statsMutex sync.Mutex + + // Channel for tasks that should be prioritized + chTasks chan ExecVersionView + + // Channel for speculative tasks + chSpeculativeTasks chan struct{} + + // Channel to signal that the result of a transaction could be written to storage + specTaskQueue SafeQueue + + // A priority queue that stores speculative tasks + chSettle chan int + + // Channel to signal that a transaction has finished executing + chResults chan struct{} + + // A priority queue that stores the transaction index of results, so we can validate the results in order + resultQueue SafeQueue + + // A wait group to wait for all settling tasks to finish + settleWg sync.WaitGroup + + // An integer that tracks the index of last settled transaction + lastSettled int + + // For a task that runs only after all of its preceding tasks have finished and passed validation, + // its result will be absolutely valid and therefore its validation could be skipped. + // This map stores the boolean value indicating whether a task satisfy this condition ( absolutely valid). + skipCheck map[int]bool + + // Execution tasks stores the state of each execution task + execTasks taskStatusManager + + // Validate tasks stores the state of each validation task + validateTasks taskStatusManager + + // Stats for debugging purposes + cntExec, cntSuccess, cntAbort, cntTotalValidations, cntValidationFail int + + diagExecSuccess, diagExecAbort []int + + // Multi-version hash map + mvh *MVHashMap + + // Stores the inputs and outputs of the last incardanotion of all transactions + lastTxIO *TxnInputOutput + + // Tracks the incarnation number of each transaction + txIncarnations []int + + // A map that stores the estimated dependency of a transaction if it is aborted without any known dependency + estimateDeps map[int][]int + + // A map that records whether a transaction result has been speculatively validated + preValidated map[int]bool + + // Time records when the parallel execution starts + begin time.Time + + // Enable profiling + profile bool + + // Worker wait group + workerWg sync.WaitGroup +} + +type ExecutionStat struct { + TxIdx int + Incarnation int + Start uint64 + End uint64 + Worker int +} + +func NewParallelExecutor(tasks []ExecTask, profile bool, metadata bool) *ParallelExecutor { + numTasks := len(tasks) + + var resultQueue SafeQueue + + var specTaskQueue SafeQueue + + if metadata { + resultQueue = NewSafeFIFOQueue(numTasks) + specTaskQueue = NewSafeFIFOQueue(numTasks) + } else { + resultQueue = NewSafePriorityQueue(numTasks) + specTaskQueue = NewSafePriorityQueue(numTasks) + } + + pe := &ParallelExecutor{ + tasks: tasks, + stats: make(map[int]ExecutionStat, numTasks), + chTasks: make(chan ExecVersionView, numTasks), + chSpeculativeTasks: make(chan struct{}, numTasks), + chSettle: make(chan int, numTasks), + chResults: make(chan struct{}, numTasks), + specTaskQueue: specTaskQueue, + resultQueue: resultQueue, + lastSettled: -1, + skipCheck: make(map[int]bool), + execTasks: makeStatusManager(numTasks), + validateTasks: makeStatusManager(0), + diagExecSuccess: make([]int, numTasks), + diagExecAbort: make([]int, numTasks), + mvh: MakeMVHashMap(), + lastTxIO: MakeTxnInputOutput(numTasks), + txIncarnations: make([]int, numTasks), + estimateDeps: make(map[int][]int), + preValidated: make(map[int]bool), + begin: time.Now(), + profile: profile, + } + + return pe +} + +// nolint: gocognit +func (pe *ParallelExecutor) Prepare() error { + prevSenderTx := make(map[common.Address]int) + + for i, t := range pe.tasks { + clearPendingFlag := false + + pe.skipCheck[i] = false + pe.estimateDeps[i] = make([]int, 0) + + if len(t.Dependencies()) > 0 { + for _, val := range t.Dependencies() { + clearPendingFlag = true + + pe.execTasks.addDependencies(val, i) + } + + if clearPendingFlag { + pe.execTasks.clearPending(i) + + clearPendingFlag = false + } + } else { + if tx, ok := prevSenderTx[t.Sender()]; ok { + pe.execTasks.addDependencies(tx, i) + pe.execTasks.clearPending(i) + } + + prevSenderTx[t.Sender()] = i + } + } + + pe.workerWg.Add(NumSpeculativeProcs + numGoProcs) + + // Launch workers that execute transactions + for i := 0; i < NumSpeculativeProcs+numGoProcs; i++ { + go func(procNum int) { + defer pe.workerWg.Done() + + doWork := func(task ExecVersionView) { + start := time.Duration(0) + if pe.profile { + start = time.Since(pe.begin) + } + + res := task.Execute() + + if res.err == nil { + pe.mvh.FlushMVWriteSet(res.txAllOut) + } + + pe.resultQueue.Push(res.ver.TxnIndex, res) + pe.chResults <- struct{}{} + + if pe.profile { + end := time.Since(pe.begin) + + pe.statsMutex.Lock() + pe.stats[res.ver.TxnIndex] = ExecutionStat{ + TxIdx: res.ver.TxnIndex, + Incarnation: res.ver.Incarnation, + Start: uint64(start), + End: uint64(end), + Worker: procNum, + } + pe.statsMutex.Unlock() + } + } + + if procNum < NumSpeculativeProcs { + for range pe.chSpeculativeTasks { + doWork(pe.specTaskQueue.Pop().(ExecVersionView)) + } + } else { + for task := range pe.chTasks { + doWork(task) + } + } + }(i) + } + + pe.settleWg.Add(1) + + go func() { + for t := range pe.chSettle { + pe.tasks[t].Settle() + } + + pe.settleWg.Done() + }() + + // bootstrap first execution + tx := pe.execTasks.takeNextPending() + + if tx == -1 { + return ParallelExecFailedError{"no executable transactions due to bad dependency"} + } + + pe.cntExec++ + + pe.chTasks <- ExecVersionView{ver: Version{tx, 0}, et: pe.tasks[tx], mvh: pe.mvh, sender: pe.tasks[tx].Sender()} + + return nil +} + +func (pe *ParallelExecutor) Close(wait bool) { + close(pe.chTasks) + close(pe.chSpeculativeTasks) + close(pe.chSettle) + + if wait { + pe.workerWg.Wait() + } + + if wait { + pe.settleWg.Wait() + } +} + +// nolint: gocognit +func (pe *ParallelExecutor) Step(res *ExecResult) (result ParallelExecutionResult, err error) { + tx := res.ver.TxnIndex + + if abortErr, ok := res.err.(ErrExecAbortError); ok && abortErr.OriginError != nil && pe.skipCheck[tx] { + // If the transaction failed when we know it should not fail, this means the transaction itself is + // bad (e.g. wrong nonce), and we should exit the execution immediately + err = fmt.Errorf("could not apply tx %d [%v]: %w", tx, pe.tasks[tx].Hash(), abortErr.OriginError) + pe.Close(true) + + return + } + + // nolint: nestif + if execErr, ok := res.err.(ErrExecAbortError); ok { + addedDependencies := false + + if execErr.Dependency >= 0 { + l := len(pe.estimateDeps[tx]) + for l > 0 && pe.estimateDeps[tx][l-1] > execErr.Dependency { + pe.execTasks.removeDependency(pe.estimateDeps[tx][l-1]) + pe.estimateDeps[tx] = pe.estimateDeps[tx][:l-1] + l-- + } + + addedDependencies = pe.execTasks.addDependencies(execErr.Dependency, tx) + } else { + estimate := 0 + + if len(pe.estimateDeps[tx]) > 0 { + estimate = pe.estimateDeps[tx][len(pe.estimateDeps[tx])-1] + } + addedDependencies = pe.execTasks.addDependencies(estimate, tx) + newEstimate := estimate + (estimate+tx)/2 + if newEstimate >= tx { + newEstimate = tx - 1 + } + pe.estimateDeps[tx] = append(pe.estimateDeps[tx], newEstimate) + } + + pe.execTasks.clearInProgress(tx) + + if !addedDependencies { + pe.execTasks.pushPending(tx) + } + pe.txIncarnations[tx]++ + pe.diagExecAbort[tx]++ + pe.cntAbort++ + } else { + pe.lastTxIO.recordRead(tx, res.txIn) + + if res.ver.Incarnation == 0 { + pe.lastTxIO.recordWrite(tx, res.txOut) + pe.lastTxIO.recordAllWrite(tx, res.txAllOut) + } else { + if res.txAllOut.hasNewWrite(pe.lastTxIO.AllWriteSet(tx)) { + pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1)) + } + + prevWrite := pe.lastTxIO.AllWriteSet(tx) + + // Remove entries that were previously written but are no longer written + + cmpMap := make(map[Key]bool) + + for _, w := range res.txAllOut { + cmpMap[w.Path] = true + } + + for _, v := range prevWrite { + if _, ok := cmpMap[v.Path]; !ok { + pe.mvh.Delete(v.Path, tx) + } + } + + pe.lastTxIO.recordWrite(tx, res.txOut) + pe.lastTxIO.recordAllWrite(tx, res.txAllOut) + } + + pe.validateTasks.pushPending(tx) + pe.execTasks.markComplete(tx) + pe.diagExecSuccess[tx]++ + pe.cntSuccess++ + + pe.execTasks.removeDependency(tx) + } + + // do validations ... + maxComplete := pe.execTasks.maxAllComplete() + + toValidate := make([]int, 0, 2) + + for pe.validateTasks.minPending() <= maxComplete && pe.validateTasks.minPending() >= 0 { + toValidate = append(toValidate, pe.validateTasks.takeNextPending()) + } + + for i := 0; i < len(toValidate); i++ { + pe.cntTotalValidations++ + + tx := toValidate[i] + + if pe.skipCheck[tx] || ValidateVersion(tx, pe.lastTxIO, pe.mvh) { + pe.validateTasks.markComplete(tx) + } else { + pe.cntValidationFail++ + pe.diagExecAbort[tx]++ + for _, v := range pe.lastTxIO.AllWriteSet(tx) { + pe.mvh.MarkEstimate(v.Path, tx) + } + // 'create validation tasks for all transactions > tx ...' + pe.validateTasks.pushPendingSet(pe.execTasks.getRevalidationRange(tx + 1)) + pe.validateTasks.clearInProgress(tx) // clear in progress - pending will be added again once new incarnation executes + + pe.execTasks.clearComplete(tx) + pe.execTasks.pushPending(tx) + + pe.preValidated[tx] = false + pe.txIncarnations[tx]++ + } + } + + // Settle transactions that have been validated to be correct and that won't be re-executed again + maxValidated := pe.validateTasks.maxAllComplete() + + for pe.lastSettled < maxValidated { + pe.lastSettled++ + if pe.execTasks.checkInProgress(pe.lastSettled) || pe.execTasks.checkPending(pe.lastSettled) || pe.execTasks.isBlocked(pe.lastSettled) { + pe.lastSettled-- + break + } + pe.chSettle <- pe.lastSettled + } + + if pe.validateTasks.countComplete() == len(pe.tasks) && pe.execTasks.countComplete() == len(pe.tasks) { + log.Debug("blockstm exec summary", "execs", pe.cntExec, "success", pe.cntSuccess, "aborts", pe.cntAbort, "validations", pe.cntTotalValidations, "failures", pe.cntValidationFail, "#tasks/#execs", fmt.Sprintf("%.2f%%", float64(len(pe.tasks))/float64(pe.cntExec)*100)) + + pe.Close(true) + + var allDeps map[int]map[int]bool + + var deps DAG + + if pe.profile { + allDeps = GetDep(*pe.lastTxIO) + deps = BuildDAG(*pe.lastTxIO) + } + + return ParallelExecutionResult{pe.lastTxIO, &pe.stats, &deps, allDeps}, err + } + + // Send the next immediate pending transaction to be executed + if pe.execTasks.minPending() != -1 && pe.execTasks.minPending() == maxValidated+1 { + nextTx := pe.execTasks.takeNextPending() + if nextTx != -1 { + pe.cntExec++ + + pe.skipCheck[nextTx] = true + + pe.chTasks <- ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()} + } + } + + // Send speculative tasks + for pe.execTasks.minPending() != -1 { + nextTx := pe.execTasks.takeNextPending() + + if nextTx != -1 { + pe.cntExec++ + + task := ExecVersionView{ver: Version{nextTx, pe.txIncarnations[nextTx]}, et: pe.tasks[nextTx], mvh: pe.mvh, sender: pe.tasks[nextTx].Sender()} + + pe.specTaskQueue.Push(nextTx, task) + pe.chSpeculativeTasks <- struct{}{} + } + } + + return +} + +type PropertyCheck func(*ParallelExecutor) error + +func executeParallelWithCheck(tasks []ExecTask, profile bool, check PropertyCheck, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) { + if len(tasks) == 0 { + return ParallelExecutionResult{MakeTxnInputOutput(len(tasks)), nil, nil, nil}, nil + } + + pe := NewParallelExecutor(tasks, profile, metadata) + err = pe.Prepare() + + if err != nil { + pe.Close(true) + return + } + + for range pe.chResults { + if interruptCtx != nil && interruptCtx.Err() != nil { + pe.Close(true) + return result, interruptCtx.Err() + } + + res := pe.resultQueue.Pop().(ExecResult) + + result, err = pe.Step(&res) + + if err != nil { + return result, err + } + + if check != nil { + err = check(pe) + } + + if result.TxIO != nil || err != nil { + return result, err + } + } + + return +} + +func ExecuteParallel(tasks []ExecTask, profile bool, metadata bool, interruptCtx context.Context) (result ParallelExecutionResult, err error) { + return executeParallelWithCheck(tasks, profile, nil, metadata, interruptCtx) +} diff --git a/core/blockstm/executor_test.go b/core/blockstm/executor_test.go new file mode 100644 index 0000000000..511d9c774a --- /dev/null +++ b/core/blockstm/executor_test.go @@ -0,0 +1,984 @@ +package blockstm + +import ( + "context" + "fmt" + "math/big" + "math/rand" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" +) + +type OpType int + +const readType = 0 +const writeType = 1 +const otherType = 2 +const greenTick = "✅" +const redCross = "❌" + +const threeRockets = "🚀🚀🚀" + +type Op struct { + key Key + duration time.Duration + opType OpType + val int +} + +type testExecTask struct { + txIdx int + ops []Op + readMap map[Key]ReadDescriptor + writeMap map[Key]WriteDescriptor + sender common.Address + nonce int + dependencies []int +} + +type PathGenerator func(addr common.Address, i int, j int, total int) Key + +type TaskRunner func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) + +type TaskRunnerWithMetadata func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) + +type Timer func(txIdx int, opIdx int) time.Duration + +type Sender func(int) common.Address + +func NewTestExecTask(txIdx int, ops []Op, sender common.Address, nonce int) *testExecTask { + return &testExecTask{ + txIdx: txIdx, + ops: ops, + readMap: make(map[Key]ReadDescriptor), + writeMap: make(map[Key]WriteDescriptor), + sender: sender, + nonce: nonce, + dependencies: []int{}, + } +} + +func sleep(i time.Duration) { + start := time.Now() + for time.Since(start) < i { + } +} + +func (t *testExecTask) Execute(mvh *MVHashMap, incarnation int) error { + // Sleep for 50 microsecond to simulate setup time + sleep(time.Microsecond * 50) + + version := Version{TxnIndex: t.txIdx, Incarnation: incarnation} + + t.readMap = make(map[Key]ReadDescriptor) + t.writeMap = make(map[Key]WriteDescriptor) + + deps := -1 + + for i, op := range t.ops { + k := op.key + + switch op.opType { + case readType: + if _, ok := t.writeMap[k]; ok { + sleep(op.duration) + continue + } + + result := mvh.Read(k, t.txIdx) + + val := result.Value() + + if i == 0 && val != nil && (val.(int) != t.nonce) { + return ErrExecAbortError{} + } + + if result.Status() == MVReadResultDependency { + if result.depIdx > deps { + deps = result.depIdx + } + } + + var readKind int + + if result.Status() == MVReadResultDone { + readKind = ReadKindMap + } else if result.Status() == MVReadResultNone { + readKind = ReadKindStorage + } + + sleep(op.duration) + + t.readMap[k] = ReadDescriptor{k, readKind, Version{TxnIndex: result.depIdx, Incarnation: result.incarnation}} + case writeType: + t.writeMap[k] = WriteDescriptor{k, version, op.val} + case otherType: + sleep(op.duration) + default: + panic(fmt.Sprintf("Unknown op type: %d", op.opType)) + } + } + + if deps != -1 { + return ErrExecAbortError{deps, fmt.Errorf("Dependency error")} + } + + return nil +} + +func (t *testExecTask) MVWriteList() []WriteDescriptor { + return t.MVFullWriteList() +} + +func (t *testExecTask) MVFullWriteList() []WriteDescriptor { + writes := make([]WriteDescriptor, 0, len(t.writeMap)) + + for _, v := range t.writeMap { + writes = append(writes, v) + } + + return writes +} + +func (t *testExecTask) MVReadList() []ReadDescriptor { + reads := make([]ReadDescriptor, 0, len(t.readMap)) + + for _, v := range t.readMap { + reads = append(reads, v) + } + + return reads +} + +func (t *testExecTask) Settle() {} + +func (t *testExecTask) Sender() common.Address { + return t.sender +} + +func (t *testExecTask) Hash() common.Hash { + return common.BytesToHash([]byte(fmt.Sprintf("%d", t.txIdx))) +} + +func (t *testExecTask) Dependencies() []int { + return t.dependencies +} + +func randTimeGenerator(min time.Duration, max time.Duration) func(txIdx int, opIdx int) time.Duration { + return func(txIdx int, opIdx int) time.Duration { + return time.Duration(rand.Int63n(int64(max-min))) + min + } +} + +func longTailTimeGenerator(min time.Duration, max time.Duration, i int, j int) func(txIdx int, opIdx int) time.Duration { + return func(txIdx int, opIdx int) time.Duration { + if txIdx%i == 0 && opIdx == j { + return max * 100 + } else { + return time.Duration(rand.Int63n(int64(max-min))) + min + } + } +} + +var randomPathGenerator = func(sender common.Address, i int, j int, total int) Key { + return NewStateKey(common.BigToAddress((big.NewInt(int64(i % 10)))), common.BigToHash((big.NewInt(int64(total))))) +} + +var dexPathGenerator = func(sender common.Address, i int, j int, total int) Key { + if j == total-1 || j == 2 { + return NewSubpathKey(common.BigToAddress(big.NewInt(int64(0))), 1) + } else { + return NewSubpathKey(common.BigToAddress(big.NewInt(int64(j))), 1) + } +} + +var readTime = randTimeGenerator(4*time.Microsecond, 12*time.Microsecond) +var writeTime = randTimeGenerator(2*time.Microsecond, 6*time.Microsecond) +var nonIOTime = randTimeGenerator(1*time.Microsecond, 2*time.Microsecond) + +func taskFactory(numTask int, sender Sender, readsPerT int, writesPerT int, nonIOPerT int, pathGenerator PathGenerator, readTime Timer, writeTime Timer, nonIOTime Timer) ([]ExecTask, time.Duration) { + exec := make([]ExecTask, 0, numTask) + + var serialDuration time.Duration + + senderNonces := make(map[common.Address]int) + + for i := 0; i < numTask; i++ { + s := sender(i) + + // Set first two ops to always read and write nonce + ops := make([]Op, 0, readsPerT+writesPerT+nonIOPerT) + + ops = append(ops, Op{opType: readType, key: NewSubpathKey(s, 2), duration: readTime(i, 0), val: senderNonces[s]}) + + senderNonces[s]++ + + ops = append(ops, Op{opType: writeType, key: NewSubpathKey(s, 2), duration: writeTime(i, 1), val: senderNonces[s]}) + + for j := 0; j < readsPerT-1; j++ { + ops = append(ops, Op{opType: readType}) + } + + for j := 0; j < nonIOPerT; j++ { + ops = append(ops, Op{opType: otherType}) + } + + for j := 0; j < writesPerT-1; j++ { + ops = append(ops, Op{opType: writeType}) + } + + // shuffle ops except for the first three (read nonce, write nonce, another read) ops and last write op. + // This enables random path generator to generate deterministic paths for these "special" ops. + for j := 3; j < len(ops)-1; j++ { + k := rand.Intn(len(ops)-j-1) + j + ops[j], ops[k] = ops[k], ops[j] + } + + // Generate time and key path for each op except first two that are always read and write nonce + for j := 2; j < len(ops); j++ { + if ops[j].opType == readType { + ops[j].key = pathGenerator(s, i, j, len(ops)) + ops[j].duration = readTime(i, j) + } else if ops[j].opType == writeType { + ops[j].key = pathGenerator(s, i, j, len(ops)) + ops[j].duration = writeTime(i, j) + } else { + ops[j].duration = nonIOTime(i, j) + } + + serialDuration += ops[j].duration + } + + if ops[len(ops)-1].opType != writeType { + panic("Last op must be a write") + } + + t := NewTestExecTask(i, ops, s, senderNonces[s]-1) + exec = append(exec, t) + } + + return exec, serialDuration +} + +func testExecutorComb(t *testing.T, totalTxs []int, numReads []int, numWrites []int, numNonIO []int, taskRunner TaskRunner) { + t.Helper() + log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat(false)))) + + improved := 0 + total := 0 + + totalExecDuration := time.Duration(0) + totalSerialDuration := time.Duration(0) + + for _, numTx := range totalTxs { + for _, numRead := range numReads { + for _, numWrite := range numWrites { + for _, numNonIO := range numNonIO { + log.Info("Executing block", "numTx", numTx, "numRead", numRead, "numWrite", numWrite, "numNonIO", numNonIO) + execDuration, expectedSerialDuration := taskRunner(numTx, numRead, numWrite, numNonIO) + + if execDuration < expectedSerialDuration { + improved++ + } + total++ + + performance := greenTick + + if execDuration >= expectedSerialDuration { + performance = redCross + } + + fmt.Printf("exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance) + + totalExecDuration += execDuration + totalSerialDuration += expectedSerialDuration + } + } + } + } + + fmt.Println("Improved: ", improved, "Total: ", total, "success rate: ", float64(improved)/float64(total)*100) + fmt.Printf("Total exec duration: %v, total serial duration: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100) +} + +// nolint: gocognit +func testExecutorCombWithMetadata(t *testing.T, totalTxs []int, numReads []int, numWrites []int, numNonIOs []int, taskRunner TaskRunnerWithMetadata) { + t.Helper() + log.Root().SetHandler(log.LvlFilterHandler(log.LvlDebug, log.StreamHandler(os.Stderr, log.TerminalFormat(false)))) + + improved := 0 + improvedMetadata := 0 + rocket := 0 + total := 0 + + totalExecDuration := time.Duration(0) + totalExecDurationMetadata := time.Duration(0) + totalSerialDuration := time.Duration(0) + + for _, numTx := range totalTxs { + for _, numRead := range numReads { + for _, numWrite := range numWrites { + for _, numNonIO := range numNonIOs { + log.Info("Executing block", "numTx", numTx, "numRead", numRead, "numWrite", numWrite, "numNonIO", numNonIO) + execDuration, execDurationMetadata, expectedSerialDuration := taskRunner(numTx, numRead, numWrite, numNonIO) + + if execDuration < expectedSerialDuration { + improved++ + } + total++ + + performance := greenTick + + if execDuration >= expectedSerialDuration { + performance = redCross + + if execDurationMetadata <= expectedSerialDuration { + performance = threeRockets + rocket++ + } + } + + if execDuration >= execDurationMetadata { + improvedMetadata++ + } + + fmt.Printf("WITHOUT METADATA: exec duration %v, serial duration %v, time reduced %v %.2f%%, %v \n", execDuration, expectedSerialDuration, expectedSerialDuration-execDuration, float64(expectedSerialDuration-execDuration)/float64(expectedSerialDuration)*100, performance) + fmt.Printf("WITH METADATA: exec duration %v, exec duration with metadata %v, time reduced %v %.2f%%\n", execDuration, execDurationMetadata, execDuration-execDurationMetadata, float64(execDuration-execDurationMetadata)/float64(execDuration)*100) + + totalExecDuration += execDuration + totalExecDurationMetadata += execDurationMetadata + totalSerialDuration += expectedSerialDuration + } + } + } + } + + fmt.Println("\nImproved: ", improved, "Total: ", total, "success rate: ", float64(improved)/float64(total)*100) + fmt.Println("Metadata Better: ", improvedMetadata, "out of: ", total, "success rate: ", float64(improvedMetadata)/float64(total)*100) + fmt.Println("Rockets (Time of: metadata < serial < without metadata): ", rocket) + fmt.Printf("\nWithout metadata <> serial: Total exec duration: %v, total serial duration : %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalSerialDuration, totalSerialDuration-totalExecDuration, float64(totalSerialDuration-totalExecDuration)/float64(totalSerialDuration)*100) + fmt.Printf("With metadata <> serial: Total exec duration metadata: %v, total serial duration : %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDurationMetadata, totalSerialDuration, totalSerialDuration-totalExecDurationMetadata, float64(totalSerialDuration-totalExecDurationMetadata)/float64(totalSerialDuration)*100) + fmt.Printf("Without metadata <> with metadata: Total exec duration: %v, total exec duration metadata: %v, time reduced: %v, time reduced percent: %.2f%%\n", totalExecDuration, totalExecDurationMetadata, totalExecDuration-totalExecDurationMetadata, float64(totalExecDuration-totalExecDurationMetadata)/float64(totalExecDuration)*100) +} + +func composeValidations(checks []PropertyCheck) PropertyCheck { + return func(pe *ParallelExecutor) error { + for _, check := range checks { + err := check(pe) + if err != nil { + return err + } + } + + return nil + } +} + +func checkNoStatusOverlap(pe *ParallelExecutor) error { + seen := make(map[int]string) + + for _, tx := range pe.execTasks.complete { + seen[tx] = "complete" + } + + for _, tx := range pe.execTasks.inProgress { + if v, ok := seen[tx]; ok { + return fmt.Errorf("tx %v is in both %v and inProgress", v, tx) + } + + seen[tx] = "inProgress" + } + + for _, tx := range pe.execTasks.pending { + if v, ok := seen[tx]; ok { + return fmt.Errorf("tx %v is in both %v complete and pending", v, tx) + } + + seen[tx] = "pending" + } + + return nil +} + +func checkNoDroppedTx(pe *ParallelExecutor) error { + for i := 0; i < len(pe.tasks); i++ { + if !pe.execTasks.checkComplete(i) && !pe.execTasks.checkInProgress(i) && !pe.execTasks.checkPending(i) { + if !pe.execTasks.isBlocked(i) { + return fmt.Errorf("tx %v is not in any status and is not blocked by any other tx", i) + } + } + } + + return nil +} + +// nolint: unparam +func runParallel(t *testing.T, tasks []ExecTask, validation PropertyCheck, metadata bool) time.Duration { + t.Helper() + + profile := false + + start := time.Now() + result, err := executeParallelWithCheck(tasks, false, validation, metadata, nil) + + if result.Deps != nil && profile { + result.Deps.Report(*result.Stats, func(str string) { fmt.Println(str) }) + } + + assert.NoError(t, err, "error occur during parallel execution") + + // Need to apply the final write set to storage + + finalWriteSet := make(map[Key]time.Duration) + + for _, task := range tasks { + task := task.(*testExecTask) + for _, op := range task.ops { + if op.opType == writeType { + finalWriteSet[op.key] = op.duration + } + } + } + + for _, v := range finalWriteSet { + sleep(v) + } + + duration := time.Since(start) + + return duration +} + +func runParallelGetMetadata(t *testing.T, tasks []ExecTask, validation PropertyCheck) map[int]map[int]bool { + t.Helper() + + res, err := executeParallelWithCheck(tasks, true, validation, false, nil) + + assert.NoError(t, err, "error occur during parallel execution") + + return res.AllDeps +} + +func TestLessConflicts(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i % randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, checks, false), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestLessConflictsWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIOs := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i % randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIOs, taskRunner) +} + +func TestZeroTx(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{0} + numReads := []int{20} + numWrites := []int{20} + numNonIO := []int{100} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(1))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, checks, false), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestAlternatingTx(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{200} + numReads := []int{20} + numWrites := []int{20} + numNonIO := []int{100} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, checks, false), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestAlternatingTxWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{200} + numReads := []int{20} + numWrites := []int{20} + numNonIO := []int{100} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i % 2))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestMoreConflicts(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, checks, false), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestMoreConflictsWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestRandomTx(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + // Randomly assign this tx to one of 10 senders + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, checks, false), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestRandomTxWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + // Randomly assign this tx to one of 10 senders + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(rand.Intn(10)))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestTxWithLongTailRead(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + + longTailReadTimer := longTailTimeGenerator(4*time.Microsecond, 12*time.Microsecond, 7, 10) + + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime) + + return runParallel(t, tasks, checks, false), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestTxWithLongTailReadWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { + randomness := rand.Intn(10) + 10 + return common.BigToAddress(big.NewInt(int64(i / randomness))) + } + + longTailReadTimer := longTailTimeGenerator(4*time.Microsecond, 12*time.Microsecond, 7, 10) + + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, randomPathGenerator, longTailReadTimer, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestDexScenario(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{10, 50, 100, 200, 300} + numReads := []int{20, 100, 200} + numWrites := []int{20, 100, 200} + numNonIO := []int{100, 500} + + postValidation := func(pe *ParallelExecutor) error { + if pe.lastSettled == len(pe.tasks) { + for i, inputs := range pe.lastTxIO.inputs { + for _, input := range inputs { + if input.V.TxnIndex != i-1 { + return fmt.Errorf("Tx %d should depend on tx %d, but it actually depends on %d", i, i-1, input.V.TxnIndex) + } + } + } + } + + return nil + } + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, postValidation, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime) + + return runParallel(t, tasks, checks, false), serialDuration + } + + testExecutorComb(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestDexScenarioWithMetadata(t *testing.T) { + t.Parallel() + rand.Seed(0) + + totalTxs := []int{300} + numReads := []int{100, 200} + numWrites := []int{100, 200} + numNonIO := []int{100, 500} + + postValidation := func(pe *ParallelExecutor) error { + if pe.lastSettled == len(pe.tasks) { + for i, inputs := range pe.lastTxIO.inputs { + for _, input := range inputs { + if input.V.TxnIndex != i-1 { + return fmt.Errorf("Tx %d should depend on tx %d, but it actually depends on %d", i, i-1, input.V.TxnIndex) + } + } + } + } + + return nil + } + + checks := composeValidations([]PropertyCheck{checkNoStatusOverlap, postValidation, checkNoDroppedTx}) + + taskRunner := func(numTx int, numRead int, numWrite int, numNonIO int) (time.Duration, time.Duration, time.Duration) { + sender := func(i int) common.Address { return common.BigToAddress(big.NewInt(int64(i))) } + tasks, serialDuration := taskFactory(numTx, sender, numRead, numWrite, numNonIO, dexPathGenerator, readTime, writeTime, nonIOTime) + + parallelDuration := runParallel(t, tasks, checks, false) + + allDeps := runParallelGetMetadata(t, tasks, checks) + + newTasks := make([]ExecTask, 0, len(tasks)) + + for _, t := range tasks { + temp := t.(*testExecTask) + + keys := make([]int, len(allDeps[temp.txIdx])) + + i := 0 + + for k := range allDeps[temp.txIdx] { + keys[i] = k + i++ + } + + temp.dependencies = keys + newTasks = append(newTasks, temp) + } + + return parallelDuration, runParallel(t, newTasks, checks, true), serialDuration + } + + testExecutorCombWithMetadata(t, totalTxs, numReads, numWrites, numNonIO, taskRunner) +} + +func TestBreakFromCircularDependency(t *testing.T) { + t.Parallel() + rand.Seed(0) + + tasks := make([]ExecTask, 5) + + for i := range tasks { + tasks[i] = &testExecTask{ + txIdx: i, + dependencies: []int{ + (i + len(tasks) - 1) % len(tasks), + }, + } + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // This should not hang + _, err := ExecuteParallel(tasks, false, true, ctx) + + if err == nil { + t.Error("Expected cancel error") + } +} + +func TestBreakFromPartialCircularDependency(t *testing.T) { + t.Parallel() + rand.Seed(0) + + tasks := make([]ExecTask, 5) + + for i := range tasks { + if i < 3 { + tasks[i] = &testExecTask{ + txIdx: i, + dependencies: []int{ + (i + 2) % 3, + }, + } + } else { + tasks[i] = &testExecTask{ + txIdx: i, + dependencies: []int{}, + } + } + } + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + // This should not hang + _, err := ExecuteParallel(tasks, false, true, ctx) + + if err == nil { + t.Error("Expected cancel error") + } +} diff --git a/core/blockstm/mvhashmap.go b/core/blockstm/mvhashmap.go new file mode 100644 index 0000000000..2a517bcc84 --- /dev/null +++ b/core/blockstm/mvhashmap.go @@ -0,0 +1,290 @@ +package blockstm + +import ( + "fmt" + "sync" + + "github.com/emirpasic/gods/maps/treemap" + + "github.com/ethereum/go-ethereum/common" +) + +const FlagDone = 0 +const FlagEstimate = 1 + +const addressType = 1 +const stateType = 2 +const subpathType = 3 + +const KeyLength = common.AddressLength + common.HashLength + 2 + +type Key [KeyLength]byte + +func (k Key) IsAddress() bool { + return k[KeyLength-1] == addressType +} + +func (k Key) IsState() bool { + return k[KeyLength-1] == stateType +} + +func (k Key) IsSubpath() bool { + return k[KeyLength-1] == subpathType +} + +func (k Key) GetAddress() common.Address { + return common.BytesToAddress(k[:common.AddressLength]) +} + +func (k Key) GetStateKey() common.Hash { + return common.BytesToHash(k[common.AddressLength : KeyLength-2]) +} + +func (k Key) GetSubpath() byte { + return k[KeyLength-2] +} + +func newKey(addr common.Address, hash common.Hash, subpath byte, keyType byte) Key { + var k Key + + copy(k[:common.AddressLength], addr.Bytes()) + copy(k[common.AddressLength:KeyLength-2], hash.Bytes()) + k[KeyLength-2] = subpath + k[KeyLength-1] = keyType + + return k +} + +func NewAddressKey(addr common.Address) Key { + return newKey(addr, common.Hash{}, 0, addressType) +} + +func NewStateKey(addr common.Address, hash common.Hash) Key { + k := newKey(addr, hash, 0, stateType) + if !k.IsState() { + panic(fmt.Errorf("key is not a state key")) + } + + return k +} + +func NewSubpathKey(addr common.Address, subpath byte) Key { + return newKey(addr, common.Hash{}, subpath, subpathType) +} + +type MVHashMap struct { + m sync.Map + s sync.Map +} + +func MakeMVHashMap() *MVHashMap { + return &MVHashMap{} +} + +type WriteCell struct { + flag uint + incarnation int + data interface{} +} + +type TxnIndexCells struct { + rw sync.RWMutex + tm *treemap.Map +} + +type Version struct { + TxnIndex int + Incarnation int +} + +func (mv *MVHashMap) getKeyCells(k Key, fNoKey func(kenc Key) *TxnIndexCells) (cells *TxnIndexCells) { + val, ok := mv.m.Load(k) + + if !ok { + cells = fNoKey(k) + } else { + cells = val.(*TxnIndexCells) + } + + return +} + +func (mv *MVHashMap) Write(k Key, v Version, data interface{}) { + cells := mv.getKeyCells(k, func(kenc Key) (cells *TxnIndexCells) { + n := &TxnIndexCells{ + rw: sync.RWMutex{}, + tm: treemap.NewWithIntComparator(), + } + cells = n + val, _ := mv.m.LoadOrStore(kenc, n) + cells = val.(*TxnIndexCells) + return + }) + + cells.rw.RLock() + ci, ok := cells.tm.Get(v.TxnIndex) + cells.rw.RUnlock() + + if ok { + if ci.(*WriteCell).incarnation > v.Incarnation { + panic(fmt.Errorf("existing transaction value does not have lower incarnation: %v, %v", + k, v.TxnIndex)) + } + + ci.(*WriteCell).flag = FlagDone + ci.(*WriteCell).incarnation = v.Incarnation + ci.(*WriteCell).data = data + } else { + cells.rw.Lock() + if ci, ok = cells.tm.Get(v.TxnIndex); !ok { + cells.tm.Put(v.TxnIndex, &WriteCell{ + flag: FlagDone, + incarnation: v.Incarnation, + data: data, + }) + } else { + ci.(*WriteCell).flag = FlagDone + ci.(*WriteCell).incarnation = v.Incarnation + ci.(*WriteCell).data = data + } + cells.rw.Unlock() + } +} + +func (mv *MVHashMap) ReadStorage(k Key, fallBack func() any) any { + data, ok := mv.s.Load(string(k[:])) + if !ok { + data = fallBack() + data, _ = mv.s.LoadOrStore(string(k[:]), data) + } + + return data +} + +func (mv *MVHashMap) MarkEstimate(k Key, txIdx int) { + cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells { + panic(fmt.Errorf("path must already exist")) + }) + + cells.rw.RLock() + if ci, ok := cells.tm.Get(txIdx); !ok { + panic(fmt.Sprintf("should not happen - cell should be present for path. TxIdx: %v, path, %x, cells keys: %v", txIdx, k, cells.tm.Keys())) + } else { + ci.(*WriteCell).flag = FlagEstimate + } + cells.rw.RUnlock() +} + +func (mv *MVHashMap) Delete(k Key, txIdx int) { + cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells { + panic(fmt.Errorf("path must already exist")) + }) + + cells.rw.Lock() + defer cells.rw.Unlock() + cells.tm.Remove(txIdx) +} + +const ( + MVReadResultDone = 0 + MVReadResultDependency = 1 + MVReadResultNone = 2 +) + +type MVReadResult struct { + depIdx int + incarnation int + value interface{} +} + +func (res *MVReadResult) DepIdx() int { + return res.depIdx +} + +func (res *MVReadResult) Incarnation() int { + return res.incarnation +} + +func (res *MVReadResult) Value() interface{} { + return res.value +} + +func (mvr MVReadResult) Status() int { + if mvr.depIdx != -1 { + if mvr.incarnation == -1 { + return MVReadResultDependency + } else { + return MVReadResultDone + } + } + + return MVReadResultNone +} + +func (mv *MVHashMap) Read(k Key, txIdx int) (res MVReadResult) { + res.depIdx = -1 + res.incarnation = -1 + + cells := mv.getKeyCells(k, func(_ Key) *TxnIndexCells { + return nil + }) + if cells == nil { + return + } + + cells.rw.RLock() + fk, fv := cells.tm.Floor(txIdx - 1) + cells.rw.RUnlock() + + if fk != nil && fv != nil { + c := fv.(*WriteCell) + switch c.flag { + case FlagEstimate: + res.depIdx = fk.(int) + res.value = c.data + case FlagDone: + { + res.depIdx = fk.(int) + res.incarnation = c.incarnation + res.value = c.data + } + default: + panic(fmt.Errorf("should not happen - unknown flag value")) + } + } + + return +} + +func (mv *MVHashMap) FlushMVWriteSet(writes []WriteDescriptor) { + for _, v := range writes { + mv.Write(v.Path, v.V, v.Val) + } +} + +func ValidateVersion(txIdx int, lastInputOutput *TxnInputOutput, versionedData *MVHashMap) (valid bool) { + valid = true + + for _, rd := range lastInputOutput.ReadSet(txIdx) { + mvResult := versionedData.Read(rd.Path, txIdx) + switch mvResult.Status() { + case MVReadResultDone: + valid = rd.Kind == ReadKindMap && rd.V == Version{ + TxnIndex: mvResult.depIdx, + Incarnation: mvResult.incarnation, + } + case MVReadResultDependency: + valid = false + case MVReadResultNone: + valid = rd.Kind == ReadKindStorage // feels like an assertion? + default: + panic(fmt.Errorf("should not happen - undefined mv read status: %ver", mvResult.Status())) + } + + if !valid { + break + } + } + + return +} diff --git a/core/blockstm/mvhashmap_test.go b/core/blockstm/mvhashmap_test.go new file mode 100644 index 0000000000..7ed728426c --- /dev/null +++ b/core/blockstm/mvhashmap_test.go @@ -0,0 +1,344 @@ +package blockstm + +import ( + "fmt" + "math/big" + "math/rand" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" +) + +var randomness = rand.Intn(10) + 10 + +// create test data for a given txIdx and incarnation +func valueFor(txIdx, inc int) []byte { + return []byte(fmt.Sprintf("%ver:%ver:%ver", txIdx*5, txIdx+inc, inc*5)) +} + +func getCommonAddress(i int) common.Address { + return common.BigToAddress(big.NewInt(int64(i % randomness))) +} + +func TestHelperFunctions(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + ap2 := NewAddressKey(getCommonAddress(2)) + + mvh := MakeMVHashMap() + + mvh.Write(ap1, Version{0, 1}, valueFor(0, 1)) + mvh.Write(ap1, Version{0, 2}, valueFor(0, 2)) + res := mvh.Read(ap1, 0) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + mvh.Write(ap2, Version{1, 1}, valueFor(1, 1)) + mvh.Write(ap2, Version{1, 2}, valueFor(1, 2)) + res = mvh.Read(ap2, 1) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + mvh.Write(ap1, Version{2, 1}, valueFor(2, 1)) + mvh.Write(ap1, Version{2, 2}, valueFor(2, 2)) + res = mvh.Read(ap1, 2) + require.Equal(t, 0, res.DepIdx()) + require.Equal(t, 2, res.Incarnation()) + require.Equal(t, valueFor(0, 2), res.Value().([]byte)) + require.Equal(t, 0, res.Status()) +} + +func TestFlushMVWrite(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + ap2 := NewAddressKey(getCommonAddress(2)) + + mvh := MakeMVHashMap() + + var res MVReadResult + + wd := []WriteDescriptor{} + + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{0, 1}, + Val: valueFor(0, 1), + }) + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{0, 2}, + Val: valueFor(0, 2), + }) + wd = append(wd, WriteDescriptor{ + Path: ap2, + V: Version{1, 1}, + Val: valueFor(1, 1), + }) + wd = append(wd, WriteDescriptor{ + Path: ap2, + V: Version{1, 2}, + Val: valueFor(1, 2), + }) + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{2, 1}, + Val: valueFor(2, 1), + }) + wd = append(wd, WriteDescriptor{ + Path: ap1, + V: Version{2, 2}, + Val: valueFor(2, 2), + }) + + mvh.FlushMVWriteSet(wd) + + res = mvh.Read(ap1, 0) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + res = mvh.Read(ap2, 1) + require.Equal(t, -1, res.DepIdx()) + require.Equal(t, -1, res.Incarnation()) + require.Equal(t, 2, res.Status()) + + res = mvh.Read(ap1, 2) + require.Equal(t, 0, res.DepIdx()) + require.Equal(t, 2, res.Incarnation()) + require.Equal(t, valueFor(0, 2), res.Value().([]byte)) + require.Equal(t, 0, res.Status()) +} + +// TODO - handle panic +func TestLowerIncarnation(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + + mvh := MakeMVHashMap() + + mvh.Write(ap1, Version{0, 2}, valueFor(0, 2)) + mvh.Read(ap1, 0) + mvh.Write(ap1, Version{1, 2}, valueFor(1, 2)) + mvh.Write(ap1, Version{0, 5}, valueFor(0, 5)) + mvh.Write(ap1, Version{1, 5}, valueFor(1, 5)) +} + +func TestMarkEstimate(t *testing.T) { + t.Parallel() + + ap1 := NewAddressKey(getCommonAddress(1)) + + mvh := MakeMVHashMap() + + mvh.Write(ap1, Version{7, 2}, valueFor(7, 2)) + mvh.MarkEstimate(ap1, 7) + mvh.Write(ap1, Version{7, 4}, valueFor(7, 4)) +} + +func TestMVHashMapBasics(t *testing.T) { + t.Parallel() + + // memory locations + ap1 := NewAddressKey(getCommonAddress(1)) + ap2 := NewAddressKey(getCommonAddress(2)) + ap3 := NewAddressKey(getCommonAddress(3)) + + mvh := MakeMVHashMap() + + res := mvh.Read(ap1, 5) + require.Equal(t, -1, res.depIdx) + + mvh.Write(ap1, Version{10, 1}, valueFor(10, 1)) + + res = mvh.Read(ap1, 9) + require.Equal(t, -1, res.depIdx, "reads that should go the the DB return dependency -1") + res = mvh.Read(ap1, 10) + require.Equal(t, -1, res.depIdx, "Read returns entries from smaller txns, not txn 10") + + // Reads for a higher txn return the entry written by txn 10. + res = mvh.Read(ap1, 15) + require.Equal(t, 10, res.depIdx, "reads for a higher txn return the entry written by txn 10.") + require.Equal(t, 1, res.incarnation) + require.Equal(t, valueFor(10, 1), res.value) + + // More writes. + mvh.Write(ap1, Version{12, 0}, valueFor(12, 0)) + mvh.Write(ap1, Version{8, 3}, valueFor(8, 3)) + + // Verify reads. + res = mvh.Read(ap1, 15) + require.Equal(t, 12, res.depIdx) + require.Equal(t, 0, res.incarnation) + require.Equal(t, valueFor(12, 0), res.value) + + res = mvh.Read(ap1, 11) + require.Equal(t, 10, res.depIdx) + require.Equal(t, 1, res.incarnation) + require.Equal(t, valueFor(10, 1), res.value) + + res = mvh.Read(ap1, 10) + require.Equal(t, 8, res.depIdx) + require.Equal(t, 3, res.incarnation) + require.Equal(t, valueFor(8, 3), res.value) + + // Mark the entry written by 10 as an estimate. + mvh.MarkEstimate(ap1, 10) + + res = mvh.Read(ap1, 11) + require.Equal(t, 10, res.depIdx) + require.Equal(t, -1, res.incarnation, "dep at tx 10 is now an estimate") + + // Delete the entry written by 10, write to a different ap. + mvh.Delete(ap1, 10) + mvh.Write(ap2, Version{10, 2}, valueFor(10, 2)) + + // Read by txn 11 no longer observes entry from txn 10. + res = mvh.Read(ap1, 11) + require.Equal(t, 8, res.depIdx) + require.Equal(t, 3, res.incarnation) + require.Equal(t, valueFor(8, 3), res.value) + + // Reads, writes for ap2 and ap3. + mvh.Write(ap2, Version{5, 0}, valueFor(5, 0)) + mvh.Write(ap3, Version{20, 4}, valueFor(20, 4)) + + res = mvh.Read(ap2, 10) + require.Equal(t, 5, res.depIdx) + require.Equal(t, 0, res.incarnation) + require.Equal(t, valueFor(5, 0), res.value) + + res = mvh.Read(ap3, 21) + require.Equal(t, 20, res.depIdx) + require.Equal(t, 4, res.incarnation) + require.Equal(t, valueFor(20, 4), res.value) + + // Clear ap1 and ap3. + mvh.Delete(ap1, 12) + mvh.Delete(ap1, 8) + mvh.Delete(ap3, 20) + + // Reads from ap1 and ap3 go to db. + res = mvh.Read(ap1, 30) + require.Equal(t, -1, res.depIdx) + + res = mvh.Read(ap3, 30) + require.Equal(t, -1, res.depIdx) + + // No-op delete at ap2 - doesn't panic because ap2 does exist + mvh.Delete(ap2, 11) + + // Read entry by txn 10 at ap2. + res = mvh.Read(ap2, 15) + require.Equal(t, 10, res.depIdx) + require.Equal(t, 2, res.incarnation) + require.Equal(t, valueFor(10, 2), res.value) +} + +func BenchmarkWriteTimeSameLocationDifferentTxIdx(b *testing.B) { + mvh2 := MakeMVHashMap() + ap2 := NewAddressKey(getCommonAddress(2)) + + randInts := []int{} + for i := 0; i < b.N; i++ { + randInts = append(randInts, rand.Intn(1000000000000000)) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + mvh2.Write(ap2, Version{randInts[i], 1}, valueFor(randInts[i], 1)) + } +} + +func BenchmarkReadTimeSameLocationDifferentTxIdx(b *testing.B) { + mvh2 := MakeMVHashMap() + ap2 := NewAddressKey(getCommonAddress(2)) + txIdxSlice := []int{} + + for i := 0; i < b.N; i++ { + txIdx := rand.Intn(1000000000000000) + txIdxSlice = append(txIdxSlice, txIdx) + mvh2.Write(ap2, Version{txIdx, 1}, valueFor(txIdx, 1)) + } + + b.ResetTimer() + + for _, value := range txIdxSlice { + mvh2.Read(ap2, value) + } +} + +func TestTimeComplexity(t *testing.T) { + t.Parallel() + + // for 1000000 read and write with no dependency at different memory location + mvh1 := MakeMVHashMap() + + for i := 0; i < 1000000; i++ { + ap1 := NewAddressKey(getCommonAddress(i)) + mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1)) + mvh1.Read(ap1, i) + } + + // for 1000000 read and write with dependency at same memory location + mvh2 := MakeMVHashMap() + ap2 := NewAddressKey(getCommonAddress(2)) + + for i := 0; i < 1000000; i++ { + mvh2.Write(ap2, Version{i, 1}, valueFor(i, 1)) + mvh2.Read(ap2, i) + } +} + +func TestWriteTimeSameLocationDifferentTxnIdx(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + ap1 := NewAddressKey(getCommonAddress(1)) + + for i := 0; i < 1000000; i++ { + mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1)) + } +} + +func TestWriteTimeSameLocationSameTxnIdx(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + ap1 := NewAddressKey(getCommonAddress(1)) + + for i := 0; i < 1000000; i++ { + mvh1.Write(ap1, Version{1, i}, valueFor(i, 1)) + } +} + +func TestWriteTimeDifferentLocation(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + + for i := 0; i < 1000000; i++ { + ap1 := NewAddressKey(getCommonAddress(i)) + mvh1.Write(ap1, Version{i, 1}, valueFor(i, 1)) + } +} + +func TestReadTimeSameLocation(t *testing.T) { + t.Parallel() + + mvh1 := MakeMVHashMap() + ap1 := NewAddressKey(getCommonAddress(1)) + + mvh1.Write(ap1, Version{1, 1}, valueFor(1, 1)) + + for i := 0; i < 1000000; i++ { + mvh1.Read(ap1, 2) + } +} diff --git a/core/blockstm/status.go b/core/blockstm/status.go new file mode 100644 index 0000000000..3025cf6c3e --- /dev/null +++ b/core/blockstm/status.go @@ -0,0 +1,225 @@ +package blockstm + +import ( + "fmt" + "sort" +) + +func makeStatusManager(numTasks int) (t taskStatusManager) { + t.pending = make([]int, numTasks) + for i := 0; i < numTasks; i++ { + t.pending[i] = i + } + + t.dependency = make(map[int]map[int]bool, numTasks) + t.blocker = make(map[int]map[int]bool, numTasks) + + for i := 0; i < numTasks; i++ { + t.blocker[i] = make(map[int]bool) + } + + return +} + +type taskStatusManager struct { + pending []int + inProgress []int + complete []int + dependency map[int]map[int]bool + blocker map[int]map[int]bool +} + +func insertInList(l []int, v int) []int { + if len(l) == 0 || v > l[len(l)-1] { + return append(l, v) + } else { + x := sort.SearchInts(l, v) + if x < len(l) && l[x] == v { + // already in list + return l + } + a := append(l[:x+1], l[x:]...) + a[x] = v + return a + } +} + +func (m *taskStatusManager) takeNextPending() int { + if len(m.pending) == 0 { + return -1 + } + + x := m.pending[0] + m.pending = m.pending[1:] + m.inProgress = insertInList(m.inProgress, x) + + return x +} + +func hasNoGap(l []int) bool { + return l[0]+len(l) == l[len(l)-1]+1 +} + +func (m taskStatusManager) maxAllComplete() int { + if len(m.complete) == 0 || m.complete[0] != 0 { + return -1 + } else if m.complete[len(m.complete)-1] == len(m.complete)-1 { + return m.complete[len(m.complete)-1] + } else { + for i := len(m.complete) - 2; i >= 0; i-- { + if hasNoGap(m.complete[:i+1]) { + return m.complete[i] + } + } + } + + return -1 +} + +func (m *taskStatusManager) pushPending(tx int) { + m.pending = insertInList(m.pending, tx) +} + +func removeFromList(l []int, v int, expect bool) []int { + x := sort.SearchInts(l, v) + if x == -1 || l[x] != v { + if expect { + panic(fmt.Errorf("should not happen - element expected in list")) + } + + return l + } + + switch x { + case 0: + return l[1:] + case len(l) - 1: + return l[:len(l)-1] + default: + return append(l[:x], l[x+1:]...) + } +} + +func (m *taskStatusManager) markComplete(tx int) { + m.inProgress = removeFromList(m.inProgress, tx, true) + m.complete = insertInList(m.complete, tx) +} + +func (m *taskStatusManager) minPending() int { + if len(m.pending) == 0 { + return -1 + } else { + return m.pending[0] + } +} + +func (m *taskStatusManager) countComplete() int { + return len(m.complete) +} + +func (m *taskStatusManager) addDependencies(blocker int, dependent int) bool { + if blocker < 0 || blocker >= dependent { + return false + } + + curblockers := m.blocker[dependent] + + if m.checkComplete(blocker) { + // Blocker has already completed + delete(curblockers, blocker) + + return len(curblockers) > 0 + } + + if _, ok := m.dependency[blocker]; !ok { + m.dependency[blocker] = make(map[int]bool) + } + + m.dependency[blocker][dependent] = true + curblockers[blocker] = true + + return true +} + +func (m *taskStatusManager) isBlocked(tx int) bool { + return len(m.blocker[tx]) > 0 +} + +func (m *taskStatusManager) removeDependency(tx int) { + if deps, ok := m.dependency[tx]; ok && len(deps) > 0 { + for k := range deps { + delete(m.blocker[k], tx) + + if len(m.blocker[k]) == 0 { + if !m.checkComplete(k) && !m.checkPending(k) && !m.checkInProgress(k) { + m.pushPending(k) + } + } + } + + delete(m.dependency, tx) + } +} + +func (m *taskStatusManager) clearInProgress(tx int) { + m.inProgress = removeFromList(m.inProgress, tx, true) +} + +func (m *taskStatusManager) checkInProgress(tx int) bool { + x := sort.SearchInts(m.inProgress, tx) + if x < len(m.inProgress) && m.inProgress[x] == tx { + return true + } + + return false +} + +func (m *taskStatusManager) checkPending(tx int) bool { + x := sort.SearchInts(m.pending, tx) + if x < len(m.pending) && m.pending[x] == tx { + return true + } + + return false +} + +func (m *taskStatusManager) checkComplete(tx int) bool { + x := sort.SearchInts(m.complete, tx) + if x < len(m.complete) && m.complete[x] == tx { + return true + } + + return false +} + +// getRevalidationRange: this range will be all tasks from tx (inclusive) that are not currently in progress up to the +// +// 'all complete' limit +func (m *taskStatusManager) getRevalidationRange(txFrom int) (ret []int) { + max := m.maxAllComplete() // haven't learned to trust compilers :) + for x := txFrom; x <= max; x++ { + if !m.checkInProgress(x) { + ret = append(ret, x) + } + } + + return +} + +func (m *taskStatusManager) pushPendingSet(set []int) { + for _, v := range set { + if m.checkComplete(v) { + m.clearComplete(v) + } + + m.pushPending(v) + } +} + +func (m *taskStatusManager) clearComplete(tx int) { + m.complete = removeFromList(m.complete, tx, false) +} + +func (m *taskStatusManager) clearPending(tx int) { + m.pending = removeFromList(m.pending, tx, false) +} diff --git a/core/blockstm/status_test.go b/core/blockstm/status_test.go new file mode 100644 index 0000000000..aff00d9a2f --- /dev/null +++ b/core/blockstm/status_test.go @@ -0,0 +1,82 @@ +package blockstm + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestStatusBasics(t *testing.T) { + t.Parallel() + + s := makeStatusManager(10) + + x := s.takeNextPending() + require.Equal(t, 0, x) + require.True(t, s.checkInProgress(x)) + + x = s.takeNextPending() + require.Equal(t, 1, x) + require.True(t, s.checkInProgress(x)) + + x = s.takeNextPending() + require.Equal(t, 2, x) + require.True(t, s.checkInProgress(x)) + + s.markComplete(0) + require.False(t, s.checkInProgress(0)) + s.markComplete(1) + s.markComplete(2) + require.False(t, s.checkInProgress(1)) + require.False(t, s.checkInProgress(2)) + require.Equal(t, 2, s.maxAllComplete()) + + x = s.takeNextPending() + require.Equal(t, 3, x) + + x = s.takeNextPending() + require.Equal(t, 4, x) + + s.markComplete(x) + require.False(t, s.checkInProgress(4)) + require.Equal(t, 2, s.maxAllComplete(), "zero should still be min complete") + + exp := []int{1, 2} + require.Equal(t, exp, s.getRevalidationRange(1)) +} + +func TestMaxComplete(t *testing.T) { + t.Parallel() + + s := makeStatusManager(10) + + for { + tx := s.takeNextPending() + + if tx == -1 { + break + } + + if tx != 7 { + s.markComplete(tx) + } + } + + require.Equal(t, 6, s.maxAllComplete()) + + s2 := makeStatusManager(10) + + for { + tx := s2.takeNextPending() + + if tx == -1 { + break + } + } + s2.markComplete(2) + s2.markComplete(4) + require.Equal(t, -1, s2.maxAllComplete()) + + s2.complete = insertInList(s2.complete, 4) + require.Equal(t, 2, s2.countComplete()) +} diff --git a/core/blockstm/txio.go b/core/blockstm/txio.go new file mode 100644 index 0000000000..19955fb152 --- /dev/null +++ b/core/blockstm/txio.go @@ -0,0 +1,106 @@ +package blockstm + +const ( + ReadKindMap = 0 + ReadKindStorage = 1 +) + +type ReadDescriptor struct { + Path Key + Kind int + V Version +} + +type WriteDescriptor struct { + Path Key + V Version + Val interface{} +} + +type TxnInput []ReadDescriptor +type TxnOutput []WriteDescriptor + +// hasNewWrite: returns true if the current set has a new write compared to the input +func (txo TxnOutput) hasNewWrite(cmpSet []WriteDescriptor) bool { + if len(txo) == 0 { + return false + } else if len(cmpSet) == 0 || len(txo) > len(cmpSet) { + return true + } + + cmpMap := map[Key]bool{cmpSet[0].Path: true} + + for i := 1; i < len(cmpSet); i++ { + cmpMap[cmpSet[i].Path] = true + } + + for _, v := range txo { + if !cmpMap[v.Path] { + return true + } + } + + return false +} + +type TxnInputOutput struct { + inputs []TxnInput + outputs []TxnOutput // write sets that should be checked during validation + outputsSet []map[Key]struct{} + allOutputs []TxnOutput // entire write sets in MVHashMap. allOutputs should always be a parent set of outputs +} + +func (io *TxnInputOutput) ReadSet(txnIdx int) []ReadDescriptor { + return io.inputs[txnIdx] +} + +func (io *TxnInputOutput) WriteSet(txnIdx int) []WriteDescriptor { + return io.outputs[txnIdx] +} + +func (io *TxnInputOutput) AllWriteSet(txnIdx int) []WriteDescriptor { + return io.allOutputs[txnIdx] +} + +func (io *TxnInputOutput) HasWritten(txnIdx int, k Key) bool { + _, ok := io.outputsSet[txnIdx][k] + return ok +} + +func MakeTxnInputOutput(numTx int) *TxnInputOutput { + return &TxnInputOutput{ + inputs: make([]TxnInput, numTx), + outputs: make([]TxnOutput, numTx), + outputsSet: make([]map[Key]struct{}, numTx), + allOutputs: make([]TxnOutput, numTx), + } +} + +func (io *TxnInputOutput) recordRead(txId int, input []ReadDescriptor) { + io.inputs[txId] = input +} + +func (io *TxnInputOutput) recordWrite(txId int, output []WriteDescriptor) { + io.outputs[txId] = output + io.outputsSet[txId] = make(map[Key]struct{}, len(output)) + + for _, v := range output { + io.outputsSet[txId][v.Path] = struct{}{} + } +} + +func (io *TxnInputOutput) recordAllWrite(txId int, output []WriteDescriptor) { + io.allOutputs[txId] = output +} + +func (io *TxnInputOutput) RecordReadAtOnce(inputs [][]ReadDescriptor) { + for ind, val := range inputs { + io.inputs[ind] = val + } +} + +func (io *TxnInputOutput) RecordAllWriteAtOnce(outputs [][]WriteDescriptor) { + for ind, val := range outputs { + io.allOutputs[ind] = val + } +} diff --git a/core/chain_makers.go b/core/chain_makers.go index 0828a9ec7c..85fe8648e9 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -434,6 +434,19 @@ func makeBlockChainWithGenesis(genesis *Genesis, n int, engine consensus.Engine, return db, blocks } +// makeBlockChain creates a deterministic chain of blocks rooted at parent with fake invalid transactions. +func makeFakeNonEmptyBlockChain(parent *types.Block, n int, engine consensus.Engine, db ethdb.Database, seed int, numTx int) []*types.Block { + blocks, _ := GenerateChain(params.TestChainConfig, parent, engine, db, n, func(i int, b *BlockGen) { + addr := common.Address{0: byte(seed), 19: byte(i)} + b.SetCoinbase(addr) + for j := 0; j < numTx; j++ { + b.txs = append(b.txs, types.NewTransaction(0, addr, big.NewInt(1000), params.TxGas, nil, nil)) + } + }) + + return blocks +} + type fakeChainReader struct { config *params.ChainConfig stateSyncData []*types.StateSyncData diff --git a/core/evm.go b/core/evm.go index f62106b179..979bcf1716 100644 --- a/core/evm.go +++ b/core/evm.go @@ -18,6 +18,7 @@ package core import ( "math/big" + "sync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" @@ -83,12 +84,18 @@ func GetHashFn(ref *types.Header, chain ChainContext) func(n uint64) common.Hash // Then fill up with [refHash.p, refHash.pp, refHash.ppp, ...] var cache []common.Hash + cacheMutex := &sync.Mutex{} + return func(n uint64) common.Hash { if ref.Number.Uint64() <= n { // This situation can happen if we're doing tracing and using // block overrides. return common.Hash{} } + + cacheMutex.Lock() + defer cacheMutex.Unlock() + // If there's no hash cache yet, make one if len(cache) == 0 { cache = append(cache, ref.ParentHash) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go new file mode 100644 index 0000000000..8956f6f4d2 --- /dev/null +++ b/core/parallel_state_processor.go @@ -0,0 +1,429 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "context" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core/blockstm" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/params" +) + +type ParallelEVMConfig struct { + Enable bool + SpeculativeProcesses int +} + +// StateProcessor is a basic Processor, which takes care of transitioning +// state from one point to another. +// +// StateProcessor implements Processor. +type ParallelStateProcessor struct { + config *params.ChainConfig // Chain configuration options + bc *BlockChain // Canonical block chain + engine consensus.Engine // Consensus engine used for block rewards +} + +// NewStateProcessor initialises a new StateProcessor. +func NewParallelStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consensus.Engine) *ParallelStateProcessor { + return &ParallelStateProcessor{ + config: config, + bc: bc, + engine: engine, + } +} + +type ExecutionTask struct { + msg types.Message + config *params.ChainConfig + + gasLimit uint64 + blockNumber *big.Int + blockHash common.Hash + tx *types.Transaction + index int + statedb *state.StateDB // State database that stores the modified values after tx execution. + cleanStateDB *state.StateDB // A clean copy of the initial statedb. It should not be modified. + finalStateDB *state.StateDB // The final statedb. + header *types.Header + blockChain *BlockChain + evmConfig vm.Config + result *ExecutionResult + shouldDelayFeeCal *bool + shouldRerunWithoutFeeDelay bool + sender common.Address + totalUsedGas *uint64 + receipts *types.Receipts + allLogs *[]*types.Log + + // length of dependencies -> 2 + k (k = a whole number) + // first 2 element in dependencies -> transaction index, and flag representing if delay is allowed or not + // (0 -> delay is not allowed, 1 -> delay is allowed) + // next k elements in dependencies -> transaction indexes on which transaction i is dependent on + dependencies []int + coinbase common.Address + blockContext vm.BlockContext +} + +func (task *ExecutionTask) Execute(mvh *blockstm.MVHashMap, incarnation int) (err error) { + task.statedb = task.cleanStateDB.Copy() + task.statedb.Prepare(task.tx.Hash(), task.index) + task.statedb.SetMVHashmap(mvh) + task.statedb.SetIncarnation(incarnation) + + evm := vm.NewEVM(task.blockContext, vm.TxContext{}, task.statedb, task.config, task.evmConfig) + + // Create a new context to be used in the EVM environment. + txContext := NewEVMTxContext(task.msg) + evm.Reset(txContext, task.statedb) + + defer func() { + if r := recover(); r != nil { + // In some pre-matured executions, EVM will panic. Recover from panic and retry the execution. + log.Debug("Recovered from EVM failure.", "Error:", r) + + err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex()} + + return + } + }() + + // Apply the transaction to the current state (included in the env). + if *task.shouldDelayFeeCal { + task.result, err = ApplyMessageNoFeeBurnOrTip(evm, task.msg, new(GasPool).AddGas(task.gasLimit), nil) + + if task.result == nil || err != nil { + return blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err} + } + + reads := task.statedb.MVReadMap() + + if _, ok := reads[blockstm.NewSubpathKey(task.blockContext.Coinbase, state.BalancePath)]; ok { + log.Info("Coinbase is in MVReadMap", "address", task.blockContext.Coinbase) + + task.shouldRerunWithoutFeeDelay = true + } + + if _, ok := reads[blockstm.NewSubpathKey(task.result.BurntContractAddress, state.BalancePath)]; ok { + log.Info("BurntContractAddress is in MVReadMap", "address", task.result.BurntContractAddress) + + task.shouldRerunWithoutFeeDelay = true + } + } else { + task.result, err = ApplyMessage(evm, task.msg, new(GasPool).AddGas(task.gasLimit), nil) + } + + if task.statedb.HadInvalidRead() || err != nil { + err = blockstm.ErrExecAbortError{Dependency: task.statedb.DepTxIndex(), OriginError: err} + return + } + + task.statedb.Finalise(task.config.IsEIP158(task.blockNumber)) + + return +} + +func (task *ExecutionTask) MVReadList() []blockstm.ReadDescriptor { + return task.statedb.MVReadList() +} + +func (task *ExecutionTask) MVWriteList() []blockstm.WriteDescriptor { + return task.statedb.MVWriteList() +} + +func (task *ExecutionTask) MVFullWriteList() []blockstm.WriteDescriptor { + return task.statedb.MVFullWriteList() +} + +func (task *ExecutionTask) Sender() common.Address { + return task.sender +} + +func (task *ExecutionTask) Hash() common.Hash { + return task.tx.Hash() +} + +func (task *ExecutionTask) Dependencies() []int { + return task.dependencies +} + +func (task *ExecutionTask) Settle() { + task.finalStateDB.Prepare(task.tx.Hash(), task.index) + + coinbaseBalance := task.finalStateDB.GetBalance(task.coinbase) + + task.finalStateDB.ApplyMVWriteSet(task.statedb.MVFullWriteList()) + + for _, l := range task.statedb.GetLogs(task.tx.Hash(), task.blockHash) { + task.finalStateDB.AddLog(l) + } + + if *task.shouldDelayFeeCal { + if task.config.IsLondon(task.blockNumber) { + task.finalStateDB.AddBalance(task.result.BurntContractAddress, task.result.FeeBurnt) + } + + task.finalStateDB.AddBalance(task.coinbase, task.result.FeeTipped) + output1 := new(big.Int).SetBytes(task.result.SenderInitBalance.Bytes()) + output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) + + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + AddFeeTransferLog( + task.finalStateDB, + + task.msg.From(), + task.coinbase, + + task.result.FeeTipped, + task.result.SenderInitBalance, + coinbaseBalance, + output1.Sub(output1, task.result.FeeTipped), + output2.Add(output2, task.result.FeeTipped), + ) + } + + for k, v := range task.statedb.Preimages() { + task.finalStateDB.AddPreimage(k, v) + } + + // Update the state with pending changes. + var root []byte + + if task.config.IsByzantium(task.blockNumber) { + task.finalStateDB.Finalise(true) + } else { + root = task.finalStateDB.IntermediateRoot(task.config.IsEIP158(task.blockNumber)).Bytes() + } + + *task.totalUsedGas += task.result.UsedGas + + // Create a new receipt for the transaction, storing the intermediate root and gas used + // by the tx. + receipt := &types.Receipt{Type: task.tx.Type(), PostState: root, CumulativeGasUsed: *task.totalUsedGas} + if task.result.Failed() { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + + receipt.TxHash = task.tx.Hash() + receipt.GasUsed = task.result.UsedGas + + // If the transaction created a contract, store the creation address in the receipt. + if task.msg.To() == nil { + receipt.ContractAddress = crypto.CreateAddress(task.msg.From(), task.tx.Nonce()) + } + + // Set the receipt logs and create the bloom filter. + receipt.Logs = task.finalStateDB.GetLogs(task.tx.Hash(), task.blockHash) + receipt.Bloom = types.CreateBloom(types.Receipts{receipt}) + receipt.BlockHash = task.blockHash + receipt.BlockNumber = task.blockNumber + receipt.TransactionIndex = uint(task.finalStateDB.TxIndex()) + + *task.receipts = append(*task.receipts, receipt) + *task.allLogs = append(*task.allLogs, receipt.Logs...) +} + +var parallelizabilityTimer = metrics.NewRegisteredTimer("block/parallelizability", nil) + +// Process processes the state changes according to the Ethereum rules by running +// the transaction messages using the statedb and applying any rewards to both +// the processor (coinbase) and any included uncles. +// +// Process returns the receipts and logs accumulated during the process and +// returns the amount of gas that was used in the process. If any of the +// transactions failed to execute due to insufficient gas it will return an error. +// nolint:gocognit +func (p *ParallelStateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) { + blockstm.SetProcs(cfg.ParallelSpeculativeProcesses) + + var ( + receipts types.Receipts + header = block.Header() + blockHash = block.Hash() + blockNumber = block.Number() + allLogs []*types.Log + usedGas = new(uint64) + metadata bool + ) + + // Mutate the block and state according to any hard-fork specs + if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { + misc.ApplyDAOHardFork(statedb) + } + + tasks := make([]blockstm.ExecTask, 0, len(block.Transactions())) + + shouldDelayFeeCal := true + + coinbase, _ := p.bc.Engine().Author(header) + + deps := GetDeps(block.Header().TxDependency) + + if block.Header().TxDependency != nil { + metadata = true + } + + blockContext := NewEVMBlockContext(header, p.bc, nil) + + // Iterate over and process the individual transactions + for i, tx := range block.Transactions() { + msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) + if err != nil { + log.Error("error creating message", "err", err) + return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + } + + cleansdb := statedb.Copy() + + if msg.From() == coinbase { + shouldDelayFeeCal = false + } + + if len(header.TxDependency) != len(block.Transactions()) { + task := &ExecutionTask{ + msg: msg, + config: p.config, + gasLimit: block.GasLimit(), + blockNumber: blockNumber, + blockHash: blockHash, + tx: tx, + index: i, + cleanStateDB: cleansdb, + finalStateDB: statedb, + blockChain: p.bc, + header: header, + evmConfig: cfg, + shouldDelayFeeCal: &shouldDelayFeeCal, + sender: msg.From(), + totalUsedGas: usedGas, + receipts: &receipts, + allLogs: &allLogs, + dependencies: deps[i], + coinbase: coinbase, + blockContext: blockContext, + } + + tasks = append(tasks, task) + } else { + task := &ExecutionTask{ + msg: msg, + config: p.config, + gasLimit: block.GasLimit(), + blockNumber: blockNumber, + blockHash: blockHash, + tx: tx, + index: i, + cleanStateDB: cleansdb, + finalStateDB: statedb, + blockChain: p.bc, + header: header, + evmConfig: cfg, + shouldDelayFeeCal: &shouldDelayFeeCal, + sender: msg.From(), + totalUsedGas: usedGas, + receipts: &receipts, + allLogs: &allLogs, + dependencies: nil, + coinbase: coinbase, + blockContext: blockContext, + } + + tasks = append(tasks, task) + } + } + + backupStateDB := statedb.Copy() + + profile := false + result, err := blockstm.ExecuteParallel(tasks, profile, metadata, interruptCtx) + + if err == nil && profile && result.Deps != nil { + _, weight := result.Deps.LongestPath(*result.Stats) + + serialWeight := uint64(0) + + for i := 0; i < len(result.Deps.GetVertices()); i++ { + serialWeight += (*result.Stats)[i].End - (*result.Stats)[i].Start + } + + parallelizabilityTimer.Update(time.Duration(serialWeight * 100 / weight)) + } + + for _, task := range tasks { + task := task.(*ExecutionTask) + if task.shouldRerunWithoutFeeDelay { + shouldDelayFeeCal = false + + statedb.StopPrefetcher() + *statedb = *backupStateDB + + allLogs = []*types.Log{} + receipts = types.Receipts{} + usedGas = new(uint64) + + for _, t := range tasks { + t := t.(*ExecutionTask) + t.finalStateDB = backupStateDB + t.allLogs = &allLogs + t.receipts = &receipts + t.totalUsedGas = usedGas + } + + _, err = blockstm.ExecuteParallel(tasks, false, metadata, interruptCtx) + + break + } + } + + if err != nil { + return nil, nil, 0, err + } + + // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) + p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles()) + + return receipts, allLogs, *usedGas, nil +} + +func GetDeps(txDependency [][]uint64) map[int][]int { + deps := make(map[int][]int) + + for i := 0; i <= len(txDependency)-1; i++ { + deps[i] = []int{} + + for j := 0; j <= len(txDependency[i])-1; j++ { + deps[i] = append(deps[i], int(txDependency[i][j])) + } + } + + return deps +} diff --git a/core/state/journal.go b/core/state/journal.go index 1722fb4c02..0d2155398f 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -20,6 +20,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/blockstm" ) // journalEntry is a modification entry in the state change journal that can be @@ -148,6 +149,7 @@ type ( func (ch createObjectChange) revert(s *StateDB) { delete(s.stateObjects, *ch.account) delete(s.stateObjectsDirty, *ch.account) + RevertWrite(s, blockstm.NewAddressKey(*ch.account)) } func (ch createObjectChange) dirtied() *common.Address { @@ -156,6 +158,7 @@ func (ch createObjectChange) dirtied() *common.Address { func (ch resetObjectChange) revert(s *StateDB) { s.setStateObject(ch.prev) + RevertWrite(s, blockstm.NewAddressKey(ch.prev.address)) if !ch.prevdestruct { delete(s.stateObjectsDestruct, ch.prev.address) } @@ -170,6 +173,7 @@ func (ch suicideChange) revert(s *StateDB) { if obj != nil { obj.suicided = ch.prev obj.setBalance(ch.prevbalance) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, SuicidePath)) } } @@ -204,6 +208,7 @@ func (ch nonceChange) dirtied() *common.Address { func (ch codeChange) revert(s *StateDB) { s.getStateObject(*ch.account).setCode(common.BytesToHash(ch.prevhash), ch.prevcode) + RevertWrite(s, blockstm.NewSubpathKey(*ch.account, CodePath)) } func (ch codeChange) dirtied() *common.Address { @@ -212,6 +217,7 @@ func (ch codeChange) dirtied() *common.Address { func (ch storageChange) revert(s *StateDB) { s.getStateObject(*ch.account).setState(ch.key, ch.prevalue) + RevertWrite(s, blockstm.NewStateKey(*ch.account, ch.key)) } func (ch storageChange) dirtied() *common.Address { diff --git a/core/state/statedb.go b/core/state/statedb.go index 9ba8604bfb..b68480989e 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -25,6 +25,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" @@ -78,6 +79,14 @@ type StateDB struct { stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution stateObjectsDestruct map[common.Address]struct{} // State objects destructed in the block + // Block-stm related fields + mvHashmap *blockstm.MVHashMap + incarnation int + readMap map[blockstm.Key]blockstm.ReadDescriptor + writeMap map[blockstm.Key]blockstm.WriteDescriptor + revertedKeys map[blockstm.Key]struct{} + dep int + // DB error. // State objects are used by the consensus core and VM which are // unable to deal with database-level errors. Any error that occurs @@ -144,6 +153,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) stateObjectsPending: make(map[common.Address]struct{}), stateObjectsDirty: make(map[common.Address]struct{}), stateObjectsDestruct: make(map[common.Address]struct{}), + revertedKeys: make(map[blockstm.Key]struct{}), logs: make(map[common.Hash][]*types.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), @@ -160,6 +170,281 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) return sdb, nil } +func NewWithMVHashmap(root common.Hash, db Database, snaps *snapshot.Tree, mvhm *blockstm.MVHashMap) (*StateDB, error) { + if sdb, err := New(root, db, snaps); err != nil { + return nil, err + } else { + sdb.mvHashmap = mvhm + sdb.dep = -1 + return sdb, nil + } +} + +func (s *StateDB) SetMVHashmap(mvhm *blockstm.MVHashMap) { + s.mvHashmap = mvhm + s.dep = -1 +} + +func (s *StateDB) GetMVHashmap() *blockstm.MVHashMap { + return s.mvHashmap +} + +func (s *StateDB) MVWriteList() []blockstm.WriteDescriptor { + writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) + + for _, v := range s.writeMap { + if _, ok := s.revertedKeys[v.Path]; !ok { + writes = append(writes, v) + } + } + + return writes +} + +func (s *StateDB) MVFullWriteList() []blockstm.WriteDescriptor { + writes := make([]blockstm.WriteDescriptor, 0, len(s.writeMap)) + + for _, v := range s.writeMap { + writes = append(writes, v) + } + + return writes +} + +func (s *StateDB) MVReadMap() map[blockstm.Key]blockstm.ReadDescriptor { + return s.readMap +} + +func (s *StateDB) MVReadList() []blockstm.ReadDescriptor { + reads := make([]blockstm.ReadDescriptor, 0, len(s.readMap)) + + for _, v := range s.MVReadMap() { + reads = append(reads, v) + } + + return reads +} + +func (s *StateDB) ensureReadMap() { + if s.readMap == nil { + s.readMap = make(map[blockstm.Key]blockstm.ReadDescriptor) + } +} + +func (s *StateDB) ensureWriteMap() { + if s.writeMap == nil { + s.writeMap = make(map[blockstm.Key]blockstm.WriteDescriptor) + } +} + +func (s *StateDB) ClearReadMap() { + s.readMap = make(map[blockstm.Key]blockstm.ReadDescriptor) +} + +func (s *StateDB) ClearWriteMap() { + s.writeMap = make(map[blockstm.Key]blockstm.WriteDescriptor) +} + +func (s *StateDB) HadInvalidRead() bool { + return s.dep >= 0 +} + +func (s *StateDB) DepTxIndex() int { + return s.dep +} + +func (s *StateDB) SetIncarnation(inc int) { + s.incarnation = inc +} + +type StorageVal[T any] struct { + Value *T +} + +func MVRead[T any](s *StateDB, k blockstm.Key, defaultV T, readStorage func(s *StateDB) T) (v T) { + if s.mvHashmap == nil { + return readStorage(s) + } + + s.ensureReadMap() + + if s.writeMap != nil { + if _, ok := s.writeMap[k]; ok { + return readStorage(s) + } + } + + if !k.IsAddress() { + // If we are reading subpath from a deleted account, return default value instead of reading from MVHashmap + addr := k.GetAddress() + if s.getStateObject(addr) == nil { + return defaultV + } + } + + res := s.mvHashmap.Read(k, s.txIndex) + + var rd blockstm.ReadDescriptor + + rd.V = blockstm.Version{ + TxnIndex: res.DepIdx(), + Incarnation: res.Incarnation(), + } + + rd.Path = k + + switch res.Status() { + case blockstm.MVReadResultDone: + { + v = readStorage(res.Value().(*StateDB)) + rd.Kind = blockstm.ReadKindMap + } + case blockstm.MVReadResultDependency: + { + s.dep = res.DepIdx() + + panic("Found dependency") + } + case blockstm.MVReadResultNone: + { + v = readStorage(s) + rd.Kind = blockstm.ReadKindStorage + } + default: + return defaultV + } + + // TODO: I assume we don't want to overwrite an existing read because this could - for example - change a storage + // read to map if the same value is read multiple times. + if _, ok := s.readMap[k]; !ok { + s.readMap[k] = rd + } + + return +} + +func MVWrite(s *StateDB, k blockstm.Key) { + if s.mvHashmap != nil { + s.ensureWriteMap() + s.writeMap[k] = blockstm.WriteDescriptor{ + Path: k, + V: s.Version(), + Val: s, + } + } +} + +func RevertWrite(s *StateDB, k blockstm.Key) { + s.revertedKeys[k] = struct{}{} +} + +func MVWritten(s *StateDB, k blockstm.Key) bool { + if s.mvHashmap == nil || s.writeMap == nil { + return false + } + + _, ok := s.writeMap[k] + + return ok +} + +// FlushMVWriteSet applies entries in the write set to MVHashMap. Note that this function does not clear the write set. +func (s *StateDB) FlushMVWriteSet() { + if s.mvHashmap != nil && s.writeMap != nil { + s.mvHashmap.FlushMVWriteSet(s.MVFullWriteList()) + } +} + +// ApplyMVWriteSet applies entries in a given write set to StateDB. Note that this function does not change MVHashMap nor write set +// of the current StateDB. +func (s *StateDB) ApplyMVWriteSet(writes []blockstm.WriteDescriptor) { + for i := range writes { + path := writes[i].Path + sr := writes[i].Val.(*StateDB) + + if path.IsState() { + addr := path.GetAddress() + stateKey := path.GetStateKey() + state := sr.GetState(addr, stateKey) + s.SetState(addr, stateKey, state) + } else if path.IsAddress() { + continue + } else { + addr := path.GetAddress() + switch path.GetSubpath() { + case BalancePath: + s.SetBalance(addr, sr.GetBalance(addr)) + case NoncePath: + s.SetNonce(addr, sr.GetNonce(addr)) + case CodePath: + s.SetCode(addr, sr.GetCode(addr)) + case SuicidePath: + stateObject := sr.getDeletedStateObject(addr) + if stateObject != nil && stateObject.deleted { + s.Suicide(addr) + } + default: + panic(fmt.Errorf("unknown key type: %d", path.GetSubpath())) + } + } + } +} + +type DumpStruct struct { + TxIdx int + TxInc int + VerIdx int + VerInc int + Path []byte + Op string +} + +// GetReadMapDump gets readMap Dump of format: "TxIdx, Inc, Path, Read" +func (s *StateDB) GetReadMapDump() []DumpStruct { + readList := s.MVReadList() + res := make([]DumpStruct, 0, len(readList)) + + for _, val := range readList { + temp := &DumpStruct{ + TxIdx: s.txIndex, + TxInc: s.incarnation, + VerIdx: val.V.TxnIndex, + VerInc: val.V.Incarnation, + Path: val.Path[:], + Op: "Read\n", + } + res = append(res, *temp) + } + + return res +} + +// GetWriteMapDump gets writeMap Dump of format: "TxIdx, Inc, Path, Write" +func (s *StateDB) GetWriteMapDump() []DumpStruct { + writeList := s.MVReadList() + res := make([]DumpStruct, 0, len(writeList)) + + for _, val := range writeList { + temp := &DumpStruct{ + TxIdx: s.txIndex, + TxInc: s.incarnation, + VerIdx: val.V.TxnIndex, + VerInc: val.V.Incarnation, + Path: val.Path[:], + Op: "Write\n", + } + res = append(res, *temp) + } + + return res +} + +// AddEmptyMVHashMap adds empty MVHashMap to StateDB +func (s *StateDB) AddEmptyMVHashMap() { + mvh := blockstm.MakeMVHashMap() + s.mvHashmap = mvh +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. @@ -267,22 +552,40 @@ func (s *StateDB) Empty(addr common.Address) bool { return so == nil || so.empty() } +// Create a unique path for special fields (e.g. balance, code) in a state object. +// func subPath(prefix []byte, s uint8) [blockstm.KeyLength]byte { +// path := append(prefix, common.Hash{}.Bytes()...) // append a full empty hash to avoid collision with storage state +// path = append(path, s) // append the special field identifier + +// return path +// } + +const BalancePath = 1 +const NoncePath = 2 +const CodePath = 3 +const SuicidePath = 4 + // GetBalance retrieves the balance from the given address or 0 if object not found func (s *StateDB) GetBalance(addr common.Address) *big.Int { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.Balance() - } - return common.Big0 + return MVRead(s, blockstm.NewSubpathKey(addr, BalancePath), common.Big0, func(s *StateDB) *big.Int { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Balance() + } + + return common.Big0 + }) } func (s *StateDB) GetNonce(addr common.Address) uint64 { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.Nonce() - } + return MVRead(s, blockstm.NewSubpathKey(addr, NoncePath), 0, func(s *StateDB) uint64 { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Nonce() + } - return 0 + return 0 + }) } // TxIndex returns the current transaction index set by Prepare. @@ -290,37 +593,52 @@ func (s *StateDB) TxIndex() int { return s.txIndex } -func (s *StateDB) GetCode(addr common.Address) []byte { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.Code(s.db) +func (s *StateDB) Version() blockstm.Version { + return blockstm.Version{ + TxnIndex: s.txIndex, + Incarnation: s.incarnation, } - return nil +} + +func (s *StateDB) GetCode(addr common.Address) []byte { + return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), nil, func(s *StateDB) []byte { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.Code(s.db) + } + return nil + }) } func (s *StateDB) GetCodeSize(addr common.Address) int { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.CodeSize(s.db) - } - return 0 + return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), 0, func(s *StateDB) int { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.CodeSize(s.db) + } + return 0 + }) } func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { - stateObject := s.getStateObject(addr) - if stateObject == nil { - return common.Hash{} - } - return common.BytesToHash(stateObject.CodeHash()) + return MVRead(s, blockstm.NewSubpathKey(addr, CodePath), common.Hash{}, func(s *StateDB) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject == nil { + return common.Hash{} + } + return common.BytesToHash(stateObject.CodeHash()) + }) } // GetState retrieves a value from the given account's storage trie. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.GetState(s.db, hash) - } - return common.Hash{} + return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetState(s.db, hash) + } + return common.Hash{} + }) } // GetProof returns the Merkle proof for a given account. @@ -354,11 +672,13 @@ func (s *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byte, // GetCommittedState retrieves a value from the given account's committed storage trie. func (s *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.GetCommittedState(s.db, hash) - } - return common.Hash{} + return MVRead(s, blockstm.NewStateKey(addr, hash), common.Hash{}, func(s *StateDB) common.Hash { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.GetCommittedState(s.db, hash) + } + return common.Hash{} + }) } // Database retrieves the low level database supporting the lower level trie ops. @@ -382,11 +702,13 @@ func (s *StateDB) StorageTrie(addr common.Address) (Trie, error) { } func (s *StateDB) HasSuicided(addr common.Address) bool { - stateObject := s.getStateObject(addr) - if stateObject != nil { - return stateObject.suicided - } - return false + return MVRead(s, blockstm.NewSubpathKey(addr, SuicidePath), false, func(s *StateDB) bool { + stateObject := s.getStateObject(addr) + if stateObject != nil { + return stateObject.suicided + } + return false + }) } /* @@ -396,44 +718,68 @@ func (s *StateDB) HasSuicided(addr common.Address) bool { // AddBalance adds amount to the account associated with addr. func (s *StateDB) AddBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) + + if s.mvHashmap != nil { + // ensure a read balance operation is recorded in mvHashmap + s.GetBalance(addr) + } + if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.AddBalance(amount) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } // SubBalance subtracts amount from the account associated with addr. func (s *StateDB) SubBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) + + if s.mvHashmap != nil { + // ensure a read balance operation is recorded in mvHashmap + s.GetBalance(addr) + } + if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SubBalance(amount) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } func (s *StateDB) SetBalance(addr common.Address, amount *big.Int) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetBalance(amount) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } func (s *StateDB) SetNonce(addr common.Address, nonce uint64) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetNonce(nonce) + MVWrite(s, blockstm.NewSubpathKey(addr, NoncePath)) } } func (s *StateDB) SetCode(addr common.Address, code []byte) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetCode(crypto.Keccak256Hash(code), code) + MVWrite(s, blockstm.NewSubpathKey(addr, CodePath)) } } func (s *StateDB) SetState(addr common.Address, key, value common.Hash) { stateObject := s.GetOrNewStateObject(addr) if stateObject != nil { + stateObject = s.mvRecordWritten(stateObject) stateObject.SetState(s.db, key, value) + MVWrite(s, blockstm.NewStateKey(addr, key)) } } @@ -462,6 +808,8 @@ func (s *StateDB) Suicide(addr common.Address) bool { if stateObject == nil { return false } + + stateObject = s.mvRecordWritten(stateObject) s.journal.append(suicideChange{ account: &addr, prev: stateObject.suicided, @@ -470,6 +818,9 @@ func (s *StateDB) Suicide(addr common.Address) bool { stateObject.markSuicided() stateObject.data.Balance = new(big.Int) + MVWrite(s, blockstm.NewSubpathKey(addr, SuicidePath)) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) + return true } @@ -553,56 +904,58 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { // flag set. This is needed by the state journal to revert to the correct s- // destructed object instead of wiping all knowledge about the state object. func (s *StateDB) getDeletedStateObject(addr common.Address) *stateObject { - // Prefer live objects if any is available - if obj := s.stateObjects[addr]; obj != nil { - return obj - } - // If no live objects are available, attempt to use snapshots - var data *types.StateAccount - if s.snap != nil { - start := time.Now() - acc, err := s.snap.Account(crypto.HashData(s.hasher, addr.Bytes())) - if metrics.EnabledExpensive { - s.SnapshotAccountReads += time.Since(start) + return MVRead(s, blockstm.NewAddressKey(addr), nil, func(s *StateDB) *stateObject { + // Prefer live objects if any is available + if obj := s.stateObjects[addr]; obj != nil { + return obj } - if err == nil { - if acc == nil { - return nil + // If no live objects are available, attempt to use snapshots + var data *types.StateAccount + if s.snap != nil { // nolint + start := time.Now() + acc, err := s.snap.Account(crypto.HashData(crypto.NewKeccakState(), addr.Bytes())) + if metrics.EnabledExpensive { + s.SnapshotAccountReads += time.Since(start) } - data = &types.StateAccount{ - Nonce: acc.Nonce, - Balance: acc.Balance, - CodeHash: acc.CodeHash, - Root: common.BytesToHash(acc.Root), - } - if len(data.CodeHash) == 0 { + if err == nil { + if acc == nil { + return nil + } + data = &types.StateAccount{ + Nonce: acc.Nonce, + Balance: acc.Balance, + CodeHash: acc.CodeHash, + Root: common.BytesToHash(acc.Root), + } + if len(data.CodeHash) == 0 { data.CodeHash = types.EmptyCodeHash.Bytes() - } - if data.Root == (common.Hash{}) { + } + if data.Root == (common.Hash{}) { data.Root = types.EmptyRootHash + } } } - } - // If snapshot unavailable or reading from it failed, load from the database - if data == nil { - start := time.Now() + // If snapshot unavailable or reading from it failed, load from the database + if data == nil { + start := time.Now() var err error data, err = s.trie.GetAccount(addr) - if metrics.EnabledExpensive { - s.AccountReads += time.Since(start) - } - if err != nil { + if metrics.EnabledExpensive { + s.AccountReads += time.Since(start) + } + if err != nil { s.setError(fmt.Errorf("getDeleteStateObject (%x) error: %w", addr.Bytes(), err)) - return nil - } + return nil + } if data == nil { - return nil + return nil + } } - } - // Insert into the live set - obj := newObject(s, addr, *data) - s.setStateObject(obj) - return obj + // Insert into the live set + obj := newObject(s, addr, *data) + s.setStateObject(obj) + return obj + }) } func (s *StateDB) setStateObject(object *stateObject) { @@ -618,6 +971,28 @@ func (s *StateDB) GetOrNewStateObject(addr common.Address) *stateObject { return stateObject } +// mvRecordWritten checks whether a state object is already present in the current MV writeMap. +// If yes, it returns the object directly. +// If not, it clones the object and inserts it into the writeMap before returning it. +func (s *StateDB) mvRecordWritten(object *stateObject) *stateObject { + if s.mvHashmap == nil { + return object + } + + addrKey := blockstm.NewAddressKey(object.Address()) + + if MVWritten(s, addrKey) { + return object + } + + // Deepcopy is needed to ensure that objects are not written by multiple transactions at the same time, because + // the input state object can come from a different transaction. + s.setStateObject(object.deepCopy(s)) + MVWrite(s, addrKey) + + return s.stateObjects[object.Address()] +} + // createObject creates a new state object. If there is an existing account with // the given address, it is overwritten and returned as the second return value. func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) { @@ -637,6 +1012,8 @@ func (s *StateDB) createObject(addr common.Address) (newobj, prev *stateObject) s.journal.append(resetObjectChange{prev: prev, prevdestruct: prevdestruct}) } s.setStateObject(newobj) + + MVWrite(s, blockstm.NewAddressKey(addr)) if prev != nil && !prev.deleted { return newobj, prev } @@ -657,22 +1034,23 @@ func (s *StateDB) CreateAccount(addr common.Address) { newObj, prev := s.createObject(addr) if prev != nil { newObj.setBalance(prev.data.Balance) + MVWrite(s, blockstm.NewSubpathKey(addr, BalancePath)) } } -func (db *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { - so := db.getStateObject(addr) +func (s *StateDB) ForEachStorage(addr common.Address, cb func(key, value common.Hash) bool) error { + so := s.getStateObject(addr) if so == nil { return nil } - tr, err := so.getTrie(db.db) + tr, err := so.getTrie(s.db) if err != nil { return err } it := trie.NewIterator(tr.NodeIterator(nil)) for it.Next() { - key := common.BytesToHash(db.trie.GetKey(it.Key)) + key := common.BytesToHash(s.trie.GetKey(it.Key)) if value, dirty := so.dirtyStorage[key]; dirty { if !cb(key, value) { return nil @@ -705,6 +1083,7 @@ func (s *StateDB) Copy() *StateDB { stateObjectsPending: make(map[common.Address]struct{}, len(s.stateObjectsPending)), stateObjectsDirty: make(map[common.Address]struct{}, len(s.journal.dirties)), stateObjectsDestruct: make(map[common.Address]struct{}, len(s.stateObjectsDestruct)), + revertedKeys: make(map[blockstm.Key]struct{}), refund: s.refund, logs: make(map[common.Hash][]*types.Log, len(s.logs)), logSize: s.logSize, @@ -796,6 +1175,10 @@ func (s *StateDB) Copy() *StateDB { state.snapStorage[k] = temp } } + + if s.mvHashmap != nil { + state.mvHashmap = s.mvHashmap + } return state } diff --git a/core/state/statedb_test.go b/core/state/statedb_test.go index 8aa59e3ee5..7f4c2c848e 100644 --- a/core/state/statedb_test.go +++ b/core/state/statedb_test.go @@ -29,7 +29,10 @@ import ( "testing" "testing/quick" + "github.com/stretchr/testify/assert" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" ) @@ -498,6 +501,457 @@ func TestTouchDelete(t *testing.T) { } } +func TestMVHashMapReadWriteDelete(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val := common.HexToHash("0x01") + balance := new(big.Int).SetUint64(uint64(100)) + + // Tx0 read + v := states[0].GetState(addr, key) + + assert.Equal(t, common.Hash{}, v) + + // Tx1 write + states[1].GetOrNewStateObject(addr) + states[1].SetState(addr, key, val) + states[1].SetBalance(addr, balance) + states[1].FlushMVWriteSet() + + // Tx1 read + v = states[1].GetState(addr, key) + b := states[1].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + + // Tx2 read + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + + // Tx3 delete + states[3].Suicide(addr) + + // Within Tx 3, the state should not change before finalize + v = states[3].GetState(addr, key) + assert.Equal(t, val, v) + + // After finalizing Tx 3, the state will change + states[3].Finalise(false) + v = states[3].GetState(addr, key) + assert.Equal(t, common.Hash{}, v) + states[3].FlushMVWriteSet() + + // Tx4 read + v = states[4].GetState(addr, key) + b = states[4].GetBalance(addr) + + assert.Equal(t, common.Hash{}, v) + assert.Equal(t, common.Big0, b) +} + +func TestMVHashMapRevert(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val := common.HexToHash("0x01") + balance := new(big.Int).SetUint64(uint64(100)) + + // Tx0 write + states[0].GetOrNewStateObject(addr) + states[0].SetState(addr, key, val) + states[0].SetBalance(addr, balance) + states[0].FlushMVWriteSet() + + // Tx1 perform some ops and then revert + snapshot := states[1].Snapshot() + states[1].AddBalance(addr, new(big.Int).SetUint64(uint64(100))) + states[1].SetState(addr, key, common.HexToHash("0x02")) + v := states[1].GetState(addr, key) + b := states[1].GetBalance(addr) + assert.Equal(t, new(big.Int).SetUint64(uint64(200)), b) + assert.Equal(t, common.HexToHash("0x02"), v) + + states[1].Suicide(addr) + + states[1].RevertToSnapshot(snapshot) + + v = states[1].GetState(addr, key) + b = states[1].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + states[1].Finalise(false) + states[1].FlushMVWriteSet() + + // Tx2 check the state and balance + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) +} + +func TestMVHashMapMarkEstimate(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val := common.HexToHash("0x01") + balance := new(big.Int).SetUint64(uint64(100)) + + // Tx0 read + v := states[0].GetState(addr, key) + assert.Equal(t, common.Hash{}, v) + + // Tx0 write + states[0].SetState(addr, key, val) + v = states[0].GetState(addr, key) + assert.Equal(t, val, v) + states[0].FlushMVWriteSet() + + // Tx1 write + states[1].GetOrNewStateObject(addr) + states[1].SetState(addr, key, val) + states[1].SetBalance(addr, balance) + states[1].FlushMVWriteSet() + + // Tx2 read + v = states[2].GetState(addr, key) + b := states[2].GetBalance(addr) + + assert.Equal(t, val, v) + assert.Equal(t, balance, b) + + // Tx1 mark estimate + for _, v := range states[1].MVWriteList() { + mvhm.MarkEstimate(v.Path, 1) + } + + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else { + t.Log("Recovered in f", r) + } + }() + + // Tx2 read again should get default (empty) vals because its dependency Tx1 is marked as estimate + states[2].GetState(addr, key) + states[2].GetBalance(addr) + + // Tx1 read again should get Tx0 vals + v = states[1].GetState(addr, key) + assert.Equal(t, val, v) +} + +func TestMVHashMapOverwrite(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key := common.HexToHash("0x01") + val1 := common.HexToHash("0x01") + balance1 := new(big.Int).SetUint64(uint64(100)) + val2 := common.HexToHash("0x02") + balance2 := new(big.Int).SetUint64(uint64(200)) + + // Tx0 write + states[0].GetOrNewStateObject(addr) + states[0].SetState(addr, key, val1) + states[0].SetBalance(addr, balance1) + states[0].FlushMVWriteSet() + + // Tx1 write + states[1].SetState(addr, key, val2) + states[1].SetBalance(addr, balance2) + v := states[1].GetState(addr, key) + b := states[1].GetBalance(addr) + states[1].FlushMVWriteSet() + + assert.Equal(t, val2, v) + assert.Equal(t, balance2, b) + + // Tx2 read should get Tx1's value + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val2, v) + assert.Equal(t, balance2, b) + + // Tx1 delete + for _, v := range states[1].writeMap { + mvhm.Delete(v.Path, 1) + + states[1].writeMap = nil + } + + // Tx2 read should get Tx0's value + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, val1, v) + assert.Equal(t, balance1, b) + + // Tx1 read should get Tx0's value + v = states[1].GetState(addr, key) + b = states[1].GetBalance(addr) + + assert.Equal(t, val1, v) + assert.Equal(t, balance1, b) + + // Tx0 delete + for _, v := range states[0].writeMap { + mvhm.Delete(v.Path, 0) + + states[0].writeMap = nil + } + + // Tx2 read again should get default vals + v = states[2].GetState(addr, key) + b = states[2].GetBalance(addr) + + assert.Equal(t, common.Hash{}, v) + assert.Equal(t, common.Big0, b) +} + +func TestMVHashMapWriteNoConflict(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr := common.HexToAddress("0x01") + key1 := common.HexToHash("0x01") + key2 := common.HexToHash("0x02") + val1 := common.HexToHash("0x01") + balance1 := new(big.Int).SetUint64(uint64(100)) + val2 := common.HexToHash("0x02") + + // Tx0 write + states[0].GetOrNewStateObject(addr) + states[0].FlushMVWriteSet() + + // Tx2 write + states[2].SetState(addr, key2, val2) + states[2].FlushMVWriteSet() + + // Tx1 write + tx1Snapshot := states[1].Snapshot() + states[1].SetState(addr, key1, val1) + states[1].SetBalance(addr, balance1) + states[1].FlushMVWriteSet() + + // Tx1 read + assert.Equal(t, val1, states[1].GetState(addr, key1)) + assert.Equal(t, balance1, states[1].GetBalance(addr)) + // Tx1 should see empty value in key2 + assert.Equal(t, common.Hash{}, states[1].GetState(addr, key2)) + + // Tx2 read + assert.Equal(t, val2, states[2].GetState(addr, key2)) + // Tx2 should see values written by Tx1 + assert.Equal(t, val1, states[2].GetState(addr, key1)) + assert.Equal(t, balance1, states[2].GetBalance(addr)) + + // Tx3 read + assert.Equal(t, val1, states[3].GetState(addr, key1)) + assert.Equal(t, val2, states[3].GetState(addr, key2)) + assert.Equal(t, balance1, states[3].GetBalance(addr)) + + // Tx2 delete + for _, v := range states[2].writeMap { + mvhm.Delete(v.Path, 2) + + states[2].writeMap = nil + } + + assert.Equal(t, val1, states[3].GetState(addr, key1)) + assert.Equal(t, balance1, states[3].GetBalance(addr)) + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2)) + + // Tx1 revert + states[1].RevertToSnapshot(tx1Snapshot) + states[1].FlushMVWriteSet() + + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key1)) + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2)) + assert.Equal(t, common.Big0, states[3].GetBalance(addr)) + + // Tx1 delete + for _, v := range states[1].writeMap { + mvhm.Delete(v.Path, 1) + + states[1].writeMap = nil + } + + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key1)) + assert.Equal(t, common.Hash{}, states[3].GetState(addr, key2)) + assert.Equal(t, common.Big0, states[3].GetBalance(addr)) +} + +func TestApplyMVWriteSet(t *testing.T) { + t.Parallel() + + db := NewDatabase(rawdb.NewMemoryDatabase()) + mvhm := blockstm.MakeMVHashMap() + s, _ := NewWithMVHashmap(common.Hash{}, db, nil, mvhm) + + sClean := s.Copy() + sClean.mvHashmap = nil + + sSingleProcess := sClean.Copy() + + states := []*StateDB{s} + + // Create copies of the original state for each transition + for i := 1; i <= 4; i++ { + sCopy := s.Copy() + sCopy.txIndex = i + states = append(states, sCopy) + } + + addr1 := common.HexToAddress("0x01") + addr2 := common.HexToAddress("0x02") + addr3 := common.HexToAddress("0x03") + key1 := common.HexToHash("0x01") + key2 := common.HexToHash("0x02") + val1 := common.HexToHash("0x01") + balance1 := new(big.Int).SetUint64(uint64(100)) + val2 := common.HexToHash("0x02") + balance2 := new(big.Int).SetUint64(uint64(200)) + code := []byte{1, 2, 3} + + // Tx0 write + states[0].GetOrNewStateObject(addr1) + states[0].SetState(addr1, key1, val1) + states[0].SetBalance(addr1, balance1) + states[0].SetState(addr2, key2, val2) + states[0].GetOrNewStateObject(addr3) + states[0].Finalise(true) + states[0].FlushMVWriteSet() + + sSingleProcess.GetOrNewStateObject(addr1) + sSingleProcess.SetState(addr1, key1, val1) + sSingleProcess.SetBalance(addr1, balance1) + sSingleProcess.SetState(addr2, key2, val2) + sSingleProcess.GetOrNewStateObject(addr3) + + sClean.ApplyMVWriteSet(states[0].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) + + // Tx1 write + states[1].SetState(addr1, key2, val2) + states[1].SetBalance(addr1, balance2) + states[1].SetNonce(addr1, 1) + states[1].Finalise(true) + states[1].FlushMVWriteSet() + + sSingleProcess.SetState(addr1, key2, val2) + sSingleProcess.SetBalance(addr1, balance2) + sSingleProcess.SetNonce(addr1, 1) + + sClean.ApplyMVWriteSet(states[1].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) + + // Tx2 write + states[2].SetState(addr1, key1, val2) + states[2].SetBalance(addr1, balance2) + states[2].SetNonce(addr1, 2) + states[2].Finalise(true) + states[2].FlushMVWriteSet() + + sSingleProcess.SetState(addr1, key1, val2) + sSingleProcess.SetBalance(addr1, balance2) + sSingleProcess.SetNonce(addr1, 2) + + sClean.ApplyMVWriteSet(states[2].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) + + // Tx3 write + states[3].Suicide(addr2) + states[3].SetCode(addr1, code) + states[3].Finalise(true) + states[3].FlushMVWriteSet() + + sSingleProcess.Suicide(addr2) + sSingleProcess.SetCode(addr1, code) + + sClean.ApplyMVWriteSet(states[3].MVWriteList()) + + assert.Equal(t, sSingleProcess.IntermediateRoot(true), sClean.IntermediateRoot(true)) +} + // TestCopyOfCopy tests that modified objects are carried over to the copy, and the copy of the copy. // See https://github.com/ethereum/go-ethereum/pull/15225#issuecomment-380191512 func TestCopyOfCopy(t *testing.T) { diff --git a/core/state_processor.go b/core/state_processor.go index e4c775ec13..a02bfea9ec 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -57,7 +57,7 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen // Process returns the receipts and logs accumulated during the process and // returns the amount of gas that was used in the process. If any of the // transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) { +func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) { var ( receipts types.Receipts usedGas = new(uint64) @@ -75,12 +75,19 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { + if interruptCtx != nil { + select { + case <-interruptCtx.Done(): + return nil, nil, 0, interruptCtx.Err() + default: + } + } msg, err := TransactionToMessage(tx, types.MakeSigner(p.config, header.Number), header.BaseFee) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } statedb.SetTxContext(tx.Hash(), i) - receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, nil) + receipt, err := applyTransaction(msg, p.config, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, interruptCtx) if err != nil { return nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } @@ -89,6 +96,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg } // Fail if Shanghai not enabled and len(withdrawals) is non-zero. withdrawals := block.Withdrawals() + // TODO marcello IsShanghai if len(withdrawals) > 0 && !p.config.IsShanghai(block.Time()) { return nil, nil, 0, fmt.Errorf("withdrawals before shanghai") } @@ -104,12 +112,51 @@ func applyTransaction(msg *Message, config *params.ChainConfig, gp *GasPool, sta txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb) - // Apply the transaction to the current state (included in the env). - result, err := ApplyMessage(evm, msg, gp, interruptCtx) + var result *ExecutionResult + + var err error + + backupMVHashMap := statedb.GetMVHashmap() + + // pause recording read and write + statedb.SetMVHashmap(nil) + + coinbaseBalance := statedb.GetBalance(evm.Context.Coinbase) + + // resume recording read and write + statedb.SetMVHashmap(backupMVHashMap) + + result, err = ApplyMessageNoFeeBurnOrTip(evm, *msg, gp, interruptCtx) if err != nil { return nil, err } + // stop recording read and write + statedb.SetMVHashmap(nil) + + if evm.ChainConfig().IsLondon(blockNumber) { + statedb.AddBalance(result.BurntContractAddress, result.FeeBurnt) + } + + statedb.AddBalance(evm.Context.Coinbase, result.FeeTipped) + output1 := new(big.Int).SetBytes(result.SenderInitBalance.Bytes()) + output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) + + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + AddFeeTransferLog( + statedb, + + msg.From, + evm.Context.Coinbase, + + result.FeeTipped, + result.SenderInitBalance, + coinbaseBalance, + output1.Sub(output1, result.FeeTipped), + output2.Add(output2, result.FeeTipped), + ) + if result.Err == vm.ErrInterrupt { return nil, result.Err } diff --git a/core/state_processor_test.go b/core/state_processor_test.go index 5a2589fb85..0865c32a4b 100644 --- a/core/state_processor_test.go +++ b/core/state_processor_test.go @@ -247,27 +247,32 @@ func TestStateProcessorErrors(t *testing.T) { }, }, } - blockchain, _ = NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + blockchain, _ = NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + parallelBlockchain, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil) ) defer blockchain.Stop() - for i, tt := range []struct { - txs []*types.Transaction - want string - }{ - { // ErrTxTypeNotSupported - txs: []*types.Transaction{ - mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + defer parallelBlockchain.Stop() + + for _, bc := range []*BlockChain{blockchain, parallelBlockchain} { + for i, tt := range []struct { + txs []*types.Transaction + want string + }{ + { // ErrTxTypeNotSupported + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + }, + want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: transaction type not supported", }, - want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: transaction type not supported", - }, - } { - block := GenerateBadBlock(gspec.ToBlock(), ethash.NewFaker(), tt.txs, gspec.Config) - _, err := blockchain.InsertChain(types.Blocks{block}) - if err == nil { - t.Fatal("block imported without errors") - } - if have, want := err.Error(), tt.want; have != want { - t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } { + block := GenerateBadBlock(gspec.ToBlock(), ethash.NewFaker(), tt.txs, gspec.Config) + _, err := bc.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block imported without errors") + } + if have, want := err.Error(), tt.want; have != want { + t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } } } } @@ -286,27 +291,32 @@ func TestStateProcessorErrors(t *testing.T) { }, }, } - blockchain, _ = NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + blockchain, _ = NewBlockChain(db, nil, gspec, nil, ethash.NewFaker(), vm.Config{}, nil, nil, nil) + parallelBlockchain, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil) ) defer blockchain.Stop() - for i, tt := range []struct { - txs []*types.Transaction - want string - }{ - { // ErrSenderNoEOA - txs: []*types.Transaction{ - mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + defer parallelBlockchain.Stop() + + for _, bc := range []*BlockChain{blockchain, parallelBlockchain} { + for i, tt := range []struct { + txs []*types.Transaction + want string + }{ + { // ErrSenderNoEOA + txs: []*types.Transaction{ + mkDynamicTx(0, common.Address{}, params.TxGas-1000, big.NewInt(0), big.NewInt(0)), + }, + want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: sender not an eoa: address 0x71562b71999873DB5b286dF957af199Ec94617F7, codehash: 0x9280914443471259d4570a8661015ae4a5b80186dbc619658fb494bebc3da3d1", }, - want: "could not apply tx 0 [0x88626ac0d53cb65308f2416103c62bb1f18b805573d4f96a3640bbbfff13c14f]: sender not an eoa: address 0x71562b71999873DB5b286dF957af199Ec94617F7, codehash: 0x9280914443471259d4570a8661015ae4a5b80186dbc619658fb494bebc3da3d1", - }, - } { - block := GenerateBadBlock(gspec.ToBlock(), ethash.NewFaker(), tt.txs, gspec.Config) - _, err := blockchain.InsertChain(types.Blocks{block}) - if err == nil { - t.Fatal("block imported without errors") - } - if have, want := err.Error(), tt.want; have != want { - t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } { + block := GenerateBadBlock(gspec.ToBlock(), ethash.NewFaker(), tt.txs, gspec.Config) + _, err := bc.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block imported without errors") + } + if have, want := err.Error(), tt.want; have != want { + t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } } } } @@ -344,36 +354,42 @@ func TestStateProcessorErrors(t *testing.T) { }, }, } - genesis = gspec.MustCommit(db) - blockchain, _ = NewBlockChain(db, nil, gspec, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil, nil, nil) - tooBigInitCode = [params.MaxInitCodeSize + 1]byte{} - smallInitCode = [320]byte{} + genesis = gspec.MustCommit(db) + blockchain, _ = NewBlockChain(db, nil, gspec, nil, beacon.New(ethash.NewFaker()), vm.Config{}, nil, nil, nil) + parallelBlockchain, _ = NewParallelBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil) + tooBigInitCode = [params.MaxInitCodeSize + 1]byte{} + smallInitCode = [320]byte{} ) defer blockchain.Stop() - for i, tt := range []struct { - txs []*types.Transaction - want string - }{ - { // ErrMaxInitCodeSizeExceeded - txs: []*types.Transaction{ - mkDynamicCreationTx(0, 500000, common.Big0, misc.CalcBaseFee(config, genesis.Header()), tooBigInitCode[:]), + defer parallelBlockchain.Stop() + + for _, bc := range []*BlockChain{blockchain, parallelBlockchain} { + + for i, tt := range []struct { + txs []*types.Transaction + want string + }{ + { // ErrMaxInitCodeSizeExceeded + txs: []*types.Transaction{ + mkDynamicCreationTx(0, 500000, common.Big0, misc.CalcBaseFee(config, genesis.Header()), tooBigInitCode[:]), + }, + want: "could not apply tx 0 [0x832b54a6c3359474a9f504b1003b2cc1b6fcaa18e4ef369eb45b5d40dad6378f]: max initcode size exceeded: code size 49153 limit 49152", }, - want: "could not apply tx 0 [0x832b54a6c3359474a9f504b1003b2cc1b6fcaa18e4ef369eb45b5d40dad6378f]: max initcode size exceeded: code size 49153 limit 49152", - }, - { // ErrIntrinsicGas: Not enough gas to cover init code - txs: []*types.Transaction{ - mkDynamicCreationTx(0, 54299, common.Big0, misc.CalcBaseFee(config, genesis.Header()), smallInitCode[:]), + { // ErrIntrinsicGas: Not enough gas to cover init code + txs: []*types.Transaction{ + mkDynamicCreationTx(0, 54299, common.Big0, misc.CalcBaseFee(config, genesis.Header()), smallInitCode[:]), + }, + want: "could not apply tx 0 [0x39b7436cb432d3662a25626474282c5c4c1a213326fd87e4e18a91477bae98b2]: intrinsic gas too low: have 54299, want 54300", }, - want: "could not apply tx 0 [0x39b7436cb432d3662a25626474282c5c4c1a213326fd87e4e18a91477bae98b2]: intrinsic gas too low: have 54299, want 54300", - }, - } { - block := GenerateBadBlock(genesis, beacon.New(ethash.NewFaker()), tt.txs, gspec.Config) - _, err := blockchain.InsertChain(types.Blocks{block}) - if err == nil { - t.Fatal("block imported without errors") - } - if have, want := err.Error(), tt.want; have != want { - t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } { + block := GenerateBadBlock(genesis, beacon.New(ethash.NewFaker()), tt.txs, gspec.Config) + _, err := bc.InsertChain(types.Blocks{block}) + if err == nil { + t.Fatal("block imported without errors") + } + if have, want := err.Error(), tt.want; have != want { + t.Errorf("test %d:\nhave \"%v\"\nwant \"%v\"\n", i, have, want) + } } } } diff --git a/core/state_transition.go b/core/state_transition.go index 43c4422921..a4e3212349 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -35,6 +35,10 @@ type ExecutionResult struct { UsedGas uint64 // Total used gas but include the refunded gas Err error // Any error encountered during the execution(listed in core/vm/errors.go) ReturnData []byte // Returned data from evm(function result or data supplied with revert opcode) + SenderInitBalance *big.Int + FeeBurnt *big.Int + BurntContractAddress common.Address + FeeTipped *big.Int } // Unwrap returns the internal evm error which allows us for further @@ -177,6 +181,13 @@ func ApplyMessage(evm *vm.EVM, msg *Message, gp *GasPool, interruptCtx context.C return NewStateTransition(evm, msg, gp).TransitionDb(interruptCtx) } +func ApplyMessageNoFeeBurnOrTip(evm *vm.EVM, msg Message, gp *GasPool, interruptCtx context.Context) (*ExecutionResult, error) { + st := NewStateTransition(evm, msg, gp) + st.noFeeBurnAndTip = true + + return st.TransitionDb(interruptCtx) +} + // StateTransition represents a state transition. // // == The State Transitioning Model @@ -311,8 +322,10 @@ func (st *StateTransition) preCheck() error { // nil evm execution result. func (st *StateTransition) TransitionDb(interruptCtx context.Context) (*ExecutionResult, error) { input1 := st.state.GetBalance(st.msg.From) - input2 := st.state.GetBalance(st.evm.Context.Coinbase) - + var input2 *big.Int + if !st.noFeeBurnAndTip { + input2 := st.state.GetBalance(st.evm.Context.Coinbase) + } // First check this message satisfies all consensus rules before // applying the message. The rules include these clauses // @@ -404,34 +417,47 @@ func (st *StateTransition) TransitionDb(interruptCtx context.Context) (*Executio amount := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip) + var burnAmount *big.Int + + var burntContractAddress common.Address + if rules.IsLondon { burntContractAddress := common.HexToAddress(st.evm.ChainConfig().Bor.CalculateBurntContract(st.evm.Context.BlockNumber.Uint64())) burnAmount := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) - st.state.AddBalance(burntContractAddress, burnAmount) + if !st.noFeeBurnAndTip { + st.state.AddBalance(burntContractAddress, burnAmount) + } } - st.state.AddBalance(st.evm.Context.Coinbase, amount) - output1 := new(big.Int).SetBytes(input1.Bytes()) - output2 := new(big.Int).SetBytes(input2.Bytes()) + if !st.noFeeBurnAndTip { + st.state.AddBalance(st.evm.Context.Coinbase, amount) + output1 := new(big.Int).SetBytes(input1.Bytes()) + output2 := new(big.Int).SetBytes(input2.Bytes()) - // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 - // add transfer log - AddFeeTransferLog( - st.state, + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + AddFeeTransferLog( + st.state, - msg.From, - st.evm.Context.Coinbase, + msg.From, + st.evm.Context.Coinbase, + + amount, + input1, + input2, + output1.Sub(output1, amount), + output2.Add(output2, amount), + ) + } - amount, - input1, - input2, - output1.Sub(output1, amount), - output2.Add(output2, amount), - ) return &ExecutionResult{ UsedGas: st.gasUsed(), Err: vmerr, ReturnData: ret, + SenderInitBalance: input1, + FeeBurnt: burnAmount, + BurntContractAddress: burntContractAddress, + FeeTipped: amount, }, nil } diff --git a/core/types.go b/core/types.go index 36eb0d1ded..1bb04fbd75 100644 --- a/core/types.go +++ b/core/types.go @@ -17,6 +17,7 @@ package core import ( + "context" "sync/atomic" "github.com/ethereum/go-ethereum/core/state" @@ -49,5 +50,5 @@ type Processor interface { // Process processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. - Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, error) + Process(block *types.Block, statedb *state.StateDB, cfg vm.Config, interruptCtx context.Context) (types.Receipts, []*types.Log, uint64, error) } diff --git a/core/types/block.go b/core/types/block.go index a1a14f5b16..6d9a8d29ed 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -88,6 +88,11 @@ type Header struct { // ExcessDataGas was added by EIP-4844 and is ignored in legacy headers. ExcessDataGas *big.Int `json:"excessDataGas" rlp:"optional"` + // length of TxDependency -> n (n = number of transactions in the block) + // length of TxDependency[i] -> k (k = a whole number) + // k elements in TxDependency[i] -> transaction indexes on which transaction i is dependent on + TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` + /* TODO (MariusVanDerWijden) Add this field once needed // Random was added during the merge and contains the BeaconState randomness @@ -285,6 +290,15 @@ func CopyHeader(h *Header) *Header { cpy.WithdrawalsHash = new(common.Hash) *cpy.WithdrawalsHash = *h.WithdrawalsHash } + + if len(h.TxDependency) > 0 { + cpy.TxDependency = make([][]uint64, len(h.TxDependency)) + + for i, dep := range h.TxDependency { + cpy.TxDependency[i] = make([]uint64, len(dep)) + copy(cpy.TxDependency[i], dep) + } + } return &cpy } @@ -341,6 +355,7 @@ func (b *Block) TxHash() common.Hash { return b.header.TxHash } func (b *Block) ReceiptHash() common.Hash { return b.header.ReceiptHash } func (b *Block) UncleHash() common.Hash { return b.header.UncleHash } func (b *Block) Extra() []byte { return common.CopyBytes(b.header.Extra) } +func (b *Block) TxDependency() [][]uint64 { return b.header.TxDependency } func (b *Block) BaseFee() *big.Int { if b.header.BaseFee == nil { diff --git a/core/types/block_test.go b/core/types/block_test.go index 966015eb01..672e285840 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -68,6 +68,51 @@ func TestBlockEncoding(t *testing.T) { } } +func TestTxDependencyBlockEncoding(t *testing.T) { + t.Parallel() + + blockEnc := common.FromHex("f90268f90201a083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c480c6c20201c20180f861f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1c0") + + var block Block + + if err := rlp.DecodeBytes(blockEnc, &block); err != nil { + t.Fatal("decode error: ", err) + } + + check := func(f string, got, want interface{}) { + if !reflect.DeepEqual(got, want) { + t.Errorf("%s mismatch: got %v, want %v", f, got, want) + } + } + + check("Difficulty", block.Difficulty(), big.NewInt(131072)) + check("GasLimit", block.GasLimit(), uint64(3141592)) + check("GasUsed", block.GasUsed(), uint64(21000)) + check("Coinbase", block.Coinbase(), common.HexToAddress("8888f1f195afa192cfee860698584c030f4c9db1")) + check("MixDigest", block.MixDigest(), common.HexToHash("bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff498")) + check("Root", block.Root(), common.HexToHash("ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017")) + check("Hash", block.Hash(), common.HexToHash("0xc6d8dc8995c0a4374bb9f87bd0dd8c0761e6e026a71edbfed5e961c9e55dbd6a")) + check("Nonce", block.Nonce(), uint64(0xa13a5a8c8f2bb1c4)) + check("Time", block.Time(), uint64(1426516743)) + check("Size", block.Size(), common.StorageSize(len(blockEnc))) + check("TxDependency", block.TxDependency(), [][]uint64{{2, 1}, {1, 0}}) + + tx1 := NewTransaction(0, common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), big.NewInt(10), 50000, big.NewInt(10), nil) + tx1, _ = tx1.WithSignature(HomesteadSigner{}, common.Hex2Bytes("9bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094f8a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b100")) + + check("len(Transactions)", len(block.Transactions()), 1) + check("Transactions[0].Hash", block.Transactions()[0].Hash(), tx1.Hash()) + ourBlockEnc, err := rlp.EncodeToBytes(&block) + + if err != nil { + t.Fatal("encode error: ", err) + } + + if !bytes.Equal(ourBlockEnc, blockEnc) { + t.Errorf("encoded block mismatch:\ngot: %x\nwant: %x", ourBlockEnc, blockEnc) + } +} + func TestEIP1559BlockEncoding(t *testing.T) { blockEnc := common.FromHex("f9030bf901fea083cafc574e1f51ba9dc0568fc617a08ea2429fb384059c972f13b19fa1c8dd55a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347948888f1f195afa192cfee860698584c030f4c9db1a0ef1552a40b7165c3cd773806b9e0c165b75356e0314bf0706f279c729f51e017a05fe50b260da6308036625b850b5d6ced6d0a9f814c0688bc91ffb7b7a3a54b67a0bc37d79753ad738a6dac4921e57392f145d8887476de3f783dfa7edae9283e52b90100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008302000001832fefd8825208845506eb0780a0bd4472abb6659ebe3ee06ee4d7b72a00a9f4d001caca51342001075469aff49888a13a5a8c8f2bb1c4843b9aca00f90106f85f800a82c35094095e7baea6a6c7c4c2dfeb977efac326af552d870a801ba09bea4c4daac7c7c52e093e6a4c35dbbcf8856f1af7b059ba20253e70848d094fa08a8fae537ce25ed8cb5af9adac3f141af69bd515bd2ba031522df09b97dd72b1b8a302f8a0018080843b9aca008301e24194095e7baea6a6c7c4c2dfeb977efac326af552d878080f838f7940000000000000000000000000000000000000001e1a0000000000000000000000000000000000000000000000000000000000000000080a0fe38ca4e44a30002ac54af7cf922a6ac2ba11b7d22f548e8ecb3f51f41cb31b0a06de6a5cbae13c0c856e33acf021b51819636cfc009d39eafb9f606d546e305a8c0") var block Block diff --git a/core/types/gen_header_json.go b/core/types/gen_header_json.go index 5c8b81652d..61b7f128ca 100644 --- a/core/types/gen_header_json.go +++ b/core/types/gen_header_json.go @@ -33,6 +33,7 @@ func (h Header) MarshalJSON() ([]byte, error) { Nonce BlockNonce `json:"nonce"` BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` Hash common.Hash `json:"hash"` } var enc Header @@ -53,6 +54,7 @@ func (h Header) MarshalJSON() ([]byte, error) { enc.Nonce = h.Nonce enc.BaseFee = (*hexutil.Big)(h.BaseFee) enc.WithdrawalsHash = h.WithdrawalsHash + enc.TxDependency = h.TxDependency enc.Hash = h.Hash() return json.Marshal(&enc) } @@ -77,6 +79,7 @@ func (h *Header) UnmarshalJSON(input []byte) error { Nonce *BlockNonce `json:"nonce"` BaseFee *hexutil.Big `json:"baseFeePerGas" rlp:"optional"` WithdrawalsHash *common.Hash `json:"withdrawalsRoot" rlp:"optional"` + TxDependency [][]uint64 `json:"txDependency" rlp:"optional"` } var dec Header if err := json.Unmarshal(input, &dec); err != nil { @@ -145,5 +148,8 @@ func (h *Header) UnmarshalJSON(input []byte) error { if dec.WithdrawalsHash != nil { h.WithdrawalsHash = dec.WithdrawalsHash } + if dec.TxDependency != nil { + h.TxDependency = dec.TxDependency + } return nil } diff --git a/core/types/gen_header_rlp.go b/core/types/gen_header_rlp.go index 7fd2cf8f2d..7d00a78ccc 100644 --- a/core/types/gen_header_rlp.go +++ b/core/types/gen_header_rlp.go @@ -42,7 +42,8 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBytes(obj.Nonce[:]) _tmp1 := obj.BaseFee != nil _tmp2 := obj.WithdrawalsHash != nil - if _tmp1 || _tmp2 { + _tmp3 := len(obj.TxDependency) > 0 + if _tmp1 || _tmp2 || _tmp3 { if obj.BaseFee == nil { w.Write(rlp.EmptyString) } else { @@ -59,6 +60,17 @@ func (obj *Header) EncodeRLP(_w io.Writer) error { w.WriteBytes(obj.WithdrawalsHash[:]) } } + if _tmp3 { + _tmp3 := w.List() + for _, _tmp4 := range obj.TxDependency { + _tmp5 := w.List() + for _, _tmp6 := range _tmp4 { + w.WriteUint64(_tmp6) + } + w.ListEnd(_tmp5) + } + w.ListEnd(_tmp3) + } w.ListEnd(_tmp0) return w.Flush() } diff --git a/core/vm/interpreter.go b/core/vm/interpreter.go index 2e05cee976..0e99230009 100644 --- a/core/vm/interpreter.go +++ b/core/vm/interpreter.go @@ -52,6 +52,10 @@ type Config struct { NoBaseFee bool // Forces the EIP-1559 baseFee to 0 (needed for 0 price calls) EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages ExtraEips []int // Additional EIPS that are to be enabled + + // parallel EVM configs + ParallelEnable bool + ParallelSpeculativeProcesses int } // ScopeContext contains the things that are per-call, such as stack and memory, @@ -265,6 +269,10 @@ func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool, i txHash, _ := GetCurrentTxFromContext(interruptCtx) interruptedTxCache, _ := GetCache(interruptCtx) + if interruptedTxCache == nil { + break + } + // if the tx is already in the cache, it means that it has been interrupted before and we will not interrupt it again found, _ := interruptedTxCache.Cache.ContainsOrAdd(txHash, true) if found { @@ -425,6 +433,11 @@ func (in *EVMInterpreter) RunWithDelay(contract *Contract, input []byte, readOnl case <-interruptCtx.Done(): txHash, _ := GetCurrentTxFromContext(interruptCtx) interruptedTxCache, _ := GetCache(interruptCtx) + + if interruptedTxCache == nil { + break + } + // if the tx is already in the cache, it means that it has been interrupted before and we will not interrupt it again found, _ := interruptedTxCache.Cache.ContainsOrAdd(txHash, true) log.Info("FOUND", "found", found, "txHash", txHash) diff --git a/docs/cli/example_config.toml b/docs/cli/example_config.toml index bffbd77631..f62fcfebdb 100644 --- a/docs/cli/example_config.toml +++ b/docs/cli/example_config.toml @@ -35,9 +35,9 @@ devfakeauthor = false # Run miner without validator set authorization port = 30303 # Network listening port nodiscover = false # Disables the peer discovery mechanism (manual peer addition) nat = "any" # NAT port mapping mechanism (any|none|upnp|pmp|extip:) - netrestrict = "" # Restricts network communication to the given IP networks (CIDR masks) - nodekey = "" # P2P node key file - nodekeyhex = "" # P2P node key as hex + netrestrict = "" # Restricts network communication to the given IP networks (CIDR masks) + nodekey = "" # P2P node key file + nodekeyhex = "" # P2P node key as hex txarrivalwait = "500ms" # Maximum duration to wait before requesting an announced transaction [p2p.discovery] v5disc = false # Enables the experimental RLPx V5 (Topic Discovery) mechanism @@ -73,6 +73,7 @@ devfakeauthor = false # Run miner without validator set authorization gaslimit = 30000000 # Target gas ceiling for mined blocks gasprice = "1000000000" # Minimum gas price for mining a transaction (recommended for mainnet = 30000000000, default suitable for mumbai/devnet) recommit = "2m5s" # The time interval for miner to re-create mining work + commitinterrupt = true # Interrupt the current mining work when time is exceeded and create partial blocks [jsonrpc] ipcdisable = false # Disable the IPC-RPC server @@ -129,7 +130,7 @@ devfakeauthor = false # Run miner without validator set authorization metrics = false # Enable metrics collection and reporting expensive = false # Enable expensive metrics collection and reporting prometheus-addr = "127.0.0.1:7071" # Address for Prometheus Server - opencollector-endpoint = "127.0.0.1:4317" # OpenCollector Endpoint (host:port) + opencollector-endpoint = "" # OpenCollector Endpoint (host:port) [telemetry.influx] influxdb = false # Enable metrics export/push to an external InfluxDB database (v1) endpoint = "" # InfluxDB API endpoint to report metrics to diff --git a/docs/cli/server.md b/docs/cli/server.md index 2f049ec210..d7a9a1296e 100644 --- a/docs/cli/server.md +++ b/docs/cli/server.md @@ -72,6 +72,10 @@ The ```bor server``` command runs the Bor client. - ```dev.period```: Block period to use in developer mode (0 = mine only if transaction pending) (default: 0) +- ```parallelevm.enable```: Enable Block STM (default: true) + +- ```parallelevm.procs```: Number of speculative processes (cores) in Block STM (default: 8) + - ```dev.gaslimit```: Initial block gas limit (default: 11500000) - ```pprof```: Enable the pprof HTTP server (default: false) @@ -216,7 +220,7 @@ The ```bor server``` command runs the Bor client. - ```v5disc```: Enables the experimental RLPx V5 (Topic Discovery) mechanism (default: false) -- ```txarrivalwait```: Maximum duration to wait before requesting an announced transaction (default: 500ms) +- ```txarrivalwait```: Maximum duration to wait for a transaction before explicitly requesting it (defaults to 500ms) (default: 500ms) ### Sealer Options @@ -232,6 +236,8 @@ The ```bor server``` command runs the Bor client. - ```miner.recommit```: The time interval for miner to re-create mining work (default: 2m5s) +- ```miner.interruptcommit```: Interrupt block commit when block creation time is passed (default: true) + ### Telemetry Options - ```metrics```: Enable metrics collection and reporting (default: false) @@ -284,4 +290,4 @@ The ```bor server``` command runs the Bor client. - ```txpool.globalqueue```: Maximum number of non-executable transaction slots for all accounts (default: 32768) -- ```txpool.lifetime```: Maximum amount of time non-executable transaction are queued (default: 3h0m0s) \ No newline at end of file +- ```txpool.lifetime```: Maximum amount of time non-executable transaction are queued (default: 3h0m0s) diff --git a/eth/backend.go b/eth/backend.go index ddadae5433..80cf7f01b2 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -111,7 +111,6 @@ type Ethereum struct { // New creates a new Ethereum object (including the // initialisation of the common Ethereum object) -// TODO fixme marcello c'é qcls che non va qua (CreateConsensusEngine chiamata due volte) func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { // Ensure configuration values are compatible and sane if config.SyncMode == downloader.LightSync { @@ -207,7 +206,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } var ( vmConfig = vm.Config{ - EnablePreimageRecording: config.EnablePreimageRecording, + EnablePreimageRecording: config.EnablePreimageRecording, + ParallelEnable: config.ParallelEVM.Enable, + ParallelSpeculativeProcesses: config.ParallelEVM.SpeculativeProcesses, } cacheConfig = &core.CacheConfig{ TrieCleanLimit: config.TrieCleanCache, @@ -230,7 +231,15 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { checker := whitelist.NewService(10) - ethereum.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, config.Genesis, &overrides, ethereum.engine, vmConfig, ethereum.shouldPreserve, &config.TxLookupLimit, checker) + // check if Parallel EVM is enabled + // if enabled, use parallel state processor + if config.ParallelEVM.Enable { + // TODO marcello fix NewParallelBlockChain + ethereum.blockchain, err = core.NewParallelBlockChain(chainDb, cacheConfig, config.Genesis, ethereum.engine, vmConfig, ethereum.shouldPreserve, &config.TxLookupLimit, checker) + } else { + ethereum.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, config.Genesis, &overrides, ethereum.engine, vmConfig, ethereum.shouldPreserve, &config.TxLookupLimit, checker) + } + if err != nil { return nil, err } diff --git a/eth/downloader/downloader_test.go b/eth/downloader/downloader_test.go index 5d63b75996..1fbd0287bc 100644 --- a/eth/downloader/downloader_test.go +++ b/eth/downloader/downloader_test.go @@ -1608,9 +1608,9 @@ func TestFakedSyncProgress66NoRemoteCheckpoint(t *testing.T) { tester := newTester(t) validate := func(count int) (bool, error) { - // only return the `ErrNoRemoteCheckoint` error for the first call + // only return the `ErrNoRemoteCheckpoint` error for the first call if count == 0 { - return false, whitelist.ErrNoRemoteCheckoint + return false, whitelist.ErrNoRemoteCheckpoint } return true, nil @@ -1626,7 +1626,7 @@ func TestFakedSyncProgress66NoRemoteCheckpoint(t *testing.T) { // Synchronise with the peer and make sure all blocks were retrieved // Should fail in first attempt if err := tester.sync("light", nil, mode); err != nil { - assert.Equal(t, whitelist.ErrNoRemoteCheckoint, err, "failed synchronisation") + assert.Equal(t, whitelist.ErrNoRemoteCheckpoint, err, "failed synchronisation") } // Try syncing again, should succeed diff --git a/eth/downloader/whitelist/service.go b/eth/downloader/whitelist/service.go index a2edcfb797..5d3fe477d3 100644 --- a/eth/downloader/whitelist/service.go +++ b/eth/downloader/whitelist/service.go @@ -31,7 +31,7 @@ func NewService(maxCapacity uint) *Service { var ( ErrCheckpointMismatch = errors.New("checkpoint mismatch") ErrLongFutureChain = errors.New("received future chain of unacceptable length") - ErrNoRemoteCheckoint = errors.New("remote peer doesn't have a checkoint") + ErrNoRemoteCheckpoint = errors.New("remote peer doesn't have a checkpoint") ) // IsValidPeer checks if the chain we're about to receive from a peer is valid or not @@ -55,11 +55,11 @@ func (w *Service) IsValidPeer(remoteHeader *types.Header, fetchHeadersByNumber f // todo: we can extract this as an interface and mock as well or just test IsValidChain in isolation from downloader passing fake fetchHeadersByNumber functions headers, hashes, err := fetchHeadersByNumber(lastCheckpointBlockNum, 1, 0, false) if err != nil { - return false, fmt.Errorf("%w: last checkpoint %d, err %v", ErrNoRemoteCheckoint, lastCheckpointBlockNum, err) + return false, fmt.Errorf("%w: last checkpoint %d, err %v", ErrNoRemoteCheckpoint, lastCheckpointBlockNum, err) } if len(headers) == 0 { - return false, fmt.Errorf("%w: last checkpoint %d", ErrNoRemoteCheckoint, lastCheckpointBlockNum) + return false, fmt.Errorf("%w: last checkpoint %d", ErrNoRemoteCheckpoint, lastCheckpointBlockNum) } reqBlockNum := headers[0].Number.Uint64() diff --git a/eth/downloader/whitelist/service_test.go b/eth/downloader/whitelist/service_test.go index a91148c761..cf0b076cfd 100644 --- a/eth/downloader/whitelist/service_test.go +++ b/eth/downloader/whitelist/service_test.go @@ -64,14 +64,14 @@ func TestIsValidPeer(t *testing.T) { } // case2: false fetchHeadersByNumber function provided, should consider the chain as invalid - // and throw `ErrNoRemoteCheckoint` error + // and throw `ErrNoRemoteCheckpoint` error res, err = s.IsValidPeer(nil, falseFetchHeadersByNumber) if err == nil { t.Fatal("expected error, got nil") } - if !errors.Is(err, ErrNoRemoteCheckoint) { - t.Fatalf("expected error ErrNoRemoteCheckoint, got %v", err) + if !errors.Is(err, ErrNoRemoteCheckpoint) { + t.Fatalf("expected error ErrNoRemoteCheckpoint, got %v", err) } require.Equal(t, res, false, "expected chain to be invalid") diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index 08101e3420..e8a1725a94 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -18,7 +18,7 @@ package ethconfig import ( - "github.com/ethereum/go-ethereum/internal/ethapi" + "math/big" "os" "os/user" "path/filepath" @@ -41,6 +41,7 @@ import ( "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/ethdb" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/miner" "github.com/ethereum/go-ethereum/node" @@ -242,6 +243,9 @@ type Config struct { // Bor logs flag BorLogs bool + // Parallel EVM (Block-STM) related config + ParallelEVM core.ParallelEVMConfig `toml:",omitempty"` + // Develop Fake Author mode to produce blocks without authorisation DevFakeAuthor bool `hcl:"devfakeauthor,optional" toml:"devfakeauthor,optional"` } diff --git a/eth/protocols/eth/dispatcher.go b/eth/protocols/eth/dispatcher.go index 3f81e045ba..0b16060978 100644 --- a/eth/protocols/eth/dispatcher.go +++ b/eth/protocols/eth/dispatcher.go @@ -212,6 +212,7 @@ func (p *Peer) dispatcher() { } // Stop tracking the request delete(pending, cancelOp.id) + requestTracker.Fulfil(p.id, p.version, req.code, cancelOp.id) cancelOp.fail <- nil case resOp := <-p.resDispatch: diff --git a/eth/state_accessor.go b/eth/state_accessor.go index a3d2d70682..55d2ffe914 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -159,7 +159,9 @@ func (eth *Ethereum) StateAtBlock(ctx context.Context, block *types.Block, reexe if current = eth.blockchain.GetBlockByNumber(next); current == nil { return nil, nil, fmt.Errorf("block #%d not found", next) } - _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) + + _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}, nil) + if err != nil { return nil, nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) } diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 9f5f8c54a5..0261b6b04d 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -20,10 +20,14 @@ import ( "bufio" "bytes" "context" + "encoding/hex" "encoding/json" "errors" "fmt" + "io/ioutil" + "math/big" "os" + "path/filepath" "runtime" "sync" "time" @@ -68,6 +72,10 @@ const ( // for tracing. The creation of trace state will be paused if the unused // trace states exceed this limit. maximumPendingTraceStates = 128 + + defaultPath = string(".") + + defaultIOFlag = false ) var defaultBorTraceEnabled = newBoolPtr(false) @@ -78,6 +86,8 @@ var errTxNotFound = errors.New("transaction not found") // historical state for tracing purposes. type StateReleaseFunc func() +var allowIOTracing = false // Change this to true to enable IO tracing for debugging + // Backend interface provides the common API services (that are provided by // both full and light clients) with access to necessary functions. type Backend interface { @@ -204,9 +214,11 @@ type TraceConfig struct { Tracer *string Timeout *string Reexec *uint64 + Path *string + IOFlag *bool // Config specific to given tracer. Note struct logger // config are historically embedded in main object. - TracerConfig json.RawMessage + TracerConfig json.RawMessage BorTraceEnabled *bool BorTx *bool } @@ -222,8 +234,8 @@ type TraceCallConfig struct { // StdTraceConfig holds extra parameters to standard-json trace functions. type StdTraceConfig struct { logger.Config - Reexec *uint64 - TxHash common.Hash + Reexec *uint64 + TxHash common.Hash BorTraceEnabled *bool } @@ -691,7 +703,6 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config return roots, nil } - // StandardTraceBadBlockToFile dumps the structured logs created during the // execution of EVM against a block pulled from the pool of bad ones to the // local file system and returns a list of files to the caller. @@ -734,19 +745,36 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac if config != nil && config.Reexec != nil { reexec = *config.Reexec } + + path := defaultPath + if config != nil && config.Path != nil { + path = *config.Path + } + + ioflag := defaultIOFlag + if allowIOTracing && config != nil && config.IOFlag != nil { + ioflag = *config.IOFlag + } + statedb, release, err := api.backend.StateAtBlock(ctx, parent, reexec, nil, true, false) if err != nil { return nil, err } defer release() + // create and add empty mvHashMap in statedb as StateAtBlock does not have mvHashmap in it. + if ioflag { + statedb.AddEmptyMVHashMap() + } + + // Execute all the transaction contained within the block concurrently var ( txs, stateSyncPresent = api.getAllBlockTransactions(ctx, block) - blockHash = block.Hash() - blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) - signer = types.MakeSigner(api.backend.ChainConfig(), block.Number()) - results = make([]*txTraceResult, len(txs)) - pend sync.WaitGroup + blockHash = block.Hash() + blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + signer = types.MakeSigner(api.backend.ChainConfig(), block.Number()) + results = make([]*txTraceResult, len(txs)) + pend sync.WaitGroup ) threads := runtime.NumCPU() if threads > len(txs) { @@ -790,10 +818,30 @@ func (api *API) traceBlock(ctx context.Context, block *types.Block, config *Trac }() } + var IOdump string + + var RWstruct []state.DumpStruct + + var london bool + + if ioflag { + IOdump = "TransactionIndex, Incarnation, VersionTxIdx, VersionInc, Path, Operation\n" + RWstruct = []state.DumpStruct{} + } // Feed the transactions into the tracers and return var failed error + + if ioflag { + london = api.backend.ChainConfig().IsLondon(block.Number()) + } + txloop: for i, tx := range txs { + if ioflag { + // copy of statedb + statedb = statedb.Copy() + } + // Send the trace task over for execution task := &txTraceTask{statedb: statedb.Copy(), index: i} select { @@ -807,27 +855,88 @@ txloop: msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) statedb.SetTxContext(tx.Hash(), i) vmenv := vm.NewEVM(blockCtx, core.NewEVMTxContext(msg), statedb, api.backend.ChainConfig(), vm.Config{}) - //nolint: nestif - if stateSyncPresent && i == len(txs)-1 { - if *config.BorTraceEnabled { - callmsg := prepareCallMessage(*msg) - if _, err := statefull.ApplyBorMessage(*vmenv, callmsg); err != nil { - failed = err + + // nolint: nestif + if !ioflag { + //nolint: nestif + if stateSyncPresent && i == len(txs)-1 { + if *config.BorTraceEnabled { + callmsg := prepareCallMessage(*msg) + // nolint : contextcheck + if _, err := statefull.ApplyBorMessage(*vmenv, callmsg); err != nil { + failed = err + break txloop + } + } else { break txloop } } else { - break txloop + // nolint : contextcheck + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit), context.Background()); err != nil { + failed = err + break txloop + } + // Finalize the state so any modifications are written to the trie + // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect + statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) } } else { + coinbaseBalance := statedb.GetBalance(blockCtx.Coinbase) // nolint : contextcheck - if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.GasLimit), context.Background()); err != nil { + result, err := core.ApplyMessageNoFeeBurnOrTip(vmenv, *msg, new(core.GasPool).AddGas(msg.GasLimit), context.Background()) + + if err != nil { failed = err - break txloop + break } + + if london { + statedb.AddBalance(result.BurntContractAddress, result.FeeBurnt) + } + + statedb.AddBalance(blockCtx.Coinbase, result.FeeTipped) + output1 := new(big.Int).SetBytes(result.SenderInitBalance.Bytes()) + output2 := new(big.Int).SetBytes(coinbaseBalance.Bytes()) + + // Deprecating transfer log and will be removed in future fork. PLEASE DO NOT USE this transfer log going forward. Parameters won't get updated as expected going forward with EIP1559 + // add transfer log + core.AddFeeTransferLog( + statedb, + + msg.From, + blockCtx.Coinbase, + + result.FeeTipped, + result.SenderInitBalance, + coinbaseBalance, + output1.Sub(output1, result.FeeTipped), + output2.Add(output2, result.FeeTipped), + ) + + // Finalize the state so any modifications are written to the trie + // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect + statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) + statedb.FlushMVWriteSet() + + structRead := statedb.GetReadMapDump() + structWrite := statedb.GetWriteMapDump() + + RWstruct = append(RWstruct, structRead...) + RWstruct = append(RWstruct, structWrite...) } - // Finalize the state so any modifications are written to the trie - // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect - statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) + } + + if ioflag { + for _, val := range RWstruct { + IOdump += fmt.Sprintf("%v , %v, %v , %v, ", val.TxIdx, val.TxInc, val.VerIdx, val.VerInc) + hex.EncodeToString(val.Path) + ", " + val.Op + } + + // make sure that the file exists and write IOdump + err = ioutil.WriteFile(filepath.Join(path, "data.csv"), []byte(fmt.Sprint(IOdump)), 0600) + if err != nil { + return nil, err + } + } close(jobs) @@ -845,7 +954,6 @@ txloop: } } - // standardTraceBlockToFile configures a new tracer which uses standard JSON output, // and traces either a full block or an individual transaction. The return value will // be one filename per transaction traced. @@ -1177,7 +1285,7 @@ func (api *API) traceTx(ctx context.Context, message *core.Message, txctx *Conte } else { // nolint : contextcheck if _, err = core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.GasLimit), context.Background()); err != nil { - return nil, fmt.Errorf("tracing failed: %w", err) + return nil, fmt.Errorf("tracing failed: %w", err) } } return tracer.GetResult() diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index ec6f1dfd72..130ff77fbe 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -460,6 +460,85 @@ func TestTraceBlock(t *testing.T) { } } +func TestIOdump(t *testing.T) { + t.Parallel() + + // Initialize test accounts + accounts := newAccounts(5) + genesis := &core.Genesis{Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + accounts[2].addr: {Balance: big.NewInt(params.Ether)}, + accounts[3].addr: {Balance: big.NewInt(params.Ether)}, + accounts[4].addr: {Balance: big.NewInt(params.Ether)}, + }} + genBlocks := 1 + signer := types.HomesteadSigner{} + api := NewAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1], account[1] to account[2], account[2] to account[3], account[3] to account[4], account[4] to account[0] + // value: 1000 wei + // fee: 0 wei + + for j := 0; j < 5; j++ { + tx, _ := types.SignTx(types.NewTransaction(uint64(i), accounts[(j+1)%5].addr, big.NewInt(1000), params.TxGas, b.BaseFee(), nil), signer, accounts[j].key) + b.AddTx(tx) + } + })) + + allowIOTracing = true + + ioflag := new(bool) + + *ioflag = true + + var testSuite = []struct { + blockNumber rpc.BlockNumber + config *TraceConfig + want string + expectErr error + }{ + // Trace head block + { + config: &TraceConfig{ + IOFlag: ioflag, + }, + blockNumber: rpc.BlockNumber(genBlocks), + want: `[{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}},{"result":{"gas":21000,"failed":false,"returnValue":"","structLogs":[]}}]`, + }, + } + + for i, tc := range testSuite { + result, err := api.TraceBlockByNumber(context.Background(), tc.blockNumber, tc.config) + if tc.expectErr != nil { + if err == nil { + t.Errorf("test %d, want error %v", i, tc.expectErr) + continue + } + + if !reflect.DeepEqual(err, tc.expectErr) { + t.Errorf("test %d: error mismatch, want %v, get %v", i, tc.expectErr, err) + } + + continue + } + + if err != nil { + t.Errorf("test %d, want no error, have %v", i, err) + continue + } + + have, err := json.Marshal(result) + if err != nil { + t.Errorf("Error in Marshal: %v", err) + } + + want := tc.want + if string(have) != want { + t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, string(have), want) + } + } +} + func TestTracingWithOverrides(t *testing.T) { t.Parallel() // Initialize test accounts diff --git a/go.mod b/go.mod index 968b96afc5..593fd031c8 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,8 @@ require ( github.com/hashicorp/go-bexpr v0.1.10 github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d github.com/hashicorp/hcl/v2 v2.10.1 + github.com/heimdalr/dag v1.2.1 + github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e github.com/holiman/bloomfilter/v2 v2.0.3 github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c github.com/huin/goupnp v1.0.3 @@ -180,6 +182,10 @@ require ( github.com/cosmos/go-bip39 v0.0.0-20180618194314-52158e4697b8 // indirect github.com/cosmos/ledger-cosmos-go v0.10.3 // indirect github.com/cosmos/ledger-go v0.9.2 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/deepmap/oapi-codegen v1.8.2 // indirect + github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect + github.com/emirpasic/gods v1.18.1 github.com/etcd-io/bbolt v1.3.3 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect diff --git a/graphql/graphql.go b/graphql/graphql.go index 45a1f7a2aa..f2cba8f381 100644 --- a/graphql/graphql.go +++ b/graphql/graphql.go @@ -1067,7 +1067,8 @@ func (c *CallResult) Status() Long { func (b *Block) Call(ctx context.Context, args struct { Data ethapi.TransactionArgs }) (*CallResult, error) { - result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap()) + // TODO marcello check args number + result, err := ethapi.DoCall(ctx, b.r.backend, args.Data, *b.numberOrHash, nil, nil, b.r.backend.RPCEVMTimeout(), b.r.backend.RPCGasCap()) if err != nil { return nil, err } @@ -1131,7 +1132,7 @@ func (p *Pending) Call(ctx context.Context, args struct { Data ethapi.TransactionArgs }) (*CallResult, error) { pendingBlockNr := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) - result, err := ethapi.DoCall(ctx, p.r.backend, args.Data, pendingBlockNr, nil, p.r.backend.RPCEVMTimeout(), p.r.backend.RPCGasCap()) + result, err := ethapi.DoCall(ctx, p.r.backend, args.Data, pendingBlockNr, nil, nil, p.r.backend.RPCEVMTimeout(), p.r.backend.RPCGasCap()) if err != nil { return nil, err } diff --git a/internal/cli/server/chains/mumbai.go b/internal/cli/server/chains/mumbai.go index b66b46a86f..f9dd7fd989 100644 --- a/internal/cli/server/chains/mumbai.go +++ b/internal/cli/server/chains/mumbai.go @@ -28,8 +28,13 @@ var mumbaiTestnet = &Chain{ BerlinBlock: big.NewInt(13996000), LondonBlock: big.NewInt(22640000), Bor: ¶ms.BorConfig{ - JaipurBlock: big.NewInt(22770000), - DelhiBlock: big.NewInt(29638656), + JaipurBlock: big.NewInt(22770000), + DelhiBlock: big.NewInt(29638656), + ParallelUniverseBlock: big.NewInt(0), + IndoreBlock: big.NewInt(36877056), + StateSyncConfirmationDelay: map[string]uint64{ + "36877056": 128, + }, Period: map[string]uint64{ "0": 2, "25275000": 5, diff --git a/internal/cli/server/chains/test_files/chain_legacy_test.json b/internal/cli/server/chains/test_files/chain_legacy_test.json index b97c8b1f8e..b67dfd30d5 100644 --- a/internal/cli/server/chains/test_files/chain_legacy_test.json +++ b/internal/cli/server/chains/test_files/chain_legacy_test.json @@ -48,7 +48,12 @@ "22640000": "0x70bcA57F4579f58670aB2d18Ef16e02C17553C38" }, "jaipurBlock": 22770000, - "delhiBlock": 29638656 + "delhiBlock": 29638656, + "parallelUniverseBlock": 0, + "indoreBlock": 36877056, + "stateSyncConfirmationDelay": { + "36877056": 128 + } } }, "nonce": "0x0", diff --git a/internal/cli/server/chains/test_files/chain_test.json b/internal/cli/server/chains/test_files/chain_test.json index 7907adfcfa..5b1f9ada40 100644 --- a/internal/cli/server/chains/test_files/chain_test.json +++ b/internal/cli/server/chains/test_files/chain_test.json @@ -50,7 +50,12 @@ "22640000":"0x70bcA57F4579f58670aB2d18Ef16e02C17553C38" }, "jaipurBlock":22770000, - "delhiBlock": 29638656 + "delhiBlock": 29638656, + "parallelUniverseBlock": 0, + "indoreBlock": 36877056, + "stateSyncConfirmationDelay": { + "36877056": 128 + } } }, "nonce":"0x0", diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 50a3a41743..4e8553ec62 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -127,6 +127,9 @@ type Config struct { // Developer has the developer mode related settings Developer *DeveloperConfig `hcl:"developer,block" toml:"developer,block"` + // ParallelEVM has the parallel evm related settings + ParallelEVM *ParallelEVMConfig `hcl:"parallelevm,block" toml:"parallelevm,block"` + // Develop Fake Author mode to produce blocks without authorisation DevFakeAuthor bool `hcl:"devfakeauthor,optional" toml:"devfakeauthor,optional"` @@ -309,9 +312,10 @@ type SealerConfig struct { GasPriceRaw string `hcl:"gasprice,optional" toml:"gasprice,optional"` // The time interval for miner to re-create mining work. - Recommit time.Duration `hcl:"-,optional" toml:"-"` - RecommitRaw string `hcl:"recommit,optional" toml:"recommit,optional"` - CommitInterruptFlag bool `hcl:"commitinterrupt,optional" toml:"commitinterrupt,optional"` + Recommit time.Duration `hcl:"-,optional" toml:"-"` + RecommitRaw string `hcl:"recommit,optional" toml:"recommit,optional"` + + CommitInterruptFlag bool `hcl:"commitinterrupt,optional" toml:"commitinterrupt,optional"` } type JsonRPCConfig struct { @@ -569,6 +573,12 @@ type DeveloperConfig struct { GasLimit uint64 `hcl:"gaslimit,optional" toml:"gaslimit,optional"` } +type ParallelEVMConfig struct { + Enable bool `hcl:"enable,optional" toml:"enable,optional"` + + SpeculativeProcesses int `hcl:"procs,optional" toml:"procs,optional"` +} + func DefaultConfig() *Config { return &Config{ Chain: "mainnet", @@ -695,7 +705,7 @@ func DefaultConfig() *Config { Enabled: false, Expensive: false, PrometheusAddr: "127.0.0.1:7071", - OpenCollectorEndpoint: "127.0.0.1:4317", + OpenCollectorEndpoint: "", InfluxDB: &InfluxDBConfig{ V1Enabled: false, Endpoint: "", @@ -748,6 +758,10 @@ func DefaultConfig() *Config { BlockProfileRate: 0, // CPUProfile: "", }, + ParallelEVM: &ParallelEVMConfig{ + Enable: true, + SpeculativeProcesses: 8, + }, } } @@ -1131,6 +1145,8 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* n.BorLogs = c.BorLogs n.DatabaseHandles = dbHandles + n.ParallelEVM.Enable = c.ParallelEVM.Enable + n.ParallelEVM.SpeculativeProcesses = c.ParallelEVM.SpeculativeProcesses n.RPCReturnDataLimit = c.RPCReturnDataLimit if c.Ancient != "" { diff --git a/internal/cli/server/flags.go b/internal/cli/server/flags.go index 0c38da3823..470a377012 100644 --- a/internal/cli/server/flags.go +++ b/internal/cli/server/flags.go @@ -904,6 +904,20 @@ func (c *Command) Flags() *flagset.Flagset { Value: &c.cliConfig.Developer.Period, Default: c.cliConfig.Developer.Period, }) + + // parallelevm + f.BoolFlag(&flagset.BoolFlag{ + Name: "parallelevm.enable", + Usage: "Enable Block STM", + Value: &c.cliConfig.ParallelEVM.Enable, + Default: c.cliConfig.ParallelEVM.Enable, + }) + f.IntFlag(&flagset.IntFlag{ + Name: "parallelevm.procs", + Usage: "Number of speculative processes (cores) in Block STM", + Value: &c.cliConfig.ParallelEVM.SpeculativeProcesses, + Default: c.cliConfig.ParallelEVM.SpeculativeProcesses, + }) f.Uint64Flag(&flagset.Uint64Flag{ Name: "dev.gaslimit", Usage: "Initial block gas limit", diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index b60ed933fd..265ed53bb2 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1053,16 +1053,40 @@ func (diff *BlockOverrides) Apply(blockCtx *vm.BlockContext) { } } -func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { +func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, state *state.StateDB, overrides *StateOverride, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now()) - state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) - if state == nil || err != nil { - return nil, err + var ( + header *types.Header + err error + ) + + // Fetch the state and header from blockNumberOrHash if it's coming from normal eth_call path. + if state == nil { + state, header, err = b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + } else { + // Fetch the header from the given blockNumberOrHash. Note that this path is only taken + // when we're doing a call from bor consensus to fetch data from genesis contracts. It's + // necessary to fetch header using header hash as we might be experiencing a reorg and there + // can be multiple headers with same number. + header, err = b.HeaderByHash(ctx, *blockNrOrHash.BlockHash) + if header == nil || err != nil { + log.Warn("Error fetching header on CallWithState", "err", err) + return nil, err + } } + if err := overrides.Apply(state); err != nil { return nil, err } + + return doCallWithState(ctx, b, args, header, state, timeout, globalGasCap) +} + +func doCallWithState(ctx context.Context, b Backend, args TransactionArgs, header *types.Header, state *state.StateDB, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { // Setup context so it may be cancelled the call has completed // or, in case of unmetered gas, setup a context with a timeout. var cancel context.CancelFunc @@ -1146,7 +1170,20 @@ func (e *revertError) ErrorData() interface{} { // Note, this function doesn't make and changes in the state/blockchain and is // useful to execute and retrieve values. func (s *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) { - result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap()) + return s.CallWithState(ctx, args, blockNrOrHash, nil, overrides) +} + +// CallWithState executes the given transaction on the given state for +// the given block number. Note that as it does an EVM call, fields in +// the underlying state will change. Make sure to handle it outside of +// this function (ideally by sending a copy of state). +// +// Additionally, the caller can specify a batch of contract for fields overriding. +// +// Note, this function doesn't make and changes in the state/blockchain and is +// useful to execute and retrieve values. +func (s *BlockChainAPI) CallWithState(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, state *state.StateDB, overrides *StateOverride) (hexutil.Bytes, error) { + result, err := DoCall(ctx, s.b, args, blockNrOrHash, state, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap()) if err != nil { return nil, err } @@ -1236,7 +1273,7 @@ func DoEstimateGas(ctx context.Context, b Backend, args TransactionArgs, blockNr executable := func(gas uint64) (bool, *core.ExecutionResult, error) { args.Gas = (*hexutil.Uint64)(&gas) - result, err := DoCall(ctx, b, args, blockNrOrHash, nil, 0, gasCap) + result, err := DoCall(ctx, b, args, blockNrOrHash, nil, nil, 0, gasCap) if err != nil { if errors.Is(err, core.ErrIntrinsicGas) { return true, nil, nil // Special case, raise gas limit diff --git a/log/logger.go b/log/logger.go index fcc1623bab..cd076b2a1e 100644 --- a/log/logger.go +++ b/log/logger.go @@ -18,13 +18,13 @@ const skipLevel = 2 type Lvl int const ( - LvlDiscard Lvl = -1 - LvlCrit Lvl = iota + LvlCrit Lvl = iota LvlError LvlWarn LvlInfo LvlDebug LvlTrace + LvlDiscard Lvl = -1 ) // AlignedString returns a 5-character string containing the name of a Lvl. diff --git a/metrics/prometheus/collector.go b/metrics/prometheus/collector.go index 2bd9bf22cc..20e6ae9054 100644 --- a/metrics/prometheus/collector.go +++ b/metrics/prometheus/collector.go @@ -30,6 +30,7 @@ var ( typeCounterTpl = "# TYPE %s counter\n" typeSummaryTpl = "# TYPE %s summary\n" keyValueTpl = "%s %v\n\n" + keyCounterTpl = "%s %v\n" keyQuantileTagValueTpl = "%s {quantile=\"%s\"} %v\n" ) @@ -65,11 +66,16 @@ func (c *collector) addGaugeFloat64(name string, m metrics.GaugeFloat64) { func (c *collector) addHistogram(name string, m metrics.Histogram) { pv := []float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999} ps := m.Percentiles(pv) - c.writeSummaryCounter(name, m.Count()) + + var sum float64 = 0 c.buff.WriteString(fmt.Sprintf(typeSummaryTpl, mutateKey(name))) for i := range pv { c.writeSummaryPercentile(name, strconv.FormatFloat(pv[i], 'f', -1, 64), ps[i]) + sum += ps[i] } + + c.writeSummarySum(name, fmt.Sprintf("%f", sum)) + c.writeSummaryCounter(name, len(ps)) c.buff.WriteRune('\n') } @@ -80,7 +86,7 @@ func (c *collector) addMeter(name string, m metrics.Meter) { func (c *collector) addTimer(name string, m metrics.Timer) { pv := []float64{0.5, 0.75, 0.95, 0.99, 0.999, 0.9999} ps := m.Percentiles(pv) - c.writeSummaryCounter(name, m.Count()) + c.writeCounter(name, m.Count()) c.buff.WriteString(fmt.Sprintf(typeSummaryTpl, mutateKey(name))) for i := range pv { c.writeSummaryPercentile(name, strconv.FormatFloat(pv[i], 'f', -1, 64), ps[i]) @@ -94,11 +100,19 @@ func (c *collector) addResettingTimer(name string, m metrics.ResettingTimer) { } ps := m.Percentiles([]float64{50, 95, 99}) val := m.Values() - c.writeSummaryCounter(name, len(val)) c.buff.WriteString(fmt.Sprintf(typeSummaryTpl, mutateKey(name))) c.writeSummaryPercentile(name, "0.50", ps[0]) c.writeSummaryPercentile(name, "0.95", ps[1]) c.writeSummaryPercentile(name, "0.99", ps[2]) + + var sum int64 = 0 + + for _, v := range val { + sum += v + } + + c.writeSummarySum(name, fmt.Sprintf("%d", sum)) + c.writeSummaryCounter(name, len(val)) c.buff.WriteRune('\n') } @@ -108,17 +122,27 @@ func (c *collector) writeGaugeCounter(name string, value interface{}) { c.buff.WriteString(fmt.Sprintf(keyValueTpl, name, value)) } -func (c *collector) writeSummaryCounter(name string, value interface{}) { +func (c *collector) writeCounter(name string, value interface{}) { name = mutateKey(name + "_count") c.buff.WriteString(fmt.Sprintf(typeCounterTpl, name)) c.buff.WriteString(fmt.Sprintf(keyValueTpl, name, value)) } +func (c *collector) writeSummaryCounter(name string, value interface{}) { + name = mutateKey(name + "_count") + c.buff.WriteString(fmt.Sprintf(keyCounterTpl, name, value)) +} + func (c *collector) writeSummaryPercentile(name, p string, value interface{}) { name = mutateKey(name) c.buff.WriteString(fmt.Sprintf(keyQuantileTagValueTpl, name, p, value)) } +func (c *collector) writeSummarySum(name string, value string) { + name = mutateKey(name + "_sum") + c.buff.WriteString(fmt.Sprintf(keyCounterTpl, name, value)) +} + func mutateKey(key string) string { return strings.ReplaceAll(key, "/", "_") } diff --git a/metrics/prometheus/collector_test.go b/metrics/prometheus/collector_test.go index ff87c8e765..2e5075d0f1 100644 --- a/metrics/prometheus/collector_test.go +++ b/metrics/prometheus/collector_test.go @@ -74,9 +74,6 @@ test_gauge 23456 # TYPE test_gauge_float64 gauge test_gauge_float64 34567.89 -# TYPE test_histogram_count counter -test_histogram_count 0 - # TYPE test_histogram summary test_histogram {quantile="0.5"} 0 test_histogram {quantile="0.75"} 0 @@ -84,6 +81,8 @@ test_histogram {quantile="0.95"} 0 test_histogram {quantile="0.99"} 0 test_histogram {quantile="0.999"} 0 test_histogram {quantile="0.9999"} 0 +test_histogram_sum 0.000000 +test_histogram_count 6 # TYPE test_meter gauge test_meter 9999999 @@ -99,15 +98,17 @@ test_timer {quantile="0.99"} 1.2e+08 test_timer {quantile="0.999"} 1.2e+08 test_timer {quantile="0.9999"} 1.2e+08 -# TYPE test_resetting_timer_count counter -test_resetting_timer_count 6 - # TYPE test_resetting_timer summary test_resetting_timer {quantile="0.50"} 12000000 test_resetting_timer {quantile="0.95"} 120000000 test_resetting_timer {quantile="0.99"} 120000000 +test_resetting_timer_sum 180000000 +test_resetting_timer_count 6 ` + + c.addResettingTimer("test/empty_resetting_timer", emptyResettingTimer) + exp := c.buff.String() if exp != expectedOutput { t.Log("Expected Output:\n", expectedOutput) diff --git a/miner/fake_miner.go b/miner/fake_miner.go index 1d623aed52..b5b90a1e4b 100644 --- a/miner/fake_miner.go +++ b/miner/fake_miner.go @@ -224,7 +224,7 @@ var ( // Test accounts testBankKey, _ = crypto.GenerateKey() TestBankAddress = crypto.PubkeyToAddress(testBankKey.PublicKey) - testBankFunds = big.NewInt(8000000000000000000) + testBankFunds = big.NewInt(9000000000000000000) testUserKey, _ = crypto.GenerateKey() testUserAddress = crypto.PubkeyToAddress(testUserKey.PublicKey) diff --git a/miner/test_backend.go b/miner/test_backend.go index 3654986c1f..a0867165e3 100644 --- a/miner/test_backend.go +++ b/miner/test_backend.go @@ -204,7 +204,7 @@ func (b *testWorkerBackend) newRandomTxWithNonce(creation bool, nonce uint64) *t return tx } -// newRandomTxWithGas creates a new transactionto deploy a storage smart contract. +// newRandomTxWithGas creates a new transaction to deploy a storage smart contract. func (b *testWorkerBackend) newStorageCreateContractTx() (*types.Transaction, common.Address) { var tx *types.Transaction @@ -427,28 +427,13 @@ func (w *worker) mainLoopWithDelay(ctx context.Context, delay uint, opcodeDelay txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, cmath.FromBig(w.current.header.BaseFee)) tcount := w.current.tcount - var interruptCtx = context.Background() - stopFn := func() {} - defer func() { - stopFn() - }() - - if w.interruptCommitFlag { - block := w.chain.GetBlockByHash(w.chain.CurrentBlock().Hash()) - interruptCtx, stopFn = getInterruptTimer(ctx, w.current, block) - // nolint : staticcheck - interruptCtx = vm.PutCache(interruptCtx, w.interruptedTxCache) - } - - w.commitTransactionsWithDelay(w.current, txset, nil, interruptCtx) + w.commitTransactions(w.current, txset, nil, context.Background()) // Only update the snapshot if any new transactions were added // to the pending block if tcount != w.current.tcount { w.updateSnapshot(w.current) } - - stopFn() } else { // Special case, if the consensus engine is 0 period clique(dev mode), // submit sealing work here since all empty submission will be rejected @@ -502,7 +487,7 @@ func (w *worker) commitWorkWithDelay(ctx context.Context, interrupt *int32, noem return } - // nolint : contextcheck + //nolint:contextcheck var interruptCtx = context.Background() stopFn := func() {} @@ -813,7 +798,6 @@ mainloop: continue } // Start executing the transaction - // TODO marcello check how "Prepare" has been replaced env.state.SetTxContext(tx.Hash(), env.tcount) var start time.Time diff --git a/miner/worker.go b/miner/worker.go index 200d54938a..f5bb3b9362 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -43,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/bor" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -988,6 +989,53 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP } var coalescedLogs []*types.Log + var depsMVReadList [][]blockstm.ReadDescriptor + + var depsMVFullWriteList [][]blockstm.WriteDescriptor + + var mvReadMapList []map[blockstm.Key]blockstm.ReadDescriptor + + var deps map[int]map[int]bool + + chDeps := make(chan blockstm.TxDep) + + var count int + + var depsWg sync.WaitGroup + + var EnableMVHashMap bool + + if w.chainConfig.Bor.IsParallelUniverse(env.header.Number) { + EnableMVHashMap = true + } else { + EnableMVHashMap = false + } + + // create and add empty mvHashMap in statedb + if EnableMVHashMap { + depsMVReadList = [][]blockstm.ReadDescriptor{} + + depsMVFullWriteList = [][]blockstm.WriteDescriptor{} + + mvReadMapList = []map[blockstm.Key]blockstm.ReadDescriptor{} + + deps = map[int]map[int]bool{} + + chDeps = make(chan blockstm.TxDep) + + count = 0 + + depsWg.Add(1) + + go func(chDeps chan blockstm.TxDep) { + for t := range chDeps { + deps = blockstm.UpdateDeps(deps, t) + } + + depsWg.Done() + }(chDeps) + } + initialGasLimit := env.gasPool.Gas() initialTxs := txs.GetTxs() @@ -1007,6 +1055,10 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP mainloop: for { if interruptCtx != nil { + if EnableMVHashMap { + env.state.AddEmptyMVHashMap() + } + // case of interrupting by timeout select { case <-interruptCtx.Done(): @@ -1069,6 +1121,22 @@ mainloop: // Everything ok, collect the logs and shift in the next transaction from the same account coalescedLogs = append(coalescedLogs, logs...) env.tcount++ + + if EnableMVHashMap { + depsMVReadList = append(depsMVReadList, env.state.MVReadList()) + depsMVFullWriteList = append(depsMVFullWriteList, env.state.MVFullWriteList()) + mvReadMapList = append(mvReadMapList, env.state.MVReadMap()) + + temp := blockstm.TxDep{ + Index: env.tcount - 1, + ReadList: depsMVReadList[count], + FullWriteList: depsMVFullWriteList, + } + + chDeps <- temp + count++ + } + txs.Shift() log.OnDebug(func(lg log.Logging) { @@ -1081,6 +1149,50 @@ mainloop: log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) txs.Pop() } + + if EnableMVHashMap { + env.state.ClearReadMap() + env.state.ClearWriteMap() + } + } + + // nolint:nestif + if EnableMVHashMap { + close(chDeps) + depsWg.Wait() + + if len(mvReadMapList) > 0 { + tempDeps := make([][]uint64, len(mvReadMapList)) + + for j := range deps[0] { + tempDeps[0] = append(tempDeps[0], uint64(j)) + } + + delayFlag := true + + for i := 1; i <= len(mvReadMapList)-1; i++ { + reads := mvReadMapList[i-1] + + _, ok1 := reads[blockstm.NewSubpathKey(env.coinbase, state.BalancePath)] + _, ok2 := reads[blockstm.NewSubpathKey(common.HexToAddress(w.chainConfig.Bor.CalculateBurntContract(env.header.Number.Uint64())), state.BalancePath)] + + if ok1 || ok2 { + delayFlag = false + } + + for j := range deps[i] { + tempDeps[i] = append(tempDeps[i], uint64(j)) + } + } + + if delayFlag { + env.header.TxDependency = tempDeps + } else { + env.header.TxDependency = nil + } + } else { + env.header.TxDependency = nil + } } if !w.isRunning() && len(coalescedLogs) > 0 { // We don't push the pendingLogsEvent while we are sealing. The reason is that @@ -1493,7 +1605,7 @@ func (w *worker) commitWork(ctx context.Context, interrupt *atomic.Int32, noempt return } - // nolint : contextcheck + // nolint:contextcheck var interruptCtx = context.Background() stopFn := func() {} diff --git a/miner/worker_test.go b/miner/worker_test.go index dbce0d01c1..51e7cef4c2 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -889,11 +889,6 @@ func BenchmarkBorMining(b *testing.B) { chain, _ := core.NewBlockChain(rawdb.NewMemoryDatabase(), nil, back.Genesis, nil, engine, vm.Config{}, nil, nil, nil) defer chain.Stop() - // Ignore empty commit here for less noise. - w.skipSealHook = func(task *task) bool { - return len(task.receipts) == 0 - } - // fulfill tx pool const ( totalGas = testGas + params.TxGas @@ -950,3 +945,126 @@ func BenchmarkBorMining(b *testing.B) { } } } + +// uses core.NewParallelBlockChain to use the dependencies present in the block header +// params.BorUnittestChainConfig contains the ParallelUniverseBlock ad big.NewInt(5), so the first 4 blocks will not have metadata. +// nolint: gocognit +func BenchmarkBorMiningBlockSTMMetadata(b *testing.B) { + chainConfig := params.BorUnittestChainConfig + + ctrl := gomock.NewController(b) + defer ctrl.Finish() + + ethAPIMock := api.NewMockCaller(ctrl) + ethAPIMock.EXPECT().Call(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + + spanner := bor.NewMockSpanner(ctrl) + spanner.EXPECT().GetCurrentValidatorsByHash(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*valset.Validator{ + { + ID: 0, + Address: TestBankAddress, + VotingPower: 100, + ProposerPriority: 0, + }, + }, nil).AnyTimes() + + heimdallClientMock := mocks.NewMockIHeimdallClient(ctrl) + heimdallClientMock.EXPECT().Close().Times(1) + + contractMock := bor.NewMockGenesisContract(ctrl) + + db, _, _ := NewDBForFakes(b) + + engine := NewFakeBor(b, db, chainConfig, ethAPIMock, spanner, heimdallClientMock, contractMock) + defer engine.Close() + + chainConfig.LondonBlock = big.NewInt(0) + + w, back, _ := NewTestWorker(b, chainConfig, engine, db, 0, 0, 0, 0) + defer w.close() + + // This test chain imports the mined blocks. + db2 := rawdb.NewMemoryDatabase() + back.Genesis.MustCommit(db2) + + chain, _ := core.NewParallelBlockChain(db2, nil, back.chain.Config(), engine, vm.Config{ParallelEnable: true, ParallelSpeculativeProcesses: 8}, nil, nil, nil) + defer chain.Stop() + + // Ignore empty commit here for less noise. + w.skipSealHook = func(task *task) bool { + return len(task.receipts) == 0 + } + + // fulfill tx pool + const ( + totalGas = testGas + params.TxGas + totalBlocks = 10 + ) + + var err error + + txInBlock := int(back.Genesis.GasLimit/totalGas) + 1 + + // a bit risky + for i := 0; i < 2*totalBlocks*txInBlock; i++ { + err = back.txPool.AddLocal(back.newRandomTx(true)) + if err != nil { + b.Fatal("while adding a local transaction", err) + } + + err = back.txPool.AddLocal(back.newRandomTx(false)) + if err != nil { + b.Fatal("while adding a remote transaction", err) + } + } + + // Wait for mined blocks. + sub := w.mux.Subscribe(core.NewMinedBlockEvent{}) + defer sub.Unsubscribe() + + b.ResetTimer() + + prev := uint64(time.Now().Unix()) + + // Start mining! + w.start() + + blockPeriod, ok := back.Genesis.Config.Bor.Period["0"] + if !ok { + blockPeriod = 1 + } + + for i := 0; i < totalBlocks; i++ { + select { + case ev := <-sub.Chan(): + block := ev.Data.(core.NewMinedBlockEvent).Block + + if _, err := chain.InsertChain([]*types.Block{block}); err != nil { + b.Fatalf("failed to insert new mined block %d: %v", block.NumberU64(), err) + } + + // check for dependencies for block number > 4 + if block.NumberU64() <= 4 { + if block.TxDependency() != nil { + b.Fatalf("dependency not nil") + } + } else { + deps := block.TxDependency() + if len(deps[0]) != 0 { + b.Fatalf("wrong dependency") + } + for i := 1; i < block.Transactions().Len(); i++ { + if deps[i][0] != uint64(i-1) || len(deps[i]) != 1 { + b.Fatalf("wrong dependency") + } + } + } + + b.Log("block", block.NumberU64(), "time", block.Time()-prev, "txs", block.Transactions().Len(), "gasUsed", block.GasUsed(), "gasLimit", block.GasLimit()) + + prev = block.Time() + case <-time.After(time.Duration(blockPeriod) * time.Second): + b.Fatalf("timeout") + } + } +} diff --git a/packaging/templates/mainnet-v1/archive/config.toml b/packaging/templates/mainnet-v1/archive/config.toml index b2b6d769bf..869dae3c58 100644 --- a/packaging/templates/mainnet-v1/archive/config.toml +++ b/packaging/templates/mainnet-v1/archive/config.toml @@ -66,6 +66,7 @@ gcmode = "archive" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/packaging/templates/mainnet-v1/sentry/sentry/bor/config.toml b/packaging/templates/mainnet-v1/sentry/sentry/bor/config.toml index f2482e0cc6..7413bc63f2 100644 --- a/packaging/templates/mainnet-v1/sentry/sentry/bor/config.toml +++ b/packaging/templates/mainnet-v1/sentry/sentry/bor/config.toml @@ -66,6 +66,7 @@ syncmode = "full" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/packaging/templates/mainnet-v1/sentry/validator/bor/config.toml b/packaging/templates/mainnet-v1/sentry/validator/bor/config.toml index 08a113b08b..1b21818adc 100644 --- a/packaging/templates/mainnet-v1/sentry/validator/bor/config.toml +++ b/packaging/templates/mainnet-v1/sentry/validator/bor/config.toml @@ -68,6 +68,7 @@ syncmode = "full" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/packaging/templates/mainnet-v1/without-sentry/bor/config.toml b/packaging/templates/mainnet-v1/without-sentry/bor/config.toml index 03c34dbea6..ffbaf984ca 100644 --- a/packaging/templates/mainnet-v1/without-sentry/bor/config.toml +++ b/packaging/templates/mainnet-v1/without-sentry/bor/config.toml @@ -68,6 +68,7 @@ syncmode = "full" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/packaging/templates/package_scripts/control b/packaging/templates/package_scripts/control index 1e517b7c76..8e50843583 100644 --- a/packaging/templates/package_scripts/control +++ b/packaging/templates/package_scripts/control @@ -1,5 +1,5 @@ Source: bor -Version: 0.3.9-beta +Version: 0.3.9-stable Section: develop Priority: standard Maintainer: Polygon diff --git a/packaging/templates/package_scripts/control.arm64 b/packaging/templates/package_scripts/control.arm64 index a4d2998e31..64682b7b31 100644 --- a/packaging/templates/package_scripts/control.arm64 +++ b/packaging/templates/package_scripts/control.arm64 @@ -1,5 +1,5 @@ Source: bor -Version: 0.3.9-beta +Version: 0.3.9-stable Section: develop Priority: standard Maintainer: Polygon diff --git a/packaging/templates/package_scripts/control.profile.amd64 b/packaging/templates/package_scripts/control.profile.amd64 index a53dec12a6..66dc5f4f30 100644 --- a/packaging/templates/package_scripts/control.profile.amd64 +++ b/packaging/templates/package_scripts/control.profile.amd64 @@ -1,5 +1,5 @@ Source: bor-profile -Version: 0.3.9-beta +Version: 0.3.9-stable Section: develop Priority: standard Maintainer: Polygon diff --git a/packaging/templates/package_scripts/control.profile.arm64 b/packaging/templates/package_scripts/control.profile.arm64 index b279a50500..5c9a9ca472 100644 --- a/packaging/templates/package_scripts/control.profile.arm64 +++ b/packaging/templates/package_scripts/control.profile.arm64 @@ -1,5 +1,5 @@ Source: bor-profile -Version: 0.3.9-beta +Version: 0.3.9-stable Section: develop Priority: standard Maintainer: Polygon diff --git a/packaging/templates/package_scripts/control.validator b/packaging/templates/package_scripts/control.validator index 8d26132a9a..dbe6ad6c63 100644 --- a/packaging/templates/package_scripts/control.validator +++ b/packaging/templates/package_scripts/control.validator @@ -1,5 +1,5 @@ Source: bor-profile -Version: 0.3.9-beta +Version: 0.3.9-stable Section: develop Priority: standard Maintainer: Polygon diff --git a/packaging/templates/package_scripts/control.validator.arm64 b/packaging/templates/package_scripts/control.validator.arm64 index 2b923088a6..90da8544e4 100644 --- a/packaging/templates/package_scripts/control.validator.arm64 +++ b/packaging/templates/package_scripts/control.validator.arm64 @@ -1,5 +1,5 @@ Source: bor-profile -Version: 0.3.9-beta +Version: 0.3.9-stable Section: develop Priority: standard Maintainer: Polygon diff --git a/packaging/templates/testnet-v4/archive/config.toml b/packaging/templates/testnet-v4/archive/config.toml index 5831169eff..6b372e9b60 100644 --- a/packaging/templates/testnet-v4/archive/config.toml +++ b/packaging/templates/testnet-v4/archive/config.toml @@ -66,6 +66,7 @@ gcmode = "archive" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/packaging/templates/testnet-v4/sentry/sentry/bor/config.toml b/packaging/templates/testnet-v4/sentry/sentry/bor/config.toml index c4ca1fa915..cf81d9a9d6 100644 --- a/packaging/templates/testnet-v4/sentry/sentry/bor/config.toml +++ b/packaging/templates/testnet-v4/sentry/sentry/bor/config.toml @@ -66,6 +66,7 @@ syncmode = "full" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/packaging/templates/testnet-v4/sentry/validator/bor/config.toml b/packaging/templates/testnet-v4/sentry/validator/bor/config.toml index b39e0827a5..57e3ed286c 100644 --- a/packaging/templates/testnet-v4/sentry/validator/bor/config.toml +++ b/packaging/templates/testnet-v4/sentry/validator/bor/config.toml @@ -68,6 +68,7 @@ syncmode = "full" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/packaging/templates/testnet-v4/without-sentry/bor/config.toml b/packaging/templates/testnet-v4/without-sentry/bor/config.toml index eb34a264aa..1f0dabff5d 100644 --- a/packaging/templates/testnet-v4/without-sentry/bor/config.toml +++ b/packaging/templates/testnet-v4/without-sentry/bor/config.toml @@ -68,6 +68,7 @@ syncmode = "full" # etherbase = "" # extradata = "" # recommit = "2m5s" + # commitinterrupt = true [jsonrpc] ipcpath = "/var/lib/bor/bor.ipc" diff --git a/params/config.go b/params/config.go index f432ec6bbb..6ea1a6c41d 100644 --- a/params/config.go +++ b/params/config.go @@ -279,6 +279,7 @@ var ( BerlinBlock: big.NewInt(0), LondonBlock: big.NewInt(0), Bor: &BorConfig{ + ParallelUniverseBlock: big.NewInt(5), Period: map[string]uint64{ "0": 1, }, @@ -316,8 +317,13 @@ var ( BerlinBlock: big.NewInt(13996000), LondonBlock: big.NewInt(22640000), Bor: &BorConfig{ - JaipurBlock: big.NewInt(22770000), - DelhiBlock: big.NewInt(29638656), + JaipurBlock: big.NewInt(22770000), + DelhiBlock: big.NewInt(29638656), + ParallelUniverseBlock: big.NewInt(0), + IndoreBlock: big.NewInt(36877056), + StateSyncConfirmationDelay: map[string]uint64{ + "36877056": 128, + }, Period: map[string]uint64{ "0": 2, "25275000": 5, @@ -369,8 +375,9 @@ var ( BerlinBlock: big.NewInt(14750000), LondonBlock: big.NewInt(23850000), Bor: &BorConfig{ - JaipurBlock: big.NewInt(23850000), - DelhiBlock: big.NewInt(38189056), + JaipurBlock: big.NewInt(23850000), + DelhiBlock: big.NewInt(38189056), + ParallelUniverseBlock: big.NewInt(0), Period: map[string]uint64{ "0": 2, }, @@ -659,17 +666,20 @@ func (c *CliqueConfig) String() string { // BorConfig is the consensus engine configs for Matic bor based sealing. type BorConfig struct { - Period map[string]uint64 `json:"period"` // Number of seconds between blocks to enforce - ProducerDelay map[string]uint64 `json:"producerDelay"` // Number of seconds delay between two producer interval - Sprint map[string]uint64 `json:"sprint"` // Epoch length to proposer - BackupMultiplier map[string]uint64 `json:"backupMultiplier"` // Backup multiplier to determine the wiggle time - ValidatorContract string `json:"validatorContract"` // Validator set contract - StateReceiverContract string `json:"stateReceiverContract"` // State receiver contract - OverrideStateSyncRecords map[string]int `json:"overrideStateSyncRecords"` // override state records count - BlockAlloc map[string]interface{} `json:"blockAlloc"` - BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork - JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) - DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) + Period map[string]uint64 `json:"period"` // Number of seconds between blocks to enforce + ProducerDelay map[string]uint64 `json:"producerDelay"` // Number of seconds delay between two producer interval + Sprint map[string]uint64 `json:"sprint"` // Epoch length to proposer + BackupMultiplier map[string]uint64 `json:"backupMultiplier"` // Backup multiplier to determine the wiggle time + ValidatorContract string `json:"validatorContract"` // Validator set contract + StateReceiverContract string `json:"stateReceiverContract"` // State receiver contract + OverrideStateSyncRecords map[string]int `json:"overrideStateSyncRecords"` // override state records count + BlockAlloc map[string]interface{} `json:"blockAlloc"` + BurntContract map[string]string `json:"burntContract"` // governance contract where the token will be sent to and burnt in london fork + JaipurBlock *big.Int `json:"jaipurBlock"` // Jaipur switch block (nil = no fork, 0 = already on jaipur) + DelhiBlock *big.Int `json:"delhiBlock"` // Delhi switch block (nil = no fork, 0 = already on delhi) + ParallelUniverseBlock *big.Int `json:"parallelUniverseBlock"` // TODO: update all occurrence, change name and finalize number (hardfork for block-stm related changes) + IndoreBlock *big.Int `json:"indoreBlock"` // Indore switch block (nil = no fork, 0 = already on indore) + StateSyncConfirmationDelay map[string]uint64 `json:"stateSyncConfirmationDelay"` // StateSync Confirmation Delay, in seconds, to calculate `to` } // String implements the stringer interface, returning the consensus engine details. @@ -678,11 +688,11 @@ func (b *BorConfig) String() string { } func (c *BorConfig) CalculateProducerDelay(number uint64) uint64 { - return c.calculateSprintSizeHelper(c.ProducerDelay, number) + return borKeyValueConfigHelper(c.ProducerDelay, number) } func (c *BorConfig) CalculateSprint(number uint64) uint64 { - return c.calculateSprintSizeHelper(c.Sprint, number) + return borKeyValueConfigHelper(c.Sprint, number) } func (c *BorConfig) CalculateBackupMultiplier(number uint64) uint64 { @@ -701,6 +711,23 @@ func (c *BorConfig) IsDelhi(number *big.Int) bool { return isBlockForked(c.DelhiBlock, number) } +func (c *BorConfig) IsIndore(number *big.Int) bool { + return isForked(c.IndoreBlock, number) +} + +func (c *BorConfig) CalculateStateSyncDelay(number uint64) uint64 { + return borKeyValueConfigHelper(c.StateSyncConfirmationDelay, number) +} + +// TODO: modify this function once the block number is finalized +func (c *BorConfig) IsParallelUniverse(number *big.Int) bool { + if c.ParallelUniverseBlock == big.NewInt(0) { + return false + } + + return isForked(c.ParallelUniverseBlock, number) +} + func (c *BorConfig) IsSprintStart(number uint64) bool { return number%c.CalculateSprint(number) == 0 } @@ -725,7 +752,7 @@ func (c *BorConfig) calculateBorConfigHelper(field map[string]uint64, number uin return field[keys[len(keys)-1]] } -func (c *BorConfig) calculateSprintSizeHelper(field map[string]uint64, number uint64) uint64 { +func borKeyValueConfigHelper(field map[string]uint64, number uint64) uint64 { keys := make([]string, 0, len(field)) for k := range field { keys = append(keys, k) diff --git a/params/version.go b/params/version.go index beccb09af5..7209254a62 100644 --- a/params/version.go +++ b/params/version.go @@ -21,10 +21,10 @@ import ( ) const ( - VersionMajor = 0 // Major version component of the current release - VersionMinor = 3 // Minor version component of the current release - VersionPatch = 9 // Patch version component of the current release - VersionMeta = "beta" // Version metadata to append to the version string + VersionMajor = 0 // Major version component of the current release + VersionMinor = 3 // Minor version component of the current release + VersionPatch = 9 // Patch version component of the current release + VersionMeta = "stable" // Version metadata to append to the version string ) var GitCommit string @@ -43,7 +43,7 @@ var VersionWithMeta = func() string { return v }() -// VersionWithMetaCommitDetails holds the textual version string including the metadata and Git Details. +// VersionWithCommitDetails holds the textual version string including the metadata and Git Details. var VersionWithMetaCommitDetails = func() string { v := Version if VersionMeta != "" { diff --git a/scripts/updateVersion.sh b/scripts/updateVersion.sh new file mode 100755 index 0000000000..ab8f3aedfa --- /dev/null +++ b/scripts/updateVersion.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +echo "The version is of form - VersionMajor.VersionMinor.VersionPatch-VersionMeta" +echo "Let's take 0.3.4-beta as an example. Here:" +echo "* VersionMajor is - 0" +echo "* VersionMinor is - 3" +echo "* VersionPatch is - 4" +echo "* VersionMeta is - beta" +echo "" +echo "Now, enter the new version step-by-step below:" + +version="" + +# VersionMajor +read -p "* VersionMajor: " VersionMajor +if [ -z "$VersionMajor" ] +then + echo "VersionMajor cannot be NULL" + exit -1 +fi +version+=$VersionMajor + +# VersionMinor +read -p "* VersionMinor: " VersionMinor +if [ -z "$VersionMinor" ] +then + echo "VersionMinor cannot be NULL" + exit -1 +fi +version+="."$VersionMinor + +# VersionPatch +read -p "* VersionPatch: " VersionPatch +if [ -z "$VersionPatch" ] +then + echo "VersionPatch cannot be NULL" + exit -1 +fi +version+="."$VersionPatch + +# VersionMeta (optional) +read -p "* VersionMeta (optional, press enter if not needed): " VersionMeta +if [[ ! -z "$VersionMeta" ]] +then + version+="-"$VersionMeta +fi + +echo "" +echo "New version is: $version" + +# update version in all the 6 templates +replace="Version: "$version +fileArray=( + "${DIR}/../packaging/templates/package_scripts/control" + "${DIR}/../packaging/templates/package_scripts/control.arm64" + "${DIR}/../packaging/templates/package_scripts/control.profile.amd64" + "${DIR}/../packaging/templates/package_scripts/control.profile.arm64" + "${DIR}/../packaging/templates/package_scripts/control.validator" + "${DIR}/../packaging/templates/package_scripts/control.validator.arm64" +) +for file in ${fileArray[@]}; do + # get the line starting with `Version` in the control file and store it in the $temp variable + temp=$(grep "^Version.*" $file) + sed -i '' "s%$temp%$replace%" $file +done + +# update version in ../params/version.go +versionFile="${DIR}/../params/version.go" +sed -i '' "s% = .*// Major% = $VersionMajor // Major%g" $versionFile +sed -i '' "s% = .*// Minor% = $VersionMinor // Minor%g" $versionFile +sed -i '' "s% = .*// Patch% = $VersionPatch // Patch%g" $versionFile +sed -i '' "s% = .*// Version metadata% = \"$VersionMeta\" // Version metadata%g" $versionFile +gofmt -w $versionFile + +echo "" +echo "Updating Version Done" + +exit 0 diff --git a/tests/bor/bor_test.go b/tests/bor/bor_test.go index 5469bbccd0..24c579caad 100644 --- a/tests/bor/bor_test.go +++ b/tests/bor/bor_test.go @@ -544,7 +544,7 @@ func TestFetchStateSyncEvents_2(t *testing.T) { insertNewBlock(t, chain, block) } - lastStateID, _ := _bor.GenesisContractsClient.LastStateId(sprintSize) + lastStateID, _ := _bor.GenesisContractsClient.LastStateId(nil, sprintSize, block.Hash()) // state 6 was not written require.Equal(t, uint64(4), lastStateID.Uint64()) @@ -573,7 +573,7 @@ func TestFetchStateSyncEvents_2(t *testing.T) { insertNewBlock(t, chain, block) } - lastStateID, _ = _bor.GenesisContractsClient.LastStateId(spanSize) + lastStateID, _ = _bor.GenesisContractsClient.LastStateId(nil, spanSize, block.Hash()) require.Equal(t, uint64(6), lastStateID.Uint64()) }