diff --git a/.github/workflows/ci-pr.yaml b/.github/workflows/ci-pr.yaml index 7e2069955f..c593851295 100644 --- a/.github/workflows/ci-pr.yaml +++ b/.github/workflows/ci-pr.yaml @@ -40,26 +40,6 @@ jobs: path: go/src/github.com/harmony-one/harmony persist-credentials: false - - &checkout-mcl - name: Checkout mcl - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 - with: - repository: harmony-one/mcl - fetch-depth: 1 - ref: master - path: go/src/github.com/harmony-one/mcl - persist-credentials: false - - - &checkout-bls - name: Checkout bls - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 - with: - repository: harmony-one/bls - fetch-depth: 1 - ref: master - path: go/src/github.com/harmony-one/bls - persist-credentials: false - - &setup-go name: Set up Go uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 #v6.5.0 @@ -90,15 +70,7 @@ jobs: strategy: *arch-matrix steps: - *checkout-harmony - - *checkout-mcl - - *checkout-bls - *setup-go - - name: Build mcl - run: make -j4 - working-directory: go/src/github.com/harmony-one/mcl - - name: Build bls - run: make BLS_SWAP_G=1 -j4 - working-directory: go/src/github.com/harmony-one/bls - name: Install tools run: | go install golang.org/x/tools/cmd/goimports@v0.30.0 @@ -163,7 +135,6 @@ jobs: strategy: *arch-matrix steps: - *checkout-harmony - - *checkout-bls - *download-harmony-binaries - *prepare-harmony-binaries - *checkout-harmony-test diff --git a/.github/workflows/ci-release.yaml b/.github/workflows/ci-release.yaml index 9dbaed2a0b..fad3a6e8ec 100644 --- a/.github/workflows/ci-release.yaml +++ b/.github/workflows/ci-release.yaml @@ -83,24 +83,6 @@ jobs: - ubuntu-24.04-arm steps: - - name: Checkout mcl - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 - with: - repository: harmony-one/mcl - path: mcl - ref: master - fetch-depth: 1 - persist-credentials: false - - - name: Checkout bls - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 - with: - repository: harmony-one/bls - path: bls - ref: master - fetch-depth: 1 - persist-credentials: false - - name: Checkout harmony core code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 #v7.0.0 with: diff --git a/Dockerfile b/Dockerfile index 90ce25f97b..35a6b890d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,11 +9,6 @@ ENV GOPATH=/root/go ENV GO111MODULE=on ENV HMY_PATH=${GOPATH}/src/github.com/harmony-one ENV OPENSSL_DIR=/usr/lib/ssl -ENV MCL_DIR=${HMY_PATH}/mcl -ENV BLS_DIR=${HMY_PATH}/bls -ENV CGO_CFLAGS="-I${BLS_DIR}/include -I${MCL_DIR}/include" -ENV CGO_LDFLAGS="-L${BLS_DIR}/lib" -ENV LD_LIBRARY_PATH=${BLS_DIR}/lib:${MCL_DIR}/lib ENV GIMME_GO_VERSION=${GOLANG_VERSION} ENV PATH="/root/bin:${PATH}" @@ -31,14 +26,8 @@ RUN eval "$(~/bin/gimme ${GIMME_GO_VERSION})" RUN git clone https://github.com/harmony-one/harmony.git ${HMY_PATH}/harmony -RUN git clone https://github.com/harmony-one/bls.git ${HMY_PATH}/bls - -RUN git clone https://github.com/harmony-one/mcl.git ${HMY_PATH}/mcl - RUN git clone https://github.com/harmony-one/go-sdk.git ${HMY_PATH}/go-sdk -RUN cd ${HMY_PATH}/bls && make -j8 BLS_SWAP_G=1 - RUN touch /root/.bash_profile && \ gimme ${GIMME_GO_VERSION} >> /root/.bash_profile && \ echo "GIMME_GO_VERSION='${GIMME_GO_VERSION}'" >> /root/.bash_profile && \ diff --git a/Makefile b/Makefile index 844f6c5099..36d0f99625 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,3 @@ -TOP:=$(realpath ..) -export CGO_CFLAGS:=-I$(TOP)/bls/include -I$(TOP)/mcl/include -I/opt/homebrew/opt/openssl@1.1/include -export CGO_LDFLAGS:=-L$(TOP)/bls/lib -L/opt/homebrew/opt/openssl@1.1/lib -export LD_LIBRARY_PATH:=$(TOP)/bls/lib:$(TOP)/mcl/lib:/opt/homebrew/opt/openssl@1.1/lib:/opt/homebrew/opt/gmp/lib/:/opt/homebrew/opt/openssl@1.1/lib -export LIBRARY_PATH:=$(LD_LIBRARY_PATH) -export DYLD_FALLBACK_LIBRARY_PATH:=$(LD_LIBRARY_PATH) export GO111MODULE:=on PKGNAME=harmony VERSION?=$(shell git tag -l --sort=-v:refname | head -n 1 | tr -d v) @@ -70,8 +64,7 @@ help: @echo "protofiles - generate Go code from protobuf files" libs: - make -C $(TOP)/mcl -j8 - make -C $(TOP)/bls BLS_SWAP_G=1 -j8 + go mod download github.com/herumi/bls-eth-go-binary exe: bash ./scripts/go_executable_build.sh -S @@ -144,11 +137,8 @@ clean: rm -f coverage.txt distclean: clean - make -C $(TOP)/mcl clean - make -C $(TOP)/bls clean go-get: - source ./scripts/setup_bls_build_flags.sh go get -v ./... test: @@ -170,8 +160,6 @@ test-rosetta-attach: bash ./test/rosetta.sh attach linux_static: - make -C $(TOP)/mcl -j8 - make -C $(TOP)/bls minimised_static BLS_SWAP_G=1 -j8 bash ./scripts/go_executable_build.sh -s linux_static_quick: @@ -271,4 +259,4 @@ debug-delete-log: docker-go-test: - docker run --rm -it -v "$PWD":/go/src/github.com/harmony-one/harmony frozen621/harmony-test bash -c 'make go-test' \ No newline at end of file + docker run --rm -it -v "$PWD":/go/src/github.com/harmony-one/harmony frozen621/harmony-test bash -c 'make go-test' diff --git a/README.md b/README.md index ba69a146f5..df73df32ba 100644 --- a/README.md +++ b/README.md @@ -54,38 +54,15 @@ On macOS, make sure you have the **Xcode Command Line Tools** installed. This in xcode-select --install ``` -## Setting Up MCL & BLS Libraries on macOS +## BLS dependency -The Harmony project depends on the MCL (multi-curve library) and BLS (Boneh-Lynn-Shacham) cryptographic libraries. These need to be installed and configured before building the project. +Harmony uses Herumi's official Go package with prebuilt static BLS libraries. +No separate BLS or MCL checkout is required: -### Clone and Build MCL & BLS Repositories -First, clone the MCL and BLS repositories: ```bash -git clone https://github.com/harmony-one/mcl.git -git clone https://github.com/harmony-one/bls.git -``` - -### Update `.zshrc` for MCL and BLS Paths - -To ensure the libraries are correctly located when building the project, you need to add the MCL and BLS library paths to your `.zshrc` file. - -Add the following lines to your `.zshrc` (or `.bash_profile` for bash users): -```bash -# MCL & BLS paths for Harmony -export MCL_PATH=$GOPATH/src/github.com/harmony-one/mcl -export BLS_PATH=$GOPATH/src/github.com/harmony-one/bls - -# Add library paths for MCL and BLS -export CGO_CFLAGS="-I$MCL_PATH/include -I$BLS_PATH/include -I/opt/homebrew/opt/openssl@1.1/include" -export CGO_LDFLAGS="-L$MCL_PATH/lib -L$BLS_PATH/lib -L/opt/homebrew/opt/openssl@1.1/lib" -export LD_LIBRARY_PATH=$MCL_PATH/lib:$BLS_PATH/lib:/opt/homebrew/opt/openssl@1.1/lib -export LIBRARY_PATH=$LD_LIBRARY_PATH -export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH -``` - -Then, apply the changes by running: -```bash -source ~/.zshrc +git clone https://github.com/harmony-one/harmony.git +cd harmony +go mod download ``` ## Dev Environment @@ -102,10 +79,8 @@ cd $(go env GOPATH)/src/github.com/harmony-one ``` > If you get 'unknown command' or something along those lines, make sure to install [golang](https://golang.org/doc/install) first. -2. Clone this repo & dependent repos. +2. Clone this repo. ```bash -git clone https://github.com/harmony-one/mcl.git -git clone https://github.com/harmony-one/bls.git git clone https://github.com/harmony-one/harmony.git cd harmony ``` @@ -151,17 +126,7 @@ Learn more about docker [here](https://docker-curriculum.com/). The `make` command should automatically build the Harmony binary & all dependent libs. -However, if you wish to bypass the Makefile, first export the build flags: -```bash -export CGO_CFLAGS="-I$GOPATH/src/github.com/harmony-one/bls/include -I$GOPATH/src/github.com/harmony-one/mcl/include -I/opt/homebrew/opt/openssl@1.1/include" -export CGO_LDFLAGS="-L$GOPATH/src/github.com/harmony-one/bls/lib -L/opt/homebrew/opt/openssl@1.1/lib" -export LD_LIBRARY_PATH=$GOPATH/src/github.com/harmony-one/bls/lib:$GOPATH/src/github.com/harmony-one/mcl/lib:/opt/homebrew/opt/openssl@1.1/lib -export LIBRARY_PATH=$LD_LIBRARY_PATH -export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH -export GO111MODULE=on -``` - -Then you can build all executables with the following command: +You can build all executables directly with: ```bash bash ./scripts/go_executable_build.sh -S ``` @@ -276,4 +241,3 @@ See [`CONTRIBUTING`](CONTRIBUTING.md) for details. - Integration with WASM - Fast state synchronization - Auditable privacy asset using ZK proof - diff --git a/Test.Dockerfile b/Test.Dockerfile index ad0a974f3b..4a0863c510 100644 --- a/Test.Dockerfile +++ b/Test.Dockerfile @@ -8,15 +8,11 @@ RUN apt-get update && \ WORKDIR /go/src/github.com/harmony-one -RUN git clone https://github.com/harmony-one/mcl.git && \ - git clone https://github.com/harmony-one/bls.git - RUN echo "Cloning branch: ${ENV}" && \ git clone -b ${ENV} https://github.com/harmony-one/harmony.git && \ cd harmony && \ - go mod tidy && \ - make deps + go mod download WORKDIR /go/src/github.com/harmony-one/harmony -CMD ["make", "go-test"] \ No newline at end of file +CMD ["make", "go-test"] diff --git a/accounts/abi/abi.go b/accounts/abi/abi.go index 9950e13725..34895f04e1 100644 --- a/accounts/abi/abi.go +++ b/accounts/abi/abi.go @@ -261,6 +261,19 @@ func (abi *ABI) HasReceive() bool { // revertSelector is a special function selector for revert reason unpacking. var revertSelector = crypto.Keccak256([]byte("Error(string)"))[:4] +// PackRevert ABI-encodes a revert reason using the standard Error(string) selector. +func PackRevert(reason string) ([]byte, error) { + typ, err := NewType("string", "", nil) + if err != nil { + return nil, err + } + packed, err := (Arguments{{Type: typ}}).Pack(reason) + if err != nil { + return nil, err + } + return append(append([]byte(nil), revertSelector...), packed...), nil +} + // UnpackRevert resolves the abi-encoded revert reason. According to the solidity // spec https://solidity.readthedocs.io/en/latest/control-structures.html#revert, // the provided revert reason is abi-encoded as if it were a call to a function diff --git a/cmd/harmony/main.go b/cmd/harmony/main.go index 791bf21816..ff65c133e5 100644 --- a/cmd/harmony/main.go +++ b/cmd/harmony/main.go @@ -16,7 +16,6 @@ import ( ethCommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" - "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/api/service" "github.com/harmony-one/harmony/api/service/crosslink_sending" "github.com/harmony-one/harmony/api/service/pprof" @@ -28,6 +27,7 @@ import ( "github.com/harmony-one/harmony/consensus" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core" + bls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/chain" "github.com/harmony-one/harmony/internal/cli" "github.com/harmony-one/harmony/internal/common" diff --git a/consensus/beacon_header_slash_test.go b/consensus/beacon_header_slash_test.go index 5399db5b5d..33e2063d6f 100644 --- a/consensus/beacon_header_slash_test.go +++ b/consensus/beacon_header_slash_test.go @@ -6,7 +6,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/common/denominations" @@ -18,6 +17,7 @@ import ( coretypes "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" hmybls "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" chain2 "github.com/harmony-one/harmony/internal/chain" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" diff --git a/consensus/checks.go b/consensus/checks.go index 739c19dd6c..d927653bf4 100644 --- a/consensus/checks.go +++ b/consensus/checks.go @@ -4,10 +4,10 @@ import ( "bytes" "encoding/binary" - libbls "github.com/harmony-one/bls/ffi/go/bls" msg_pb "github.com/harmony-one/harmony/api/proto/message" "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/crypto/bls" + libbls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" "github.com/pkg/errors" protobuf "google.golang.org/protobuf/proto" diff --git a/consensus/consensus.go b/consensus/consensus.go index 311c01967e..251f5acdc1 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -7,13 +7,13 @@ import ( "time" "github.com/harmony-one/abool" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/consensus/engine" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/crypto/bls" bls_cosi "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/registry" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/multibls" diff --git a/consensus/consensus_service.go b/consensus/consensus_service.go index 0cc54f8829..cb1ed1b16e 100644 --- a/consensus/consensus_service.go +++ b/consensus/consensus_service.go @@ -6,7 +6,6 @@ import ( "time" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/api/proto" msg_pb "github.com/harmony-one/harmony/api/proto/message" consensus_engine "github.com/harmony-one/harmony/consensus/engine" @@ -16,6 +15,7 @@ import ( "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/crypto/bls" bls_cosi "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" "github.com/harmony-one/harmony/internal/chain" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" diff --git a/consensus/consensus_v2.go b/consensus/consensus_v2.go index 728a964687..7305bede5b 100644 --- a/consensus/consensus_v2.go +++ b/consensus/consensus_v2.go @@ -9,7 +9,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" - bls2 "github.com/harmony-one/bls/ffi/go/bls" msg_pb "github.com/harmony-one/harmony/api/proto/message" proto_node "github.com/harmony-one/harmony/api/proto/node" "github.com/harmony-one/harmony/block" @@ -18,6 +17,7 @@ import ( "github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/crypto/bls" + bls2 "github.com/harmony-one/harmony/crypto/bls/core" vrf_bls "github.com/harmony-one/harmony/crypto/vrf/bls" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" "github.com/harmony-one/harmony/numeric" diff --git a/consensus/construct.go b/consensus/construct.go index dab4026918..f2a39f20a9 100644 --- a/consensus/construct.go +++ b/consensus/construct.go @@ -4,11 +4,11 @@ import ( "bytes" "errors" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/api/proto" msg_pb "github.com/harmony-one/harmony/api/proto/message" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" protobuf "google.golang.org/protobuf/proto" ) diff --git a/consensus/construct_test.go b/consensus/construct_test.go index 96bb21390c..f012479d09 100644 --- a/consensus/construct_test.go +++ b/consensus/construct_test.go @@ -5,10 +5,10 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" msg_pb "github.com/harmony-one/harmony/api/proto/message" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/registry" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/multibls" diff --git a/consensus/crosslink_cache_invalid_shard_reward_test.go b/consensus/crosslink_cache_invalid_shard_reward_test.go index ea38137f04..768c7a48aa 100644 --- a/consensus/crosslink_cache_invalid_shard_reward_test.go +++ b/consensus/crosslink_cache_invalid_shard_reward_test.go @@ -8,7 +8,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/consensus/quorum" @@ -19,6 +18,7 @@ import ( coretypes "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" hmybls "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" chain2 "github.com/harmony-one/harmony/internal/chain" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" diff --git a/consensus/double_sign.go b/consensus/double_sign.go index 6709d540a0..30cb0bd596 100644 --- a/consensus/double_sign.go +++ b/consensus/double_sign.go @@ -5,9 +5,9 @@ import ( "sort" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/staking/slash" ) diff --git a/consensus/fbft_log.go b/consensus/fbft_log.go index 60c4d248e1..da8c407a40 100644 --- a/consensus/fbft_log.go +++ b/consensus/fbft_log.go @@ -8,7 +8,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/pkg/errors" "github.com/rs/zerolog" diff --git a/consensus/leader.go b/consensus/leader.go index af7e39a0d7..9e901e70c6 100644 --- a/consensus/leader.go +++ b/consensus/leader.go @@ -9,10 +9,10 @@ import ( nodeconfig "github.com/harmony-one/harmony/internal/configs/node" "github.com/ethereum/go-ethereum/rlp" - bls_core "github.com/harmony-one/bls/ffi/go/bls" msg_pb "github.com/harmony-one/harmony/api/proto/message" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core/types" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/p2p" ) diff --git a/consensus/quorum/one-node-one-vote.go b/consensus/quorum/one-node-one-vote.go index 4962dfc86e..2662b0360d 100644 --- a/consensus/quorum/one-node-one-vote.go +++ b/consensus/quorum/one-node-one-vote.go @@ -6,8 +6,8 @@ import ( "github.com/pkg/errors" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/ethereum/go-ethereum/common" "github.com/harmony-one/harmony/consensus/votepower" diff --git a/consensus/quorum/one-node-staked-vote.go b/consensus/quorum/one-node-staked-vote.go index 45f4289eab..b681bdbae1 100644 --- a/consensus/quorum/one-node-staked-vote.go +++ b/consensus/quorum/one-node-staked-vote.go @@ -9,7 +9,7 @@ import ( "github.com/harmony-one/harmony/internal/utils" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/pkg/errors" "github.com/harmony-one/harmony/consensus/votepower" diff --git a/consensus/quorum/one-node-staked-vote_test.go b/consensus/quorum/one-node-staked-vote_test.go index fcce3c61b0..c3e1451dfe 100644 --- a/consensus/quorum/one-node-staked-vote_test.go +++ b/consensus/quorum/one-node-staked-vote_test.go @@ -12,7 +12,7 @@ import ( shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/numeric" "github.com/harmony-one/harmony/shard" ) @@ -42,7 +42,9 @@ func generateRandomSlot() (shard.Slot, bls_core.SecretKey) { addr := common.Address{} addr.SetBytes(big.NewInt(int64(accountGen.Int63n(maxAccountGen))).Bytes()) secretKey := bls_core.SecretKey{} - secretKey.Deserialize(big.NewInt(int64(keyGen.Int63n(maxKeyGen))).Bytes()) + if err := secretKey.SetLittleEndian(big.NewInt(int64(keyGen.Int63n(maxKeyGen))).Bytes()); err != nil { + panic(err) + } key := bls.SerializedPublicKey{} key.FromLibBLSPublicKey(secretKey.GetPublicKey()) stake := numeric.NewDecFromBigInt(big.NewInt(int64(stakeGen.Int63n(maxStakeGen)))) diff --git a/consensus/quorum/quorom_test.go b/consensus/quorum/quorom_test.go index 6b0ae95968..59ea163b90 100644 --- a/consensus/quorum/quorom_test.go +++ b/consensus/quorum/quorom_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - bls_core "github.com/harmony-one/bls/ffi/go/bls" harmony_bls "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" "github.com/harmony-one/harmony/shard" "github.com/stretchr/testify/assert" diff --git a/consensus/quorum/quorum.go b/consensus/quorum/quorum.go index c6954f0246..3f6fc99e96 100644 --- a/consensus/quorum/quorum.go +++ b/consensus/quorum/quorum.go @@ -8,9 +8,9 @@ import ( "github.com/harmony-one/harmony/crypto/bls" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/consensus/votepower" bls_cosi "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/multibls" diff --git a/consensus/quorum/thread_safe_decider.go b/consensus/quorum/thread_safe_decider.go index 5a74975f91..bfa6e7e3e2 100644 --- a/consensus/quorum/thread_safe_decider.go +++ b/consensus/quorum/thread_safe_decider.go @@ -5,9 +5,9 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/consensus/votepower" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" "github.com/harmony-one/harmony/multibls" "github.com/harmony-one/harmony/numeric" diff --git a/consensus/slash_group_test.go b/consensus/slash_group_test.go index 409b428566..e8dde17f9b 100644 --- a/consensus/slash_group_test.go +++ b/consensus/slash_group_test.go @@ -6,7 +6,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/common/denominations" @@ -16,6 +15,7 @@ import ( coretypes "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" hmybls "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" chain2 "github.com/harmony-one/harmony/internal/chain" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" diff --git a/consensus/validator_wrapper_payout_test.go b/consensus/validator_wrapper_payout_test.go index 5301d947ed..c529bbb49e 100644 --- a/consensus/validator_wrapper_payout_test.go +++ b/consensus/validator_wrapper_payout_test.go @@ -10,7 +10,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/common/denominations" @@ -22,6 +21,7 @@ import ( coretypes "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" hmybls "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" chain2 "github.com/harmony-one/harmony/internal/chain" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" diff --git a/consensus/view_change_construct.go b/consensus/view_change_construct.go index 5d25531757..80b3f5ef9d 100644 --- a/consensus/view_change_construct.go +++ b/consensus/view_change_construct.go @@ -10,10 +10,10 @@ import ( "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core/types" - bls_core "github.com/harmony-one/bls/ffi/go/bls" msg_pb "github.com/harmony-one/harmony/api/proto/message" "github.com/harmony-one/harmony/crypto/bls" bls_cosi "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/chain" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/multibls" diff --git a/consensus/view_change_msg.go b/consensus/view_change_msg.go index 1ba9c76063..fb939c20df 100644 --- a/consensus/view_change_msg.go +++ b/consensus/view_change_msg.go @@ -7,10 +7,10 @@ import ( "github.com/ethereum/go-ethereum/rlp" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/api/proto" msg_pb "github.com/harmony-one/harmony/api/proto/message" bls_cosi "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/multibls" "github.com/pkg/errors" diff --git a/consensus/view_change_test.go b/consensus/view_change_test.go index 9514981d55..eae7f35998 100644 --- a/consensus/view_change_test.go +++ b/consensus/view_change_test.go @@ -5,8 +5,8 @@ import ( "github.com/harmony-one/harmony/crypto/bls" - bls_core "github.com/harmony-one/bls/ffi/go/bls" harmony_bls "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/stretchr/testify/assert" ) diff --git a/consensus/votepower/roster.go b/consensus/votepower/roster.go index 96a3ac9c6b..b82ef97fe7 100644 --- a/consensus/votepower/roster.go +++ b/consensus/votepower/roster.go @@ -10,8 +10,8 @@ import ( "github.com/harmony-one/harmony/shard" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" common2 "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/numeric" diff --git a/consensus/votepower/roster_test.go b/consensus/votepower/roster_test.go index 0e7148fa27..f70cda0893 100644 --- a/consensus/votepower/roster_test.go +++ b/consensus/votepower/roster_test.go @@ -10,7 +10,7 @@ import ( shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/numeric" "github.com/harmony-one/harmony/shard" ) @@ -48,7 +48,9 @@ func generateRandomSlot() shard.Slot { addr := common.Address{} addr.SetBytes(big.NewInt(int64(accountGen.Int63n(maxAccountGen))).Bytes()) secretKey := bls_core.SecretKey{} - secretKey.Deserialize(big.NewInt(int64(keyGen.Int63n(maxKeyGen))).Bytes()) + if err := secretKey.SetLittleEndian(big.NewInt(int64(keyGen.Int63n(maxKeyGen))).Bytes()); err != nil { + panic(err) + } key := bls.SerializedPublicKey{} key.FromLibBLSPublicKey(secretKey.GetPublicKey()) stake := numeric.NewDecFromBigInt(big.NewInt(int64(stakeGen.Int63n(maxStakeGen)))) diff --git a/core/blockchain_impl.go b/core/blockchain_impl.go index a696f554f9..ea75b76f59 100644 --- a/core/blockchain_impl.go +++ b/core/blockchain_impl.go @@ -40,7 +40,6 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" - bls2 "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" consensus_engine "github.com/harmony-one/harmony/consensus/engine" "github.com/harmony-one/harmony/consensus/reward" @@ -52,6 +51,7 @@ import ( "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/crypto/bls" + bls2 "github.com/harmony-one/harmony/crypto/bls/core" harmonyconfig "github.com/harmony-one/harmony/internal/configs/harmony" "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/internal/tikv" @@ -3228,6 +3228,24 @@ func (bc *BlockChainImpl) prepareStakingMetaData( case staking.DirectiveUndelegate: case staking.DirectiveCollectRewards: + case staking.DirectiveBatchDelegate: + batchDelegate := decodePayload.(*staking.BatchDelegate) + for _, delegationAction := range batchDelegate.Delegations { + delegate := &staking.Delegate{ + DelegatorAddress: batchDelegate.DelegatorAddress, + ValidatorAddress: delegationAction.ValidatorAddress, + Amount: delegationAction.Amount, + } + if err := processDelegateMetadata(delegate, + newDelegations, + state, + bc, + blockNum); err != nil { + return nil, nil, err + } + } + case staking.DirectiveBatchUndelegate: + case staking.DirectiveUndelegateAll: default: } } diff --git a/core/evm.go b/core/evm.go index f8e94067df..3b75ce5bdd 100644 --- a/core/evm.go +++ b/core/evm.go @@ -93,6 +93,9 @@ func NewEVMBlockContext(msg Message, header *block.Header, chain ChainContext, a Delegate: DelegateFn(header, chain), Undelegate: UndelegateFn(header, chain), CollectRewards: CollectRewardsFn(header, chain), + BatchDelegate: BatchDelegateFn(header, chain), + BatchUndelegate: BatchUndelegateFn(header, chain), + UndelegateAll: UndelegateAllFn(header, chain), CalculateMigrationGas: CalculateMigrationGasFn(chain), ShardID: chain.ShardID(), NumShards: shard.Schedule.InstanceForEpoch(header.Epoch()).NumShards(), @@ -329,6 +332,183 @@ func CollectRewardsFn(ref *block.Header, chain ChainContext) vm.CollectRewardsFu } } +func BatchDelegateFn(ref *block.Header, chain ChainContext) vm.BatchDelegateFunc { + return func(db vm.StateDB, rosettaTracer vm.RosettaTracer, batchDelegate *stakingTypes.BatchDelegate) error { + delegations, err := chain.ReadDelegationsByDelegatorAt(batchDelegate.DelegatorAddress, big.NewInt(0).Sub(ref.Number(), big.NewInt(1))) + if err != nil { + return err + } + updatedValidatorWrappers, balanceToBeDeducted, fromLockedTokens, err := VerifyAndBatchDelegateFromMsg( + db, ref.Epoch(), batchDelegate, delegations, chain.Config()) + if err != nil { + return err + } + for _, wrapper := range updatedValidatorWrappers { + if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil { + return err + } + } + + db.SubBalance(batchDelegate.DelegatorAddress, balanceToBeDeducted) + + if rosettaTracer != nil && balanceToBeDeducted.Sign() != 0 { + dest := batchDelegate.Delegations[0].ValidatorAddress + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + }, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + SubAccount: &dest, + Metadata: map[string]interface{}{"type": "delegation"}, + }, + balanceToBeDeducted, + ) + } + + if len(fromLockedTokens) > 0 { + sortedKeys := []common.Address{} + for key := range fromLockedTokens { + sortedKeys = append(sortedKeys, key) + } + sort.SliceStable(sortedKeys, func(i, j int) bool { + return bytes.Compare(sortedKeys[i][:], sortedKeys[j][:]) < 0 + }) + var singleDest *common.Address + if len(batchDelegate.Delegations) == 1 { + singleDest = &batchDelegate.Delegations[0].ValidatorAddress + } + for _, key := range sortedKeys { + redelegatedToken, ok := fromLockedTokens[key] + if !ok { + return errors.New("Key missing for delegation receipt") + } + encodedRedelegationData := []byte{} + addrBytes := key.Bytes() + encodedRedelegationData = append(encodedRedelegationData, addrBytes...) + encodedRedelegationData = append(encodedRedelegationData, redelegatedToken.Bytes()...) + db.AddLog(&types.Log{ + Address: batchDelegate.DelegatorAddress, + Topics: []common.Hash{staking.DelegateTopic}, + Data: encodedRedelegationData, + BlockNumber: ref.Number().Uint64(), + }) + + if rosettaTracer != nil { + fromAccount := common.BytesToAddress(key.Bytes()) + toAccount := fromAccount + metaType := "redelegation" + if singleDest != nil { + toAccount = *singleDest + metaType = "delegation" + } + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + SubAccount: &fromAccount, + Metadata: map[string]interface{}{"type": "undelegation"}, + }, + &vm.RosettaLogAddressItem{ + Account: &batchDelegate.DelegatorAddress, + SubAccount: &toAccount, + Metadata: map[string]interface{}{"type": metaType}, + }, + redelegatedToken, + ) + } + } + } + return nil + } +} + +func BatchUndelegateFn(ref *block.Header, chain ChainContext) vm.BatchUndelegateFunc { + return func(db vm.StateDB, rosettaTracer vm.RosettaTracer, batchUndelegate *stakingTypes.BatchUndelegate) error { + updatedValidatorWrappers, err := VerifyAndBatchUndelegateFromMsg(db, ref.Epoch(), batchUndelegate, chain.Config()) + if err != nil { + return err + } + + for _, wrapper := range updatedValidatorWrappers { + if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil { + return err + } + } + + if rosettaTracer != nil { + for _, action := range batchUndelegate.Undelegations { + amount := action.Amount + validatorAddr := action.ValidatorAddress + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &batchUndelegate.DelegatorAddress, + SubAccount: &validatorAddr, + Metadata: map[string]interface{}{"type": "delegation"}, + }, + &vm.RosettaLogAddressItem{ + Account: &batchUndelegate.DelegatorAddress, + SubAccount: &validatorAddr, + Metadata: map[string]interface{}{"type": "undelegation"}, + }, + amount, + ) + } + } + + return nil + } +} + +func UndelegateAllFn(ref *block.Header, chain ChainContext) vm.UndelegateAllFunc { + return func(db vm.StateDB, rosettaTracer vm.RosettaTracer, undelegateAll *stakingTypes.UndelegateAll) error { + if chain == nil { + return errors.New("[UndelegateAll] No chain context provided") + } + delegations, err := chain.ReadDelegationsByDelegatorAt(undelegateAll.DelegatorAddress, big.NewInt(0).Sub(ref.Number(), big.NewInt(1))) + if err != nil { + return err + } + + updatedValidatorWrappers, actions, err := VerifyAndUndelegateAllFromMsg( + db, ref.Epoch(), undelegateAll, delegations, chain, chain.Config(), + ) + if err != nil { + return err + } + for _, wrapper := range updatedValidatorWrappers { + if err := db.UpdateValidatorWrapperWithRevert(wrapper.Address, wrapper); err != nil { + return err + } + } + + if rosettaTracer != nil { + for _, action := range actions { + validatorAddr := action.ValidatorAddress + amount := action.Amount + rosettaTracer.AddRosettaLog( + vm.CALL, + &vm.RosettaLogAddressItem{ + Account: &undelegateAll.DelegatorAddress, + SubAccount: &validatorAddr, + Metadata: map[string]interface{}{"type": "delegation"}, + }, + &vm.RosettaLogAddressItem{ + Account: &undelegateAll.DelegatorAddress, + SubAccount: &validatorAddr, + Metadata: map[string]interface{}{"type": "undelegation"}, + }, + amount, + ) + } + } + + return nil + } +} + //func MigrateDelegationsFn(ref *block.Header, chain ChainContext) vm.MigrateDelegationsFunc { // return func(db vm.StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error) { // // get existing delegations diff --git a/core/evm_test.go b/core/evm_test.go index e095e5917b..adc34f0037 100644 --- a/core/evm_test.go +++ b/core/evm_test.go @@ -11,7 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + "github.com/harmony-one/harmony/accounts/abi" "github.com/harmony-one/harmony/block" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/common/denominations" @@ -20,6 +20,7 @@ import ( "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" chain2 "github.com/harmony-one/harmony/internal/chain" "github.com/harmony-one/harmony/internal/params" @@ -444,15 +445,25 @@ func TestWriteCapablePrecompilesIntegration(t *testing.T) { evm := vm.NewEVM(ctx, NewEVMTxContext(msg), db, params.TestChainConfig, vm.Config{}) // interpreter := vm.NewEVMInterpreter(evm, vm.Config{}) address := common.BytesToAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 252}) - // caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) - _, _, err := evm.Call(vm.AccountRef(common.Address{}), address, - []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19}, - math.MaxUint64, new(big.Int)) - expectedError := errors.New("abi: cannot marshal in to go type: length insufficient 31 require 32") - if err != nil { - if err.Error() != expectedError.Error() { - t.Error(fmt.Sprintf("Got error %v in evm.Call but expected %v", err, expectedError)) + malformedInput := []byte{109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19} + expectedABIError := "abi: cannot marshal in to go type: length insufficient 31 require 32" + ret, _, err := evm.Call(vm.AccountRef(common.Address{}), address, malformedInput, math.MaxUint64, new(big.Int)) + if err == nil { + t.Fatal("expected error from malformed staking precompile input") + } + if params.TestChainConfig.IsStakingV2(header.Epoch()) { + if !errors.Is(err, vm.ErrExecutionReverted) { + t.Errorf("Got error %v in evm.Call but expected %v", err, vm.ErrExecutionReverted) + } + reason, unpackErr := abi.UnpackRevert(ret) + if unpackErr != nil { + t.Fatalf("failed to unpack revert data: %v", unpackErr) + } + if reason != expectedABIError { + t.Errorf("Got revert reason %q but expected %q", reason, expectedABIError) } + } else if err.Error() != expectedABIError { + t.Errorf("Got error %v in evm.Call but expected %v", err, expectedABIError) } // now add a validator, and send its address as caller diff --git a/core/rawdb/accessors_indexes_test.go b/core/rawdb/accessors_indexes_test.go index 41f54ac98a..0ede88d8d2 100644 --- a/core/rawdb/accessors_indexes_test.go +++ b/core/rawdb/accessors_indexes_test.go @@ -25,9 +25,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - bls_core "github.com/harmony-one/bls/ffi/go/bls" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/core/types" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" "github.com/harmony-one/harmony/numeric" staking "github.com/harmony-one/harmony/staking/types" diff --git a/core/staking_verifier.go b/core/staking_verifier.go index a428cc4c64..92c11a380d 100644 --- a/core/staking_verifier.go +++ b/core/staking_verifier.go @@ -277,6 +277,15 @@ var ( // Note that this function never updates the stateDB, it only reads from stateDB. func VerifyAndDelegateFromMsg( stateDB vm.StateDB, epoch *big.Int, msg *staking.Delegate, delegations []staking.DelegationIndex, chainConfig *params.ChainConfig, +) ([]*staking.ValidatorWrapper, *big.Int, map[common.Address]*big.Int, error) { + return verifyAndDelegateFromMsg(stateDB, epoch, msg, delegations, chainConfig, nil) +} + +// verifyAndDelegateFromMsg implements single and batch delegation. +// When wrapperCache is non-nil, wrappers are reused across calls. +func verifyAndDelegateFromMsg( + stateDB vm.StateDB, epoch *big.Int, msg *staking.Delegate, delegations []staking.DelegationIndex, chainConfig *params.ChainConfig, + wrapperCache map[common.Address]*staking.ValidatorWrapper, ) ([]*staking.ValidatorWrapper, *big.Int, map[common.Address]*big.Int, error) { if stateDB == nil { return nil, nil, nil, errStateDBIsMissing @@ -297,6 +306,22 @@ func VerifyAndDelegateFromMsg( } } + getWrapper := func(addr common.Address) (*staking.ValidatorWrapper, error) { + if wrapperCache != nil { + if cached, ok := wrapperCache[addr]; ok { + return cached, nil + } + } + wrapper, err := stateDB.ValidatorWrapper(addr, false, true) + if err != nil { + return nil, err + } + if wrapperCache != nil { + wrapperCache[addr] = wrapper + } + return wrapper, nil + } + updatedValidatorWrappers := []*staking.ValidatorWrapper{} delegateBalance := big.NewInt(0).Set(msg.Amount) fromLockedTokens := map[common.Address]*big.Int{} @@ -306,8 +331,7 @@ func VerifyAndDelegateFromMsg( // Check if we can use tokens in undelegation to delegate (redelegate) for i := range delegations { delegationIndex := &delegations[i] - // request a copy, and since delegations will be changed, copy them too - wrapper, err := stateDB.ValidatorWrapper(delegationIndex.ValidatorAddress, false, true) + wrapper, err := getWrapper(delegationIndex.ValidatorAddress) if err != nil { return nil, nil, nil, err } @@ -330,24 +354,58 @@ func VerifyAndDelegateFromMsg( startBalance := big.NewInt(0).Set(delegateBalance) // Start from the oldest undelegated tokens curIndex := 0 - for ; curIndex < len(delegation.Undelegations); curIndex++ { - if delegation.Undelegations[curIndex].Epoch.Cmp(epoch) >= 0 { - break + isStakingV2 := chainConfig.IsStakingV2(epoch) + + if isStakingV2 { + newUndelegations := []staking.Undelegation{} + for curIndex < len(delegation.Undelegations) { + entry := &delegation.Undelegations[curIndex] + if entry.Epoch.Cmp(epoch) >= 0 { + newUndelegations = append(newUndelegations, delegation.Undelegations[curIndex:]...) + break + } + + if entry.Amount.Cmp(delegateBalance) <= 0 { + delegateBalance.Sub(delegateBalance, entry.Amount) + } else { + remainingAmount := big.NewInt(0).Sub(entry.Amount, delegateBalance) + newUndelegations = append(newUndelegations, staking.Undelegation{ + Amount: remainingAmount, + Epoch: entry.Epoch, + }) + delegateBalance = big.NewInt(0) + curIndex++ + if curIndex < len(delegation.Undelegations) { + newUndelegations = append(newUndelegations, delegation.Undelegations[curIndex:]...) + } + break + } + curIndex++ } - if delegation.Undelegations[curIndex].Amount.Cmp(delegateBalance) <= 0 { - delegateBalance.Sub(delegateBalance, delegation.Undelegations[curIndex].Amount) - } else { - delegation.Undelegations[curIndex].Amount.Sub( - delegation.Undelegations[curIndex].Amount, delegateBalance, - ) - delegateBalance = big.NewInt(0) - break + if startBalance.Cmp(delegateBalance) > 0 { + delegation.Undelegations = newUndelegations + } + } else { + for ; curIndex < len(delegation.Undelegations); curIndex++ { + if delegation.Undelegations[curIndex].Epoch.Cmp(epoch) >= 0 { + break + } + if delegation.Undelegations[curIndex].Amount.Cmp(delegateBalance) <= 0 { + delegateBalance.Sub(delegateBalance, delegation.Undelegations[curIndex].Amount) + } else { + delegation.Undelegations[curIndex].Amount.Sub( + delegation.Undelegations[curIndex].Amount, delegateBalance, + ) + delegateBalance = big.NewInt(0) + break + } } } if startBalance.Cmp(delegateBalance) > 0 { - // Used undelegated token for redelegation - delegation.Undelegations = delegation.Undelegations[curIndex:] + if !isStakingV2 { + delegation.Undelegations = delegation.Undelegations[curIndex:] + } if err := wrapper.SanityCheck(); err != nil { return nil, nil, nil, err } @@ -363,8 +421,7 @@ func VerifyAndDelegateFromMsg( if delegateeWrapper == nil { var err error - // request a copy, and since delegations will be changed, copy them too - delegateeWrapper, err = stateDB.ValidatorWrapper(msg.ValidatorAddress, false, true) + delegateeWrapper, err = getWrapper(msg.ValidatorAddress) if err != nil { return nil, nil, nil, err } @@ -406,9 +463,8 @@ func VerifyAndDelegateFromMsg( return updatedValidatorWrappers, big.NewInt(0), fromLockedTokens, nil } - // Still need to deduct tokens from balance for delegation - // Check if there is enough liquid token to delegate - if !CanTransfer(stateDB, msg.DelegatorAddress, delegateBalance) { + // Liquid balance is checked only when wrapperCache is nil. + if wrapperCache == nil && !CanTransfer(stateDB, msg.DelegatorAddress, delegateBalance) { return nil, nil, nil, errors.Wrapf( errInsufficientBalanceForStake, "totalRedelegatable: %v, balance: %v; trying to stake %v", big.NewInt(0).Sub(msg.Amount, delegateBalance), stateDB.GetBalance(msg.DelegatorAddress), msg.Amount) @@ -466,6 +522,340 @@ func VerifyAndUndelegateFromMsg( return nil, errNoDelegationToUndelegate } +// VerifyAndBatchDelegateFromMsg verifies batch delegation message using the stateDB +// and returns all updated validator wrappers, total balance to be deducted, and locked tokens map. +// +// Note that this function never updates the stateDB, it only reads from stateDB. +func VerifyAndBatchDelegateFromMsg( + stateDB vm.StateDB, epoch *big.Int, msg *staking.BatchDelegate, delegations []staking.DelegationIndex, chainConfig *params.ChainConfig, +) ([]*staking.ValidatorWrapper, *big.Int, map[common.Address]*big.Int, error) { + if stateDB == nil { + return nil, nil, nil, errStateDBIsMissing + } + if epoch == nil { + return nil, nil, nil, errEpochMissing + } + if chainConfig == nil { + return nil, nil, nil, errors.New("chain config is required") + } + if !chainConfig.IsStakingV2(epoch) { + return nil, nil, nil, errors.New("batch delegation is only available in StakingV2 epoch") + } + if len(msg.Delegations) == 0 { + return nil, nil, nil, errors.New("batch delegation must contain at least one delegation") + } + if len(msg.Delegations) > staking.MaxBatchStakingActions { + return nil, nil, nil, staking.ErrBatchTooLarge + } + + wrapperCache := map[common.Address]*staking.ValidatorWrapper{} + totalBalanceToDeduct := big.NewInt(0) + allFromLockedTokens := map[common.Address]*big.Int{} + + for _, delegationAction := range msg.Delegations { + if !stateDB.IsValidator(delegationAction.ValidatorAddress) { + return nil, nil, nil, errValidatorNotExist + } + if delegationAction.Amount == nil || delegationAction.Amount.Sign() == -1 { + return nil, nil, nil, errNegativeAmount + } + if delegationAction.Amount.Cmp(minimumDelegation) < 0 { + if chainConfig.IsMinDelegation100(epoch) { + if delegationAction.Amount.Cmp(minimumDelegationV2) < 0 { + return nil, nil, nil, errDelegationTooSmallV2 + } + } else { + return nil, nil, nil, errDelegationTooSmall + } + } + + delegateMsg := &staking.Delegate{ + DelegatorAddress: msg.DelegatorAddress, + ValidatorAddress: delegationAction.ValidatorAddress, + Amount: delegationAction.Amount, + } + + _, balanceToDeduct, fromLockedTokens, err := verifyAndDelegateFromMsg( + stateDB, epoch, delegateMsg, delegations, chainConfig, wrapperCache, + ) + if err != nil { + return nil, nil, nil, err + } + + totalBalanceToDeduct.Add(totalBalanceToDeduct, balanceToDeduct) + + for validatorAddr, amount := range fromLockedTokens { + if existingAmount, exists := allFromLockedTokens[validatorAddr]; exists { + allFromLockedTokens[validatorAddr] = new(big.Int).Add(existingAmount, amount) + } else { + allFromLockedTokens[validatorAddr] = new(big.Int).Set(amount) + } + } + } + + if totalBalanceToDeduct.Cmp(big.NewInt(0)) > 0 { + if !CanTransfer(stateDB, msg.DelegatorAddress, totalBalanceToDeduct) { + return nil, nil, nil, errors.Wrapf( + errInsufficientBalanceForStake, "insufficient balance for batch delegation: %v", + totalBalanceToDeduct, + ) + } + } + + return sortedWrappersFromCache(wrapperCache, msg.Delegations), totalBalanceToDeduct, allFromLockedTokens, nil +} + +// sortedWrappersFromCache returns wrappers for action destinations in order, +// then any remaining cached addresses sorted by address. +func sortedWrappersFromCache( + wrapperCache map[common.Address]*staking.ValidatorWrapper, + actions []staking.DelegationAction, +) []*staking.ValidatorWrapper { + allUpdatedWrappers := make([]*staking.ValidatorWrapper, 0, len(wrapperCache)) + seen := map[common.Address]bool{} + for _, action := range actions { + addr := action.ValidatorAddress + if w, ok := wrapperCache[addr]; ok && !seen[addr] { + allUpdatedWrappers = append(allUpdatedWrappers, w) + seen[addr] = true + } + } + rest := make([]common.Address, 0, len(wrapperCache)) + for addr := range wrapperCache { + if !seen[addr] { + rest = append(rest, addr) + } + } + sort.Slice(rest, func(i, j int) bool { + return bytes.Compare(rest[i][:], rest[j][:]) < 0 + }) + for _, addr := range rest { + allUpdatedWrappers = append(allUpdatedWrappers, wrapperCache[addr]) + } + return allUpdatedWrappers +} + +// VerifyAndBatchUndelegateFromMsg verifies batch undelegation message using the stateDB +// and returns all updated validator wrappers. +// +// Note that this function never updates the stateDB, it only reads from stateDB. +func VerifyAndBatchUndelegateFromMsg( + stateDB vm.StateDB, epoch *big.Int, msg *staking.BatchUndelegate, chainConfig *params.ChainConfig, +) ([]*staking.ValidatorWrapper, error) { + if stateDB == nil { + return nil, errStateDBIsMissing + } + if epoch == nil { + return nil, errEpochMissing + } + if chainConfig == nil { + return nil, errors.New("chain config is required") + } + if !chainConfig.IsStakingV2(epoch) { + return nil, errors.New("batch undelegation is only available in StakingV2 epoch") + } + if len(msg.Undelegations) == 0 { + return nil, errors.New("batch undelegation must contain at least one undelegation") + } + if len(msg.Undelegations) > staking.MaxBatchStakingActions { + return nil, staking.ErrBatchTooLarge + } + + wrapperMap := map[common.Address]*staking.ValidatorWrapper{} + touched := []common.Address{} + + for _, action := range msg.Undelegations { + amount := action.Amount + if amount == nil || amount.Sign() < 0 { + return nil, errNegativeAmount + } + if amount.Sign() == 0 { + return nil, errors.New("invalid amount, must be positive") + } + + if !stateDB.IsValidator(action.ValidatorAddress) { + return nil, errValidatorNotExist + } + + wrapper, exists := wrapperMap[action.ValidatorAddress] + if !exists { + var err error + wrapper, err = stateDB.ValidatorWrapper(action.ValidatorAddress, false, true) + if err != nil { + return nil, err + } + if err := checkValidatorWrapperAddressBinding( + chainConfig, epoch, action.ValidatorAddress, wrapper, + ); err != nil { + return nil, err + } + wrapperMap[action.ValidatorAddress] = wrapper + touched = append(touched, action.ValidatorAddress) + } + + found := false + for i := range wrapper.Delegations { + delegation := &wrapper.Delegations[i] + if bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { + if err := delegation.Undelegate(epoch, amount); err != nil { + return nil, err + } + found = true + break + } + } + if !found { + return nil, errNoDelegationToUndelegate + } + } + + allUpdatedWrappers := make([]*staking.ValidatorWrapper, 0, len(touched)) + for _, addr := range touched { + wrapper := wrapperMap[addr] + if err := wrapper.SanityCheck(); err != nil { + if errors.Cause(err) == staking.ErrInvalidSelfDelegation { + wrapper.Status = effective.Inactive + } else { + return nil, err + } + } + allUpdatedWrappers = append(allUpdatedWrappers, wrapper) + } + + return allUpdatedWrappers, nil +} + +// VerifyAndUndelegateAllFromMsg undelegates all active stake for a delegator. +// It uses the provided delegation indexes and scans validators for any others. +// +// Note that this function never updates the stateDB, it only reads from stateDB. +// The returned UndelegationAction slice lists every undelegation applied. +func VerifyAndUndelegateAllFromMsg( + stateDB vm.StateDB, epoch *big.Int, msg *staking.UndelegateAll, delegations []staking.DelegationIndex, chainContext ChainContext, chainConfig *params.ChainConfig, +) ([]*staking.ValidatorWrapper, []staking.UndelegationAction, error) { + if stateDB == nil { + return nil, nil, errStateDBIsMissing + } + if epoch == nil { + return nil, nil, errEpochMissing + } + if chainConfig == nil { + if chainContext != nil { + chainConfig = chainContext.Config() + } + } + if chainConfig == nil { + return nil, nil, errors.New("chain config is required") + } + if !chainConfig.IsStakingV2(epoch) { + return nil, nil, errors.New("undelegate all is only available in StakingV2 epoch") + } + + actions := []staking.UndelegationAction{} + processedValidators := map[common.Address]map[uint64]bool{} + + // Process delegations from the provided index list. + for _, delegationIndex := range delegations { + if !stateDB.IsValidator(delegationIndex.ValidatorAddress) { + continue + } + + wrapper, err := stateDB.ValidatorWrapper(delegationIndex.ValidatorAddress, false, false) + if err != nil { + continue + } + + if uint64(len(wrapper.Delegations)) <= delegationIndex.Index { + continue + } + + delegation := &wrapper.Delegations[delegationIndex.Index] + if !bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { + continue + } + + if delegation.Amount.Cmp(common.Big0) <= 0 { + continue + } + + actions = append(actions, staking.UndelegationAction{ + ValidatorAddress: delegationIndex.ValidatorAddress, + Amount: new(big.Int).Set(delegation.Amount), + }) + + if processedValidators[delegationIndex.ValidatorAddress] == nil { + processedValidators[delegationIndex.ValidatorAddress] = make(map[uint64]bool) + } + processedValidators[delegationIndex.ValidatorAddress][delegationIndex.Index] = true + } + + // Scan validators for active delegations not already processed. + if chainContext != nil { + validatorList, err := chainContext.ReadValidatorList() + if err == nil { + for _, validatorAddr := range validatorList { + if !stateDB.IsValidator(validatorAddr) { + continue + } + + wrapper, err := stateDB.ValidatorWrapper(validatorAddr, false, false) + if err != nil { + continue + } + + for i := range wrapper.Delegations { + delegation := &wrapper.Delegations[i] + if !bytes.Equal(delegation.DelegatorAddress.Bytes(), msg.DelegatorAddress.Bytes()) { + continue + } + + if delegation.Amount.Cmp(common.Big0) <= 0 { + continue + } + + if processedValidators[validatorAddr] != nil && processedValidators[validatorAddr][uint64(i)] { + continue + } + + actions = append(actions, staking.UndelegationAction{ + ValidatorAddress: validatorAddr, + Amount: new(big.Int).Set(delegation.Amount), + }) + + if processedValidators[validatorAddr] == nil { + processedValidators[validatorAddr] = make(map[uint64]bool) + } + processedValidators[validatorAddr][uint64(i)] = true + } + } + } + } + + if len(actions) == 0 { + if chainContext == nil && len(delegations) == 0 { + return nil, nil, errors.New("no delegations to undelegate") + } + return nil, nil, errors.New("no active delegations to undelegate") + } + if len(actions) > staking.MaxBatchStakingActions { + return nil, nil, errors.Errorf( + "undelegate all has %d active delegations; max is %d", + len(actions), staking.MaxBatchStakingActions, + ) + } + + batchUndelegateMsg := &staking.BatchUndelegate{ + DelegatorAddress: msg.DelegatorAddress, + Undelegations: actions, + } + + wrappers, err := VerifyAndBatchUndelegateFromMsg(stateDB, epoch, batchUndelegateMsg, chainConfig) + if err != nil { + return nil, nil, err + } + return wrappers, actions, nil +} + // VerifyAndMigrateFromMsg verifies and transfers all delegations of // msg.From to msg.To. Returns all modified validator wrappers and delegate msgs // for metadata @@ -530,15 +920,17 @@ func VerifyAndMigrateFromMsg( totalAmount = delegation.Amount.Add(delegation.Amount, delegationAmountToMigrate) // and the undelegations for _, undelegationToMigrate := range undelegationsToMigrate { - exist := false - for _, entry := range delegation.Undelegations { - if entry.Epoch.Cmp(undelegationToMigrate.Epoch) == 0 { - exist = true - entry.Amount.Add(entry.Amount, undelegationToMigrate.Amount) + merged := false + for i := range delegation.Undelegations { + if delegation.Undelegations[i].Epoch.Cmp(undelegationToMigrate.Epoch) == 0 { + delegation.Undelegations[i].Amount.Add( + delegation.Undelegations[i].Amount, undelegationToMigrate.Amount, + ) + merged = true break } } - if !exist { + if !merged { delegation.Undelegations = append(delegation.Undelegations, undelegationToMigrate) } diff --git a/core/staking_verifier_test.go b/core/staking_verifier_test.go index 24173fbd44..9df1626c34 100644 --- a/core/staking_verifier_test.go +++ b/core/staking_verifier_test.go @@ -1134,6 +1134,35 @@ func makeStateForRedelegate(t *testing.T) *state.DB { return sdb } +// makeStateForRedelegateCornerCases creates state with multiple undelegation entries for corner case testing +func makeStateForRedelegateCornerCases(t *testing.T, validatorAddr common.Address, undelegations []struct { + amount *big.Int + epoch *big.Int +}) *state.DB { + sdb := makeStateDBForStake(t) + + w, err := sdb.ValidatorWrapper(validatorAddr, false, true) + if err != nil { + t.Fatal(err) + } + + // Add delegation with multiple undelegation entries + delegation := staking.NewDelegation(delegatorAddr, new(big.Int).Set(twentyKOnes)) + for _, undel := range undelegations { + if err := delegation.Undelegate(undel.epoch, undel.amount); err != nil { + t.Fatal(err) + } + } + w.Delegations = append(w.Delegations, delegation) + + if err := sdb.UpdateValidatorWrapper(validatorAddr, w); err != nil { + t.Fatal(err) + } + + sdb.IntermediateRoot(true) + return sdb +} + func addStateUndelegationForAddr(sdb *state.DB, addr common.Address, epoch *big.Int) error { w, err := sdb.ValidatorWrapper(addr, false, true) if err != nil { @@ -1722,6 +1751,8 @@ func (chain *fakeChainContext) Config() *params.ChainConfig { config := ¶ms.ChainConfig{} config.MinCommissionRateEpoch = big.NewInt(0) config.MinCommissionPromoPeriod = big.NewInt(10) + config.StakingV2Epoch = big.NewInt(0) + config.RedelegationEpoch = big.NewInt(0) return config } @@ -1838,3 +1869,1182 @@ func assertError(got, expect error) error { } return nil } + +// TestRedelegationCornerCases tests corner cases in redelegation logic that demonstrate +// bugs in the old implementation and fixes in Staking V2 +func TestRedelegationCornerCases(t *testing.T) { + epoch := big.NewInt(10) // Current epoch + epoch1 := big.NewInt(5) // Old undelegation epoch + epoch2 := big.NewInt(6) // Old undelegation epoch + epoch3 := big.NewInt(7) // Old undelegation epoch + + tests := []struct { + name string + undelegations []struct { + amount *big.Int + epoch *big.Int + } + delegateAmount *big.Int + stakingV2 bool + expectedUndelegations []struct { + amount *big.Int + epoch *big.Int + } + expectedBalanceDeducted *big.Int + description string + }{ + { + name: "CornerCase1_FullyConsumeFirst_PartiallyConsumeSecond", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch2}, // 10000 - partially consumed (need 5000, so 5000 remains) + {amount: fiveKOnes, epoch: epoch3}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Add(fiveKOnes, fiveKOnes), // 10000 total + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic bug: keeps partially consumed entry but may have issues with slice manipulation + {amount: fiveKOnes, epoch: epoch2}, // Partially consumed (should be 5000) + {amount: fiveKOnes, epoch: epoch3}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), // All from undelegations + description: "Old logic: Fully consume first entry, partially consume second. May have slice manipulation issues.", + }, + { + name: "CornerCase1_FullyConsumeFirst_PartiallyConsumeSecond_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch2}, // 10000 - partially consumed (need 5000, so 5000 remains) + {amount: fiveKOnes, epoch: epoch3}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Add(fiveKOnes, fiveKOnes), // 10000 total + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: correctly removes fully consumed, keeps partially consumed with correct amount + {amount: fiveKOnes, epoch: epoch2}, // Partially consumed (5000 remains) + {amount: fiveKOnes, epoch: epoch3}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), // All from undelegations + description: "New logic: Correctly removes fully consumed entry, keeps partially consumed with correct amount.", + }, + { + name: "CornerCase2_PartiallyConsumeFirst", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: tenKOnes, epoch: epoch1}, // 10000 - partially consumed (need 3000, so 7000 remains) + {amount: fiveKOnes, epoch: epoch2}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Mul(big.NewInt(3000), oneBig), // 3000 ONE (meets minimum) + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic: modifies entry in place, then slices + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(3000), oneBig)), epoch: epoch1}, // 7000 + {amount: fiveKOnes, epoch: epoch2}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), + description: "Old logic: Partially consumes first entry. May work but uses error-prone slice manipulation.", + }, + { + name: "CornerCase2_PartiallyConsumeFirst_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: tenKOnes, epoch: epoch1}, // 10000 - partially consumed (need 3000, so 7000 remains) + {amount: fiveKOnes, epoch: epoch2}, // 5000 - untouched + }, + delegateAmount: new(big.Int).Mul(big.NewInt(3000), oneBig), // 3000 ONE (meets minimum) + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: creates new entry with correct remaining amount + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(3000), oneBig)), epoch: epoch1}, // 7000 + {amount: fiveKOnes, epoch: epoch2}, // Untouched + }, + expectedBalanceDeducted: big.NewInt(0), + description: "New logic: Explicitly creates new entry with correct remaining amount.", + }, + { + name: "CornerCase3_FullyConsumeAll", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch3}, // 5000 - fully consumed + }, + delegateAmount: new(big.Int).Mul(big.NewInt(15000), oneBig), // 15000 ONE (all three, meets minimum) + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic: should remove all, but may have issues + }, + expectedBalanceDeducted: big.NewInt(0), + description: "Old logic: Fully consumes all entries. Should result in empty undelegations.", + }, + { + name: "CornerCase3_FullyConsumeAll_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch3}, // 5000 - fully consumed + }, + delegateAmount: new(big.Int).Mul(big.NewInt(15000), oneBig), // 15000 ONE (all three, meets minimum) + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: correctly removes all fully consumed entries + }, + expectedBalanceDeducted: big.NewInt(0), + description: "New logic: Correctly removes all fully consumed entries, results in empty undelegations.", + }, + { + name: "CornerCase4_MixedConsumption", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch3}, // 10000 - partially consumed (need 2000, so 8000 remains) + }, + delegateAmount: new(big.Int).Mul(big.NewInt(12000), oneBig), // 12000 ONE (meets minimum) + stakingV2: false, // Test old logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // Old logic: may have issues with multiple full consumptions followed by partial + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(2000), oneBig)), epoch: epoch3}, // 8000 + }, + expectedBalanceDeducted: big.NewInt(0), + description: "Old logic: Mixed full and partial consumption. May have slice manipulation issues.", + }, + { + name: "CornerCase4_MixedConsumption_V2", + undelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: fiveKOnes, epoch: epoch1}, // 5000 - fully consumed + {amount: fiveKOnes, epoch: epoch2}, // 5000 - fully consumed + {amount: tenKOnes, epoch: epoch3}, // 10000 - partially consumed (need 2000, so 8000 remains) + }, + delegateAmount: new(big.Int).Mul(big.NewInt(12000), oneBig), // 12000 ONE (meets minimum) + stakingV2: true, // Test new logic + expectedUndelegations: []struct { + amount *big.Int + epoch *big.Int + }{ + // New logic: correctly handles mixed consumption + {amount: new(big.Int).Sub(tenKOnes, new(big.Int).Mul(big.NewInt(2000), oneBig)), epoch: epoch3}, // 8000 + }, + expectedBalanceDeducted: big.NewInt(0), + description: "New logic: Correctly handles mixed full and partial consumption.", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // Create state with undelegations + sdb := makeStateForRedelegateCornerCases(t, validatorAddr, test.undelegations) + + // Create delegate message + msg := staking.Delegate{ + DelegatorAddress: delegatorAddr, + ValidatorAddress: validatorAddr, + Amount: new(big.Int).Set(test.delegateAmount), + } + + // Get delegation index + w, err := sdb.ValidatorWrapper(validatorAddr, false, false) + if err != nil { + t.Fatal(err) + } + delegationIndex := []staking.DelegationIndex{ + { + ValidatorAddress: validatorAddr, + Index: uint64(len(w.Delegations) - 1), // Last delegation (the one with undelegations) + BlockNum: big.NewInt(100), + }, + } + + // Configure chain config + config := ¶ms.ChainConfig{} + config.MinDelegation100Epoch = big.NewInt(100) + config.RedelegationEpoch = epoch // Enable redelegation + if test.stakingV2 { + config.StakingV2Epoch = epoch // Enable Staking V2 + } else { + config.StakingV2Epoch = big.NewInt(10000000) // EpochTBD - disable Staking V2 + } + + // Execute redelegation + ws, balanceDeducted, fromLockedTokens, err := VerifyAndDelegateFromMsg( + sdb, epoch, &msg, delegationIndex, config, + ) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // Verify balance deducted + if balanceDeducted.Cmp(test.expectedBalanceDeducted) != 0 { + t.Errorf("Balance deducted mismatch: got %v, expected %v", balanceDeducted, test.expectedBalanceDeducted) + } + + // Verify fromLockedTokens + if test.expectedBalanceDeducted.Cmp(big.NewInt(0)) == 0 { + // All from locked tokens + if len(fromLockedTokens) == 0 { + t.Errorf("Expected fromLockedTokens to be non-empty") + } else if lockedAmt, ok := fromLockedTokens[validatorAddr]; !ok { + t.Errorf("Expected fromLockedTokens to contain validatorAddr") + } else if lockedAmt.Cmp(test.delegateAmount) != 0 { + t.Errorf("FromLockedTokens amount mismatch: got %v, expected %v", lockedAmt, test.delegateAmount) + } + } + + // Verify undelegations in the result + if len(ws) == 0 { + t.Fatal("Expected at least one validator wrapper") + } + + foundDelegation := false + for _, w := range ws { + if w.Address == validatorAddr { + for _, del := range w.Delegations { + if del.DelegatorAddress == delegatorAddr { + foundDelegation = true + + // Verify undelegations + if len(del.Undelegations) != len(test.expectedUndelegations) { + t.Errorf("Undelegations count mismatch: got %d, expected %d. Description: %s", + len(del.Undelegations), len(test.expectedUndelegations), test.description) + t.Logf("Got undelegations: %+v", del.Undelegations) + t.Logf("Expected undelegations: %+v", test.expectedUndelegations) + } else { + for i, expectedUndel := range test.expectedUndelegations { + if i >= len(del.Undelegations) { + t.Errorf("Missing undelegation at index %d", i) + continue + } + actualUndel := del.Undelegations[i] + if actualUndel.Amount.Cmp(expectedUndel.amount) != 0 { + t.Errorf("Undelegation[%d] amount mismatch: got %v, expected %v. Description: %s", + i, actualUndel.Amount, expectedUndel.amount, test.description) + } + if actualUndel.Epoch.Cmp(expectedUndel.epoch) != 0 { + t.Errorf("Undelegation[%d] epoch mismatch: got %v, expected %v", + i, actualUndel.Epoch, expectedUndel.epoch) + } + } + } + break + } + } + break + } + } + + if !foundDelegation { + t.Fatal("Could not find delegation in result") + } + }) + } +} + +func TestVerifyAndBatchDelegateFromMsg(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + stakingV2Epoch := big.NewInt(defaultEpoch) + + tests := []struct { + name string + sdb vm.StateDB + epoch *big.Int + msg staking.BatchDelegate + delegations []staking.DelegationIndex + chainConfig *params.ChainConfig + + expVWrappers []staking.ValidatorWrapper + expAmt *big.Int + expRedel map[common.Address]*big.Int + expErr error + }{ + { + name: "successful batch delegate to two validators", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + {ValidatorAddress: validatorAddr2, Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + config.MinDelegation100Epoch = big.NewInt(100) + return config + }(), + expVWrappers: func() []staking.ValidatorWrapper { + w1 := makeVWrapperByIndex(validatorIndex) + w1.Delegations = append(w1.Delegations, staking.NewDelegation(delegatorAddr, tenKOnes)) + w2 := makeVWrapperByIndex(validator2Index) + w2.Delegations = append(w2.Delegations, staking.NewDelegation(delegatorAddr, fiveKOnes)) + return []staking.ValidatorWrapper{w1, w2} + }(), + expAmt: new(big.Int).Add(tenKOnes, fiveKOnes), + }, + { + name: "nil state db", + sdb: nil, + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errStateDBIsMissing, + }, + { + name: "nil epoch", + sdb: makeStateDBForStake(t), + epoch: nil, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errEpochMissing, + }, + { + name: "not StakingV2 epoch", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = big.NewInt(10000000) // Disabled + return config + }(), + expErr: errors.New("batch delegation is only available in StakingV2 epoch"), + }, + { + name: "empty delegations", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{}, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errors.New("batch delegation must contain at least one delegation"), + }, + { + name: "invalid validator", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: makeTestAddr("not exist"), Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errValidatorNotExist, + }, + { + name: "negative amount", + sdb: makeStateDBForStake(t), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: big.NewInt(-1)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errNegativeAmount, + }, + { + name: "insufficient balance", + sdb: func() *state.DB { + sdb := makeStateDBForStake(t) + sdb.SetBalance(delegatorAddr, big.NewInt(100)) + return sdb + }(), + epoch: epoch, + msg: staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + }, + delegations: makeMsgCollectRewards(), + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = stakingV2Epoch + return config + }(), + expErr: errInsufficientBalanceForStake, + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + ws, amt, amtRedel, err := VerifyAndBatchDelegateFromMsg( + test.sdb, test.epoch, &test.msg, test.delegations, test.chainConfig, + ) + + if assErr := assertError(err, test.expErr); assErr != nil { + t.Errorf("Test %v: %v", i, assErr) + } + if err != nil || test.expErr != nil { + return + } + + if amt.Cmp(test.expAmt) != 0 { + t.Errorf("Test %v: unexpected amount %v / %v", i, amt, test.expAmt) + } + + if len(amtRedel) != len(test.expRedel) { + t.Errorf("Test %v: wrong expected redelegation length %d / %d", i, len(amtRedel), len(test.expRedel)) + } else { + for key, value := range test.expRedel { + actValue, ok := amtRedel[key] + if !ok { + t.Errorf("Test %v: missing expected redelegation key/value %v / %v", i, key, value) + } + if value.Cmp(actValue) != 0 { + t.Errorf("Test %v: unexpected redelegation value %v / %v", i, actValue, value) + } + } + } + + if len(ws) != len(test.expVWrappers) { + t.Errorf("Test %v: wrong wrapper count %d / %d", i, len(ws), len(test.expVWrappers)) + return + } + + for j := range ws { + if err := staketest.CheckValidatorWrapperEqual(*ws[j], test.expVWrappers[j]); err != nil { + t.Errorf("Test %v wrapper %v: %v", i, j, err) + } + } + }) + } +} + +func TestVerifyAndBatchUndelegateFromMsg(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + stakingV2Config := func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + return config + } + + tests := []struct { + name string + sdb vm.StateDB + epoch *big.Int + msg staking.BatchUndelegate + chainConfig *params.ChainConfig + expErr error + checkAmounts bool + }{ + { + name: "successful batch undelegate from two validators", + sdb: func() *state.DB { + sdb := makeDefaultStateForUndelegate(t) + w2 := makeVWrapperByIndex(validator2Index) + newDelegation2 := staking.NewDelegation(delegatorAddr, new(big.Int).Set(twentyKOnes)) + w2.Delegations = append(w2.Delegations, newDelegation2) + if err := sdb.UpdateValidatorWrapper(validatorAddr2, &w2); err != nil { + t.Fatal(err) + } + sdb.IntermediateRoot(true) + return sdb + }(), + epoch: epoch, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + {ValidatorAddress: validatorAddr2, Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + checkAmounts: true, + }, + { + name: "nil state db", + sdb: nil, + epoch: epoch, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + expErr: errStateDBIsMissing, + }, + { + name: "nil epoch", + sdb: makeDefaultStateForUndelegate(t), + epoch: nil, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + expErr: errEpochMissing, + }, + { + name: "not StakingV2 epoch", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + chainConfig: func() *params.ChainConfig { + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = big.NewInt(10000000) + return config + }(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + expErr: errors.New("batch undelegation is only available in StakingV2 epoch"), + }, + { + name: "empty undelegations", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{}, + }, + expErr: errors.New("batch undelegation must contain at least one undelegation"), + }, + { + name: "invalid validator", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: makeTestAddr("not exist"), Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + expErr: errValidatorNotExist, + }, + { + name: "negative amount", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: big.NewInt(-1)}, + }, + }, + expErr: errNegativeAmount, + }, + { + name: "no delegation for delegator", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: makeTestAddr("wrong delegator"), + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + }, + }, + expErr: errNoDelegationToUndelegate, + }, + { + name: "insufficient stake", + sdb: makeDefaultStateForUndelegate(t), + epoch: epoch, + chainConfig: stakingV2Config(), + msg: staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(hundredKOnes)}, + }, + }, + expErr: errors.New("insufficient balance to undelegate"), + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + ws, err := VerifyAndBatchUndelegateFromMsg(test.sdb, test.epoch, &test.msg, test.chainConfig) + + if assErr := assertError(err, test.expErr); assErr != nil { + t.Errorf("Test %v: %v", i, assErr) + } + if err != nil || test.expErr != nil { + return + } + + if len(ws) == 0 { + t.Errorf("Test %v: expected at least one wrapper", i) + } + if test.checkAmounts { + for _, wrapper := range ws { + for _, del := range wrapper.Delegations { + if del.DelegatorAddress != delegatorAddr { + continue + } + if wrapper.Address == validatorAddr { + // started 20k, prior 5k undelegation left 15k, then another 5k -> 10k active + if del.Amount.Cmp(tenKOnes) != 0 { + t.Errorf("validator1 amount = %s, want 10k", del.Amount) + } + // prior same-epoch entry 5k + new 5k = 10k + if len(del.Undelegations) != 1 || del.Undelegations[0].Amount.Cmp(tenKOnes) != 0 { + t.Errorf("validator1 undelegation = %+v, want 10k single entry", del.Undelegations) + } + } + if wrapper.Address == validatorAddr2 { + if del.Amount.Cmp(fifteenKOnes) != 0 { + t.Errorf("validator2 amount = %s, want 15k", del.Amount) + } + if len(del.Undelegations) != 1 || del.Undelegations[0].Amount.Cmp(fiveKOnes) != 0 { + t.Errorf("validator2 undelegation = %+v, want 5k", del.Undelegations) + } + } + } + } + } + }) + } +} + +func TestVerifyAndUndelegateAllFromMsg(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + + tests := []struct { + name string + sdb vm.StateDB + epoch *big.Int + msg staking.UndelegateAll + delegations []staking.DelegationIndex + chain ChainContext + expErr error + }{ + { + name: "successful undelegate all", + sdb: func() *state.DB { + sdb := makeDefaultStateForUndelegate(t) + w2 := makeVWrapperByIndex(validator2Index) + newDelegation2 := staking.NewDelegation(delegatorAddr, new(big.Int).Set(twentyKOnes)) + w2.Delegations = append(w2.Delegations, newDelegation2) + if err := sdb.UpdateValidatorWrapper(validatorAddr2, &w2); err != nil { + t.Fatal(err) + } + sdb.IntermediateRoot(true) + return sdb + }(), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: func() []staking.DelegationIndex { + return []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + {ValidatorAddress: validatorAddr2, Index: 1, BlockNum: big.NewInt(100)}, + } + }(), + chain: makeFakeChainContextForStake(), + }, + { + name: "nil state db", + sdb: nil, + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + chain: makeFakeChainContextForStake(), + expErr: errStateDBIsMissing, + }, + { + name: "nil epoch", + sdb: makeDefaultStateForUndelegate(t), + epoch: nil, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + chain: makeFakeChainContextForStake(), + expErr: errEpochMissing, + }, + { + name: "no delegations in list but found in state scan", + sdb: func() *state.DB { + sdb := makeDefaultStateForUndelegate(t) + return sdb + }(), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + chain: makeFakeChainContextForStake(), + expErr: nil, + }, + { + name: "no delegations at all", + sdb: func() *state.DB { + sdb := makeStateDBForStake(t) + return sdb + }(), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{}, + chain: makeFakeChainContextForStake(), + expErr: errors.New("no active delegations to undelegate"), + }, + { + name: "no active delegations", + sdb: func() *state.DB { + sdb := makeStateDBForStake(t) + w, _ := sdb.ValidatorWrapper(validatorAddr, false, true) + delegation := staking.NewDelegation(delegatorAddr, big.NewInt(0)) + w.Delegations = append(w.Delegations, delegation) + sdb.UpdateValidatorWrapper(validatorAddr, w) + return sdb + }(), + epoch: epoch, + msg: staking.UndelegateAll{ + DelegatorAddress: delegatorAddr, + }, + delegations: []staking.DelegationIndex{ + {ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}, + }, + chain: makeFakeChainContextForStake(), + expErr: errors.New("no active delegations to undelegate"), + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + ws, actions, err := VerifyAndUndelegateAllFromMsg(test.sdb, test.epoch, &test.msg, test.delegations, test.chain, nil) + + if assErr := assertError(err, test.expErr); assErr != nil { + t.Errorf("Test %v: %v", i, assErr) + } + if err != nil || test.expErr != nil { + return + } + + if len(ws) == 0 { + t.Errorf("Test %v: expected at least one wrapper", i) + } + if len(actions) == 0 { + t.Errorf("Test %v: expected undelegation actions for Rosetta", i) + } + }) + } +} + +func TestBatchDelegateRedelegationComposition(t *testing.T) { + epoch := big.NewInt(10) + oldEpoch := big.NewInt(5) + + sdb := makeStateForRedelegateCornerCases(t, validatorAddr, []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: new(big.Int).Set(fifteenKOnes), epoch: oldEpoch}, + }) + + w, err := sdb.ValidatorWrapper(validatorAddr, false, false) + if err != nil { + t.Fatal(err) + } + delegationIndex := []staking.DelegationIndex{{ + ValidatorAddress: validatorAddr, + Index: uint64(len(w.Delegations) - 1), + BlockNum: big.NewInt(100), + }} + + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + config.RedelegationEpoch = epoch + config.MinDelegation100Epoch = big.NewInt(100) + + msg := staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + {ValidatorAddress: validatorAddr2, Amount: new(big.Int).Set(tenKOnes)}, + }, + } + + ws, balance, fromLocked, err := VerifyAndBatchDelegateFromMsg( + sdb, epoch, &msg, delegationIndex, config, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if balance.Sign() != 0 { + t.Fatalf("expected fully locked-funded batch, got liquid deduct %v", balance) + } + locked, ok := fromLocked[validatorAddr] + if !ok || locked.Cmp(fifteenKOnes) != 0 { + t.Fatalf("expected 15k locked from source validator, got %v (ok=%v)", locked, ok) + } + + var sourceUndelegations staking.Undelegations + for _, wrapper := range ws { + if wrapper.Address == validatorAddr { + for _, del := range wrapper.Delegations { + if del.DelegatorAddress == delegatorAddr { + sourceUndelegations = del.Undelegations + } + } + } + } + if len(sourceUndelegations) != 0 { + t.Fatalf("expected all locked tokens consumed, remaining %+v", sourceUndelegations) + } +} + +func TestBatchDelegateSameValidatorTwice(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + config.MinDelegation100Epoch = big.NewInt(100) + + msg := staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + } + + ws, balance, _, err := VerifyAndBatchDelegateFromMsg( + makeStateDBForStake(t), epoch, &msg, nil, config, + ) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if balance.Cmp(fifteenKOnes) != 0 { + t.Fatalf("expected liquid deduct 15k, got %v", balance) + } + if len(ws) != 1 { + t.Fatalf("expected single wrapper, got %d", len(ws)) + } + found := false + for _, del := range ws[0].Delegations { + if del.DelegatorAddress == delegatorAddr { + found = true + if del.Amount.Cmp(fifteenKOnes) != 0 { + t.Fatalf("expected combined delegation 15k, got %v", del.Amount) + } + } + } + if !found { + t.Fatal("missing combined delegation") + } +} + +func TestRedelegationSkipsSameEpochUndelegation(t *testing.T) { + epoch := big.NewInt(10) + sdb := makeStateForRedelegateCornerCases(t, validatorAddr, []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: new(big.Int).Set(tenKOnes), epoch: epoch}, // same epoch: not eligible + }) + // Leave almost no liquid balance so same-epoch locked funds cannot silently fund the stake. + sdb.SetBalance(delegatorAddr, big.NewInt(0)) + + w, err := sdb.ValidatorWrapper(validatorAddr, false, false) + if err != nil { + t.Fatal(err) + } + delegationIndex := []staking.DelegationIndex{{ + ValidatorAddress: validatorAddr, + Index: uint64(len(w.Delegations) - 1), + BlockNum: big.NewInt(100), + }} + + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + config.RedelegationEpoch = epoch + config.MinDelegation100Epoch = big.NewInt(100) + + msg := staking.Delegate{ + DelegatorAddress: delegatorAddr, + ValidatorAddress: validatorAddr, + Amount: new(big.Int).Set(fiveKOnes), + } + _, _, _, err = VerifyAndDelegateFromMsg(sdb, epoch, &msg, delegationIndex, config) + if assErr := assertError(err, errInsufficientBalanceForStake); assErr != nil { + t.Fatal(assErr) + } +} + +func TestUndelegateAllRequiresStakingV2(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = big.NewInt(10000000) + + _, _, err := VerifyAndUndelegateAllFromMsg( + makeDefaultStateForUndelegate(t), + epoch, + &staking.UndelegateAll{DelegatorAddress: delegatorAddr}, + []staking.DelegationIndex{{ValidatorAddress: validatorAddr, Index: 1, BlockNum: big.NewInt(100)}}, + makeFakeChainContextForStake(), + config, + ) + if assErr := assertError(err, errors.New("undelegate all is only available in StakingV2 epoch")); assErr != nil { + t.Fatal(assErr) + } +} + +func TestBatchUndelegateSameValidatorTwice(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + + msg := staking.BatchUndelegate{ + DelegatorAddress: delegatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fiveKOnes)}, + }, + } + ws, err := VerifyAndBatchUndelegateFromMsg(makeDefaultStateForUndelegate(t), epoch, &msg, config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(ws) != 1 { + t.Fatalf("expected 1 wrapper, got %d", len(ws)) + } + for _, del := range ws[0].Delegations { + if del.DelegatorAddress != delegatorAddr { + continue + } + // 20k - 5k prior - 5k - 5k = 5k active; undelegation 5k+5k+5k = 15k + if del.Amount.Cmp(fiveKOnes) != 0 { + t.Fatalf("active amount = %s, want 5k", del.Amount) + } + if len(del.Undelegations) != 1 || del.Undelegations[0].Amount.Cmp(fifteenKOnes) != 0 { + t.Fatalf("undelegation = %+v, want 15k merged", del.Undelegations) + } + } +} + +func TestBatchUndelegateSelfDelegationInactive(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + + msg := staking.BatchUndelegate{ + DelegatorAddress: validatorAddr, + Undelegations: []staking.UndelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(fifteenKOnes)}, + }, + } + ws, err := VerifyAndBatchUndelegateFromMsg(makeDefaultStateForUndelegate(t), epoch, &msg, config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(ws) != 1 { + t.Fatalf("expected 1 wrapper, got %d", len(ws)) + } + if ws[0].Status != effective.Inactive { + t.Fatalf("expected Inactive status, got %v", ws[0].Status) + } +} + +func TestBatchDelegateTooLarge(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + config.MinDelegation100Epoch = big.NewInt(100) + + actions := make([]staking.DelegationAction, staking.MaxBatchStakingActions+1) + for i := range actions { + actions[i] = staking.DelegationAction{ + ValidatorAddress: validatorAddr, + Amount: new(big.Int).Set(fiveKOnes), + } + } + msg := staking.BatchDelegate{DelegatorAddress: delegatorAddr, Delegations: actions} + _, _, _, err := VerifyAndBatchDelegateFromMsg(makeStateDBForStake(t), epoch, &msg, nil, config) + if assErr := assertError(err, staking.ErrBatchTooLarge); assErr != nil { + t.Fatal(assErr) + } +} + +func TestBatchUndelegateTooLarge(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + + actions := make([]staking.UndelegationAction, staking.MaxBatchStakingActions+1) + for i := range actions { + actions[i] = staking.UndelegationAction{ + ValidatorAddress: validatorAddr, + Amount: big.NewInt(1), + } + } + msg := staking.BatchUndelegate{DelegatorAddress: delegatorAddr, Undelegations: actions} + _, err := VerifyAndBatchUndelegateFromMsg(makeDefaultStateForUndelegate(t), epoch, &msg, config) + if assErr := assertError(err, staking.ErrBatchTooLarge); assErr != nil { + t.Fatal(assErr) + } +} + +func TestBatchDelegateMaxTotalDelegation(t *testing.T) { + epoch := big.NewInt(defaultEpoch) + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + config.MinDelegation100Epoch = big.NewInt(100) + + sdb := makeStateDBForStake(t) + w, err := sdb.ValidatorWrapper(validatorAddr, false, true) + if err != nil { + t.Fatal(err) + } + current := w.TotalDelegation() + w.MaxTotalDelegation = new(big.Int).Add(current, fiveKOnes) + if err := sdb.UpdateValidatorWrapper(validatorAddr, w); err != nil { + t.Fatal(err) + } + sdb.IntermediateRoot(true) + + msg := staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, + }, + } + _, _, _, err = VerifyAndBatchDelegateFromMsg(sdb, epoch, &msg, nil, config) + if err == nil { + t.Fatal("expected max total delegation error") + } +} + +func TestBatchDelegateMixedLiquidAndLocked(t *testing.T) { + epoch := big.NewInt(10) + oldEpoch := big.NewInt(5) + sdb := makeStateForRedelegateCornerCases(t, validatorAddr, []struct { + amount *big.Int + epoch *big.Int + }{ + {amount: new(big.Int).Set(fiveKOnes), epoch: oldEpoch}, + }) + sdb.SetBalance(delegatorAddr, new(big.Int).Set(fiveKOnes)) + + w, err := sdb.ValidatorWrapper(validatorAddr, false, false) + if err != nil { + t.Fatal(err) + } + delegationIndex := []staking.DelegationIndex{{ + ValidatorAddress: validatorAddr, + Index: uint64(len(w.Delegations) - 1), + BlockNum: big.NewInt(100), + }} + config := ¶ms.ChainConfig{} + config.StakingV2Epoch = epoch + config.RedelegationEpoch = epoch + config.MinDelegation100Epoch = big.NewInt(100) + + msg := staking.BatchDelegate{ + DelegatorAddress: delegatorAddr, + Delegations: []staking.DelegationAction{ + {ValidatorAddress: validatorAddr, Amount: new(big.Int).Set(tenKOnes)}, // 5k locked + 5k liquid + }, + } + _, balance, fromLocked, err := VerifyAndBatchDelegateFromMsg(sdb, epoch, &msg, delegationIndex, config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if balance.Cmp(fiveKOnes) != 0 { + t.Fatalf("liquid deduct = %s, want 5k", balance) + } + if fromLocked[validatorAddr].Cmp(fiveKOnes) != 0 { + t.Fatalf("locked used = %s, want 5k", fromLocked[validatorAddr]) + } +} diff --git a/core/state/journal.go b/core/state/journal.go index c5e3f743e7..c301eac59a 100644 --- a/core/state/journal.go +++ b/core/state/journal.go @@ -158,6 +158,7 @@ func (v validatorWrapperChange) dirtied() *common.Address { // revert undoes the changes introduced by this journal entry. func (v validatorWrapperChange) revert(s *DB) { s.stateValidators[*(v.address)] = v.prev + s.MarkValidatorWrapperDirty(*(v.address)) } func (ch createObjectChange) revert(s *DB) { diff --git a/core/state/statedb.go b/core/state/statedb.go index fb5d1710de..9693e2b430 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -93,6 +93,7 @@ type DB struct { stateObjectsDirty map[common.Address]struct{} // State objects modified in the current execution stateObjectsDestruct map[common.Address]struct{} // State objects destructed in the block stateValidators map[common.Address]*stk.ValidatorWrapper + stateValidatorsDirty map[common.Address]struct{} // Cached wrappers that need a write on Finalise // DB error. // State objects are used by the consensus core and VM which are @@ -163,6 +164,7 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*DB, error) { stateObjectsDirty: make(map[common.Address]struct{}), stateObjectsDestruct: make(map[common.Address]struct{}), stateValidators: make(map[common.Address]*stk.ValidatorWrapper), + stateValidatorsDirty: make(map[common.Address]struct{}), logs: make(map[common.Hash][]*types2.Log), preimages: make(map[common.Hash][]byte), journal: newJournal(), @@ -224,6 +226,7 @@ func (db *DB) Reset(root common.Hash) error { db.stateObjectsPending = make(map[common.Address]struct{}) db.stateObjectsDirty = make(map[common.Address]struct{}) db.stateValidators = make(map[common.Address]*stk.ValidatorWrapper) + db.stateValidatorsDirty = make(map[common.Address]struct{}) db.thash = common.Hash{} db.bhash = common.Hash{} db.ethTxHash = common.Hash{} @@ -781,6 +784,7 @@ func (db *DB) Copy() *DB { stateObjectsDirty: make(map[common.Address]struct{}, len(db.journal.dirties)), stateObjectsDestruct: make(map[common.Address]struct{}, len(db.stateObjectsDestruct)), stateValidators: make(map[common.Address]*stk.ValidatorWrapper), + stateValidatorsDirty: make(map[common.Address]struct{}, len(db.stateValidatorsDirty)), refund: db.refund, logs: make(map[common.Hash][]*types2.Log, len(db.logs)), logSize: db.logSize, @@ -824,10 +828,17 @@ func (db *DB) Copy() *DB { for addr := range db.stateObjectsDestruct { state.stateObjectsDestruct[addr] = struct{}{} } + // Deep-copy all cached validator wrappers and preserve dirty flags. for addr, wrapper := range db.stateValidators { + if wrapper == nil { + continue + } copied := staketest.CopyValidatorWrapper(*wrapper) state.stateValidators[addr] = &copied } + for addr := range db.stateValidatorsDirty { + state.stateValidatorsDirty[addr] = struct{}{} + } for hash, logs := range db.logs { cpy := make([]*types2.Log, len(logs)) for i, l := range logs { @@ -912,16 +923,22 @@ func (db *DB) GetRefund() uint64 { // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. func (db *DB) Finalise(deleteEmptyObjects bool) { - // Commit validator changes in cache to stateObjects - // TODO: remove validator cache after commit - for addr, wrapper := range db.stateValidators { + // Persist dirty validator wrappers into account code. + remainingDirty := make(map[common.Address]struct{}) + for addr := range db.stateValidatorsDirty { + wrapper, ok := db.stateValidators[addr] + if !ok || wrapper == nil { + continue + } if err := db.UpdateValidatorWrapper(addr, wrapper); err != nil { utils.Logger().Warn().Err(err). Str("name", wrapper.Name). Str("addr", addr.String()). Msg("Unable to update the validator wrapper on the finalize") + remainingDirty[addr] = struct{}{} } } + db.stateValidatorsDirty = remainingDirty addressesToPrefetch := make([][]byte, 0, len(db.journal.dirties)) for addr := range db.journal.dirties { obj, exist := db.stateObjects[addr] @@ -1297,6 +1314,14 @@ func (db *DB) CachedValidatorAddresses() []common.Address { return addrs } +// MarkValidatorWrapperDirty marks the cached validator wrapper for write on Finalise. +func (db *DB) MarkValidatorWrapperDirty(addr common.Address) { + if db.stateValidatorsDirty == nil { + db.stateValidatorsDirty = make(map[common.Address]struct{}) + } + db.stateValidatorsDirty[addr] = struct{}{} +} + // ValidatorWrapper retrieves the existing validator in the cache, if sendOriginal // else it will return a copy of the wrapper - which needs to be explicitly committed // with UpdateValidatorWrapper. @@ -1376,6 +1401,7 @@ func (db *DB) UpdateValidatorWrapper( db.SetCode(addr, by, true) // update cache db.stateValidators[addr] = val + db.MarkValidatorWrapperDirty(addr) return nil } @@ -1468,6 +1494,7 @@ func (db *DB) AddReward( return nil } + db.MarkValidatorWrapperDirty(snapshot.Address) rewardPool := big.NewInt(0).Set(reward) curValidator.BlockReward.Add(curValidator.BlockReward, reward) // Payout commission diff --git a/core/state/validator_dirty_test.go b/core/state/validator_dirty_test.go new file mode 100644 index 0000000000..237a84aa88 --- /dev/null +++ b/core/state/validator_dirty_test.go @@ -0,0 +1,180 @@ +package state + +import ( + "bytes" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/harmony-one/harmony/core/rawdb" + "github.com/harmony-one/harmony/numeric" + staketest "github.com/harmony-one/harmony/staking/types/test" +) + +func TestFinaliseWritesOnlyDirtyValidators(t *testing.T) { + db, _ := New(common.Hash{}, NewDatabase(rawdb.NewMemoryDatabase()), nil) + + cleanAddr := common.BytesToAddress([]byte("validator-clean")) + dirtyAddr := common.BytesToAddress([]byte("validator-dirty")) + + clean := staketest.GetDefaultValidatorWrapper() + clean.Address = cleanAddr + clean.Delegations[0].DelegatorAddress = cleanAddr + dirty := staketest.GetDefaultValidatorWrapper() + dirty.Address = dirtyAddr + dirty.Delegations[0].DelegatorAddress = dirtyAddr + + if err := db.UpdateValidatorWrapper(cleanAddr, &clean); err != nil { + t.Fatalf("UpdateValidatorWrapper clean: %v", err) + } + if err := db.UpdateValidatorWrapper(dirtyAddr, &dirty); err != nil { + t.Fatalf("UpdateValidatorWrapper dirty: %v", err) + } + db.Finalise(false) + + cleanCodeBefore := append([]byte(nil), db.GetCode(cleanAddr)...) + dirtyCodeBefore := append([]byte(nil), db.GetCode(dirtyAddr)...) + if len(cleanCodeBefore) == 0 || len(dirtyCodeBefore) == 0 { + t.Fatal("expected validator code to be present after finalize") + } + + if _, err := db.ValidatorWrapper(cleanAddr, true, false); err != nil { + t.Fatalf("load clean: %v", err) + } + dirtyWrapper, err := db.ValidatorWrapper(dirtyAddr, true, false) + if err != nil { + t.Fatalf("load dirty: %v", err) + } + if _, ok := db.stateValidatorsDirty[cleanAddr]; ok { + t.Fatal("read-only load marked clean validator dirty") + } + if _, ok := db.stateValidatorsDirty[dirtyAddr]; ok { + t.Fatal("read-only load marked dirty validator dirty") + } + + dirtyWrapper.BlockReward = big.NewInt(12345) + db.MarkValidatorWrapperDirty(dirtyAddr) + + db.Finalise(false) + + if !bytes.Equal(db.GetCode(cleanAddr), cleanCodeBefore) { + t.Fatal("clean validator was re-encoded on finalize") + } + if bytes.Equal(db.GetCode(dirtyAddr), dirtyCodeBefore) { + t.Fatal("dirty validator was not written on finalize") + } + if _, ok := db.stateValidatorsDirty[dirtyAddr]; ok { + t.Fatal("successful finalize left validator dirty") + } + + got, err := db.ValidatorWrapper(dirtyAddr, true, false) + if err != nil { + t.Fatalf("reload dirty: %v", err) + } + if got.BlockReward.Cmp(big.NewInt(12345)) != 0 { + t.Fatalf("dirty BlockReward = %v, want 12345", got.BlockReward) + } +} + +func TestAddRewardMarksValidatorDirty(t *testing.T) { + db, _ := New(common.Hash{}, NewDatabase(rawdb.NewMemoryDatabase()), nil) + + addr := common.BytesToAddress([]byte("validator-reward")) + wrapper := staketest.GetDefaultValidatorWrapper() + wrapper.Address = addr + wrapper.Delegations[0].DelegatorAddress = addr + if err := db.UpdateValidatorWrapper(addr, &wrapper); err != nil { + t.Fatalf("UpdateValidatorWrapper: %v", err) + } + db.Finalise(false) + + snapshot := staketest.GetDefaultValidatorWrapper() + snapshot.Address = addr + snapshot.Delegations[0].DelegatorAddress = addr + shares := map[common.Address]numeric.Dec{ + addr: numeric.NewDec(1), + } + if err := db.AddReward(&snapshot, big.NewInt(1000), shares); err != nil { + t.Fatalf("AddReward: %v", err) + } + if _, ok := db.stateValidatorsDirty[addr]; !ok { + t.Fatal("AddReward did not mark validator dirty") + } +} + +func TestCopyPreservesValidatorCacheAndDirtyFlags(t *testing.T) { + db, _ := New(common.Hash{}, NewDatabase(rawdb.NewMemoryDatabase()), nil) + + cleanAddr := common.BytesToAddress([]byte("copy-clean")) + dirtyAddr := common.BytesToAddress([]byte("copy-dirty")) + + clean := staketest.GetDefaultValidatorWrapper() + clean.Address = cleanAddr + clean.Delegations[0].DelegatorAddress = cleanAddr + dirty := staketest.GetDefaultValidatorWrapper() + dirty.Address = dirtyAddr + dirty.Delegations[0].DelegatorAddress = dirtyAddr + + if err := db.UpdateValidatorWrapper(cleanAddr, &clean); err != nil { + t.Fatalf("UpdateValidatorWrapper clean: %v", err) + } + if err := db.UpdateValidatorWrapper(dirtyAddr, &dirty); err != nil { + t.Fatalf("UpdateValidatorWrapper dirty: %v", err) + } + db.Finalise(false) + + if _, err := db.ValidatorWrapper(cleanAddr, true, false); err != nil { + t.Fatalf("load clean: %v", err) + } + dirtyWrapper, err := db.ValidatorWrapper(dirtyAddr, true, false) + if err != nil { + t.Fatalf("load dirty: %v", err) + } + dirtyWrapper.BlockReward = big.NewInt(7) + db.MarkValidatorWrapperDirty(dirtyAddr) + + copied := db.Copy() + if _, ok := copied.stateValidators[cleanAddr]; !ok { + t.Fatal("copy missing clean validator wrapper") + } + if _, ok := copied.stateValidators[dirtyAddr]; !ok { + t.Fatal("copy missing dirty validator wrapper") + } + if _, ok := copied.stateValidatorsDirty[dirtyAddr]; !ok { + t.Fatal("copy missing dirty flag") + } + if _, ok := copied.stateValidatorsDirty[cleanAddr]; ok { + t.Fatal("copy marked clean validator dirty") + } + if copied.stateValidators[dirtyAddr].BlockReward.Cmp(big.NewInt(7)) != 0 { + t.Fatal("copy did not preserve dirty wrapper mutation") + } + if copied.stateValidators[dirtyAddr] == db.stateValidators[dirtyAddr] { + t.Fatal("copy shared wrapper pointer with original") + } +} + +func TestCachedValidatorAddressesSurvivesFinaliseAndCopy(t *testing.T) { + db, _ := New(common.Hash{}, NewDatabase(rawdb.NewMemoryDatabase()), nil) + + addr := common.BytesToAddress([]byte("cached-validator")) + wrapper := staketest.GetDefaultValidatorWrapper() + wrapper.Address = addr + wrapper.Delegations[0].DelegatorAddress = addr + db.SetValidatorFlag(addr) + if err := db.UpdateValidatorWrapper(addr, &wrapper); err != nil { + t.Fatalf("UpdateValidatorWrapper: %v", err) + } + db.Finalise(false) + + addrs := db.CachedValidatorAddresses() + if len(addrs) != 1 || addrs[0] != addr { + t.Fatalf("CachedValidatorAddresses after Finalise = %v, want [%s]", addrs, addr.Hex()) + } + + copied := db.Copy() + copiedAddrs := copied.CachedValidatorAddresses() + if len(copiedAddrs) != 1 || copiedAddrs[0] != addr { + t.Fatalf("CachedValidatorAddresses on Copy = %v, want [%s]", copiedAddrs, addr.Hex()) + } +} diff --git a/core/state_processor.go b/core/state_processor.go index dc10469945..c2466916d7 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -554,6 +554,7 @@ func MayShardReduction(bc ChainContext, statedb *state.DB, header *block.Header) // once epoch X begins, they can terminate servers from shards 2 and 3. if curShard >= uint64(nextNumShards) || curShard != nextShard { validator.Status = effective.Inactive + statedb.MarkValidatorWrapperDirty(address) break } } diff --git a/core/state_transition.go b/core/state_transition.go index 1decefd18b..8927ab2111 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -18,6 +18,7 @@ package core import ( "fmt" + "math" "math/big" "github.com/ethereum/go-ethereum/common" @@ -352,6 +353,29 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) { if err != nil { return 0, err } + var batchDirective stakingTypes.Directive + hasBatchExtraGas := false + switch msg.Type() { + case types.BatchDelegate: + batchDirective = stakingTypes.DirectiveBatchDelegate + hasBatchExtraGas = true + case types.BatchUndelegate: + batchDirective = stakingTypes.DirectiveBatchUndelegate + hasBatchExtraGas = true + case types.UndelegateAll: + batchDirective = stakingTypes.DirectiveUndelegateAll + hasBatchExtraGas = true + } + if hasBatchExtraGas { + extra, extraErr := stakingTypes.ExtraGasForStakingDirective(batchDirective, st.data) + if extraErr != nil { + return 0, extraErr + } + if gas > math.MaxUint64-extra { + return 0, vm.ErrGasUintOverflow + } + gas += extra + } if err = st.useGas(gas); err != nil { return 0, err } @@ -420,6 +444,45 @@ func (st *StateTransition) StakingTransitionDb() (usedGas uint64, err error) { return 0, errInvalidSigner } err = st.evm.Context.CollectRewards(st.evm.StateDB, nil, stkMsg) + case types.BatchDelegate: + if !st.evm.ChainConfig().IsStakingV2(st.evm.Context.EpochNumber) { + return 0, errors.New("batch delegation is only available in StakingV2 epoch") + } + stkMsg := &stakingTypes.BatchDelegate{} + if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil { + return 0, err + } + utils.Logger().Info().Msgf("[DEBUG STAKING] staking type: %s, gas: %d, txn: %+v", msg.Type(), gas, stkMsg) + if msg.From() != stkMsg.DelegatorAddress { + return 0, errInvalidSigner + } + err = st.evm.Context.BatchDelegate(st.evm.StateDB, nil, stkMsg) + case types.BatchUndelegate: + if !st.evm.ChainConfig().IsStakingV2(st.evm.Context.EpochNumber) { + return 0, errors.New("batch undelegation is only available in StakingV2 epoch") + } + stkMsg := &stakingTypes.BatchUndelegate{} + if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil { + return 0, err + } + utils.Logger().Info().Msgf("[DEBUG STAKING] staking type: %s, gas: %d, txn: %+v", msg.Type(), gas, stkMsg) + if msg.From() != stkMsg.DelegatorAddress { + return 0, errInvalidSigner + } + err = st.evm.Context.BatchUndelegate(st.evm.StateDB, nil, stkMsg) + case types.UndelegateAll: + if !st.evm.ChainConfig().IsStakingV2(st.evm.Context.EpochNumber) { + return 0, errors.New("undelegate all is only available in StakingV2 epoch") + } + stkMsg := &stakingTypes.UndelegateAll{} + if err = rlp.DecodeBytes(msg.Data(), stkMsg); err != nil { + return 0, err + } + utils.Logger().Info().Msgf("[DEBUG STAKING] staking type: %s, gas: %d, txn: %+v", msg.Type(), gas, stkMsg) + if msg.From() != stkMsg.DelegatorAddress { + return 0, errInvalidSigner + } + err = st.evm.Context.UndelegateAll(st.evm.StateDB, nil, stkMsg) default: return 0, stakingTypes.ErrInvalidStakingKind } diff --git a/core/tx_pool.go b/core/tx_pool.go index 53801fad94..b1cabd929d 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -38,6 +38,7 @@ import ( hmyCommon "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/shard" + stakingabi "github.com/harmony-one/harmony/staking" staking "github.com/harmony-one/harmony/staking/types" ) @@ -754,18 +755,31 @@ func (pool *TxPool) validateTx(tx types.PoolTransaction, local bool) error { return err } stakingTx, isStakingTx := tx.(*staking.StakingTransaction) + isPrecompileDelegate := pool.isStakingPrecompileDelegate(tx, from) if !isStakingTx || (isStakingTx && stakingTx.StakingType() != staking.DirectiveDelegate) { - if pool.currentState.GetBalance(from).Cmp(cost) < 0 { - return errors.Wrapf( - ErrInsufficientFunds, - "current shard-id: %d", - pool.chain.CurrentBlock().ShardID(), - ) + if !isPrecompileDelegate { + if pool.currentState.GetBalance(from).Cmp(cost) < 0 { + return errors.Wrapf( + ErrInsufficientFunds, + "current shard-id: %d", + pool.chain.CurrentBlock().ShardID(), + ) + } } } intrGas := uint64(0) if isStakingTx { intrGas, err = vm.IntrinsicGas(tx.Data(), false, pool.homestead, pool.istanbul, stakingTx.StakingType() == staking.DirectiveCreateValidator, pool.isEIP3860) + if err == nil { + extra, extraErr := staking.ExtraGasForStakingDirective(stakingTx.StakingType(), tx.Data()) + if extraErr != nil { + return extraErr + } + if intrGas > ^uint64(0)-extra { + return errors.New("staking batch gas overflow") + } + intrGas += extra + } } else { intrGas, err = vm.IntrinsicGas(tx.Data(), tx.To() == nil, pool.homestead, pool.istanbul, false, pool.isEIP3860) } @@ -779,7 +793,86 @@ func (pool *TxPool) validateTx(tx types.PoolTransaction, local bool) error { if isStakingTx { return pool.validateStakingTx(stakingTx) } - return nil + return pool.validateStakingPrecompileCall(tx, from) +} + +func (pool *TxPool) isStakingPrecompileDelegate(tx types.PoolTransaction, from common.Address) bool { + if pool.chain.CurrentBlock().ShardID() != shard.BeaconChainShardID { + return false + } + if !pool.chainconfig.IsStakingPrecompile(pool.pendingEpoch()) { + return false + } + to := tx.To() + if to == nil || *to != stakingabi.PrecompileAddress { + return false + } + stakeMsg, err := stakingabi.ParseStakeMsg(from, tx.Data()) + if err != nil { + return false + } + _, ok := stakeMsg.(*staking.Delegate) + return ok +} + +func (pool *TxPool) validateStakingPrecompileCall(tx types.PoolTransaction, from common.Address) error { + if pool.chain.CurrentBlock().ShardID() != shard.BeaconChainShardID { + return nil + } + if !pool.chainconfig.IsStakingPrecompile(pool.pendingEpoch()) { + return nil + } + to := tx.To() + if to == nil || *to != stakingabi.PrecompileAddress { + return nil + } + stakeMsg, err := stakingabi.ParseStakeMsg(from, tx.Data()) + if err != nil { + return err + } + + b32, _ := hmyCommon.AddressToBech32(from) + switch msg := stakeMsg.(type) { + case *staking.Delegate: + chain, ok := pool.chain.(ChainContext) + if !ok { + utils.Logger().Debug().Msg("Missing chain context in txPool") + return nil + } + delegations, err := chain.ReadDelegationsByDelegator(msg.DelegatorAddress) + if err != nil { + return err + } + pendingEpoch := pool.pendingEpoch() + _, delegateAmt, _, err := VerifyAndDelegateFromMsg( + pool.currentState, pendingEpoch, msg, delegations, pool.chainconfig) + if err != nil { + return err + } + gasAmt := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.GasLimit())) + totalAmt := new(big.Int).Add(delegateAmt, gasAmt) + if bal := pool.currentState.GetBalance(from); bal.Cmp(totalAmt) < 0 { + return fmt.Errorf("not enough balance for delegation: %v < %v", bal, delegateAmt) + } + return nil + case *staking.Undelegate: + _, err := VerifyAndUndelegateFromMsg(pool.currentState, pool.pendingEpoch(), msg) + return err + case *staking.CollectRewards: + chain, ok := pool.chain.(ChainContext) + if !ok { + utils.Logger().Debug().Msg("Missing chain context in txPool") + return nil + } + delegations, err := chain.ReadDelegationsByDelegator(msg.DelegatorAddress) + if err != nil { + return err + } + _, _, err = VerifyAndCollectRewardsFromDelegation(pool.currentState, delegations) + return err + default: + return errors.WithMessagef(ErrInvalidSender, "staking precompile sender is %s", b32) + } } // validateStakingTx checks the staking message based on the staking directive @@ -907,6 +1000,87 @@ func (pool *TxPool) validateStakingTx(tx *staking.StakingTransaction) error { _, _, err = VerifyAndCollectRewardsFromDelegation(pool.currentState, delegations) return err + case staking.DirectiveBatchDelegate: + pendingEpoch := pool.pendingEpoch() + if !pool.chainconfig.IsStakingV2(pendingEpoch) { + return errors.New("batch delegation is only available in StakingV2 epoch") + } + msg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveBatchDelegate) + if err != nil { + return err + } + stkMsg, ok := msg.(*staking.BatchDelegate) + if !ok { + return ErrInvalidMsgForStakingDirective + } + if from != stkMsg.DelegatorAddress { + return errors.WithMessagef(ErrInvalidSender, "staking transaction sender is %s", b32) + } + chain, ok := pool.chain.(ChainContext) + if !ok { + utils.Logger().Debug().Msg("Missing chain context in txPool") + return nil + } + delegations, err := chain.ReadDelegationsByDelegator(stkMsg.DelegatorAddress) + if err != nil { + return err + } + _, delegateAmt, _, err := VerifyAndBatchDelegateFromMsg( + pool.currentState, pendingEpoch, stkMsg, delegations, pool.chainconfig) + if err != nil { + return err + } + gasAmt := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.GasLimit())) + totalAmt := new(big.Int).Add(delegateAmt, gasAmt) + if bal := pool.currentState.GetBalance(from); bal.Cmp(totalAmt) < 0 { + return fmt.Errorf("not enough balance for batch delegation: %v < %v", bal, delegateAmt) + } + return nil + case staking.DirectiveBatchUndelegate: + pendingEpoch := pool.pendingEpoch() + if !pool.chainconfig.IsStakingV2(pendingEpoch) { + return errors.New("batch undelegation is only available in StakingV2 epoch") + } + msg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveBatchUndelegate) + if err != nil { + return err + } + stkMsg, ok := msg.(*staking.BatchUndelegate) + if !ok { + return ErrInvalidMsgForStakingDirective + } + if from != stkMsg.DelegatorAddress { + return errors.WithMessagef(ErrInvalidSender, "staking transaction sender is %s", b32) + } + _, err = VerifyAndBatchUndelegateFromMsg(pool.currentState, pendingEpoch, stkMsg, pool.chainconfig) + return err + case staking.DirectiveUndelegateAll: + pendingEpoch := pool.pendingEpoch() + if !pool.chainconfig.IsStakingV2(pendingEpoch) { + return errors.New("undelegate all is only available in StakingV2 epoch") + } + msg, err := staking.RLPDecodeStakeMsg(tx.Data(), staking.DirectiveUndelegateAll) + if err != nil { + return err + } + stkMsg, ok := msg.(*staking.UndelegateAll) + if !ok { + return ErrInvalidMsgForStakingDirective + } + if from != stkMsg.DelegatorAddress { + return errors.WithMessagef(ErrInvalidSender, "staking transaction sender is %s", b32) + } + chain, ok := pool.chain.(ChainContext) + if !ok { + utils.Logger().Debug().Msg("Missing chain context in txPool") + return nil + } + delegations, err := chain.ReadDelegationsByDelegator(stkMsg.DelegatorAddress) + if err != nil { + return err + } + _, _, err = VerifyAndUndelegateAllFromMsg(pool.currentState, pendingEpoch, stkMsg, delegations, chain, pool.chainconfig) + return err default: return staking.ErrInvalidStakingKind } diff --git a/core/tx_pool_test.go b/core/tx_pool_test.go index f1e7ce5ce7..826c1d283d 100644 --- a/core/tx_pool_test.go +++ b/core/tx_pool_test.go @@ -33,12 +33,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/event" - bls_core "github.com/harmony-one/bls/ffi/go/bls" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/common/denominations" "github.com/harmony-one/harmony/core/state" "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" chain2 "github.com/harmony-one/harmony/internal/chain" "github.com/harmony-one/harmony/internal/params" diff --git a/core/types/transaction.go b/core/types/transaction.go index 1364ccfac5..bc0a89dd3a 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -63,12 +63,22 @@ const ( Delegate Undelegate CollectRewards + BatchDelegate + BatchUndelegate + UndelegateAll ) // StakingTypeMap is the map from staking type to transactionType -var StakingTypeMap = map[staking.Directive]TransactionType{staking.DirectiveCreateValidator: StakeCreateVal, - staking.DirectiveEditValidator: StakeEditVal, staking.DirectiveDelegate: Delegate, - staking.DirectiveUndelegate: Undelegate, staking.DirectiveCollectRewards: CollectRewards} +var StakingTypeMap = map[staking.Directive]TransactionType{ + staking.DirectiveCreateValidator: StakeCreateVal, + staking.DirectiveEditValidator: StakeEditVal, + staking.DirectiveDelegate: Delegate, + staking.DirectiveUndelegate: Undelegate, + staking.DirectiveCollectRewards: CollectRewards, + staking.DirectiveBatchDelegate: BatchDelegate, + staking.DirectiveBatchUndelegate: BatchUndelegate, + staking.DirectiveUndelegateAll: UndelegateAll, +} // InternalTransaction defines the common interface for harmony and ethereum transactions. type InternalTransaction interface { diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 59ec9549eb..94fef3b22b 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -25,6 +25,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" + "github.com/harmony-one/harmony/accounts/abi" "github.com/harmony-one/harmony/internal/params" "github.com/ethereum/go-ethereum/common/math" @@ -295,14 +296,36 @@ func RunPrecompiledContract(p WriteCapablePrecompiledContract, evm *EVM, contrac } gasCost, err := p.RequiredGas(evm, contract, input) if err != nil { - return nil, 0, err + return wrapWritePrecompileError(evm, p, nil, 0, err) } if suppliedGas < gasCost { return nil, 0, ErrOutOfGas } suppliedGas -= gasCost output, err := p.RunWriteCapable(evm, contract, input) - return output, suppliedGas, err + return wrapWritePrecompileError(evm, p, output, suppliedGas, err) +} + +func wrapWritePrecompileError( + evm *EVM, p WriteCapablePrecompiledContract, output []byte, remainingGas uint64, err error, +) ([]byte, uint64, error) { + if err == nil || err == ErrExecutionReverted { + return output, remainingGas, err + } + // Only the staking precompile switches to ABI Error(string) reverts after StakingV2. + // Other write-capable precompiles keep their prior exceptional-error behavior. + if _, ok := p.(*stakingPrecompile); !ok { + return output, remainingGas, err + } + if evm == nil || evm.ChainConfig() == nil || + !evm.ChainConfig().IsStakingV2(evm.Context.EpochNumber) { + return output, remainingGas, err + } + revertData, packErr := abi.PackRevert(err.Error()) + if packErr != nil { + return output, remainingGas, err + } + return revertData, remainingGas, ErrExecutionReverted } // ECRECOVER implemented as a native contract. diff --git a/core/vm/evm.go b/core/vm/evm.go index 9f64457ab0..80183b9e25 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -61,6 +61,9 @@ type ( DelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.Delegate) error UndelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.Undelegate) error CollectRewardsFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.CollectRewards) error + BatchDelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.BatchDelegate) error + BatchUndelegateFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.BatchUndelegate) error + UndelegateAllFunc func(db StateDB, rosettaTracer RosettaTracer, stakeMsg *stakingTypes.UndelegateAll) error // Used for migrating delegations via the staking precompile //MigrateDelegationsFunc func(db StateDB, migrationMsg *stakingTypes.MigrationMsg) ([]interface{}, error) CalculateMigrationGasFunc func(db StateDB, migrationMsg *stakingTypes.MigrationMsg, homestead bool, istanbul bool, isEIP3860 bool) (uint64, error) @@ -180,6 +183,9 @@ type BlockContext struct { Delegate DelegateFunc Undelegate UndelegateFunc CollectRewards CollectRewardsFunc + BatchDelegate BatchDelegateFunc + BatchUndelegate BatchUndelegateFunc + UndelegateAll UndelegateAllFunc CalculateMigrationGas CalculateMigrationGasFunc ShardID uint32 // Used by staking and cross shard transfer precompile diff --git a/core/vm/precompile_revert_test.go b/core/vm/precompile_revert_test.go new file mode 100644 index 0000000000..460fa53920 --- /dev/null +++ b/core/vm/precompile_revert_test.go @@ -0,0 +1,101 @@ +package vm + +import ( + "errors" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/harmony-one/harmony/accounts/abi" + "github.com/harmony-one/harmony/internal/params" + "github.com/stretchr/testify/require" +) + +func TestWrapWritePrecompileErrorBeforeFork(t *testing.T) { + cfg := *params.TestChainConfig + cfg.StakingV2Epoch = big.NewInt(100) + evm := NewEVM( + BlockContext{EpochNumber: big.NewInt(1)}, + TxContext{}, + nil, + &cfg, + Config{}, + ) + + origErr := errors.New("insufficient balance to stake") + output, gas, err := wrapWritePrecompileError(evm, &stakingPrecompile{}, nil, 42_000, origErr) + require.Equal(t, origErr, err) + require.Equal(t, uint64(42_000), gas) + require.Nil(t, output) +} + +func TestWrapWritePrecompileErrorAfterFork(t *testing.T) { + evm := NewEVM( + BlockContext{EpochNumber: big.NewInt(6)}, + TxContext{}, + nil, + params.LocalnetChainConfig, + Config{}, + ) + + origErr := errors.New("insufficient balance to stake") + output, gas, err := wrapWritePrecompileError(evm, &stakingPrecompile{}, nil, 42_000, origErr) + require.ErrorIs(t, err, ErrExecutionReverted) + require.Equal(t, uint64(42_000), gas) + + reason, unpackErr := abi.UnpackRevert(output) + require.NoError(t, unpackErr) + require.Equal(t, origErr.Error(), reason) +} + +func TestWrapWritePrecompileErrorSkipsNonStaking(t *testing.T) { + evm := NewEVM( + BlockContext{EpochNumber: big.NewInt(6)}, + TxContext{}, + nil, + params.LocalnetChainConfig, + Config{}, + ) + + origErr := errors.New("cross shard transfer failed") + output, gas, err := wrapWritePrecompileError(evm, &crossShardXferPrecompile{}, nil, 42_000, origErr) + require.Equal(t, origErr, err) + require.Equal(t, uint64(42_000), gas) + require.Nil(t, output) +} + +func TestStakingPrecompileAddressMismatchRevertsAfterFork(t *testing.T) { + env := NewEVM(BlockContext{ + CollectRewards: CollectRewardsFn(), + Delegate: DelegateFn(), + Undelegate: UndelegateFn(), + CreateValidator: CreateValidatorFn(), + EditValidator: EditValidatorFn(), + ShardID: 0, + EpochNumber: big.NewInt(6), + CalculateMigrationGas: CalculateMigrationGasFn(), + }, TxContext{}, nil, params.LocalnetChainConfig, Config{}) + + input := []byte{ + 109, 107, 47, 119, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 19, 56, + } + contract := NewContract( + AccountRef(common.HexToAddress("0x1337")), + AccountRef(common.HexToAddress("0x1338")), + nil, + 1_000_000, + ) + p := &stakingPrecompile{} + gas, err := p.RequiredGas(env, contract, input) + require.NoError(t, err) + contract.Gas = gas + 1_000 + + output, remainingGas, err := RunPrecompiledContract(p, env, contract, input, gas+1_000, false) + require.ErrorIs(t, err, ErrExecutionReverted) + require.Equal(t, uint64(1_000), remainingGas) + + reason, unpackErr := abi.UnpackRevert(output) + require.NoError(t, unpackErr) + require.NotEmpty(t, reason) +} diff --git a/crypto/bls/bls.go b/crypto/bls/bls.go index 4b6fe593bc..5f47eb4510 100644 --- a/crypto/bls/bls.go +++ b/crypto/bls/bls.go @@ -5,7 +5,7 @@ import ( "encoding/hex" "math/big" - "github.com/harmony-one/bls/ffi/go/bls" + bls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/pkg/errors" ) diff --git a/crypto/bls/core/core.go b/crypto/bls/core/core.go new file mode 100644 index 0000000000..330770e8a1 --- /dev/null +++ b/crypto/bls/core/core.go @@ -0,0 +1,190 @@ +// Package bls configures the Herumi BLS implementation for Harmony consensus. +package bls + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "unsafe" + + herumi "github.com/herumi/bls-eth-go-binary/bls" +) + +type ( + ID = herumi.ID + + // SecretKey wraps Herumi's key with Harmony-safe cgo boundaries. + SecretKey struct{ herumi.SecretKey } + + // PublicKey wraps Herumi's public key with Harmony-specific operations. + PublicKey struct{ herumi.PublicKey } + + // Sign wraps a Herumi BLS signature. + Sign struct{ herumi.Sign } +) + +const BLS12_381 = herumi.BLS12_381 + +// This is the G1 generator produced by the historical Harmony BLS_SWAP_G +// implementation. Keeping it preserves all existing public keys. +const harmonyGenerator = "e500361ff315734cccd8f9b721ec159995e9e622be17afd41ac2f037a583e81b98c2320e0bf853a8f929e89e3d8ff504" + +func init() { + if err := Init(BLS12_381); err != nil { + panic(err) + } +} + +// Init initializes Herumi with Harmony's consensus-critical group layout, +// serialization, hash-to-curve mode and public-key generator. +func Init(curve int) error { + if err := herumi.Init(curve); err != nil { + return err + } + herumi.SetETHserialization(false) + if err := herumi.SetMapToMode(0); err != nil { + return fmt.Errorf("bls: failed to select Harmony map-to-curve mode: %w", err) + } + if err := herumi.SetETHmode(herumi.EthModeOld); err != nil { + return err + } + raw, err := hex.DecodeString(harmonyGenerator) + if err != nil { + return err + } + var generator PublicKey + if err := generator.Deserialize(raw); err != nil { + return fmt.Errorf("bls: decode Harmony generator: %w", err) + } + if err := herumi.SetGeneratorOfPublicKey(&generator.PublicKey); err != nil { + return fmt.Errorf("bls: install Harmony generator: %w", err) + } + return nil +} + +// GetAddress derives the Harmony address of a BLS public key. +func (pub *PublicKey) GetAddress() [20]byte { + var address [20]byte + if pub == nil { + return address + } + hash := sha256.Sum256(pub.Serialize()) + copy(address[:], hash[:len(address)]) + return address +} + +// GetPublicKey returns the public key corresponding to this secret key. +func (secret *SecretKey) GetPublicKey() *PublicKey { + if secret == nil { + return nil + } + public := secret.SecretKey.GetPublicKey() + if public == nil { + return nil + } + return &PublicKey{PublicKey: *public} +} + +// IsEqual reports whether two secret keys are equal. +func (secret *SecretKey) IsEqual(rhs *SecretKey) bool { + return secret != nil && rhs != nil && secret.SecretKey.IsEqual(&rhs.SecretKey) +} + +// Sign signs a string using Harmony's configured BLS mode. +func (secret *SecretKey) Sign(message string) *Sign { + if secret == nil { + return nil + } + signature := secret.SecretKey.Sign(message) + if signature == nil { + return nil + } + return &Sign{Sign: *signature} +} + +// SignHash copies the hash before crossing the cgo boundary. +func (secret *SecretKey) SignHash(hash []byte) *Sign { + if secret == nil { + return nil + } + signature := secret.SecretKey.SignHash(append([]byte(nil), hash...)) + if signature == nil { + return nil + } + return &Sign{Sign: *signature} +} + +// SignByte copies the message before crossing the cgo boundary. +func (secret *SecretKey) SignByte(message []byte) *Sign { + if secret == nil { + return nil + } + signature := secret.SecretKey.SignByte(append([]byte(nil), message...)) + if signature == nil { + return nil + } + return &Sign{Sign: *signature} +} + +// VerifyHash copies the hash before crossing the cgo boundary. +func (signature *Sign) VerifyHash(public *PublicKey, hash []byte) bool { + if signature == nil || public == nil { + return false + } + return signature.Sign.VerifyHash( + &public.PublicKey, + append([]byte(nil), hash...), + ) +} + +// VerifyByte copies the message before crossing the cgo boundary. +func (signature *Sign) VerifyByte(public *PublicKey, message []byte) bool { + if signature == nil || public == nil { + return false + } + return signature.Sign.VerifyByte( + &public.PublicKey, + append([]byte(nil), message...), + ) +} + +// Verify verifies a string signature. +func (signature *Sign) Verify(public *PublicKey, message string) bool { + if signature == nil || public == nil { + return false + } + return signature.Sign.Verify(&public.PublicKey, message) +} + +// Add adds another public key. +func (pub *PublicKey) Add(rhs *PublicKey) { + if pub != nil && rhs != nil { + pub.PublicKey.Add(&rhs.PublicKey) + } +} + +// IsEqual reports whether two public keys are equal. +func (pub *PublicKey) IsEqual(rhs *PublicKey) bool { + return pub != nil && rhs != nil && pub.PublicKey.IsEqual(&rhs.PublicKey) +} + +// Sub subtracts another BLS public key. +func (pub *PublicKey) Sub(rhs *PublicKey) { + if pub == nil || rhs == nil { + return + } + out := (*herumi.G1)(unsafe.Pointer(&pub.PublicKey)) + herumi.G1Sub(out, out, (*herumi.G1)(unsafe.Pointer(&rhs.PublicKey))) +} + +// Add adds another signature. +func (signature *Sign) Add(rhs *Sign) { + if signature != nil && rhs != nil { + signature.Sign.Add(&rhs.Sign) + } +} + +// IsEqual reports whether two signatures are equal. +func (signature *Sign) IsEqual(rhs *Sign) bool { + return signature != nil && rhs != nil && signature.Sign.IsEqual(&rhs.Sign) +} diff --git a/crypto/bls/core/core_test.go b/crypto/bls/core/core_test.go new file mode 100644 index 0000000000..5c18e6f593 --- /dev/null +++ b/crypto/bls/core/core_test.go @@ -0,0 +1,37 @@ +package bls + +import ( + "encoding/hex" + "testing" +) + +func TestHarmonyWireCompatibility(t *testing.T) { + var secret SecretKey + if err := secret.SetLittleEndian([]byte{1}); err != nil { + t.Fatal(err) + } + + const ( + wantSecret = "0100000000000000000000000000000000000000000000000000000000000000" + wantPublic = "e500361ff315734cccd8f9b721ec159995e9e622be17afd41ac2f037a583e81b98c2320e0bf853a8f929e89e3d8ff504" + wantSign = "c2aba747613499b2e086c3bd9714f6da7159b3cb256d246f1c41dcdd61b00fd608e7caadef2376ed77e68ffc0b486113617d21668e56f930bdee39af0af1fcb42dc7396c5de18af5f2560dc6a11f5e9dd04f9df5e1ac68773d85c8585dcf9c11" + wantHash = "1d730dd8da233d2fccc254b9b3fece52a6f15dd4522f3cd600f14551b5cd76c95ea6ebc7ec077ccc83dead3b2b5b8a12f05b8492119c773c81eb5fb56060f585e711e763f4a85221a5ba72894b87fab619fab4a4c0e478969125c75a9216d093" + ) + + assertHex := func(name string, got []byte, want string) { + t.Helper() + if hex.EncodeToString(got) != want { + t.Fatalf("%s changed:\n got %x\nwant %s", name, got, want) + } + } + + public := secret.GetPublicKey() + hash, err := hex.DecodeString("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f") + if err != nil { + t.Fatal(err) + } + assertHex("secret key", secret.Serialize(), wantSecret) + assertHex("public key", public.Serialize(), wantPublic) + assertHex("message signature", secret.Sign("harmony-bls-upgrade-compatibility").Serialize(), wantSign) + assertHex("hash signature", secret.SignHash(hash).Serialize(), wantHash) +} diff --git a/crypto/bls/mask.go b/crypto/bls/mask.go index 2bd908fd7c..cda255926a 100644 --- a/crypto/bls/mask.go +++ b/crypto/bls/mask.go @@ -1,7 +1,7 @@ package bls import ( - "github.com/harmony-one/bls/ffi/go/bls" + bls "github.com/harmony-one/harmony/crypto/bls/core" lru "github.com/hashicorp/golang-lru" "github.com/pkg/errors" ) @@ -44,7 +44,10 @@ func BytesToBLSPublicKey(bytes []byte) (*bls.PublicKey, error) { return nil, errPubKeyCast } pubKey := &bls.PublicKey{} - err := pubKey.Deserialize(bytes) + // The upstream wrapper passes this buffer to C. Copy it so callers may + // safely provide a slice backed by a Go struct containing pointer fields. + buf := append([]byte(nil), bytes...) + err := pubKey.Deserialize(buf) if err == nil { BLSPubKeyCache.Add(kkey, *pubKey) diff --git a/crypto/bls/mask_test.go b/crypto/bls/mask_test.go index 17770fd668..d920980216 100644 --- a/crypto/bls/mask_test.go +++ b/crypto/bls/mask_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/harmony-one/bls/ffi/go/bls" + bls "github.com/harmony-one/harmony/crypto/bls/core" ) // Test the basic functionality of a BLS multi-sig mask. diff --git a/crypto/vrf/bls/bls_vrf.go b/crypto/vrf/bls/bls_vrf.go index 54e1820c76..e7d273c974 100644 --- a/crypto/vrf/bls/bls_vrf.go +++ b/crypto/vrf/bls/bls_vrf.go @@ -5,7 +5,7 @@ import ( "crypto/sha256" "errors" - "github.com/harmony-one/bls/ffi/go/bls" + bls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/vrf" ) diff --git a/go.mod b/go.mod index c29c096e16..42658cd200 100644 --- a/go.mod +++ b/go.mod @@ -24,11 +24,11 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.3 github.com/harmony-one/abool v1.0.1 - github.com/harmony-one/bls v0.0.6 github.com/harmony-one/taggedrlp v0.1.4 github.com/harmony-one/vdf v0.0.0-20190924175951-620379da8849 github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d + github.com/herumi/bls-eth-go-binary v1.37.0 github.com/ipfs/go-ds-badger v0.3.0 github.com/json-iterator/go v1.1.12 github.com/libp2p/go-libp2p v0.36.2 diff --git a/go.sum b/go.sum index c50ab74642..cfb51d999a 100644 --- a/go.sum +++ b/go.sum @@ -645,8 +645,6 @@ github.com/grpc-ecosystem/grpc-gateway v1.12.1/go.mod h1:8XEsbTttt/W+VvjtQhLACqC github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/harmony-one/abool v1.0.1 h1:SjXLmrr3W8h6lY37gRuWtLiRknUOchnUnsXJWK6Gbm4= github.com/harmony-one/abool v1.0.1/go.mod h1:9sq0PJzb1SqRpKrpEV4Ttvm9WV5uud8sfrsPw3AIBJA= -github.com/harmony-one/bls v0.0.6 h1:KG4q4JwdkPf3DtFvJmAgMRWT6QdY1A/wqN/Qt+S4VaQ= -github.com/harmony-one/bls v0.0.6/go.mod h1:ML9osB/z3hR9WAYZVj7qH+IP6oaPRPmshDbxrQyia7g= github.com/harmony-one/taggedrlp v0.1.4 h1:RZ+qy0VCzT+d/mTfq23gH3an5tSvxOhg6AddLDO6tKw= github.com/harmony-one/taggedrlp v0.1.4/go.mod h1:osO5TRXLKdgCP+oj2J9qfqhywMOOA+4nP5q+o8nDSYA= github.com/harmony-one/vdf v0.0.0-20190924175951-620379da8849 h1:rMY4jLAen3pMTq9KO7kSXzuMaicnOHP5n1MgpA1T6G4= @@ -686,6 +684,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/herumi/bls-eth-go-binary v1.37.0 h1:EaLF+MWndrF3Vbd9VkbG0T9tad3wBbGwh+6kCYcY5QA= +github.com/herumi/bls-eth-go-binary v1.37.0/go.mod h1:luAnRm3OsMQeokhGzpYmc0ZKwawY7o87PUEP11Z7r7U= github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e h1:pIYdhNkDh+YENVNi3gto8n9hAmRxKxoar0iE6BLucjw= github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT0oKJ38PylVfqohClLr3CvDC+Qcg+lhU= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= diff --git a/internal/blsgen/helper.go b/internal/blsgen/helper.go index a1b178a78b..a9856743ed 100644 --- a/internal/blsgen/helper.go +++ b/internal/blsgen/helper.go @@ -5,7 +5,7 @@ import ( "os" "path/filepath" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/multibls" ) diff --git a/internal/blsgen/helper_test.go b/internal/blsgen/helper_test.go index bd5580f00e..03c0bc462a 100644 --- a/internal/blsgen/helper_test.go +++ b/internal/blsgen/helper_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" ) const ( diff --git a/internal/blsgen/kms.go b/internal/blsgen/kms.go index 0a197a54e0..4bc5bf8e5b 100644 --- a/internal/blsgen/kms.go +++ b/internal/blsgen/kms.go @@ -11,7 +11,7 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/kms" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/pkg/errors" ) diff --git a/internal/blsgen/kms_test.go b/internal/blsgen/kms_test.go index 7fde9fdfcb..1e0f737902 100644 --- a/internal/blsgen/kms_test.go +++ b/internal/blsgen/kms_test.go @@ -13,8 +13,8 @@ import ( "github.com/aws/aws-sdk-go/service/kms" "github.com/ethereum/go-ethereum/common" - ffi_bls "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + ffi_bls "github.com/harmony-one/harmony/crypto/bls/core" ) var TestAwsConfig = AwsConfig{ diff --git a/internal/blsgen/lib.go b/internal/blsgen/lib.go index ec1edc0b4f..c46a3b21df 100644 --- a/internal/blsgen/lib.go +++ b/internal/blsgen/lib.go @@ -12,8 +12,8 @@ import ( "strings" "github.com/aws/aws-sdk-go/service/kms" - ffi_bls "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + ffi_bls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/pkg/errors" ) diff --git a/internal/blsgen/loader.go b/internal/blsgen/loader.go index 4a3b1dc6aa..87982ca0bc 100644 --- a/internal/blsgen/loader.go +++ b/internal/blsgen/loader.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/multibls" ) diff --git a/internal/blsgen/passphrase.go b/internal/blsgen/passphrase.go index e26a0a1ebd..f01a368dbe 100644 --- a/internal/blsgen/passphrase.go +++ b/internal/blsgen/passphrase.go @@ -8,7 +8,7 @@ import ( "strings" "sync" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" ) // PassSrcType is the type of passphrase provider source. diff --git a/internal/blsgen/utils.go b/internal/blsgen/utils.go index 0ab96f4c96..a0118bbfbe 100644 --- a/internal/blsgen/utils.go +++ b/internal/blsgen/utils.go @@ -5,7 +5,7 @@ import ( "os" "strings" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" ) func loadBasicKeyWithProvider(blsKeyFile string, pp passProvider) (*bls_core.SecretKey, error) { diff --git a/internal/chain/engine.go b/internal/chain/engine.go index 8b701a3dce..8c87c1e37e 100644 --- a/internal/chain/engine.go +++ b/internal/chain/engine.go @@ -10,7 +10,7 @@ import ( "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/numeric" - bls2 "github.com/harmony-one/bls/ffi/go/bls" + bls2 "github.com/harmony-one/harmony/crypto/bls/core" blsvrf "github.com/harmony-one/harmony/crypto/vrf/bls" "github.com/ethereum/go-ethereum/common" @@ -428,15 +428,23 @@ func payoutUndelegations( "[Finalize] failed to get validator from state to finalize", ) } + wrapperDirty := false for i := range wrapper.Delegations { delegation := &wrapper.Delegations[i] + before := len(delegation.Undelegations) totalWithdraw := delegation.RemoveUnlockedUndelegations( header.Epoch(), wrapper.LastEpochInCommittee, lockPeriod, noEarlyUnlock, isMaxRate, ) + if len(delegation.Undelegations) != before { + wrapperDirty = true + } if totalWithdraw.Sign() != 0 { state.AddBalance(delegation.DelegatorAddress, totalWithdraw) } } + if wrapperDirty { + state.MarkValidatorWrapperDirty(validator) + } countTrack[validator] = len(wrapper.Delegations) } @@ -484,6 +492,7 @@ func setElectionEpochAndMinFee(chain engine.ChainReader, header *block.Header, s } // Set last epoch in committee wrapper.LastEpochInCommittee = newShardState.Epoch + state.MarkValidatorWrapperDirty(addr) if minRateNotZero { // Set first election epoch (applies only if previously unset) state.SetValidatorFirstElectionEpoch(addr, newShardState.Epoch) diff --git a/internal/chain/engine_test.go b/internal/chain/engine_test.go index 677cb26a53..170cce75fd 100644 --- a/internal/chain/engine_test.go +++ b/internal/chain/engine_test.go @@ -7,12 +7,12 @@ import ( "time" "github.com/ethereum/go-ethereum/trie" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/consensus/engine" consensus_sig "github.com/harmony-one/harmony/consensus/signature" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/numeric" "github.com/harmony-one/harmony/shard" "github.com/harmony-one/harmony/staking/effective" diff --git a/internal/chain/sig.go b/internal/chain/sig.go index 23b51d5f1b..912c95cbd2 100644 --- a/internal/chain/sig.go +++ b/internal/chain/sig.go @@ -3,7 +3,7 @@ package chain import ( "errors" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/bls" "github.com/harmony-one/harmony/internal/utils" diff --git a/internal/configs/node/config.go b/internal/configs/node/config.go index 6f7caf3cf7..c694dcc4f4 100644 --- a/internal/configs/node/config.go +++ b/internal/configs/node/config.go @@ -10,8 +10,8 @@ import ( "sync" "time" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/multibls" diff --git a/internal/configs/node/config_test.go b/internal/configs/node/config_test.go index 9a6606c4f9..8a116afa34 100644 --- a/internal/configs/node/config_test.go +++ b/internal/configs/node/config_test.go @@ -3,8 +3,8 @@ package nodeconfig import ( "testing" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/blsgen" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" diff --git a/internal/genesis/genesis_test.go b/internal/genesis/genesis_test.go index 4ae5f8a423..59cf5425df 100644 --- a/internal/genesis/genesis_test.go +++ b/internal/genesis/genesis_test.go @@ -7,7 +7,7 @@ import ( "github.com/btcsuite/btcutil/bech32" ethCommon "github.com/ethereum/go-ethereum/common" - "github.com/harmony-one/bls/ffi/go/bls" + bls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/common" ) diff --git a/internal/params/config.go b/internal/params/config.go index 5c4ee23446..f7027dff0a 100644 --- a/internal/params/config.go +++ b/internal/params/config.go @@ -105,6 +105,7 @@ var ( SlashBallotSignerFixEpoch: big.NewInt(2964), VerifyBeaconHeaderSlashEpoch: big.NewInt(2964), BloomEpoch: big.NewInt(2964), + StakingV2Epoch: EpochTBD, } // TestnetChainConfig contains the chain parameters to run a node on the harmony test network. @@ -179,6 +180,7 @@ var ( CXMerkleProofReplayFixEpoch: big.NewInt(7385), BLSProofBindEpoch: big.NewInt(7420), BloomEpoch: big.NewInt(7414), + StakingV2Epoch: EpochTBD, } // PangaeaChainConfig contains the chain parameters for the Pangaea network. // All features except for CrossLink are enabled at launch. @@ -251,6 +253,7 @@ var ( SlashBallotSignerFixEpoch: EpochTBD, VerifyBeaconHeaderSlashEpoch: EpochTBD, BloomEpoch: EpochTBD, + StakingV2Epoch: EpochTBD, } // PartnerChainConfig contains the chain parameters for the Partner network. @@ -325,6 +328,7 @@ var ( SlashBallotSignerFixEpoch: big.NewInt(52650), VerifyBeaconHeaderSlashEpoch: big.NewInt(53000), BloomEpoch: big.NewInt(53508), + StakingV2Epoch: EpochTBD, } // StressnetChainConfig contains the chain parameters for the Stress test network. @@ -398,6 +402,7 @@ var ( SlashBallotSignerFixEpoch: EpochTBD, VerifyBeaconHeaderSlashEpoch: EpochTBD, BloomEpoch: EpochTBD, + StakingV2Epoch: EpochTBD, } // LocalnetChainConfig contains the chain parameters to run for local development. @@ -471,6 +476,7 @@ var ( SlashBallotSignerFixEpoch: big.NewInt(5), VerifyBeaconHeaderSlashEpoch: big.NewInt(5), BloomEpoch: big.NewInt(5), + StakingV2Epoch: big.NewInt(6), } // AllProtocolChanges ... @@ -546,6 +552,7 @@ var ( big.NewInt(1), // SlashBallotSignerFixEpoch big.NewInt(1), // VerifyBeaconHeaderSlashEpoch big.NewInt(1), // BloomEpoch + big.NewInt(0), // StakingV2Epoch } // TestChainConfig ... @@ -621,6 +628,7 @@ var ( big.NewInt(1), // SlashBallotSignerFixEpoch big.NewInt(1), // VerifyBeaconHeaderSlashEpoch big.NewInt(1), // BloomEpoch + big.NewInt(0), // StakingV2Epoch } // TestRules ... @@ -882,6 +890,8 @@ type ChainConfig struct { // have individual activation epochs; each feature is active once the chain // reaches the earlier of BloomEpoch and that feature's epoch. BloomEpoch *big.Int `json:"bloom-epoch,omitempty"` + // StakingV2Epoch is the epoch when Staking V2 is activated. + StakingV2Epoch *big.Int `json:"staking-v2-epoch,omitempty"` } // String implements the fmt.Stringer interface. @@ -1304,6 +1314,10 @@ func (c *ChainConfig) IsPrague(epoch *big.Int) bool { return isForked(c.PragueEpoch, epoch) } +func (c *ChainConfig) IsStakingV2(epoch *big.Int) bool { + return isForked(c.StakingV2Epoch, epoch) +} + // During this epoch, shards 2 and 3 will start sending // their balances over to shard 0 or 1. func (c *ChainConfig) IsOneEpochBeforeHIP30(epoch *big.Int) bool { diff --git a/internal/params/protocol_params.go b/internal/params/protocol_params.go index f79b2367d3..c54dc629dd 100644 --- a/internal/params/protocol_params.go +++ b/internal/params/protocol_params.go @@ -34,6 +34,10 @@ const ( TxGasContractCreation uint64 = 53000 // Per transaction that creates a contract. NOTE: Not payable on data of calls between transactions. // TxGasValidatorCreation ... TxGasValidatorCreation uint64 = 5300000 // Per transaction that creates a new validator. NOTE: Not payable on data of calls between transactions. + // TxGasPerBatchStakingAction is charged per BatchDelegate / BatchUndelegate action. + TxGasPerBatchStakingAction uint64 = 25000 + // TxGasUndelegateAll is charged for UndelegateAll. + TxGasUndelegateAll uint64 = 500000 // TxDataZeroGas ... TxDataZeroGas uint64 = 4 // Per byte of data attached to a transaction that equals zero. NOTE: Not payable on data of calls between transactions. // QuadCoeffDiv ... diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 5152eb28e0..fe9f241a12 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -5,10 +5,10 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/consensus/engine" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" "github.com/harmony-one/harmony/internal/shardchain" "github.com/harmony-one/harmony/multibls" diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 28af81aed8..b277e77d33 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -15,8 +15,8 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" p2p_crypto "github.com/libp2p/go-libp2p/core/crypto" "github.com/pkg/errors" ) diff --git a/multibls/multibls.go b/multibls/multibls.go index dc09d9a868..22bb84ba19 100644 --- a/multibls/multibls.go +++ b/multibls/multibls.go @@ -5,8 +5,8 @@ import ( "github.com/harmony-one/harmony/internal/utils" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" ) // PrivateKeys stores the bls secret keys that belongs to the node diff --git a/node/harmony/addresses.go b/node/harmony/addresses.go index 44279981c2..6ee8d6d657 100644 --- a/node/harmony/addresses.go +++ b/node/harmony/addresses.go @@ -5,8 +5,8 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" common2 "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/registry" "github.com/harmony-one/harmony/internal/utils" diff --git a/node/harmony/node_cross_link.go b/node/harmony/node_cross_link.go index c772a9309d..271c842621 100644 --- a/node/harmony/node_cross_link.go +++ b/node/harmony/node_cross_link.go @@ -6,9 +6,9 @@ import ( common2 "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" - ffi_bls "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/core" "github.com/harmony-one/harmony/core/types" + ffi_bls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/shard" "github.com/pkg/errors" diff --git a/node/harmony/node_test.go b/node/harmony/node_test.go index c7444d8d8f..48c5a504a8 100644 --- a/node/harmony/node_test.go +++ b/node/harmony/node_test.go @@ -9,10 +9,10 @@ import ( "testing" "time" - ffi_bls "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/consensus" "github.com/harmony-one/harmony/consensus/quorum" "github.com/harmony-one/harmony/core" + ffi_bls "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/internal/chain" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" "github.com/harmony-one/harmony/internal/registry" diff --git a/node/harmony/worker/slash_duplicate_reporter_test.go b/node/harmony/worker/slash_duplicate_reporter_test.go index ad3f6b6447..d5229b3a84 100644 --- a/node/harmony/worker/slash_duplicate_reporter_test.go +++ b/node/harmony/worker/slash_duplicate_reporter_test.go @@ -6,7 +6,6 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" blockfactory "github.com/harmony-one/harmony/block/factory" consensus_sig "github.com/harmony-one/harmony/consensus/signature" @@ -16,6 +15,7 @@ import ( "github.com/harmony-one/harmony/core/types" "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" chain2 "github.com/harmony-one/harmony/internal/chain" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" diff --git a/p2p/host.go b/p2p/host.go index c89fbc9189..23dbb33322 100644 --- a/p2p/host.go +++ b/p2p/host.go @@ -14,9 +14,9 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/harmony-one/abool" - "github.com/harmony-one/bls/ffi/go/bls" prom "github.com/harmony-one/harmony/api/service/prometheus" "github.com/harmony-one/harmony/common/clock" + bls "github.com/harmony-one/harmony/crypto/bls/core" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/internal/utils/blockedpeers" @@ -496,10 +496,14 @@ func NewHost(cfg HostConfig) (Host, error) { trustedPeersDnsResolvedCounter.Add(0) trustedPeersConnectFailuresCounter.Add(0) + consensusPubKey := "" + if self.ConsensusPubKey != nil { + consensusPubKey = self.ConsensusPubKey.SerializeToHexStr() + } utils.Logger().Info(). Str("self", net.JoinHostPort(self.IP, self.Port)). Interface("PeerID", self.PeerID). - Str("PubKey", self.ConsensusPubKey.SerializeToHexStr()). + Str("PubKey", consensusPubKey). Msg("libp2p host ready") return h, nil } diff --git a/rosetta/infra/Dockerfile b/rosetta/infra/Dockerfile index 28d11a885f..9b925b81fe 100644 --- a/rosetta/infra/Dockerfile +++ b/rosetta/infra/Dockerfile @@ -12,9 +12,7 @@ RUN mkdir -p $HMY_PATH WORKDIR $HMY_PATH -RUN git clone https://github.com/harmony-one/harmony.git && \ - git clone https://github.com/harmony-one/bls.git && \ - git clone https://github.com/harmony-one/mcl.git +RUN git clone https://github.com/harmony-one/harmony.git WORKDIR $HMY_PATH/harmony diff --git a/rpc/harmony/staking.go b/rpc/harmony/staking.go index 1715a486b4..273c71af06 100644 --- a/rpc/harmony/staking.go +++ b/rpc/harmony/staking.go @@ -878,11 +878,10 @@ func (s *PublicStakingService) GetAvailableRedelegationBalance( redelegationTotal := big.NewInt(0) for _, d := range delegations { - for _, u := range d.Undelegations { - if u.Epoch.Cmp(currEpoch) < 1 { // Undelegation.Epoch < currentEpoch - redelegationTotal.Add(redelegationTotal, u.Amount) - } - } + redelegationTotal.Add( + redelegationTotal, + staking.TotalRedelegatableUndelegations(d.Undelegations, currEpoch), + ) } return redelegationTotal, nil } diff --git a/scripts/go_executable_build.sh b/scripts/go_executable_build.sh index 05c465cecf..5a43dc43da 100755 --- a/scripts/go_executable_build.sh +++ b/scripts/go_executable_build.sh @@ -34,22 +34,13 @@ case "${0}" in *) progdir=.;; esac -. "${progdir}/setup_bls_build_flags.sh" - declare -A LIB if [ "$(uname -s)" == "Darwin" ]; then MD5='md5 -r' GOOS=darwin - LIB[libbls384_256.dylib]=${BLS_DIR}/lib/libbls384_256.dylib - LIB[libmcl.dylib]=${MCL_DIR}/lib/libmcl.dylib - LIB[libgmp.10.dylib]=/opt/homebrew/opt/gmp/lib/libgmp.10.dylib - LIB[libgmpxx.4.dylib]=/opt/homebrew/opt/gmp/lib/libgmpxx.4.dylib - LIB[libcrypto.1.1.dylib]=/opt/homebrew/opt/openssl@1.1/lib/libcrypto.1.1.dylib else MD5=md5sum - LIB[libbls384_256.so]=${BLS_DIR}/lib/libbls384_256.so - LIB[libmcl.so]=${MCL_DIR}/lib/libmcl.so fi function usage @@ -155,11 +146,7 @@ function build_only fi done - $MD5 "${!SRC[@]}" "${!LIB[@]}" > md5sum.txt - # hardcode the prebuilt libcrypto to md5sum.txt - if [ "$(uname -s)" == "Linux" ]; then - echo '771150db04267126823190c873a96e48 libcrypto.so.10' >> md5sum.txt - fi + $MD5 "${!SRC[@]}" > md5sum.txt fi popd } diff --git a/scripts/macos_docker/Dockerfile b/scripts/macos_docker/Dockerfile index e8ad312bb6..bedc81bd99 100644 --- a/scripts/macos_docker/Dockerfile +++ b/scripts/macos_docker/Dockerfile @@ -9,11 +9,6 @@ ENV GOPATH=/root/go ENV GO111MODULE=on ENV HMY_PATH=${GOPATH}/src/github.com/harmony-one ENV OPENSSL_DIR=/usr/lib/ssl -ENV MCL_DIR=${HMY_PATH}/mcl -ENV BLS_DIR=${HMY_PATH}/bls -ENV CGO_CFLAGS="-I${BLS_DIR}/include -I${MCL_DIR}/include" -ENV CGO_LDFLAGS="-L${BLS_DIR}/lib" -ENV LD_LIBRARY_PATH=${BLS_DIR}/lib:${MCL_DIR}/lib ENV GIMME_GO_VERSION=${GOLANG_VERSION} ENV PATH="/root/bin:${PATH}" @@ -31,18 +26,8 @@ RUN eval "$(~/bin/gimme ${GIMME_GO_VERSION})" #RUN git clone https://github.com/harmony-one/harmony.git ${HMY_PATH}/harmony -RUN git clone https://github.com/harmony-one/mcl.git ${HMY_PATH}/mcl && \ - echo "mcl repository cloned" && \ - ls -la ${HMY_PATH}/mcl # List the contents of the mcl directory - -RUN git clone https://github.com/harmony-one/bls.git ${HMY_PATH}/bls && \ - echo "bls repository cloned" && \ - ls -la ${HMY_PATH}/bls # List the contents of the bls directory - RUN git clone https://github.com/harmony-one/go-sdk.git ${HMY_PATH}/go-sdk -RUN cd ${HMY_PATH}/bls && make -j8 BLS_SWAP_G=1 - RUN touch /root/.bash_profile && \ gimme ${GIMME_GO_VERSION} >> /root/.bash_profile && \ echo "GIMME_GO_VERSION='${GIMME_GO_VERSION}'" >> /root/.bash_profile && \ @@ -61,4 +46,3 @@ RUN . ~/.bash_profile; \ go install github.com/stamblerre/gocode; \ go install golang.org/x/tools/...; \ go install honnef.co/go/tools/cmd/staticcheck/... - diff --git a/scripts/setup_bls_build_flags.sh b/scripts/setup_bls_build_flags.sh deleted file mode 100644 index 92ee20579e..0000000000 --- a/scripts/setup_bls_build_flags.sh +++ /dev/null @@ -1,44 +0,0 @@ -# no shebang; to be sourced from other scripts - -unset -v progdir -case "${0}" in -*/*) progdir="${0%/*}";; -*) progdir=.;; -esac - -case "${HMY_PATH+set}" in -"") - unset -v gopath - gopath=$(go env GOPATH) - # HMY_PATH is the common root directory of all harmony repos - HMY_PATH="${gopath%%:*}/src/github.com/harmony-one" - if [ ! -d $HMY_PATH ]; then - # "env pwd" uses external pwd(1) implementation and not the Bash built-in, - # which does not fully dereference symlinks. - HMY_PATH=$(cd $progdir/../.. && env pwd) - fi - ;; -esac -: ${OPENSSL_DIR="/opt/homebrew/opt/openssl@1.1"} -: ${MCL_DIR="${HMY_PATH}/mcl"} -: ${BLS_DIR="${HMY_PATH}/bls"} -export CGO_CFLAGS="-I${BLS_DIR}/include -I${MCL_DIR}/include" -export CGO_LDFLAGS="-L${BLS_DIR}/lib" -export LD_LIBRARY_PATH=${BLS_DIR}/lib:${MCL_DIR}/lib - -OS=$(uname -s) -case $OS in - Darwin) - export CGO_CFLAGS="-I${BLS_DIR}/include -I${MCL_DIR}/include -I${OPENSSL_DIR}/include" - export CGO_LDFLAGS="-L${BLS_DIR}/lib -L${OPENSSL_DIR}/lib" - export LD_LIBRARY_PATH=${BLS_DIR}/lib:${MCL_DIR}/lib:${OPENSSL_DIR}/lib - export DYLD_FALLBACK_LIBRARY_PATH=$LD_LIBRARY_PATH - ;; -esac - -if [ "$1" = "-v" ]; then - echo "{ \"CGO_CFLAGS\" : \"$CGO_CFLAGS\", - \"CGO_LDFLAGS\" : \"$CGO_LDFLAGS\", - \"LD_LIBRARY_PATH\" : \"$LD_LIBRARY_PATH\", - \"DYLD_FALLBACK_LIBRARY_PATH\" : \"$DYLD_FALLBACK_LIBRARY_PATH\"}" | jq "." -fi diff --git a/scripts/travis_go_checker.sh b/scripts/travis_go_checker.sh index c6c7640caf..d115ce50e5 100755 --- a/scripts/travis_go_checker.sh +++ b/scripts/travis_go_checker.sh @@ -14,8 +14,6 @@ tmpdir= trap 'case "${tmpdir}" in ?*) rm -rf "${tmpdir}";; esac' EXIT tmpdir=$(mktemp -d) -. "${progdir}/setup_bls_build_flags.sh" - echo "Checking go.mod..." gomod_diff_output="${tmpdir}/gomod.diff" if git diff --exit-code -- go.mod > "${gomod_diff_output}" diff --git a/shard/committee/assignment.go b/shard/committee/assignment.go index 5fba3e6200..57f8153d62 100644 --- a/shard/committee/assignment.go +++ b/shard/committee/assignment.go @@ -9,9 +9,9 @@ import ( "github.com/harmony-one/harmony/crypto/bls" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/block" "github.com/harmony-one/harmony/core/types" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" common2 "github.com/harmony-one/harmony/internal/common" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" diff --git a/staking/availability/interface.go b/staking/availability/interface.go index f7df8482ef..810de2427a 100644 --- a/staking/availability/interface.go +++ b/staking/availability/interface.go @@ -27,4 +27,5 @@ type RoundHeader interface { type ValidatorState interface { ValidatorWrapper(common.Address, bool, bool) (*staking.ValidatorWrapper, error) UpdateValidatorWrapper(common.Address, *staking.ValidatorWrapper) error + MarkValidatorWrapperDirty(common.Address) } diff --git a/staking/availability/measure.go b/staking/availability/measure.go index 07f8c4e60e..5196ca34fa 100644 --- a/staking/availability/measure.go +++ b/staking/availability/measure.go @@ -119,6 +119,7 @@ func bumpCount( wrapper.Counters.NumBlocksSigned, common.Big1, ) } + state.MarkValidatorWrapperDirty(addr) } } @@ -216,6 +217,7 @@ func ComputeAndMutateEPOSStatus( switch computed.IsBelowThreshold { case missedTooManyBlocks: wrapper.Status = effective.Inactive + state.MarkValidatorWrapperDirty(addr) utils.Logger().Info(). Str("threshold", measure.String()). Interface("computed", computed). @@ -260,6 +262,7 @@ func UpdateMinimumCommissionFee( Str("firstElectionEpoch", firstElectionEpoch.String()). Msg("updating min commission rate") wrapper.Rate.SetBytes(minRate.Bytes()) + state.MarkValidatorWrapperDirty(addr) return true, nil } } @@ -268,6 +271,7 @@ func UpdateMinimumCommissionFee( type stateValidatorWrapper interface { ValidatorWrapper(addr common.Address, sendOriginal bool, copyDelegations bool) (*staking.ValidatorWrapper, error) + MarkValidatorWrapperDirty(addr common.Address) } // UpdateMaxCommissionFee makes sure the max-rate is at least higher than the rate + max-rate-change. @@ -295,6 +299,7 @@ func UpdateMaxCommissionFee(IsTopMaxRate bool, state stateValidatorWrapper, addr Str("new max-rate", newRate.String()). Msg("updating max commission rate") wrapper.MaxRate.SetBytes(newRate.Bytes()) + state.MarkValidatorWrapperDirty(addr) } return nil diff --git a/staking/availability/measure_test.go b/staking/availability/measure_test.go index 8d635f45b9..ef2a4a8cb1 100644 --- a/staking/availability/measure_test.go +++ b/staking/availability/measure_test.go @@ -660,6 +660,8 @@ func (state testStateDB) UpdateValidatorWrapper(addr common.Address, wrapper *st return nil } +func (state testStateDB) MarkValidatorWrapperDirty(addr common.Address) {} + func (state testStateDB) GetCode(addr common.Address, isValidatorCode bool) []byte { wrapper, ok := state[addr] if !ok { @@ -794,6 +796,8 @@ func (a stateValidatorWrapperImpl) ValidatorWrapper(addr common.Address, sendOri return a.v, a.err } +func (a stateValidatorWrapperImpl) MarkValidatorWrapperDirty(addr common.Address) {} + func TestUpdateMaxCommissionFee(t *testing.T) { t.Run("0.6 + 0.6 = 1 (100%)", func(t *testing.T) { v1 := stateValidatorWrapperImpl{ diff --git a/staking/effective/calculate_test.go b/staking/effective/calculate_test.go index 7127192c3b..ebbde3e7be 100644 --- a/staking/effective/calculate_test.go +++ b/staking/effective/calculate_test.go @@ -9,8 +9,8 @@ import ( "sort" "testing" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/ethereum/go-ethereum/common" "github.com/harmony-one/harmony/numeric" diff --git a/staking/precompile_address.go b/staking/precompile_address.go new file mode 100644 index 0000000000..b805020790 --- /dev/null +++ b/staking/precompile_address.go @@ -0,0 +1,6 @@ +package staking + +import "github.com/ethereum/go-ethereum/common" + +// PrecompileAddress is the EVM staking precompile at 0x…fc (decimal 252). +var PrecompileAddress = common.BytesToAddress([]byte{252}) diff --git a/staking/slash/double-sign.go b/staking/slash/double-sign.go index dbbeb6a66a..4b93fbe3a8 100644 --- a/staking/slash/double-sign.go +++ b/staking/slash/double-sign.go @@ -9,9 +9,9 @@ import ( "github.com/harmony-one/harmony/shard" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" consensus_sig "github.com/harmony-one/harmony/consensus/signature" "github.com/harmony-one/harmony/core/state" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" common2 "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/utils" @@ -403,6 +403,7 @@ func delegatorSlashApplyDebt( slashTrack *Application, useSlashExternalStakeDenomFix bool, ) error { + state.MarkValidatorWrapperDirty(current.Address) slashIndexPairs, totalStake := makeSlashList(snapshot, current) validatorDelegation := ¤t.Delegations[0] selfStakeForExternalDenom := validatorDelegation.Amount diff --git a/staking/slash/double-sign_test.go b/staking/slash/double-sign_test.go index fcb0972c75..506ea949d2 100644 --- a/staking/slash/double-sign_test.go +++ b/staking/slash/double-sign_test.go @@ -13,11 +13,11 @@ import ( "github.com/harmony-one/harmony/crypto/bls" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" blockfactory "github.com/harmony-one/harmony/block/factory" consensus_sig "github.com/harmony-one/harmony/consensus/signature" "github.com/harmony-one/harmony/core/state" "github.com/harmony-one/harmony/core/types" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" shardingconfig "github.com/harmony-one/harmony/internal/configs/sharding" "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/numeric" @@ -324,7 +324,7 @@ func TestVerify(t *testing.T) { sdb: defaultTestStateDB(), chain: defaultFakeBlockChain(), - expErr: errors.New("Empty buf"), + expErr: errors.New("err blsSignatureDeserialize"), }, { // false signature diff --git a/staking/types/delegation.go b/staking/types/delegation.go index 65bf36cfe5..8f7cbf536e 100644 --- a/staking/types/delegation.go +++ b/staking/types/delegation.go @@ -129,29 +129,37 @@ func (d *Delegation) Undelegate(epoch *big.Int, amt *big.Int) error { } d.Amount.Sub(d.Amount, amt) - exist := false - for _, entry := range d.Undelegations { - if entry.Epoch.Cmp(epoch) == 0 { - exist = true - entry.Amount.Add(entry.Amount, amt) + for i := range d.Undelegations { + if d.Undelegations[i].Epoch.Cmp(epoch) == 0 { + d.Undelegations[i].Amount.Add(d.Undelegations[i].Amount, amt) return nil } } - if !exist { - item := Undelegation{amt, epoch} - d.Undelegations = append(d.Undelegations, item) + d.Undelegations = append(d.Undelegations, Undelegation{amt, epoch}) - // Always sort the undelegate by epoch in increasing order - sort.SliceStable( - d.Undelegations, - func(i, j int) bool { return d.Undelegations[i].Epoch.Cmp(d.Undelegations[j].Epoch) < 0 }, - ) - } + // Always sort the undelegate by epoch in increasing order + sort.SliceStable( + d.Undelegations, + func(i, j int) bool { return d.Undelegations[i].Epoch.Cmp(d.Undelegations[j].Epoch) < 0 }, + ) return nil } +// TotalRedelegatableUndelegations returns undelegated tokens eligible for redelegation +// at currEpoch. This matches the redelegation loop in core/staking_verifier.go, which +// only consumes entries with undelegation.Epoch strictly before the current epoch. +func TotalRedelegatableUndelegations(undelegations Undelegations, currEpoch *big.Int) *big.Int { + total := big.NewInt(0) + for _, u := range undelegations { + if u.Epoch.Cmp(currEpoch) < 0 { + total.Add(total, u.Amount) + } + } + return total +} + // TotalInUndelegation - return the total amount of token in undelegation (locking period) func (d *Delegation) TotalInUndelegation() *big.Int { total := big.NewInt(0) diff --git a/staking/types/delegation_redelegation_test.go b/staking/types/delegation_redelegation_test.go new file mode 100644 index 0000000000..b553410690 --- /dev/null +++ b/staking/types/delegation_redelegation_test.go @@ -0,0 +1,20 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTotalRedelegatableUndelegations(t *testing.T) { + currEpoch := big.NewInt(2965) + delegations := Undelegations{ + {Amount: big.NewInt(100), Epoch: big.NewInt(2964)}, + {Amount: big.NewInt(200), Epoch: big.NewInt(2965)}, + {Amount: big.NewInt(300), Epoch: big.NewInt(2963)}, + } + + total := TotalRedelegatableUndelegations(delegations, currEpoch) + require.Equal(t, int64(400), total.Int64()) +} diff --git a/staking/types/delegation_test.go b/staking/types/delegation_test.go index ff0c25c02b..7031f28d02 100644 --- a/staking/types/delegation_test.go +++ b/staking/types/delegation_test.go @@ -16,6 +16,36 @@ var ( delegation = NewDelegation(delegatorAddr, delegationAmt) ) +func TestUndelegateSameEpoch(t *testing.T) { + d := NewDelegation(delegatorAddr, big.NewInt(100000)) + epoch := big.NewInt(10) + amount1 := big.NewInt(1000) + amount2 := big.NewInt(2000) + + if err := d.Undelegate(epoch, amount1); err != nil { + t.Fatalf("first undelegate failed: %v", err) + } + if err := d.Undelegate(epoch, amount2); err != nil { + t.Fatalf("second undelegate failed: %v", err) + } + + if len(d.Undelegations) != 1 { + t.Fatalf("expected one undelegation entry, got %d", len(d.Undelegations)) + } + expectedUndelegated := big.NewInt(3000) + if d.Undelegations[0].Amount.Cmp(expectedUndelegated) != 0 { + t.Errorf("same-epoch merge: undelegation amount = %s, want %s", + d.Undelegations[0].Amount, expectedUndelegated) + } + if d.Undelegations[0].Epoch.Cmp(epoch) != 0 { + t.Errorf("undelegation epoch = %s, want %s", d.Undelegations[0].Epoch, epoch) + } + expectedDelegated := big.NewInt(97000) + if d.Amount.Cmp(expectedDelegated) != 0 { + t.Errorf("delegated amount = %s, want %s", d.Amount, expectedDelegated) + } +} + func TestUndelegate(t *testing.T) { epoch1 := big.NewInt(10) amount1 := big.NewInt(1000) diff --git a/staking/types/gas.go b/staking/types/gas.go new file mode 100644 index 0000000000..5b00cde16b --- /dev/null +++ b/staking/types/gas.go @@ -0,0 +1,62 @@ +package types + +import ( + "math" + + "github.com/harmony-one/harmony/internal/params" + "github.com/pkg/errors" +) + +var ( + // ErrBatchTooLarge is returned when a batch staking tx exceeds MaxBatchStakingActions. + ErrBatchTooLarge = errors.New("batch staking action count exceeds maximum") +) + +// ExtraGasForStakingDirective returns gas for batch staking directives. +// data is the RLP-encoded stake message payload. +func ExtraGasForStakingDirective(directive Directive, data []byte) (uint64, error) { + switch directive { + case DirectiveBatchDelegate: + msg, err := RLPDecodeStakeMsg(data, DirectiveBatchDelegate) + if err != nil { + return 0, err + } + batch, ok := msg.(*BatchDelegate) + if !ok { + return 0, ErrInvalidStakingKind + } + n := len(batch.Delegations) + if n > MaxBatchStakingActions { + return 0, ErrBatchTooLarge + } + return mulGas(uint64(n), params.TxGasPerBatchStakingAction) + case DirectiveBatchUndelegate: + msg, err := RLPDecodeStakeMsg(data, DirectiveBatchUndelegate) + if err != nil { + return 0, err + } + batch, ok := msg.(*BatchUndelegate) + if !ok { + return 0, ErrInvalidStakingKind + } + n := len(batch.Undelegations) + if n > MaxBatchStakingActions { + return 0, ErrBatchTooLarge + } + return mulGas(uint64(n), params.TxGasPerBatchStakingAction) + case DirectiveUndelegateAll: + return params.TxGasUndelegateAll, nil + default: + return 0, nil + } +} + +func mulGas(count, per uint64) (uint64, error) { + if count == 0 { + return 0, nil + } + if per != 0 && count > math.MaxUint64/per { + return 0, errors.New("staking batch gas overflow") + } + return count * per, nil +} diff --git a/staking/types/gas_test.go b/staking/types/gas_test.go new file mode 100644 index 0000000000..c3d49a90f7 --- /dev/null +++ b/staking/types/gas_test.go @@ -0,0 +1,80 @@ +package types + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/harmony-one/harmony/internal/params" +) + +func TestExtraGasForStakingDirective(t *testing.T) { + delegator := common.HexToAddress("0x1") + validator := common.HexToAddress("0x2") + + batchDelegate := BatchDelegate{ + DelegatorAddress: delegator, + Delegations: []DelegationAction{ + {ValidatorAddress: validator, Amount: big.NewInt(1000)}, + {ValidatorAddress: validator, Amount: big.NewInt(2000)}, + }, + } + data, err := rlp.EncodeToBytes(batchDelegate) + if err != nil { + t.Fatal(err) + } + gas, err := ExtraGasForStakingDirective(DirectiveBatchDelegate, data) + if err != nil { + t.Fatal(err) + } + want := 2 * params.TxGasPerBatchStakingAction + if gas != want { + t.Fatalf("batch delegate gas = %d, want %d", gas, want) + } + + batchUndelegate := BatchUndelegate{ + DelegatorAddress: delegator, + Undelegations: []UndelegationAction{ + {ValidatorAddress: validator, Amount: big.NewInt(1000)}, + }, + } + data, err = rlp.EncodeToBytes(batchUndelegate) + if err != nil { + t.Fatal(err) + } + gas, err = ExtraGasForStakingDirective(DirectiveBatchUndelegate, data) + if err != nil { + t.Fatal(err) + } + if gas != params.TxGasPerBatchStakingAction { + t.Fatalf("batch undelegate gas = %d, want %d", gas, params.TxGasPerBatchStakingAction) + } + + gas, err = ExtraGasForStakingDirective(DirectiveUndelegateAll, nil) + if err != nil { + t.Fatal(err) + } + if gas != params.TxGasUndelegateAll { + t.Fatalf("undelegate all gas = %d, want %d", gas, params.TxGasUndelegateAll) + } + + tooLarge := BatchDelegate{ + DelegatorAddress: delegator, + Delegations: make([]DelegationAction, MaxBatchStakingActions+1), + } + for i := range tooLarge.Delegations { + tooLarge.Delegations[i] = DelegationAction{ + ValidatorAddress: validator, + Amount: big.NewInt(1), + } + } + data, err = rlp.EncodeToBytes(tooLarge) + if err != nil { + t.Fatal(err) + } + _, err = ExtraGasForStakingDirective(DirectiveBatchDelegate, data) + if err != ErrBatchTooLarge { + t.Fatalf("expected ErrBatchTooLarge, got %v", err) + } +} diff --git a/staking/types/messages.go b/staking/types/messages.go index bddcbacf0b..aa4ef232cf 100644 --- a/staking/types/messages.go +++ b/staking/types/messages.go @@ -27,6 +27,12 @@ const ( DirectiveUndelegate // DirectiveCollectRewards ... DirectiveCollectRewards + // DirectiveBatchDelegate ... + DirectiveBatchDelegate + // DirectiveBatchUndelegate ... + DirectiveBatchUndelegate + // DirectiveUndelegateAll ... + DirectiveUndelegateAll ) var ( @@ -36,6 +42,9 @@ var ( DirectiveDelegate: "Delegate", DirectiveUndelegate: "Undelegate", DirectiveCollectRewards: "CollectRewards", + DirectiveBatchDelegate: "BatchDelegate", + DirectiveBatchUndelegate: "BatchUndelegate", + DirectiveUndelegateAll: "UndelegateAll", } // ErrInvalidStakingKind given when caller gives bad staking message kind ErrInvalidStakingKind = errors.New("bad staking kind") @@ -263,3 +272,147 @@ func (v MigrationMsg) Copy() MigrationMsg { func (v MigrationMsg) Equals(s MigrationMsg) bool { return v.From == s.From && v.To == s.To } + +// DelegationAction represents a single delegation action in a batch operation +type DelegationAction struct { + ValidatorAddress common.Address `json:"validator_address"` + Amount *big.Int `json:"amount"` +} + +// BatchDelegate - type for delegating to multiple validators in one transaction +type BatchDelegate struct { + DelegatorAddress common.Address `json:"delegator_address"` + Delegations []DelegationAction `json:"delegations"` +} + +// Type of BatchDelegate +func (v BatchDelegate) Type() Directive { + return DirectiveBatchDelegate +} + +// Copy returns a deep copy of the BatchDelegate as a StakeMsg interface +func (v BatchDelegate) Copy() StakeMsg { + cp := BatchDelegate{ + DelegatorAddress: v.DelegatorAddress, + Delegations: make([]DelegationAction, len(v.Delegations)), + } + for i, d := range v.Delegations { + cp.Delegations[i] = DelegationAction{ + ValidatorAddress: d.ValidatorAddress, + } + if d.Amount != nil { + cp.Delegations[i].Amount = new(big.Int).Set(d.Amount) + } + } + return cp +} + +// Equals returns if v and s are equal +func (v BatchDelegate) Equals(s BatchDelegate) bool { + if !bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) { + return false + } + if len(v.Delegations) != len(s.Delegations) { + return false + } + for i := range v.Delegations { + if !bytes.Equal(v.Delegations[i].ValidatorAddress.Bytes(), s.Delegations[i].ValidatorAddress.Bytes()) { + return false + } + if v.Delegations[i].Amount == nil { + if s.Delegations[i].Amount != nil { + return false + } + } else if s.Delegations[i].Amount == nil { + return false + } else if v.Delegations[i].Amount.Cmp(s.Delegations[i].Amount) != 0 { + return false + } + } + return true +} + +// MaxBatchStakingActions is the maximum number of actions allowed in a single +// BatchDelegate or BatchUndelegate transaction. +const MaxBatchStakingActions = 50 + +// UndelegationAction represents a single undelegation action in a batch operation +type UndelegationAction struct { + ValidatorAddress common.Address `json:"validator_address"` + Amount *big.Int `json:"amount"` +} + +// BatchUndelegate - type for undelegating from multiple validators in one transaction +type BatchUndelegate struct { + DelegatorAddress common.Address `json:"delegator_address"` + Undelegations []UndelegationAction `json:"undelegations"` +} + +// Type of BatchUndelegate +func (v BatchUndelegate) Type() Directive { + return DirectiveBatchUndelegate +} + +// Copy returns a deep copy of the BatchUndelegate as a StakeMsg interface +func (v BatchUndelegate) Copy() StakeMsg { + cp := BatchUndelegate{ + DelegatorAddress: v.DelegatorAddress, + Undelegations: make([]UndelegationAction, len(v.Undelegations)), + } + for i, u := range v.Undelegations { + cp.Undelegations[i] = UndelegationAction{ + ValidatorAddress: u.ValidatorAddress, + } + if u.Amount != nil { + cp.Undelegations[i].Amount = new(big.Int).Set(u.Amount) + } + } + return cp +} + +// Equals returns if v and s are equal +func (v BatchUndelegate) Equals(s BatchUndelegate) bool { + if !bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) { + return false + } + if len(v.Undelegations) != len(s.Undelegations) { + return false + } + for i := range v.Undelegations { + if !bytes.Equal(v.Undelegations[i].ValidatorAddress.Bytes(), s.Undelegations[i].ValidatorAddress.Bytes()) { + return false + } + if v.Undelegations[i].Amount == nil { + if s.Undelegations[i].Amount != nil { + return false + } + } else if s.Undelegations[i].Amount == nil { + return false + } else if v.Undelegations[i].Amount.Cmp(s.Undelegations[i].Amount) != 0 { + return false + } + } + return true +} + +// UndelegateAll - type for undelegating all from all validators +type UndelegateAll struct { + DelegatorAddress common.Address `json:"delegator_address"` +} + +// Type of UndelegateAll +func (v UndelegateAll) Type() Directive { + return DirectiveUndelegateAll +} + +// Copy returns a deep copy of the UndelegateAll as a StakeMsg interface +func (v UndelegateAll) Copy() StakeMsg { + return UndelegateAll{ + DelegatorAddress: v.DelegatorAddress, + } +} + +// Equals returns if v and s are equal +func (v UndelegateAll) Equals(s UndelegateAll) bool { + return bytes.Equal(v.DelegatorAddress.Bytes(), s.DelegatorAddress.Bytes()) +} diff --git a/staking/types/transaction.go b/staking/types/transaction.go index c9923bcdfe..0d011bcd74 100644 --- a/staking/types/transaction.go +++ b/staking/types/transaction.go @@ -299,6 +299,12 @@ func RLPDecodeStakeMsg(payload []byte, d Directive) (interface{}, error) { ds = &Undelegate{} case DirectiveCollectRewards: ds = &CollectRewards{} + case DirectiveBatchDelegate: + ds = &BatchDelegate{} + case DirectiveBatchUndelegate: + ds = &BatchUndelegate{} + case DirectiveUndelegateAll: + ds = &UndelegateAll{} default: return nil, nil } diff --git a/staking/types/transaction_test.go b/staking/types/transaction_test.go index 55f21053aa..2d251c54cf 100644 --- a/staking/types/transaction_test.go +++ b/staking/types/transaction_test.go @@ -9,7 +9,7 @@ import ( "github.com/harmony-one/harmony/crypto/bls" "github.com/ethereum/go-ethereum/common" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" common2 "github.com/harmony-one/harmony/internal/common" numeric "github.com/harmony-one/harmony/numeric" ) diff --git a/staking/types/validator.go b/staking/types/validator.go index 4f35a03e4f..395fdeec6b 100644 --- a/staking/types/validator.go +++ b/staking/types/validator.go @@ -9,9 +9,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/rlp" - bls_core "github.com/harmony-one/bls/ffi/go/bls" "github.com/harmony-one/harmony/common/denominations" "github.com/harmony-one/harmony/consensus/votepower" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" common2 "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/genesis" diff --git a/test/chain/reward/main.go b/test/chain/reward/main.go index aee4c4ed0e..4482cf27cc 100644 --- a/test/chain/reward/main.go +++ b/test/chain/reward/main.go @@ -8,7 +8,6 @@ import ( common2 "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" - bls_core "github.com/harmony-one/bls/ffi/go/bls" msg_pb "github.com/harmony-one/harmony/api/proto/message" blockfactory "github.com/harmony-one/harmony/block/factory" "github.com/harmony-one/harmony/core" @@ -16,6 +15,7 @@ import ( "github.com/harmony-one/harmony/core/state" "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/crypto/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" "github.com/harmony-one/harmony/internal/chain" "github.com/harmony-one/harmony/internal/common" diff --git a/test/chain/vrf/main.go b/test/chain/vrf/main.go index 1a44b9e416..81f503c605 100644 --- a/test/chain/vrf/main.go +++ b/test/chain/vrf/main.go @@ -7,7 +7,7 @@ import ( "github.com/harmony-one/harmony/crypto/bls" - bls_core "github.com/harmony-one/bls/ffi/go/bls" + bls_core "github.com/harmony-one/harmony/crypto/bls/core" "github.com/harmony-one/harmony/crypto/hash" vrf_bls "github.com/harmony-one/harmony/crypto/vrf/bls" ) diff --git a/test/deploy.sh b/test/deploy.sh index b06e35cf01..69a86ca5bc 100755 --- a/test/deploy.sh +++ b/test/deploy.sh @@ -11,8 +11,6 @@ ROOT="${progdir}/.." USER=$(whoami) OS=$(uname -s) -. "${ROOT}/scripts/setup_bls_build_flags.sh" - function cleanup() { if [[ "${CLEAN_START}" == "true" ]]; then "${progdir}/kill_node.sh" diff --git a/test/deploy_newnode.sh b/test/deploy_newnode.sh index a678a5314a..29a1bdf83c 100755 --- a/test/deploy_newnode.sh +++ b/test/deploy_newnode.sh @@ -3,8 +3,6 @@ ROOT=$(dirname $0)/.. USER=$(whoami) -. "${ROOT}/scripts/setup_bls_build_flags.sh" - set -x set -eo pipefail diff --git a/test/helpers/p2p.go b/test/helpers/p2p.go index aaf087452c..6bb9af06a7 100644 --- a/test/helpers/p2p.go +++ b/test/helpers/p2p.go @@ -1,8 +1,8 @@ package helpers import ( - "github.com/harmony-one/bls/ffi/go/bls" harmony_bls "github.com/harmony-one/harmony/crypto/bls" + bls "github.com/harmony-one/harmony/crypto/bls/core" nodeconfig "github.com/harmony-one/harmony/internal/configs/node" "github.com/harmony-one/harmony/p2p" libp2p_crypto "github.com/libp2p/go-libp2p/core/crypto"