diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 52d6e2c62..a71d04fe3 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,5 @@ --- -name: Bug report -about: Create a report to help us improve +name: Bug report about: Create a report to help us improve --- @@ -9,6 +8,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -21,8 +21,9 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. Ubuntu 16.04] - - Minter Version [e.g. 22] + +- OS: [e.g. Ubuntu 16.04] +- Minter Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/m2-testnet.yml b/.github/workflows/m2-testnet.yml new file mode 100644 index 000000000..0cd1716a7 --- /dev/null +++ b/.github/workflows/m2-testnet.yml @@ -0,0 +1,75 @@ +name: m2-testnet + +on: + push: + branches: [ v2.0 ] + +jobs: + build: + name: Build & Deploy + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.15 + uses: actions/setup-go@v2 + with: + go-version: ^1.15 + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: | + go mod download + + - name: Build + env: + CGO_ENABLED: 1 + run: go build -tags 'minter gcc' -ldflags "-s -w -X github.com/MinterTeam/minter-go-node/version.GitCommit=${{ github.sha }}" -v -o 'build/minter' ./cmd/minter + + - name: Copy file via scp + uses: appleboy/scp-action@master + env: + HOST: ${{ secrets.NODE_V2_HOST }} + USERNAME: ${{ secrets.NODE_V2_USERNAME }} + PORT: ${{ secrets.NODE_V2_PORT }} + KEY: ${{ secrets.NODE_V2_SSH }} + with: + source: "build" + target: "/tmp/node" + rm: true + + - name: Rename old build + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.NODE_V2_HOST }} + USERNAME: ${{ secrets.NODE_V2_USERNAME }} + PORT: ${{ secrets.NODE_V2_PORT }} + KEY: ${{ secrets.NODE_V2_SSH }} + script: mv /opt/minter/node/minter /opt/minter/node/minter_${{ github.sha }} + + - name: Copy new build + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.NODE_V2_HOST }} + USERNAME: ${{ secrets.NODE_V2_USERNAME }} + PORT: ${{ secrets.NODE_V2_PORT }} + KEY: ${{ secrets.NODE_V2_SSH }} + script: mv /tmp/node/build/minter /opt/minter/node/minter + + - name: Restart service + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.NODE_V2_HOST }} + USERNAME: ${{ secrets.NODE_V2_USERNAME }} + PORT: ${{ secrets.NODE_V2_PORT }} + KEY: ${{ secrets.NODE_V2_SSH }} + script: sudo systemctl restart minter-node.service + + - name: notification + if: cancelled() == false + uses: xinthink/action-telegram@v1.1 + with: + botToken: ${{ secrets.TELEGRAM_CI_TOKEN }} + chatId: ${{ secrets.TELEGRAM_CI_TO }} + jobStatus: ${{ job.status }} + skipSuccess: false \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6a09ad1ea..e7666638c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -25,7 +25,7 @@ jobs: uses: actions/checkout@v1 with: fetch-depth: 1 - + # if secret DOCKER_HUB_REPO is not set DOCKER_HUB_USER will be used instead of REPO # otherwise secrets are empty and repo "testbuild" will be used - name: Set envs diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9a2f211..336ef4942 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ BUG FIXES IMPROVEMENT -- [tendermint] Upgrade to [v0.33.3](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0333) +- [tendermint] Upgrade to [v0.33.3](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0333) BUG FIXES @@ -82,7 +82,7 @@ BREAKING CHANGES IMPROVEMENT -- [tendermint] Upgrade to [v0.33.2](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0332) +- [tendermint] Upgrade to [v0.33.2](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0332) ## 1.1.0 @@ -104,7 +104,7 @@ BREAKING CHANGES - [gui] Remove GUI - [config] KeepStateHistory -> KeepLastStates - [config] Add state_cache_size option -- [tendermint] Upgrade to [v0.33.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0331) +- [tendermint] Upgrade to [v0.33.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0331) ## 1.0.5 @@ -122,7 +122,7 @@ IMPROVEMENT IMPROVEMENT -- [tendermint] Update to [v0.32.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0321) +- [tendermint] Update to [v0.32.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0321) - [api] Add page and perPage params to /api/transactions (@Danmer) - [cmd] Add `minter version` command @@ -186,7 +186,7 @@ BUG FIXES IMPROVEMENT -- [core] Add remainder to total slashed +- [core] Add remainder to total slashed - [cmd] Add `--network-id` flag ## 0.19.2 @@ -211,7 +211,7 @@ IMPROVEMENT BUG FIXES - [core] Fix incorrect coin conversion -- [tendermint] Update to [v0.31.5](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0315) +- [tendermint] Update to [v0.31.5](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0315) ## 0.18.1 @@ -244,7 +244,7 @@ BUG FIXES - [core] Set start height for validators count - [core] Add value to existing basecoin stake if exists when deleting coin instead of creating new one - [core] Fix issue with coin deletion -- [tendermint] Update to [v0.31.3](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0313) +- [tendermint] Update to [v0.31.3](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0313) ## 0.16.0 @@ -267,14 +267,14 @@ BUG FIXES - [core] Fix issue with multiple punishments to byzantine validator - [core] Make accum reward of dropped validator distributes again between active ones -- [tendermint] Update to [v0.31.2](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0312) +- [tendermint] Update to [v0.31.2](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0312) ## 0.15.2 IMPROVEMENT - [cmd] `--show_validator` flag now returns hex public key of a validator -- [tendermint] Update to [v0.31.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0311) +- [tendermint] Update to [v0.31.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0311) ## 0.15.1 @@ -286,7 +286,7 @@ IMPROVEMENT BREAKING CHANGES -- [tendermint] Update to [v0.31.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0310) +- [tendermint] Update to [v0.31.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0310) IMPROVEMENT @@ -325,7 +325,7 @@ IMPROVEMENT - [api] Add /addresses endpoint - [api] Add evidence data to /block -- [tendermint] Update to [v0.30.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0301) +- [tendermint] Update to [v0.30.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0301) BUG FIXES @@ -341,7 +341,7 @@ BUG FIXES BREAKING CHANGES -- [tendermint] Update to [v0.30.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0300) +- [tendermint] Update to [v0.30.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0300) BUG FIXES @@ -367,23 +367,25 @@ BREAKING CHANGES BREAKING CHANGES - [core] Fix coin convert issue -- [tendermint] Update to [v0.29.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0291) +- [tendermint] Update to [v0.29.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0291) ## 0.10.1 + *Jan 22th, 2019* BREAKING CHANGES -- [tendermint] Update to [v0.29.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0290) +- [tendermint] Update to [v0.29.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0290) ## 0.10.0 + *Jan 20th, 2019* BREAKING CHANGES - [core] Add EditCandidate transaction - [core] Make validators count logic conforms to mainnet -- [tendermint] Update to [v0.28.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0281) +- [tendermint] Update to [v0.28.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0281) BUG FIXES @@ -397,6 +399,7 @@ IMPROVEMENT - [rpc] Prettify RPC errors ## 0.9.6 + *Dec 27th, 2018* BUG FIXES @@ -404,6 +407,7 @@ BUG FIXES - [core] Fix issue with corrupted db ## 0.9.5 + *Dec 26th, 2018* BUG FIXES @@ -411,6 +415,7 @@ BUG FIXES - [core] Fix issue with corrupted db ## 0.9.4 + *Dec 26th, 2018* IMPROVEMENT @@ -422,6 +427,7 @@ BUG FIXES - [core] Fix issue with bag tx occupying mempool ## 0.9.3 + *Dec 25th, 2018* BUG FIXES @@ -429,6 +435,7 @@ BUG FIXES - [core] Fix sell all coin tx ## 0.9.2 + *Dec 25th, 2018* BUG FIXES @@ -436,6 +443,7 @@ BUG FIXES - [core] Increase max block bytes ## 0.9.1 + *Dec 24th, 2018* BUG FIXES @@ -443,6 +451,7 @@ BUG FIXES - [api] Fix create coin tx error ## 0.9.0 + *Dec 24th, 2018* IMPROVEMENT @@ -465,6 +474,7 @@ BUG FIXES - [api] Fix tx tags ## 0.8.5 + *Dec 11th, 2018* BUG FIXES @@ -473,6 +483,7 @@ BUG FIXES - [api] Set quotes as not necessary attribute ## 0.8.4 + *Dec 10th, 2018* BUG FIXES @@ -480,6 +491,7 @@ BUG FIXES - [core] Fix tx processing bug ## 0.8.3 + *Dec 10th, 2018* BUG FIXES @@ -487,6 +499,7 @@ BUG FIXES - [events] Fix pub key formatting in API ## 0.8.2 + *Dec 10th, 2018* BUG FIXES @@ -494,6 +507,7 @@ BUG FIXES - [log] Add json log format ## 0.8.1 + *Dec 10th, 2018* IMPROVEMENT @@ -505,6 +519,7 @@ BUG FIXES - [config] Change default seed node ## 0.8.0 + *Dec 3rd, 2018* BREAKING CHANGES @@ -515,7 +530,7 @@ BREAKING CHANGES - [core] Limit coins supply to 1,000,000,000,000,000 - [core] Set minimal reserve and min/max coin supply in CreateCoin tx - [core] Add MinimumValueToBuy and MaximumValueToSell to convert transactions -- [tendermint] Update to [v0.27.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0270) +- [tendermint] Update to [v0.27.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0270) IMPROVEMENT @@ -523,17 +538,19 @@ IMPROVEMENT - [events] Add UnbondEvent ## 0.7.6 + *Nov 27th, 2018* IMPROVEMENT -- [tendermint] Update to [v0.26.4](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0264) +- [tendermint] Update to [v0.26.4](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0264) BUG FIXES - [node] Fix issue [#168](https://github.com/MinterTeam/minter-go-node/issues/168) with unexpected database corruption ## 0.7.5 + *Nov 22th, 2018* BUG FIXES @@ -541,6 +558,7 @@ BUG FIXES - [api] Fix issue in which transaction appeared in `/api/transaction` before actual execution ## 0.7.4 + *Nov 20th, 2018* BUG FIXES @@ -548,10 +566,11 @@ BUG FIXES - [tendermint] "Send failed" is logged at debug level instead of error - [tendermint] Set connection config properly instead of always using default - [tendermint] Seed mode fixes: - - Only disconnect from inbound peers - - Use FlushStop instead of Sleep to ensure all messages are sent before disconnecting + - Only disconnect from inbound peers + - Use FlushStop instead of Sleep to ensure all messages are sent before disconnecting ## 0.7.3 + *Nov 18th, 2018* BUG FIXES @@ -559,6 +578,7 @@ BUG FIXES - [core] More fixes on issue with negative coin reserve ## 0.7.2 + *Nov 18th, 2018* BUG FIXES @@ -566,37 +586,43 @@ BUG FIXES - [core] Fix issue with negative coin reserve ## 0.7.1 + *Nov 16th, 2018* IMPROVEMENT -- [tendermint] Update to [v0.26.2](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0262) + +- [tendermint] Update to [v0.26.2](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0262) ## 0.7.0 + *Nov 15th, 2018* BREAKING CHANGES -- [api] `/api/sendTransaction` is now returns only `checkTx` result. Applications are now forced to manually check if transaction is included in blockchain. -- [tendermint] Update to [v0.26.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0261) +- [api] `/api/sendTransaction` is now returns only `checkTx` result. Applications are now forced to manually check if + transaction is included in blockchain. +- [tendermint] Update to [v0.26.1](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0261) - [core] Block hash is now 32 bytes length IMPROVEMENT - [core] Add `MultisendTx` - [core] Add special cases to Formulas [#140](https://github.com/MinterTeam/minter-go-node/issues/140) -- [core] Stake unbond now instant after dropping of from 1,000st place [#146](https://github.com/MinterTeam/minter-go-node/issues/146) +- [core] Stake unbond now instant after dropping of from 1,000st + place [#146](https://github.com/MinterTeam/minter-go-node/issues/146) - [p2p] Default send and receive rates are now 15mB/s - [mempool] Set max mempool size to 10,000txs - [gui] Small GUI improvements ## 0.6.0 + *Oct 30th, 2018* BREAKING CHANGES - [core] Set validators limit to 100 for testnet - [core] SetCandidateOff transaction now applies immediately -- [tendermint] Update to [v0.26.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0260) +- [tendermint] Update to [v0.26.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0260) IMPROVEMENT @@ -604,6 +630,7 @@ IMPROVEMENT - [api] Limit API requests ## 0.5.1 + *Oct 22th, 2018* BUG FIXES @@ -611,6 +638,7 @@ BUG FIXES - [core] Fixed bug with unexpected node backoff ## 0.5.0 + *Oct 15th, 2018* BREAKING CHANGES @@ -623,9 +651,10 @@ BREAKING CHANGES IMPROVEMENT -- [tendermint] Update to [v0.25.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0250) +- [tendermint] Update to [v0.25.0](https://github.com/tendermint/tendermint/blob/master/CHANGELOG.md#v0250) ## 0.4.2 + *Sept 21th, 2018* BUG FIXES @@ -633,17 +662,19 @@ BUG FIXES - [api] Fix concurrent API calls ## 0.4.1 + *Sept 20th, 2018* IMPROVEMENT -- [core] Speed up synchronization +- [core] Speed up synchronization BUG FIXES - [gui] Fix validator status ## 0.4.0 + *Sept 18th, 2018* BREAKING CHANGES @@ -659,6 +690,7 @@ IMPROVEMENT - [gui] Add validator status ## 0.3.8 + *Sept 17th, 2018* BUG FIXES @@ -666,6 +698,7 @@ BUG FIXES - [core] Proper handle of db errors ## 0.3.7 + *Sept 17th, 2018* IMPROVEMENT @@ -673,6 +706,7 @@ IMPROVEMENT - [core] Performance update ## 0.3.6 + *Sept 15th, 2018* BUG FIXES @@ -680,6 +714,7 @@ BUG FIXES - [core] Critical fix ## 0.3.5 + *Sept 13th, 2018* IMPROVEMENT @@ -687,6 +722,7 @@ IMPROVEMENT - [api] Add Code and Log fields in transaction api ## 0.3.4 + *Sept 13th, 2018* IMPROVEMENT @@ -695,15 +731,17 @@ IMPROVEMENT - [api] Refactor api ## 0.3.3 + *Sept 8th, 2018* IMPROVEMENT - [api] Add block size in bytes -- [api] [#100](https://github.com/MinterTeam/minter-go-node/issues/100) Add "events" to block response. To get events add ?withEvents=true to request URL. -WARNING! You should sync blockchain from scratch to get this feature working +- [api] [#100](https://github.com/MinterTeam/minter-go-node/issues/100) Add "events" to block response. To get events + add ?withEvents=true to request URL. WARNING! You should sync blockchain from scratch to get this feature working ## 0.3.2 + *Sept 8th, 2018* BUG FIXES @@ -711,6 +749,7 @@ BUG FIXES - [core] Fix null pointer exception ## 0.3.1 + *Sept 8th, 2018* BUG FIXES @@ -718,12 +757,13 @@ BUG FIXES - [core] Fix shutdown issue ## 0.3.0 + *Sept 8th, 2018* BREAKING CHANGES - [core] Validators are now updated each 120 blocks -- [core] Validators are now updated then at least one of current validators exceed 12 missed blocks +- [core] Validators are now updated then at least one of current validators exceed 12 missed blocks - [tendermint] Update Tendermint to v0.24.0 IMPROVEMENT @@ -749,6 +789,7 @@ BUG FIXES - [api] Fix issue [#82](https://github.com/MinterTeam/minter-go-node/issues/82) ## 0.2.4 + *Aug 24th, 2018* BUG FIXES @@ -760,10 +801,10 @@ IMPROVEMENT - [gui] Minor GUI updates ## 0.2.2 + *Aug 23th, 2018* -In this update we well test blockchain's hardfork. -There is no need to wipe old data, just be sure to update binary +In this update we well test blockchain's hardfork. There is no need to wipe old data, just be sure to update binary until 15000 block. BUG FIXES @@ -771,10 +812,10 @@ BUG FIXES - [validators] Fix api ## 0.2.1 + *Aug 23th, 2018* -In this update we well test blockchain's hardfork. -There is no need to wipe old data, just be sure to update binary +In this update we well test blockchain's hardfork. There is no need to wipe old data, just be sure to update binary until 15000 block. BUG FIXES @@ -782,6 +823,7 @@ BUG FIXES - [validators] Fix validators issue ## 0.2.0 + *Aug 22th, 2018* BREAKING CHANGES @@ -789,7 +831,7 @@ BREAKING CHANGES - [testnet] New testnet id - [core] New rewards - [core] Validators list are now updated each 12 blocks -- [core] Set DAO commission to 10% +- [core] Set DAO commission to 10% - [core] Add Developers commission of 10% - [core] Now stake of custom coin is calculated by selling all such staked coins - [api] Reformatted candidates and validators endpoints @@ -805,18 +847,23 @@ IMPROVEMENT - [gui] Minor GUI update ## 0.1.9 + *Aug 19th, 2018* BUG FIXES + - [core] Critical fix ## 0.1.8 + *Aug 4th, 2018* BUG FIXES + - [core] Critical fix ## 0.1.7 + *Jule 30th, 2018* BREAKING CHANGES @@ -829,6 +876,7 @@ IMPROVEMENT - [testnet] Main validator stake is set to 1 mln MNT by default ## 0.1.6 + *Jule 30th, 2018* BREAKING CHANGES @@ -840,6 +888,7 @@ BUG FIXES - [core] Fixed critical bug ## 0.1.5 + *Jule 28th, 2018* BUG FIXES @@ -848,6 +897,7 @@ BUG FIXES - [core] Temporary critical fix ## 0.1.4 + *Jule 25th, 2018* IMPROVEMENT @@ -855,6 +905,7 @@ IMPROVEMENT - [tendermint] Update tendermint to 0.22.6 ## 0.1.3 + *Jule 25th, 2018* IMPROVEMENT @@ -862,6 +913,7 @@ IMPROVEMENT - [tendermint] Update tendermint to 0.22.5 ## 0.1.0 + *Jule 23th, 2018* BREAKING CHANGES @@ -887,6 +939,7 @@ BUG FIXES - [api] Fixed raw transaction output ## 0.0.6 + *Jule 16th, 2018* BREAKING CHANGES @@ -908,13 +961,14 @@ IMPROVEMENT - [client] Add --disable-api flag to client ## 0.0.5 + *Jule 4rd, 2018* BREAKING CHANGES - [core] Remove Reserve Coin from coin object. All coins should be reserved with base coin - [core] Limit tx payload and service data to 128 bytes -- [core] Fix critical issue with instant convert of 2 custom coins +- [core] Fix critical issue with instant convert of 2 custom coins - [testnet] New testnet chain id (minter-test-network-9) - [tendermint] Switched to v0.22.0 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index cdc3d055d..f0568015c 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -2,7 +2,10 @@ ## Our Pledge -In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making +participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, +disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, +religion, or sexual identity and orientation. ## Our Standards @@ -24,23 +27,35 @@ Examples of unacceptable behavior by participants include: ## Our Responsibilities -Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take +appropriate and fair corrective action in response to any instances of unacceptable behavior. -Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, +issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any +contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope -This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the +project or its community. Examples of representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed representative at an online or offline +event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dl@decenter.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at +dl@decenter.org. The project team will review and investigate all complaints, and will respond in a way that it deems +appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter +of an incident. Further details of specific enforcement policies may be posted separately. -Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent +repercussions as determined by other members of the project's leadership. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available +at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org + [version]: http://contributor-covenant.org/version/1/4/ diff --git a/README.md b/README.md index ed0773806..1bbefd6ad 100644 --- a/README.md +++ b/README.md @@ -10,23 +10,30 @@ Docker Pulls

-Minter is a global rewards and loyalty points network powered by a fast blockchain. Any brand, community, or blogger can create their own coins and launch their reward or loyalty system in minutes. +Minter is a global rewards and loyalty points network powered by a fast blockchain. Any brand, community, or blogger can +create their own coins and launch their reward or loyalty system in minutes. _NOTE: This is alpha software. Please contact us if you intend to run it in production._ ## Installation + ### Docker -1. Grab latest docker-compose, save docker-compose.yml and run ```docker-compose up -d```. -To run it in production we recommend to use bind host mount instead of volume. + +1. Grab latest docker-compose, + save + docker-compose.yml and run ```docker-compose up -d```. To run it in production we recommend to use bind host + mount instead of volume. 2. To build from source clone this repo, make your changes and run ```docker-compose up --build -d``` ### Manual + You can get official installation instructions in our [docs](https://docs.minter.network/#section/Install-Minter). 1. Download Minter Node - Get [latest binary build](https://github.com/MinterTeam/minter-go-node/releases) suitable for your architecture and unpack it to desired folder. + Get [latest binary build](https://github.com/MinterTeam/minter-go-node/releases) suitable for your architecture and + unpack it to desired folder. 2. Run Minter Node @@ -50,6 +57,7 @@ You can get official installation instructions in our [docs](https://docs.minter - [node-grpc-gateway](https://github.com/MinterTeam/node-grpc-gateway) - gRPC interface and Swagger for Node API v2 ### Community + - [Telegram Channel (English)](https://t.me/MinterTeam) - [Telegram Channel (Russian)](https://t.me/MinterNetwork) - [Telegram Chat (English)](http://t.me/joinchat/EafyERJSJZJ-nwH_139jLQ) @@ -59,20 +67,19 @@ You can get official installation instructions in our [docs](https://docs.minter ### SemVer -Minter uses [SemVer](http://semver.org/) to determine when and how the version changes. -According to SemVer, anything in the public API can change at any time before version 1.0.0 +Minter uses [SemVer](http://semver.org/) to determine when and how the version changes. According to SemVer, anything in +the public API can change at any time before version 1.0.0 -To provide some stability to Minter users in these 0.X.X days, the MINOR version is used -to signal breaking changes across a subset of the total public API. This subset includes all -interfaces exposed to other processes, but does not include the in-process Go APIs. +To provide some stability to Minter users in these 0.X.X days, the MINOR version is used to signal breaking changes +across a subset of the total public API. This subset includes all interfaces exposed to other processes, but does not +include the in-process Go APIs. ### Upgrades -In an effort to avoid accumulating technical debt prior to 1.0.0, -we do not guarantee that breaking changes (ie. bumps in the MINOR version) -will work with existing blockchain. In these cases you will -have to start a new blockchain, or write something custom to get the old -data into the new chain. +In an effort to avoid accumulating technical debt prior to 1.0.0, we do not guarantee that breaking changes (ie. bumps +in the MINOR version) +will work with existing blockchain. In these cases you will have to start a new blockchain, or write something custom to +get the old data into the new chain. However, any bump in the PATCH version should be compatible with existing histories (if not please open an [issue](https://github.com/MinterTeam/minter-go-node/issues)). diff --git a/api/address.go b/api/address.go deleted file mode 100644 index 2322618df..000000000 --- a/api/address.go +++ /dev/null @@ -1,61 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/types" -) - -type AddressResponse struct { - Balance []BalanceItem `json:"balances"` - TransactionCount uint64 `json:"transaction_count"` -} - -type BalanceItem struct { - CoinID uint32 `json:"coin_id"` - Symbol string `json:"symbol"` - Value string `json:"value"` -} - -type Coin struct { - ID uint32 `json:"id"` - Symbol string `json:"symbol"` -} - -func Address(address types.Address, height int) (*AddressResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - balances := cState.Accounts().GetBalances(address) - - response := AddressResponse{ - Balance: make([]BalanceItem, len(balances)), - TransactionCount: cState.Accounts().GetNonce(address), - } - - isBaseCoinExists := false - for k, b := range balances { - response.Balance[k] = BalanceItem{ - CoinID: b.Coin.ID.Uint32(), - Symbol: b.Coin.GetFullSymbol(), - Value: b.Value.String(), - } - - if b.Coin.ID.IsBaseCoin() { - isBaseCoinExists = true - } - } - - if !isBaseCoinExists { - response.Balance = append(response.Balance, BalanceItem{ - CoinID: types.GetBaseCoinID().Uint32(), - Symbol: types.GetBaseCoin().String(), - Value: "0", - }) - } - - return &response, nil -} diff --git a/api/addresses.go b/api/addresses.go deleted file mode 100644 index 13940e303..000000000 --- a/api/addresses.go +++ /dev/null @@ -1,58 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/types" -) - -type AddressesResponse struct { - Address string `json:"address"` - Balance []BalanceItem `json:"balance"` - TransactionCount uint64 `json:"transaction_count"` -} - -func Addresses(addresses []types.Address, height int) (*[]AddressesResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - response := make([]AddressesResponse, len(addresses)) - - for i, address := range addresses { - balances := cState.Accounts().GetBalances(address) - - data := AddressesResponse{ - Address: address.String(), - Balance: make([]BalanceItem, len(balances)), - TransactionCount: cState.Accounts().GetNonce(address), - } - - isBaseCoinExists := false - for k, b := range balances { - data.Balance[k] = BalanceItem{ - CoinID: b.Coin.ID.Uint32(), - Symbol: b.Coin.GetFullSymbol(), - Value: b.Value.String(), - } - - if b.Coin.ID.IsBaseCoin() { - isBaseCoinExists = true - } - } - - if !isBaseCoinExists { - data.Balance = append(data.Balance, BalanceItem{ - CoinID: types.GetBaseCoinID().Uint32(), - Symbol: types.GetBaseCoin().String(), - Value: "0", - }) - } - - response[i] = data - } - - return &response, nil -} diff --git a/api/api.go b/api/api.go deleted file mode 100644 index fb1cfc834..000000000 --- a/api/api.go +++ /dev/null @@ -1,141 +0,0 @@ -package api - -import ( - "fmt" - "github.com/MinterTeam/minter-go-node/config" - "github.com/MinterTeam/minter-go-node/core/minter" - "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/rpc/lib/server" - "github.com/rs/cors" - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/libs/log" - rpc "github.com/tendermint/tendermint/rpc/client/local" - "net/http" - "net/url" - "strings" - "time" -) - -var ( - cdc = amino.NewCodec() - blockchain *minter.Blockchain - client *rpc.Local - minterCfg *config.Config -) - -var Routes = map[string]*rpcserver.RPCFunc{ - "status": rpcserver.NewRPCFunc(Status, ""), - "candidates": rpcserver.NewRPCFunc(Candidates, "height,include_stakes"), - "candidate": rpcserver.NewRPCFunc(Candidate, "pub_key,height"), - "validators": rpcserver.NewRPCFunc(Validators, "height"), - "address": rpcserver.NewRPCFunc(Address, "address,height"), - "addresses": rpcserver.NewRPCFunc(Addresses, "addresses,height"), - "send_transaction": rpcserver.NewRPCFunc(SendTransaction, "tx"), - "transaction": rpcserver.NewRPCFunc(Transaction, "hash"), - "transactions": rpcserver.NewRPCFunc(Transactions, "query,page,perPage"), - "block": rpcserver.NewRPCFunc(Block, "height"), - "events": rpcserver.NewRPCFunc(Events, "height"), - "net_info": rpcserver.NewRPCFunc(NetInfo, ""), - "coin_info": rpcserver.NewRPCFunc(CoinInfo, "symbol,id,height"), - "estimate_coin_sell": rpcserver.NewRPCFunc(EstimateCoinSell, "coin_to_sell,coin_to_buy,value_to_sell,height"), - "estimate_coin_sell_all": rpcserver.NewRPCFunc(EstimateCoinSellAll, "coin_to_sell,coin_to_buy,value_to_sell,height"), - "estimate_coin_buy": rpcserver.NewRPCFunc(EstimateCoinBuy, "coin_to_sell,coin_to_buy,value_to_buy,height"), - "estimate_tx_commission": rpcserver.NewRPCFunc(EstimateTxCommission, "tx,height"), - "unconfirmed_txs": rpcserver.NewRPCFunc(UnconfirmedTxs, "limit"), - "max_gas": rpcserver.NewRPCFunc(MaxGas, "height"), - "min_gas_price": rpcserver.NewRPCFunc(MinGasPrice, ""), - "genesis": rpcserver.NewRPCFunc(Genesis, ""), - "missed_blocks": rpcserver.NewRPCFunc(MissedBlocks, "pub_key,height"), - "waitlist": rpcserver.NewRPCFunc(Waitlist, "pub_key,address,height"), -} - -func responseTime(b *minter.Blockchain) func(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { - return func(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - f(w, r) - go b.StatisticData().SetApiTime(time.Since(start), r.URL.Path) - } - } -} - -// RunAPI start -func RunAPI(codec *amino.Codec, b *minter.Blockchain, tmRPC *rpc.Local, cfg *config.Config, logger log.Logger) { - cdc = codec - minterCfg = cfg - client = tmRPC - blockchain = b - waitForTendermint() - - m := http.NewServeMux() - - rpcserver.RegisterRPCFuncs(m, Routes, cdc, logger.With("module", "rpc"), responseTime(b)) - listener, err := rpcserver.Listen(cfg.APIListenAddress, rpcserver.Config{ - MaxOpenConnections: cfg.APISimultaneousRequests, - }) - - if err != nil { - panic(err) - } - - c := cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{"POST", "GET"}, - AllowCredentials: true, - }) - - handler := c.Handler(m) - logger.Error("Failed to start API", "err", rpcserver.StartHTTPServer(listener, Handler(handler), logger)) -} - -func Handler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - query := r.URL.Query() - - for key, value := range query { - val := value[0] - if strings.HasPrefix(val, "Mx") { - query.Set(key, fmt.Sprintf("\"%s\"", val)) - } - - if strings.HasPrefix(val, "Mp") { - query.Set(key, fmt.Sprintf("\"%s\"", val)) - } - } - - var err error - r.URL, err = url.ParseRequestURI(fmt.Sprintf("%s?%s", r.URL.Path, query.Encode())) - if err != nil { - panic(err) - } - - h.ServeHTTP(w, r) - }) -} - -func waitForTendermint() { - for { - _, err := client.Health() - if err == nil { - break - } - - time.Sleep(1 * time.Second) - } -} - -type Response struct { - Code uint32 `json:"code"` - Result interface{} `json:"result,omitempty"` - Log string `json:"log,omitempty"` -} - -func GetStateForHeight(height int) (*state.CheckState, error) { - if height > 0 { - cState, err := blockchain.GetStateForHeight(uint64(height)) - - return cState, err - } - - return blockchain.CurrentState(), nil -} diff --git a/api/block.go b/api/block.go deleted file mode 100644 index cc08ce71a..000000000 --- a/api/block.go +++ /dev/null @@ -1,174 +0,0 @@ -package api - -import ( - "bytes" - "encoding/hex" - "encoding/json" - "fmt" - "time" - - "github.com/MinterTeam/minter-go-node/core/rewards" - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/core/transaction/encoder" - "github.com/MinterTeam/minter-go-node/core/types" - rpctypes "github.com/MinterTeam/minter-go-node/rpc/lib/types" - core_types "github.com/tendermint/tendermint/rpc/core/types" - tmTypes "github.com/tendermint/tendermint/types" -) - -type BlockResponse struct { - Hash string `json:"hash"` - Height int64 `json:"height"` - Time time.Time `json:"time"` - NumTxs int64 `json:"num_txs"` - Transactions []BlockTransactionResponse `json:"transactions"` - BlockReward string `json:"block_reward"` - Size int `json:"size"` - Proposer *string `json:"proposer,omitempty"` - Validators []BlockValidatorResponse `json:"validators,omitempty"` - Evidence tmTypes.EvidenceData `json:"evidence,omitempty"` -} - -type BlockTransactionResponse struct { - Hash string `json:"hash"` - RawTx string `json:"raw_tx"` - From string `json:"from"` - Nonce uint64 `json:"nonce"` - GasPrice uint32 `json:"gas_price"` - Type uint8 `json:"type"` - Data json.RawMessage `json:"data"` - Payload []byte `json:"payload"` - ServiceData []byte `json:"service_data"` - Gas int64 `json:"gas"` - GasCoin string `json:"gas_coin"` - Tags map[string]string `json:"tags"` - Code uint32 `json:"code,omitempty"` - Log string `json:"log,omitempty"` -} - -type BlockValidatorResponse struct { - Pubkey string `json:"pub_key"` - Signed bool `json:"signed"` -} - -func Block(height int64) (*BlockResponse, error) { - block, err := client.Block(&height) - if err != nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Block not found", Data: err.Error()} - } - - blockResults, err := client.BlockResults(&height) - if err != nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Block results not found", Data: err.Error()} - } - - valHeight := height - 1 - if valHeight < 1 { - valHeight = 1 - } - - tmValidators, err := client.Validators(&valHeight, 1, 100) - if err != nil { - return nil, rpctypes.RPCError{Code: 500, Message: err.Error()} - } - totalValidators := tmValidators.Validators - - cState, err := GetStateForHeight(0) - if err != nil { - return nil, err - } - - txJsonEncoder := encoder.NewTxEncoderJSON(cState) - - txs := make([]BlockTransactionResponse, len(block.Block.Data.Txs)) - for i, rawTx := range block.Block.Data.Txs { - tx, _ := transaction.TxDecoder.DecodeFromBytes(rawTx) - sender, _ := tx.Sender() - - if len(blockResults.TxsResults) == 0 { - break - } - - tags := make(map[string]string) - for _, tag := range blockResults.TxsResults[i].Events[0].Attributes { - tags[string(tag.Key)] = string(tag.Value) - } - - data, err := txJsonEncoder.EncodeData(tx) - if err != nil { - return nil, err - } - - txs[i] = BlockTransactionResponse{ - Hash: fmt.Sprintf("Mt%x", rawTx.Hash()), - RawTx: fmt.Sprintf("%x", []byte(rawTx)), - From: sender.String(), - Nonce: tx.Nonce, - GasPrice: tx.GasPrice, - Type: uint8(tx.Type), - Data: data, - Payload: tx.Payload, - ServiceData: tx.ServiceData, - Gas: tx.Gas(), - GasCoin: tx.GasCoin.String(), - Tags: tags, - Code: blockResults.TxsResults[i].Code, - Log: blockResults.TxsResults[i].Log, - } - } - - var validators []BlockValidatorResponse - var proposer *string - if height > 1 { - p, err := getBlockProposer(block, totalValidators) - if err != nil { - return nil, err - } - - if p != nil { - str := p.String() - proposer = &str - } - - validators = make([]BlockValidatorResponse, len(totalValidators)) - for i, tmval := range totalValidators { - signed := false - for _, vote := range block.Block.LastCommit.Signatures { - if bytes.Equal(vote.ValidatorAddress.Bytes(), tmval.Address.Bytes()) { - signed = true - break - } - } - - validators[i] = BlockValidatorResponse{ - Pubkey: fmt.Sprintf("Mp%x", tmval.PubKey.Bytes()[5:]), - Signed: signed, - } - } - } - - return &BlockResponse{ - Hash: hex.EncodeToString(block.Block.Hash()), - Height: block.Block.Height, - Time: block.Block.Time, - NumTxs: int64(len(block.Block.Txs)), - Transactions: txs, - BlockReward: rewards.GetRewardForBlock(uint64(height)).String(), - Size: len(cdc.MustMarshalBinaryLengthPrefixed(block)), - Proposer: proposer, - Validators: validators, - Evidence: block.Block.Evidence, - }, nil -} - -func getBlockProposer(block *core_types.ResultBlock, vals []*tmTypes.Validator) (*types.Pubkey, error) { - for _, tmval := range vals { - if bytes.Equal(tmval.Address.Bytes(), block.Block.ProposerAddress.Bytes()) { - var result types.Pubkey - copy(result[:], tmval.PubKey.Bytes()[5:]) - return &result, nil - } - } - - return nil, rpctypes.RPCError{Code: 404, Message: "Block proposer not found"} -} diff --git a/api/candidate.go b/api/candidate.go deleted file mode 100644 index 4172e4723..000000000 --- a/api/candidate.go +++ /dev/null @@ -1,79 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/core/state/candidates" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/rpc/lib/types" -) - -type Stake struct { - Owner string `json:"owner"` - Coin Coin `json:"coin"` - Value string `json:"value"` - BipValue string `json:"bip_value"` -} - -type CandidateResponse struct { - RewardAddress string `json:"reward_address"` - OwnerAddress string `json:"owner_address"` - TotalStake string `json:"total_stake"` - PubKey string `json:"pub_key"` - Commission uint32 `json:"commission"` - Stakes []Stake `json:"stakes,omitempty"` - Status byte `json:"status"` -} - -func makeResponseCandidate(state *state.CheckState, c candidates.Candidate, includeStakes bool) CandidateResponse { - candidate := CandidateResponse{ - RewardAddress: c.RewardAddress.String(), - OwnerAddress: c.OwnerAddress.String(), - TotalStake: state.Candidates().GetTotalStake(c.PubKey).String(), - PubKey: c.PubKey.String(), - Commission: c.Commission, - Status: c.Status, - } - - if includeStakes { - stakes := state.Candidates().GetStakes(c.PubKey) - candidate.Stakes = make([]Stake, len(stakes)) - for i, stake := range stakes { - candidate.Stakes[i] = Stake{ - Owner: stake.Owner.String(), - Coin: Coin{ - ID: stake.Coin.Uint32(), - Symbol: state.Coins().GetCoin(stake.Coin).GetFullSymbol(), - }, - Value: stake.Value.String(), - BipValue: stake.BipValue.String(), - } - } - } - - return candidate -} - -func Candidate(pubkey types.Pubkey, height int) (*CandidateResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - if height != 0 { - cState.Lock() - cState.Candidates().LoadCandidates() - cState.Candidates().LoadStakesOfCandidate(pubkey) - cState.Unlock() - } - - cState.RLock() - defer cState.RUnlock() - - candidate := cState.Candidates().GetCandidate(pubkey) - if candidate == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Candidate not found"} - } - - response := makeResponseCandidate(cState, *candidate, true) - return &response, nil -} diff --git a/api/candidates.go b/api/candidates.go deleted file mode 100644 index 50455b552..000000000 --- a/api/candidates.go +++ /dev/null @@ -1,29 +0,0 @@ -package api - -func Candidates(height int, includeStakes bool) (*[]CandidateResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - if height != 0 { - cState.Lock() - cState.Candidates().LoadCandidates() - if includeStakes { - cState.Candidates().LoadStakes() - } - cState.Unlock() - } - - cState.RLock() - defer cState.RUnlock() - - candidates := cState.Candidates().GetCandidates() - - result := make([]CandidateResponse, len(candidates)) - for i, candidate := range candidates { - result[i] = makeResponseCandidate(cState, *candidate, includeStakes) - } - - return &result, nil -} diff --git a/api/coin_info.go b/api/coin_info.go deleted file mode 100644 index fae90461e..000000000 --- a/api/coin_info.go +++ /dev/null @@ -1,66 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/state/coins" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/rpc/lib/types" -) - -type CoinInfoResponse struct { - ID uint32 `json:"id"` - Name string `json:"name"` - Symbol string `json:"symbol"` - Volume string `json:"volume"` - Crr uint32 `json:"crr"` - ReserveBalance string `json:"reserve_balance"` - MaxSupply string `json:"max_supply"` - OwnerAddress *string `json:"owner_address"` -} - -func CoinInfo(coinSymbol *string, id *int, height int) (*CoinInfoResponse, error) { - var coin *coins.Model - - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - if coinSymbol == nil && id == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin not found"} - } - - if coinSymbol != nil { - coin = cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(*coinSymbol), types.GetVersionFromSymbol(*coinSymbol)) - if coin == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin not found"} - } - } - - if id != nil { - coin = cState.Coins().GetCoin(types.CoinID(*id)) - if coin == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin not found"} - } - } - - var ownerAddress *string - info := cState.Coins().GetSymbolInfo(coin.Symbol()) - if info != nil && info.OwnerAddress() != nil { - owner := info.OwnerAddress().String() - ownerAddress = &owner - } - - return &CoinInfoResponse{ - ID: coin.ID().Uint32(), - Name: coin.Name(), - Symbol: coin.GetFullSymbol(), - Volume: coin.Volume().String(), - Crr: coin.Crr(), - ReserveBalance: coin.Reserve().String(), - MaxSupply: coin.MaxSupply().String(), - OwnerAddress: ownerAddress, - }, nil -} diff --git a/api/estimate_coin_buy.go b/api/estimate_coin_buy.go deleted file mode 100644 index a6caa9c49..000000000 --- a/api/estimate_coin_buy.go +++ /dev/null @@ -1,80 +0,0 @@ -package api - -import ( - "fmt" - "github.com/MinterTeam/minter-go-node/core/commissions" - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "math/big" -) - -// EstimateCoinBuyResponse returns an estimate of buy coin transaction -type EstimateCoinBuyResponse struct { - WillPay string `json:"will_pay"` - Commission string `json:"commission"` -} - -// EstimateCoinBuy returns an estimate of buy coin transaction -func EstimateCoinBuy(coinToSell, coinToBuy string, valueToBuy *big.Int, height int) (*EstimateCoinBuyResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - coinFrom := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(coinToSell), types.GetVersionFromSymbol(coinToSell)) - if coinFrom == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin to sell not exists"} - } - - coinTo := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(coinToBuy), types.GetVersionFromSymbol(coinToBuy)) - if coinTo == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin to buy not exists"} - } - - if coinFrom.ID() == coinTo.ID() { - return nil, rpctypes.RPCError{Code: 400, Message: "\"From\" coin equals to \"to\" coin"} - } - - commissionInBaseCoin := big.NewInt(commissions.ConvertTx) - commissionInBaseCoin.Mul(commissionInBaseCoin, transaction.CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !coinFrom.ID().IsBaseCoin() { - if coinFrom.Reserve().Cmp(commissionInBaseCoin) < 0 { - return nil, rpctypes.RPCError{Code: 400, Message: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", - coinFrom.Reserve().String(), commissionInBaseCoin.String())} - } - commission = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), commissionInBaseCoin) - } - - var result *big.Int - - switch { - case coinTo.ID().IsBaseCoin(): - if coinFrom.Reserve().Cmp(valueToBuy) < 0 { - return nil, rpctypes.RPCError{Code: 400, Message: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", - coinFrom.Reserve().String(), valueToBuy.String())} - } - result = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToBuy) - case coinFrom.ID().IsBaseCoin(): - result = formula.CalculatePurchaseAmount(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), valueToBuy) - default: - baseCoinNeeded := formula.CalculatePurchaseAmount(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), valueToBuy) - if coinFrom.Reserve().Cmp(baseCoinNeeded) < 0 { - return nil, rpctypes.RPCError{Code: 400, Message: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", - coinFrom.Reserve().String(), baseCoinNeeded.String())} - } - - result = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), baseCoinNeeded) - } - - return &EstimateCoinBuyResponse{ - WillPay: result.String(), - Commission: commission.String(), - }, nil -} diff --git a/api/estimate_coin_sell.go b/api/estimate_coin_sell.go deleted file mode 100644 index ca447962e..000000000 --- a/api/estimate_coin_sell.go +++ /dev/null @@ -1,77 +0,0 @@ -package api - -import ( - "fmt" - "github.com/MinterTeam/minter-go-node/core/commissions" - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "math/big" -) - -// EstimateCoinSellResponse returns an estimate of sell coin transaction -type EstimateCoinSellResponse struct { - WillGet string `json:"will_get"` - Commission string `json:"commission"` -} - -// EstimateCoinSell returns an estimate of sell coin transaction -func EstimateCoinSell(coinToSell, coinToBuy string, valueToSell *big.Int, height int) (*EstimateCoinSellResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - coinFrom := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(coinToSell), types.GetVersionFromSymbol(coinToSell)) - if coinFrom == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin to sell not exists"} - } - - coinTo := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(coinToBuy), types.GetVersionFromSymbol(coinToBuy)) - if coinTo == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin to buy not exists"} - } - - if coinFrom.ID() == coinTo.ID() { - return nil, rpctypes.RPCError{Code: 400, Message: "\"From\" coin equals to \"to\" coin"} - } - - var result *big.Int - - commissionInBaseCoin := big.NewInt(commissions.ConvertTx) - commissionInBaseCoin.Mul(commissionInBaseCoin, transaction.CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !coinFrom.ID().IsBaseCoin() { - if coinFrom.Reserve().Cmp(commissionInBaseCoin) < 0 { - return nil, rpctypes.RPCError{Code: 400, Message: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", - coinFrom.Reserve().String(), commissionInBaseCoin.String())} - } - - if coinFrom.Volume().Cmp(valueToSell) < 0 { - return nil, rpctypes.RPCError{Code: 400, Message: fmt.Sprintf("Coin volume is not sufficient for transaction. Has: %s, required %s", - coinFrom.Volume().String(), valueToSell.String())} - } - - commission = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), commissionInBaseCoin) - } - - switch { - case coinFrom.ID().IsBaseCoin(): - result = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), valueToSell) - case coinTo.ID().IsBaseCoin(): - result = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToSell) - default: - basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToSell) - result = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), basecoinValue) - } - - return &EstimateCoinSellResponse{ - WillGet: result.String(), - Commission: commission.String(), - }, nil -} diff --git a/api/estimate_coin_sell_all.go b/api/estimate_coin_sell_all.go deleted file mode 100644 index f42fdb654..000000000 --- a/api/estimate_coin_sell_all.go +++ /dev/null @@ -1,76 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/commissions" - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "math/big" -) - -// EstimateCoinSellAllResponse returns an of sell all coin transaction -type EstimateCoinSellAllResponse struct { - WillGet string `json:"will_get"` -} - -// EstimateCoinSellAll returns an estimate of sell all coin transaction -func EstimateCoinSellAll(coinToSell, coinToBuy string, valueToSell *big.Int, height int) (*EstimateCoinSellAllResponse, - error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - coinFrom := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(coinToSell), types.GetVersionFromSymbol(coinToSell)) - if coinFrom == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin to sell not exists"} - } - - coinTo := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(coinToBuy), types.GetVersionFromSymbol(coinToBuy)) - if coinTo == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Coin to buy not exists"} - } - - if coinFrom.ID() == coinTo.ID() { - return nil, rpctypes.RPCError{Code: 400, Message: "\"From\" coin equals to \"to\" coin"} - } - - commissionInBaseCoin := big.NewInt(commissions.ConvertTx) - commissionInBaseCoin.Mul(commissionInBaseCoin, transaction.CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) - - var result *big.Int - - switch { - case coinFrom.ID().IsBaseCoin(): - valueToSell.Sub(valueToSell, commission) - if valueToSell.Sign() != 1 { - return nil, rpctypes.RPCError{Code: 400, Message: "Not enough coins to pay commission"} - } - - result = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), valueToSell) - case coinTo.ID().IsBaseCoin(): - result = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToSell) - - result.Sub(result, commission) - if result.Cmp(big.NewInt(0)) != 1 { - return nil, rpctypes.RPCError{Code: 400, Message: "Not enough coins to pay commission"} - } - default: - basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToSell) - basecoinValue.Sub(basecoinValue, commission) - if basecoinValue.Cmp(big.NewInt(0)) != 1 { - return nil, rpctypes.RPCError{Code: 400, Message: "Not enough coins to pay commission"} - } - - result = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), basecoinValue) - } - - return &EstimateCoinSellAllResponse{ - WillGet: result.String(), - }, nil -} diff --git a/api/estimate_tx_commission.go b/api/estimate_tx_commission.go deleted file mode 100644 index 01e073c4b..000000000 --- a/api/estimate_tx_commission.go +++ /dev/null @@ -1,46 +0,0 @@ -package api - -import ( - "fmt" - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "math/big" -) - -type TxCommissionResponse struct { - Commission string `json:"commission"` -} - -func EstimateTxCommission(tx []byte, height int) (*TxCommissionResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - decodedTx, err := transaction.TxDecoder.DecodeFromBytesWithoutSig(tx) - if err != nil { - return nil, rpctypes.RPCError{Code: 400, Message: "Cannot decode transaction", Data: err.Error()} - } - - commissionInBaseCoin := decodedTx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !decodedTx.GasCoin.IsBaseCoin() { - coin := cState.Coins().GetCoin(decodedTx.GasCoin) - - if coin.Reserve().Cmp(commissionInBaseCoin) < 0 { - return nil, rpctypes.RPCError{Code: 400, Message: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", - coin.Reserve().String(), commissionInBaseCoin.String())} - } - - commission = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), commissionInBaseCoin) - } - - return &TxCommissionResponse{ - Commission: commission.String(), - }, nil -} diff --git a/api/events.go b/api/events.go deleted file mode 100644 index 5de924b5f..000000000 --- a/api/events.go +++ /dev/null @@ -1,15 +0,0 @@ -package api - -import ( - eventsdb "github.com/MinterTeam/minter-go-node/core/events" -) - -type EventsResponse struct { - Events eventsdb.Events `json:"events"` -} - -func Events(height uint64) (*EventsResponse, error) { - return &EventsResponse{ - Events: blockchain.GetEventsDB().LoadEvents(uint32(height)), - }, nil -} diff --git a/api/genesis.go b/api/genesis.go deleted file mode 100644 index a8e8dabd7..000000000 --- a/api/genesis.go +++ /dev/null @@ -1,7 +0,0 @@ -package api - -import core_types "github.com/tendermint/tendermint/rpc/core/types" - -func Genesis() (*core_types.ResultGenesis, error) { - return client.Genesis() -} diff --git a/api/maxgas.go b/api/maxgas.go deleted file mode 100644 index 3154b5f6d..000000000 --- a/api/maxgas.go +++ /dev/null @@ -1,14 +0,0 @@ -package api - -func MaxGas(height int) (*uint64, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - maxGas := cState.App().GetMaxGas() - return &maxGas, nil -} diff --git a/api/min_gas_price.go b/api/min_gas_price.go deleted file mode 100644 index 4da7bc935..000000000 --- a/api/min_gas_price.go +++ /dev/null @@ -1,5 +0,0 @@ -package api - -func MinGasPrice() (uint64, error) { - return uint64(blockchain.MinGasPrice()), nil -} diff --git a/api/missed_blocks.go b/api/missed_blocks.go deleted file mode 100644 index 3f0e7c324..000000000 --- a/api/missed_blocks.go +++ /dev/null @@ -1,43 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/rpc/lib/types" -) - -type MissedBlocksResponse struct { - MissedBlocks *types.BitArray `json:"missed_blocks"` - MissedBlocksCount int `json:"missed_blocks_count"` -} - -func MissedBlocks(pubkey types.Pubkey, height int) (*MissedBlocksResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - if height != 0 { - cState.Lock() - cState.Validators().LoadValidators() - cState.Unlock() - } - - cState.RLock() - defer cState.RUnlock() - - vals := cState.Validators().GetValidators() - if vals == nil { - return nil, rpctypes.RPCError{Code: 404, Message: "Validators not found"} - } - - for _, val := range vals { - if val.PubKey == pubkey { - return &MissedBlocksResponse{ - MissedBlocks: val.AbsentTimes, - MissedBlocksCount: val.CountAbsentTimes(), - }, nil - } - } - - return nil, rpctypes.RPCError{Code: 404, Message: "Validator not found"} -} diff --git a/api/net_info.go b/api/net_info.go deleted file mode 100644 index 1cd6800b5..000000000 --- a/api/net_info.go +++ /dev/null @@ -1,9 +0,0 @@ -package api - -import ( - core_types "github.com/tendermint/tendermint/rpc/core/types" -) - -func NetInfo() (*core_types.ResultNetInfo, error) { - return client.NetInfo() -} diff --git a/api/send_transaction.go b/api/send_transaction.go deleted file mode 100644 index 0cbfbc4e3..000000000 --- a/api/send_transaction.go +++ /dev/null @@ -1,22 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/rpc/lib/types" - core_types "github.com/tendermint/tendermint/rpc/core/types" -) - -func SendTransaction(tx []byte) (*core_types.ResultBroadcastTx, error) { - result, err := client.BroadcastTxSync(tx) - if err != nil { - return nil, err - } - - if result.Code != 0 { - return nil, rpctypes.TxError{ - Code: result.Code, - Log: result.Log, - } - } - - return result, nil -} diff --git a/api/status.go b/api/status.go deleted file mode 100644 index 8bc2af607..000000000 --- a/api/status.go +++ /dev/null @@ -1,35 +0,0 @@ -package api - -import ( - "fmt" - "github.com/MinterTeam/minter-go-node/version" - core_types "github.com/tendermint/tendermint/rpc/core/types" - "time" -) - -type StatusResponse struct { - MinterVersion string `json:"version"` - LatestBlockHash string `json:"latest_block_hash"` - LatestAppHash string `json:"latest_app_hash"` - LatestBlockHeight int64 `json:"latest_block_height"` - LatestBlockTime time.Time `json:"latest_block_time"` - KeepLastStates int64 `json:"keep_last_states"` - TmStatus *core_types.ResultStatus `json:"tm_status"` -} - -func Status() (*StatusResponse, error) { - result, err := client.Status() - if err != nil { - return nil, err - } - - return &StatusResponse{ - MinterVersion: version.Version, - LatestBlockHash: fmt.Sprintf("%X", result.SyncInfo.LatestBlockHash), - LatestAppHash: fmt.Sprintf("%X", result.SyncInfo.LatestAppHash), - KeepLastStates: minterCfg.BaseConfig.KeepLastStates, - LatestBlockHeight: result.SyncInfo.LatestBlockHeight, - LatestBlockTime: result.SyncInfo.LatestBlockTime, - TmStatus: result, - }, nil -} diff --git a/api/transaction.go b/api/transaction.go deleted file mode 100644 index 4aafd4312..000000000 --- a/api/transaction.go +++ /dev/null @@ -1,29 +0,0 @@ -package api - -import ( - "encoding/json" - - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/core/transaction/encoder" -) - -func Transaction(hash []byte) (json.RawMessage, error) { - tx, err := client.Tx(hash, false) - if err != nil { - return nil, err - } - - decodedTx, _ := transaction.TxDecoder.DecodeFromBytes(tx.Tx) - - cState, err := GetStateForHeight(0) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - txJsonEncoder := encoder.NewTxEncoderJSON(cState) - - return txJsonEncoder.Encode(decodedTx, tx) -} diff --git a/api/transactions.go b/api/transactions.go deleted file mode 100644 index ac8dc9bde..000000000 --- a/api/transactions.go +++ /dev/null @@ -1,68 +0,0 @@ -package api - -import ( - "encoding/json" - - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/core/transaction/encoder" - core_types "github.com/tendermint/tendermint/rpc/core/types" -) - -type TransactionResponse struct { - Hash string `json:"hash"` - RawTx string `json:"raw_tx"` - Height int64 `json:"height"` - Index uint32 `json:"index"` - From string `json:"from"` - Nonce uint64 `json:"nonce"` - Gas int64 `json:"gas"` - GasPrice uint32 `json:"gas_price"` - GasCoin Coin `json:"gas_coin"` - Type uint8 `json:"type"` - Data json.RawMessage `json:"data"` - Payload []byte `json:"payload"` - Tags map[string]string `json:"tags"` - Code uint32 `json:"code,omitempty"` - Log string `json:"log,omitempty"` -} - -type ResultTxSearch struct { - Txs []*core_types.ResultTx `json:"txs"` - TotalCount int `json:"total_count"` -} - -func Transactions(query string, page, perPage int) (*[]json.RawMessage, error) { - if page == 0 { - page = 1 - } - if perPage == 0 { - perPage = 100 - } - - rpcResult, err := client.TxSearch(query, false, page, perPage, "desc") - if err != nil { - return nil, err - } - - cState, err := GetStateForHeight(0) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - result := make([]json.RawMessage, 0, len(rpcResult.Txs)) - for _, tx := range rpcResult.Txs { - decodedTx, _ := transaction.TxDecoder.DecodeFromBytes(tx.Tx) - txJsonEncoder := encoder.NewTxEncoderJSON(cState) - response, err := txJsonEncoder.Encode(decodedTx, tx) - if err != nil { - return nil, err - } - - result = append(result, response) - } - - return &result, nil -} diff --git a/api/unconfirmed_txs.go b/api/unconfirmed_txs.go deleted file mode 100644 index a07774224..000000000 --- a/api/unconfirmed_txs.go +++ /dev/null @@ -1,9 +0,0 @@ -package api - -import ( - core_types "github.com/tendermint/tendermint/rpc/core/types" -) - -func UnconfirmedTxs(limit int) (*core_types.ResultUnconfirmedTxs, error) { - return client.UnconfirmedTxs(limit) -} diff --git a/api/v2/service/address.go b/api/v2/service/address.go index a625383e7..186743213 100644 --- a/api/v2/service/address.go +++ b/api/v2/service/address.go @@ -38,20 +38,10 @@ func (s *Service) Address(ctx context.Context, req *pb.AddressRequest) (*pb.Addr return nil, status.Error(codes.NotFound, err.Error()) } - if req.Height != 0 && req.Delegated { - cState.Lock() - cState.Candidates().LoadCandidates() - cState.Candidates().LoadStakes() - cState.Unlock() - } - - if timeoutStatus := s.checkTimeout(ctx, "LoadCandidates", "LoadStakes"); timeoutStatus != nil { + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { return nil, timeoutStatus.Err() } - cState.RLock() - defer cState.RUnlock() - balances := cState.Accounts().GetBalances(address) var res pb.AddressResponse @@ -60,18 +50,24 @@ func (s *Service) Address(ctx context.Context, req *pb.AddressRequest) (*pb.Addr res.Balance = make([]*pb.AddressBalance, 0, len(balances)) for _, coin := range balances { totalStakesGroupByCoin[coin.Coin.ID] = coin.Value + coinModel := cState.Coins().GetCoin(coin.Coin.ID) res.Balance = append(res.Balance, &pb.AddressBalance{ Coin: &pb.Coin{ Id: uint64(coin.Coin.ID), - Symbol: cState.Coins().GetCoin(coin.Coin.ID).GetFullSymbol(), + Symbol: coinModel.GetFullSymbol(), }, Value: coin.Value.String(), - BipValue: customCoinBipBalance(coin.Coin.ID, coin.Value, cState.Coins()).String(), + BipValue: customCoinBipBalance(coin.Value, coinModel).String(), }) } if req.Delegated { - if timeoutStatus := s.checkTimeout(ctx, "Delegated"); timeoutStatus != nil { + cState.Candidates().LoadCandidates() + if timeoutStatus := s.checkTimeout(ctx, "LoadCandidates"); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + cState.Candidates().LoadStakes() + if timeoutStatus := s.checkTimeout(ctx, "LoadStakes"); timeoutStatus != nil { return nil, timeoutStatus.Err() } var userDelegatedStakesGroupByCoin = map[types.CoinID]*stakeUser{} @@ -103,14 +99,15 @@ func (s *Service) Address(ctx context.Context, req *pb.AddressRequest) (*pb.Addr res.Delegated = make([]*pb.AddressDelegatedBalance, 0, len(userDelegatedStakesGroupByCoin)) for coinID, delegatedStake := range userDelegatedStakesGroupByCoin { + coinModel := cState.Coins().GetCoin(coinID) res.Delegated = append(res.Delegated, &pb.AddressDelegatedBalance{ Coin: &pb.Coin{ Id: uint64(coinID), - Symbol: cState.Coins().GetCoin(coinID).GetFullSymbol(), + Symbol: coinModel.GetFullSymbol(), }, Value: delegatedStake.Value.String(), DelegateBipValue: delegatedStake.BipValue.String(), - BipValue: customCoinBipBalance(coinID, delegatedStake.Value, cState.Coins()).String(), + BipValue: customCoinBipBalance(delegatedStake.Value, coinModel).String(), }) totalStake, ok := totalStakesGroupByCoin[coinID] @@ -129,12 +126,13 @@ func (s *Service) Address(ctx context.Context, req *pb.AddressRequest) (*pb.Addr coinsBipValue := big.NewInt(0) res.Total = make([]*pb.AddressBalance, 0, len(totalStakesGroupByCoin)) for coinID, stake := range totalStakesGroupByCoin { - balance := customCoinBipBalance(coinID, stake, cState.Coins()) + coinModel := cState.Coins().GetCoin(coinID) + balance := customCoinBipBalance(stake, coinModel) if req.Delegated { res.Total = append(res.Total, &pb.AddressBalance{ Coin: &pb.Coin{ Id: uint64(coinID), - Symbol: cState.Coins().GetCoin(coinID).GetFullSymbol(), + Symbol: coinModel.GetFullSymbol(), }, Value: stake.String(), BipValue: balance.String(), @@ -147,12 +145,11 @@ func (s *Service) Address(ctx context.Context, req *pb.AddressRequest) (*pb.Addr return &res, nil } -func customCoinBipBalance(coinToSell types.CoinID, valueToSell *big.Int, coins coins.RCoins) *big.Int { - if coinToSell.IsBaseCoin() { +func customCoinBipBalance(valueToSell *big.Int, coinFrom *coins.Model) *big.Int { + if coinFrom.ID().IsBaseCoin() { return valueToSell } - coinFrom := coins.GetCoin(coinToSell) return formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToSell) } diff --git a/api/v2/service/addresses.go b/api/v2/service/addresses.go index 2cfcce1ad..27d3aba99 100644 --- a/api/v2/service/addresses.go +++ b/api/v2/service/addresses.go @@ -20,19 +20,16 @@ func (s *Service) Addresses(ctx context.Context, req *pb.AddressesRequest) (*pb. } if req.Height != 0 && req.Delegated { - cState.Lock() cState.Candidates().LoadCandidates() + if timeoutStatus := s.checkTimeout(ctx, "LoadCandidates"); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } cState.Candidates().LoadStakes() - cState.Unlock() - } - - if timeoutStatus := s.checkTimeout(ctx, "LoadCandidates", "LoadStakes"); timeoutStatus != nil { - return nil, timeoutStatus.Err() + if timeoutStatus := s.checkTimeout(ctx, "LoadStakes"); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } } - cState.RLock() - defer cState.RUnlock() - response := &pb.AddressesResponse{ Addresses: make(map[string]*pb.AddressesResponse_Result, len(req.Addresses)), } @@ -57,13 +54,14 @@ func (s *Service) Addresses(ctx context.Context, req *pb.AddressesRequest) (*pb. res.Balance = make([]*pb.AddressBalance, 0, len(balances)) for _, coin := range balances { totalStakesGroupByCoin[coin.Coin.ID] = coin.Value + coinModel := cState.Coins().GetCoin(coin.Coin.ID) res.Balance = append(res.Balance, &pb.AddressBalance{ Coin: &pb.Coin{ Id: uint64(coin.Coin.ID), - Symbol: cState.Coins().GetCoin(coin.Coin.ID).GetFullSymbol(), + Symbol: coinModel.GetFullSymbol(), }, Value: coin.Value.String(), - BipValue: customCoinBipBalance(coin.Coin.ID, coin.Value, cState.Coins()).String(), + BipValue: customCoinBipBalance(coin.Value, coinModel).String(), }) } @@ -98,14 +96,15 @@ func (s *Service) Addresses(ctx context.Context, req *pb.AddressesRequest) (*pb. res.Delegated = make([]*pb.AddressDelegatedBalance, 0, len(userDelegatedStakesGroupByCoin)) for coinID, delegatedStake := range userDelegatedStakesGroupByCoin { + coinModel := cState.Coins().GetCoin(coinID) res.Delegated = append(res.Delegated, &pb.AddressDelegatedBalance{ Coin: &pb.Coin{ Id: uint64(coinID), - Symbol: cState.Coins().GetCoin(coinID).GetFullSymbol(), + Symbol: coinModel.GetFullSymbol(), }, Value: delegatedStake.Value.String(), DelegateBipValue: delegatedStake.BipValue.String(), - BipValue: customCoinBipBalance(coinID, delegatedStake.Value, cState.Coins()).String(), + BipValue: customCoinBipBalance(delegatedStake.Value, coinModel).String(), }) totalStake, ok := totalStakesGroupByCoin[coinID] @@ -124,12 +123,13 @@ func (s *Service) Addresses(ctx context.Context, req *pb.AddressesRequest) (*pb. coinsBipValue := big.NewInt(0) res.Total = make([]*pb.AddressBalance, 0, len(totalStakesGroupByCoin)) for coinID, stake := range totalStakesGroupByCoin { - balance := customCoinBipBalance(coinID, stake, cState.Coins()) + coinModel := cState.Coins().GetCoin(coinID) + balance := customCoinBipBalance(stake, coinModel) if req.Delegated { res.Total = append(res.Total, &pb.AddressBalance{ Coin: &pb.Coin{ Id: uint64(coinID), - Symbol: cState.Coins().GetCoin(coinID).GetFullSymbol(), + Symbol: coinModel.GetFullSymbol(), }, Value: stake.String(), BipValue: balance.String(), diff --git a/api/v2/service/block.go b/api/v2/service/block.go index 9dab7526b..2c5f37c89 100644 --- a/api/v2/service/block.go +++ b/api/v2/service/block.go @@ -6,13 +6,12 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/rewards" - "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/state/coins" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" pb "github.com/MinterTeam/node-grpc-gateway/api_pb" _struct "github.com/golang/protobuf/ptypes/struct" - "github.com/tendermint/iavl" + tmjson "github.com/tendermint/tendermint/libs/json" core_types "github.com/tendermint/tendermint/rpc/core/types" tmTypes "github.com/tendermint/tendermint/types" "google.golang.org/grpc/codes" @@ -24,148 +23,84 @@ import ( // Block returns block data at given height. func (s *Service) Block(ctx context.Context, req *pb.BlockRequest) (*pb.BlockResponse, error) { height := int64(req.Height) - block, err := s.client.Block(&height) + block, err := s.client.Block(ctx, &height) if err != nil { return nil, status.Error(codes.NotFound, "Block not found") } - blockResults, err := s.client.BlockResults(&height) - if err != nil { - return nil, status.Error(codes.NotFound, "Block results not found") - } - - valHeight := height - 1 - if valHeight < 1 { - valHeight = 1 - } - - response := &pb.BlockResponse{ - Hash: hex.EncodeToString(block.Block.Hash()), - Height: uint64(block.Block.Height), - Time: block.Block.Time.Format(time.RFC3339Nano), - TransactionCount: uint64(len(block.Block.Txs)), - } - - var totalValidators []*tmTypes.Validator - - if len(req.Fields) == 0 { - response.Size = uint64(len(s.cdc.MustMarshalBinaryLengthPrefixed(block))) - response.BlockReward = rewards.GetRewardForBlock(uint64(height)).String() - - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { - return nil, timeoutStatus.Err() - } - - currentState := s.blockchain.CurrentState() - currentState.RLock() - defer currentState.RUnlock() - - response.Transactions, err = s.blockTransaction(block, blockResults, currentState.Coins()) - if err != nil { - return nil, err - } - - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { - return nil, timeoutStatus.Err() - } - - tmValidators, err := s.client.Validators(&valHeight, 1, 100) - if err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } - totalValidators = tmValidators.Validators - - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { - return nil, timeoutStatus.Err() - } - - response.Proposer, err = blockProposer(block, totalValidators) - if err != nil { - return nil, err - } - - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { - return nil, timeoutStatus.Err() + fields := map[pb.BlockField]struct{}{} + if len(req.Fields) > 0 { + for _, field := range req.Fields { + fields[field] = struct{}{} } - - response.Validators = blockValidators(totalValidators, block) - - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { - return nil, timeoutStatus.Err() + } else { + for _, field := range pb.BlockField_value { + fields[pb.BlockField(field)] = struct{}{} } + } - response.Evidence, err = blockEvidence(block) + var blockResults *core_types.ResultBlockResults + if _, ok := fields[pb.BlockField_transactions]; ok { + blockResults, err = s.client.BlockResults(ctx, &height) if err != nil { - return nil, err - } - - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { - return nil, timeoutStatus.Err() + return nil, status.Error(codes.NotFound, "Block results not found") } + } - cStateOld, err := s.blockchain.GetStateForHeight(uint64(height)) - if err != iavl.ErrVersionDoesNotExist && err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } + var totalValidators []*tmTypes.Validator + { + _, okValidators := fields[pb.BlockField_validators] + _, okEvidence := fields[pb.BlockField_evidence] + if okValidators || okEvidence { + valHeight := height - 1 + if valHeight < 1 { + valHeight = 1 + } - if err != iavl.ErrVersionDoesNotExist { - response.Missed = missedBlockValidators(cStateOld) + var page = 1 + var perPage = 100 + tmValidators, err := s.client.Validators(ctx, &valHeight, &page, &perPage) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + totalValidators = tmValidators.Validators } + } - return response, nil + response := &pb.BlockResponse{ + Hash: hex.EncodeToString(block.Block.Hash()), + Height: uint64(block.Block.Height), + Time: block.Block.Time.Format(time.RFC3339Nano), + TransactionCount: uint64(len(block.Block.Txs)), } - for _, field := range req.Fields { + for field := range fields { if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { return nil, timeoutStatus.Err() } switch field { - case pb.BlockRequest_size: - response.Size = uint64(len(s.cdc.MustMarshalBinaryLengthPrefixed(block))) - case pb.BlockRequest_block_reward: + case pb.BlockField_size: + response.Size = uint64(block.Block.Size()) + case pb.BlockField_block_reward: response.BlockReward = rewards.GetRewardForBlock(uint64(height)).String() - case pb.BlockRequest_transactions: - cState := s.blockchain.CurrentState() - - response.Transactions, err = s.blockTransaction(block, blockResults, cState.Coins()) + case pb.BlockField_transactions: + response.Transactions, err = s.blockTransaction(block, blockResults, s.blockchain.CurrentState().Coins(), req.FailedTxs) if err != nil { return nil, err } - case pb.BlockRequest_missed: - cStateOld, err := s.blockchain.GetStateForHeight(uint64(height)) - if err != iavl.ErrVersionDoesNotExist && err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } - - if err != iavl.ErrVersionDoesNotExist { - response.Missed = missedBlockValidators(cStateOld) - } - - case pb.BlockRequest_proposer, pb.BlockRequest_validators: - if len(totalValidators) == 0 { - tmValidators, err := s.client.Validators(&valHeight, 1, 100) - if err != nil { - return nil, status.Error(codes.Internal, err.Error()) - } - totalValidators = tmValidators.Validators - } - - if pb.BlockRequest_validators == field { - response.Validators = blockValidators(totalValidators, block) - continue - } - + case pb.BlockField_proposer: response.Proposer, err = blockProposer(block, totalValidators) if err != nil { return nil, err } - case pb.BlockRequest_evidence: + case pb.BlockField_validators: + response.Validators = blockValidators(totalValidators, block) + case pb.BlockField_evidence: response.Evidence, err = blockEvidence(block) if err != nil { return nil, err } } - } return response, nil @@ -174,11 +109,11 @@ func (s *Service) Block(ctx context.Context, req *pb.BlockRequest) (*pb.BlockRes func blockEvidence(block *core_types.ResultBlock) (*pb.BlockResponse_Evidence, error) { evidences := make([]*_struct.Struct, 0, len(block.Block.Evidence.Evidence)) for _, evidence := range block.Block.Evidence.Evidence { - proto, err := tmTypes.EvidenceToProto(evidence) + data, err := tmjson.Marshal(evidence) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } - str, err := toStruct(proto.GetSum()) + str, err := encodeToStruct(data) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } @@ -198,7 +133,7 @@ func blockValidators(totalValidators []*tmTypes.Validator, block *core_types.Res } } validators = append(validators, &pb.BlockResponse_Validator{ - PublicKey: fmt.Sprintf("Mp%x", tmval.PubKey.Bytes()[5:]), + PublicKey: fmt.Sprintf("Mp%x", tmval.PubKey.Bytes()[:]), Signed: signed, }) } @@ -206,15 +141,6 @@ func blockValidators(totalValidators []*tmTypes.Validator, block *core_types.Res return validators } -func missedBlockValidators(s *state.CheckState) []string { - var missedBlocks []string - for _, val := range s.Validators().GetValidators() { - missedBlocks = append(missedBlocks, val.AbsentTimes.String()) - } - - return missedBlocks -} - func blockProposer(block *core_types.ResultBlock, totalValidators []*tmTypes.Validator) (string, error) { p := getBlockProposer(block, totalValidators) if p != nil { @@ -223,16 +149,22 @@ func blockProposer(block *core_types.ResultBlock, totalValidators []*tmTypes.Val return "", nil } -func (s *Service) blockTransaction(block *core_types.ResultBlock, blockResults *core_types.ResultBlockResults, coins coins.RCoins) ([]*pb.BlockResponse_Transaction, error) { - txs := make([]*pb.BlockResponse_Transaction, 0, len(block.Block.Data.Txs)) +func (s *Service) blockTransaction(block *core_types.ResultBlock, blockResults *core_types.ResultBlockResults, coins coins.RCoins, failed bool) ([]*pb.TransactionResponse, error) { + txs := make([]*pb.TransactionResponse, 0, len(block.Block.Data.Txs)) for i, rawTx := range block.Block.Data.Txs { - tx, _ := transaction.TxDecoder.DecodeFromBytes(rawTx) + if blockResults.TxsResults[i].Code != 0 && !failed { + continue + } + + tx, _ := transaction.DecodeFromBytes(rawTx) sender, _ := tx.Sender() tags := make(map[string]string) for _, tag := range blockResults.TxsResults[i].Events[0].Attributes { - tags[string(tag.Key)] = string(tag.Value) + key := string(tag.Key) + value := string(tag.Value) + tags[key] = value } data, err := encode(tx.GetDecodedData(), coins) @@ -240,9 +172,11 @@ func (s *Service) blockTransaction(block *core_types.ResultBlock, blockResults * return nil, status.Error(codes.Internal, err.Error()) } - txs = append(txs, &pb.BlockResponse_Transaction{ + txs = append(txs, &pb.TransactionResponse{ Hash: strings.Title(fmt.Sprintf("Mt%x", rawTx.Hash())), RawTx: fmt.Sprintf("%x", []byte(rawTx)), + Height: uint64(block.Block.Height), + Index: uint64(i), From: sender.String(), Nonce: tx.Nonce, GasPrice: uint64(tx.GasPrice), @@ -267,7 +201,7 @@ func getBlockProposer(block *core_types.ResultBlock, vals []*tmTypes.Validator) for _, tmval := range vals { if bytes.Equal(tmval.Address.Bytes(), block.Block.ProposerAddress.Bytes()) { var result types.Pubkey - copy(result[:], tmval.PubKey.Bytes()[5:]) + copy(result[:], tmval.PubKey.Bytes()[:]) return &result } } diff --git a/api/v2/service/blocks.go b/api/v2/service/blocks.go new file mode 100644 index 000000000..e2dd97b7a --- /dev/null +++ b/api/v2/service/blocks.go @@ -0,0 +1,31 @@ +package service + +import ( + "context" + pb "github.com/MinterTeam/node-grpc-gateway/api_pb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Blocks ... +func (s *Service) Blocks(ctx context.Context, req *pb.BlocksRequest) (*pb.BlocksResponse, error) { + const limit = 100 + + var blocksResponse []*pb.BlockResponse + for i := req.FromHeight; i <= req.ToHeight && i <= req.FromHeight+limit; i++ { + block, err := s.Block(ctx, &pb.BlockRequest{ + Height: i, + Fields: req.Fields, + FailedTxs: req.FailedTxs, + }) + if err != nil { + if status.Code(err) == codes.NotFound { + break + } + return nil, err + } + blocksResponse = append(blocksResponse, block) + } + + return &pb.BlocksResponse{Blocks: blocksResponse}, nil +} diff --git a/api/v2/service/candidate.go b/api/v2/service/candidate.go index 2374254b9..44803f9d9 100644 --- a/api/v2/service/candidate.go +++ b/api/v2/service/candidate.go @@ -33,21 +33,20 @@ func (s *Service) Candidate(ctx context.Context, req *pb.CandidateRequest) (*pb. } if req.Height != 0 { - cState.Lock() cState.Candidates().LoadCandidates() cState.Candidates().LoadStakesOfCandidate(pubkey) - cState.Unlock() } - cState.RLock() - defer cState.RUnlock() - candidate := cState.Candidates().GetCandidate(pubkey) if candidate == nil { return nil, status.Error(codes.NotFound, "Candidate not found") } result := makeResponseCandidate(cState, candidate, true, req.NotShowStakes) + if cState.Validators().GetByPublicKey(candidate.PubKey) != nil { + result.Validator = true + } + return result, nil } @@ -62,14 +61,11 @@ func makeResponseCandidate(state *state.CheckState, c *candidates.Candidate, inc Status: uint64(c.Status), } - if state.Validators().GetByPublicKey(c.PubKey) != nil { - candidate.Validator = true - } - if includeStakes { + state.Candidates().LoadStakesOfCandidate(c.PubKey) + stakes := state.Candidates().GetStakes(c.PubKey) addresses := map[types.Address]struct{}{} minStake := big.NewInt(0) - stakes := state.Candidates().GetStakes(c.PubKey) usedSlots := len(stakes) if !NotShowStakes { candidate.Stakes = make([]*pb.CandidateResponse_Stake, 0, usedSlots) diff --git a/api/v2/service/candidates.go b/api/v2/service/candidates.go index c887068ad..0f12451e5 100644 --- a/api/v2/service/candidates.go +++ b/api/v2/service/candidates.go @@ -15,17 +15,9 @@ func (s *Service) Candidates(ctx context.Context, req *pb.CandidatesRequest) (*p } if req.Height != 0 { - cState.Lock() cState.Candidates().LoadCandidates() - if req.IncludeStakes { - cState.Candidates().LoadStakes() - } - cState.Unlock() } - cState.RLock() - defer cState.RUnlock() - candidates := cState.Candidates().GetCandidates() response := &pb.CandidatesResponse{} @@ -35,11 +27,22 @@ func (s *Service) Candidates(ctx context.Context, req *pb.CandidatesRequest) (*p return nil, timeoutStatus.Err() } - if req.Status != pb.CandidatesRequest_all && req.Status != pb.CandidatesRequest_CandidateStatus(candidate.Status) { + isValidator := false + if cState.Validators().GetByPublicKey(candidate.PubKey) != nil { + isValidator = true + } + + if req.Status != pb.CandidatesRequest_all && !(req.Status == pb.CandidatesRequest_CandidateStatus(candidate.Status) || + !(req.Status != pb.CandidatesRequest_validator && isValidator)) { continue } - response.Candidates = append(response.Candidates, makeResponseCandidate(cState, candidate, req.IncludeStakes, req.NotShowStakes)) + cState.Candidates().LoadStakesOfCandidate(candidate.PubKey) + + responseCandidate := makeResponseCandidate(cState, candidate, req.IncludeStakes, req.NotShowStakes) + responseCandidate.Validator = isValidator + + response.Candidates = append(response.Candidates, responseCandidate) } return response, nil diff --git a/api/v2/service/coin_info.go b/api/v2/service/coin_info.go index a4fd519cf..7e4beed82 100644 --- a/api/v2/service/coin_info.go +++ b/api/v2/service/coin_info.go @@ -20,9 +20,6 @@ func (s *Service) CoinInfo(ctx context.Context, req *pb.CoinInfoRequest) (*pb.Co return nil, status.Error(codes.NotFound, err.Error()) } - cState.RLock() - defer cState.RUnlock() - coin := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(req.Symbol), types.GetVersionFromSymbol(req.Symbol)) if coin == nil { return nil, s.createError(status.New(codes.NotFound, "Coin not found"), transaction.EncodeError(code.NewCoinNotExists(req.Symbol, ""))) @@ -57,9 +54,6 @@ func (s *Service) CoinInfoById(ctx context.Context, req *pb.CoinIdRequest) (*pb. return nil, status.Error(codes.NotFound, err.Error()) } - cState.RLock() - defer cState.RUnlock() - coin := cState.Coins().GetCoin(types.CoinID(req.Id)) if coin == nil { return nil, s.createError(status.New(codes.NotFound, "Coin not found"), transaction.EncodeError(code.NewCoinNotExists("", strconv.Itoa(int(req.Id))))) diff --git a/api/v2/service/data_encoder.go b/api/v2/service/data_encoder.go index 6228f5642..831344e6b 100644 --- a/api/v2/service/data_encoder.go +++ b/api/v2/service/data_encoder.go @@ -14,19 +14,19 @@ import ( "strconv" ) -func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { +func encode(data transaction.Data, rCoins coins.RCoins) (*any.Any, error) { var m proto.Message switch d := data.(type) { case *transaction.BuyCoinData: m = &pb.BuyCoinData{ CoinToBuy: &pb.Coin{ Id: uint64(d.CoinToBuy), - Symbol: coins.GetCoin(d.CoinToBuy).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.CoinToBuy).GetFullSymbol(), }, ValueToBuy: d.ValueToBuy.String(), CoinToSell: &pb.Coin{ Id: uint64(d.CoinToSell), - Symbol: coins.GetCoin(d.CoinToSell).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.CoinToSell).GetFullSymbol(), }, MaximumValueToSell: d.MaximumValueToSell.String(), } @@ -65,7 +65,7 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { Commission: uint64(d.Commission), Coin: &pb.Coin{ Id: uint64(d.Coin), - Symbol: coins.GetCoin(d.Coin).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.Coin).GetFullSymbol(), }, Stake: d.Stake.String(), } @@ -74,7 +74,7 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { PubKey: d.PubKey.String(), Coin: &pb.Coin{ Id: uint64(d.Coin), - Symbol: coins.GetCoin(d.Coin).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.Coin).GetFullSymbol(), }, Value: d.Value.String(), } @@ -110,7 +110,7 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { list = append(list, &pb.SendData{ Coin: &pb.Coin{ Id: uint64(item.Coin), - Symbol: coins.GetCoin(item.Coin).GetFullSymbol(), + Symbol: rCoins.GetCoin(item.Coin).GetFullSymbol(), }, To: item.To.String(), Value: item.Value.String(), @@ -141,11 +141,11 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { m = &pb.SellAllCoinData{ CoinToSell: &pb.Coin{ Id: uint64(d.CoinToSell), - Symbol: coins.GetCoin(d.CoinToSell).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.CoinToSell).GetFullSymbol(), }, CoinToBuy: &pb.Coin{ Id: uint64(d.CoinToBuy), - Symbol: coins.GetCoin(d.CoinToBuy).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.CoinToBuy).GetFullSymbol(), }, MinimumValueToBuy: d.MinimumValueToBuy.String(), } @@ -153,12 +153,12 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { m = &pb.SellCoinData{ CoinToSell: &pb.Coin{ Id: uint64(d.CoinToSell), - Symbol: coins.GetCoin(d.CoinToSell).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.CoinToSell).GetFullSymbol(), }, ValueToSell: d.ValueToSell.String(), CoinToBuy: &pb.Coin{ Id: uint64(d.CoinToBuy), - Symbol: coins.GetCoin(d.CoinToBuy).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.CoinToBuy).GetFullSymbol(), }, MinimumValueToBuy: d.MinimumValueToBuy.String(), } @@ -166,7 +166,7 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { m = &pb.SendData{ Coin: &pb.Coin{ Id: uint64(d.Coin), - Symbol: coins.GetCoin(d.Coin).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.Coin).GetFullSymbol(), }, To: d.To.String(), Value: d.Value.String(), @@ -189,10 +189,145 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { PubKey: d.PubKey.String(), Coin: &pb.Coin{ Id: uint64(d.Coin), - Symbol: coins.GetCoin(d.Coin).GetFullSymbol(), + Symbol: rCoins.GetCoin(d.Coin).GetFullSymbol(), }, Value: d.Value.String(), } + case *transaction.AddLiquidityData: + m = &pb.AddLiquidityData{ + Coin0: &pb.Coin{ + Id: uint64(d.Coin0), + Symbol: rCoins.GetCoin(d.Coin0).GetFullSymbol(), + }, + Coin1: &pb.Coin{ + Id: uint64(d.Coin1), + Symbol: rCoins.GetCoin(d.Coin1).GetFullSymbol(), + }, + Volume0: d.Volume0.String(), + MaximumVolume1: d.MaximumVolume1.String(), + } + case *transaction.RemoveLiquidity: + m = &pb.RemoveLiquidityData{ + Coin0: &pb.Coin{ + Id: uint64(d.Coin0), + Symbol: rCoins.GetCoin(d.Coin0).GetFullSymbol(), + }, + Coin1: &pb.Coin{ + Id: uint64(d.Coin1), + Symbol: rCoins.GetCoin(d.Coin1).GetFullSymbol(), + }, + Liquidity: d.Liquidity.String(), + MinimumVolume0: d.MinimumVolume0.String(), + MinimumVolume1: d.MinimumVolume1.String(), + } + case *transaction.BuySwapPoolData: + var coinsInfo []*pb.Coin + for _, coin := range d.Coins { + coinsInfo = append(coinsInfo, &pb.Coin{ + Id: uint64(coin), + Symbol: rCoins.GetCoin(coin).GetFullSymbol(), + }) + } + m = &pb.BuySwapPoolData{ + Coins: coinsInfo, + ValueToBuy: d.ValueToBuy.String(), + MaximumValueToSell: d.MaximumValueToSell.String(), + } + case *transaction.SellSwapPoolData: + var coinsInfo []*pb.Coin + for _, coin := range d.Coins { + coinsInfo = append(coinsInfo, &pb.Coin{ + Id: uint64(coin), + Symbol: rCoins.GetCoin(coin).GetFullSymbol(), + }) + } + m = &pb.SellSwapPoolData{ + Coins: coinsInfo, + ValueToSell: d.ValueToSell.String(), + MinimumValueToBuy: d.MinimumValueToBuy.String(), + } + case *transaction.SellAllSwapPoolData: + var coinsInfo []*pb.Coin + for _, coin := range d.Coins { + coinsInfo = append(coinsInfo, &pb.Coin{ + Id: uint64(coin), + Symbol: rCoins.GetCoin(coin).GetFullSymbol(), + }) + } + m = &pb.SellAllSwapPoolData{ + Coins: coinsInfo, + MinimumValueToBuy: d.MinimumValueToBuy.String(), + } + case *transaction.CreateTokenData: + m = &pb.CreateTokenData{ + Name: d.Name, + Symbol: d.Symbol.String(), + InitialAmount: d.InitialAmount.String(), + MaxSupply: d.MaxSupply.String(), + Mintable: d.Mintable, + Burnable: d.Burnable, + } + case *transaction.RecreateTokenData: + m = &pb.RecreateTokenData{ + Name: d.Name, + Symbol: d.Symbol.String(), + InitialAmount: d.InitialAmount.String(), + MaxSupply: d.MaxSupply.String(), + Mintable: d.Mintable, + Burnable: d.Burnable, + } + case *transaction.BurnTokenData: + m = &pb.BurnTokenData{ + Coin: &pb.Coin{ + Id: uint64(d.Coin), + Symbol: rCoins.GetCoin(d.Coin).GetFullSymbol(), + }, + Value: d.Value.String(), + } + case *transaction.MintTokenData: + m = &pb.MintTokenData{ + Coin: &pb.Coin{ + Id: uint64(d.Coin), + Symbol: rCoins.GetCoin(d.Coin).GetFullSymbol(), + }, + Value: d.Value.String(), + } + case *transaction.EditCandidateCommission: + m = &pb.EditCandidateCommission{ + PubKey: d.PubKey.String(), + Commission: uint64(d.Commission), + } + case *transaction.MoveStakeData: + m = &pb.MoveStakeData{ + From: d.From.String(), + To: d.To.String(), + Coin: &pb.Coin{ + Id: uint64(d.Coin), + Symbol: rCoins.GetCoin(d.Coin).GetFullSymbol(), + }, + Stake: d.Stake.String(), + } + case *transaction.VoteCommissionData: + m = priceCommissionData(d, rCoins.GetCoin(d.Coin)) + case *transaction.VoteUpdateData: + m = &pb.VoteUpdateData{ + PubKey: d.PubKey.String(), + Height: d.Height, + Version: d.Version, + } + case *transaction.CreateSwapPoolData: + m = &pb.CreateSwapPoolData{ + Coin0: &pb.Coin{ + Id: uint64(d.Coin0), + Symbol: rCoins.GetCoin(d.Coin0).GetFullSymbol(), + }, + Coin1: &pb.Coin{ + Id: uint64(d.Coin1), + Symbol: rCoins.GetCoin(d.Coin1).GetFullSymbol(), + }, + Volume0: d.Volume0.String(), + Volume1: d.Volume1.String(), + } default: return nil, errors.New("unknown tx type") } @@ -205,6 +340,61 @@ func encode(data transaction.Data, coins coins.RCoins) (*any.Any, error) { return a, nil } +func priceCommissionData(d *transaction.VoteCommissionData, coin *coins.Model) proto.Message { + return &pb.VoteCommissionData{ + PubKey: d.PubKey.String(), + Height: d.Height, + Coin: &pb.Coin{ + Id: uint64(d.Coin), + Symbol: coin.GetFullSymbol(), + }, + PayloadByte: d.PayloadByte.String(), + Send: d.Send.String(), + BuyBancor: d.BuyBancor.String(), + SellBancor: d.SellBancor.String(), + SellAllBancor: d.SellAllBancor.String(), + BuyPoolBase: d.BuyPoolBase.String(), + BuyPoolDelta: d.BuyPoolDelta.String(), + SellPoolBase: d.SellPoolBase.String(), + SellPoolDelta: d.SellPoolDelta.String(), + SellAllPoolBase: d.SellAllPoolBase.String(), + SellAllPoolDelta: d.SellAllPoolDelta.String(), + CreateTicker3: d.CreateTicker3.String(), + CreateTicker4: d.CreateTicker4.String(), + CreateTicker5: d.CreateTicker5.String(), + CreateTicker6: d.CreateTicker6.String(), + CreateTicker7_10: d.CreateTicker7to10.String(), + CreateCoin: d.CreateCoin.String(), + CreateToken: d.CreateToken.String(), + RecreateCoin: d.RecreateCoin.String(), + RecreateToken: d.RecreateToken.String(), + DeclareCandidacy: d.DeclareCandidacy.String(), + Delegate: d.Delegate.String(), + Unbond: d.Unbond.String(), + RedeemCheck: d.RedeemCheck.String(), + SetCandidateOn: d.SetCandidateOn.String(), + SetCandidateOff: d.SetCandidateOff.String(), + CreateMultisig: d.CreateMultisig.String(), + MultisendBase: d.MultisendBase.String(), + MultisendDelta: d.MultisendDelta.String(), + EditCandidate: d.EditCandidate.String(), + SetHaltBlock: d.SetHaltBlock.String(), + EditTickerOwner: d.EditTickerOwner.String(), + EditMultisig: d.EditMultisig.String(), + PriceVote: d.PriceVote.String(), + EditCandidatePublicKey: d.EditCandidatePublicKey.String(), + CreateSwapPool: d.CreateSwapPool.String(), + AddLiquidity: d.AddLiquidity.String(), + RemoveLiquidity: d.RemoveLiquidity.String(), + EditCandidateCommission: d.EditCandidateCommission.String(), + MoveStake: d.MoveStake.String(), + MintToken: d.MintToken.String(), + BurnToken: d.BurnToken.String(), + VoteCommission: d.VoteCommission.String(), + VoteUpdate: d.VoteUpdate.String(), + } +} + func encodeToStruct(b []byte) (*_struct.Struct, error) { dataStruct := &_struct.Struct{} if err := dataStruct.UnmarshalJSON(b); err != nil { diff --git a/api/v2/service/estimate_coin_buy.go b/api/v2/service/estimate_coin_buy.go index 9394be1a0..cc4659e61 100644 --- a/api/v2/service/estimate_coin_buy.go +++ b/api/v2/service/estimate_coin_buy.go @@ -2,8 +2,11 @@ package service import ( "context" + "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/state/swap" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" @@ -20,14 +23,15 @@ func (s *Service) EstimateCoinBuy(ctx context.Context, req *pb.EstimateCoinBuyRe return nil, status.Error(codes.InvalidArgument, "Value to buy not specified") } + if len(req.Route) > 3 { + return nil, s.createError(status.New(codes.OutOfRange, "maximum allowed length of the exchange chain is 5"), transaction.EncodeError(code.NewCustomCode(code.TooLongSwapRoute))) + } + cState, err := s.blockchain.GetStateForHeight(req.Height) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } - cState.RLock() - defer cState.RUnlock() - var coinToBuy types.CoinID if req.GetCoinToBuy() != "" { symbol := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(req.GetCoinToBuy()), types.GetVersionFromSymbol(req.GetCoinToBuy())) @@ -61,34 +65,236 @@ func (s *Service) EstimateCoinBuy(ctx context.Context, req *pb.EstimateCoinBuyRe transaction.EncodeError(code.NewCrossConvert(coinToSell.String(), cState.Coins().GetCoin(coinToSell).GetFullSymbol(), coinToBuy.String(), cState.Coins().GetCoin(coinToBuy).GetFullSymbol()))) } - commissionInBaseCoin := big.NewInt(0).Mul(big.NewInt(commissions.ConvertTx), transaction.CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) + var requestCoinCommissionID types.CoinID + if req.GetCoinCommission() != "" { + symbol := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(req.GetCoinToSell()), types.GetVersionFromSymbol(req.GetCoinToSell())) + if symbol == nil { + return nil, s.createError(status.New(codes.NotFound, "Coin to pay commission not exists"), transaction.EncodeError(code.NewCoinNotExists(req.GetCoinToSell(), ""))) + } + requestCoinCommissionID = symbol.ID() + } else { + requestCoinCommissionID = types.CoinID(req.GetCoinIdCommission()) + if !cState.Coins().Exists(coinToSell) { + return nil, s.createError(status.New(codes.NotFound, "Coin to pay commission not exists"), transaction.EncodeError(code.NewCoinNotExists("", coinToSell.String()))) + } + } - coinFrom := cState.Coins().GetCoin(coinToSell) - coinTo := cState.Coins().GetCoin(coinToBuy) + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + var coinFrom, coinTo transaction.CalculateCoin + coinFrom = cState.Coins().GetCoin(coinToSell) + coinTo = cState.Coins().GetCoin(coinToBuy) + + value := big.NewInt(0) + commissions := cState.Commission().GetCommissions() + var resultCommission *big.Int + swapFrom := req.SwapFrom + + reverseCoinIds(req.Route) + + switch req.SwapFrom { + case pb.SwapFrom_bancor: + commissionBancor, valueBancor, err := s.calcBuyBancorWithCommission(commissions, cState, requestCoinCommissionID, coinTo, coinFrom, valueToBuy) + if err != nil { + return nil, err + } + value = valueBancor + resultCommission = commissionBancor + case pb.SwapFrom_pool: + commissionPool, valuePool, err := s.calcBuyPoolWithCommission(ctx, commissions, cState, requestCoinCommissionID, valueToBuy, coinFrom, coinTo, req.Route) + if err != nil { + return nil, err + } + value = valuePool + resultCommission = commissionPool + default: + commissionBancor, valueBancor, errBancor := s.calcBuyBancorWithCommission(commissions, cState, requestCoinCommissionID, coinTo, coinFrom, valueToBuy) + commissionPool, valuePool, errPool := s.calcBuyPoolWithCommission(ctx, commissions, cState, requestCoinCommissionID, valueToBuy, coinFrom, coinTo, req.Route) + + if valueBancor != nil && valuePool != nil { + if valueBancor.Cmp(valuePool) == 1 { + value = valuePool + resultCommission = commissionPool + swapFrom = pb.SwapFrom_pool + } else { + value = valueBancor + resultCommission = commissionBancor + swapFrom = pb.SwapFrom_bancor + } + break + } - if !coinToSell.IsBaseCoin() { - commission = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), commissionInBaseCoin) + if valueBancor != nil { + value = valueBancor + resultCommission = commissionBancor + swapFrom = pb.SwapFrom_bancor + break + } + if valuePool != nil { + value = valuePool + resultCommission = commissionPool + swapFrom = pb.SwapFrom_pool + break + } + + respBancor, _ := status.FromError(errBancor) + respPool, _ := status.FromError(errPool) + return nil, s.createError(status.New(codes.FailedPrecondition, "not possible to exchange"), + transaction.EncodeError(code.NewCommissionCoinNotSufficient(respBancor.Message(), respPool.Message()))) } - value := valueToBuy + return &pb.EstimateCoinBuyResponse{ + WillPay: value.String(), + Commission: resultCommission.String(), + SwapFrom: swapFrom, + }, nil +} + +func (s *Service) calcBuyFromPool(ctx context.Context, value *big.Int, cState *state.CheckState, coinFrom transaction.CalculateCoin, coinTo transaction.CalculateCoin, route []uint64, commissionPoolSwapper swap.EditableChecker) (*big.Int, error) { + buyCoinID := coinTo.ID() + buyValue := big.NewInt(0).Set(value) + coinBuy := coinTo + for _, sellCoinInt := range append(route, uint64(coinFrom.ID())) { + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + sellCoinID := types.CoinID(sellCoinInt) + swapChecker := cState.Swap().GetSwapper(sellCoinID, buyCoinID) + + if !swapChecker.IsExist() { + return nil, s.createError(status.New(codes.NotFound, fmt.Sprintf("swap pair beetwen coins %s and %s not exists", coinFrom.GetFullSymbol(), coinBuy.GetFullSymbol())), transaction.EncodeError(code.NewPairNotExists(coinFrom.ID().String(), coinBuy.ID().String()))) + } + + if swapChecker.CoinID() == commissionPoolSwapper.CoinID() { + if sellCoinID != types.GetBaseCoinID() { + swapChecker = commissionPoolSwapper.Revert() + } else { + swapChecker = commissionPoolSwapper + } + } + + sellValue := swapChecker.CalculateSellForBuy(buyValue) + if sellValue == nil { + reserve0, reserve1 := swapChecker.Reserves() + symbolOut := coinBuy.GetFullSymbol() + return nil, s.createError(status.New(codes.FailedPrecondition, fmt.Sprintf("You wanted to buy %s %s, but pool reserve has only %s %s", value, symbolOut, reserve1.String(), symbolOut)), transaction.EncodeError(code.NewInsufficientLiquidity(coinFrom.ID().String(), sellValue.String(), coinBuy.ID().String(), value.String(), reserve0.String(), reserve1.String()))) + } + + coinSell := coinFrom + if sellCoinID != coinSell.ID() { + coinSell = cState.Coins().GetCoin(sellCoinID) + } - if !coinToBuy.IsBaseCoin() { - if errResp := transaction.CheckForCoinSupplyOverflow(coinTo, valueToBuy); errResp != nil { + if errResp := transaction.CheckSwap(swapChecker, coinSell, coinBuy, sellValue, buyValue, true); errResp != nil { return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) } - value = formula.CalculatePurchaseAmount(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), valueToBuy) + + buyValue = sellValue + coinBuy = coinSell + buyCoinID = sellCoinID } - if !coinToSell.IsBaseCoin() { - if errResp := transaction.CheckReserveUnderflow(coinFrom, value); errResp != nil { + return buyValue, nil +} + +func (s *Service) calcBuyFromBancor(value *big.Int, coinTo transaction.CalculateCoin, coinFrom transaction.CalculateCoin) (*big.Int, error) { + if !coinTo.BaseOrHasReserve() { + return nil, s.createError(status.New(codes.FailedPrecondition, "coin to buy has no reserve"), transaction.EncodeError(code.NewCoinHasNotReserve( + coinTo.GetFullSymbol(), + coinTo.ID().String(), + ))) + } + if !coinFrom.BaseOrHasReserve() { + return nil, s.createError(status.New(codes.FailedPrecondition, "sell coin has no reserve"), transaction.EncodeError(code.NewCoinHasNotReserve( + coinFrom.GetFullSymbol(), + coinFrom.ID().String(), + ))) + } + + if !coinTo.ID().IsBaseCoin() { + if errResp := transaction.CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) } + value = formula.CalculatePurchaseAmount(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), value) + } + if !coinFrom.ID().IsBaseCoin() { value = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), value) + if errResp := transaction.CheckReserveUnderflow(coinFrom, value); errResp != nil { + return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) + } } + return value, nil +} - return &pb.EstimateCoinBuyResponse{ - WillPay: value.String(), - Commission: commission.String(), - }, nil +func (s *Service) calcBuyBancorWithCommission(commissions *commission.Price, cState *state.CheckState, requestCoinCommissionID types.CoinID, coinTo transaction.CalculateCoin, coinFrom transaction.CalculateCoin, valueToBuy *big.Int) (*big.Int, *big.Int, error) { + commissionInBaseCoin := commissions.BuyBancor + commission, commissionFromPool, err := s.commissionInCoin(cState, requestCoinCommissionID, commissions.Coin, commissionInBaseCoin) + if err != nil { + return nil, nil, err + } + + if !commissionFromPool { + if requestCoinCommissionID == coinTo.ID() { + coinTo = transaction.NewDummyCoin( + coinTo.ID(), + big.NewInt(0).Sub(coinTo.Volume(), commission), + big.NewInt(0).Sub(coinTo.Reserve(), commissionInBaseCoin), + coinTo.Crr(), + coinTo.GetFullSymbol(), + coinTo.MaxSupply(), + ) + } else if requestCoinCommissionID == coinFrom.ID() { + coinFrom = transaction.NewDummyCoin( + coinFrom.ID(), + big.NewInt(0).Sub(coinFrom.Volume(), commission), + big.NewInt(0).Sub(coinFrom.Reserve(), commissionInBaseCoin), + coinFrom.Crr(), + coinFrom.GetFullSymbol(), + coinFrom.MaxSupply(), + ) + } + } + + valueBancor, errBancor := s.calcBuyFromBancor(valueToBuy, coinTo, coinFrom) + if errBancor != nil { + return nil, nil, errBancor + } + return commission, valueBancor, nil +} + +func (s *Service) calcBuyPoolWithCommission(ctx context.Context, commissions *commission.Price, cState *state.CheckState, requestCoinCommissionID types.CoinID, valueToBuy *big.Int, coinFrom transaction.CalculateCoin, coinTo transaction.CalculateCoin, route []uint64) (*big.Int, *big.Int, error) { + commissionInBaseCoin := big.NewInt(0).Add(commissions.BuyPoolBase, big.NewInt(0).Mul(commissions.BuyPoolDelta, big.NewInt(int64(len(route))))) + commission, commissionFromPool, err := s.commissionInCoin(cState, requestCoinCommissionID, commissions.Coin, commissionInBaseCoin) + if err != nil { + return nil, nil, err + } + + commissionPoolSwapper := cState.Swap().GetSwapper(requestCoinCommissionID, types.GetBaseCoinID()) + if commissionFromPool { + commissionPoolSwapper = commissionPoolSwapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, nil, timeoutStatus.Err() + } + + valuePool, errPool := s.calcBuyFromPool(ctx, valueToBuy, cState, coinFrom, coinTo, route, commissionPoolSwapper) + if errPool != nil { + return nil, nil, errPool + } + return commission, valuePool, nil +} + +func reverseCoinIds(a []uint64) { + if len(a) == 0 { + return + } + for i := len(a)/2 - 1; i >= 0; i-- { + opp := len(a) - 1 - i + a[i], a[opp] = a[opp], a[i] + } } diff --git a/api/v2/service/estimate_coin_sell.go b/api/v2/service/estimate_coin_sell.go index 0dafb2a52..31a5579c2 100644 --- a/api/v2/service/estimate_coin_sell.go +++ b/api/v2/service/estimate_coin_sell.go @@ -2,8 +2,11 @@ package service import ( "context" + "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/state/swap" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" @@ -20,14 +23,15 @@ func (s *Service) EstimateCoinSell(ctx context.Context, req *pb.EstimateCoinSell return nil, status.Error(codes.InvalidArgument, "Value to sell not specified") } + if len(req.Route) > 3 { + return nil, s.createError(status.New(codes.OutOfRange, "maximum allowed length of the exchange chain is 5"), transaction.EncodeError(code.NewCustomCode(code.TooLongSwapRoute))) + } + cState, err := s.blockchain.GetStateForHeight(req.Height) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } - cState.RLock() - defer cState.RUnlock() - var coinToBuy types.CoinID if req.GetCoinToBuy() != "" { symbol := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(req.GetCoinToBuy()), types.GetVersionFromSymbol(req.GetCoinToBuy())) @@ -61,35 +65,247 @@ func (s *Service) EstimateCoinSell(ctx context.Context, req *pb.EstimateCoinSell transaction.EncodeError(code.NewCrossConvert(coinToSell.String(), cState.Coins().GetCoin(coinToSell).GetFullSymbol(), coinToBuy.String(), cState.Coins().GetCoin(coinToBuy).GetFullSymbol()))) } - commissionInBaseCoin := big.NewInt(0).Mul(big.NewInt(commissions.ConvertTx), transaction.CommissionMultiplier) - commission := big.NewInt(0).Set(commissionInBaseCoin) + var requestCoinCommissionID types.CoinID + if req.GetCoinCommission() != "" { + symbol := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(req.GetCoinToSell()), types.GetVersionFromSymbol(req.GetCoinToSell())) + if symbol == nil { + return nil, s.createError(status.New(codes.NotFound, "Coin to pay commission not exists"), transaction.EncodeError(code.NewCoinNotExists(req.GetCoinToSell(), ""))) + } + requestCoinCommissionID = symbol.ID() + } else { + requestCoinCommissionID = types.CoinID(req.GetCoinIdCommission()) + if !cState.Coins().Exists(coinToSell) { + return nil, s.createError(status.New(codes.NotFound, "Coin to pay commission not exists"), transaction.EncodeError(code.NewCoinNotExists("", coinToSell.String()))) + } + } + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + var coinFrom, coinTo transaction.CalculateCoin + coinFrom = cState.Coins().GetCoin(coinToSell) + coinTo = cState.Coins().GetCoin(coinToBuy) + + value := big.NewInt(0) + commissions := cState.Commission().GetCommissions() + var resultCommission *big.Int + swapFrom := req.SwapFrom + + switch req.SwapFrom { + case pb.SwapFrom_bancor: + commission, valueBancor, err := s.calcSellBancorWithCommission(commissions, cState, requestCoinCommissionID, coinTo, coinFrom, valueToSell) + if err != nil { + return nil, err + } + value = valueBancor + resultCommission = commission + case pb.SwapFrom_pool: + commission, valuePool, err := s.calcSellPoolWithCommission(ctx, commissions, cState, requestCoinCommissionID, valueToSell, coinFrom, coinTo, req.Route) + if err != nil { + return nil, err + } + value = valuePool + resultCommission = commission + default: + commissionBancor, valueBancor, errBancor := s.calcSellBancorWithCommission(commissions, cState, requestCoinCommissionID, coinTo, coinFrom, valueToSell) + commissionPool, valuePool, errPool := s.calcSellPoolWithCommission(ctx, commissions, cState, requestCoinCommissionID, valueToSell, coinFrom, coinTo, req.Route) + + if valueBancor != nil && valuePool != nil { + if valueBancor.Cmp(valuePool) == -1 { + value = valuePool + resultCommission = commissionPool + swapFrom = pb.SwapFrom_pool + } else { + value = valueBancor + resultCommission = commissionBancor + swapFrom = pb.SwapFrom_bancor + } + break + } + + if valueBancor != nil { + value = valueBancor + resultCommission = commissionBancor + swapFrom = pb.SwapFrom_bancor + break + } + if valuePool != nil { + value = valuePool + resultCommission = commissionPool + swapFrom = pb.SwapFrom_pool + break + } + + respBancor, _ := status.FromError(errBancor) + respPool, _ := status.FromError(errPool) + return nil, s.createError(status.New(codes.FailedPrecondition, "not possible to exchange"), + transaction.EncodeError(code.NewCommissionCoinNotSufficient(respBancor.Message(), respPool.Message()))) + } + + res := &pb.EstimateCoinSellResponse{ + WillGet: value.String(), + Commission: resultCommission.String(), + SwapFrom: swapFrom, + } + return res, nil +} - coinFrom := cState.Coins().GetCoin(coinToSell) - coinTo := cState.Coins().GetCoin(coinToBuy) +func (s *Service) calcSellBancorWithCommission(commissions *commission.Price, cState *state.CheckState, requestCoinCommissionID types.CoinID, coinTo transaction.CalculateCoin, coinFrom transaction.CalculateCoin, valueToSell *big.Int) (*big.Int, *big.Int, error) { + commissionInBaseCoin := commissions.SellBancor + commission, commissionFromPool, err := s.commissionInCoin(cState, requestCoinCommissionID, commissions.Coin, commissionInBaseCoin) + if err != nil { + return nil, nil, err + } - if !coinToSell.IsBaseCoin() { - commission = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), commissionInBaseCoin) + if !commissionFromPool { + if requestCoinCommissionID == coinTo.ID() { + coinTo = transaction.NewDummyCoin( + coinTo.ID(), + big.NewInt(0).Sub(coinTo.Volume(), commission), + big.NewInt(0).Sub(coinTo.Reserve(), commissionInBaseCoin), + coinTo.Crr(), + coinTo.GetFullSymbol(), + coinTo.MaxSupply(), + ) + } else if requestCoinCommissionID == coinFrom.ID() { + coinFrom = transaction.NewDummyCoin( + coinFrom.ID(), + big.NewInt(0).Sub(coinFrom.Volume(), commission), + big.NewInt(0).Sub(coinFrom.Reserve(), commissionInBaseCoin), + coinFrom.Crr(), + coinFrom.GetFullSymbol(), + coinFrom.MaxSupply(), + ) + } } - value := valueToSell + valueBancor, errBancor := s.calcSellFromBancor(valueToSell, coinTo, coinFrom) + if errBancor != nil { + return nil, nil, errBancor + } + return commission, valueBancor, nil +} + +func (s *Service) calcSellPoolWithCommission(ctx context.Context, commissions *commission.Price, cState *state.CheckState, requestCoinCommissionID types.CoinID, valueToSell *big.Int, coinFrom transaction.CalculateCoin, coinTo transaction.CalculateCoin, route []uint64) (*big.Int, *big.Int, error) { + commissionInBaseCoin := big.NewInt(0).Add(commissions.SellPoolBase, big.NewInt(0).Mul(commissions.SellPoolDelta, big.NewInt(int64(len(route))))) + commission, commissionFromPool, err := s.commissionInCoin(cState, requestCoinCommissionID, commissions.Coin, commissionInBaseCoin) + if err != nil { + return nil, nil, err + } - if !coinToSell.IsBaseCoin() { - value = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToSell) + commissionPoolSwapper := cState.Swap().GetSwapper(requestCoinCommissionID, types.GetBaseCoinID()) + if commissionFromPool { + commissionPoolSwapper = commissionPoolSwapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, nil, timeoutStatus.Err() + } + + valuePool, errPool := s.calcSellFromPool(ctx, valueToSell, cState, coinFrom, coinTo, route, commissionPoolSwapper) + if errPool != nil { + return nil, nil, errPool + } + return commission, valuePool, nil +} + +func (s *Service) commissionInCoin(cState *state.CheckState, coinCommissionID types.CoinID, commissionsCoin types.CoinID, commissionInBaseCoin *big.Int) (*big.Int, bool, error) { + coinCommission := cState.Coins().GetCoin(coinCommissionID) + + var isSwapFromPool bool + var commission *big.Int + switch coinCommissionID { + case commissionsCoin: + commission = commissionInBaseCoin + case types.GetBaseCoinID(): + commission = cState.Swap().GetSwapper(types.GetBaseCoinID(), commissionsCoin).CalculateSellForBuy(commissionInBaseCoin) + default: + if !commissionsCoin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap().GetSwapper(types.GetBaseCoinID(), commissionsCoin).CalculateSellForBuy(commissionInBaseCoin) + } + commissionPoolSwapper := cState.Swap().GetSwapper(coinCommissionID, types.GetBaseCoinID()) + + comm, fromPool, errResp := transaction.CalculateCommission(cState, commissionPoolSwapper, coinCommission, commissionInBaseCoin) + if errResp != nil { + return nil, false, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) + } + commission = comm + isSwapFromPool = bool(fromPool) + } + return commission, isSwapFromPool, nil +} + +func (s *Service) calcSellFromPool(ctx context.Context, value *big.Int, cState *state.CheckState, coinFrom transaction.CalculateCoin, coinTo transaction.CalculateCoin, route []uint64, commissionPoolSwapper swap.EditableChecker) (*big.Int, error) { + sellCoinID := coinFrom.ID() + sellValue := big.NewInt(0).Set(value) + coinSell := coinFrom + for _, buyCoinInt := range append(route, uint64(coinTo.ID())) { + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + buyCoinID := types.CoinID(buyCoinInt) + swapChecker := cState.Swap().GetSwapper(sellCoinID, buyCoinID) + + coinBuy := coinTo + if buyCoinID != coinBuy.ID() { + coinBuy = cState.Coins().GetCoin(buyCoinID) + } + if !swapChecker.IsExist() { + return nil, s.createError(status.New(codes.NotFound, fmt.Sprintf("swap pool between coins %s and %s not exists", coinSell.GetFullSymbol(), coinBuy.GetFullSymbol())), transaction.EncodeError(code.NewPairNotExists(coinSell.ID().String(), coinBuy.ID().String()))) + } + + if swapChecker.CoinID() == commissionPoolSwapper.CoinID() { + if sellCoinID != types.GetBaseCoinID() { + swapChecker = commissionPoolSwapper.Revert() + } else { + swapChecker = commissionPoolSwapper + } + } + + buyValue := swapChecker.CalculateBuyForSell(sellValue) + if buyValue == nil { // todo + reserve0, reserve1 := swapChecker.Reserves() + return nil, s.createError(status.New(codes.OutOfRange, fmt.Sprintf("swap pool has reserves %s %s and %d %s, you wanted sell %s %s", reserve0, coinSell.GetFullSymbol(), reserve1, coinBuy.GetFullSymbol(), sellValue, coinSell.GetFullSymbol())), "") + } + + if errResp := transaction.CheckSwap(swapChecker, coinSell, coinBuy, sellValue, buyValue, false); errResp != nil { + return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) + } + + sellValue = buyValue + coinSell = coinBuy + sellCoinID = buyCoinID + } + + return sellValue, nil +} + +func (s *Service) calcSellFromBancor(value *big.Int, coinTo transaction.CalculateCoin, coinFrom transaction.CalculateCoin) (*big.Int, error) { + if !coinTo.BaseOrHasReserve() { + return nil, s.createError(status.New(codes.FailedPrecondition, "coin to buy has no reserve"), transaction.EncodeError(code.NewCoinHasNotReserve( + coinTo.GetFullSymbol(), + coinTo.ID().String(), + ))) + } + if !coinFrom.BaseOrHasReserve() { + return nil, s.createError(status.New(codes.FailedPrecondition, "sell coin has no reserve"), transaction.EncodeError(code.NewCoinHasNotReserve( + coinFrom.GetFullSymbol(), + coinFrom.ID().String(), + ))) + } + + if !coinFrom.ID().IsBaseCoin() { + value = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), value) if errResp := transaction.CheckReserveUnderflow(coinFrom, value); errResp != nil { return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) } } - if !coinToBuy.IsBaseCoin() { + if !coinTo.ID().IsBaseCoin() { value = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), value) if errResp := transaction.CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) } } - - res := &pb.EstimateCoinSellResponse{ - WillGet: value.String(), - Commission: commission.String(), - } - return res, nil + return value, nil } diff --git a/api/v2/service/estimate_coin_sell_all.go b/api/v2/service/estimate_coin_sell_all.go index 141f1b682..4e45bbbd0 100644 --- a/api/v2/service/estimate_coin_sell_all.go +++ b/api/v2/service/estimate_coin_sell_all.go @@ -3,7 +3,7 @@ package service import ( "context" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" + "github.com/MinterTeam/minter-go-node/core/state/coins" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" @@ -20,14 +20,15 @@ func (s *Service) EstimateCoinSellAll(ctx context.Context, req *pb.EstimateCoinS return nil, status.Error(codes.InvalidArgument, "Value to sell not specified") } + if len(req.Route) > 3 { + return nil, s.createError(status.New(codes.OutOfRange, "maximum allowed length of the exchange chain is 5"), transaction.EncodeError(code.NewCustomCode(code.TooLongSwapRoute))) + } + cState, err := s.blockchain.GetStateForHeight(req.Height) if err != nil { return nil, status.Error(codes.NotFound, err.Error()) } - cState.RLock() - defer cState.RUnlock() - var coinToBuy types.CoinID if req.GetCoinToBuy() != "" { symbol := cState.Coins().GetCoinBySymbol(types.StrToCoinBaseSymbol(req.GetCoinToBuy()), types.GetVersionFromSymbol(req.GetCoinToBuy())) @@ -62,19 +63,183 @@ func (s *Service) EstimateCoinSellAll(ctx context.Context, req *pb.EstimateCoinS transaction.EncodeError(code.NewCrossConvert(coinToSell.String(), cState.Coins().GetCoin(coinToSell).GetFullSymbol(), coinToBuy.String(), cState.Coins().GetCoin(coinToBuy).GetFullSymbol()))) } - commissionInBaseCoin := big.NewInt(commissions.ConvertTx) - if req.GasPrice > 1 { - commissionInBaseCoin.Mul(commissionInBaseCoin, big.NewInt(int64(req.GasPrice))) + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() } - commissionInBaseCoin = big.NewInt(0).Mul(commissionInBaseCoin, transaction.CommissionMultiplier) - coinFrom := cState.Coins().GetCoin(coinToSell) + var coinFrom transaction.CalculateCoin + coinFrom = cState.Coins().GetCoin(coinToSell) coinTo := cState.Coins().GetCoin(coinToBuy) - value := valueToSell + commissions := cState.Commission().GetCommissions() + + var valueBancor, valuePool *big.Int + var errBancor, errPool error + value := big.NewInt(0) + if req.SwapFrom == pb.SwapFrom_bancor || req.SwapFrom == pb.SwapFrom_optimal { + commissionInBaseCoin := new(big.Int).Set(commissions.SellAllBancor) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap().GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + if req.GasPrice > 1 { + commissionInBaseCoin.Mul(commissionInBaseCoin, big.NewInt(int64(req.GasPrice))) + } + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + commission := commissionInBaseCoin + if !coinFrom.ID().IsBaseCoin() { + commissionPoolSwapper := cState.Swap().GetSwapper(coinFrom.ID(), types.GetBaseCoinID()) + commissionFrom, isFromPool, errResp := transaction.CalculateCommission(cState, commissionPoolSwapper, coinFrom, commissionInBaseCoin) + if errResp != nil { + return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) + } + commission = commissionFrom + if isFromPool == false && !coinFrom.ID().IsBaseCoin() { + coinFrom = transaction.NewDummyCoin( + coinFrom.ID(), + big.NewInt(0).Sub(coinFrom.Volume(), commission), + big.NewInt(0).Sub(coinFrom.Reserve(), commissionInBaseCoin), + coinFrom.Crr(), + coinFrom.GetFullSymbol(), + coinFrom.MaxSupply(), + ) + } + } + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + valueToSell.Sub(valueToSell, commission) + if valueToSell.Sign() != 1 { + return nil, s.createError(status.New(codes.FailedPrecondition, "not enough coins to pay commission"), + transaction.EncodeError(code.NewMinimumValueToBuyReached("1", valueToSell.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String()))) + } + + valueBancor, errBancor = s.calcSellAllFromBancor(valueToSell, coinTo, coinFrom, commissionInBaseCoin) + } + if req.SwapFrom == pb.SwapFrom_pool || req.SwapFrom == pb.SwapFrom_optimal { + commissionInBaseCoin := new(big.Int).Set(commissions.SellAllPoolBase) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap().GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + if req.GasPrice > 1 { + commissionInBaseCoin.Mul(commissionInBaseCoin, big.NewInt(int64(req.GasPrice))) + } + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + commissionPoolSwapper := cState.Swap().GetSwapper(coinFrom.ID(), types.GetBaseCoinID()) + commission := commissionInBaseCoin + if !coinFrom.ID().IsBaseCoin() { + commissionFrom, isFromPool, errResp := transaction.CalculateCommission(cState, commissionPoolSwapper, coinFrom, commissionInBaseCoin) + if errResp != nil { + return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) + } + commission = commissionFrom + if isFromPool { + commissionPoolSwapper = commissionPoolSwapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + } + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + valueToSell.Sub(valueToSell, commission) + if valueToSell.Sign() != 1 { + return nil, s.createError(status.New(codes.FailedPrecondition, "not enough coins to pay commission"), + transaction.EncodeError(code.NewMinimumValueToBuyReached("1", valueToSell.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String()))) + } + valuePool, errPool = s.calcSellFromPool(ctx, valueToSell, cState, coinFrom, coinTo, req.Route, commissionPoolSwapper) + } + + if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { + return nil, timeoutStatus.Err() + } + + swapFrom := req.SwapFrom + switch req.SwapFrom { + case pb.SwapFrom_bancor: + if errBancor != nil { + return nil, errBancor + } + value = valueBancor + case pb.SwapFrom_pool: + if errPool != nil { + return nil, errPool + } + value = valuePool + default: + if valueBancor != nil && valuePool != nil { + if valueBancor.Cmp(valuePool) == -1 { + value = valuePool + swapFrom = pb.SwapFrom_pool + } else { + value = valueBancor + swapFrom = pb.SwapFrom_bancor + } + break + } + + if valueBancor != nil { + value = valueBancor + swapFrom = pb.SwapFrom_bancor + break + } + if valuePool != nil { + value = valuePool + swapFrom = pb.SwapFrom_pool + break + } - if !coinToSell.IsBaseCoin() { - value = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), valueToSell) + respBancor, _ := status.FromError(errBancor) + respPool, _ := status.FromError(errPool) + return nil, s.createError(status.New(codes.FailedPrecondition, "not possible to exchange"), + transaction.EncodeError(code.NewCommissionCoinNotSufficient(respBancor.Message(), respPool.Message()))) + } + + return &pb.EstimateCoinSellAllResponse{ + WillGet: value.String(), + SwapFrom: swapFrom, + }, nil +} + +func (s *Service) calcSellAllFromBancor(value *big.Int, coinTo *coins.Model, coinFrom transaction.CalculateCoin, commissionInBaseCoin *big.Int) (*big.Int, error) { + if !coinTo.BaseOrHasReserve() { + return nil, s.createError(status.New(codes.FailedPrecondition, "coin to buy has no reserve"), transaction.EncodeError(code.NewCoinHasNotReserve( + coinTo.GetFullSymbol(), + coinTo.ID().String(), + ))) + } + if !coinFrom.BaseOrHasReserve() { + return nil, s.createError(status.New(codes.FailedPrecondition, "sell coin has no reserve"), transaction.EncodeError(code.NewCoinHasNotReserve( + coinFrom.GetFullSymbol(), + coinFrom.ID().String(), + ))) + } + + if !coinFrom.ID().IsBaseCoin() { + value = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), value) + if errResp := transaction.CheckReserveUnderflow(coinFrom, value); errResp != nil { + return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) + } + } + + if !coinTo.ID().IsBaseCoin() { + value = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), value) + if errResp := transaction.CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { + return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) + } + } + + if !coinFrom.ID().IsBaseCoin() { + value = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), value) if errResp := transaction.CheckReserveUnderflow(coinFrom, value); errResp != nil { return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) } @@ -83,14 +248,12 @@ func (s *Service) EstimateCoinSellAll(ctx context.Context, req *pb.EstimateCoinS if value.Sign() != 1 { return nil, status.New(codes.FailedPrecondition, "Not enough coins to pay commission").Err() } - if !coinToBuy.IsBaseCoin() { + if !coinTo.ID().IsBaseCoin() { value = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), value) if errResp := transaction.CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) } } - return &pb.EstimateCoinSellAllResponse{ - WillGet: value.String(), - }, nil + return value, nil } diff --git a/api/v2/service/estimate_tx_commission.go b/api/v2/service/estimate_tx_commission.go index 50820296c..67fd02f33 100644 --- a/api/v2/service/estimate_tx_commission.go +++ b/api/v2/service/estimate_tx_commission.go @@ -3,13 +3,10 @@ package service import ( "context" "encoding/hex" - "fmt" - "math/big" + "github.com/MinterTeam/minter-go-node/core/types" "strings" - "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/formula" pb "github.com/MinterTeam/node-grpc-gateway/api_pb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -22,9 +19,6 @@ func (s *Service) EstimateTxCommission(ctx context.Context, req *pb.EstimateTxCo return nil, status.Error(codes.NotFound, err.Error()) } - cState.RLock() - defer cState.RUnlock() - if !strings.HasPrefix(strings.Title(req.GetTx()), "0x") { return nil, status.Error(codes.InvalidArgument, "invalid transaction") } @@ -34,31 +28,22 @@ func (s *Service) EstimateTxCommission(ctx context.Context, req *pb.EstimateTxCo return nil, status.Error(codes.InvalidArgument, err.Error()) } - decodedTx, err := transaction.TxDecoder.DecodeFromBytesWithoutSig(decodeString) + decodedTx, err := transaction.DecodeFromBytesWithoutSig(decodeString) if err != nil { return nil, status.Errorf(codes.InvalidArgument, "Cannot decode transaction: %s", err.Error()) } - commissionInBaseCoin := decodedTx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !decodedTx.GasCoin.IsBaseCoin() { - coin := cState.Coins().GetCoin(decodedTx.GasCoin) - - if coin.Reserve().Cmp(commissionInBaseCoin) < 0 { - return nil, s.createError( - status.New(codes.InvalidArgument, fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s, required %s", - coin.Reserve().String(), commissionInBaseCoin.String())), - transaction.EncodeError(code.NewCoinReserveNotSufficient( - coin.GetFullSymbol(), - coin.ID().String(), - coin.Reserve().String(), - commissionInBaseCoin.String(), - )), - ) - } + commissions := cState.Commission().GetCommissions() + price := decodedTx.Price(commissions) + if !commissions.Coin.IsBaseCoin() { + price = cState.Swap().GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(price) + } - commission = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), commissionInBaseCoin) + commissionInBaseCoin := decodedTx.Commission(price) + commissionPoolSwapper := cState.Swap().GetSwapper(decodedTx.GasCoin, types.GetBaseCoinID()) + commission, _, errResp := transaction.CalculateCommission(cState, commissionPoolSwapper, cState.Coins().GetCoin(decodedTx.GasCoin), commissionInBaseCoin) + if errResp != nil { + return nil, s.createError(status.New(codes.FailedPrecondition, errResp.Log), errResp.Info) } return &pb.EstimateTxCommissionResponse{ diff --git a/api/v2/service/events.go b/api/v2/service/events.go index 6eb42c553..27f3daee0 100644 --- a/api/v2/service/events.go +++ b/api/v2/service/events.go @@ -2,41 +2,46 @@ package service import ( "context" + "github.com/MinterTeam/minter-go-node/core/events" pb "github.com/MinterTeam/node-grpc-gateway/api_pb" _struct "github.com/golang/protobuf/ptypes/struct" + tmjson "github.com/tendermint/tendermint/libs/json" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) // Events returns events at given height. func (s *Service) Events(ctx context.Context, req *pb.EventsRequest) (*pb.EventsResponse, error) { - currentHeight := s.blockchain.Height() - if req.Height > currentHeight { - return nil, status.Errorf(codes.NotFound, "wanted to load target %d but only found up to %d", req.Height, currentHeight) - } - height := uint32(req.Height) - events := s.blockchain.GetEventsDB().LoadEvents(height) - resultEvents := make([]*_struct.Struct, 0, len(events)) - for _, event := range events { + loadEvents := s.blockchain.GetEventsDB().LoadEvents(height) + if loadEvents == nil { + return nil, status.Errorf(codes.NotFound, "version %d doesn't exist yet", req.Height) + } + resultEvents := make([]*_struct.Struct, 0, len(loadEvents)) + for _, event := range loadEvents { if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { return nil, timeoutStatus.Err() } - var find = true - for _, s := range req.Search { - if event.AddressString() == s || event.ValidatorPubKeyString() == s { - find = true - break + if len(req.Search) > 0 { + if e, ok := event.(events.Stake); ok { + var find = true + for _, s := range req.Search { + if e.AddressString() == s || e.ValidatorPubKeyString() == s { + find = true + break + } + find = false + } + if !find { + continue + } + } else { + continue } - find = false } - if !find { - continue - } - - marshalJSON, err := s.cdc.MarshalJSON(event) + marshalJSON, err := tmjson.Marshal(event) if err != nil { return nil, status.Errorf(codes.Internal, err.Error()) } diff --git a/api/v2/service/frozen.go b/api/v2/service/frozen.go index 34eb21ce6..a0ebda207 100644 --- a/api/v2/service/frozen.go +++ b/api/v2/service/frozen.go @@ -3,7 +3,6 @@ package service import ( "context" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/state/candidates" "github.com/MinterTeam/minter-go-node/core/state/coins" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" @@ -20,8 +19,6 @@ func (s *Service) Frozen(ctx context.Context, req *pb.FrozenRequest) (*pb.Frozen } cState := s.blockchain.CurrentState() - cState.RLock() - defer cState.RUnlock() var reqCoin *coins.Model @@ -36,7 +33,7 @@ func (s *Service) Frozen(ctx context.Context, req *pb.FrozenRequest) (*pb.Frozen cState.FrozenFunds().GetFrozenFunds(s.blockchain.Height()) - for i := s.blockchain.Height(); i <= s.blockchain.Height()+candidates.UnbondPeriod; i++ { + for i := s.blockchain.Height(); i <= s.blockchain.Height()+types.GetUnbondPeriod(); i++ { if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { return nil, timeoutStatus.Err() diff --git a/api/v2/service/genesis.go b/api/v2/service/genesis.go index 91e17316d..f685bc86e 100644 --- a/api/v2/service/genesis.go +++ b/api/v2/service/genesis.go @@ -12,7 +12,7 @@ import ( // Genesis returns genesis file. func (s *Service) Genesis(ctx context.Context, _ *empty.Empty) (*pb.GenesisResponse, error) { - result, err := s.client.Genesis() + result, err := s.client.Genesis(ctx) if err != nil { return nil, status.Error(codes.FailedPrecondition, err.Error()) } @@ -32,8 +32,9 @@ func (s *Service) Genesis(ctx context.Context, _ *empty.Empty) (*pb.GenesisRespo } return &pb.GenesisResponse{ - GenesisTime: result.Genesis.GenesisTime.Format(time.RFC3339Nano), - ChainId: result.Genesis.ChainID, + GenesisTime: result.Genesis.GenesisTime.Format(time.RFC3339Nano), + ChainId: result.Genesis.ChainID, + InitialHeight: uint64(result.Genesis.InitialHeight), ConsensusParams: &pb.GenesisResponse_ConsensusParams{ Block: &pb.GenesisResponse_ConsensusParams_Block{ MaxBytes: result.Genesis.ConsensusParams.Block.MaxBytes, diff --git a/api/v2/service/halts.go b/api/v2/service/halts.go index 2f3280779..39a5a4f91 100644 --- a/api/v2/service/halts.go +++ b/api/v2/service/halts.go @@ -3,16 +3,11 @@ package service import ( "context" pb "github.com/MinterTeam/node-grpc-gateway/api_pb" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" ) // Halts returns votes func (s *Service) Halts(_ context.Context, req *pb.HaltsRequest) (*pb.HaltsResponse, error) { - cState, err := s.blockchain.GetStateForHeight(req.Height) - if err != nil { - return nil, status.Error(codes.NotFound, err.Error()) - } + cState := s.blockchain.CurrentState() blocks := cState.Halts().GetHaltBlocks(req.Height) diff --git a/api/v2/service/missed_blocks.go b/api/v2/service/missed_blocks.go index 776d70d57..65092dab5 100644 --- a/api/v2/service/missed_blocks.go +++ b/api/v2/service/missed_blocks.go @@ -21,14 +21,9 @@ func (s *Service) MissedBlocks(ctx context.Context, req *pb.MissedBlocksRequest) } if req.Height != 0 { - cState.Lock() cState.Validators().LoadValidators() - cState.Unlock() } - cState.RLock() - defer cState.RUnlock() - val := cState.Validators().GetByPublicKey(types.HexToPubkey(req.PublicKey)) if val == nil { return nil, status.Error(codes.NotFound, "Validator not found") diff --git a/api/v2/service/net_info.go b/api/v2/service/net_info.go index f68894789..c8c910b97 100644 --- a/api/v2/service/net_info.go +++ b/api/v2/service/net_info.go @@ -15,7 +15,7 @@ import ( // NetInfo returns network info. func (s *Service) NetInfo(ctx context.Context, _ *empty.Empty) (*pb.NetInfoResponse, error) { - result, err := s.client.NetInfo() + result, err := s.client.NetInfo(ctx) if err != nil { return nil, status.Error(codes.FailedPrecondition, err.Error()) } diff --git a/api/v2/service/price_commission.go b/api/v2/service/price_commission.go new file mode 100644 index 000000000..f2adfc0c3 --- /dev/null +++ b/api/v2/service/price_commission.go @@ -0,0 +1,75 @@ +package service + +import ( + "context" + "github.com/MinterTeam/minter-go-node/core/state/coins" + "github.com/MinterTeam/minter-go-node/core/state/commission" + pb "github.com/MinterTeam/node-grpc-gateway/api_pb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// PriceCommission returns current tx commissions +func (s *Service) PriceCommission(ctx context.Context, req *pb.PriceCommissionRequest) (*pb.PriceCommissionResponse, error) { + cState, err := s.blockchain.GetStateForHeight(req.Height) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + + price := cState.Commission().GetCommissions() + + return priceCommissionResponse(price, cState.Coins().GetCoin(price.Coin)), nil +} + +func priceCommissionResponse(price *commission.Price, coin *coins.Model) *pb.PriceCommissionResponse { + return &pb.PriceCommissionResponse{ + Coin: &pb.Coin{ + Id: uint64(price.Coin), + Symbol: coin.GetFullSymbol(), + }, + PayloadByte: price.PayloadByte.String(), + Send: price.Send.String(), + BuyBancor: price.BuyBancor.String(), + SellBancor: price.SellBancor.String(), + SellAllBancor: price.SellAllBancor.String(), + BuyPoolBase: price.BuyPoolBase.String(), + SellPoolBase: price.SellPoolBase.String(), + SellAllPoolBase: price.SellAllPoolBase.String(), + BuyPoolDelta: price.BuyPoolDelta.String(), + SellPoolDelta: price.SellPoolDelta.String(), + SellAllPoolDelta: price.SellAllPoolDelta.String(), + CreateTicker3: price.CreateTicker3.String(), + CreateTicker4: price.CreateTicker4.String(), + CreateTicker5: price.CreateTicker5.String(), + CreateTicker6: price.CreateTicker6.String(), + CreateTicker7_10: price.CreateTicker7to10.String(), + CreateCoin: price.CreateCoin.String(), + CreateToken: price.CreateToken.String(), + RecreateCoin: price.RecreateCoin.String(), + RecreateToken: price.RecreateToken.String(), + DeclareCandidacy: price.DeclareCandidacy.String(), + Delegate: price.Delegate.String(), + Unbond: price.Unbond.String(), + RedeemCheck: price.RedeemCheck.String(), + SetCandidateOn: price.SetCandidateOn.String(), + SetCandidateOff: price.SetCandidateOff.String(), + CreateMultisig: price.CreateMultisig.String(), + MultisendBase: price.MultisendBase.String(), + MultisendDelta: price.MultisendDelta.String(), + EditCandidate: price.EditCandidate.String(), + SetHaltBlock: price.SetHaltBlock.String(), + EditTickerOwner: price.EditTickerOwner.String(), + EditMultisig: price.EditMultisig.String(), + PriceVote: price.PriceVote.String(), + EditCandidatePublicKey: price.EditCandidatePublicKey.String(), + CreateSwapPool: price.CreateSwapPool.String(), + AddLiquidity: price.AddLiquidity.String(), + RemoveLiquidity: price.RemoveLiquidity.String(), + EditCandidateCommission: price.EditCandidateCommission.String(), + MoveStake: price.MoveStake.String(), + MintToken: price.MintToken.String(), + BurnToken: price.BurnToken.String(), + VoteCommission: price.VoteCommission.String(), + VoteUpdate: price.VoteUpdate.String(), + } +} diff --git a/api/v2/service/price_votes.go b/api/v2/service/price_votes.go new file mode 100644 index 000000000..0296ba374 --- /dev/null +++ b/api/v2/service/price_votes.go @@ -0,0 +1,35 @@ +package service + +import ( + "context" + "github.com/MinterTeam/minter-go-node/core/state/commission" + pb "github.com/MinterTeam/node-grpc-gateway/api_pb" +) + +// PriceVotes returns votes of new tx commissions. +func (s *Service) PriceVotes(ctx context.Context, req *pb.PriceVotesRequest) (*pb.PriceVotesResponse, error) { + cState := s.blockchain.CurrentState() + + votes := cState.Commission().GetVotes(req.Height) + + if len(votes) == 0 { + return &pb.PriceVotesResponse{}, nil + } + + resp := make([]*pb.PriceVotesResponse_Vote, 0, len(votes)) + for _, vote := range votes { + pubKeys := make([]string, 0, len(vote.Votes)) + for _, pubkey := range vote.Votes { + pubKeys = append(pubKeys, pubkey.String()) + } + price := commission.Decode(vote.Price) + resp = append(resp, &pb.PriceVotesResponse_Vote{ + Price: priceCommissionResponse(price, cState.Coins().GetCoin(price.Coin)), + PublicKeys: pubKeys, + }) + } + + return &pb.PriceVotesResponse{ + PriceVotes: resp, + }, nil +} diff --git a/api/v2/service/send_transaction.go b/api/v2/service/send_transaction.go index 8c6397427..d416f8f3b 100644 --- a/api/v2/service/send_transaction.go +++ b/api/v2/service/send_transaction.go @@ -24,21 +24,20 @@ func (s *Service) SendTransaction(ctx context.Context, req *pb.SendTransactionRe return nil, status.Error(codes.InvalidArgument, err.Error()) } - result, statusErr := s.broadcastTxSync(decodeString, ctx /*timeout*/) + result, statusErr := s.broadcastTxSync(ctx, decodeString) if statusErr != nil { return nil, statusErr.Err() } - switch result.Code { - case code.OK: - return &pb.SendTransactionResponse{ - Code: uint64(result.Code), - Log: result.Log, - Hash: "Mt" + strings.ToLower(result.Hash.String()), - }, nil - default: + if result.Code != code.OK { return nil, s.createError(status.New(codes.InvalidArgument, result.Log), result.Info) } + + return &pb.SendTransactionResponse{ + Code: uint64(result.Code), + Log: result.Log, + Hash: "Mt" + strings.ToLower(result.Hash.String()), + }, nil } type ResultBroadcastTx struct { @@ -49,7 +48,7 @@ type ResultBroadcastTx struct { Hash bytes.HexBytes `json:"hash"` } -func (s *Service) broadcastTxSync(tx types.Tx, ctx context.Context) (*ResultBroadcastTx, *status.Status) { +func (s *Service) broadcastTxSync(ctx context.Context, tx types.Tx) (*ResultBroadcastTx, *status.Status) { resCh := make(chan *abci.Response, 1) err := s.tmNode.Mempool().CheckTx(tx, func(res *abci.Response) { resCh <- res @@ -72,10 +71,6 @@ func (s *Service) broadcastTxSync(tx types.Tx, ctx context.Context) (*ResultBroa Hash: tx.Hash(), }, nil case <-ctx.Done(): - if ctx.Err() != context.DeadlineExceeded { - return nil, status.New(codes.Canceled, ctx.Err().Error()) - } - return nil, status.New(codes.DeadlineExceeded, ctx.Err().Error()) + return nil, status.FromContextError(ctx.Err()) } - } diff --git a/api/v2/service/service.go b/api/v2/service/service.go index cf32ea995..c6747a023 100644 --- a/api/v2/service/service.go +++ b/api/v2/service/service.go @@ -5,7 +5,6 @@ import ( "github.com/MinterTeam/minter-go-node/config" "github.com/MinterTeam/minter-go-node/core/minter" "github.com/MinterTeam/node-grpc-gateway/api_pb" - "github.com/tendermint/go-amino" tmNode "github.com/tendermint/tendermint/node" rpc "github.com/tendermint/tendermint/rpc/client/local" "google.golang.org/grpc/grpclog" @@ -16,7 +15,6 @@ import ( // Service is gRPC implementation ApiServiceServer type Service struct { - cdc *amino.Codec blockchain *minter.Blockchain client *rpc.Local tmNode *tmNode.Node @@ -26,9 +24,8 @@ type Service struct { } // NewService create gRPC server implementation -func NewService(cdc *amino.Codec, blockchain *minter.Blockchain, client *rpc.Local, node *tmNode.Node, minterCfg *config.Config, version string) *Service { +func NewService(blockchain *minter.Blockchain, client *rpc.Local, node *tmNode.Node, minterCfg *config.Config, version string) *Service { return &Service{ - cdc: cdc, blockchain: blockchain, client: client, minterCfg: minterCfg, diff --git a/api/v2/service/status.go b/api/v2/service/status.go index 98e967d72..be39a2b5c 100644 --- a/api/v2/service/status.go +++ b/api/v2/service/status.go @@ -11,15 +11,13 @@ import ( ) // Status returns current min gas price. -func (s *Service) Status(context.Context, *empty.Empty) (*pb.StatusResponse, error) { - result, err := s.client.Status() +func (s *Service) Status(ctx context.Context, _ *empty.Empty) (*pb.StatusResponse, error) { + result, err := s.client.Status(ctx) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } cState := s.blockchain.CurrentState() - cState.RLock() - defer cState.RUnlock() return &pb.StatusResponse{ Version: s.version, @@ -31,7 +29,7 @@ func (s *Service) Status(context.Context, *empty.Empty) (*pb.StatusResponse, err KeepLastStates: uint64(s.minterCfg.BaseConfig.KeepLastStates), TotalSlashed: cState.App().GetTotalSlashed().String(), CatchingUp: result.SyncInfo.CatchingUp, - PublicKey: fmt.Sprintf("Mp%x", result.ValidatorInfo.PubKey.Bytes()[5:]), + PublicKey: fmt.Sprintf("Mp%x", result.ValidatorInfo.PubKey.Bytes()[:]), NodeId: string(result.NodeInfo.ID()), }, nil } diff --git a/api/v2/service/ws.go b/api/v2/service/subscribe.go similarity index 89% rename from api/v2/service/ws.go rename to api/v2/service/subscribe.go index 5afc23d83..e70021654 100644 --- a/api/v2/service/ws.go +++ b/api/v2/service/subscribe.go @@ -4,7 +4,6 @@ import ( "context" "fmt" pb "github.com/MinterTeam/node-grpc-gateway/api_pb" - "github.com/google/uuid" core_types "github.com/tendermint/tendermint/rpc/core/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/peer" @@ -28,17 +27,14 @@ func (s *Service) Subscribe(request *pb.SubscribeRequest, stream pb.ApiService_S ctx, cancel := context.WithTimeout(stream.Context(), subscribeTimeout) defer cancel() - remote := uuid.New().String() - subscriber, ok := peer.FromContext(ctx) - if ok { - remote = subscriber.Addr.String() - } + subscriber, _ := peer.FromContext(ctx) + remote := subscriber.Addr.String() sub, err := s.client.Subscribe(ctx, remote, request.Query) if err != nil { return status.Error(codes.InvalidArgument, err.Error()) } defer func() { - if err := s.client.UnsubscribeAll(context.Background(), remote); err != nil { + if err := s.client.Unsubscribe(context.Background(), remote, request.Query); err != nil { s.client.Logger.Error(err.Error()) } }() diff --git a/api/v2/service/swap.go b/api/v2/service/swap.go new file mode 100644 index 000000000..8efac9f3e --- /dev/null +++ b/api/v2/service/swap.go @@ -0,0 +1,71 @@ +package service + +import ( + "context" + "encoding/hex" + "github.com/MinterTeam/minter-go-node/core/transaction" + "github.com/MinterTeam/minter-go-node/core/types" + pb "github.com/MinterTeam/node-grpc-gateway/api_pb" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "strings" +) + +func (s *Service) SwapPool(_ context.Context, req *pb.SwapPoolRequest) (*pb.SwapPoolResponse, error) { + if req.Coin0 == req.Coin1 { + return nil, status.Error(codes.InvalidArgument, "equal coins id") + } + + cState, err := s.blockchain.GetStateForHeight(req.Height) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + + reserve0, reserve1, liquidityID := cState.Swap().SwapPool(types.CoinID(req.Coin0), types.CoinID(req.Coin1)) + if liquidityID == 0 { + return nil, status.Error(codes.NotFound, "pair not found") + } + + return &pb.SwapPoolResponse{ + Amount0: reserve0.String(), + Amount1: reserve1.String(), + Liquidity: cState.Coins().GetCoinBySymbol(transaction.LiquidityCoinSymbol(liquidityID), 0).Volume().String(), + }, nil +} + +func (s *Service) SwapPoolProvider(_ context.Context, req *pb.SwapPoolProviderRequest) (*pb.SwapPoolResponse, error) { + if req.Coin0 == req.Coin1 { + return nil, status.Error(codes.InvalidArgument, "equal coins id") + } + + if !strings.HasPrefix(strings.Title(req.Provider), "Mx") { + return nil, status.Error(codes.InvalidArgument, "invalid address") + } + + decodeString, err := hex.DecodeString(req.Provider[2:]) + if err != nil { + return nil, status.Error(codes.InvalidArgument, "invalid address") + } + address := types.BytesToAddress(decodeString) + + cState, err := s.blockchain.GetStateForHeight(req.Height) + if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + + swapper := cState.Swap().GetSwapper(types.CoinID(req.Coin0), types.CoinID(req.Coin1)) + liquidityID := swapper.CoinID() + if liquidityID == 0 { + return nil, status.Error(codes.NotFound, "pair from provider not found") + } + + liquidityCoin := cState.Coins().GetCoinBySymbol(transaction.LiquidityCoinSymbol(liquidityID), 0) + balance := cState.Accounts().GetBalance(address, liquidityCoin.ID()) + + amount0, amount1 := swapper.Amounts(balance, liquidityCoin.Volume()) + return &pb.SwapPoolResponse{ + Amount0: amount0.String(), + Amount1: amount1.String(), + Liquidity: balance.String(), + }, nil +} diff --git a/api/v2/service/test_block.go b/api/v2/service/test_block.go index 8f24f721d..012a1d2bb 100644 --- a/api/v2/service/test_block.go +++ b/api/v2/service/test_block.go @@ -235,10 +235,12 @@ func (s *Service) TestBlock(context.Context, *empty.Empty) (*pb.BlockResponse, e return nil, status.Error(codes.Internal, err.Error()) } - transactions := []*pb.BlockResponse_Transaction{ + transactions := []*pb.TransactionResponse{ { Hash: "Mt88d02883c07a1dfc5ff5b2d27eacfd0d82706ba113f2e77a42a1a3d3c369c249", RawTx: "f8700102018001a0df0194a83d8ebbe688b853775a698683b77afa305a661e880de0b6b3a7640000808001b845f8431ca0acde2fa28bf063bffb14f667a2219b641205bd67a5bc6b664cbd44a62504c897a03649ae0ae4881426a95ccc80866c259032f22811777777c7327e9b99f766ad00", + Height: 123, + Index: 1, From: "Mx0c5d5f646556d663e1eaf87150d987b9f2b858b6", Nonce: 1, GasPrice: 1, diff --git a/api/v2/service/transaction.go b/api/v2/service/transaction.go index e88a76b2f..a4b26a715 100644 --- a/api/v2/service/transaction.go +++ b/api/v2/service/transaction.go @@ -8,6 +8,7 @@ import ( pb "github.com/MinterTeam/node-grpc-gateway/api_pb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "strconv" "strings" ) @@ -21,24 +22,27 @@ func (s *Service) Transaction(ctx context.Context, req *pb.TransactionRequest) ( return nil, status.Error(codes.InvalidArgument, err.Error()) } - tx, err := s.client.Tx(decodeString, false) + tx, err := s.client.Tx(ctx, decodeString, false) if err != nil { return nil, status.Error(codes.FailedPrecondition, err.Error()) } - decodedTx, _ := transaction.TxDecoder.DecodeFromBytes(tx.Tx) + decodedTx, _ := transaction.DecodeFromBytes(tx.Tx) sender, _ := decodedTx.Sender() tags := make(map[string]string) + var gas int for _, tag := range tx.TxResult.Events[0].Attributes { - tags[string(tag.Key)] = string(tag.Value) + key := string(tag.Key) + value := string(tag.Value) + tags[key] = value + if key == "tx.gas" { + gas, _ = strconv.Atoi(value) + } } cState := s.blockchain.CurrentState() - cState.RLock() - defer cState.RUnlock() - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { return nil, timeoutStatus.Err() } @@ -60,12 +64,13 @@ func (s *Service) Transaction(ctx context.Context, req *pb.TransactionRequest) ( Id: uint64(decodedTx.GasCoin), Symbol: cState.Coins().GetCoin(decodedTx.GasCoin).GetFullSymbol(), }, - Gas: uint64(decodedTx.Gas()), - Type: uint64(decodedTx.Type), - Data: dataStruct, - Payload: decodedTx.Payload, - Tags: tags, - Code: uint64(tx.TxResult.Code), - Log: tx.TxResult.Log, + Gas: uint64(gas), + Type: uint64(decodedTx.Type), + Data: dataStruct, + Payload: decodedTx.Payload, + ServiceData: decodedTx.ServiceData, + Tags: tags, + Code: uint64(tx.TxResult.Code), + Log: tx.TxResult.Log, }, nil } diff --git a/api/v2/service/transactions.go b/api/v2/service/transactions.go index bfe5cbd5d..d0c1db1d4 100644 --- a/api/v2/service/transactions.go +++ b/api/v2/service/transactions.go @@ -8,12 +8,15 @@ import ( pb "github.com/MinterTeam/node-grpc-gateway/api_pb" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "strconv" "strings" ) // Transactions return transactions by query. func (s *Service) Transactions(ctx context.Context, req *pb.TransactionsRequest) (*pb.TransactionsResponse, error) { - rpcResult, err := s.client.TxSearch(req.Query, false, int(req.Page), int(req.PerPage), "desc") + page := int(req.Page) + perPage := int(req.PerPage) + rpcResult, err := s.client.TxSearch(ctx, req.Query, false, &page, &perPage, "desc") if err != nil { return nil, status.Error(codes.FailedPrecondition, err.Error()) } @@ -23,8 +26,6 @@ func (s *Service) Transactions(ctx context.Context, req *pb.TransactionsRequest) if lenTx != 0 { cState := s.blockchain.CurrentState() - cState.RLock() - defer cState.RUnlock() for _, tx := range rpcResult.Txs { @@ -32,12 +33,18 @@ func (s *Service) Transactions(ctx context.Context, req *pb.TransactionsRequest) return nil, timeoutStatus.Err() } - decodedTx, _ := transaction.TxDecoder.DecodeFromBytes(tx.Tx) + decodedTx, _ := transaction.DecodeFromBytes(tx.Tx) sender, _ := decodedTx.Sender() tags := make(map[string]string) + var gas int for _, tag := range tx.TxResult.Events[0].Attributes { - tags[string(tag.Key)] = string(tag.Value) + key := string(tag.Key) + value := string(tag.Value) + tags[key] = value + if key == "tx.gas" { + gas, _ = strconv.Atoi(value) + } } data, err := encode(decodedTx.GetDecodedData(), cState.Coins()) @@ -57,8 +64,8 @@ func (s *Service) Transactions(ctx context.Context, req *pb.TransactionsRequest) Id: uint64(decodedTx.GasCoin), Symbol: cState.Coins().GetCoin(decodedTx.GasCoin).GetFullSymbol(), }, - Gas: uint64(decodedTx.Gas()), - Type: uint64(uint8(decodedTx.Type)), + Gas: uint64(gas), + Type: uint64(decodedTx.Type), Data: data, Payload: decodedTx.Payload, Tags: tags, diff --git a/api/v2/service/unconfirmed_txs.go b/api/v2/service/unconfirmed_txs.go index 7bf9778e5..9b80e9ed7 100644 --- a/api/v2/service/unconfirmed_txs.go +++ b/api/v2/service/unconfirmed_txs.go @@ -9,7 +9,8 @@ import ( // UnconfirmedTxs returns unconfirmed transactions. func (s *Service) UnconfirmedTxs(ctx context.Context, req *pb.UnconfirmedTxsRequest) (*pb.UnconfirmedTxsResponse, error) { - txs, err := s.client.UnconfirmedTxs(int(req.Limit)) + limit := int(req.Limit) + txs, err := s.client.UnconfirmedTxs(ctx, &limit) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } diff --git a/api/v2/service/validators.go b/api/v2/service/validators.go index 7c7334b80..0c59e58ff 100644 --- a/api/v2/service/validators.go +++ b/api/v2/service/validators.go @@ -14,8 +14,9 @@ func (s *Service) Validators(ctx context.Context, req *pb.ValidatorsRequest) (*p if height == 0 { height = int64(s.blockchain.Height()) } - - tmVals, err := s.client.Validators(&height, 1, 100) + var page = 1 + var perPage = 100 + tmVals, err := s.client.Validators(ctx, &height, &page, &perPage) if err != nil { return nil, status.Error(codes.Internal, err.Error()) } @@ -27,7 +28,7 @@ func (s *Service) Validators(ctx context.Context, req *pb.ValidatorsRequest) (*p responseValidators := make([]*pb.ValidatorsResponse_Result, 0, len(tmVals.Validators)) for _, val := range tmVals.Validators { var pk types.Pubkey - copy(pk[:], val.PubKey.Bytes()[5:]) + copy(pk[:], val.PubKey.Bytes()[:]) responseValidators = append(responseValidators, &pb.ValidatorsResponse_Result{ PublicKey: pk.String(), VotingPower: uint64(val.VotingPower), diff --git a/api/v2/service/waitlist.go b/api/v2/service/waitlist.go index d94361f27..d585a6e9f 100644 --- a/api/v2/service/waitlist.go +++ b/api/v2/service/waitlist.go @@ -30,14 +30,9 @@ func (s *Service) WaitList(ctx context.Context, req *pb.WaitListRequest) (*pb.Wa } if req.Height != 0 { - cState.Lock() cState.Candidates().LoadCandidates() - cState.Unlock() } - cState.RLock() - defer cState.RUnlock() - if timeoutStatus := s.checkTimeout(ctx); timeoutStatus != nil { return nil, timeoutStatus.Err() } diff --git a/api/validators.go b/api/validators.go deleted file mode 100644 index b370fc082..000000000 --- a/api/validators.go +++ /dev/null @@ -1,36 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/types" -) - -type ValidatorResponse struct { - Pubkey string `json:"pub_key"` - VotingPower int64 `json:"voting_power"` -} - -type ResponseValidators []ValidatorResponse - -func Validators(height uint64) (*ResponseValidators, error) { - if height == 0 { - height = blockchain.Height() - } - - h := int64(height) - tmVals, err := client.Validators(&h, 1, 100) - if err != nil { - return nil, err - } - - responseValidators := make(ResponseValidators, len(tmVals.Validators)) - for i, val := range tmVals.Validators { - var pk types.Pubkey - copy(pk[:], val.PubKey.Bytes()[5:]) - responseValidators[i] = ValidatorResponse{ - Pubkey: pk.String(), - VotingPower: val.VotingPower, - } - } - - return &responseValidators, nil -} diff --git a/api/waitlist.go b/api/waitlist.go deleted file mode 100644 index 0ebb6accb..000000000 --- a/api/waitlist.go +++ /dev/null @@ -1,39 +0,0 @@ -package api - -import ( - "github.com/MinterTeam/minter-go-node/core/types" -) - -type WaitlistResponse struct { - List []*Wait `json:"list"` -} - -type Wait struct { - Coin Coin `json:"coin"` - Value string `json:"value"` -} - -func Waitlist(pubkey types.Pubkey, address types.Address, height int) (*WaitlistResponse, error) { - cState, err := GetStateForHeight(height) - if err != nil { - return nil, err - } - - cState.RLock() - defer cState.RUnlock() - - response := new(WaitlistResponse) - items := cState.WaitList().GetByAddressAndPubKey(address, pubkey) - response.List = make([]*Wait, 0, len(items)) - for _, item := range items { - response.List = append(response.List, &Wait{ - Coin: Coin{ - ID: item.Coin.Uint32(), - Symbol: cState.Coins().GetCoin(item.Coin).GetFullSymbol(), - }, - Value: item.Value.String(), - }) - } - - return response, nil -} diff --git a/cli/README.md b/cli/README.md index 2e48e964d..0c58166fa 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,26 +1,31 @@ # Node Command Line Interface ## Manager + non-interactive mode + ```sh $ ./node manager [command] [command options] ``` ## Console + interactive mode + ```sh $ ./node console >>> [command] [command options] ``` - ### Global Options + ```text --help, -h show help (default: false) --version, -v print the version (default: false) ``` ### Commands + ```text dial_peer, dp connect a new peer prune_blocks, pb delete block information @@ -31,7 +36,9 @@ help, h Shows a list of commands or help for one command ``` #### dial_peer + connect a new peer + ```text OPTIONS: --address value, -a value id@ip:port @@ -40,7 +47,9 @@ OPTIONS: ``` #### prune_blocks + delete block information + ```text OPTIONS: --from value, -f value (default: 0) @@ -50,7 +59,9 @@ OPTIONS: ``` #### status + display the current status of the blockchain + ```text OPTIONS: --json, -j echo in json format (default: false) @@ -58,7 +69,9 @@ OPTIONS: ``` #### net_info + display network data + ````text OPTIONS: --json, -j echo in json format (default: false) @@ -66,4 +79,6 @@ OPTIONS: ```` #### Small talk -- Sergey Klimov ([@klim0v](https://github.com/klim0v)): [Workshops MDD Dec'19: Node Command Line Interface](http://minter.link/p3) + +- Sergey + Klimov ([@klim0v](https://github.com/klim0v)): [Workshops MDD Dec'19: Node Command Line Interface](http://minter.link/p3) diff --git a/cli/service/client.go b/cli/service/client.go index 1053fcea4..4f0478ad1 100644 --- a/cli/service/client.go +++ b/cli/service/client.go @@ -225,9 +225,9 @@ func dashboardCMD(client pb.ManagerServiceClient) func(c *cli.Context) error { ui.SetKeybinding("Esc", func() { ui.Quit() }) ui.SetKeybinding("Ctrl+C", func() { ui.Quit() }) ui.SetKeybinding("q", func() { ui.Quit() }) - errCh := make(chan error, 2) + errCh := make(chan error, 1) uiStart := make(chan struct{}) - go func() { uiStart <- struct{}{}; errCh <- ui.Run() }() + go func() { close(uiStart); errCh <- ui.Run() }() <-uiStart defer ui.Quit() var dashboardFunc func(recv *pb.DashboardResponse) @@ -419,8 +419,8 @@ func pruneBlocksCMD(client pb.ManagerServiceClient) func(c *cli.Context) error { } now := time.Now() - errCh := make(chan error) - recvCh := make(chan *pb.PruneBlocksResponse) + errCh := make(chan error, 1) + recvCh := make(chan *pb.PruneBlocksResponse, 1) go func() { for { diff --git a/cli/service/service.go b/cli/service/service.go index 1cb5d7eb1..8bcee0e91 100644 --- a/cli/service/service.go +++ b/cli/service/service.go @@ -60,21 +60,20 @@ func (m *managerServer) Dashboard(_ *empty.Empty, stream pb.ManagerService_Dashb protoTime, _ := ptypes.TimestampProto(info.HeaderTimestamp) var mem runtime.MemStats runtime.ReadMemStats(&mem) - resultStatus, err := m.tmRPC.Status() + resultStatus, err := m.tmRPC.Status(context.Background()) if err != nil { return status.Error(codes.Internal, err.Error()) } - netInfo, err := m.tmRPC.NetInfo() + netInfo, err := m.tmRPC.NetInfo(context.Background()) if err != nil { return status.Error(codes.Internal, err.Error()) } var missedBlocks string var stake string - pubkey := types.BytesToPubkey(resultStatus.ValidatorInfo.PubKey.Bytes()[5:]) + pubkey := types.BytesToPubkey(resultStatus.ValidatorInfo.PubKey.Bytes()[:]) var pbValidatorStatus pb.DashboardResponse_ValidatorStatus cState := m.blockchain.CurrentState() - cState.RLock() candidate := cState.Candidates().GetCandidate(pubkey) if candidate == nil { pbValidatorStatus = pb.DashboardResponse_NotDeclared @@ -88,14 +87,13 @@ func (m *managerServer) Dashboard(_ *empty.Empty, stream pb.ManagerService_Dashb if validator != nil { missedBlocks = validator.AbsentTimes.String() var address types.TmAddress - copy(address[:], ed25519.PubKeyEd25519(pubkey).Address().Bytes()) + copy(address[:], ed25519.PubKey(pubkey[:]).Address().Bytes()) if m.blockchain.GetValidatorStatus(address) == minter.ValidatorPresent { pbValidatorStatus = pb.DashboardResponse_Validating } } } } - cState.RUnlock() if err := stream.Send(&pb.DashboardResponse{ LatestHeight: info.Height, @@ -119,7 +117,7 @@ func (m *managerServer) Dashboard(_ *empty.Empty, stream pb.ManagerService_Dashb } func (m *managerServer) Status(context.Context, *empty.Empty) (*pb.StatusResponse, error) { - result, err := m.tmRPC.Status() + result, err := m.tmRPC.Status(context.Background()) if err != nil { return new(pb.StatusResponse), status.Error(codes.Internal, err.Error()) } @@ -132,7 +130,7 @@ func (m *managerServer) Status(context.Context, *empty.Empty) (*pb.StatusRespons LatestBlockTime: result.SyncInfo.LatestBlockTime.Format(time.RFC3339Nano), KeepLastStates: fmt.Sprintf("%d", m.cfg.BaseConfig.KeepLastStates), CatchingUp: result.SyncInfo.CatchingUp, - PublicKey: fmt.Sprintf("Mp%x", result.ValidatorInfo.PubKey.Bytes()[5:]), + PublicKey: fmt.Sprintf("Mp%x", result.ValidatorInfo.PubKey.Bytes()[:]), NodeId: string(result.NodeInfo.ID()), } @@ -140,7 +138,7 @@ func (m *managerServer) Status(context.Context, *empty.Empty) (*pb.StatusRespons } func (m *managerServer) NetInfo(context.Context, *empty.Empty) (*pb.NetInfoResponse, error) { - resultNetInfo, err := m.tmRPC.NetInfo() + resultNetInfo, err := m.tmRPC.NetInfo(context.Background()) if err != nil { return new(pb.NetInfoResponse), status.Error(codes.Internal, err.Error()) } @@ -231,7 +229,7 @@ func (m *managerServer) NetInfo(context.Context, *empty.Empty) (*pb.NetInfoRespo } func (m *managerServer) AvailableVersions(context.Context, *empty.Empty) (*pb.AvailableVersionsResponse, error) { - versions := m.blockchain.CurrentState().Tree().AvailableVersions() + versions := m.blockchain.AvailableVersions() intervals := map[int]int{} var fromVersion int64 for i := 0; i < len(versions); i++ { @@ -300,7 +298,7 @@ func (m *managerServer) PruneBlocks(req *pb.PruneBlocksRequest, stream pb.Manage func (m *managerServer) DealPeer(_ context.Context, req *pb.DealPeerRequest) (*empty.Empty, error) { res := new(empty.Empty) - _, err := m.tmRPC.DialPeers([]string{req.Address}, req.Persistent) + _, err := m.tmRPC.DialPeers(context.Background(), []string{req.Address}, req.Persistent, false, false) // todo if err != nil { return res, status.Error(codes.FailedPrecondition, err.Error()) } diff --git a/cmd/minter/cmd/export.go b/cmd/minter/cmd/export.go index 2befc9d20..8c162de16 100644 --- a/cmd/minter/cmd/export.go +++ b/cmd/minter/cmd/export.go @@ -8,8 +8,8 @@ import ( "github.com/MinterTeam/minter-go-node/core/state" "github.com/spf13/cobra" "github.com/tendermint/go-amino" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" "github.com/tendermint/tendermint/types" - db "github.com/tendermint/tm-db" "io" "log" "os" @@ -63,7 +63,7 @@ func export(cmd *cobra.Command, args []string) error { fmt.Println("Start exporting...") - ldb, err := db.NewGoLevelDB("state", utils.GetMinterHome()+"/data") + ldb, err := utils.NewStorage("", "").InitStateLevelDB("state", nil) if err != nil { log.Panicf("Cannot load db: %s", err) } @@ -73,11 +73,12 @@ func export(cmd *cobra.Command, args []string) error { log.Panicf("Cannot new state at given height: %s", err) } - exportTimeStart, newState := time.Now(), currentState.Export(height) + exportTimeStart, newState := time.Now(), currentState.Export() fmt.Printf("State has been exported. Took %s", time.Since(exportTimeStart)) + initialHeight := height if startHeight > 0 { - newState.StartHeight = startHeight + initialHeight = startHeight } var jsonBytes []byte @@ -94,19 +95,20 @@ func export(cmd *cobra.Command, args []string) error { // compose genesis genesis := types.GenesisDoc{ - GenesisTime: time.Unix(0, 0).Add(genesisTime), - ChainID: chainID, - ConsensusParams: &types.ConsensusParams{ - Block: types.BlockParams{ + GenesisTime: time.Unix(0, 0).Add(genesisTime), + InitialHeight: int64(initialHeight), + ChainID: chainID, + ConsensusParams: &tmproto.ConsensusParams{ + Block: tmproto.BlockParams{ MaxBytes: blockMaxBytes, MaxGas: blockMaxGas, TimeIotaMs: blockTimeIotaMs, }, - Evidence: types.EvidenceParams{ + Evidence: tmproto.EvidenceParams{ MaxAgeNumBlocks: evidenceMaxAgeNumBlocks, MaxAgeDuration: evidenceMaxAgeDuration, }, - Validator: types.ValidatorParams{ + Validator: tmproto.ValidatorParams{ PubKeyTypes: []string{ types.ABCIPubKeyTypeEd25519, }, diff --git a/cmd/minter/cmd/manager.go b/cmd/minter/cmd/manager.go index 47b8fae27..9ed9c7dc9 100644 --- a/cmd/minter/cmd/manager.go +++ b/cmd/minter/cmd/manager.go @@ -15,7 +15,15 @@ var ManagerCommand = &cobra.Command{ DisableFlagParsing: true, RunE: func(cmd *cobra.Command, args []string) error { newArgs := setParentFlags(cmd, args) - console, err := service.NewCLI(utils.GetMinterHome() + "/manager.sock") + homeDir, err := cmd.Flags().GetString("home-dir") + if err != nil { + return err + } + configDir, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + console, err := service.NewCLI(utils.NewStorage(homeDir, configDir).GetMinterHome() + "/manager.sock") if err != nil { return nil } @@ -36,7 +44,15 @@ var ManagerConsole = &cobra.Command{ DisableFlagParsing: true, RunE: func(cmd *cobra.Command, args []string) error { _ = setParentFlags(cmd, args) - console, err := service.NewCLI(utils.GetMinterHome() + "/manager.sock") + homeDir, err := cmd.Flags().GetString("home-dir") + if err != nil { + return err + } + configDir, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + console, err := service.NewCLI(utils.NewStorage(homeDir, configDir).GetMinterHome() + "/manager.sock") if err != nil { return nil } diff --git a/cmd/minter/cmd/node.go b/cmd/minter/cmd/node.go index 9847f7a0f..ba1df0544 100644 --- a/cmd/minter/cmd/node.go +++ b/cmd/minter/cmd/node.go @@ -1,27 +1,20 @@ package cmd import ( + "context" "fmt" - apiV1 "github.com/MinterTeam/minter-go-node/api" apiV2 "github.com/MinterTeam/minter-go-node/api/v2" serviceApi "github.com/MinterTeam/minter-go-node/api/v2/service" "github.com/MinterTeam/minter-go-node/cli/service" "github.com/MinterTeam/minter-go-node/cmd/utils" "github.com/MinterTeam/minter-go-node/config" - eventsdb "github.com/MinterTeam/minter-go-node/core/events" "github.com/MinterTeam/minter-go-node/core/minter" "github.com/MinterTeam/minter-go-node/core/statistics" "github.com/MinterTeam/minter-go-node/log" "github.com/MinterTeam/minter-go-node/version" "github.com/spf13/cobra" - "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/abci/types" tmCfg "github.com/tendermint/tendermint/config" - "github.com/tendermint/tendermint/crypto" - "github.com/tendermint/tendermint/crypto/ed25519" - "github.com/tendermint/tendermint/crypto/multisig" - "github.com/tendermint/tendermint/crypto/secp256k1" - "github.com/tendermint/tendermint/evidence" tmLog "github.com/tendermint/tendermint/libs/log" tmOS "github.com/tendermint/tendermint/libs/os" tmNode "github.com/tendermint/tendermint/node" @@ -29,7 +22,6 @@ import ( "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" rpc "github.com/tendermint/tendermint/rpc/client/local" - "github.com/tendermint/tendermint/store" tmTypes "github.com/tendermint/tendermint/types" "io" "net/http" @@ -56,8 +48,18 @@ func runNode(cmd *cobra.Command) error { panic(err) } + homeDir, err := cmd.Flags().GetString("home-dir") + if err != nil { + return err + } + configDir, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + storages := utils.NewStorage(homeDir, configDir) + // ensure /config and /tmdata dirs - if err := ensureDirs(); err != nil { + if err := ensureDirs(storages.GetMinterHome()); err != nil { return err } @@ -74,13 +76,23 @@ func runNode(cmd *cobra.Command) error { tmConfig := config.GetTmConfig(cfg) - app := minter.NewMinterBlockchain(cfg) + if !cfg.ValidatorMode { + _, err = storages.InitEventLevelDB("data/events", minter.GetDbOpts(1024)) + if err != nil { + return err + } + } + _, err = storages.InitStateLevelDB("data/state", minter.GetDbOpts(cfg.StateMemAvailable)) + if err != nil { + return err + } + app := minter.NewMinterBlockchain(storages, cfg, cmd.Context()) // update BlocksTimeDelta in case it was corrupted - updateBlocksTimeDelta(app, tmConfig) + // updateBlocksTimeDelta(app, tmConfig) // start TM node - node := startTendermintNode(app, tmConfig, logger) + node := startTendermintNode(app, tmConfig, logger, storages.GetMinterHome()) client := rpc.New(node) app.SetTmNode(node) @@ -88,62 +100,25 @@ func runNode(cmd *cobra.Command) error { runAPI(logger, app, client, node) } - runCLI(cmd, app, client, node) + runCLI(cmd.Context(), app, client, node, storages.GetMinterHome()) if cfg.Instrumentation.Prometheus { go app.SetStatisticData(statistics.New()).Statistic(cmd.Context()) } - <-cmd.Context().Done() - - defer app.Stop() - if err := node.Stop(); err != nil { - return err - } - - return nil + return app.WaitStop() } -func runCLI(cmd *cobra.Command, app *minter.Blockchain, client *rpc.Local, tmNode *tmNode.Node) { +func runCLI(ctx context.Context, app *minter.Blockchain, client *rpc.Local, tmNode *tmNode.Node, home string) { go func() { - err := service.StartCLIServer(utils.GetMinterHome()+"/manager.sock", service.NewManager(app, client, tmNode, cfg), cmd.Context()) + err := service.StartCLIServer(home+"/manager.sock", service.NewManager(app, client, tmNode, cfg), ctx) if err != nil { panic(err) } }() } -// RegisterAmino registers all crypto related types in the given (amino) codec. -func registerCryptoAmino(cdc *amino.Codec) { - // These are all written here instead of - cdc.RegisterInterface((*crypto.PubKey)(nil), nil) - cdc.RegisterConcrete(ed25519.PubKeyEd25519{}, - ed25519.PubKeyAminoName, nil) - cdc.RegisterConcrete(secp256k1.PubKeySecp256k1{}, - secp256k1.PubKeyAminoName, nil) - cdc.RegisterConcrete(multisig.PubKeyMultisigThreshold{}, - multisig.PubKeyMultisigThresholdAminoRoute, nil) - - cdc.RegisterInterface((*crypto.PrivKey)(nil), nil) - cdc.RegisterConcrete(ed25519.PrivKeyEd25519{}, - ed25519.PrivKeyAminoName, nil) - cdc.RegisterConcrete(secp256k1.PrivKeySecp256k1{}, - secp256k1.PrivKeyAminoName, nil) -} - -func registerEvidenceMessages(cdc *amino.Codec) { - cdc.RegisterInterface((*evidence.Message)(nil), nil) - cdc.RegisterConcrete(&evidence.ListMessage{}, - "tendermint/evidence/EvidenceListMessage", nil) - cdc.RegisterInterface((*tmTypes.Evidence)(nil), nil) - cdc.RegisterConcrete(&tmTypes.DuplicateVoteEvidence{}, "tendermint/DuplicateVoteEvidence", nil) -} - func runAPI(logger tmLog.Logger, app *minter.Blockchain, client *rpc.Local, node *tmNode.Node) { - cdc := amino.NewCodec() - registerCryptoAmino(cdc) - eventsdb.RegisterAminoEvents(cdc) - registerEvidenceMessages(cdc) go func(srv *serviceApi.Service) { grpcURL, err := url.Parse(cfg.GRPCListenAddress) if err != nil { @@ -155,9 +130,7 @@ func runAPI(logger tmLog.Logger, app *minter.Blockchain, client *rpc.Local, node } logger.Error("Failed to start Api V2 in both gRPC and RESTful", apiV2.Run(srv, grpcURL.Host, apiV2url.Host, logger.With("module", "rpc"))) - }(serviceApi.NewService(cdc, app, client, node, cfg, version.Version)) - - go apiV1.RunAPI(cdc, app, client, cfg, logger) + }(serviceApi.NewService(app, client, node, cfg, version.Version)) } func enablePprof(cmd *cobra.Command, logger tmLog.Logger) error { @@ -177,12 +150,12 @@ func enablePprof(cmd *cobra.Command, logger tmLog.Logger) error { return nil } -func ensureDirs() error { - if err := tmOS.EnsureDir(utils.GetMinterHome()+"/config", 0777); err != nil { +func ensureDirs(homeDir string) error { + if err := tmOS.EnsureDir(homeDir+"/config", 0777); err != nil { return err } - if err := tmOS.EnsureDir(utils.GetMinterHome()+"/tmdata", 0777); err != nil { + if err := tmOS.EnsureDir(homeDir+"/tmdata", 0777); err != nil { return err } @@ -210,26 +183,7 @@ func checkRlimits() error { return nil } -func updateBlocksTimeDelta(app *minter.Blockchain, config *tmCfg.Config) { - blockStoreDB, err := tmNode.DefaultDBProvider(&tmNode.DBContext{ID: "blockstore", Config: config}) - if err != nil { - panic(err) - } - - blockStore := store.NewBlockStore(blockStoreDB) - height := uint64(blockStore.Height()) - count := uint64(3) - if _, err := app.GetBlocksTimeDelta(height, count); height >= 20 && err != nil { - blockA := blockStore.LoadBlockMeta(int64(height - count - 1)) - blockB := blockStore.LoadBlockMeta(int64(height - 1)) - - delta := int(blockB.Header.Time.Sub(blockA.Header.Time).Seconds()) - app.SetBlocksTimeDelta(height, delta) - } - blockStoreDB.Close() -} - -func startTendermintNode(app types.Application, cfg *tmCfg.Config, logger tmLog.Logger) *tmNode.Node { +func startTendermintNode(app types.Application, cfg *tmCfg.Config, logger tmLog.Logger, home string) *tmNode.Node { nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) if err != nil { panic(err) @@ -240,7 +194,7 @@ func startTendermintNode(app types.Application, cfg *tmCfg.Config, logger tmLog. privval.LoadOrGenFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()), nodeKey, proxy.NewLocalClientCreator(app), - getGenesis, + getGenesis(home+"/config/genesis.json"), tmNode.DefaultDBProvider, tmNode.DefaultMetricsProvider(cfg.Instrumentation), logger.With("module", "tendermint"), @@ -261,24 +215,32 @@ func startTendermintNode(app types.Application, cfg *tmCfg.Config, logger tmLog. return node } -func getGenesis() (doc *tmTypes.GenesisDoc, e error) { - genDocFile := utils.GetMinterHome() + "/config/genesis.json" - _, err := os.Stat(genDocFile) - if err != nil { - if !os.IsNotExist(err) { - return nil, err +func getGenesis(genDocFile string) func() (doc *tmTypes.GenesisDoc, e error) { + return func() (doc *tmTypes.GenesisDoc, e error) { + _, err := os.Stat(genDocFile) + if err != nil { + if !os.IsNotExist(err) { + return nil, err + } + + genesis, err := RootCmd.Flags().GetString("genesis") + if err != nil { + return nil, err + } + + if err := downloadFile(genDocFile, genesis); err != nil { + return nil, err + } } - - genesis, err := RootCmd.Flags().GetString("genesis") + doc, err = tmTypes.GenesisDocFromFile(genDocFile) if err != nil { return nil, err } - - if err := downloadFile(genDocFile, genesis); err != nil { - return nil, err + if len(doc.AppHash) == 0 { + doc.AppHash = nil } + return doc, err } - return tmTypes.GenesisDocFromFile(genDocFile) } func downloadFile(filepath string, url string) error { diff --git a/cmd/minter/cmd/root.go b/cmd/minter/cmd/root.go index 4a75ea4c3..adc59dae8 100644 --- a/cmd/minter/cmd/root.go +++ b/cmd/minter/cmd/root.go @@ -16,8 +16,17 @@ var RootCmd = &cobra.Command{ Short: "Minter Go Node", PersistentPreRun: func(cmd *cobra.Command, args []string) { v := viper.New() - v.SetConfigFile(utils.GetMinterConfigPath()) - cfg = config.GetConfig() + homeDir, err := cmd.Flags().GetString("home-dir") + if err != nil { + panic(err) + } + configDir, err := cmd.Flags().GetString("config") + if err != nil { + panic(err) + } + storage := utils.NewStorage(homeDir, configDir) + v.SetConfigFile(storage.GetMinterConfigPath()) + cfg = config.GetConfig(storage.GetMinterHome()) if err := v.ReadInConfig(); err != nil { panic(err) diff --git a/cmd/minter/cmd/show_validator.go b/cmd/minter/cmd/show_validator.go index 0b2c4e3d8..ba507277b 100644 --- a/cmd/minter/cmd/show_validator.go +++ b/cmd/minter/cmd/show_validator.go @@ -4,8 +4,6 @@ import ( "fmt" "github.com/MinterTeam/minter-go-node/log" "github.com/spf13/cobra" - "github.com/tendermint/go-amino" - cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" tmos "github.com/tendermint/tendermint/libs/os" "github.com/tendermint/tendermint/privval" "os" @@ -18,9 +16,6 @@ var ShowValidator = &cobra.Command{ } func showValidator(cmd *cobra.Command, args []string) error { - cdc := amino.NewCodec() - cryptoAmino.RegisterAmino(cdc) - keyFilePath := cfg.PrivValidatorKeyFile() logger := log.NewLogger(cfg) if !tmos.FileExists(keyFilePath) { @@ -33,6 +28,6 @@ func showValidator(cmd *cobra.Command, args []string) error { if err != nil { panic(err) } - fmt.Printf("Mp%x\n", key.Bytes()[5:]) + fmt.Printf("Mp%x\n", key.Bytes()[:]) return nil } diff --git a/cmd/minter/cmd/verify_genesis.go b/cmd/minter/cmd/verify_genesis.go index 70a19a404..93a4ea104 100644 --- a/cmd/minter/cmd/verify_genesis.go +++ b/cmd/minter/cmd/verify_genesis.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + "github.com/MinterTeam/minter-go-node/cmd/utils" "github.com/MinterTeam/minter-go-node/core/types" "github.com/spf13/cobra" "github.com/tendermint/go-amino" @@ -14,7 +15,15 @@ var VerifyGenesis = &cobra.Command{ } func verifyGenesis(cmd *cobra.Command, args []string) error { - genesis, err := getGenesis() + homeDir, err := cmd.Flags().GetString("home-dir") + if err != nil { + return err + } + configDir, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + genesis, err := getGenesis(utils.NewStorage(homeDir, configDir).GetMinterHome() + "/config/genesis.json")() if err != nil { return err } diff --git a/cmd/minter/main.go b/cmd/minter/main.go index f01bde04e..1131ce4ab 100644 --- a/cmd/minter/main.go +++ b/cmd/minter/main.go @@ -3,10 +3,10 @@ package main import ( "context" "github.com/MinterTeam/minter-go-node/cmd/minter/cmd" - "github.com/MinterTeam/minter-go-node/cmd/utils" "github.com/tendermint/tendermint/libs/log" tmos "github.com/tendermint/tendermint/libs/os" "os" + "time" ) func main() { @@ -16,6 +16,7 @@ func main() { tmos.TrapSignal(log.NewTMLogger(os.Stdout).With("module", "consensus"), func() { cancel() + time.Sleep(time.Second * 10) }) rootCmd.AddCommand( @@ -29,8 +30,8 @@ func main() { cmd.ExportCommand, ) - rootCmd.PersistentFlags().StringVar(&utils.MinterHome, "home-dir", "", "base dir (default is $HOME/.minter)") - rootCmd.PersistentFlags().StringVar(&utils.MinterConfig, "config", "", "path to config (default is $(home-dir)/config/config.toml)") + rootCmd.PersistentFlags().String("home-dir", "", "base dir (default is $HOME/.minter)") + rootCmd.PersistentFlags().String("config", "", "path to config (default is $(home-dir)/config/config.toml)") rootCmd.PersistentFlags().Bool("testnet", false, "use \"true\" for testnet, mainnet is default") rootCmd.PersistentFlags().Bool("pprof", false, "enable pprof") rootCmd.PersistentFlags().String("pprof-addr", "0.0.0.0:6060", "pprof listen addr") diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index df9ec272d..e5f565d7d 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -1,33 +1,77 @@ package utils import ( + "github.com/syndtr/goleveldb/leveldb/opt" + db "github.com/tendermint/tm-db" "os" "path/filepath" ) -var ( - MinterHome string - MinterConfig string -) +type Storage struct { + minterHome string + minterConfig string + eventDB db.DB + stateDB db.DB +} + +func (s *Storage) SetMinterConfig(minterConfig string) { + s.minterConfig = minterConfig +} + +func (s *Storage) SetMinterHome(minterHome string) { + s.minterHome = minterHome +} + +func (s *Storage) EventDB() db.DB { + return s.eventDB +} + +func (s *Storage) StateDB() db.DB { + return s.stateDB +} + +func NewStorage(home string, config string) *Storage { + return &Storage{eventDB: db.NewMemDB(), stateDB: db.NewMemDB(), minterConfig: config, minterHome: home} +} + +func (s *Storage) InitEventLevelDB(name string, opts *opt.Options) (db.DB, error) { + levelDB, err := db.NewGoLevelDBWithOpts(name, s.GetMinterHome(), opts) + if err != nil { + return nil, err + } + s.eventDB = levelDB + return s.eventDB, nil +} + +func (s *Storage) InitStateLevelDB(name string, opts *opt.Options) (db.DB, error) { + levelDB, err := db.NewGoLevelDBWithOpts(name, s.GetMinterHome(), opts) + if err != nil { + return nil, err + } + s.stateDB = levelDB + return s.stateDB, nil +} -func GetMinterHome() string { - if MinterHome != "" { - return MinterHome +func (s *Storage) GetMinterHome() string { + if s.minterHome != "" { + return s.minterHome } - home := os.Getenv("MINTERHOME") + s.minterHome = os.Getenv("MINTERHOME") - if home != "" { - return home + if s.minterHome != "" { + return s.minterHome } - return os.ExpandEnv(filepath.Join("$HOME", ".minter")) + s.minterHome = os.ExpandEnv(filepath.Join("$HOME", ".minter")) + return s.minterHome } -func GetMinterConfigPath() string { - if MinterConfig != "" { - return MinterConfig +func (s *Storage) GetMinterConfigPath() string { + if s.minterConfig != "" { + return s.minterConfig } - return GetMinterHome() + "/config/config.toml" + s.minterConfig = s.GetMinterHome() + "/config/config.toml" + return s.minterConfig } diff --git a/config/config.go b/config/config.go index 80a6012f2..123b5ab25 100644 --- a/config/config.go +++ b/config/config.go @@ -2,7 +2,6 @@ package config import ( "fmt" - "github.com/MinterTeam/minter-go-node/cmd/utils" tmConfig "github.com/tendermint/tendermint/config" "os" "path/filepath" @@ -15,8 +14,8 @@ const ( // LogFormatJSON is a format for json output LogFormatJSON = "json" - defaultConfigDir = "config" - defaultDataDir = "data" + DefaultConfigDir = "config" + DefaultDataDir = "data" defaultConfigFileName = "config.toml" defaultGenesisJSONName = "genesis.json" @@ -27,11 +26,11 @@ const ( ) var ( - defaultConfigFilePath = filepath.Join(defaultConfigDir, defaultConfigFileName) - defaultGenesisJSONPath = filepath.Join(defaultConfigDir, defaultGenesisJSONName) - defaultPrivValKeyPath = filepath.Join(defaultConfigDir, defaultPrivValName) - defaultPrivValStatePath = filepath.Join(defaultConfigDir, defaultPrivValStateName) - defaultNodeKeyPath = filepath.Join(defaultConfigDir, defaultNodeKeyName) + defaultConfigFilePath = filepath.Join(DefaultConfigDir, defaultConfigFileName) + defaultGenesisJSONPath = filepath.Join(DefaultConfigDir, defaultGenesisJSONName) + defaultPrivValKeyPath = filepath.Join(DefaultConfigDir, defaultPrivValName) + defaultPrivValStatePath = filepath.Join(DefaultConfigDir, defaultPrivValStateName) + defaultNodeKeyPath = filepath.Join(DefaultConfigDir, defaultNodeKeyName) ) // DefaultConfig returns config with predefined values @@ -44,9 +43,7 @@ func DefaultConfig() *Config { "bab220855eb9625ea547f1ef1d11692c60a7a406@138.201.28.219:26656" cfg.TxIndex = &tmConfig.TxIndexConfig{ - Indexer: "kv", - IndexKeys: "", - IndexAllKeys: true, + Indexer: "kv", } cfg.DBPath = "tmdata" @@ -76,12 +73,10 @@ func DefaultConfig() *Config { } // GetConfig returns DefaultConfig with some changes -func GetConfig() *Config { +func GetConfig(home string) *Config { cfg := DefaultConfig() if cfg.ValidatorMode { - cfg.TxIndex.IndexAllKeys = false - cfg.TxIndex.IndexKeys = "" cfg.RPC.ListenAddress = "" cfg.RPC.GRPCListenAddress = "" @@ -91,8 +86,8 @@ func GetConfig() *Config { cfg.P2P.AddrBook = "config/addrbook.json" - cfg.SetRoot(utils.GetMinterHome()) - EnsureRoot(utils.GetMinterHome()) + cfg.SetRoot(home) + EnsureRoot(home) return cfg } @@ -152,20 +147,29 @@ func GetTmConfig(cfg *Config) *tmConfig.Config { PrivValidatorListenAddr: cfg.PrivValidatorListenAddr, NodeKey: cfg.NodeKey, ABCI: cfg.ABCI, - ProfListenAddress: cfg.ProfListenAddress, FilterPeers: cfg.FilterPeers, }, - RPC: cfg.RPC, - P2P: cfg.P2P, - Mempool: cfg.Mempool, - FastSync: &tmConfig.FastSyncConfig{Version: "v0"}, + RPC: cfg.RPC, + P2P: cfg.P2P, + Mempool: cfg.Mempool, + // StateSync: &tmConfig.StateSyncConfig{ + // Enable: true, + // TempDir: "", + // RPCServers: []string{}, // todo + // TrustPeriod: 168 * time.Hour, + // TrustHeight: 0, + // TrustHash: "", + // DiscoveryTime: 15 * time.Second, + // }, + StateSync: tmConfig.DefaultStateSyncConfig(), + FastSync: tmConfig.DefaultFastSyncConfig(), Consensus: cfg.Consensus, TxIndex: cfg.TxIndex, Instrumentation: cfg.Instrumentation, } } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // BaseConfig // BaseConfig defines the base configuration for a Tendermint node @@ -327,7 +331,7 @@ func DefaultPackageLogLevels() string { return fmt.Sprintf("consensus:info,main:info,state:info,*:%s", DefaultLogLevel()) } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // Utils // helper function to make config creation independent of root dir diff --git a/config/toml.go b/config/toml.go index f4b00ca58..e2ec9b99d 100644 --- a/config/toml.go +++ b/config/toml.go @@ -24,10 +24,10 @@ func EnsureRoot(rootDir string) { if err := os.EnsureDir(rootDir, 0700); err != nil { panic(err.Error()) } - if err := os.EnsureDir(filepath.Join(rootDir, defaultConfigDir), 0700); err != nil { + if err := os.EnsureDir(filepath.Join(rootDir, DefaultConfigDir), 0700); err != nil { panic(err.Error()) } - if err := os.EnsureDir(filepath.Join(rootDir, defaultDataDir), 0700); err != nil { + if err := os.EnsureDir(filepath.Join(rootDir, DefaultDataDir), 0700); err != nil { panic(err.Error()) } diff --git a/core/appdb/appdb.go b/core/appdb/appdb.go index a764a86b9..a93bffa30 100644 --- a/core/appdb/appdb.go +++ b/core/appdb/appdb.go @@ -3,17 +3,14 @@ package appdb import ( "encoding/binary" "errors" - "github.com/MinterTeam/minter-go-node/cmd/utils" + "fmt" "github.com/MinterTeam/minter-go-node/config" - "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/abci/types" + abciTypes "github.com/tendermint/tendermint/abci/types" + tmjson "github.com/tendermint/tendermint/libs/json" "github.com/tendermint/tm-db" ) -var ( - cdc = amino.NewCodec() -) - const ( hashPath = "hash" heightPath = "height" @@ -24,26 +21,34 @@ const ( dbName = "app" ) +func init() { + tmjson.RegisterType(&lastBlocksTimeDelta{}, "last_blocks_time_delta") +} + // AppDB is responsible for storing basic information about app state on disk type AppDB struct { - db db.DB + db db.DB + startHeight uint64 + blocksDelta []*lastBlocksTimeDelta + validators abciTypes.ValidatorUpdates } // Close closes db connection, panics on error -func (appDB *AppDB) Close() { +func (appDB *AppDB) Close() error { if err := appDB.db.Close(); err != nil { - panic(err) + return err } + return nil } // GetLastBlockHash returns latest block hash stored on disk func (appDB *AppDB) GetLastBlockHash() []byte { - var hash [32]byte - rawHash, err := appDB.db.Get([]byte(hashPath)) if err != nil { panic(err) } + + var hash [32]byte copy(hash[:], rawHash) return hash[:] @@ -87,25 +92,32 @@ func (appDB *AppDB) SetStartHeight(height uint64) { if err := appDB.db.Set([]byte(startHeightPath), h); err != nil { panic(err) } + appDB.startHeight = height } // GetStartHeight returns start height stored on disk func (appDB *AppDB) GetStartHeight() uint64 { + if appDB.startHeight != 0 { + return appDB.startHeight + } result, err := appDB.db.Get([]byte(startHeightPath)) if err != nil { panic(err) } - var height uint64 if result != nil { - height = binary.BigEndian.Uint64(result) + appDB.startHeight = binary.BigEndian.Uint64(result) } - return height + return appDB.startHeight } // GetValidators returns list of latest validators stored on dist func (appDB *AppDB) GetValidators() types.ValidatorUpdates { + if appDB.validators != nil { + return appDB.validators + } + result, err := appDB.db.Get([]byte(validatorsPath)) if err != nil { panic(err) @@ -117,7 +129,7 @@ func (appDB *AppDB) GetValidators() types.ValidatorUpdates { var vals types.ValidatorUpdates - err = cdc.UnmarshalBinaryBare(result, &vals) + err = tmjson.Unmarshal(result, &vals) if err != nil { panic(err) } @@ -125,9 +137,17 @@ func (appDB *AppDB) GetValidators() types.ValidatorUpdates { return vals } -// SaveValidators stores given validators list on disk, panics on error -func (appDB *AppDB) SaveValidators(vals types.ValidatorUpdates) { - data, err := cdc.MarshalBinaryBare(vals) +// SetValidators sets given validators list on mem +func (appDB *AppDB) SetValidators(vals types.ValidatorUpdates) { + appDB.validators = vals +} + +// FlushValidators stores validators list from mem to disk, panics on error +func (appDB *AppDB) FlushValidators() { + if appDB.validators == nil { + return + } + data, err := tmjson.Marshal(appDB.validators) if err != nil { panic(err) } @@ -135,6 +155,7 @@ func (appDB *AppDB) SaveValidators(vals types.ValidatorUpdates) { if err := appDB.db.Set([]byte(validatorsPath), data); err != nil { panic(err) } + appDB.validators = nil } type lastBlocksTimeDelta struct { @@ -142,36 +163,60 @@ type lastBlocksTimeDelta struct { Delta int } +const blockDeltaCount = 3 + // GetLastBlocksTimeDelta returns delta of time between latest blocks -func (appDB *AppDB) GetLastBlocksTimeDelta(height uint64) (int, error) { - result, err := appDB.db.Get([]byte(blockTimeDeltaPath)) - if err != nil { - panic(err) - } - if result == nil { - return 0, errors.New("no info about lastBlocksTimeDelta is available") - } +func (appDB *AppDB) GetLastBlocksTimeDelta(height uint64) (int, int, error) { + if len(appDB.blocksDelta) == 0 { + result, err := appDB.db.Get([]byte(blockTimeDeltaPath)) + if err != nil { + panic(err) + } + if len(result) == 0 { + return 0, 0, errors.New("no info about BlocksTimeDelta is available") + } + err = tmjson.Unmarshal(result, &appDB.blocksDelta) + if err != nil { + panic(err) + } + } + + return calcBlockDelta(height, appDB.blocksDelta) +} - data := lastBlocksTimeDelta{} - err = cdc.UnmarshalBinaryBare(result, &data) - if err != nil { - panic(err) +func calcBlockDelta(height uint64, deltas []*lastBlocksTimeDelta) (int, int, error) { + count := len(deltas) + if count == 0 { + return 0, 0, errors.New("no info about BlocksTimeDelta is available") } - - if data.Height != height { - return 0, errors.New("no info about lastBlocksTimeDelta is available") + for i, delta := range deltas { + if height-delta.Height != uint64(count-i) { + return 0, 0, fmt.Errorf("no info about BlocksTimeDelta is available, but has info about %d block height", delta.Height) + } } - - return data.Delta, nil + var result int + for _, delta := range deltas { + result += delta.Delta + } + return result, count, nil } -// SetLastBlocksTimeDelta stores delta of time between latest blocks -func (appDB *AppDB) SetLastBlocksTimeDelta(height uint64, delta int) { - data, err := cdc.MarshalBinaryBare(lastBlocksTimeDelta{ +func (appDB *AppDB) AddBlocksTimeDelta(height uint64, delta int) { + for _, timeDelta := range appDB.blocksDelta { + if timeDelta.Height == height { + return + } + } + appDB.blocksDelta = append(appDB.blocksDelta, &lastBlocksTimeDelta{ Height: height, Delta: delta, }) + count := len(appDB.blocksDelta) + if count > blockDeltaCount { + appDB.blocksDelta = appDB.blocksDelta[count-blockDeltaCount:] + } + data, err := tmjson.Marshal(appDB.blocksDelta) if err != nil { panic(err) } @@ -182,8 +227,12 @@ func (appDB *AppDB) SetLastBlocksTimeDelta(height uint64, delta int) { } // NewAppDB creates AppDB instance with given config -func NewAppDB(cfg *config.Config) *AppDB { +func NewAppDB(homeDir string, cfg *config.Config) *AppDB { + newDB, err := db.NewDB(dbName, db.BackendType(cfg.DBBackend), homeDir+"/data") + if err != nil { + panic(err) + } return &AppDB{ - db: db.NewDB(dbName, db.BackendType(cfg.DBBackend), utils.GetMinterHome()+"/data"), + db: newDB, } } diff --git a/core/check/check.go b/core/check/check.go index 15940ee98..acc039e12 100644 --- a/core/check/check.go +++ b/core/check/check.go @@ -17,6 +17,13 @@ var ( ErrInvalidSig = errors.New("invalid transaction v, r, s values") ) +type gasPaymentMethod byte + +const ( + Bancor gasPaymentMethod = iota + Pool +) + // Check is like an ordinary bank check. // Each user of network can issue check with any amount of coins and pass it to another person. // Receiver will be able to cash a check from arbitrary account. @@ -35,10 +42,11 @@ type Check struct { Coin types.CoinID Value *big.Int GasCoin types.CoinID - Lock *big.Int - V *big.Int - R *big.Int - S *big.Int + // GasPay gasPaymentMethod // todo: add + Lock *big.Int + V *big.Int + R *big.Int + S *big.Int } // Sender returns sender's address of a Check, recovered from signature diff --git a/core/code/code.go b/core/code/code.go index b1f8fceae..6fc1c0598 100644 --- a/core/code/code.go +++ b/core/code/code.go @@ -24,13 +24,19 @@ const ( CoinReserveUnderflow uint32 = 116 WrongHaltHeight uint32 = 117 HaltAlreadyExists uint32 = 118 + CommissionCoinNotSufficient uint32 = 119 + VoiceExpired uint32 = 120 + VoiceAlreadyExists uint32 = 121 + WrongUpdateVersionName uint32 = 122 // coin creation + CoinHasNotReserve uint32 = 200 CoinAlreadyExists uint32 = 201 WrongCrr uint32 = 202 InvalidCoinSymbol uint32 = 203 InvalidCoinName uint32 = 204 WrongCoinSupply uint32 = 205 + WrongCoinEmission uint32 = 206 // recreate coin IsNotOwnerOfCoin uint32 = 206 @@ -53,6 +59,7 @@ const ( PublicKeyInBlockList uint32 = 410 NewPublicKeyIsBad uint32 = 411 InsufficientWaitList uint32 = 412 + PeriodLimitReached uint32 = 413 // check CheckInvalidLock uint32 = 501 @@ -72,8 +79,150 @@ const ( DifferentCountAddressesAndWeights uint32 = 607 IncorrectTotalWeights uint32 = 608 NotEnoughMultisigVotes uint32 = 609 + + // swap pool + SwapPoolUnknown uint32 = 700 + PairNotExists uint32 = 701 + InsufficientInputAmount uint32 = 702 + InsufficientLiquidity uint32 = 703 + InsufficientLiquidityMinted uint32 = 704 + InsufficientLiquidityBurned uint32 = 705 + InsufficientLiquidityBalance uint32 = 706 + InsufficientOutputAmount uint32 = 707 + PairAlreadyExists uint32 = 708 + TooLongSwapRoute uint32 = 709 + + // emission coin + CoinIsNotToken uint32 = 800 + CoinNotMintable uint32 = 801 + CoinNotBurnable uint32 = 802 ) +func NewInsufficientLiquidityBalance(liquidity, amount0, coin0, amount1, coin1, requestedLiquidity string) *insufficientLiquidityBalance { + return &insufficientLiquidityBalance{Code: strconv.Itoa(int(InsufficientLiquidityBalance)), Coin0: coin0, Coin1: coin1, Amount0: amount0, Amount1: amount1, Liquidity: liquidity, RequestedLiquidity: requestedLiquidity} +} + +type insufficientLiquidityBalance struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + Amount0 string `json:"amount0,omitempty"` + WantedAmount0 string `json:"wanted_amount0,omitempty"` + Coin1 string `json:"coin1,omitempty"` + Amount1 string `json:"amount1,omitempty"` + WantedAmount1 string `json:"wanted_amount1,omitempty"` + Liquidity string `json:"liquidity,omitempty"` + RequestedLiquidity string `json:"requested_liquidity,omitempty"` +} + +func NewInsufficientLiquidityBurned(wantGetAmount0 string, coin0 string, wantGetAmount1 string, coin1 string, liquidity string, amount0 string, amount1 string) *insufficientLiquidityBurned { + return &insufficientLiquidityBurned{Code: strconv.Itoa(int(InsufficientLiquidityBurned)), Coin0: coin0, Coin1: coin1, WantAmount0: wantGetAmount0, WantAmount1: wantGetAmount1, Amount0Out: amount0, Amount1Out: amount1, RequestedLiquidity: liquidity} +} + +type insufficientLiquidityBurned struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + WantAmount0 string `json:"want_amount0,omitempty"` + Amount0Out string `json:"amount0_out,omitempty"` + Coin1 string `json:"coin1,omitempty"` + WantAmount1 string `json:"want_amount1,omitempty"` + Amount1Out string `json:"amount1_out,omitempty"` + RequestedLiquidity string `json:"requested_liquidity,omitempty"` +} + +func NewInsufficientLiquidity(coin0, value0, coin1, value1, reserve0, reserve1 string) *insufficientLiquidity { + return &insufficientLiquidity{Code: strconv.Itoa(int(InsufficientLiquidity)), Coin0: coin0, Coin1: coin1, Reserve0: reserve0, Reserve1: reserve1, Amount0In: value0, Amount1Out: value1} +} + +type insufficientLiquidity struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + Reserve0 string `json:"reserve0,omitempty"` + Amount0In string `json:"amount0_in,omitempty"` + + Coin1 string `json:"coin1,omitempty"` + Reserve1 string `json:"reserve1,omitempty"` + Amount1Out string `json:"amount1_out,omitempty"` +} + +func NewInsufficientInputAmount(coin0, value0, coin1, value1, neededValue1 string) *insufficientInputAmount { + return &insufficientInputAmount{Code: strconv.Itoa(int(InsufficientInputAmount)), Coin0: coin0, Coin1: coin1, Amount0: value0, Amount1: value1, NeededAmount1: neededValue1} +} + +type insufficientInputAmount struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + Amount0 string `json:"amount0,omitempty"` + + Coin1 string `json:"coin1,omitempty"` + Amount1 string `json:"amount1,omitempty"` + NeededAmount1 string `json:"needed_amount1,omitempty"` +} + +func NewInsufficientOutputAmount(coin0, value0, coin1, value1 string) *insufficientOutputAmount { + return &insufficientOutputAmount{Code: strconv.Itoa(int(InsufficientOutputAmount)), Coin0: coin0, Coin1: coin1, Amount0: value0, Amount1: value1} +} + +type insufficientOutputAmount struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + Amount0 string `json:"amount0,omitempty"` + Coin1 string `json:"coin1,omitempty"` + Amount1 string `json:"amount1,omitempty"` +} + +func NewInsufficientLiquidityMinted(coin0, value0, coin1, value1 string) *insufficientLiquidityMinted { + return &insufficientLiquidityMinted{Code: strconv.Itoa(int(InsufficientLiquidityMinted)), Coin0: coin0, Coin1: coin1, NeededAmount0: value0, NeededAmount1: value1} +} + +type insufficientLiquidityMinted struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + NeededAmount0 string `json:"needed_amount0,omitempty"` + + Coin1 string `json:"coin1,omitempty"` + NeededAmount1 string `json:"needed_amount1,omitempty"` +} + +type pairNotExists struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + Coin1 string `json:"coin1,omitempty"` +} + +func NewPairNotExists(coin0 string, coin1 string) *pairNotExists { + return &pairNotExists{Code: strconv.Itoa(int(PairNotExists)), Coin0: coin0, Coin1: coin1} +} + +type pairAlreadyExists struct { + Code string `json:"code,omitempty"` + Coin0 string `json:"coin0,omitempty"` + Coin1 string `json:"coin1,omitempty"` +} + +func NewPairAlreadyExists(coin0 string, coin1 string) *pairAlreadyExists { + return &pairAlreadyExists{Code: strconv.Itoa(int(PairAlreadyExists)), Coin0: coin0, Coin1: coin1} +} + +type voiceExpired struct { + Code string `json:"code,omitempty"` + Block string `json:"block,omitempty"` + CurrentBlock string `json:"current_block,omitempty"` +} + +func NewVoiceExpired(block string, current string) *voiceExpired { + return &voiceExpired{Code: strconv.Itoa(int(VoiceExpired)), Block: block, CurrentBlock: current} +} + +type commissionCoinNotSufficient struct { + Code string `json:"code,omitempty"` + Pool string `json:"pool,omitempty"` + Bancor string `json:"bancor,omitempty"` +} + +func NewCommissionCoinNotSufficient(bancor string, pool string) *commissionCoinNotSufficient { + return &commissionCoinNotSufficient{Code: strconv.Itoa(int(CommissionCoinNotSufficient)), Pool: pool, Bancor: bancor} +} + type wrongNonce struct { Code string `json:"code,omitempty"` ExpectedNonce string `json:"expected_nonce,omitempty"` @@ -94,6 +243,19 @@ func NewCoinNotExists(coinSymbol string, coinId string) *coinNotExists { return &coinNotExists{Code: strconv.Itoa(int(CoinNotExists)), CoinSymbol: coinSymbol, CoinId: coinId} } +type coinIsNotMintableOrBurnable struct { + Code string `json:"code,omitempty"` + CoinSymbol string `json:"coin_symbol,omitempty"` + CoinId string `json:"coin_id,omitempty"` +} + +func NewCoinIsNotMintable(coinSymbol string, coinId string) *coinIsNotMintableOrBurnable { + return &coinIsNotMintableOrBurnable{Code: strconv.Itoa(int(CoinNotMintable)), CoinSymbol: coinSymbol, CoinId: coinId} +} +func NewCoinIsNotBurnable(coinSymbol string, coinId string) *coinIsNotMintableOrBurnable { + return &coinIsNotMintableOrBurnable{Code: strconv.Itoa(int(CoinNotBurnable)), CoinSymbol: coinSymbol, CoinId: coinId} +} + type wrongGasCoin struct { Code string `json:"code,omitempty"` TxGasCoinSymbol string `json:"tx_coin_symbol,omitempty"` @@ -118,6 +280,26 @@ func NewCoinReserveNotSufficient(coinSymbol string, coinId string, hasBipValue s return &coinReserveNotSufficient{Code: strconv.Itoa(int(CoinReserveNotSufficient)), CoinSymbol: coinSymbol, CoinId: coinId, HasBipValue: hasBipValue, RequiredBipValue: requiredBipValue} } +type coinHasNotReserve struct { + Code string `json:"code,omitempty"` + CoinSymbol string `json:"coin_symbol,omitempty"` + CoinId string `json:"coin_id,omitempty"` +} + +func NewCoinHasNotReserve(coinSymbol string, coinId string) *coinHasNotReserve { + return &coinHasNotReserve{Code: strconv.Itoa(int(CoinHasNotReserve)), CoinSymbol: coinSymbol, CoinId: coinId} +} + +type coinInNotToken struct { + Code string `json:"code,omitempty"` + CoinSymbol string `json:"coin_symbol,omitempty"` + CoinId string `json:"coin_id,omitempty"` +} + +func NewCoinInNotToken(coinSymbol string, coinId string) *coinInNotToken { + return &coinInNotToken{Code: strconv.Itoa(int(CoinIsNotToken)), CoinSymbol: coinSymbol, CoinId: coinId} +} + type txTooLarge struct { Code string `json:"code,omitempty"` MaxTxLength string `json:"max_tx_length,omitempty"` @@ -542,10 +724,20 @@ type wrongHaltHeight struct { Height string `json:"height,omitempty"` } -func NewWrongHaltHeight(height string, pubkey string) *wrongHaltHeight { +func NewHaltAlreadyExists(height string, pubkey string) *wrongHaltHeight { return &wrongHaltHeight{Code: strconv.Itoa(int(WrongHaltHeight)), Height: height, PublicKey: pubkey} } +type voiceAlreadyExists struct { + Code string `json:"code,omitempty"` + PublicKey string `json:"public_key,omitempty"` + Height string `json:"height,omitempty"` +} + +func NewVoiceAlreadyExists(height string, pubkey string) *voiceAlreadyExists { + return &voiceAlreadyExists{Code: strconv.Itoa(int(VoiceAlreadyExists)), Height: height, PublicKey: pubkey} +} + type tooLowStake struct { Code string `json:"code,omitempty"` Sender string `json:"sender,omitempty"` @@ -579,6 +771,16 @@ func NewMultisigNotExists(address string) *multisigNotExists { return &multisigNotExists{Code: strconv.Itoa(int(MultisigNotExists)), Address: address} } +type periodLimitReached struct { + Code string `json:"code,omitempty"` + NextTime string `json:"next_time,omitempty"` + PreviousTime string `json:"previous_time,omitempty"` +} + +func NewPeriodLimitReached(next string, last string) *periodLimitReached { + return &periodLimitReached{Code: strconv.Itoa(int(PeriodLimitReached)), NextTime: next, PreviousTime: last} +} + type multisigExists struct { Code string `json:"code,omitempty"` Address string `json:"address,omitempty"` @@ -592,16 +794,37 @@ type wrongCoinSupply struct { Code string `json:"code,omitempty"` MaxCoinSupply string `json:"max_coin_supply,omitempty"` + MinCoinSupply string `json:"min_coin_supply,omitempty"` CurrentCoinSupply string `json:"current_coin_supply,omitempty"` MinInitialReserve string `json:"min_initial_reserve,omitempty"` CurrentInitialReserve string `json:"current_initial_reserve,omitempty"` - MinInitialAmount string `json:"min_initial_amount,omitempty"` - MaxInitialAmount string `json:"max_initial_amount,omitempty"` CurrentInitialAmount string `json:"current_initial_amount,omitempty"` } -func NewWrongCoinSupply(maxCoinSupply string, currentCoinSupply string, minInitialReserve string, currentInitialReserve string, minInitialAmount string, maxInitialAmount string, currentInitialAmount string) *wrongCoinSupply { - return &wrongCoinSupply{Code: strconv.Itoa(int(WrongCoinSupply)), MaxCoinSupply: maxCoinSupply, CurrentCoinSupply: currentCoinSupply, MinInitialReserve: minInitialReserve, CurrentInitialReserve: currentInitialReserve, MinInitialAmount: minInitialAmount, MaxInitialAmount: maxInitialAmount, CurrentInitialAmount: currentInitialAmount} +func NewWrongCoinSupply(minCoinSupply string, maxCoinSupply string, currentCoinSupply string, minInitialReserve string, currentInitialReserve string, initialAmount string) *wrongCoinSupply { + return &wrongCoinSupply{Code: strconv.Itoa(int(WrongCoinSupply)), MinCoinSupply: minCoinSupply, MaxCoinSupply: maxCoinSupply, CurrentCoinSupply: currentCoinSupply, MinInitialReserve: minInitialReserve, CurrentInitialReserve: currentInitialReserve, CurrentInitialAmount: initialAmount} +} + +type wrongCoinEmission struct { + Code string `json:"code,omitempty"` + + MaxCoinSupply string `json:"max_coin_supply,omitempty"` + MinCoinSupply string `json:"min_coin_supply,omitempty"` + CoinSupply string `json:"coin_supply,omitempty"` + SubAmount string `json:"sub_amount,omitempty"` + AddAmount string `json:"add_amount,omitempty"` +} + +func NewWrongCoinEmission(minCoinSupply string, maxCoinSupply string, coinSupply string, addAmount, subAmount string) *wrongCoinEmission { + return &wrongCoinEmission{Code: strconv.Itoa(int(WrongCoinEmission)), MinCoinSupply: minCoinSupply, MaxCoinSupply: maxCoinSupply, CoinSupply: coinSupply, AddAmount: addAmount, SubAmount: subAmount} +} + +type customCode struct { + Code string `json:"code,omitempty"` +} + +func NewCustomCode(code uint32) *customCode { + return &customCode{Code: strconv.Itoa(int(code))} } diff --git a/core/commissions/commissions.go b/core/commissions/commissions.go deleted file mode 100644 index aa7b21f51..000000000 --- a/core/commissions/commissions.go +++ /dev/null @@ -1,23 +0,0 @@ -package commissions - -// all commissions are divided by 10^15 -// actual commission is SendTx * 10^15 = 10 000 000 000 000 000 PIP = 0,01 BIP -const ( - SendTx int64 = 10 - CreateMultisig int64 = 100 - ConvertTx int64 = 100 - DeclareCandidacyTx int64 = 10000 - DelegateTx int64 = 200 - UnbondTx int64 = 200 - PayloadByte int64 = 2 - ToggleCandidateStatus int64 = 100 - EditCandidate int64 = 10000 - EditCandidatePublicKey int64 = 100000000 - MultisendDelta int64 = 5 - RedeemCheckTx = SendTx * 3 - SetHaltBlock int64 = 1000 - RecreateCoin int64 = 10000000 - EditOwner int64 = 10000000 - EditMultisigData int64 = 1000 - PriceVoteData int64 = 10 -) diff --git a/core/events/store.go b/core/events/store.go index f8bfed745..b1138e9bc 100644 --- a/core/events/store.go +++ b/core/events/store.go @@ -2,20 +2,42 @@ package events import ( "encoding/binary" - "github.com/tendermint/go-amino" + tmjson "github.com/tendermint/tendermint/libs/json" db "github.com/tendermint/tm-db" "sync" ) +func init() { + tmjson.RegisterType(&reward{}, "reward") + tmjson.RegisterType(&slash{}, "slash") + tmjson.RegisterType(&unbond{}, "unbond") + tmjson.RegisterType(&kick{}, "kick") + tmjson.RegisterType(&move{}, "move") + tmjson.RegisterType(&RewardEvent{}, TypeRewardEvent) + tmjson.RegisterType(&SlashEvent{}, TypeSlashEvent) + tmjson.RegisterType(&UnbondEvent{}, TypeUnbondEvent) + tmjson.RegisterType(&StakeKickEvent{}, TypeStakeKickEvent) + tmjson.RegisterType(&StakeMoveEvent{}, TypeStakeMoveEvent) + tmjson.RegisterType(&UpdateNetworkEvent{}, TypeUpdateNetworkEvent) + tmjson.RegisterType(&UpdateCommissionsEvent{}, TypeUpdateCommissionsEvent) +} + // IEventsDB is an interface of Events type IEventsDB interface { - AddEvent(height uint32, event Event) + AddEvent(event Event) LoadEvents(height uint32) Events - CommitEvents() error + CommitEvents(uint32) error + Close() error } +type MockEvents struct{} + +func (e MockEvents) AddEvent(event Event) {} +func (e MockEvents) LoadEvents(height uint32) Events { return nil } +func (e MockEvents) CommitEvents(uint32) error { return nil } +func (e MockEvents) Close() error { return nil } + type eventsStore struct { - cdc *amino.Codec sync.RWMutex db db.DB pending pendingEvents @@ -27,22 +49,12 @@ type eventsStore struct { type pendingEvents struct { sync.Mutex - height uint32 - items Events + items Events } // NewEventsStore creates new events store in given DB func NewEventsStore(db db.DB) IEventsDB { - codec := amino.NewCodec() - codec.RegisterInterface((*Event)(nil), nil) - codec.RegisterInterface((*compactEvent)(nil), nil) - codec.RegisterConcrete(&reward{}, "reward", nil) - codec.RegisterConcrete(&slash{}, "slash", nil) - codec.RegisterConcrete(&unbond{}, "unbond", nil) - codec.RegisterConcrete(&stakeKick{}, "stakeKick", nil) - return &eventsStore{ - cdc: codec, RWMutex: sync.RWMutex{}, db: db, pending: pendingEvents{}, @@ -58,19 +70,19 @@ func (store *eventsStore) cachePubKey(id uint16, key [32]byte) { store.pubKeyID[key] = id } +func (store *eventsStore) Close() error { + return store.db.Close() +} + func (store *eventsStore) cacheAddress(id uint32, address [20]byte) { store.idAddress[id] = address store.addressID[address] = id } -func (store *eventsStore) AddEvent(height uint32, event Event) { +func (store *eventsStore) AddEvent(event Event) { store.pending.Lock() defer store.pending.Unlock() - if store.pending.height != height { - store.pending.items = Events{} - } store.pending.items = append(store.pending.items, event) - store.pending.height = height } func (store *eventsStore) LoadEvents(height uint32) Events { @@ -80,46 +92,59 @@ func (store *eventsStore) LoadEvents(height uint32) Events { if err != nil { panic(err) } + if bytes == nil { + return nil + } if len(bytes) == 0 { return Events{} } - var items []compactEvent - if err := store.cdc.UnmarshalBinaryBare(bytes, &items); err != nil { + var items []compact + if err := tmjson.Unmarshal(bytes, &items); err != nil { panic(err) } resultEvents := make(Events, 0, len(items)) for _, compactEvent := range items { - event := compactEvent.compile(store.idPubKey[compactEvent.pubKeyID()], store.idAddress[compactEvent.addressID()]) - resultEvents = append(resultEvents, event) + if stake, ok := compactEvent.(stake); ok { + resultEvents = append(resultEvents, stake.compile(store.idPubKey[stake.pubKeyID()], store.idAddress[stake.addressID()])) + } else if c, ok := compactEvent.(Event); ok { + resultEvents = append(resultEvents, c) + } else { + panic("unemployment event interface") + } } return resultEvents } -func (store *eventsStore) CommitEvents() error { +func (store *eventsStore) CommitEvents(height uint32) error { store.loadCache() store.pending.Lock() defer store.pending.Unlock() - var data []compactEvent + var data []compact for _, item := range store.pending.items { - pubKey := store.savePubKey(item.validatorPubKey()) - address := store.saveAddress(item.address()) - data = append(data, item.convert(pubKey, address)) + if stake, ok := item.(Stake); ok { + pubKey := store.savePubKey(stake.validatorPubKey()) + address := store.saveAddress(stake.address()) + data = append(data, stake.convert(pubKey, address)) + continue + } + data = append(data, item) } - bytes, err := store.cdc.MarshalBinaryBare(data) + bytes, err := tmjson.Marshal(data) if err != nil { return err } store.Lock() defer store.Unlock() - if err := store.db.Set(uint32ToBytes(store.pending.height), bytes); err != nil { + if err := store.db.Set(uint32ToBytes(height), bytes); err != nil { return err } + store.pending.items = Events{} return nil } diff --git a/core/events/store_test.go b/core/events/store_test.go index 3b11f8045..76ace20c7 100644 --- a/core/events/store_test.go +++ b/core/events/store_test.go @@ -16,7 +16,7 @@ func TestIEventsDB(t *testing.T) { Amount: "111497225000000000000", ValidatorPubKey: types.HexToPubkey("Mp9e13f2f5468dd782b316444fbd66595e13dba7d7bd3efa1becd50b42045f58c6"), } - store.AddEvent(12, event) + store.AddEvent(event) } { event := &StakeKickEvent{ @@ -25,9 +25,9 @@ func TestIEventsDB(t *testing.T) { Amount: "891977800000000000000", ValidatorPubKey: types.HexToPubkey("Mp738da41ba6a7b7d69b7294afa158b89c5a1b410cbf0c2443c85c5fe24ad1dd1c"), } - store.AddEvent(12, event) + store.AddEvent(event) } - err := store.CommitEvents() + err := store.CommitEvents(12) if err != nil { t.Fatal(err) } @@ -39,7 +39,7 @@ func TestIEventsDB(t *testing.T) { Amount: "891977800000000000001", ValidatorPubKey: types.HexToPubkey("Mp738da41ba6a7b7d69b7294afa158b89c5a1b410cbf0c2443c85c5fe24ad1dd11"), } - store.AddEvent(14, event) + store.AddEvent(event) } { event := &UnbondEvent{ @@ -48,9 +48,9 @@ func TestIEventsDB(t *testing.T) { Amount: "891977800000000000002", ValidatorPubKey: types.HexToPubkey("Mp738da41ba6a7b7d69b7294afa158b89c5a1b410cbf0c2443c85c5fe24ad1dd12"), } - store.AddEvent(14, event) + store.AddEvent(event) } - err = store.CommitEvents() + err = store.CommitEvents(14) if err != nil { t.Fatal(err) } @@ -62,9 +62,9 @@ func TestIEventsDB(t *testing.T) { Amount: "891977800000000000010", ValidatorPubKey: types.HexToPubkey("Mp738da41ba6a7b7d69b7294afa158b89c5a1b410cbf0c2443c85c5fe24ad1dd10"), } - store.AddEvent(11, event) + store.AddEvent(event) } - err = store.CommitEvents() + err = store.CommitEvents(11) if err != nil { t.Fatal(err) } @@ -167,3 +167,54 @@ func TestIEventsDB(t *testing.T) { t.Fatal("invalid Coin") } } + +func TestIEventsDBm2(t *testing.T) { + store := NewEventsStore(db.NewMemDB()) + + { + event := &UpdateCommissionsEvent{ + Send: "1000000000", + } + store.AddEvent(event) + } + { + event := &UpdateNetworkEvent{ + Version: "m2", + } + store.AddEvent(event) + } + err := store.CommitEvents(12) + if err != nil { + t.Fatal(err) + } + + loadEvents := store.LoadEvents(12) + + if len(loadEvents) != 2 { + t.Fatalf("count of events not equal 2, got %d", len(loadEvents)) + } + + if loadEvents[0].Type() != TypeUpdateCommissionsEvent { + t.Fatal("invalid event type") + } + if loadEvents[0].(*UpdateCommissionsEvent).Send != "1000000000" { + t.Fatal("invalid Amount") + } + +} + +func TestIEventsNil(t *testing.T) { + store := NewEventsStore(db.NewMemDB()) + err := store.CommitEvents(12) + if err != nil { + t.Fatal(err) + } + + if store.LoadEvents(12) == nil { + t.Fatalf("nil") + } + + if store.LoadEvents(13) != nil { + t.Fatalf("not nil") + } +} diff --git a/core/events/types.go b/core/events/types.go index 34b674477..221dbfb61 100644 --- a/core/events/types.go +++ b/core/events/types.go @@ -3,45 +3,42 @@ package events import ( "fmt" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/tendermint/go-amino" "math/big" ) // Event type names const ( - TypeRewardEvent = "minter/RewardEvent" - TypeSlashEvent = "minter/SlashEvent" - TypeUnbondEvent = "minter/UnbondEvent" - TypeStakeKickEvent = "minter/StakeKickEvent" + TypeRewardEvent = "minter/RewardEvent" + TypeSlashEvent = "minter/SlashEvent" + TypeUnbondEvent = "minter/UnbondEvent" + TypeStakeKickEvent = "minter/StakeKickEvent" + TypeStakeMoveEvent = "minter/StakeMoveEvent" + TypeUpdateNetworkEvent = "minter/UpdateNetworkEvent" + TypeUpdateCommissionsEvent = "minter/UpdateCommissionsEvent" ) -func RegisterAminoEvents(codec *amino.Codec) { - codec.RegisterInterface((*Event)(nil), nil) - codec.RegisterConcrete(RewardEvent{}, - TypeRewardEvent, nil) - codec.RegisterConcrete(SlashEvent{}, - TypeSlashEvent, nil) - codec.RegisterConcrete(UnbondEvent{}, - TypeUnbondEvent, nil) - codec.RegisterConcrete(StakeKickEvent{}, - TypeStakeKickEvent, nil) -} - -type Event interface { - Type() string +type Stake interface { AddressString() string ValidatorPubKeyString() string validatorPubKey() types.Pubkey address() types.Address - convert(pubKeyID uint16, addressID uint32) compactEvent + convert(pubKeyID uint16, addressID uint32) compact +} + +type Event interface { + Type() string } -type compactEvent interface { +type stake interface { compile(pubKey [32]byte, address [20]byte) Event addressID() uint32 pubKeyID() uint16 } +type compact interface { + // compact() +} + type Events []Event type Role byte @@ -134,7 +131,7 @@ func (re *RewardEvent) validatorPubKey() types.Pubkey { return re.ValidatorPubKey } -func (re *RewardEvent) convert(pubKeyID uint16, addressID uint32) compactEvent { +func (re *RewardEvent) convert(pubKeyID uint16, addressID uint32) compact { result := new(reward) result.AddressID = addressID result.Role = NewRole(re.Role) @@ -195,7 +192,7 @@ func (se *SlashEvent) validatorPubKey() types.Pubkey { return se.ValidatorPubKey } -func (se *SlashEvent) convert(pubKeyID uint16, addressID uint32) compactEvent { +func (se *SlashEvent) convert(pubKeyID uint16, addressID uint32) compact { result := new(slash) result.AddressID = addressID result.Coin = uint32(se.Coin) @@ -256,7 +253,7 @@ func (ue *UnbondEvent) validatorPubKey() types.Pubkey { return ue.ValidatorPubKey } -func (ue *UnbondEvent) convert(pubKeyID uint16, addressID uint32) compactEvent { +func (ue *UnbondEvent) convert(pubKeyID uint16, addressID uint32) compact { result := new(unbond) result.AddressID = addressID result.Coin = uint32(ue.Coin) @@ -266,14 +263,79 @@ func (ue *UnbondEvent) convert(pubKeyID uint16, addressID uint32) compactEvent { return result } -type stakeKick struct { +type move struct { AddressID uint32 Amount []byte Coin uint32 PubKeyID uint16 + WaitList bool +} + +func (u *move) compile(pubKey [32]byte, address [20]byte) Event { + event := new(StakeMoveEvent) + event.ValidatorPubKey = pubKey + event.Address = address + event.Coin = uint64(u.Coin) + event.Amount = big.NewInt(0).SetBytes(u.Amount).String() + event.WaitList = u.WaitList + return event +} + +func (u *move) addressID() uint32 { + return u.AddressID +} + +func (u *move) pubKeyID() uint16 { + return u.PubKeyID } -func (u *stakeKick) compile(pubKey [32]byte, address [20]byte) Event { +type StakeMoveEvent struct { + Address types.Address `json:"address"` + Amount string `json:"amount"` + Coin uint64 `json:"coin"` + ValidatorPubKey types.Pubkey `json:"validator_pub_key"` + WaitList bool `json:"waitlist"` +} + +func (ue *StakeMoveEvent) Type() string { + return TypeStakeMoveEvent +} + +func (ue *StakeMoveEvent) AddressString() string { + return ue.Address.String() +} + +func (ue *StakeMoveEvent) address() types.Address { + return ue.Address +} + +func (ue *StakeMoveEvent) ValidatorPubKeyString() string { + return ue.ValidatorPubKey.String() +} + +func (ue *StakeMoveEvent) validatorPubKey() types.Pubkey { + return ue.ValidatorPubKey +} + +func (ue *StakeMoveEvent) convert(pubKeyID uint16, addressID uint32) compact { + result := new(move) + result.AddressID = addressID + result.Coin = uint32(ue.Coin) + bi, _ := big.NewInt(0).SetString(ue.Amount, 10) + result.Amount = bi.Bytes() + result.PubKeyID = pubKeyID + result.WaitList = ue.WaitList + return result +} + +type kick struct { + AddressID uint32 + Amount []byte + Coin uint32 + PubKeyID uint16 +} + +func (u *kick) compile(pubKey [32]byte, address [20]byte) Event { event := new(StakeKickEvent) event.ValidatorPubKey = pubKey event.Address = address @@ -282,11 +344,11 @@ func (u *stakeKick) compile(pubKey [32]byte, address [20]byte) Event { return event } -func (u *stakeKick) addressID() uint32 { +func (u *kick) addressID() uint32 { return u.AddressID } -func (u *stakeKick) pubKeyID() uint16 { +func (u *kick) pubKeyID() uint16 { return u.PubKeyID } @@ -317,8 +379,8 @@ func (ue *StakeKickEvent) validatorPubKey() types.Pubkey { return ue.ValidatorPubKey } -func (ue *StakeKickEvent) convert(pubKeyID uint16, addressID uint32) compactEvent { - result := new(stakeKick) +func (ue *StakeKickEvent) convert(pubKeyID uint16, addressID uint32) compact { + result := new(kick) result.AddressID = addressID result.Coin = uint32(ue.Coin) bi, _ := big.NewInt(0).SetString(ue.Amount, 10) @@ -326,3 +388,63 @@ func (ue *StakeKickEvent) convert(pubKeyID uint16, addressID uint32) compactEven result.PubKeyID = pubKeyID return result } + +type UpdateCommissionsEvent struct { + Coin uint64 `json:"coin"` + PayloadByte string `json:"payload_byte"` + Send string `json:"send"` + BuyBancor string `json:"buy_bancor"` + SellBancor string `json:"sell_bancor"` + SellAllBancor string `json:"sell_all_bancor"` + BuyPoolBase string `json:"buy_pool_base"` + BuyPoolDelta string `json:"buy_pool_delta"` + SellPoolBase string `json:"sell_pool_base"` + SellPoolDelta string `json:"sell_pool_delta"` + SellAllPoolBase string `json:"sell_all_pool_base"` + SellAllPoolDelta string `json:"sell_all_pool_delta"` + CreateTicker3 string `json:"create_ticker3"` + CreateTicker4 string `json:"create_ticker4"` + CreateTicker5 string `json:"create_ticker5"` + CreateTicker6 string `json:"create_ticker6"` + CreateTicker7_10 string `json:"create_ticker7_10"` + CreateCoin string `json:"create_coin"` + CreateToken string `json:"create_token"` + RecreateCoin string `json:"recreate_coin"` + RecreateToken string `json:"recreate_token"` + DeclareCandidacy string `json:"declare_candidacy"` + Delegate string `json:"delegate"` + Unbond string `json:"unbond"` + RedeemCheck string `json:"redeem_check"` + SetCandidateOn string `json:"set_candidate_on"` + SetCandidateOff string `json:"set_candidate_off"` + CreateMultisig string `json:"create_multisig"` + MultisendBase string `json:"multisend_base"` + MultisendDelta string `json:"multisend_delta"` + EditCandidate string `json:"edit_candidate"` + SetHaltBlock string `json:"set_halt_block"` + EditTickerOwner string `json:"edit_ticker_owner"` + EditMultisig string `json:"edit_multisig"` + PriceVote string `json:"price_vote"` + EditCandidatePublicKey string `json:"edit_candidate_public_key"` + CreateSwapPool string `json:"create_swap_pool"` + AddLiquidity string `json:"add_liquidity"` + RemoveLiquidity string `json:"remove_liquidity"` + EditCandidateCommission string `json:"edit_candidate_commission"` + MoveStake string `json:"move_stake"` + MintToken string `json:"mint_token"` + BurnToken string `json:"burn_token"` + VoteCommission string `json:"vote_commission"` + VoteUpdate string `json:"vote_update"` +} + +func (ce *UpdateCommissionsEvent) Type() string { + return TypeUpdateCommissionsEvent +} + +type UpdateNetworkEvent struct { + Version string `json:"version"` +} + +func (un *UpdateNetworkEvent) Type() string { + return TypeUpdateNetworkEvent +} diff --git a/core/minter/blockchain.go b/core/minter/blockchain.go new file mode 100644 index 000000000..a1483740b --- /dev/null +++ b/core/minter/blockchain.go @@ -0,0 +1,486 @@ +package minter + +import ( + "context" + "github.com/MinterTeam/minter-go-node/cmd/utils" + "github.com/MinterTeam/minter-go-node/config" + "github.com/MinterTeam/minter-go-node/core/appdb" + eventsdb "github.com/MinterTeam/minter-go-node/core/events" + "github.com/MinterTeam/minter-go-node/core/rewards" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/candidates" + "github.com/MinterTeam/minter-go-node/core/statistics" + "github.com/MinterTeam/minter-go-node/core/transaction" + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/version" + abciTypes "github.com/tendermint/tendermint/abci/types" + tmjson "github.com/tendermint/tendermint/libs/json" + tmNode "github.com/tendermint/tendermint/node" + "math/big" + "sync" + "sync/atomic" + "time" +) + +// Statuses of validators +const ( + ValidatorPresent = 1 + ValidatorAbsent = 2 +) + +// Block params +const ( + blockMaxBytes = 10000000 + defaultMaxGas = 100000 + minMaxGas = 5000 +) + +const votingPowerConsensus = 2. / 3. + +// Blockchain is a main structure of Minter +type Blockchain struct { + abciTypes.BaseApplication + + statisticData *statistics.Data + + appDB *appdb.AppDB + eventsDB eventsdb.IEventsDB + stateDeliver *state.State + stateCheck *state.CheckState + height uint64 // current Blockchain height + rewards *big.Int // Rewards pool + validatorsStatuses map[types.TmAddress]int8 + validatorsPowers map[types.Pubkey]*big.Int + totalPower *big.Int + + // local rpc client for Tendermint + tmNode *tmNode.Node + + // currentMempool is responsive for prevent sending multiple transactions from one address in one block + currentMempool *sync.Map + + lock sync.RWMutex + + haltHeight uint64 + cfg *config.Config + storages *utils.Storage + stopChan context.Context + stopped bool +} + +// NewMinterBlockchain creates Minter Blockchain instance, should be only called once +func NewMinterBlockchain(storages *utils.Storage, cfg *config.Config, ctx context.Context) *Blockchain { + // Initiate Application DB. Used for persisting data like current block, validators, etc. + applicationDB := appdb.NewAppDB(storages.GetMinterHome(), cfg) + if ctx == nil { + ctx = context.Background() + } + var eventsDB eventsdb.IEventsDB + if !cfg.ValidatorMode { + eventsDB = eventsdb.NewEventsStore(storages.EventDB()) + } else { + eventsDB = &eventsdb.MockEvents{} + } + return &Blockchain{ + appDB: applicationDB, + storages: storages, + eventsDB: eventsDB, + currentMempool: &sync.Map{}, + cfg: cfg, + stopChan: ctx, + haltHeight: uint64(cfg.HaltHeight), + } +} +func (blockchain *Blockchain) initState() { + initialHeight := blockchain.appDB.GetStartHeight() + currentHeight := blockchain.appDB.GetLastHeight() + stateDeliver, err := state.NewState(currentHeight, + blockchain.storages.StateDB(), + blockchain.eventsDB, + blockchain.cfg.StateCacheSize, + blockchain.cfg.KeepLastStates, + initialHeight) + if err != nil { + panic(err) + } + + blockchain.stateDeliver = stateDeliver + blockchain.stateCheck = state.NewCheckState(stateDeliver) + + // Set start height for rewards and validators + rewards.SetStartHeight(initialHeight) +} + +// InitChain initialize blockchain with validators and other info. Only called once. +func (blockchain *Blockchain) InitChain(req abciTypes.RequestInitChain) abciTypes.ResponseInitChain { + var genesisState types.AppState + if err := tmjson.Unmarshal(req.AppStateBytes, &genesisState); err != nil { + panic(err) + } + + blockchain.appDB.SetStartHeight(uint64(req.InitialHeight)) + blockchain.initState() + + if err := blockchain.stateDeliver.Import(genesisState, uint64(req.InitialHeight)); err != nil { + panic(err) + } + _, err := blockchain.stateDeliver.Commit() + if err != nil { + panic(err) + } + + vals := blockchain.updateValidators() + blockchain.appDB.FlushValidators() + + return abciTypes.ResponseInitChain{ + Validators: vals, + } +} + +// BeginBlock signals the beginning of a block. +func (blockchain *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.ResponseBeginBlock { + height := uint64(req.Header.Height) + if blockchain.stateDeliver == nil { + blockchain.initState() + } + blockchain.StatisticData().PushStartBlock(&statistics.StartRequest{Height: int64(height), Now: time.Now(), HeaderTime: req.Header.Time}) + blockchain.stateDeliver.Lock() + + // compute max gas + maxGas := blockchain.calcMaxGas(height) + blockchain.stateDeliver.App.SetMaxGas(maxGas) + + atomic.StoreUint64(&blockchain.height, height) + blockchain.rewards = big.NewInt(0) + + // clear absent candidates + blockchain.lock.Lock() + blockchain.validatorsStatuses = map[types.TmAddress]int8{} + + // give penalty to absent validators + for _, v := range req.LastCommitInfo.Votes { + var address types.TmAddress + copy(address[:], v.Validator.Address) + + if v.SignedLastBlock { + blockchain.stateDeliver.Validators.SetValidatorPresent(height, address) + blockchain.validatorsStatuses[address] = ValidatorPresent + } else { + blockchain.stateDeliver.Validators.SetValidatorAbsent(height, address) + blockchain.validatorsStatuses[address] = ValidatorAbsent + } + } + blockchain.lock.Unlock() + + blockchain.calculatePowers(blockchain.stateDeliver.Validators.GetValidators()) + + if blockchain.isApplicationHalted(height) { + blockchain.stop() + return abciTypes.ResponseBeginBlock{} + } + + // give penalty to Byzantine validators + for _, byzVal := range req.ByzantineValidators { + var address types.TmAddress + copy(address[:], byzVal.Validator.Address) + + // skip already offline candidates to prevent double punishing + candidate := blockchain.stateDeliver.Candidates.GetCandidateByTendermintAddress(address) + if candidate == nil || candidate.Status == candidates.CandidateStatusOffline || blockchain.stateDeliver.Validators.GetByTmAddress(address) == nil { + continue + } + + blockchain.stateDeliver.FrozenFunds.PunishFrozenFundsWithID(height, height+types.GetUnbondPeriod(), candidate.ID) + blockchain.stateDeliver.Validators.PunishByzantineValidator(address) + blockchain.stateDeliver.Candidates.PunishByzantineCandidate(height, address) + } + + // apply frozen funds (used for unbond stakes) + frozenFunds := blockchain.stateDeliver.FrozenFunds.GetFrozenFunds(uint64(req.Header.Height)) + if frozenFunds != nil { + for _, item := range frozenFunds.List { + amount := item.Value + if item.MoveToCandidate == nil { + blockchain.eventsDB.AddEvent(&eventsdb.UnbondEvent{ + Address: item.Address, + Amount: amount.String(), + Coin: uint64(item.Coin), + ValidatorPubKey: *item.CandidateKey, + }) + blockchain.stateDeliver.Accounts.AddBalance(item.Address, item.Coin, amount) + } else { + newCandidate := blockchain.stateDeliver.Candidates.PubKey(*item.MoveToCandidate) + value := big.NewInt(0).Set(amount) + if wl := blockchain.stateDeliver.Waitlist.Get(item.Address, newCandidate, item.Coin); wl != nil { + value.Add(value, wl.Value) + blockchain.stateDeliver.Waitlist.Delete(item.Address, newCandidate, item.Coin) + } + var toWaitlist bool + if blockchain.stateDeliver.Candidates.IsDelegatorStakeSufficient(item.Address, newCandidate, item.Coin, value) { + blockchain.stateDeliver.Candidates.Delegate(item.Address, newCandidate, item.Coin, value, big.NewInt(0)) + } else { + blockchain.stateDeliver.Waitlist.AddWaitList(item.Address, newCandidate, item.Coin, value) + toWaitlist = true + } + blockchain.eventsDB.AddEvent(&eventsdb.StakeMoveEvent{ + Address: item.Address, + Amount: amount.String(), + Coin: uint64(item.Coin), + ValidatorPubKey: *item.CandidateKey, + WaitList: toWaitlist, + }) + } + } + + // delete from db + blockchain.stateDeliver.FrozenFunds.Delete(frozenFunds.Height()) + } + + blockchain.stateDeliver.Halts.Delete(height) + + return abciTypes.ResponseBeginBlock{} +} + +// EndBlock signals the end of a block, returns changes to the validator set +func (blockchain *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.ResponseEndBlock { + height := uint64(req.Height) + + vals := blockchain.stateDeliver.Validators.GetValidators() + + hasDroppedValidators := false + for _, val := range vals { + if !val.IsToDrop() { + continue + } + hasDroppedValidators = true + + // Move dropped validator's accum rewards back to pool + blockchain.rewards.Add(blockchain.rewards, val.GetAccumReward()) + val.SetAccumReward(big.NewInt(0)) + } + + blockchain.calculatePowers(vals) + + // accumulate rewards + reward := rewards.GetRewardForBlock(height) + blockchain.stateDeliver.Checker.AddCoinVolume(types.GetBaseCoinID(), reward) + reward.Add(reward, blockchain.rewards) + + // compute remainder to keep total emission consist + remainder := big.NewInt(0).Set(reward) + + for i, val := range vals { + // skip if candidate is not present + if val.IsToDrop() || blockchain.GetValidatorStatus(val.GetAddress()) != ValidatorPresent { + continue + } + + r := big.NewInt(0).Set(reward) + r.Mul(r, val.GetTotalBipStake()) + r.Div(r, blockchain.totalPower) + + remainder.Sub(remainder, r) + vals[i].AddAccumReward(r) + } + + // add remainder to total slashed + blockchain.stateDeliver.App.AddTotalSlashed(remainder) + + // pay rewards + if height%120 == 0 { + blockchain.stateDeliver.Validators.PayRewards() + } + + if prices := blockchain.isUpdateCommissionsBlock(height); len(prices) != 0 { + blockchain.stateDeliver.Commission.SetNewCommissions(prices) + price := blockchain.stateDeliver.Commission.GetCommissions() + blockchain.eventsDB.AddEvent(&eventsdb.UpdateCommissionsEvent{ + Coin: uint64(price.Coin), + PayloadByte: price.PayloadByte.String(), + Send: price.Send.String(), + BuyBancor: price.BuyBancor.String(), + SellBancor: price.SellBancor.String(), + SellAllBancor: price.SellAllBancor.String(), + BuyPoolBase: price.BuyPoolBase.String(), + BuyPoolDelta: price.BuyPoolDelta.String(), + SellPoolBase: price.SellPoolBase.String(), + SellPoolDelta: price.SellPoolDelta.String(), + SellAllPoolBase: price.SellAllPoolBase.String(), + SellAllPoolDelta: price.SellAllPoolDelta.String(), + CreateTicker3: price.CreateTicker3.String(), + CreateTicker4: price.CreateTicker4.String(), + CreateTicker5: price.CreateTicker5.String(), + CreateTicker6: price.CreateTicker6.String(), + CreateTicker7_10: price.CreateTicker7to10.String(), + CreateCoin: price.CreateCoin.String(), + CreateToken: price.CreateToken.String(), + RecreateCoin: price.RecreateCoin.String(), + RecreateToken: price.RecreateToken.String(), + DeclareCandidacy: price.DeclareCandidacy.String(), + Delegate: price.Delegate.String(), + Unbond: price.Unbond.String(), + RedeemCheck: price.RedeemCheck.String(), + SetCandidateOn: price.SetCandidateOn.String(), + SetCandidateOff: price.SetCandidateOff.String(), + CreateMultisig: price.CreateMultisig.String(), + MultisendBase: price.MultisendBase.String(), + MultisendDelta: price.MultisendDelta.String(), + EditCandidate: price.EditCandidate.String(), + SetHaltBlock: price.SetHaltBlock.String(), + EditTickerOwner: price.EditTickerOwner.String(), + EditMultisig: price.EditMultisig.String(), + PriceVote: price.PriceVote.String(), + EditCandidatePublicKey: price.EditCandidatePublicKey.String(), + CreateSwapPool: price.CreateSwapPool.String(), + AddLiquidity: price.AddLiquidity.String(), + RemoveLiquidity: price.RemoveLiquidity.String(), + EditCandidateCommission: price.EditCandidateCommission.String(), + MoveStake: price.MoveStake.String(), + MintToken: price.MintToken.String(), + BurnToken: price.BurnToken.String(), + VoteCommission: price.VoteCommission.String(), + VoteUpdate: price.VoteUpdate.String(), + }) + } + blockchain.stateDeliver.Commission.Delete(height) + + hasChangedPublicKeys := false + if blockchain.stateDeliver.Candidates.IsChangedPublicKeys() { + blockchain.stateDeliver.Candidates.ResetIsChangedPublicKeys() + hasChangedPublicKeys = true + } + + // update validators + var updates []abciTypes.ValidatorUpdate + if height%120 == 0 || hasDroppedValidators || hasChangedPublicKeys { + updates = blockchain.updateValidators() + } + + defer func() { + blockchain.StatisticData().PushEndBlock(&statistics.EndRequest{TimeEnd: time.Now(), Height: int64(blockchain.Height())}) + }() + + return abciTypes.ResponseEndBlock{ + ValidatorUpdates: updates, + ConsensusParamUpdates: &abciTypes.ConsensusParams{ + Block: &abciTypes.BlockParams{ + MaxBytes: blockMaxBytes, + MaxGas: int64(blockchain.stateDeliver.App.GetMaxGas()), + }, + }, + } +} + +// Info return application info. Used for synchronization between Tendermint and Minter +func (blockchain *Blockchain) Info(_ abciTypes.RequestInfo) (resInfo abciTypes.ResponseInfo) { + return abciTypes.ResponseInfo{ + Version: version.Version, + AppVersion: version.AppVer, + LastBlockHeight: int64(blockchain.appDB.GetLastHeight()), + LastBlockAppHash: blockchain.appDB.GetLastBlockHash(), + } +} + +// DeliverTx deliver a tx for full processing +func (blockchain *Blockchain) DeliverTx(req abciTypes.RequestDeliverTx) abciTypes.ResponseDeliverTx { + response := transaction.RunTx(blockchain.stateDeliver, req.Tx, blockchain.rewards, blockchain.Height(), &sync.Map{}, 0) + + return abciTypes.ResponseDeliverTx{ + Code: response.Code, + Data: response.Data, + Log: response.Log, + Info: response.Info, + GasWanted: response.GasWanted, + GasUsed: response.GasUsed, + Events: []abciTypes.Event{ + { + Type: "tags", + Attributes: response.Tags, + }, + }, + } +} + +// CheckTx validates a tx for the mempool +func (blockchain *Blockchain) CheckTx(req abciTypes.RequestCheckTx) abciTypes.ResponseCheckTx { + response := transaction.RunTx(blockchain.CurrentState(), req.Tx, nil, blockchain.height, blockchain.currentMempool, blockchain.MinGasPrice()) + + return abciTypes.ResponseCheckTx{ + Code: response.Code, + Data: response.Data, + Log: response.Log, + Info: response.Info, + GasWanted: response.GasWanted, + GasUsed: response.GasUsed, + Events: []abciTypes.Event{ + { + Type: "tags", + Attributes: response.Tags, + }, + }, + } +} + +// Commit the state and return the application Merkle root hash +func (blockchain *Blockchain) Commit() abciTypes.ResponseCommit { + if err := blockchain.stateDeliver.Check(); err != nil { + panic(err) + } + + // Flush events db + err := blockchain.eventsDB.CommitEvents(uint32(blockchain.Height())) + if err != nil { + panic(err) + } + + // Committing Minter Blockchain state + hash, err := blockchain.stateDeliver.Commit() + if err != nil { + panic(err) + } + + // Persist application hash and height + blockchain.appDB.SetLastBlockHash(hash) + blockchain.appDB.SetLastHeight(blockchain.Height()) + blockchain.appDB.FlushValidators() + blockchain.updateBlocksTimeDelta(blockchain.Height()) + blockchain.stateDeliver.Unlock() + + // Resetting check state to be consistent with current height + blockchain.resetCheckState() // todo: should remove + + // Clear mempool + blockchain.currentMempool = &sync.Map{} + + if blockchain.checkStop() { + return abciTypes.ResponseCommit{Data: hash} + } + + return abciTypes.ResponseCommit{ + Data: hash, + } +} + +// Query Unused method, required by Tendermint +func (blockchain *Blockchain) Query(_ abciTypes.RequestQuery) abciTypes.ResponseQuery { + return abciTypes.ResponseQuery{} +} + +// SetOption Unused method, required by Tendermint +func (blockchain *Blockchain) SetOption(_ abciTypes.RequestSetOption) abciTypes.ResponseSetOption { + return abciTypes.ResponseSetOption{} +} + +// Close closes db connections +func (blockchain *Blockchain) Close() error { + if err := blockchain.appDB.Close(); err != nil { + return err + } + if err := blockchain.storages.StateDB().Close(); err != nil { + return err + } + if err := blockchain.storages.EventDB().Close(); err != nil { + return err + } + return nil +} diff --git a/core/minter/minter.go b/core/minter/minter.go index 542ce1097..dc0f5fca7 100644 --- a/core/minter/minter.go +++ b/core/minter/minter.go @@ -1,336 +1,94 @@ package minter import ( - "bytes" "fmt" - "github.com/MinterTeam/minter-go-node/cmd/utils" - "github.com/MinterTeam/minter-go-node/config" - "github.com/MinterTeam/minter-go-node/core/appdb" eventsdb "github.com/MinterTeam/minter-go-node/core/events" - "github.com/MinterTeam/minter-go-node/core/rewards" "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/core/state/candidates" + validators2 "github.com/MinterTeam/minter-go-node/core/state/validators" "github.com/MinterTeam/minter-go-node/core/statistics" - "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/core/validators" - "github.com/MinterTeam/minter-go-node/version" "github.com/syndtr/goleveldb/leveldb/filter" "github.com/syndtr/goleveldb/leveldb/opt" - "github.com/tendermint/go-amino" abciTypes "github.com/tendermint/tendermint/abci/types" tmNode "github.com/tendermint/tendermint/node" - "github.com/tendermint/tm-db" + "log" "math/big" "sort" - "sync" "sync/atomic" - "time" ) -// Statuses of validators -const ( - ValidatorPresent = 1 - ValidatorAbsent = 2 -) - -// Block params -const ( - blockMaxBytes = 10000000 - defaultMaxGas = 100000 - minMaxGas = 5000 -) - -const votingPowerConsensus = 2. / 3. - -var ( - blockchain *Blockchain -) - -// Blockchain is a main structure of Minter -type Blockchain struct { - abciTypes.BaseApplication - - statisticData *statistics.Data - - stateDB db.DB - appDB *appdb.AppDB - eventsDB eventsdb.IEventsDB - stateDeliver *state.State - stateCheck *state.CheckState - height uint64 // current Blockchain height - rewards *big.Int // Rewards pool - validatorsStatuses map[types.TmAddress]int8 - - // local rpc client for Tendermint - tmNode *tmNode.Node - - // currentMempool is responsive for prevent sending multiple transactions from one address in one block - currentMempool *sync.Map - - lock sync.RWMutex - - haltHeight uint64 - cfg *config.Config -} - -// NewMinterBlockchain creates Minter Blockchain instance, should be only called once -func NewMinterBlockchain(cfg *config.Config) *Blockchain { - var err error - - ldb, err := db.NewGoLevelDBWithOpts("state", utils.GetMinterHome()+"/data", getDbOpts(cfg.StateMemAvailable)) - if err != nil { - panic(err) - } - - // Initiate Application DB. Used for persisting data like current block, validators, etc. - applicationDB := appdb.NewAppDB(cfg) - - edb, err := db.NewGoLevelDBWithOpts("events", utils.GetMinterHome()+"/data", getDbOpts(1024)) - if err != nil { - panic(err) - } - - blockchain = &Blockchain{ - stateDB: ldb, - appDB: applicationDB, - height: applicationDB.GetLastHeight(), - eventsDB: eventsdb.NewEventsStore(edb), - currentMempool: &sync.Map{}, - cfg: cfg, - } - - // Set stateDeliver and stateCheck - blockchain.stateDeliver, err = state.NewState(blockchain.height, blockchain.stateDB, blockchain.eventsDB, cfg.StateCacheSize, cfg.KeepLastStates) - if err != nil { - panic(err) +func (blockchain *Blockchain) checkStop() bool { + if !blockchain.stopped { + select { + case <-blockchain.stopChan.Done(): + blockchain.stop() + default: + } } - - blockchain.stateCheck = state.NewCheckState(blockchain.stateDeliver) - - // Set start height for rewards and validators - rewards.SetStartHeight(applicationDB.GetStartHeight()) - - blockchain.haltHeight = uint64(cfg.HaltHeight) - - return blockchain + return blockchain.stopped } -// InitChain initialize blockchain with validators and other info. Only called once. -func (app *Blockchain) InitChain(req abciTypes.RequestInitChain) abciTypes.ResponseInitChain { - var genesisState types.AppState - if err := amino.UnmarshalJSON(req.AppStateBytes, &genesisState); err != nil { - panic(err) - } - - if err := app.stateDeliver.Import(genesisState); err != nil { - panic(err) - } - - vals := app.updateValidators(0) - - app.appDB.SetStartHeight(genesisState.StartHeight) - app.appDB.SaveValidators(vals) - rewards.SetStartHeight(genesisState.StartHeight) - - return abciTypes.ResponseInitChain{ - Validators: vals, - } +func (blockchain *Blockchain) stop() { + blockchain.stopped = true + go func() { + log.Println("Stopping Node") + log.Println("Node Stopped with error:", blockchain.tmNode.Stop()) + }() } -// BeginBlock signals the beginning of a block. -func (app *Blockchain) BeginBlock(req abciTypes.RequestBeginBlock) abciTypes.ResponseBeginBlock { - height := uint64(req.Header.Height) - - app.StatisticData().PushStartBlock(&statistics.StartRequest{Height: int64(height), Now: time.Now(), HeaderTime: req.Header.Time}) - - app.stateDeliver.Lock() - - // compute max gas - app.updateBlocksTimeDelta(height, 3) - maxGas := app.calcMaxGas(height) - app.stateDeliver.App.SetMaxGas(maxGas) - - atomic.StoreUint64(&app.height, height) - app.rewards = big.NewInt(0) - - // clear absent candidates - app.lock.Lock() - app.validatorsStatuses = map[types.TmAddress]int8{} - - // give penalty to absent validators - for _, v := range req.LastCommitInfo.Votes { - var address types.TmAddress - copy(address[:], v.Validator.Address) - - if v.SignedLastBlock { - app.stateDeliver.Validators.SetValidatorPresent(height, address) - app.validatorsStatuses[address] = ValidatorPresent - } else { - app.stateDeliver.Validators.SetValidatorAbsent(height, address) - app.validatorsStatuses[address] = ValidatorAbsent - } - } - app.lock.Unlock() - - if app.isApplicationHalted(height) { - panic(fmt.Sprintf("Application halted at height %d", height)) - } - - // give penalty to Byzantine validators - for _, byzVal := range req.ByzantineValidators { - var address types.TmAddress - copy(address[:], byzVal.Validator.Address) - - // skip already offline candidates to prevent double punishing - candidate := app.stateDeliver.Candidates.GetCandidateByTendermintAddress(address) - if candidate == nil || candidate.Status == candidates.CandidateStatusOffline || app.stateDeliver.Validators.GetByTmAddress(address) == nil { - continue - } - - app.stateDeliver.FrozenFunds.PunishFrozenFundsWithID(height, height+candidates.UnbondPeriod, candidate.ID) - app.stateDeliver.Validators.PunishByzantineValidator(address) - app.stateDeliver.Candidates.PunishByzantineCandidate(height, address) - } - - // apply frozen funds (used for unbond stakes) - frozenFunds := app.stateDeliver.FrozenFunds.GetFrozenFunds(uint64(req.Header.Height)) - if frozenFunds != nil { - for _, item := range frozenFunds.List { - app.eventsDB.AddEvent(uint32(req.Header.Height), &eventsdb.UnbondEvent{ - Address: item.Address, - Amount: item.Value.String(), - Coin: uint64(item.Coin), - ValidatorPubKey: *item.CandidateKey, - }) - app.stateDeliver.Accounts.AddBalance(item.Address, item.Coin, item.Value) - } - - // delete from db - app.stateDeliver.FrozenFunds.Delete(frozenFunds.Height()) - } - - app.stateDeliver.Halts.Delete(height) - - return abciTypes.ResponseBeginBlock{} +// Stop gracefully stopping Minter Blockchain instance +func (blockchain *Blockchain) WaitStop() error { + blockchain.tmNode.Wait() + return blockchain.Close() } -// EndBlock signals the end of a block, returns changes to the validator set -func (app *Blockchain) EndBlock(req abciTypes.RequestEndBlock) abciTypes.ResponseEndBlock { - height := uint64(req.Height) - - vals := app.stateDeliver.Validators.GetValidators() - - hasDroppedValidators := false - for _, val := range vals { - if val.IsToDrop() { - hasDroppedValidators = true - - // Move dropped validator's accum rewards back to pool - app.rewards.Add(app.rewards, val.GetAccumReward()) - val.SetAccumReward(big.NewInt(0)) - break - } - } - - // calculate total power of validators - totalPower := big.NewInt(0) +// calculatePowers calculates total power of validators +func (blockchain *Blockchain) calculatePowers(vals []*validators2.Validator) { + blockchain.validatorsPowers = map[types.Pubkey]*big.Int{} + blockchain.totalPower = big.NewInt(0) for _, val := range vals { // skip if candidate is not present - if val.IsToDrop() || app.GetValidatorStatus(val.GetAddress()) != ValidatorPresent { - continue - } - - totalPower.Add(totalPower, val.GetTotalBipStake()) - } - - if totalPower.Cmp(types.Big0) == 0 { - totalPower = big.NewInt(1) - } - - // accumulate rewards - reward := rewards.GetRewardForBlock(height) - app.stateDeliver.Checker.AddCoinVolume(types.GetBaseCoinID(), reward) - reward.Add(reward, app.rewards) - - // compute remainder to keep total emission consist - remainder := big.NewInt(0).Set(reward) - - for i, val := range vals { - // skip if candidate is not present - if val.IsToDrop() || app.GetValidatorStatus(val.GetAddress()) != ValidatorPresent { + if val.IsToDrop() || blockchain.GetValidatorStatus(val.GetAddress()) != ValidatorPresent { continue } - r := big.NewInt(0).Set(reward) - r.Mul(r, val.GetTotalBipStake()) - r.Div(r, totalPower) - - remainder.Sub(remainder, r) - vals[i].AddAccumReward(r) - } - - // add remainder to total slashed - app.stateDeliver.App.AddTotalSlashed(remainder) - - // pay rewards - if req.Height%120 == 0 { - app.stateDeliver.Validators.PayRewards(height) - } - - hasChangedPublicKeys := false - if app.stateDeliver.Candidates.IsChangedPublicKeys() { - app.stateDeliver.Candidates.ResetIsChangedPublicKeys() - hasChangedPublicKeys = true + blockchain.validatorsPowers[val.PubKey] = val.GetTotalBipStake() + blockchain.totalPower.Add(blockchain.totalPower, val.GetTotalBipStake()) } - // update validators - var updates []abciTypes.ValidatorUpdate - if req.Height%120 == 0 || hasDroppedValidators || hasChangedPublicKeys { - updates = app.updateValidators(height) - } - - defer func() { - app.StatisticData().PushEndBlock(&statistics.EndRequest{TimeEnd: time.Now(), Height: int64(app.height)}) - }() - - return abciTypes.ResponseEndBlock{ - ValidatorUpdates: updates, - ConsensusParamUpdates: &abciTypes.ConsensusParams{ - Block: &abciTypes.BlockParams{ - MaxBytes: blockMaxBytes, - MaxGas: int64(app.stateDeliver.App.GetMaxGas()), - }, - }, + if blockchain.totalPower.Sign() == 0 { + blockchain.totalPower = big.NewInt(1) } } -func (app *Blockchain) updateValidators(height uint64) []abciTypes.ValidatorUpdate { - app.stateDeliver.Candidates.RecalculateStakes(height) +func (blockchain *Blockchain) updateValidators() []abciTypes.ValidatorUpdate { + height := blockchain.Height() + blockchain.stateDeliver.Candidates.RecalculateStakes(height) valsCount := validators.GetValidatorsCountForBlock(height) - newCandidates := app.stateDeliver.Candidates.GetNewCandidates(valsCount) + newCandidates := blockchain.stateDeliver.Candidates.GetNewCandidates(valsCount) if len(newCandidates) < valsCount { valsCount = len(newCandidates) } - newValidators := make([]abciTypes.ValidatorUpdate, valsCount) + newValidators := make([]abciTypes.ValidatorUpdate, 0, valsCount) // calculate total power totalPower := big.NewInt(0) for _, candidate := range newCandidates { - totalPower.Add(totalPower, app.stateDeliver.Candidates.GetTotalStake(candidate.PubKey)) + totalPower.Add(totalPower, blockchain.stateDeliver.Candidates.GetTotalStake(candidate.PubKey)) } - for i := range newCandidates { - power := big.NewInt(0).Div(big.NewInt(0).Mul(app.stateDeliver.Candidates.GetTotalStake(newCandidates[i].PubKey), + for _, newCandidate := range newCandidates { + power := big.NewInt(0).Div(big.NewInt(0).Mul(blockchain.stateDeliver.Candidates.GetTotalStake(newCandidate.PubKey), big.NewInt(100000000)), totalPower).Int64() if power == 0 { power = 1 } - newValidators[i] = abciTypes.Ed25519ValidatorUpdate(newCandidates[i].PubKey[:], power) + newValidators = append(newValidators, abciTypes.Ed25519ValidatorUpdate(newCandidate.PubKey.Bytes(), power)) } sort.SliceStable(newValidators, func(i, j int) bool { @@ -338,18 +96,18 @@ func (app *Blockchain) updateValidators(height uint64) []abciTypes.ValidatorUpda }) // update validators in state - app.stateDeliver.Validators.SetNewValidators(newCandidates) + blockchain.stateDeliver.Validators.SetNewValidators(newCandidates) - activeValidators := app.getCurrentValidators() + activeValidators := blockchain.appDB.GetValidators() - app.saveCurrentValidators(newValidators) + blockchain.appDB.SetValidators(newValidators) updates := newValidators for _, validator := range activeValidators { persisted := false for _, newValidator := range newValidators { - if bytes.Equal(validator.PubKey.Data, newValidator.PubKey.Data) { + if validator.PubKey.Sum.Compare(newValidator.PubKey.Sum) == 0 { persisted = true break } @@ -366,121 +124,26 @@ func (app *Blockchain) updateValidators(height uint64) []abciTypes.ValidatorUpda return updates } -// Info return application info. Used for synchronization between Tendermint and Minter -func (app *Blockchain) Info(req abciTypes.RequestInfo) (resInfo abciTypes.ResponseInfo) { - return abciTypes.ResponseInfo{ - Version: version.Version, - AppVersion: version.AppVer, - LastBlockHeight: int64(app.appDB.GetLastHeight()), - LastBlockAppHash: app.appDB.GetLastBlockHash(), - } -} - -// DeliverTx deliver a tx for full processing -func (app *Blockchain) DeliverTx(req abciTypes.RequestDeliverTx) abciTypes.ResponseDeliverTx { - response := transaction.RunTx(app.stateDeliver, req.Tx, app.rewards, app.height, &sync.Map{}, 0) - - return abciTypes.ResponseDeliverTx{ - Code: response.Code, - Data: response.Data, - Log: response.Log, - Info: response.Info, - GasWanted: response.GasWanted, - GasUsed: response.GasUsed, - Events: []abciTypes.Event{ - { - Type: "tags", - Attributes: response.Tags, - }, - }, - } -} - -// CheckTx validates a tx for the mempool -func (app *Blockchain) CheckTx(req abciTypes.RequestCheckTx) abciTypes.ResponseCheckTx { - response := transaction.RunTx(app.stateCheck, req.Tx, nil, app.height, app.currentMempool, app.MinGasPrice()) - - return abciTypes.ResponseCheckTx{ - Code: response.Code, - Data: response.Data, - Log: response.Log, - Info: response.Info, - GasWanted: response.GasWanted, - GasUsed: response.GasUsed, - Events: []abciTypes.Event{ - { - Type: "tags", - Attributes: response.Tags, - }, - }, - } -} - -// Commit the state and return the application Merkle root hash -func (app *Blockchain) Commit() abciTypes.ResponseCommit { - if err := app.stateDeliver.Check(); err != nil { - panic(err) - } - - // Committing Minter Blockchain state - hash, err := app.stateDeliver.Commit() - if err != nil { - panic(err) - } - - // Flush events db - err = app.eventsDB.CommitEvents() - if err != nil { - panic(err) - } - - // Persist application hash and height - app.appDB.SetLastBlockHash(hash) - app.appDB.SetLastHeight(app.height) - - app.stateDeliver.Unlock() - - // Resetting check state to be consistent with current height - app.resetCheckState() - - // Clear mempool - app.currentMempool = &sync.Map{} - - return abciTypes.ResponseCommit{ - Data: hash, - } -} - -// Query Unused method, required by Tendermint -func (app *Blockchain) Query(reqQuery abciTypes.RequestQuery) abciTypes.ResponseQuery { - return abciTypes.ResponseQuery{} -} - -// SetOption Unused method, required by Tendermint -func (app *Blockchain) SetOption(req abciTypes.RequestSetOption) abciTypes.ResponseSetOption { - return abciTypes.ResponseSetOption{} -} +// CurrentState returns immutable state of Minter Blockchain +func (blockchain *Blockchain) CurrentState() *state.CheckState { + blockchain.lock.RLock() + defer blockchain.lock.RUnlock() -// Stop gracefully stopping Minter Blockchain instance -func (app *Blockchain) Stop() { - app.appDB.Close() - if err := app.stateDB.Close(); err != nil { - panic(err) - } + return blockchain.stateCheck } -// CurrentState returns immutable state of Minter Blockchain -func (app *Blockchain) CurrentState() *state.CheckState { - app.lock.RLock() - defer app.lock.RUnlock() +// AvailableVersions returns all available versions in ascending order +func (blockchain *Blockchain) AvailableVersions() []int { + blockchain.lock.RLock() + defer blockchain.lock.RUnlock() - return app.stateCheck + return blockchain.stateDeliver.Tree().AvailableVersions() } // GetStateForHeight returns immutable state of Minter Blockchain for given height -func (app *Blockchain) GetStateForHeight(height uint64) (*state.CheckState, error) { +func (blockchain *Blockchain) GetStateForHeight(height uint64) (*state.CheckState, error) { if height > 0 { - s, err := state.NewCheckStateAtHeight(height, app.stateDB) + s, err := state.NewCheckStateAtHeight(height, blockchain.storages.StateDB()) if err != nil { return nil, err } @@ -490,18 +153,18 @@ func (app *Blockchain) GetStateForHeight(height uint64) (*state.CheckState, erro } // Height returns current height of Minter Blockchain -func (app *Blockchain) Height() uint64 { - return atomic.LoadUint64(&app.height) +func (blockchain *Blockchain) Height() uint64 { + return atomic.LoadUint64(&blockchain.height) } // SetTmNode sets Tendermint node -func (app *Blockchain) SetTmNode(node *tmNode.Node) { - app.tmNode = node +func (blockchain *Blockchain) SetTmNode(node *tmNode.Node) { + blockchain.tmNode = node } // MinGasPrice returns minimal acceptable gas price -func (app *Blockchain) MinGasPrice() uint32 { - mempoolSize := app.tmNode.Mempool().Size() +func (blockchain *Blockchain) MinGasPrice() uint32 { + mempoolSize := blockchain.tmNode.Mempool().Size() if mempoolSize > 5000 { return 50 @@ -522,64 +185,49 @@ func (app *Blockchain) MinGasPrice() uint32 { return 1 } -func (app *Blockchain) resetCheckState() { - app.lock.Lock() - defer app.lock.Unlock() +func (blockchain *Blockchain) resetCheckState() { + blockchain.lock.Lock() + defer blockchain.lock.Unlock() - app.stateCheck = state.NewCheckState(app.stateDeliver) -} - -func (app *Blockchain) getCurrentValidators() abciTypes.ValidatorUpdates { - return app.appDB.GetValidators() -} - -func (app *Blockchain) saveCurrentValidators(vals abciTypes.ValidatorUpdates) { - app.appDB.SaveValidators(vals) + blockchain.stateCheck = state.NewCheckState(blockchain.stateDeliver) } -func (app *Blockchain) updateBlocksTimeDelta(height uint64, count int64) { +func (blockchain *Blockchain) updateBlocksTimeDelta(height uint64) { // should do this because tmNode is unavailable during Tendermint's replay mode - if app.tmNode == nil { + if blockchain.tmNode == nil { return } - if int64(height)-count-1 < 1 { + blockStore := blockchain.tmNode.BlockStore() + baseMeta := blockStore.LoadBaseMeta() + if baseMeta == nil { + return + } + if int64(height)-1 < baseMeta.Header.Height { return } - blockStore := app.tmNode.BlockStore() - - blockA := blockStore.LoadBlockMeta(int64(height) - count - 1) - blockB := blockStore.LoadBlockMeta(int64(height) - 1) + blockA := blockStore.LoadBlockMeta(int64(height) - 1) + blockB := blockStore.LoadBlockMeta(int64(height)) delta := int(blockB.Header.Time.Sub(blockA.Header.Time).Seconds()) - app.appDB.SetLastBlocksTimeDelta(height, delta) -} - -// SetBlocksTimeDelta sets current blocks time delta -func (app *Blockchain) SetBlocksTimeDelta(height uint64, value int) { - app.appDB.SetLastBlocksTimeDelta(height, value) + blockchain.appDB.AddBlocksTimeDelta(height, delta) } -// GetBlocksTimeDelta returns current blocks time delta -func (app *Blockchain) GetBlocksTimeDelta(height, count uint64) (int, error) { - return app.appDB.GetLastBlocksTimeDelta(height) -} - -func (app *Blockchain) calcMaxGas(height uint64) uint64 { +func (blockchain *Blockchain) calcMaxGas(height uint64) uint64 { const targetTime = 7 - const blockDelta = 3 - // skip first 20 blocks - if height <= 20 { + // check if blocks are created in time + delta, count, err := blockchain.appDB.GetLastBlocksTimeDelta(height) + if err != nil { + log.Println(err) return defaultMaxGas } // get current max gas - newMaxGas := app.stateCheck.App().GetMaxGas() + newMaxGas := blockchain.stateCheck.App().GetMaxGas() - // check if blocks are created in time - if delta, _ := app.GetBlocksTimeDelta(height, blockDelta); delta > targetTime*blockDelta { + if delta > targetTime*count { newMaxGas = newMaxGas * 7 / 10 // decrease by 30% } else { newMaxGas = newMaxGas * 105 / 100 // increase by 5% @@ -599,83 +247,99 @@ func (app *Blockchain) calcMaxGas(height uint64) uint64 { } // GetEventsDB returns current EventsDB -func (app *Blockchain) GetEventsDB() eventsdb.IEventsDB { - return app.eventsDB +func (blockchain *Blockchain) GetEventsDB() eventsdb.IEventsDB { + return blockchain.eventsDB } // SetStatisticData used for collection statistics about blockchain operations -func (app *Blockchain) SetStatisticData(statisticData *statistics.Data) *statistics.Data { - app.statisticData = statisticData - return app.statisticData +func (blockchain *Blockchain) SetStatisticData(statisticData *statistics.Data) *statistics.Data { + blockchain.statisticData = statisticData + return blockchain.statisticData } // StatisticData used for collection statistics about blockchain operations -func (app *Blockchain) StatisticData() *statistics.Data { - return app.statisticData +func (blockchain *Blockchain) StatisticData() *statistics.Data { + return blockchain.statisticData } // GetValidatorStatus returns given validator's status -func (app *Blockchain) GetValidatorStatus(address types.TmAddress) int8 { - app.lock.RLock() - defer app.lock.RUnlock() +func (blockchain *Blockchain) GetValidatorStatus(address types.TmAddress) int8 { + blockchain.lock.RLock() + defer blockchain.lock.RUnlock() - return app.validatorsStatuses[address] + return blockchain.validatorsStatuses[address] } // DeleteStateVersions deletes states in given range -func (app *Blockchain) DeleteStateVersions(from, to int64) error { - app.lock.RLock() - defer app.lock.RUnlock() +func (blockchain *Blockchain) DeleteStateVersions(from, to int64) error { + blockchain.lock.RLock() + defer blockchain.lock.RUnlock() - app.stateDeliver.Tree().GlobalLock() - defer app.stateDeliver.Tree().GlobalUnlock() - - return app.stateDeliver.Tree().DeleteVersionsRange(from, to) + return blockchain.stateDeliver.Tree().DeleteVersionsRange(from, to) } -func (app *Blockchain) isApplicationHalted(height uint64) bool { - if app.haltHeight > 0 && height >= app.haltHeight { +func (blockchain *Blockchain) isApplicationHalted(height uint64) bool { + if blockchain.haltHeight > 0 && height >= blockchain.haltHeight { return true } - halts := app.stateDeliver.Halts.GetHaltBlocks(height) - if halts != nil { - // calculate total power of validators - vals := app.stateDeliver.Validators.GetValidators() - totalPower, totalVotedPower := big.NewInt(0), big.NewInt(0) - for _, val := range vals { - // skip if candidate is not present - if val.IsToDrop() || app.validatorsStatuses[val.GetAddress()] != ValidatorPresent { - continue - } - - for _, halt := range halts.List { - if halt.Pubkey == val.PubKey { - totalVotedPower.Add(totalVotedPower, val.GetTotalBipStake()) - } - } + halts := blockchain.stateDeliver.Halts.GetHaltBlocks(height) + if halts == nil { + return false + } - totalPower.Add(totalPower, val.GetTotalBipStake()) + totalVotedPower := big.NewInt(0) + for _, halt := range halts.List { + if power, ok := blockchain.validatorsPowers[halt.Pubkey]; ok { + totalVotedPower.Add(totalVotedPower, power) } + } - if totalPower.Cmp(types.Big0) == 0 { - totalPower = big.NewInt(1) - } + votingResult := new(big.Float).Quo( + new(big.Float).SetInt(totalVotedPower), + new(big.Float).SetInt(blockchain.totalPower), + ) + + if votingResult.Cmp(big.NewFloat(votingPowerConsensus)) == 1 { + return true + } + return false +} + +func (blockchain *Blockchain) isUpdateCommissionsBlock(height uint64) []byte { + commissions := blockchain.stateDeliver.Commission.GetVotes(height) + if len(commissions) == 0 { + return nil + } + // calculate total power of validators + maxVotingResult, totalVotedPower := big.NewFloat(0), big.NewInt(0) + + var price string + for _, commission := range commissions { + for _, vote := range commission.Votes { + if power, ok := blockchain.validatorsPowers[vote]; ok { + totalVotedPower.Add(totalVotedPower, power) + } + } votingResult := new(big.Float).Quo( new(big.Float).SetInt(totalVotedPower), - new(big.Float).SetInt(totalPower), + new(big.Float).SetInt(blockchain.totalPower), ) - if votingResult.Cmp(big.NewFloat(votingPowerConsensus)) == 1 { - return true + if maxVotingResult.Cmp(votingResult) == -1 { + maxVotingResult = votingResult + price = commission.Price } } + if maxVotingResult.Cmp(big.NewFloat(votingPowerConsensus)) == 1 { + return []byte(price) + } - return false + return nil } -func getDbOpts(memLimit int) *opt.Options { +func GetDbOpts(memLimit int) *opt.Options { if memLimit < 1024 { panic(fmt.Sprintf("Not enough memory given to StateDB. Expected >1024M, given %d", memLimit)) } diff --git a/core/minter/minter_test.go b/core/minter/minter_test.go index a91e3d640..a7d275f66 100644 --- a/core/minter/minter_test.go +++ b/core/minter/minter_test.go @@ -3,7 +3,6 @@ package minter import ( "context" "crypto/ecdsa" - "encoding/base64" "encoding/hex" "encoding/json" "fmt" @@ -11,7 +10,7 @@ import ( "github.com/MinterTeam/minter-go-node/config" "github.com/MinterTeam/minter-go-node/core/developers" eventsdb "github.com/MinterTeam/minter-go-node/core/events" - candidates2 "github.com/MinterTeam/minter-go-node/core/state/candidates" + "github.com/MinterTeam/minter-go-node/core/state/candidates" "github.com/MinterTeam/minter-go-node/core/statistics" "github.com/MinterTeam/minter-go-node/core/transaction" "github.com/MinterTeam/minter-go-node/core/types" @@ -19,138 +18,23 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/log" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/tendermint/go-amino" - log2 "github.com/tendermint/tendermint/libs/log" - tmos "github.com/tendermint/tendermint/libs/os" + tmjson "github.com/tendermint/tendermint/libs/json" + tmnet "github.com/tendermint/tendermint/libs/net" tmNode "github.com/tendermint/tendermint/node" "github.com/tendermint/tendermint/p2p" + p2pmock "github.com/tendermint/tendermint/p2p/mock" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" rpc "github.com/tendermint/tendermint/rpc/client/local" types2 "github.com/tendermint/tendermint/types" "math/big" - "math/rand" - "os" - "strconv" "testing" "time" ) -func getPrivateKey() *ecdsa.PrivateKey { - b, _ := hex.DecodeString("825ca965c34ef1c8343e8e377959108370c23ba6194d858452b63432456403f9") - privateKey, _ := crypto.ToECDSA(b) - return privateKey -} - -func makeTestValidatorsAndCandidates(pubkeys []string, stake *big.Int) ([]types.Validator, []types.Candidate) { - vals := make([]types.Validator, 0, len(pubkeys)) - cands := make([]types.Candidate, 0, len(pubkeys)) - - for i, val := range pubkeys { - pkeyBytes, err := base64.StdEncoding.DecodeString(val) - if err != nil { - panic(err) - } - - var pkey types.Pubkey - copy(pkey[:], pkeyBytes) - addr := developers.Address - - vals = append(vals, types.Validator{ - TotalBipStake: stake.String(), - PubKey: pkey, - AccumReward: big.NewInt(0).String(), - AbsentTimes: types.NewBitArray(24), - }) - - cands = append(cands, types.Candidate{ - ID: uint64(i) + 1, - RewardAddress: addr, - OwnerAddress: crypto.PubkeyToAddress(getPrivateKey().PublicKey), - ControlAddress: addr, - TotalBipStake: stake.String(), - PubKey: pkey, - Commission: 10, - Stakes: []types.Stake{ - { - Owner: addr, - Coin: uint64(types.GetBaseCoinID()), - Value: stake.String(), - BipValue: stake.String(), - }, - }, - Status: candidates2.CandidateStatusOnline, - }) - } - - return vals, cands -} - -func getTestGenesis(pv *privval.FilePV) func() (*types2.GenesisDoc, error) { - return func() (*types2.GenesisDoc, error) { - - appHash := [32]byte{} - - validators, candidates := makeTestValidatorsAndCandidates([]string{base64.StdEncoding.EncodeToString(pv.Key.PubKey.Bytes()[5:])}, helpers.BipToPip(big.NewInt(12444011))) - - appState := types.AppState{ - TotalSlashed: "0", - Accounts: []types.Account{ - { - Address: crypto.PubkeyToAddress(getPrivateKey().PublicKey), - Balance: []types.Balance{ - { - Coin: uint64(types.GetBaseCoinID()), - Value: helpers.BipToPip(big.NewInt(9223372036854775807)).String(), - }, - }, - }, - }, - Validators: validators, - Candidates: candidates, - } - - appStateJSON, err := amino.MarshalJSON(appState) - if err != nil { - return nil, err - } - - genesisDoc := types2.GenesisDoc{ - ChainID: "minter-test-network", - GenesisTime: time.Now(), - AppHash: appHash[:], - AppState: json.RawMessage(appStateJSON), - } - - err = genesisDoc.ValidateAndComplete() - if err != nil { - return nil, err - } - - genesisFile := utils.GetMinterHome() + "/config/genesis.json" - if err := genesisDoc.SaveAs(genesisFile); err != nil { - panic(err) - } - - return &genesisDoc, nil - } -} - -var port = 0 - -func getPort() string { - port++ - return strconv.Itoa(port) -} - -func initTestNode(t *testing.T) (*Blockchain, *rpc.Local, *privval.FilePV) { - utils.MinterHome = t.TempDir() - - if err := tmos.EnsureDir(utils.GetMinterHome()+"/tmdata/blockstore.db", 0777); err != nil { - t.Fatal(err) - } - - minterCfg := config.GetConfig() +func initTestNode(t *testing.T, initialHeight int64) (*Blockchain, *rpc.Local, *privval.FilePV, func()) { + storage := utils.NewStorage(t.TempDir(), "") + minterCfg := config.GetConfig(storage.GetMinterHome()) logger := log.NewLogger(minterCfg) cfg := config.GetTmConfig(minterCfg) cfg.Consensus.TimeoutPropose = 0 @@ -162,7 +46,7 @@ func initTestNode(t *testing.T) (*Blockchain, *rpc.Local, *privval.FilePV) { cfg.Consensus.TimeoutProposeDelta = 0 cfg.Consensus.SkipTimeoutCommit = true cfg.RPC.ListenAddress = "" - cfg.P2P.ListenAddress = "0.0.0.0:2556" + getPort() // todo + cfg.P2P.ListenAddress = fmt.Sprintf("tcp://127.0.0.1:%d", getPort()) cfg.P2P.Seeds = "" cfg.P2P.PersistentPeers = "" cfg.DBBackend = "memdb" @@ -170,7 +54,9 @@ func initTestNode(t *testing.T) (*Blockchain, *rpc.Local, *privval.FilePV) { pv := privval.GenFilePV(cfg.PrivValidatorKeyFile(), cfg.PrivValidatorStateFile()) pv.Save() - app := NewMinterBlockchain(minterCfg) + ctx, cancelFunc := context.WithCancel(context.Background()) + + app := NewMinterBlockchain(storage, minterCfg, ctx) nodeKey, err := p2p.LoadOrGenNodeKey(cfg.NodeKeyFile()) if err != nil { t.Fatal(err) @@ -181,26 +67,29 @@ func initTestNode(t *testing.T) (*Blockchain, *rpc.Local, *privval.FilePV) { pv, nodeKey, proxy.NewLocalClientCreator(app), - getTestGenesis(pv), + getTestGenesis(pv, storage.GetMinterHome(), initialHeight), tmNode.DefaultDBProvider, tmNode.DefaultMetricsProvider(cfg.Instrumentation), - log2.NewTMLogger(os.Stdout), + logger, + tmNode.CustomReactors(map[string]p2p.Reactor{ + // "PEX": p2pmock.NewReactor(), + "BLOCKCHAIN": p2pmock.NewReactor(), + }), ) - if err != nil { t.Fatal(fmt.Sprintf("Failed to create a node: %v", err)) } - if err = node.Start(); err != nil { + // logger.Info("Started node", "nodeInfo", node.Switch().NodeInfo()) + app.SetTmNode(node) + + if err = app.tmNode.Start(); err != nil { t.Fatal(fmt.Sprintf("Failed to start node: %v", err)) } - logger.Info("Started node", "nodeInfo", node.Switch().NodeInfo()) - app.SetTmNode(node) - - tmCli := rpc.New(blockchain.tmNode) + tmCli := rpc.New(app.tmNode) - blocks, err := tmCli.Subscribe(context.Background(), "test-client", "tm.event = 'NewBlock'") + blocks, err := tmCli.Subscribe(context.Background(), "test-client", types2.EventQueryNewBlock.String()) if err != nil { t.Fatal(err) } @@ -215,18 +104,260 @@ func initTestNode(t *testing.T) (*Blockchain, *rpc.Local, *privval.FilePV) { t.Fatal("Timeout waiting for the first block") } - return app, tmCli, pv + return app, tmCli, pv, func() { + cancelFunc() + if err := app.WaitStop(); err != nil { + t.Error(err) + } + } +} + +func TestBlockchain_UpdateCommission(t *testing.T) { + blockchain, tmCli, pv, cancel := initTestNode(t, 100) + defer cancel() + + txs, err := tmCli.Subscribe(context.Background(), "test-client", "tm.event = 'Tx'") + if err != nil { + t.Fatal(err) + } + + data := transaction.VoteCommissionData{ + PubKey: types.BytesToPubkey(pv.Key.PubKey.Bytes()[:]), + Height: 110, + Coin: types.GetBaseCoinID(), + PayloadByte: helpers.StringToBigInt("200000000000000000"), + Send: helpers.StringToBigInt("1000000000000000000"), + BuyBancor: helpers.StringToBigInt("10000000000000000000"), + SellBancor: helpers.StringToBigInt("10000000000000000000"), + SellAllBancor: helpers.StringToBigInt("10000000000000000000"), + BuyPoolBase: helpers.StringToBigInt("10000000000000000000"), + BuyPoolDelta: helpers.StringToBigInt("5000000000000000000"), + SellPoolBase: helpers.StringToBigInt("10000000000000000000"), + SellPoolDelta: helpers.StringToBigInt("5000000000000000000"), + SellAllPoolBase: helpers.StringToBigInt("10000000000000000000"), + SellAllPoolDelta: helpers.StringToBigInt("5000000000000000000"), + CreateTicker3: helpers.StringToBigInt("100000000000000000000000000"), + CreateTicker4: helpers.StringToBigInt("10000000000000000000000000"), + CreateTicker5: helpers.StringToBigInt("1000000000000000000000000"), + CreateTicker6: helpers.StringToBigInt("100000000000000000000000"), + CreateTicker7to10: helpers.StringToBigInt("10000000000000000000000"), + CreateCoin: helpers.StringToBigInt("0"), + CreateToken: helpers.StringToBigInt("0"), + RecreateCoin: helpers.StringToBigInt("1000000000000000000000000"), + RecreateToken: helpers.StringToBigInt("1000000000000000000000000"), + DeclareCandidacy: helpers.StringToBigInt("1000000000000000000000"), + Delegate: helpers.StringToBigInt("20000000000000000000"), + Unbond: helpers.StringToBigInt("20000000000000000000"), + RedeemCheck: helpers.StringToBigInt("3000000000000000000"), + SetCandidateOn: helpers.StringToBigInt("10000000000000000000"), + SetCandidateOff: helpers.StringToBigInt("10000000000000000000"), + CreateMultisig: helpers.StringToBigInt("10000000000000000000"), + MultisendBase: helpers.StringToBigInt("1000000000000000000"), + MultisendDelta: helpers.StringToBigInt("500000000000000000"), + EditCandidate: helpers.StringToBigInt("1000000000000000000000"), + SetHaltBlock: helpers.StringToBigInt("100000000000000000000"), + EditTickerOwner: helpers.StringToBigInt("1000000000000000000000000"), + EditMultisig: helpers.StringToBigInt("100000000000000000000"), + PriceVote: helpers.StringToBigInt("1000000000000000000"), + EditCandidatePublicKey: helpers.StringToBigInt("10000000000000000000000000"), + CreateSwapPool: helpers.StringToBigInt("100000000000000000000"), + AddLiquidity: helpers.StringToBigInt("10000000000000000000"), + RemoveLiquidity: helpers.StringToBigInt("10000000000000000000"), + EditCandidateCommission: helpers.StringToBigInt("1000000000000000000000"), + MoveStake: helpers.StringToBigInt("20000000000000000000"), + MintToken: helpers.StringToBigInt("10000000000000000000"), + BurnToken: helpers.StringToBigInt("10000000000000000000"), + VoteCommission: helpers.StringToBigInt("100000000000000000000"), + VoteUpdate: helpers.StringToBigInt("100000000000000000000"), + More: nil, + } + + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + nonce := uint64(1) + tx := transaction.Transaction{ + Nonce: nonce, + ChainID: types.CurrentChainID, + GasPrice: 1, + GasCoin: types.GetBaseCoinID(), + Type: transaction.TypeVoteCommission, + Data: encodedData, + SignatureType: transaction.SigTypeSingle, + } + nonce++ + + if err := tx.Sign(getPrivateKey()); err != nil { + t.Fatal(err) + } + + txBytes, err := tx.Serialize() + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + if res.Code != 0 { + t.Fatalf("CheckTx code is not 0: %d", res.Code) + } + <-txs + + blocks, err := tmCli.Subscribe(context.Background(), "test-client", "tm.event = 'NewBlock'") + if err != nil { + t.Fatal(err) + } + + defer func() { + err = tmCli.UnsubscribeAll(context.Background(), "test-client") + if err != nil { + t.Fatal(err) + } + }() + + for { + select { + case block := <-blocks: + height := block.Data.(types2.EventDataNewBlock).Block.Height + if height < int64(data.Height) { + continue + } + + events := blockchain.eventsDB.LoadEvents(uint32(height)) + if len(events) == 0 { + t.Fatalf("not found events") + } + // for _, event := range events { + // t.Logf("%#v", event) + // } + if events[0].Type() != eventsdb.TypeUpdateCommissionsEvent { + t.Fatal("not changed") + } + return + case <-time.After(10 * time.Second): + t.Fatal("timeout") + // blockchain.lock.RLock() + // exportedState := blockchain.CurrentState().Export() + // blockchain.lock.RUnlock() + // if err := exportedState.Verify(); err != nil { + // t.Fatal(err) + // } + return + } + } +} + +func TestBlockchain_GetBlocksTimeDelta(t *testing.T) { + blockchain, tmCli, _, cancel := initTestNode(t, 100) + defer cancel() + + blocks, err := tmCli.Subscribe(context.Background(), "test-client", "tm.event = 'NewBlock'") + if err != nil { + t.Fatal(err) + } + + var startHeight int64 + for block := range blocks { + height := block.Data.(types2.EventDataNewBlock).Block.Height + if startHeight == 0 { + startHeight = height + continue + } + + delta, count, err := blockchain.appDB.GetLastBlocksTimeDelta(uint64(height + 1)) + if err != nil { + t.Fatal(err) + } + t.Log(delta, count, delta/count) + if height > startHeight+10 { + return + } + } + +} + +func TestBlockchain_Run(t *testing.T) { + _, _, _, cancel := initTestNode(t, 0) + cancel() +} + +func TestBlockchain_InitialBlockHeight(t *testing.T) { + blockchain, tmCli, _, cancel := initTestNode(t, 100) + defer cancel() + + value := helpers.BipToPip(big.NewInt(10)) + to := types.Address([20]byte{1}) + + data := transaction.SendData{ + Coin: types.GetBaseCoinID(), + To: to, + Value: value, + } + + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + nonce := uint64(1) + tx := transaction.Transaction{ + Nonce: nonce, + ChainID: types.CurrentChainID, + GasPrice: 1, + GasCoin: types.GetBaseCoinID(), + Type: transaction.TypeSend, + Data: encodedData, + SignatureType: transaction.SigTypeSingle, + } + nonce++ + + if err := tx.Sign(getPrivateKey()); err != nil { + t.Fatal(err) + } + + txBytes, err := tx.Serialize() + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + + res, err := tmCli.BroadcastTxCommit(context.Background(), txBytes) + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + + time.Sleep(time.Second) + + resultTx, err := tmCli.Tx(context.Background(), res.Hash.Bytes(), false) + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } + + _, err = blockchain.GetStateForHeight(uint64(resultTx.Height - 1)) + if err != nil { + t.Fatalf("Failed: %s", err.Error()) + } } func TestBlockchain_Height(t *testing.T) { - blockchain, tmCli, _ := initTestNode(t) - defer blockchain.Stop() + blockchain, tmCli, _, cancel := initTestNode(t, 100) + defer cancel() blocks, err := tmCli.Subscribe(context.Background(), "test-client", "tm.event = 'NewBlock'") if err != nil { t.Fatal(err) } + defer func() { + err = tmCli.UnsubscribeAll(context.Background(), "test-client") + if err != nil { + t.Fatal(err) + } + }() + block := <-blocks if block.Data.(types2.EventDataNewBlock).Block.Height != int64(blockchain.Height()) { t.Fatal("invalid blockchain height") @@ -234,15 +365,15 @@ func TestBlockchain_Height(t *testing.T) { blockchain.lock.RLock() defer blockchain.lock.RUnlock() - exportedState := blockchain.CurrentState().Export(blockchain.Height() - 1) + exportedState := blockchain.CurrentState().Export() if err := exportedState.Verify(); err != nil { t.Fatal(err) } } func TestBlockchain_SetStatisticData(t *testing.T) { - blockchain, tmCli, _ := initTestNode(t) - defer blockchain.Stop() + blockchain, tmCli, _, cancel := initTestNode(t, 0) + defer cancel() ch := make(chan struct{}) blockchain.stateDeliver.Lock() @@ -259,6 +390,13 @@ func TestBlockchain_SetStatisticData(t *testing.T) { t.Fatal(err) } + defer func() { + err = tmCli.UnsubscribeAll(context.Background(), "test-client") + if err != nil { + t.Fatal(err) + } + }() + <-blocks <-blocks <-blocks @@ -270,18 +408,17 @@ func TestBlockchain_SetStatisticData(t *testing.T) { blockchain.lock.RLock() defer blockchain.lock.RUnlock() - exportedState := blockchain.CurrentState().Export(blockchain.Height() - 1) + exportedState := blockchain.CurrentState().Export() if err := exportedState.Verify(); err != nil { t.Fatal(err) } } func TestBlockchain_IsApplicationHalted(t *testing.T) { - blockchain, tmCli, pv := initTestNode(t) - defer blockchain.Stop() - + blockchain, tmCli, pv, cancel := initTestNode(t, 0) + defer cancel() data := transaction.SetHaltBlockData{ - PubKey: types.BytesToPubkey(pv.Key.PubKey.Bytes()[5:]), + PubKey: types.BytesToPubkey(pv.Key.PubKey.Bytes()[:]), Height: 5, } @@ -305,7 +442,7 @@ func TestBlockchain_IsApplicationHalted(t *testing.T) { } txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxSync(txBytes) + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -318,6 +455,13 @@ func TestBlockchain_IsApplicationHalted(t *testing.T) { t.Fatal(err) } + defer func() { + err = tmCli.UnsubscribeAll(context.Background(), "test-client") + if err != nil { + t.Fatal(err) + } + }() + for { select { case block := <-blocks: @@ -330,8 +474,8 @@ func TestBlockchain_IsApplicationHalted(t *testing.T) { return case <-time.After(2 * time.Second): blockchain.lock.RLock() - defer blockchain.lock.RUnlock() - exportedState := blockchain.CurrentState().Export(blockchain.Height() - 1) + exportedState := blockchain.CurrentState().Export() + blockchain.lock.RUnlock() if err := exportedState.Verify(); err != nil { t.Fatal(err) } @@ -341,8 +485,8 @@ func TestBlockchain_IsApplicationHalted(t *testing.T) { } func TestBlockchain_GetStateForHeightAndDeleteStateVersions(t *testing.T) { - blockchain, tmCli, _ := initTestNode(t) - defer blockchain.Stop() + blockchain, tmCli, _, cancel := initTestNode(t, 100) + defer cancel() symbol := types.StrToCoinSymbol("AAA123") data := transaction.CreateCoinData{ @@ -374,14 +518,14 @@ func TestBlockchain_GetStateForHeightAndDeleteStateVersions(t *testing.T) { } txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxCommit(txBytes) + res, err := tmCli.BroadcastTxCommit(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } time.Sleep(time.Second) - resultTx, err := tmCli.Tx(res.Hash.Bytes(), false) + resultTx, err := tmCli.Tx(context.Background(), res.Hash.Bytes(), false) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -413,15 +557,15 @@ func TestBlockchain_GetStateForHeightAndDeleteStateVersions(t *testing.T) { blockchain.lock.RLock() defer blockchain.lock.RUnlock() - exportedState := blockchain.CurrentState().Export(blockchain.Height() - 1) + exportedState := blockchain.CurrentState().Export() if err := exportedState.Verify(); err != nil { t.Fatal(err) } } func TestBlockchain_SendTx(t *testing.T) { - blockchain, tmCli, _ := initTestNode(t) - defer blockchain.Stop() + blockchain, tmCli, _, cancel := initTestNode(t, 0) + defer cancel() value := helpers.BipToPip(big.NewInt(10)) to := types.Address([20]byte{1}) @@ -455,7 +599,7 @@ func TestBlockchain_SendTx(t *testing.T) { txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxSync(txBytes) + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -469,6 +613,13 @@ func TestBlockchain_SendTx(t *testing.T) { t.Fatal(err) } + defer func() { + err = tmCli.UnsubscribeAll(context.Background(), "test-client") + if err != nil { + t.Fatal(err) + } + }() + select { case <-txs: // got tx @@ -478,21 +629,22 @@ func TestBlockchain_SendTx(t *testing.T) { blockchain.lock.RLock() defer blockchain.lock.RUnlock() - exportedState := blockchain.CurrentState().Export(blockchain.Height() - 1) + exportedState := blockchain.CurrentState().Export() if err := exportedState.Verify(); err != nil { t.Fatal(err) } } func TestBlockchain_FrozenFunds(t *testing.T) { - blockchain, tmCli, pv := initTestNode(t) + blockchain, tmCli, pv, cancel := initTestNode(t, 0) + defer cancel() targetHeight := uint64(10) value := helpers.BipToPip(big.NewInt(1000)) - pubkey := types.BytesToPubkey(pv.Key.PubKey.Bytes()[5:]) + pubkey := types.BytesToPubkey(pv.Key.PubKey.Bytes()[:]) blockchain.stateDeliver.RLock() blockchain.stateDeliver.Candidates.SubStake(developers.Address, pubkey, 0, big.NewInt(0).Set(value)) - blockchain.stateDeliver.FrozenFunds.AddFund(targetHeight, developers.Address, pubkey, blockchain.stateDeliver.Candidates.ID(pubkey), 0, big.NewInt(0).Set(value)) + blockchain.stateDeliver.FrozenFunds.AddFund(targetHeight, developers.Address, pubkey, blockchain.stateDeliver.Candidates.ID(pubkey), 0, big.NewInt(0).Set(value), nil) blockchain.stateDeliver.RUnlock() blocks, err := tmCli.Subscribe(context.Background(), "test-client", "tm.event = 'NewBlock'") @@ -509,7 +661,7 @@ func TestBlockchain_FrozenFunds(t *testing.T) { blockchain.lock.RLock() defer blockchain.lock.RUnlock() - exportedState := blockchain.CurrentState().Export(blockchain.Height() - 1) + exportedState := blockchain.CurrentState().Export() if err := exportedState.Verify(); err != nil { t.Fatal(err) } @@ -520,20 +672,20 @@ func TestBlockchain_FrozenFunds(t *testing.T) { t.Errorf("empty events for %d block", targetHeight) } if events[0].Type() != eventsdb.TypeUnbondEvent { - t.Fatal("event is not UnbondEvent") + t.Fatal("event is not StakeMoveEvent") } - if events[0].AddressString() != developers.Address.String() { + if events[0].(eventsdb.Stake).AddressString() != developers.Address.String() { t.Error("event address invalid") } - if events[0].ValidatorPubKeyString() != pubkey.String() { + if events[0].(eventsdb.Stake).ValidatorPubKeyString() != pubkey.String() { t.Error("event validator pubkey invalid") } } func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { - blockchain, tmCli, _ := initTestNode(t) - defer blockchain.Stop() + blockchain, tmCli, _, cancel := initTestNode(t, 0) + defer cancel() txs, err := tmCli.Subscribe(context.Background(), "test-client", "tm.event = 'Tx'") if err != nil { @@ -571,7 +723,7 @@ func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { } txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxSync(txBytes) + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -612,7 +764,7 @@ func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { } txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxSync(txBytes) + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -652,7 +804,7 @@ func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxSync(txBytes) + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -691,7 +843,7 @@ func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxSync(txBytes) + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -727,7 +879,7 @@ func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { } txBytes, _ := tx.Serialize() - res, err := tmCli.BroadcastTxSync(txBytes) + res, err := tmCli.BroadcastTxSync(context.Background(), txBytes) if err != nil { t.Fatalf("Failed: %s", err.Error()) } @@ -747,13 +899,15 @@ func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { t.Fatal(err) } - targetHeight := int64(5) + 12 + 123 - var h int64 + var targetHeight int64 func() { for { select { case block := <-blocks: - h = block.Data.(types2.EventDataNewBlock).Block.Height + h := block.Data.(types2.EventDataNewBlock).Block.Height + if targetHeight == 0 { + targetHeight = 135 + } if h > targetHeight { return } @@ -773,45 +927,34 @@ func TestBlockchain_RecalculateStakes_andRemoveValidator(t *testing.T) { } blockchain.lock.RUnlock() - if candidate.Status == candidates2.CandidateStatusOnline { + if candidate.Status == candidates.CandidateStatusOnline { t.Fatal("candidate not Offline") } blockchain.lock.RLock() defer blockchain.lock.RUnlock() - exportedState := blockchain.CurrentState().Export(blockchain.Height() - 1) + exportedState := blockchain.CurrentState().Export() if err := exportedState.Verify(); err != nil { t.Fatal(err) } } func TestStopNetworkByHaltBlocks(t *testing.T) { - blockchain, _, _ := initTestNode(t) - defer blockchain.Stop() + blockchain, _, _, cancel := initTestNode(t, 0) + cancel() haltHeight := uint64(50) - v1Pubkey := [32]byte{} - v2Pubkey := [32]byte{} - v3Pubkey := [32]byte{} - - rand.Read(v1Pubkey[:]) - rand.Read(v2Pubkey[:]) - rand.Read(v3Pubkey[:]) - - blockchain.stateDeliver.Validators.Create(v1Pubkey, helpers.BipToPip(big.NewInt(3))) - blockchain.stateDeliver.Validators.Create(v2Pubkey, helpers.BipToPip(big.NewInt(5))) - blockchain.stateDeliver.Validators.Create(v3Pubkey, helpers.BipToPip(big.NewInt(3))) - - v1Address := blockchain.stateDeliver.Validators.GetValidators()[1].GetAddress() - v2Address := blockchain.stateDeliver.Validators.GetValidators()[2].GetAddress() - v3Address := blockchain.stateDeliver.Validators.GetValidators()[3].GetAddress() + v1Pubkey := types.Pubkey{1} + v2Pubkey := types.Pubkey{2} + v3Pubkey := types.Pubkey{3} blockchain.validatorsStatuses = map[types.TmAddress]int8{} - blockchain.validatorsStatuses[v1Address] = ValidatorPresent - blockchain.validatorsStatuses[v2Address] = ValidatorPresent - blockchain.validatorsStatuses[v3Address] = ValidatorPresent + blockchain.validatorsPowers[v1Pubkey] = helpers.BipToPip(big.NewInt(3)) + blockchain.validatorsPowers[v2Pubkey] = helpers.BipToPip(big.NewInt(5)) + blockchain.validatorsPowers[v3Pubkey] = helpers.BipToPip(big.NewInt(3)) + blockchain.totalPower = helpers.BipToPip(big.NewInt(11)) blockchain.stateDeliver.Halts.AddHaltBlock(haltHeight, v1Pubkey) blockchain.stateDeliver.Halts.AddHaltBlock(haltHeight, v3Pubkey) @@ -826,3 +969,110 @@ func TestStopNetworkByHaltBlocks(t *testing.T) { t.Fatalf("Application not halted at height %d", haltHeight) } } + +func getPrivateKey() *ecdsa.PrivateKey { + b, _ := hex.DecodeString("825ca965c34ef1c8343e8e377959108370c23ba6194d858452b63432456403f9") + privateKey, _ := crypto.ToECDSA(b) + return privateKey +} + +func makeTestValidatorsAndCandidates(pubkeys []string, stake *big.Int) ([]types.Validator, []types.Candidate) { + vals := make([]types.Validator, 0, len(pubkeys)) + cands := make([]types.Candidate, 0, len(pubkeys)) + + for i, val := range pubkeys { + // pkeyBytes, err := base64.StdEncoding.DecodeString(val) + pkeyBytes := []byte(val) + // if err != nil { + // panic(err) + // } + + var pkey types.Pubkey + copy(pkey[:], pkeyBytes) + addr := developers.Address + + vals = append(vals, types.Validator{ + TotalBipStake: stake.String(), + PubKey: pkey, + AccumReward: big.NewInt(0).String(), + AbsentTimes: types.NewBitArray(24), + }) + + cands = append(cands, types.Candidate{ + ID: uint64(i) + 1, + RewardAddress: addr, + OwnerAddress: crypto.PubkeyToAddress(getPrivateKey().PublicKey), + ControlAddress: addr, + TotalBipStake: stake.String(), + PubKey: pkey, + Commission: 10, + Stakes: []types.Stake{ + { + Owner: addr, + Coin: uint64(types.GetBaseCoinID()), + Value: stake.String(), + BipValue: stake.String(), + }, + }, + Status: candidates.CandidateStatusOnline, + }) + } + + return vals, cands +} + +func getTestGenesis(pv *privval.FilePV, home string, initialState int64) func() (*types2.GenesisDoc, error) { + return func() (*types2.GenesisDoc, error) { + validators, candidates := makeTestValidatorsAndCandidates([]string{string(pv.Key.PubKey.Bytes()[:])}, helpers.BipToPip(big.NewInt(12444011))) + + appState := types.AppState{ + TotalSlashed: "0", + Accounts: []types.Account{ + { + Address: crypto.PubkeyToAddress(getPrivateKey().PublicKey), + Balance: []types.Balance{ + { + Coin: uint64(types.GetBaseCoinID()), + Value: helpers.BipToPip(big.NewInt(9223372036854775807)).String(), + }, + }, + }, + }, + Validators: validators, + Candidates: candidates, + } + + appStateJSON, err := tmjson.Marshal(appState) + if err != nil { + return nil, err + } + + genesisDoc := types2.GenesisDoc{ + ChainID: "minter-test-network", + InitialHeight: initialState, + GenesisTime: time.Now(), + AppHash: nil, + AppState: json.RawMessage(appStateJSON), + } + + err = genesisDoc.ValidateAndComplete() + if err != nil { + return nil, err + } + + genesisFile := home + "/config/genesis.json" + if err := genesisDoc.SaveAs(genesisFile); err != nil { + panic(err) + } + + return &genesisDoc, nil + } +} + +func getPort() int { + port, err := tmnet.GetFreePort() + if err != nil { + panic(err) + } + return port +} diff --git a/core/rewards/rewards.go b/core/rewards/rewards.go index 623e58e12..8b8c06022 100644 --- a/core/rewards/rewards.go +++ b/core/rewards/rewards.go @@ -3,7 +3,6 @@ package rewards import ( "math/big" - "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/helpers" ) @@ -11,7 +10,7 @@ const lastBlock = 43702611 const firstReward = 333 const lastReward = 68 -var startHeight uint64 = 0 +var startHeight uint64 = 9150000 var beforeGenesis = big.NewInt(0) // GetRewardForBlock returns reward for creation of given block. If there is no reward - returns 0. @@ -29,7 +28,7 @@ func GetRewardForBlock(blockHeight uint64) *big.Int { reward := big.NewInt(firstReward) reward.Sub(reward, big.NewInt(int64(blockHeight/200000))) - if reward.Cmp(types.Big0) < 1 { + if reward.Sign() < 1 { return helpers.BipToPip(big.NewInt(1)) } diff --git a/core/rewards/rewards_test.go b/core/rewards/rewards_test.go index 6135b5fe3..6d56ec3ca 100644 --- a/core/rewards/rewards_test.go +++ b/core/rewards/rewards_test.go @@ -12,6 +12,10 @@ type Results struct { Result *big.Int } +func init() { + startHeight = 0 +} + func TestGetRewardForBlock(t *testing.T) { data := []Results{ { diff --git a/core/state/accounts/accounts.go b/core/state/accounts/accounts.go index 84edac4ca..559c96af1 100644 --- a/core/state/accounts/accounts.go +++ b/core/state/accounts/accounts.go @@ -6,7 +6,8 @@ import ( "github.com/MinterTeam/minter-go-node/core/state/bus" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" + "sync/atomic" "math/big" "sort" @@ -30,8 +31,8 @@ type Accounts struct { list map[types.Address]*Model dirty map[types.Address]struct{} - iavl tree.MTree - bus *bus.Bus + db atomic.Value + bus *bus.Bus lock sync.RWMutex } @@ -41,14 +42,30 @@ type Balance struct { Value *big.Int } -func NewAccounts(stateBus *bus.Bus, iavl tree.MTree) (*Accounts, error) { - accounts := &Accounts{iavl: iavl, bus: stateBus, list: map[types.Address]*Model{}, dirty: map[types.Address]struct{}{}} +func NewAccounts(stateBus *bus.Bus, db *iavl.ImmutableTree) *Accounts { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } + accounts := &Accounts{db: immutableTree, bus: stateBus, list: map[types.Address]*Model{}, dirty: map[types.Address]struct{}{}} accounts.bus.SetAccounts(NewBus(accounts)) - return accounts, nil + return accounts +} + +func (a *Accounts) immutableTree() *iavl.ImmutableTree { + db := a.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) } -func (a *Accounts) Commit() error { +func (a *Accounts) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + a.db.Store(immutableTree) +} + +func (a *Accounts) Commit(db *iavl.MutableTree) error { accounts := a.getOrderedDirtyAccounts() for _, address := range accounts { account := a.getFromMap(address) @@ -57,22 +74,30 @@ func (a *Accounts) Commit() error { a.lock.Unlock() // save info (nonce and multisig data) - if account.isDirty || account.isNew { + if a.IsNewOrDirty(account) { + account.lock.Lock() + account.isDirty = false + account.isNew = false data, err := rlp.EncodeToBytes(account) + account.lock.Unlock() if err != nil { return fmt.Errorf("can't encode object at %x: %v", address[:], err) } + if len(data) == 0 { + continue + } path := []byte{mainPrefix} path = append(path, address[:]...) - a.iavl.Set(path, data) - account.isDirty = false - account.isNew = false + db.Set(path, data) } // save coins list - if account.hasDirtyCoins { + if a.HasDirtyCoins(account) { + account.lock.Lock() + account.hasDirtyCoins = false coinsList, err := rlp.EncodeToBytes(account.coins) + account.lock.Unlock() if err != nil { return fmt.Errorf("can't encode object at %x: %v", address[:], err) } @@ -80,8 +105,7 @@ func (a *Accounts) Commit() error { path := []byte{mainPrefix} path = append(path, address[:]...) path = append(path, coinsPrefix) - a.iavl.Set(path, coinsList) - account.hasDirtyCoins = false + db.Set(path, coinsList) } // save balances @@ -99,24 +123,42 @@ func (a *Accounts) Commit() error { balance := account.getBalance(coin) if balance.Sign() == 0 { - a.iavl.Remove(path) + db.Remove(path) } else { - a.iavl.Set(path, balance.Bytes()) + db.Set(path, balance.Bytes()) } } + account.lock.Lock() account.dirtyBalances = map[types.CoinID]struct{}{} + account.lock.Unlock() } } return nil } +func (a *Accounts) HasDirtyCoins(account *Model) bool { + account.lock.RLock() + defer account.lock.RUnlock() + + return account.hasDirtyCoins +} + +func (a *Accounts) IsNewOrDirty(account *Model) bool { + account.lock.RLock() + defer account.lock.RUnlock() + + return account.isDirty || account.isNew +} + func (a *Accounts) getOrderedDirtyAccounts() []types.Address { + a.lock.RLock() keys := make([]types.Address, 0, len(a.dirty)) for k := range a.dirty { keys = append(keys, k) } + a.lock.RUnlock() sort.SliceStable(keys, func(i, j int) bool { return bytes.Compare(keys[i].Bytes(), keys[j].Bytes()) == 1 @@ -136,23 +178,28 @@ func (a *Accounts) GetBalance(address types.Address, coin types.CoinID) *big.Int return big.NewInt(0) } - if _, ok := account.balances[coin]; !ok { - balance := big.NewInt(0) + account.lock.RLock() + balance, ok := account.balances[coin] + account.lock.RUnlock() + if !ok { + balance = big.NewInt(0) path := []byte{mainPrefix} path = append(path, address[:]...) path = append(path, balancePrefix) path = append(path, coin.Bytes()...) - _, enc := a.iavl.Get(path) + _, enc := a.immutableTree().Get(path) if len(enc) != 0 { balance = big.NewInt(0).SetBytes(enc) } + account.lock.Lock() account.balances[coin] = balance + account.lock.Unlock() } - return big.NewInt(0).Set(account.balances[coin]) + return big.NewInt(0).Set(balance) } func (a *Accounts) SubBalance(address types.Address, coin types.CoinID, amount *big.Int) { @@ -183,6 +230,9 @@ func (a *Accounts) ExistsMultisig(msigAddress types.Address) bool { return true } + acc.lock.RLock() + defer acc.lock.RUnlock() + if acc.Nonce > 0 { return true } @@ -219,15 +269,16 @@ func (a *Accounts) CreateMultisig(weights []uint32, addresses []types.Address, t func (a *Accounts) EditMultisig(threshold uint32, weights []uint32, addresses []types.Address, address types.Address) types.Address { account := a.get(address) - msig := Multisig{ + account.lock.Lock() + account.MultisigData = Multisig{ Threshold: threshold, Weights: weights, Addresses: addresses, } + account.isDirty = true + account.lock.Unlock() - account.MultisigData = msig account.markDirty(account.address) - account.isDirty = true a.setToMap(address, account) return address @@ -240,7 +291,7 @@ func (a *Accounts) get(address types.Address) *Model { path := []byte{mainPrefix} path = append(path, address[:]...) - _, enc := a.iavl.Get(path) + _, enc := a.immutableTree().Get(path) if len(enc) == 0 { return nil } @@ -259,7 +310,7 @@ func (a *Accounts) get(address types.Address) *Model { path = []byte{mainPrefix} path = append(path, address[:]...) path = append(path, coinsPrefix) - _, enc = a.iavl.Get(path) + _, enc = a.immutableTree().Get(path) if len(enc) != 0 { var coins []types.CoinID if err := rlp.DecodeBytes(enc, &coins); err != nil { @@ -294,14 +345,21 @@ func (a *Accounts) getOrNew(address types.Address) *Model { func (a *Accounts) GetNonce(address types.Address) uint64 { account := a.getOrNew(address) + account.lock.RLock() + defer account.lock.RUnlock() + return account.Nonce } func (a *Accounts) GetBalances(address types.Address) []Balance { account := a.getOrNew(address) - balances := make([]Balance, len(account.coins)) - for key, id := range account.coins { + account.lock.RLock() + coins := account.coins + account.lock.RUnlock() + + balances := make([]Balance, len(coins)) + for key, id := range coins { balances[key] = Balance{ Coin: *a.bus.Coins().GetCoin(id), Value: a.GetBalance(address, id), @@ -312,55 +370,62 @@ func (a *Accounts) GetBalances(address types.Address) []Balance { } func (a *Accounts) markDirty(addr types.Address) { + a.lock.Lock() + defer a.lock.Unlock() + a.dirty[addr] = struct{}{} } func (a *Accounts) Export(state *types.AppState) { - // todo: iterate range? - a.iavl.Iterate(func(key []byte, value []byte) bool { - if key[0] == mainPrefix { - addressPath := key[1:] - if len(addressPath) > types.AddressLength { - return false - } + a.immutableTree().IterateRange([]byte{mainPrefix}, []byte{mainPrefix + 1}, true, func(key []byte, value []byte) bool { + addressPath := key[1:] + if len(addressPath) > types.AddressLength { + return false + } - address := types.BytesToAddress(addressPath) - account := a.get(address) + address := types.BytesToAddress(addressPath) + account := a.get(address) - var balance []types.Balance - for _, b := range a.GetBalances(account.address) { - balance = append(balance, types.Balance{ - Coin: uint64(b.Coin.ID), - Value: b.Value.String(), - }) + var balance []types.Balance + for _, b := range a.GetBalances(account.address) { + if b.Value.Sign() != 1 { + continue } - - // sort balances by coin symbol - sort.SliceStable(balance, func(i, j int) bool { - return bytes.Compare(types.CoinID(balance[i].Coin).Bytes(), types.CoinID(balance[j].Coin).Bytes()) == 1 + balance = append(balance, types.Balance{ + Coin: uint64(b.Coin.ID), + Value: b.Value.String(), }) + } - acc := types.Account{ - Address: account.address, - Balance: balance, - Nonce: account.Nonce, - } + // sort balances by coin symbol + sort.SliceStable(balance, func(i, j int) bool { + return bytes.Compare(types.CoinID(balance[i].Coin).Bytes(), types.CoinID(balance[j].Coin).Bytes()) == 1 + }) - if account.IsMultisig() { - var weights []uint64 - for _, weight := range account.MultisigData.Weights { - weights = append(weights, uint64(weight)) - } - acc.MultisigData = &types.Multisig{ - Weights: weights, - Threshold: uint64(account.MultisigData.Threshold), - Addresses: account.MultisigData.Addresses, - } + acc := types.Account{ + Address: account.address, + Balance: balance, + Nonce: account.Nonce, + } + + if account.IsMultisig() { + var weights []uint64 + for _, weight := range account.MultisigData.Weights { + weights = append(weights, uint64(weight)) + } + acc.MultisigData = &types.Multisig{ + Weights: weights, + Threshold: uint64(account.MultisigData.Threshold), + Addresses: account.MultisigData.Addresses, } + } - state.Accounts = append(state.Accounts, acc) + if len(acc.Balance) == 0 && acc.Nonce == 0 && acc.MultisigData == nil { + return false } + state.Accounts = append(state.Accounts, acc) + return false }) } diff --git a/core/state/accounts/accounts_test.go b/core/state/accounts/accounts_test.go index f7a591208..25b02d766 100644 --- a/core/state/accounts/accounts_test.go +++ b/core/state/accounts/accounts_test.go @@ -15,13 +15,12 @@ import ( ) func TestAccounts_CreateMultisig(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) + multisigAddr := accounts.CreateMultisig([]uint32{1, 1, 2}, []types.Address{[20]byte{1}, [20]byte{2}, [20]byte{3}}, 2, [20]byte{4}) account := accounts.GetAccount(multisigAddr) @@ -52,13 +51,11 @@ func TestAccounts_CreateMultisig(t *testing.T) { } func TestAccounts_SetNonce(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetNonce([20]byte{4}, 5) if accounts.GetNonce([20]byte{4}) != 5 { t.Fatal("nonce not equal 5") @@ -66,13 +63,11 @@ func TestAccounts_SetNonce(t *testing.T) { } func TestAccounts_SetBalance(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(1000)) account := accounts.GetAccount([20]byte{4}) if account == nil { @@ -84,36 +79,34 @@ func TestAccounts_SetBalance(t *testing.T) { } func TestAccounts_SetBalance_fromDB(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(1000)) - err = accounts.Commit() + + _, _, err := mutableTree.Commit(accounts) if err != nil { t.Fatal(err) } - accounts, err = NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) + if accounts.GetBalance([20]byte{4}, 0).String() != "1000" { + t.Fatal("balance of coin ID '0' not equal 1000") } + accounts = NewAccounts(b, mutableTree.GetLastImmutable()) + if accounts.GetBalance([20]byte{4}, 0).String() != "1000" { t.Fatal("balance of coin ID '0' not equal 1000") } } func TestAccounts_SetBalance_0(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(100)) accounts.SetBalance([20]byte{4}, 0, big.NewInt(0)) accounts.SetBalance([20]byte{4}, 1, big.NewInt(0)) @@ -130,24 +123,17 @@ func TestAccounts_SetBalance_0(t *testing.T) { } func TestAccounts_GetBalances(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - busCoins, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + busCoins := coins.NewCoins(b, mutableTree.GetLastImmutable()) + b.SetCoins(coins.NewBus(busCoins)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(1000)) - coinsState, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + coinsState := coins.NewCoins(b, mutableTree.GetLastImmutable()) coinsState.Create(1, types.StrToCoinSymbol("AAA"), @@ -158,7 +144,7 @@ func TestAccounts_GetBalances(t *testing.T) { big.NewInt(0).Exp(big.NewInt(10), big.NewInt(10+18), nil), nil) - err = coinsState.Commit() + _, _, err := mutableTree.Commit(accounts) if err != nil { t.Fatal(err) } @@ -184,13 +170,11 @@ func TestAccounts_GetBalances(t *testing.T) { } func TestAccounts_ExistsMultisig(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) msigAddress := CreateMultisigAddress([20]byte{4}, 12) if accounts.ExistsMultisig(msigAddress) { @@ -217,13 +201,11 @@ func TestAccounts_ExistsMultisig(t *testing.T) { } func TestAccounts_AddBalance_bus(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(1000)) accounts.bus.Accounts().AddBalance([20]byte{4}, 0, big.NewInt(1000)) @@ -234,13 +216,11 @@ func TestAccounts_AddBalance_bus(t *testing.T) { } func TestAccounts_SubBalance(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(1000)) accounts.SubBalance([20]byte{4}, 0, big.NewInt(500)) @@ -255,13 +235,11 @@ func TestAccounts_SubBalance(t *testing.T) { } func TestAccounts_EditMultisig(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) msigAddress := CreateMultisigAddress([20]byte{4}, 12) @@ -293,21 +271,14 @@ func TestAccounts_EditMultisig(t *testing.T) { } func TestAccounts_Commit(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(1000)) - err = accounts.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err := mutableTree.SaveVersion() + hash, version, err := mutableTree.Commit(accounts) if err != nil { t.Fatal(err) } @@ -322,24 +293,16 @@ func TestAccounts_Commit(t *testing.T) { } func TestAccounts_Export(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - busCoins, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + busCoins := coins.NewCoins(b, mutableTree.GetLastImmutable()) b.SetCoins(coins.NewBus(busCoins)) b.SetChecker(checker.NewChecker(b)) - accounts, err := NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accounts := NewAccounts(b, mutableTree.GetLastImmutable()) accounts.SetBalance([20]byte{4}, 0, big.NewInt(1000)) - coinsState, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + coinsState := coins.NewCoins(b, mutableTree.GetLastImmutable()) coinsState.Create(1, types.StrToCoinSymbol("AAA"), @@ -350,11 +313,10 @@ func TestAccounts_Export(t *testing.T) { big.NewInt(0).Exp(big.NewInt(10), big.NewInt(10+18), nil), nil) - err = coinsState.Commit() + _, _, err := mutableTree.Commit(accounts) if err != nil { t.Fatal(err) } - symbol := coinsState.GetCoinBySymbol(types.StrToCoinSymbol("AAA"), 0) if symbol == nil { t.Fatal("coin not found") @@ -363,11 +325,10 @@ func TestAccounts_Export(t *testing.T) { accounts.SetBalance([20]byte{4}, symbol.ID(), big.NewInt(1001)) _ = accounts.CreateMultisig([]uint32{1, 1, 2}, []types.Address{[20]byte{1}, [20]byte{2}, [20]byte{3}}, 2, [20]byte{4}) - err = accounts.Commit() + _, _, err = mutableTree.Commit(accounts) if err != nil { t.Fatal(err) } - state := new(types.AppState) accounts.Export(state) diff --git a/core/state/accounts/model.go b/core/state/accounts/model.go index b94585d07..bd2ad3bc4 100644 --- a/core/state/accounts/model.go +++ b/core/state/accounts/model.go @@ -7,6 +7,7 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "math/big" "sort" + "sync" ) type Model struct { @@ -24,12 +25,15 @@ type Model struct { isNew bool markDirty func(types.Address) + lock sync.RWMutex } type Multisig struct { Threshold uint32 Weights []uint32 Addresses []types.Address + + lock sync.RWMutex } func CreateMultisigAddress(owner types.Address, nonce uint64) types.Address { @@ -48,6 +52,9 @@ func CreateMultisigAddress(owner types.Address, nonce uint64) types.Address { } func (m *Multisig) GetWeight(address types.Address) uint32 { + m.lock.RLock() + defer m.lock.RUnlock() + for i, addr := range m.Addresses { if addr == address { return m.Weights[i] @@ -58,29 +65,43 @@ func (m *Multisig) GetWeight(address types.Address) uint32 { } func (model *Model) setNonce(nonce uint64) { + model.lock.Lock() + defer model.lock.Unlock() + model.Nonce = nonce model.isDirty = true model.markDirty(model.address) } func (model *Model) getBalance(coin types.CoinID) *big.Int { + model.lock.RLock() + defer model.lock.RUnlock() + return model.balances[coin] } func (model *Model) hasDirtyBalances() bool { + model.lock.RLock() + defer model.lock.RUnlock() + return len(model.dirtyBalances) > 0 } func (model *Model) isBalanceDirty(coin types.CoinID) bool { + model.lock.RLock() + defer model.lock.RUnlock() + _, exists := model.dirtyBalances[coin] return exists } func (model *Model) getOrderedCoins() []types.CoinID { + model.lock.RLock() keys := make([]types.CoinID, 0, len(model.balances)) for k := range model.balances { keys = append(keys, k) } + model.lock.RUnlock() sort.SliceStable(keys, func(i, j int) bool { return bytes.Compare(keys[i].Bytes(), keys[j].Bytes()) == 1 @@ -96,33 +117,47 @@ func (model *Model) setBalance(coin types.CoinID, amount *big.Int) { } var newCoins []types.CoinID + + model.lock.RLock() for _, c := range model.coins { if coin == c { continue } - newCoins = append(newCoins, c) } + model.lock.RUnlock() + model.lock.Lock() model.hasDirtyCoins = true model.coins = newCoins model.balances[coin] = amount model.dirtyBalances[coin] = struct{}{} + model.lock.Unlock() + model.markDirty(model.address) return } if !model.hasCoin(coin) { + model.lock.Lock() model.hasDirtyCoins = true model.coins = append(model.coins, coin) + model.lock.Unlock() } + + model.lock.Lock() model.dirtyBalances[coin] = struct{}{} - model.markDirty(model.address) model.balances[coin] = amount + model.lock.Unlock() + + model.markDirty(model.address) } func (model *Model) hasCoin(coin types.CoinID) bool { + model.lock.RLock() + defer model.lock.RUnlock() + for _, c := range model.coins { if c == coin { return true @@ -133,9 +168,15 @@ func (model *Model) hasCoin(coin types.CoinID) bool { } func (model *Model) IsMultisig() bool { + model.lock.RLock() + defer model.lock.RUnlock() + return len(model.MultisigData.Weights) > 0 } func (model *Model) Multisig() Multisig { + model.lock.RLock() + defer model.lock.RUnlock() + return model.MultisigData } diff --git a/core/state/app/app.go b/core/state/app/app.go index 2460858f0..9e970544b 100644 --- a/core/state/app/app.go +++ b/core/state/app/app.go @@ -5,91 +5,113 @@ import ( "github.com/MinterTeam/minter-go-node/core/state/bus" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" "math/big" + "sync" + "sync/atomic" ) const mainPrefix = 'd' type RApp interface { - Export(state *types.AppState, height uint64) + Export(state *types.AppState) GetMaxGas() uint64 GetTotalSlashed() *big.Int GetCoinsCount() uint32 GetNextCoinID() types.CoinID } -func (v *App) Tree() tree.ReadOnlyTree { - return v.iavl -} - type App struct { model *Model isDirty bool - bus *bus.Bus - iavl tree.MTree + db atomic.Value + + bus *bus.Bus + mx sync.Mutex } -func NewApp(stateBus *bus.Bus, iavl tree.MTree) (*App, error) { - app := &App{bus: stateBus, iavl: iavl} +func NewApp(stateBus *bus.Bus, db *iavl.ImmutableTree) *App { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } + app := &App{bus: stateBus, db: immutableTree} app.bus.SetApp(NewBus(app)) - return app, nil + return app } -func (v *App) Commit() error { - if !v.isDirty { +func (a *App) immutableTree() *iavl.ImmutableTree { + db := a.db.Load() + if db == nil { return nil } + return db.(*iavl.ImmutableTree) +} + +func (a *App) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + a.db.Store(immutableTree) +} - v.isDirty = false +func (a *App) Commit(db *iavl.MutableTree) error { + a.mx.Lock() + defer a.mx.Unlock() - data, err := rlp.EncodeToBytes(v.model) + if !a.isDirty { + return nil + } + + a.isDirty = false + + data, err := rlp.EncodeToBytes(a.model) if err != nil { return fmt.Errorf("can't encode legacyApp model: %s", err) } path := []byte{mainPrefix} - v.iavl.Set(path, data) + db.Set(path, data) return nil } -func (v *App) GetMaxGas() uint64 { - model := v.getOrNew() +func (a *App) GetMaxGas() uint64 { + model := a.getOrNew() return model.getMaxGas() } -func (v *App) SetMaxGas(gas uint64) { - model := v.getOrNew() +func (a *App) SetMaxGas(gas uint64) { + model := a.getOrNew() model.setMaxGas(gas) } -func (v *App) GetTotalSlashed() *big.Int { - model := v.getOrNew() +func (a *App) GetTotalSlashed() *big.Int { + model := a.getOrNew() return model.getTotalSlashed() } -func (v *App) AddTotalSlashed(amount *big.Int) { +func (a *App) AddTotalSlashed(amount *big.Int) { if amount.Cmp(big.NewInt(0)) == 0 { return } - model := v.getOrNew() + model := a.getOrNew() model.setTotalSlashed(big.NewInt(0).Add(model.getTotalSlashed(), amount)) - v.bus.Checker().AddCoin(types.GetBaseCoinID(), amount) + a.bus.Checker().AddCoin(types.GetBaseCoinID(), amount) } -func (v *App) get() *Model { - if v.model != nil { - return v.model +func (a *App) get() *Model { + a.mx.Lock() + defer a.mx.Unlock() + + if a.model != nil { + return a.model } path := []byte{mainPrefix} - _, enc := v.iavl.Get(path) + _, enc := a.immutableTree().Get(path) if len(enc) == 0 { return nil } @@ -99,48 +121,52 @@ func (v *App) get() *Model { panic(fmt.Sprintf("failed to decode legacyApp model at: %s", err)) } - v.model = model - v.model.markDirty = v.markDirty - return v.model + a.model = model + a.model.markDirty = a.markDirty + return a.model } -func (v *App) getOrNew() *Model { - model := v.get() +func (a *App) getOrNew() *Model { + model := a.get() if model == nil { model = &Model{ TotalSlashed: big.NewInt(0), CoinsCount: 0, MaxGas: 0, - markDirty: v.markDirty, + markDirty: a.markDirty, } - v.model = model + a.mx.Lock() + a.model = model + a.mx.Unlock() } return model } -func (v *App) markDirty() { - v.isDirty = true +func (a *App) markDirty() { + a.mx.Lock() + defer a.mx.Unlock() + + a.isDirty = true } -func (v *App) SetTotalSlashed(amount *big.Int) { - v.getOrNew().setTotalSlashed(amount) +func (a *App) SetTotalSlashed(amount *big.Int) { + a.getOrNew().setTotalSlashed(amount) } -func (v *App) GetCoinsCount() uint32 { - return v.getOrNew().getCoinsCount() +func (a *App) GetCoinsCount() uint32 { + return a.getOrNew().getCoinsCount() } -func (v *App) GetNextCoinID() types.CoinID { - return types.CoinID(v.GetCoinsCount() + 1) +func (a *App) GetNextCoinID() types.CoinID { + return types.CoinID(a.GetCoinsCount() + 1) } -func (v *App) SetCoinsCount(count uint32) { - v.getOrNew().setCoinsCount(count) +func (a *App) SetCoinsCount(count uint32) { + a.getOrNew().setCoinsCount(count) } -func (v *App) Export(state *types.AppState, height uint64) { - state.MaxGas = v.GetMaxGas() - state.TotalSlashed = v.GetTotalSlashed().String() - state.StartHeight = height +func (a *App) Export(state *types.AppState) { + state.MaxGas = a.GetMaxGas() + state.TotalSlashed = a.GetTotalSlashed().String() } diff --git a/core/state/app/model.go b/core/state/app/model.go index 3ca80ec15..b41100f44 100644 --- a/core/state/app/model.go +++ b/core/state/app/model.go @@ -1,6 +1,9 @@ package app -import "math/big" +import ( + "math/big" + "sync" +) type Model struct { TotalSlashed *big.Int @@ -8,13 +11,20 @@ type Model struct { MaxGas uint64 markDirty func() + mx sync.RWMutex } func (model *Model) getMaxGas() uint64 { + model.mx.RLock() + defer model.mx.RUnlock() + return model.MaxGas } func (model *Model) setMaxGas(maxGas uint64) { + model.mx.Lock() + defer model.mx.Unlock() + if model.MaxGas != maxGas { model.markDirty() } @@ -22,6 +32,9 @@ func (model *Model) setMaxGas(maxGas uint64) { } func (model *Model) getTotalSlashed() *big.Int { + model.mx.RLock() + defer model.mx.RUnlock() + if model.TotalSlashed == nil { return big.NewInt(0) } @@ -30,6 +43,9 @@ func (model *Model) getTotalSlashed() *big.Int { } func (model *Model) setTotalSlashed(totalSlashed *big.Int) { + model.mx.Lock() + defer model.mx.Unlock() + if model.TotalSlashed.Cmp(totalSlashed) != 0 { model.markDirty() } @@ -37,10 +53,16 @@ func (model *Model) setTotalSlashed(totalSlashed *big.Int) { } func (model *Model) getCoinsCount() uint32 { + model.mx.RLock() + defer model.mx.RUnlock() + return model.CoinsCount } func (model *Model) setCoinsCount(count uint32) { + model.mx.Lock() + defer model.mx.Unlock() + if model.CoinsCount != count { model.markDirty() } diff --git a/core/state/candidates/candidate_test.go b/core/state/candidates/candidate_test.go index 6179cf063..79ce603c9 100644 --- a/core/state/candidates/candidate_test.go +++ b/core/state/candidates/candidate_test.go @@ -2,7 +2,6 @@ package candidates import ( "encoding/json" - "fmt" eventsdb "github.com/MinterTeam/minter-go-node/core/events" "github.com/MinterTeam/minter-go-node/core/state/accounts" "github.com/MinterTeam/minter-go-node/core/state/app" @@ -21,15 +20,13 @@ import ( ) func TestCandidates_Create_oneCandidate(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) - err = candidates.Commit() + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -44,71 +41,51 @@ func TestCandidates_Create_oneCandidate(t *testing.T) { } } -func TestCandidates_Commit_createThreeCandidates(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } - - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) - candidates.Create([20]byte{11}, [20]byte{21}, [20]byte{31}, [32]byte{41}, 10) +func TestCandidates_Commit_createThreeCandidatesWithInitialHeight(t *testing.T) { + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 2) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) + candidates.Create([20]byte{11}, [20]byte{21}, [20]byte{31}, [32]byte{41}, 10, 0) - hash, version, err := mutableTree.SaveVersion() + _, version, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } - if version != 1 { + if version != 2 { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "D7A17D41EAE39D61D3F85BC3311DA1FE306E885FF03024D0173F23E3739E719B" { - t.Fatalf("hash %X", hash) - } - - candidates.Create([20]byte{1, 1}, [20]byte{2, 2}, [20]byte{3, 3}, [32]byte{4, 4}, 10) + // if fmt.Sprintf("%X", hash) != "D7A17D41EAE39D61D3F85BC3311DA1FE306E885FF03024D0173F23E3739E719B" { + // t.Fatalf("hash %X", hash) + // } + candidates.Create([20]byte{1, 1}, [20]byte{2, 2}, [20]byte{3, 3}, [32]byte{4, 4}, 10, 0) - err = candidates.Commit() + _, version, err = mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } - hash, version, err = mutableTree.SaveVersion() - if err != nil { - t.Fatal(err) - } - - if version != 2 { + if version != 3 { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "01E34A08A0CF18403B8C3708FA773A4D0B152635F321085CE7B68F04FD520A9A" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "51B9DC41F65A6BD3F76059E8CA1A9E3CB48750F87A2BD99376E5BA84F53AC12E" { + // t.Fatalf("hash %X", hash) + // } } func TestCandidates_Commit_changePubKeyAndCheckBlockList(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } - - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) - candidates.Create([20]byte{11}, [20]byte{21}, [20]byte{31}, [32]byte{41}, 10) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) + candidates.Create([20]byte{11}, [20]byte{21}, [20]byte{31}, [32]byte{41}, 10, 0) - hash, version, err := mutableTree.SaveVersion() + _, version, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -117,19 +94,14 @@ func TestCandidates_Commit_changePubKeyAndCheckBlockList(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "D7A17D41EAE39D61D3F85BC3311DA1FE306E885FF03024D0173F23E3739E719B" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "D7A17D41EAE39D61D3F85BC3311DA1FE306E885FF03024D0173F23E3739E719B" { + // t.Fatalf("hash %X", hash) + // } candidates.ChangePubKey([32]byte{4}, [32]byte{5}) candidates.ChangePubKey([32]byte{41}, [32]byte{6}) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err = mutableTree.SaveVersion() + _, version, err = mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -138,28 +110,23 @@ func TestCandidates_Commit_changePubKeyAndCheckBlockList(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "BB335E1AA631D9540C2CB0AC9C959B556C366B79D39B828B07106CF2DACE5A2D" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "BB335E1AA631D9540C2CB0AC9C959B556C366B79D39B828B07106CF2DACE5A2D" { + // t.Fatalf("hash %X", hash) + // } if !candidates.IsBlockedPubKey([32]byte{4}) { t.Fatal("pub_key is not blocked") } - candidates, err = NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + candidates = NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) candidates.LoadCandidates() candidate := candidates.GetCandidate([32]byte{5}) if candidate == nil { t.Fatal("candidate not found") } - var pubkey ed25519.PubKeyEd25519 - copy(pubkey[:], types.Pubkey{5}.Bytes()) var address types.TmAddress - copy(address[:], pubkey.Address().Bytes()) + copy(address[:], ed25519.PubKey(candidate.PubKey[:]).Address().Bytes()) if *(candidate.tmAddress) != address { t.Fatal("tmAddress not change") } @@ -169,11 +136,9 @@ func TestCandidates_Commit_changePubKeyAndCheckBlockList(t *testing.T) { } func TestCandidates_AddToBlockPubKey(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) candidates.AddToBlockPubKey([32]byte{4}) @@ -183,20 +148,13 @@ func TestCandidates_AddToBlockPubKey(t *testing.T) { } func TestCandidates_Commit_withStakeAndUpdate(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err := mutableTree.SaveVersion() + _, version, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -205,9 +163,9 @@ func TestCandidates_Commit_withStakeAndUpdate(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { + // t.Fatalf("hash %X", hash) + // } candidates.SetStakes([32]byte{4}, []types.Stake{ { @@ -224,12 +182,8 @@ func TestCandidates_Commit_withStakeAndUpdate(t *testing.T) { BipValue: "100", }, }) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - hash, version, err = mutableTree.SaveVersion() + _, version, err = mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -238,26 +192,19 @@ func TestCandidates_Commit_withStakeAndUpdate(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "2D206158AA79C3BDAA019C61FEAD47BB9B6170C445EE7B36E935AC954765E99F" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "C1659B82F60F0883043A6948C567A31C5B172EB99E5F5F94C346679461A47CE1" { + // t.Fatalf("hash %X", hash) + // } } func TestCandidates_Commit_edit(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err := mutableTree.SaveVersion() + _, version, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -266,18 +213,13 @@ func TestCandidates_Commit_edit(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { + // t.Fatalf("hash %X", hash) + // } candidates.Edit([32]byte{4}, [20]byte{1, 1}, [20]byte{2, 2}, [20]byte{3, 3}) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err = mutableTree.SaveVersion() + _, version, err = mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -286,9 +228,9 @@ func TestCandidates_Commit_edit(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "482BE887F2E18DC1BB829BD6AFE8887CE4EC74D4DC485DB1355D78093EAB6B35" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "482BE887F2E18DC1BB829BD6AFE8887CE4EC74D4DC485DB1355D78093EAB6B35" { + // t.Fatalf("hash %X", hash) + // } if candidates.GetCandidateControl([32]byte{4}) != [20]byte{3, 3} { t.Fatal("control address is not change") @@ -301,20 +243,13 @@ func TestCandidates_Commit_edit(t *testing.T) { } func TestCandidates_Commit_createOneCandidateWithID(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) candidates.CreateWithID([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 1) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err := mutableTree.SaveVersion() + _, version, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -323,9 +258,9 @@ func TestCandidates_Commit_createOneCandidateWithID(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { + // t.Fatalf("hash %X", hash) + // } id := candidates.ID([32]byte{4}) if id != 1 { @@ -334,22 +269,15 @@ func TestCandidates_Commit_createOneCandidateWithID(t *testing.T) { } func TestCandidates_Commit_Delegate(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } - - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) - hash, version, err := mutableTree.SaveVersion() + _, version, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -358,18 +286,12 @@ func TestCandidates_Commit_Delegate(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { - t.Fatalf("hash %X", hash) - } - + // if fmt.Sprintf("%X", hash) != "FCF3853839873D3EC344016C04A5E75166F51063745670DF5D561C060E7F45A1" { + // t.Fatalf("hash %X", hash) + // } candidates.Delegate([20]byte{1, 1}, [32]byte{4}, 0, big.NewInt(10000000), big.NewInt(10000000)) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err = mutableTree.SaveVersion() + _, version, err = mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -378,27 +300,24 @@ func TestCandidates_Commit_Delegate(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "43FE25EB54D52C6516521FB0F951E87359040A9E8DAA23BDC27C6EC5DFBC10EF" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "43FE25EB54D52C6516521FB0F951E87359040A9E8DAA23BDC27C6EC5DFBC10EF" { + // t.Fatalf("hash %X", hash) + // } } func TestCandidates_SetOnlineAndBusSetOffline(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) - err = candidates.Commit() + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } - candidates.SetOnline([32]byte{4}) candidate := candidates.GetCandidate([32]byte{4}) @@ -415,24 +334,17 @@ func TestCandidates_SetOnlineAndBusSetOffline(t *testing.T) { } func TestCandidates_Count(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) - candidates.Create([20]byte{1, 1}, [20]byte{2, 2}, [20]byte{3, 3}, [32]byte{4, 4}, 20) - candidates.Create([20]byte{1, 1, 1}, [20]byte{2, 2, 2}, [20]byte{3, 3, 3}, [32]byte{4, 4, 4}, 30) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) + candidates.Create([20]byte{1, 1}, [20]byte{2, 2}, [20]byte{3, 3}, [32]byte{4, 4}, 20, 0) + candidates.Create([20]byte{1, 1, 1}, [20]byte{2, 2, 2}, [20]byte{3, 3, 3}, [32]byte{4, 4, 4}, 30, 0) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } - - hash, version, err := mutableTree.SaveVersion() + _, version, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -441,9 +353,9 @@ func TestCandidates_Count(t *testing.T) { t.Fatalf("version %d", version) } - if fmt.Sprintf("%X", hash) != "25F7EF5A007B3D8A5FB4DCE32F9DBC28C2AE6848B893986E3055BC3045E8F00F" { - t.Fatalf("hash %X", hash) - } + // if fmt.Sprintf("%X", hash) != "25F7EF5A007B3D8A5FB4DCE32F9DBC28C2AE6848B893986E3055BC3045E8F00F" { + // t.Fatalf("hash %X", hash) + // } count := candidates.Count() if count != 3 { @@ -452,26 +364,19 @@ func TestCandidates_Count(t *testing.T) { } func TestCandidates_GetTotalStake_fromModelAndFromDB(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - wl, err := waitlist.NewWaitList(b, mutableTree) - if err != nil { - t.Fatal(err) - } + wl := waitlist.NewWaitList(b, mutableTree.GetLastImmutable()) b.SetWaitList(waitlist.NewBus(wl)) b.SetEvents(eventsdb.NewEventsStore(db.NewMemDB())) - accs, err := accounts.NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accs := accounts.NewAccounts(b, mutableTree.GetLastImmutable()) + b.SetAccounts(accounts.NewBus(accs)) b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) var stakes []types.Stake for i := 0; i < 1010; i++ { @@ -500,7 +405,7 @@ func TestCandidates_GetTotalStake_fromModelAndFromDB(t *testing.T) { candidates.RecalculateStakes(0) - err = candidates.Commit() + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -511,10 +416,8 @@ func TestCandidates_GetTotalStake_fromModelAndFromDB(t *testing.T) { t.Fatalf("total stake %s", totalStakeString) } - candidates, err = NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates = NewCandidates(b, mutableTree.GetLastImmutable()) + candidates.LoadCandidates() candidates.GetCandidate([32]byte{4}).totalBipStake = nil totalStake = candidates.GetTotalStake([32]byte{4}) @@ -525,13 +428,11 @@ func TestCandidates_GetTotalStake_fromModelAndFromDB(t *testing.T) { } func TestCandidates_Export(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidates.AddToBlockPubKey([32]byte{10}) candidates.SetStakes([32]byte{4}, []types.Stake{ { @@ -549,7 +450,8 @@ func TestCandidates_Export(t *testing.T) { }, }) candidates.recalculateStakes(0) - err = candidates.Commit() + + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -563,7 +465,7 @@ func TestCandidates_Export(t *testing.T) { } if string(bytes) != "[{\"id\":1,\"reward_address\":\"Mx0200000000000000000000000000000000000000\",\"owner_address\":\"Mx0100000000000000000000000000000000000000\",\"control_address\":\"Mx0300000000000000000000000000000000000000\",\"total_bip_stake\":\"200\",\"public_key\":\"Mp0400000000000000000000000000000000000000000000000000000000000000\",\"commission\":10,\"stakes\":[{\"owner\":\"Mx0100000000000000000000000000000000000000\",\"coin\":0,\"value\":\"100\",\"bip_value\":\"100\"},{\"owner\":\"Mx0200000000000000000000000000000000000000\",\"coin\":0,\"value\":\"100\",\"bip_value\":\"100\"}],\"updates\":[],\"status\":1}]" { - t.Fatal("not equal JSON") + t.Fatal("not equal JSON", string(bytes)) } bytes, err = json.Marshal(state.BlockListCandidates) @@ -577,13 +479,11 @@ func TestCandidates_Export(t *testing.T) { } func TestCandidates_busGetStakes(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidates.SetStakes([32]byte{4}, []types.Stake{ { Owner: [20]byte{1}, @@ -600,11 +500,10 @@ func TestCandidates_busGetStakes(t *testing.T) { }, }) - err = candidates.Commit() + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } - stakes := candidates.bus.Candidates().GetStakes([32]byte{4}) if len(stakes) != 1 { t.Fatalf("stakes count %d", len(stakes)) @@ -616,13 +515,11 @@ func TestCandidates_busGetStakes(t *testing.T) { } func TestCandidates_GetCandidateByTendermintAddress(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidate := candidates.GetCandidate([32]byte{4}) if candidate == nil { @@ -635,13 +532,11 @@ func TestCandidates_GetCandidateByTendermintAddress(t *testing.T) { } } func TestCandidates_busGetCandidateByTendermintAddress(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - candidates, err := NewCandidates(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + candidates := NewCandidates(bus.NewBus(), mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidate := candidates.GetCandidate([32]byte{4}) if candidate == nil { @@ -655,36 +550,24 @@ func TestCandidates_busGetCandidateByTendermintAddress(t *testing.T) { } func TestCandidates_Punish(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - wl, err := waitlist.NewWaitList(b, mutableTree) - if err != nil { - t.Fatal(err) - } + wl := waitlist.NewWaitList(b, mutableTree.GetLastImmutable()) b.SetEvents(eventsdb.NewEventsStore(db.NewMemDB())) b.SetWaitList(waitlist.NewBus(wl)) - accs, err := accounts.NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accs := accounts.NewAccounts(b, mutableTree.GetLastImmutable()) + b.SetAccounts(accounts.NewBus(accs)) - appBus, err := app.NewApp(b, mutableTree) - if err != nil { - t.Fatal(err) - } + appBus := app.NewApp(b, mutableTree.GetLastImmutable()) + b.SetApp(appBus) b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - coinsState, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + coinsState := coins.NewCoins(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) coinsState.Create(1, types.StrToCoinSymbol("AAA"), "AAACOIN", @@ -694,7 +577,7 @@ func TestCandidates_Punish(t *testing.T) { big.NewInt(0).Exp(big.NewInt(10), big.NewInt(10+18), nil), nil) - err = coinsState.Commit() + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -743,38 +626,27 @@ func (fr *fr) AddFrozenFund(_ uint64, _ types.Address, _ types.Pubkey, _ uint32, fr.unbounds = append(fr.unbounds, value) } func TestCandidates_PunishByzantineCandidate(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() frozenfunds := &fr{} b.SetFrozenFunds(frozenfunds) - wl, err := waitlist.NewWaitList(b, mutableTree) - if err != nil { - t.Fatal(err) - } + wl := waitlist.NewWaitList(b, mutableTree.GetLastImmutable()) + b.SetEvents(eventsdb.NewEventsStore(db.NewMemDB())) b.SetWaitList(waitlist.NewBus(wl)) - accs, err := accounts.NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accs := accounts.NewAccounts(b, mutableTree.GetLastImmutable()) + b.SetAccounts(accounts.NewBus(accs)) - appBus, err := app.NewApp(b, mutableTree) - if err != nil { - t.Fatal(err) - } + appBus := app.NewApp(b, mutableTree.GetLastImmutable()) + b.SetApp(appBus) b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - coinsState, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + coinsState := coins.NewCoins(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) coinsState.Create(1, types.StrToCoinSymbol("AAA"), "AAACOIN", @@ -784,7 +656,7 @@ func TestCandidates_PunishByzantineCandidate(t *testing.T) { big.NewInt(0).Exp(big.NewInt(10), big.NewInt(10+18), nil), nil) - err = coinsState.Commit() + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -837,15 +709,13 @@ func TestCandidates_PunishByzantineCandidate(t *testing.T) { } func TestCandidates_SubStake(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidates.SetStakes([32]byte{4}, []types.Stake{ { Owner: [20]byte{1}, @@ -854,7 +724,8 @@ func TestCandidates_SubStake(t *testing.T) { BipValue: "100", }, }, nil) - err = candidates.Commit() + + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -871,15 +742,13 @@ func TestCandidates_SubStake(t *testing.T) { } func TestCandidates_IsNewCandidateStakeSufficient(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidates.SetStakes([32]byte{4}, []types.Stake{ { Owner: [20]byte{1}, @@ -888,37 +757,31 @@ func TestCandidates_IsNewCandidateStakeSufficient(t *testing.T) { BipValue: "100", }, }, nil) - err = candidates.Commit() + + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } - if !candidates.IsNewCandidateStakeSufficient(0, big.NewInt(1000), 1) { t.Log("is not new candidate stake sufficient") } } func TestCandidates_IsDelegatorStakeSufficient(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - wl, err := waitlist.NewWaitList(b, mutableTree) - if err != nil { - t.Fatal(err) - } + wl := waitlist.NewWaitList(b, mutableTree.GetLastImmutable()) + b.SetWaitList(waitlist.NewBus(wl)) b.SetChecker(checker.NewChecker(b)) - accs, err := accounts.NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accs := accounts.NewAccounts(b, mutableTree.GetLastImmutable()) + b.SetAccounts(accounts.NewBus(accs)) b.SetEvents(eventsdb.NewEventsStore(db.NewMemDB())) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) var stakes []types.Stake for i := 0; i < 1010; i++ { @@ -938,7 +801,8 @@ func TestCandidates_IsDelegatorStakeSufficient(t *testing.T) { BipValue: "100", }, }) - err = candidates.Commit() + + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -953,25 +817,20 @@ func TestCandidates_IsDelegatorStakeSufficient(t *testing.T) { }, nil) candidates.recalculateStakes(0) - err = candidates.Commit() - if err != nil { - t.Fatal(err) - } + _, _, err = mutableTree.Commit(candidates) if candidates.IsDelegatorStakeSufficient([20]byte{1}, [32]byte{4}, 0, big.NewInt(10)) { t.Fatal("is not delegator stake sufficient") } } func TestCandidates_IsDelegatorStakeSufficient_false(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidates.SetStakes([32]byte{4}, []types.Stake{ { Owner: [20]byte{1}, @@ -982,7 +841,7 @@ func TestCandidates_IsDelegatorStakeSufficient_false(t *testing.T) { }, nil) candidates.recalculateStakes(0) - err = candidates.Commit() + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } @@ -993,15 +852,13 @@ func TestCandidates_IsDelegatorStakeSufficient_false(t *testing.T) { } func TestCandidates_GetNewCandidates(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidates.SetStakes([32]byte{4}, []types.Stake{ { Owner: [20]byte{1}, @@ -1012,7 +869,7 @@ func TestCandidates_GetNewCandidates(t *testing.T) { }, nil) candidates.SetOnline([32]byte{4}) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{5}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{5}, 10, 0) candidates.SetStakes([32]byte{5}, []types.Stake{ { Owner: [20]byte{1}, @@ -1024,11 +881,11 @@ func TestCandidates_GetNewCandidates(t *testing.T) { candidates.SetOnline([32]byte{5}) candidates.RecalculateStakes(1) - err = candidates.Commit() + + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } - newCandidates := candidates.GetNewCandidates(2) if len(newCandidates) != 2 { t.Fatal("error count of new candidates") @@ -1036,15 +893,13 @@ func TestCandidates_GetNewCandidates(t *testing.T) { } func TestCandidate_GetFilteredUpdates(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidates.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidates.SetStakes([32]byte{4}, []types.Stake{ { Owner: [20]byte{1}, @@ -1066,11 +921,11 @@ func TestCandidate_GetFilteredUpdates(t *testing.T) { BipValue: "100", }, }) - err = candidates.Commit() + + _, _, err := mutableTree.Commit(candidates) if err != nil { t.Fatal(err) } - candidate := candidates.GetCandidate([32]byte{4}) if candidate == nil { t.Fatal("candidate not found") @@ -1088,25 +943,18 @@ func TestCandidate_GetFilteredUpdates(t *testing.T) { } func TestCandidates_CalculateBipValue_RecalculateStakes_GetTotalStake(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - busCoins, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + busCoins := coins.NewCoins(b, mutableTree.GetLastImmutable()) + b.SetCoins(coins.NewBus(busCoins)) - candidates, err := NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidates := NewCandidates(b, mutableTree.GetLastImmutable()) - coinsState, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + coinsState := coins.NewCoins(b, mutableTree.GetLastImmutable()) - candidates.Create([20]byte{1}, [20]byte{1}, [20]byte{1}, [32]byte{1}, 1) + candidates.Create([20]byte{1}, [20]byte{1}, [20]byte{1}, [32]byte{1}, 1, 0) candidates.SetStakes([32]byte{1}, []types.Stake{ { Owner: types.Address{1}, diff --git a/core/state/candidates/candidates.go b/core/state/candidates/candidates.go index 734fa7890..0500ff770 100644 --- a/core/state/candidates/candidates.go +++ b/core/state/candidates/candidates.go @@ -10,7 +10,8 @@ import ( "github.com/MinterTeam/minter-go-node/formula" "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" + "sync/atomic" "math/big" "sort" @@ -22,7 +23,6 @@ const ( CandidateStatusOffline = 0x01 CandidateStatusOnline = 0x02 - UnbondPeriod = 518400 MaxDelegatorsPerCandidate = 1000 ) @@ -70,25 +70,26 @@ type Candidates struct { pubKeyIDs map[types.Pubkey]uint32 maxID uint32 - iavl tree.MTree - bus *bus.Bus + db atomic.Value + bus *bus.Bus lock sync.RWMutex loaded bool isChangedPublicKeys bool } -func (c *Candidates) IsChangedPublicKeys() bool { - return c.isChangedPublicKeys -} -func (c *Candidates) ResetIsChangedPublicKeys() { - c.isChangedPublicKeys = false -} - // NewCandidates returns newly created Candidates state with a given bus and iavl -func NewCandidates(bus *bus.Bus, iavl tree.MTree) (*Candidates, error) { +func NewCandidates(bus *bus.Bus, db *iavl.ImmutableTree) *Candidates { + immutableTree := atomic.Value{} + loaded := false + if db != nil { + immutableTree.Store(db) + } else { + loaded = true + } candidates := &Candidates{ - iavl: iavl, + db: immutableTree, + loaded: loaded, bus: bus, blockList: map[types.Pubkey]struct{}{}, pubKeyIDs: map[types.Pubkey]uint32{}, @@ -96,25 +97,57 @@ func NewCandidates(bus *bus.Bus, iavl tree.MTree) (*Candidates, error) { } candidates.bus.SetCandidates(NewBus(candidates)) - return candidates, nil + return candidates +} + +func (c *Candidates) immutableTree() *iavl.ImmutableTree { + db := c.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) +} + +func (c *Candidates) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + if c.immutableTree() == nil && c.loaded { + c.loaded = false + } + c.db.Store(immutableTree) +} + +func (c *Candidates) IsChangedPublicKeys() bool { + c.lock.RLock() + defer c.lock.RUnlock() + + return c.isChangedPublicKeys +} +func (c *Candidates) ResetIsChangedPublicKeys() { + c.lock.Lock() + defer c.lock.Unlock() + + c.isChangedPublicKeys = false } // Commit writes changes to iavl, may return an error -func (c *Candidates) Commit() error { +func (c *Candidates) Commit(db *iavl.MutableTree) error { keys := c.getOrderedCandidates() hasDirty := false for _, pubkey := range keys { - if c.getFromMap(pubkey).isDirty { + candidate := c.getFromMap(pubkey) + candidate.lock.RLock() + if candidate.isDirty { hasDirty = true + candidate.lock.RUnlock() break } + candidate.lock.RUnlock() } if hasDirty { - var candidates []*Candidate + var candidates []Candidate for _, key := range keys { - candidates = append(candidates, c.getFromMap(key)) + candidates = append(candidates, *c.getFromMap(key)) } data, err := rlp.EncodeToBytes(candidates) if err != nil { @@ -122,9 +155,10 @@ func (c *Candidates) Commit() error { } path := []byte{mainPrefix} - c.iavl.Set(path, data) + db.Set(path, data) } + c.lock.Lock() if c.isDirty { c.isDirty = false var pubIDs []pubkeyID @@ -142,7 +176,7 @@ func (c *Candidates) Commit() error { panic(fmt.Sprintf("failed to encode candidates public key with ID: %s", err)) } - c.iavl.Set([]byte{pubKeyIDPrefix}, pubIDData) + db.Set([]byte{pubKeyIDPrefix}, pubIDData) var blockList []types.Pubkey for pubKey := range c.blockList { @@ -155,38 +189,60 @@ func (c *Candidates) Commit() error { if err != nil { return fmt.Errorf("can't encode block list of candidates: %v", err) } - c.iavl.Set([]byte{blockListPrefix}, blockListData) + db.Set([]byte{blockListPrefix}, blockListData) - c.iavl.Set([]byte{maxIDPrefix}, c.maxIDBytes()) + db.Set([]byte{maxIDPrefix}, c.maxIDBytes()) } + c.lock.Unlock() for _, pubkey := range keys { candidate := c.getFromMap(pubkey) + candidate.lock.Lock() candidate.isDirty = false + dirty := candidate.isTotalStakeDirty + candidate.lock.Unlock() + + if dirty { + candidate.lock.Lock() + candidate.isTotalStakeDirty = false + totalStakeBytes := candidate.totalBipStake.Bytes() + candidate.lock.Unlock() - if candidate.isTotalStakeDirty { path := []byte{mainPrefix} path = append(path, candidate.idBytes()...) path = append(path, totalStakePrefix) - c.iavl.Set(path, candidate.totalBipStake.Bytes()) - candidate.isTotalStakeDirty = false + db.Set(path, totalStakeBytes) } for index, stake := range candidate.stakes { - if !candidate.dirtyStakes[index] { + candidate.lock.RLock() + dirtyStakes := candidate.dirtyStakes[index] + candidate.lock.RUnlock() + if !dirtyStakes { continue } + candidate.lock.Lock() candidate.dirtyStakes[index] = false + candidate.lock.Unlock() path := []byte{mainPrefix} path = append(path, candidate.idBytes()...) path = append(path, stakesPrefix) - path = append(path, []byte(fmt.Sprintf("%d", index))...) // todo big.NewInt(index).Bytes() + path = append(path, big.NewInt(int64(index)).Bytes()...) + + isEmpty := stake == nil + if !isEmpty { + stake.lock.RLock() + isEmpty = stake.Value.Sign() == 0 + stake.lock.RUnlock() + } + if isEmpty { + db.Remove(path) - if stake == nil || stake.Value.Sign() == 0 { - c.iavl.Remove(path) + candidate.lock.Lock() candidate.stakes[index] = nil + candidate.lock.Unlock() continue } @@ -195,11 +251,18 @@ func (c *Candidates) Commit() error { return fmt.Errorf("can't encode stake: %v", err) } - c.iavl.Set(path, data) + db.Set(path, data) } - if candidate.isUpdatesDirty { + candidate.lock.RLock() + updatesDirty := candidate.isUpdatesDirty + candidate.lock.RUnlock() + + if updatesDirty { + candidate.lock.Lock() + candidate.isUpdatesDirty = false data, err := rlp.EncodeToBytes(candidate.updates) + candidate.lock.Unlock() if err != nil { return fmt.Errorf("can't encode candidates updates: %v", err) } @@ -207,8 +270,7 @@ func (c *Candidates) Commit() error { path := []byte{mainPrefix} path = append(path, candidate.idBytes()...) path = append(path, updatesPrefix) - c.iavl.Set(path, data) - candidate.isUpdatesDirty = false + db.Set(path, data) } } @@ -218,23 +280,27 @@ func (c *Candidates) Commit() error { // GetNewCandidates returns list of candidates that can be the new validators // Skips offline candidates and candidates with stake less than minValidatorBipStake // Result is sorted by candidates stakes and limited to valCount -func (c *Candidates) GetNewCandidates(valCount int) []Candidate { - var result []Candidate +func (c *Candidates) GetNewCandidates(valCount int) []*Candidate { + var result []*Candidate candidates := c.GetCandidates() for _, candidate := range candidates { - if candidate.Status == CandidateStatusOffline { + candidate.lock.RLock() + if candidate.Status != CandidateStatusOnline { + candidate.lock.RUnlock() continue } if candidate.totalBipStake.Cmp(minValidatorBipStake) == -1 { + candidate.lock.RUnlock() continue } - result = append(result, *candidate) + candidate.lock.RUnlock() + result = append(result, candidate) } - sort.Slice(result, func(i, j int) bool { + sort.SliceStable(result, func(i, j int) bool { return result[i].totalBipStake.Cmp(result[j].totalBipStake) == 1 }) @@ -246,19 +312,20 @@ func (c *Candidates) GetNewCandidates(valCount int) []Candidate { } // Create creates a new candidate with given params and adds it to state -func (c *Candidates) Create(ownerAddress, rewardAddress, controlAddress types.Address, pubkey types.Pubkey, commission uint32) { +func (c *Candidates) Create(ownerAddress, rewardAddress, controlAddress types.Address, pubkey types.Pubkey, commission uint32, block uint64) { candidate := &Candidate{ - ID: 0, - PubKey: pubkey, - RewardAddress: rewardAddress, - OwnerAddress: ownerAddress, - ControlAddress: controlAddress, - Commission: commission, - Status: CandidateStatusOffline, - totalBipStake: big.NewInt(0), - stakes: [MaxDelegatorsPerCandidate]*stake{}, - isDirty: true, - isTotalStakeDirty: true, + ID: 0, + PubKey: pubkey, + RewardAddress: rewardAddress, + OwnerAddress: ownerAddress, + ControlAddress: controlAddress, + Commission: commission, + LastEditCommissionHeight: block, + Status: CandidateStatusOffline, + totalBipStake: big.NewInt(0), + stakes: [MaxDelegatorsPerCandidate]*stake{}, + isDirty: true, + isTotalStakeDirty: true, } candidate.setTmAddress() @@ -269,7 +336,7 @@ func (c *Candidates) Create(ownerAddress, rewardAddress, controlAddress types.Ad // CreateWithID uses given ID to be associated with public key of a candidate func (c *Candidates) CreateWithID(ownerAddress, rewardAddress, controlAddress types.Address, pubkey types.Pubkey, commission uint32, id uint32) { c.setPubKeyID(pubkey, id) - c.Create(ownerAddress, rewardAddress, controlAddress, pubkey, commission) + c.Create(ownerAddress, rewardAddress, controlAddress, pubkey, commission, 0) } // PunishByzantineCandidate finds candidate with given tmAddress and punishes it: @@ -301,7 +368,7 @@ func (c *Candidates) PunishByzantineCandidate(height uint64, tmAddress types.TmA c.bus.Checker().AddCoin(stake.Coin, big.NewInt(0).Neg(slashed)) - c.bus.Events().AddEvent(uint32(height), &eventsdb.SlashEvent{ + c.bus.Events().AddEvent(&eventsdb.SlashEvent{ Address: stake.Owner, Amount: slashed.String(), Coin: uint64(stake.Coin), @@ -309,7 +376,7 @@ func (c *Candidates) PunishByzantineCandidate(height uint64, tmAddress types.TmA }) c.bus.Checker().AddCoin(stake.Coin, big.NewInt(0).Neg(newValue)) - c.bus.FrozenFunds().AddFrozenFund(height+UnbondPeriod, stake.Owner, candidate.PubKey, candidate.ID, stake.Coin, newValue) + c.bus.FrozenFunds().AddFrozenFund(height+types.GetUnbondPeriod(), stake.Owner, candidate.PubKey, candidate.ID, stake.Coin, newValue) stake.setValue(big.NewInt(0)) } } @@ -408,7 +475,7 @@ func (c *Candidates) recalculateStakes(height uint64) { func (c *Candidates) stakeKick(owner types.Address, value *big.Int, coin types.CoinID, pubKey types.Pubkey, height uint64) { c.bus.WaitList().AddToWaitList(owner, pubKey, coin, value) - c.bus.Events().AddEvent(uint32(height), &eventsdb.StakeKickEvent{ + c.bus.Events().AddEvent(&eventsdb.StakeKickEvent{ Address: owner, Amount: value.String(), Coin: uint64(coin), @@ -519,6 +586,12 @@ func (c *Candidates) Edit(pubkey types.Pubkey, rewardAddress types.Address, owne candidate.setControl(controlAddress) } +// EditCommission edits a candidate commission +func (c *Candidates) EditCommission(pubkey types.Pubkey, commission uint32, height uint64) { + candidate := c.getFromMap(pubkey) + candidate.setCommission(commission, height) +} + // SetOnline sets candidate status to CandidateStatusOnline func (c *Candidates) SetOnline(pubkey types.Pubkey) { c.getFromMap(pubkey).setStatus(CandidateStatusOnline) @@ -548,20 +621,27 @@ func (c *Candidates) GetCandidates() []*Candidate { // GetTotalStake calculates and returns total stake of a candidate func (c *Candidates) GetTotalStake(pubkey types.Pubkey) *big.Int { candidate := c.getFromMap(pubkey) - if candidate.totalBipStake == nil { + candidate.lock.RLock() + notLoaded := candidate.totalBipStake == nil + candidate.lock.RUnlock() + if notLoaded { path := []byte{mainPrefix} path = append(path, candidate.idBytes()...) path = append(path, totalStakePrefix) - _, enc := c.iavl.Get(path) - if len(enc) == 0 { - candidate.totalBipStake = big.NewInt(0) - return big.NewInt(0) - } + _, enc := c.immutableTree().Get(path) - candidate.totalBipStake = big.NewInt(0).SetBytes(enc) + candidate.lock.Lock() + candidate.totalBipStake = big.NewInt(0) + if len(enc) != 0 { + candidate.totalBipStake.SetBytes(enc) + } + candidate.lock.Unlock() } - return candidate.totalBipStake + candidate.lock.RLock() + defer candidate.lock.RUnlock() + + return big.NewInt(0).Set(candidate.totalBipStake) } // GetStakes returns list of stakes of candidate with given public key @@ -633,7 +713,7 @@ func (c *Candidates) LoadCandidatesDeliver() { c.maxID = c.loadCandidatesList() - _, blockListEnc := c.iavl.Get([]byte{blockListPrefix}) + _, blockListEnc := c.immutableTree().Get([]byte{blockListPrefix}) if len(blockListEnc) != 0 { var blockList []types.Pubkey if err := rlp.DecodeBytes(blockListEnc, &blockList); err != nil { @@ -647,7 +727,7 @@ func (c *Candidates) LoadCandidatesDeliver() { c.setBlockList(blockListMap) } - _, valueMaxID := c.iavl.Get([]byte{maxIDPrefix}) + _, valueMaxID := c.immutableTree().Get([]byte{maxIDPrefix}) if len(valueMaxID) != 0 { c.maxID = binary.LittleEndian.Uint32(valueMaxID) } @@ -655,7 +735,7 @@ func (c *Candidates) LoadCandidatesDeliver() { } func (c *Candidates) loadCandidatesList() (maxID uint32) { - _, pubIDenc := c.iavl.Get([]byte{pubKeyIDPrefix}) + _, pubIDenc := c.immutableTree().Get([]byte{pubKeyIDPrefix}) if len(pubIDenc) != 0 { var pubIDs []pubkeyID if err := rlp.DecodeBytes(pubIDenc, &pubIDs); err != nil { @@ -673,18 +753,18 @@ func (c *Candidates) loadCandidatesList() (maxID uint32) { } path := []byte{mainPrefix} - _, enc := c.iavl.Get(path) + _, enc := c.immutableTree().Get(path) if len(enc) != 0 { var candidates []*Candidate if err := rlp.DecodeBytes(enc, &candidates); err != nil { panic(fmt.Sprintf("failed to decode candidates: %s", err)) } - for _, candidate := range candidates { // load total stake path = append([]byte{mainPrefix}, candidate.idBytes()...) path = append(path, totalStakePrefix) - _, enc = c.iavl.Get(path) + _, enc = c.immutableTree().Get(path) + if len(enc) == 0 { candidate.totalBipStake = big.NewInt(0) } else { @@ -706,6 +786,7 @@ func (c *Candidates) checkAndSetLoaded() bool { return true } c.lock.RUnlock() + c.lock.Lock() c.loaded = true c.lock.Unlock() @@ -769,7 +850,6 @@ func (c *Candidates) calculateBipValue(coinID types.CoinID, amount *big.Int, inc // 1. Subs 1% from each stake // 2. Calculate and return new total stake func (c *Candidates) Punish(height uint64, address types.TmAddress) *big.Int { - candidate := c.GetCandidateByTendermintAddress(address) totalStake := new(big.Int).Set(candidate.totalBipStake) stakes := c.GetStakes(candidate.PubKey) @@ -794,10 +874,9 @@ func (c *Candidates) Punish(height uint64, address types.TmAddress) *big.Int { c.bus.App().AddTotalSlashed(slashed) totalStake.Sub(totalStake, slashed) } - c.bus.Checker().AddCoin(stake.Coin, big.NewInt(0).Neg(slashed)) - c.bus.Events().AddEvent(uint32(height), &eventsdb.SlashEvent{ + c.bus.Events().AddEvent(&eventsdb.SlashEvent{ Address: stake.Owner, Amount: slashed.String(), Coin: uint64(stake.Coin), @@ -907,12 +986,13 @@ func (c *Candidates) Export(state *types.AppState) { func (c *Candidates) getOrderedCandidates() []types.Pubkey { c.lock.RLock() - defer c.lock.RUnlock() - var keys []types.Pubkey for _, candidate := range c.list { + candidate.lock.RLock() keys = append(keys, candidate.PubKey) + candidate.lock.RUnlock() } + c.lock.RUnlock() sort.SliceStable(keys, func(i, j int) bool { return bytes.Compare(keys[i].Bytes(), keys[j].Bytes()) == 1 @@ -929,10 +1009,15 @@ func (c *Candidates) getFromMap(pubkey types.Pubkey) *Candidate { } func (c *Candidates) setToMap(pubkey types.Pubkey, model *Candidate) { + model.lock.RLock() id := model.ID + model.lock.RUnlock() if id == 0 { id = c.getOrNewID(pubkey) + + model.lock.Lock() model.ID = id + model.lock.Unlock() } c.lock.Lock() @@ -970,10 +1055,12 @@ func (c *Candidates) LoadStakesOfCandidate(pubkey types.Pubkey) { path := []byte{mainPrefix} path = append(path, candidate.idBytes()...) path = append(path, stakesPrefix) - path = append(path, []byte(fmt.Sprintf("%d", index))...) - _, enc := c.iavl.Get(path) + path = append(path, big.NewInt(int64(index)).Bytes()...) + _, enc := c.immutableTree().Get(path) if len(enc) == 0 { + candidate.lock.Lock() candidate.stakes[index] = nil + candidate.lock.Unlock() continue } @@ -987,13 +1074,17 @@ func (c *Candidates) LoadStakesOfCandidate(pubkey types.Pubkey) { stakesCount++ } + candidate.lock.Lock() candidate.stakesCount = stakesCount + candidate.lock.Unlock() // load updates path := []byte{mainPrefix} path = append(path, candidate.idBytes()...) path = append(path, updatesPrefix) - _, enc := c.iavl.Get(path) + _, enc := c.immutableTree().Get(path) + + candidate.lock.Lock() if len(enc) == 0 { candidate.updates = nil } else { @@ -1005,6 +1096,8 @@ func (c *Candidates) LoadStakesOfCandidate(pubkey types.Pubkey) { for _, update := range updates { update.markDirty = (func(candidate *Candidate) func(int) { return func(i int) { + candidate.lock.Lock() + defer candidate.lock.Unlock() candidate.isUpdatesDirty = true } })(candidate) @@ -1012,16 +1105,20 @@ func (c *Candidates) LoadStakesOfCandidate(pubkey types.Pubkey) { candidate.updates = updates } + candidate.lock.Unlock() // load total stake path = append([]byte{mainPrefix}, candidate.idBytes()...) path = append(path, totalStakePrefix) - _, enc = c.iavl.Get(path) + _, enc = c.immutableTree().Get(path) + + candidate.lock.Lock() if len(enc) == 0 { candidate.totalBipStake = big.NewInt(0) } else { candidate.totalBipStake = big.NewInt(0).SetBytes(enc) } + candidate.lock.Unlock() candidate.setTmAddress() c.setToMap(candidate.PubKey, candidate) @@ -1035,9 +1132,17 @@ func (c *Candidates) ChangePubKey(old types.Pubkey, new types.Pubkey) { c.getFromMap(old).setPublicKey(new) c.setBlockPubKey(old) - c.setPubKeyID(new, c.pubKeyIDs[old]) + + c.lock.RLock() + id := c.pubKeyIDs[old] + c.lock.RUnlock() + + c.setPubKeyID(new, id) + + c.lock.Lock() delete(c.pubKeyIDs, old) c.isChangedPublicKeys = true + c.lock.Unlock() } func (c *Candidates) getOrNewID(pubKey types.Pubkey) uint32 { @@ -1051,9 +1156,9 @@ func (c *Candidates) getOrNewID(pubKey types.Pubkey) uint32 { c.lock.Lock() c.isDirty = true c.maxID++ + id = c.maxID c.lock.Unlock() - id = c.maxID c.setPubKeyID(pubKey, id) return id } @@ -1080,6 +1185,9 @@ func (c *Candidates) PubKey(id uint32) types.Pubkey { panic(fmt.Sprintf("candidate by ID %d not found", id)) } + candidate.lock.RLock() + defer candidate.lock.RUnlock() + return candidate.PubKey } diff --git a/core/state/candidates/model.go b/core/state/candidates/model.go index b32f5edcf..a12720167 100644 --- a/core/state/candidates/model.go +++ b/core/state/candidates/model.go @@ -6,6 +6,7 @@ import ( "github.com/tendermint/tendermint/crypto/ed25519" "math/big" "sort" + "sync" ) type pubkeyID struct { @@ -15,67 +16,105 @@ type pubkeyID struct { // Candidate represents candidate object which is stored on disk type Candidate struct { - PubKey types.Pubkey - RewardAddress types.Address - OwnerAddress types.Address - ControlAddress types.Address - Commission uint32 - Status byte - ID uint32 - totalBipStake *big.Int stakesCount int stakes [MaxDelegatorsPerCandidate]*stake updates []*stake tmAddress *types.TmAddress + lock sync.RWMutex isDirty bool isTotalStakeDirty bool isUpdatesDirty bool dirtyStakes [MaxDelegatorsPerCandidate]bool + + PubKey types.Pubkey + RewardAddress types.Address + OwnerAddress types.Address + ControlAddress types.Address + Commission uint32 + Status byte + ID uint32 + LastEditCommissionHeight uint64 } func (candidate *Candidate) idBytes() []byte { bs := make([]byte, 4) + + candidate.lock.RLock() + defer candidate.lock.RUnlock() + binary.LittleEndian.PutUint32(bs, candidate.ID) return bs } func (candidate *Candidate) setStatus(status byte) { + candidate.lock.Lock() + defer candidate.lock.Unlock() + candidate.isDirty = true candidate.Status = status } func (candidate *Candidate) setOwner(address types.Address) { + candidate.lock.Lock() + defer candidate.lock.Unlock() + candidate.isDirty = true candidate.OwnerAddress = address } +func (candidate *Candidate) setCommission(commission uint32, height uint64) { + candidate.lock.Lock() + defer candidate.lock.Unlock() + + candidate.isDirty = true + candidate.Commission = commission + candidate.LastEditCommissionHeight = height +} + func (candidate *Candidate) setReward(address types.Address) { + candidate.lock.Lock() + defer candidate.lock.Unlock() + candidate.isDirty = true candidate.RewardAddress = address } func (candidate *Candidate) setControl(address types.Address) { + candidate.lock.Lock() + defer candidate.lock.Unlock() + candidate.isDirty = true candidate.ControlAddress = address } func (candidate *Candidate) setPublicKey(pubKey types.Pubkey) { + candidate.lock.Lock() candidate.isDirty = true candidate.PubKey = pubKey + candidate.lock.Unlock() + candidate.setTmAddress() } func (candidate *Candidate) addUpdate(stake *stake) { + candidate.lock.Lock() + defer candidate.lock.Unlock() + candidate.isUpdatesDirty = true stake.markDirty = func(i int) { + candidate.lock.Lock() + defer candidate.lock.Unlock() candidate.isUpdatesDirty = true } candidate.updates = append(candidate.updates, stake) } func (candidate *Candidate) clearUpdates() { + candidate.lock.Lock() + defer candidate.lock.Unlock() + if len(candidate.updates) != 0 { candidate.isUpdatesDirty = true } @@ -84,6 +123,9 @@ func (candidate *Candidate) clearUpdates() { } func (candidate *Candidate) setTotalBipStake(totalBipValue *big.Int) { + candidate.lock.Lock() + defer candidate.lock.Unlock() + if totalBipValue.Cmp(candidate.totalBipStake) != 0 { candidate.isTotalStakeDirty = true } @@ -93,39 +135,53 @@ func (candidate *Candidate) setTotalBipStake(totalBipValue *big.Int) { // GetTmAddress returns tendermint-address of a candidate func (candidate *Candidate) GetTmAddress() types.TmAddress { + candidate.lock.RLock() + defer candidate.lock.RUnlock() + return *candidate.tmAddress } func (candidate *Candidate) setTmAddress() { - // set tm address - var pubkey ed25519.PubKeyEd25519 - copy(pubkey[:], candidate.PubKey[:]) + candidate.lock.Lock() + defer candidate.lock.Unlock() var address types.TmAddress - copy(address[:], pubkey.Address().Bytes()) + copy(address[:], ed25519.PubKey(candidate.PubKey[:]).Address().Bytes()) candidate.tmAddress = &address } // getFilteredUpdates returns updates which is > 0 in their value + merge similar updates func (candidate *Candidate) getFilteredUpdates() []*stake { + candidate.lock.RLock() + defer candidate.lock.RUnlock() + var updates []*stake for _, update := range candidate.updates { // skip updates with 0 stakes + update.lock.RLock() if update.Value.Cmp(big.NewInt(0)) != 1 { + update.lock.RUnlock() continue } // merge updates merged := false for _, u := range updates { + + u.lock.Lock() if u.Coin == update.Coin && u.Owner == update.Owner { u.Value = big.NewInt(0).Add(u.Value, update.Value) + u.lock.Unlock() + merged = true break } + u.lock.Unlock() } + update.lock.RUnlock() + if !merged { updates = append(updates, update) } @@ -136,9 +192,13 @@ func (candidate *Candidate) getFilteredUpdates() []*stake { // filterUpdates filters candidate updates: remove 0-valued updates and merge similar ones func (candidate *Candidate) filterUpdates() { + + candidate.lock.RLock() if len(candidate.updates) == 0 { + candidate.lock.RUnlock() return } + candidate.lock.RUnlock() updates := candidate.getFilteredUpdates() @@ -146,22 +206,32 @@ func (candidate *Candidate) filterUpdates() { return updates[i].BipValue.Cmp(updates[j].BipValue) == 1 }) + candidate.lock.Lock() candidate.updates = updates candidate.isUpdatesDirty = true + candidate.lock.Unlock() } // GetTotalBipStake returns total stake value of a candidate func (candidate *Candidate) GetTotalBipStake() *big.Int { + candidate.lock.RLock() + defer candidate.lock.RUnlock() + return big.NewInt(0).Set(candidate.totalBipStake) } func (candidate *Candidate) setStakeAtIndex(index int, stake *stake, isDirty bool) { + stake.markDirty = func(i int) { + candidate.lock.Lock() + defer candidate.lock.Unlock() candidate.dirtyStakes[i] = true } stake.index = index + candidate.lock.Lock() candidate.stakes[index] = stake + candidate.lock.Unlock() if isDirty { stake.markDirty(index) @@ -176,19 +246,31 @@ type stake struct { index int markDirty func(int) + lock sync.RWMutex } func (stake *stake) addValue(value *big.Int) { stake.markDirty(stake.index) + + stake.lock.Lock() + defer stake.lock.Unlock() + stake.Value = big.NewInt(0).Add(stake.Value, value) } func (stake *stake) subValue(value *big.Int) { stake.markDirty(stake.index) + + stake.lock.Lock() + defer stake.lock.Unlock() + stake.Value = big.NewInt(0).Sub(stake.Value, value) } func (stake *stake) setBipValue(value *big.Int) { + stake.lock.Lock() + defer stake.lock.Unlock() + if stake.BipValue.Cmp(value) != 0 { stake.markDirty(stake.index) } @@ -198,5 +280,9 @@ func (stake *stake) setBipValue(value *big.Int) { func (stake *stake) setValue(ret *big.Int) { stake.markDirty(stake.index) + + stake.lock.Lock() + defer stake.lock.Unlock() + stake.Value = big.NewInt(0).Set(ret) } diff --git a/core/state/candidates_test.go b/core/state/candidates_test.go index bd6770686..5579bab72 100644 --- a/core/state/candidates_test.go +++ b/core/state/candidates_test.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "encoding/binary" eventsdb "github.com/MinterTeam/minter-go-node/core/events" - "github.com/MinterTeam/minter-go-node/core/state/candidates" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/helpers" "github.com/tendermint/tendermint/crypto/ed25519" @@ -13,9 +12,8 @@ import ( "testing" ) -const height = 1 - func TestSimpleDelegate(t *testing.T) { + t.Parallel() st := getState() address := types.Address{} @@ -24,7 +22,7 @@ func TestSimpleDelegate(t *testing.T) { pubkey := createTestCandidate(st) st.Candidates.Delegate(address, pubkey, coin, amount, big.NewInt(0)) - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) stake := st.Candidates.GetStakeOfAddress(pubkey, address, coin) if stake == nil { @@ -46,6 +44,7 @@ func TestSimpleDelegate(t *testing.T) { } func TestDelegate(t *testing.T) { + t.Parallel() st := getState() address := types.Address{} @@ -59,7 +58,7 @@ func TestDelegate(t *testing.T) { totalAmount.Add(totalAmount, amount) } - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) stake := st.Candidates.GetStakeOfAddress(pubkey, address, coin) if stake == nil { @@ -81,6 +80,7 @@ func TestDelegate(t *testing.T) { } func TestCustomDelegate(t *testing.T) { + t.Parallel() st := getState() volume := helpers.BipToPip(big.NewInt(1000000)) @@ -96,7 +96,7 @@ func TestCustomDelegate(t *testing.T) { pubkey := createTestCandidate(st) st.Candidates.Delegate(address, pubkey, coinID, amount, big.NewInt(0)) - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) stake := st.Candidates.GetStakeOfAddress(pubkey, address, coinID) if stake == nil { @@ -119,6 +119,7 @@ func TestCustomDelegate(t *testing.T) { } func TestComplexDelegate(t *testing.T) { + t.Parallel() st := getState() coin := types.GetBaseCoinID() @@ -131,7 +132,7 @@ func TestComplexDelegate(t *testing.T) { st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) } - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) for i := uint64(0); i < 1000; i++ { var addr types.Address @@ -167,7 +168,7 @@ func TestComplexDelegate(t *testing.T) { binary.BigEndian.PutUint64(addr[:], 3000) st.Candidates.Delegate(addr, pubkey, coin, big.NewInt(3000), big.NewInt(0)) - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) replacedAddress := types.HexToAddress("Mx00000000000003e7000000000000000000000000") stake := st.Candidates.GetStakeOfAddress(pubkey, replacedAddress, coin) @@ -190,7 +191,7 @@ func TestComplexDelegate(t *testing.T) { binary.BigEndian.PutUint64(addr2[:], 3500) st.Candidates.Delegate(addr2, pubkey, coin, big.NewInt(3500), big.NewInt(0)) - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) stake := st.Candidates.GetStakeOfAddress(pubkey, addr, coin) if stake == nil { @@ -209,7 +210,7 @@ func TestComplexDelegate(t *testing.T) { binary.BigEndian.PutUint64(addr[:], 4001) st.Candidates.Delegate(addr, pubkey, coin, big.NewInt(900), big.NewInt(0)) - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) stake := st.Candidates.GetStakeOfAddress(pubkey, addr, coin) if stake != nil { @@ -224,6 +225,7 @@ func TestComplexDelegate(t *testing.T) { } func TestStakeSufficiency(t *testing.T) { + t.Parallel() st := getState() coin := types.GetBaseCoinID() @@ -236,7 +238,7 @@ func TestStakeSufficiency(t *testing.T) { st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) } - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) { stake := big.NewInt(1) @@ -278,6 +280,7 @@ func TestStakeSufficiency(t *testing.T) { } func TestDoubleSignPenalty(t *testing.T) { + t.Parallel() st := getState() pubkey := createTestCandidate(st) @@ -288,18 +291,15 @@ func TestDoubleSignPenalty(t *testing.T) { binary.BigEndian.PutUint64(addr[:], 1) st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) - st.Candidates.RecalculateStakes(height) - - st.FrozenFunds.AddFund(1, addr, pubkey, st.Candidates.ID(pubkey), coin, amount) + st.Candidates.RecalculateStakes(0) - var pk ed25519.PubKeyEd25519 - copy(pk[:], pubkey[:]) + st.FrozenFunds.AddFund(1, addr, pubkey, st.Candidates.ID(pubkey), coin, amount, nil) var tmAddr types.TmAddress - copy(tmAddr[:], pk.Address().Bytes()) + copy(tmAddr[:], ed25519.PubKey(pubkey[:]).Address().Bytes()) st.Validators.PunishByzantineValidator(tmAddr) - st.FrozenFunds.PunishFrozenFundsWithID(1, 1+candidates.UnbondPeriod, st.Candidates.ID(pubkey)) + st.FrozenFunds.PunishFrozenFundsWithID(1, 1+types.GetUnbondPeriod(), st.Candidates.ID(pubkey)) st.Candidates.PunishByzantineCandidate(1, tmAddr) stake := st.Candidates.GetStakeValueOfAddress(pubkey, addr, coin) @@ -307,7 +307,7 @@ func TestDoubleSignPenalty(t *testing.T) { t.Fatalf("Stake is not correct. Expected 0, got %s", stake.String()) } - ffs := st.FrozenFunds.GetFrozenFunds(1 + candidates.UnbondPeriod) + ffs := st.FrozenFunds.GetFrozenFunds(1 + types.GetUnbondPeriod()) exists := false for _, ff := range ffs.List { if ff.Address == addr { @@ -334,6 +334,7 @@ func TestDoubleSignPenalty(t *testing.T) { } func TestAbsentPenalty(t *testing.T) { + t.Parallel() st := getState() pubkey := createTestCandidate(st) @@ -344,13 +345,10 @@ func TestAbsentPenalty(t *testing.T) { binary.BigEndian.PutUint64(addr[:], 1) st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) - st.Candidates.RecalculateStakes(height) - - var pk ed25519.PubKeyEd25519 - copy(pk[:], pubkey[:]) + st.Candidates.RecalculateStakes(0) var tmAddr types.TmAddress - copy(tmAddr[:], pk.Address().Bytes()) + copy(tmAddr[:], ed25519.PubKey(pubkey[:]).Address().Bytes()) st.Candidates.Punish(1, tmAddr) @@ -369,6 +367,7 @@ func TestAbsentPenalty(t *testing.T) { } func TestDoubleAbsentPenalty(t *testing.T) { + t.Parallel() st := getState() pubkey := createTestCandidate(st) @@ -380,13 +379,10 @@ func TestDoubleAbsentPenalty(t *testing.T) { st.Candidates.Delegate(addr, pubkey, coin, amount, amount) st.Candidates.SetOnline(pubkey) - st.Candidates.RecalculateStakes(height) - - var pk ed25519.PubKeyEd25519 - copy(pk[:], pubkey[:]) + st.Candidates.RecalculateStakes(0) var tmAddr types.TmAddress - copy(tmAddr[:], pk.Address().Bytes()) + copy(tmAddr[:], ed25519.PubKey(pubkey[:]).Address().Bytes()) st.Validators.SetNewValidators(st.Candidates.GetNewCandidates(1)) @@ -412,6 +408,7 @@ func TestDoubleAbsentPenalty(t *testing.T) { } func TestZeroStakePenalty(t *testing.T) { + t.Parallel() st := getState() pubkey := createTestCandidate(st) @@ -422,16 +419,13 @@ func TestZeroStakePenalty(t *testing.T) { binary.BigEndian.PutUint64(addr[:], 1) st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) st.Candidates.SubStake(addr, pubkey, coin, amount) - st.FrozenFunds.AddFund(518400, addr, pubkey, st.Candidates.ID(pubkey), coin, amount) - - var pk ed25519.PubKeyEd25519 - copy(pk[:], pubkey[:]) + st.FrozenFunds.AddFund(518400, addr, pubkey, st.Candidates.ID(pubkey), coin, amount, nil) var tmAddr types.TmAddress - copy(tmAddr[:], pk.Address().Bytes()) + copy(tmAddr[:], ed25519.PubKey(pubkey[:]).Address().Bytes()) st.Candidates.Punish(1, tmAddr) @@ -449,6 +443,7 @@ func TestZeroStakePenalty(t *testing.T) { } func TestDelegationAfterUnbond(t *testing.T) { + t.Parallel() st := getState() coin := types.GetBaseCoinID() @@ -461,7 +456,7 @@ func TestDelegationAfterUnbond(t *testing.T) { st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) } - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) // unbond { @@ -470,8 +465,8 @@ func TestDelegationAfterUnbond(t *testing.T) { amount := big.NewInt(int64(1000 - 2)) st.Candidates.SubStake(addr, pubkey, coin, amount) - st.Candidates.RecalculateStakes(height) - if err := st.Candidates.Commit(); err != nil { + st.Candidates.RecalculateStakes(0) + if _, _, err := st.tree.Commit(st.Candidates); err != nil { panic(err) } } @@ -483,7 +478,7 @@ func TestDelegationAfterUnbond(t *testing.T) { amount := big.NewInt(2000) st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) value := st.Candidates.GetStakeValueOfAddress(pubkey, addr, coin) if value == nil || value.Cmp(amount) != 0 { @@ -513,6 +508,7 @@ func TestDelegationAfterUnbond(t *testing.T) { } func TestStakeKick(t *testing.T) { + t.Parallel() st := getState() coin := types.GetBaseCoinID() @@ -525,7 +521,7 @@ func TestStakeKick(t *testing.T) { st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) } - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) { amount := big.NewInt(1001) @@ -534,7 +530,7 @@ func TestStakeKick(t *testing.T) { st.Candidates.Delegate(addr, pubkey, coin, amount, big.NewInt(0)) } - st.Candidates.RecalculateStakes(height) + st.Candidates.RecalculateStakes(0) var addr types.Address binary.BigEndian.PutUint64(addr[:], 999) @@ -555,6 +551,7 @@ func TestStakeKick(t *testing.T) { } func TestRecalculateStakes(t *testing.T) { + t.Parallel() st := getState() st.Coins.Create(1, [10]byte{1}, "TestCoin", helpers.BipToPip(big.NewInt(100000)), 70, helpers.BipToPip(big.NewInt(10000)), nil, nil) @@ -564,8 +561,8 @@ func TestRecalculateStakes(t *testing.T) { amount := helpers.BipToPip(big.NewInt(1000)) st.Candidates.Delegate([20]byte{1}, pubkey, 1, amount, big.NewInt(0)) - st.Candidates.RecalculateStakes(height) - err := st.Candidates.Commit() + st.Candidates.RecalculateStakes(0) + _, _, err := st.tree.Commit(st.Candidates) if err != nil { t.Fatal(err) } @@ -585,7 +582,7 @@ func TestRecalculateStakes(t *testing.T) { } func getState() *State { - s, err := NewState(0, db.NewMemDB(), emptyEvents{}, 1, 1) + s, err := NewState(0, db.NewMemDB(), eventsdb.MockEvents{}, 1, 1, 0) if err != nil { panic(err) @@ -599,7 +596,7 @@ func checkState(cState *State) error { return err } - exportedState := cState.Export(height) + exportedState := cState.Export() if err := exportedState.Verify(); err != nil { return err } @@ -613,13 +610,7 @@ func createTestCandidate(stateDB *State) types.Pubkey { _, _ = rand.Read(pubkey[:]) stateDB.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1000))) - stateDB.Candidates.Create(address, address, address, pubkey, 10) + stateDB.Candidates.Create(address, address, address, pubkey, 10, 0) return pubkey } - -type emptyEvents struct{} - -func (e emptyEvents) AddEvent(height uint32, event eventsdb.Event) {} -func (e emptyEvents) LoadEvents(height uint32) eventsdb.Events { return eventsdb.Events{} } -func (e emptyEvents) CommitEvents() error { return nil } diff --git a/core/state/checker/checker.go b/core/state/checker/checker.go index 8dcd7e327..cc91a1e4d 100644 --- a/core/state/checker/checker.go +++ b/core/state/checker/checker.go @@ -1,14 +1,18 @@ package checker import ( + "fmt" "github.com/MinterTeam/minter-go-node/core/state/bus" "github.com/MinterTeam/minter-go-node/core/types" "math/big" + "sync" ) type Checker struct { delta map[types.CoinID]*big.Int volumeDelta map[types.CoinID]*big.Int + + lock sync.RWMutex } func NewChecker(bus *bus.Bus) *Checker { @@ -22,6 +26,9 @@ func NewChecker(bus *bus.Bus) *Checker { } func (c *Checker) AddCoin(coin types.CoinID, value *big.Int, msg ...string) { + c.lock.Lock() + defer c.lock.Unlock() + cValue, exists := c.delta[coin] if !exists { @@ -33,6 +40,9 @@ func (c *Checker) AddCoin(coin types.CoinID, value *big.Int, msg ...string) { } func (c *Checker) AddCoinVolume(coin types.CoinID, value *big.Int) { + c.lock.Lock() + defer c.lock.Unlock() + cValue, exists := c.volumeDelta[coin] if !exists { @@ -44,14 +54,35 @@ func (c *Checker) AddCoinVolume(coin types.CoinID, value *big.Int) { } func (c *Checker) Reset() { + c.lock.Lock() + defer c.lock.Unlock() c.delta = map[types.CoinID]*big.Int{} c.volumeDelta = map[types.CoinID]*big.Int{} } -func (c *Checker) Deltas() map[types.CoinID]*big.Int { +func (c *Checker) deltas() map[types.CoinID]*big.Int { return c.delta } -func (c *Checker) VolumeDeltas() map[types.CoinID]*big.Int { +func (c *Checker) volumeDeltas() map[types.CoinID]*big.Int { return c.volumeDelta } + +func (c *Checker) Check() error { + c.lock.RLock() + defer c.lock.RUnlock() + + volumeDeltas := c.volumeDeltas() + for coin, delta := range c.deltas() { + volume := volumeDeltas[coin] + if volume == nil { + volume = big.NewInt(0) + } + + if delta.Cmp(volume) != 0 { + return fmt.Errorf("invariants error on coin %s: %s", coin.String(), big.NewInt(0).Sub(volume, delta).String()) + } + } + + return nil +} diff --git a/core/state/checks/checks.go b/core/state/checks/checks.go index 6c1843717..f14289bf3 100644 --- a/core/state/checks/checks.go +++ b/core/state/checks/checks.go @@ -5,9 +5,10 @@ import ( "fmt" "github.com/MinterTeam/minter-go-node/core/check" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" "sort" "sync" + "sync/atomic" ) const mainPrefix = byte('t') @@ -20,23 +21,40 @@ type RChecks interface { type Checks struct { usedChecks map[types.Hash]struct{} - iavl tree.MTree + db atomic.Value lock sync.RWMutex } -func NewChecks(iavl tree.MTree) (*Checks, error) { - return &Checks{iavl: iavl, usedChecks: map[types.Hash]struct{}{}}, nil +func NewChecks(db *iavl.ImmutableTree) *Checks { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } + return &Checks{db: immutableTree, usedChecks: map[types.Hash]struct{}{}} +} + +func (c *Checks) immutableTree() *iavl.ImmutableTree { + db := c.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) } -func (c *Checks) Commit() error { - for _, hash := range c.getOrderedHashes() { +func (c *Checks) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + c.db.Store(immutableTree) +} + +func (c *Checks) Commit(db *iavl.MutableTree) error { + hashes := c.getOrderedHashes() + for _, hash := range hashes { c.lock.Lock() delete(c.usedChecks, hash) c.lock.Unlock() trieHash := append([]byte{mainPrefix}, hash.Bytes()...) - c.iavl.Set(trieHash, []byte{0x1}) + db.Set(trieHash, []byte{0x1}) } return nil @@ -50,7 +68,7 @@ func (c *Checks) IsCheckUsed(check *check.Check) bool { return true } - _, data := c.iavl.Get(append([]byte{mainPrefix}, check.Hash().Bytes()...)) + _, data := c.immutableTree().Get(append([]byte{mainPrefix}, check.Hash().Bytes()...)) return len(data) != 0 } @@ -67,12 +85,8 @@ func (c *Checks) UseCheckHash(hash types.Hash) { } func (c *Checks) Export(state *types.AppState) { - // todo: iterate range? - c.iavl.Iterate(func(key []byte, value []byte) bool { - if key[0] == mainPrefix { - state.UsedChecks = append(state.UsedChecks, types.UsedCheck(fmt.Sprintf("%x", key[1:]))) - } - + c.immutableTree().IterateRange([]byte{mainPrefix}, []byte{mainPrefix + 1}, true, func(key []byte, value []byte) bool { + state.UsedChecks = append(state.UsedChecks, types.UsedCheck(fmt.Sprintf("%x", key[1:]))) return false }) } diff --git a/core/state/coins/coins.go b/core/state/coins/coins.go index 6bb4906d6..ff3e6c54b 100644 --- a/core/state/coins/coins.go +++ b/core/state/coins/coins.go @@ -6,10 +6,11 @@ import ( "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" "math/big" "sort" "sync" + "sync/atomic" ) const ( @@ -75,15 +76,20 @@ type Coins struct { symbolsList map[types.CoinSymbol][]types.CoinID symbolsInfoList map[types.CoinSymbol]*SymbolInfo - bus *bus.Bus - iavl tree.MTree + bus *bus.Bus + db atomic.Value lock sync.RWMutex } -func NewCoins(stateBus *bus.Bus, iavl tree.MTree) (*Coins, error) { +func NewCoins(stateBus *bus.Bus, db *iavl.ImmutableTree) *Coins { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } coins := &Coins{ - bus: stateBus, iavl: iavl, + bus: stateBus, + db: immutableTree, list: map[types.CoinID]*Model{}, dirty: map[types.CoinID]struct{}{}, symbolsList: map[types.CoinSymbol][]types.CoinID{}, @@ -91,10 +97,22 @@ func NewCoins(stateBus *bus.Bus, iavl tree.MTree) (*Coins, error) { } coins.bus.SetCoins(NewBus(coins)) - return coins, nil + return coins +} + +func (c *Coins) immutableTree() *iavl.ImmutableTree { + db := c.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) } -func (c *Coins) Commit() error { +func (c *Coins) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + c.db.Store(immutableTree) +} + +func (c *Coins) Commit(db *iavl.MutableTree) error { coins := c.getOrderedDirtyCoins() for _, id := range coins { coin := c.getFromMap(id) @@ -109,38 +127,53 @@ func (c *Coins) Commit() error { return fmt.Errorf("can't encode object at %d: %v", id, err) } - c.iavl.Set(getSymbolCoinsPath(coin.Symbol()), data) + db.Set(getSymbolCoinsPath(coin.Symbol()), data) + coin.lock.Lock() coin.isCreated = false + coin.lock.Unlock() } if coin.IsDirty() { + coin.lock.Lock() + coin.isDirty = false data, err := rlp.EncodeToBytes(coin) + coin.lock.Unlock() + if err != nil { return fmt.Errorf("can't encode object at %d: %v", id, err) } - c.iavl.Set(getCoinPath(id), data) - coin.isDirty = false + db.Set(getCoinPath(id), data) } if coin.IsInfoDirty() { + coin.lock.RLock() + coin.info.lock.Lock() + coin.info.isDirty = false data, err := rlp.EncodeToBytes(coin.info) + coin.info.lock.Unlock() + coin.lock.RUnlock() + if err != nil { return fmt.Errorf("can't encode object at %d: %v", id, err) } - c.iavl.Set(getCoinInfoPath(id), data) - coin.info.isDirty = false + db.Set(getCoinInfoPath(id), data) } if coin.IsSymbolInfoDirty() { + coin.lock.RLock() + coin.symbolInfo.lock.Lock() + coin.symbolInfo.isDirty = false data, err := rlp.EncodeToBytes(coin.symbolInfo) + coin.symbolInfo.lock.Unlock() + coin.lock.RUnlock() + if err != nil { return fmt.Errorf("can't encode object at %d: %v", id, err) } - c.iavl.Set(getSymbolInfoPath(coin.Symbol()), data) - coin.symbolInfo.isDirty = false + db.Set(getSymbolInfoPath(coin.Symbol()), data) } } @@ -240,9 +273,9 @@ func (c *Coins) Create(id types.CoinID, symbol types.CoinSymbol, name string, isDirty: true, isCreated: true, info: &Info{ - Volume: big.NewInt(0), - Reserve: big.NewInt(0), - isDirty: false, + Volume: volume, + Reserve: reserve, + isDirty: true, }, } @@ -261,13 +294,49 @@ func (c *Coins) Create(id types.CoinID, symbol types.CoinSymbol, name string, c.setSymbolToMap(ids, coin.Symbol()) c.setToMap(coin.ID(), coin) - coin.SetReserve(reserve) - coin.SetVolume(volume) + c.bus.Checker().AddCoin(types.GetBaseCoinID(), reserve) + c.bus.Checker().AddCoinVolume(coin.id, volume) c.markDirty(coin.id) +} - c.bus.Checker().AddCoin(types.GetBaseCoinID(), reserve) - c.bus.Checker().AddCoinVolume(coin.id, volume) +func (c *Coins) CreateToken(id types.CoinID, symbol types.CoinSymbol, name string, mintable, burnable bool, initialAmount *big.Int, maxSupply *big.Int, owner *types.Address) { + coin := &Model{ + CName: name, + CCrr: 0, + CMaxSupply: maxSupply, + CSymbol: symbol, + Mintable: mintable, + Burnable: burnable, + id: id, + markDirty: c.markDirty, + isDirty: true, + isCreated: true, + info: &Info{ + Volume: initialAmount, + Reserve: big.NewInt(0), + isDirty: true, + }, + } + + if owner != nil { + coin.symbolInfo = &SymbolInfo{ + COwnerAddress: owner, + isDirty: true, + } + + c.setSymbolInfoToMap(coin.symbolInfo, coin.Symbol()) + } + + ids := c.getBySymbol(coin.Symbol()) + ids = append(ids, coin.ID()) + + c.setSymbolToMap(ids, coin.Symbol()) + c.setToMap(coin.ID(), coin) + + c.bus.Checker().AddCoinVolume(coin.id, initialAmount) + + c.markDirty(coin.id) } func (c *Coins) Recreate(newID types.CoinID, name string, symbol types.CoinSymbol, @@ -289,8 +358,10 @@ func (c *Coins) Recreate(newID types.CoinID, name string, symbol types.CoinSymbo } } + recreateCoin.lock.Lock() recreateCoin.CVersion = lastVersion + 1 recreateCoin.isDirty = true + recreateCoin.lock.Unlock() c.setToMap(recreateCoin.id, recreateCoin) c.markDirty(recreateCoin.id) @@ -298,12 +369,42 @@ func (c *Coins) Recreate(newID types.CoinID, name string, symbol types.CoinSymbo c.Create(newID, recreateCoin.Symbol(), name, volume, crr, reserve, maxSupply, nil) } +func (c *Coins) RecreateToken(newID types.CoinID, name string, symbol types.CoinSymbol, mintable, burnable bool, initialAmount, maxSupply *big.Int) { + recreateCoin := c.GetCoinBySymbol(symbol, BaseVersion) + if recreateCoin == nil { + panic("coin to recreate does not exists") + } + + // update version for recreating coin + symbolCoins := c.getBySymbol(symbol) + + lastVersion := uint16(0) + for _, id := range symbolCoins { + coin := c.get(id) + if coin.Version() > lastVersion { + lastVersion = coin.Version() + } + } + + recreateCoin.lock.Lock() + recreateCoin.CVersion = lastVersion + 1 + recreateCoin.isDirty = true + recreateCoin.lock.Unlock() + + c.setToMap(recreateCoin.id, recreateCoin) + c.markDirty(recreateCoin.id) + + c.CreateToken(newID, recreateCoin.Symbol(), name, mintable, burnable, initialAmount, maxSupply, nil) +} + func (c *Coins) ChangeOwner(symbol types.CoinSymbol, owner types.Address) { info := c.getSymbolInfo(symbol) info.setOwnerAddress(owner) coin := c.GetCoinBySymbol(symbol, BaseVersion) + coin.lock.Lock() coin.symbolInfo = info + coin.lock.Unlock() c.setSymbolInfoToMap(coin.symbolInfo, coin.Symbol()) c.setToMap(coin.ID(), coin) @@ -327,7 +428,7 @@ func (c *Coins) get(id types.CoinID) *Model { return coin } - _, enc := c.iavl.Get(getCoinPath(id)) + _, enc := c.immutableTree().Get(getCoinPath(id)) if len(enc) == 0 { return nil } @@ -337,18 +438,22 @@ func (c *Coins) get(id types.CoinID) *Model { panic(fmt.Sprintf("failed to decode coin at %d: %s", id, err)) } + coin.lock.Lock() coin.id = id coin.markDirty = c.markDirty + coin.lock.Unlock() // load info - _, enc = c.iavl.Get(getCoinInfoPath(id)) + _, enc = c.immutableTree().Get(getCoinInfoPath(id)) if len(enc) != 0 { var info Info if err := rlp.DecodeBytes(enc, &info); err != nil { panic(fmt.Sprintf("failed to decode coin info %d: %s", id, err)) } + coin.lock.Lock() coin.info = &info + coin.lock.Unlock() } c.setToMap(id, coin) @@ -363,7 +468,7 @@ func (c *Coins) getSymbolInfo(symbol types.CoinSymbol) *SymbolInfo { info := &SymbolInfo{} - _, enc := c.iavl.Get(getSymbolInfoPath(symbol)) + _, enc := c.immutableTree().Get(getSymbolInfoPath(symbol)) if len(enc) == 0 { return nil } @@ -384,7 +489,7 @@ func (c *Coins) getBySymbol(symbol types.CoinSymbol) []types.CoinID { var coins []types.CoinID - _, enc := c.iavl.Get(getSymbolCoinsPath(symbol)) + _, enc := c.immutableTree().Get(getSymbolCoinsPath(symbol)) if len(enc) == 0 { return coins } @@ -399,14 +504,19 @@ func (c *Coins) getBySymbol(symbol types.CoinSymbol) []types.CoinID { } func (c *Coins) markDirty(id types.CoinID) { + c.lock.Lock() + defer c.lock.Unlock() + c.dirty[id] = struct{}{} } func (c *Coins) getOrderedDirtyCoins() []types.CoinID { + c.lock.Lock() keys := make([]types.CoinID, 0, len(c.dirty)) for k := range c.dirty { keys = append(keys, k) } + c.lock.Unlock() sort.SliceStable(keys, func(i, j int) bool { return keys[i] > keys[j] @@ -416,39 +526,37 @@ func (c *Coins) getOrderedDirtyCoins() []types.CoinID { } func (c *Coins) Export(state *types.AppState) { - c.iavl.Iterate(func(key []byte, value []byte) bool { - if key[0] == mainPrefix { - if len(key) > 5 { - return false - } - - coinID := types.BytesToCoinID(key[1:]) - coin := c.get(coinID) + c.immutableTree().IterateRange([]byte{mainPrefix}, []byte{mainPrefix + 1}, true, func(key []byte, value []byte) bool { + if len(key) > 5 { + return false + } - owner := &types.Address{} - info := c.getSymbolInfo(coin.Symbol()) - if info != nil { - owner = info.OwnerAddress() - } + coinID := types.BytesToCoinID(key[1:]) + coin := c.get(coinID) - state.Coins = append(state.Coins, types.Coin{ - ID: uint64(coin.ID()), - Name: coin.Name(), - Symbol: coin.Symbol(), - Volume: coin.Volume().String(), - Crr: uint64(coin.Crr()), - Reserve: coin.Reserve().String(), - MaxSupply: coin.MaxSupply().String(), - Version: uint64(coin.Version()), - OwnerAddress: owner, - }) + var owner *types.Address + info := c.getSymbolInfo(coin.Symbol()) + if info != nil { + owner = info.OwnerAddress() } + state.Coins = append(state.Coins, types.Coin{ + ID: uint64(coin.ID()), + Name: coin.Name(), + Symbol: coin.Symbol(), + Volume: coin.Volume().String(), + Crr: uint64(coin.Crr()), + Reserve: coin.Reserve().String(), + MaxSupply: coin.MaxSupply().String(), + Version: uint64(coin.Version()), + OwnerAddress: owner, + }) + return false }) sort.Slice(state.Coins[:], func(i, j int) bool { - return helpers.StringToBigInt(state.Coins[i].Reserve).Cmp(helpers.StringToBigInt(state.Coins[j].Reserve)) == 1 + return state.Coins[i].ID < state.Coins[j].ID }) } diff --git a/core/state/coins/model.go b/core/state/coins/model.go index c38683a40..8a247824b 100644 --- a/core/state/coins/model.go +++ b/core/state/coins/model.go @@ -5,6 +5,7 @@ import ( "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/helpers" "math/big" + "sync" ) var minCoinReserve = helpers.BipToPip(big.NewInt(10000)) @@ -16,78 +17,197 @@ type Model struct { CVersion types.CoinVersion CSymbol types.CoinSymbol + Mintable bool + Burnable bool + id types.CoinID info *Info symbolInfo *SymbolInfo markDirty func(symbol types.CoinID) + lock sync.RWMutex isDirty bool isCreated bool } -func (m Model) Name() string { +func (m *Model) Name() string { + m.lock.RLock() + defer m.lock.RUnlock() + return m.CName } -func (m Model) Symbol() types.CoinSymbol { +func (m *Model) Symbol() types.CoinSymbol { + m.lock.RLock() + defer m.lock.RUnlock() + return m.CSymbol } -func (m Model) ID() types.CoinID { +func (m *Model) ID() types.CoinID { + m.lock.RLock() + defer m.lock.RUnlock() + return m.id } -func (m Model) Crr() uint32 { +func (m *Model) Crr() uint32 { + m.lock.RLock() + defer m.lock.RUnlock() + return m.CCrr } -func (m Model) Volume() *big.Int { +func (m *Model) Volume() *big.Int { + m.lock.RLock() + defer m.lock.RUnlock() + + // if m.info == nil { + // panic() + // return big.NewInt(0) + // } + + m.info.lock.RLock() + defer m.info.lock.RUnlock() + return big.NewInt(0).Set(m.info.Volume) } -func (m Model) Reserve() *big.Int { +func (m *Model) Reserve() *big.Int { + if m.IsToken() { + return big.NewInt(0) + } + + m.info.lock.RLock() + defer m.info.lock.RUnlock() + return big.NewInt(0).Set(m.info.Reserve) } -func (m Model) Version() uint16 { +func (m *Model) BaseOrHasReserve() bool { + return m.ID().IsBaseCoin() || (m.Crr() > 0) +} + +func (m *Model) IsToken() bool { + return !m.BaseOrHasReserve() +} + +func (m *Model) Version() uint16 { + m.lock.RLock() + defer m.lock.RUnlock() + return m.CVersion } +func (m *Model) IsMintable() bool { + m.lock.RLock() + defer m.lock.RUnlock() + + return m.Mintable +} + +func (m *Model) IsBurnable() bool { + m.lock.RLock() + defer m.lock.RUnlock() + + return m.Burnable +} + func (m *Model) SubVolume(amount *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() m.info.Volume.Sub(m.info.Volume, amount) - m.markDirty(m.id) m.info.isDirty = true + m.info.lock.Unlock() + + m.markDirty(m.id) } func (m *Model) AddVolume(amount *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() m.info.Volume.Add(m.info.Volume, amount) - m.markDirty(m.id) m.info.isDirty = true + m.info.lock.Unlock() + + m.markDirty(m.id) } func (m *Model) SubReserve(amount *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() m.info.Reserve.Sub(m.info.Reserve, amount) - m.markDirty(m.id) m.info.isDirty = true + m.info.lock.Unlock() + + m.markDirty(m.id) } func (m *Model) AddReserve(amount *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() m.info.Reserve.Add(m.info.Reserve, amount) - m.markDirty(m.id) m.info.isDirty = true + m.info.lock.Unlock() + + m.markDirty(m.id) } -func (m *Model) SetReserve(reserve *big.Int) { - m.info.Reserve.Set(reserve) +func (m *Model) Mint(amount *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() + m.CMaxSupply.Add(m.CMaxSupply, amount) + m.isDirty = true + m.info.lock.Unlock() + + m.markDirty(m.id) +} + +func (m *Model) Burn(amount *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() + m.CMaxSupply.Sub(m.CMaxSupply, amount) + m.isDirty = true + m.info.lock.Unlock() + m.markDirty(m.id) - m.info.isDirty = true } func (m *Model) SetVolume(volume *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() m.info.Volume.Set(volume) + m.info.isDirty = true + m.info.lock.Unlock() + m.markDirty(m.id) +} + +func (m *Model) SetReserve(reserve *big.Int) { + m.lock.RLock() + defer m.lock.RUnlock() + + m.info.lock.Lock() + m.info.Reserve.Set(reserve) m.info.isDirty = true + m.info.lock.Unlock() + + m.markDirty(m.id) } func (m *Model) CheckReserveUnderflow(delta *big.Int) error { @@ -101,27 +221,56 @@ func (m *Model) CheckReserveUnderflow(delta *big.Int) error { return nil } -func (m Model) IsInfoDirty() bool { +func (m *Model) IsInfoDirty() bool { + m.lock.RLock() + defer m.lock.RUnlock() + + if m.info == nil { + return false + } + + m.info.lock.RLock() + defer m.info.lock.RUnlock() + return m.info.isDirty } -func (m Model) IsSymbolInfoDirty() bool { - return m.symbolInfo != nil && m.symbolInfo.isDirty +func (m *Model) IsSymbolInfoDirty() bool { + m.lock.RLock() + defer m.lock.RUnlock() + + if m.symbolInfo == nil { + return false + } + + m.symbolInfo.lock.RLock() + defer m.symbolInfo.lock.RUnlock() + + return m.symbolInfo.isDirty } -func (m Model) IsDirty() bool { +func (m *Model) IsDirty() bool { + m.lock.RLock() + defer m.lock.RUnlock() + return m.isDirty } -func (m Model) IsCreated() bool { +func (m *Model) IsCreated() bool { + m.lock.RLock() + defer m.lock.RUnlock() + return m.isCreated } -func (m Model) MaxSupply() *big.Int { +func (m *Model) MaxSupply() *big.Int { + m.lock.RLock() + defer m.lock.RUnlock() + return m.CMaxSupply } -func (m Model) GetFullSymbol() string { +func (m *Model) GetFullSymbol() string { if m.Version() == 0 { return m.Symbol().String() } @@ -134,19 +283,28 @@ type Info struct { Reserve *big.Int isDirty bool + lock sync.RWMutex } type SymbolInfo struct { COwnerAddress *types.Address isDirty bool + + lock sync.RWMutex } func (i *SymbolInfo) setOwnerAddress(address types.Address) { + i.lock.Lock() + defer i.lock.Unlock() + i.COwnerAddress = &address i.isDirty = true } -func (i SymbolInfo) OwnerAddress() *types.Address { +func (i *SymbolInfo) OwnerAddress() *types.Address { + i.lock.RLock() + defer i.lock.RUnlock() + return i.COwnerAddress } diff --git a/core/state/commission/commission.go b/core/state/commission/commission.go new file mode 100644 index 000000000..01b591153 --- /dev/null +++ b/core/state/commission/commission.go @@ -0,0 +1,326 @@ +package commission + +import ( + "encoding/binary" + "fmt" + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "github.com/cosmos/iavl" + "sort" + "sync" + "sync/atomic" +) + +const mainPrefix = byte('p') + +type RCommission interface { + Export(state *types.AppState) + GetVotes(height uint64) []*Model + GetCommissions() *Price + IsVoteExists(height uint64, pubkey types.Pubkey) bool +} + +type Commission struct { + list map[uint64][]*Model + dirty map[uint64]struct{} + forDelete uint64 + + currentPrice *Price + dirtyCurrent bool + + db atomic.Value + lock sync.RWMutex +} + +func NewCommission(db *iavl.ImmutableTree) *Commission { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } + halts := &Commission{ + db: immutableTree, + list: map[uint64][]*Model{}, + dirty: map[uint64]struct{}{}, + forDelete: 0, + } + + return halts +} + +func (c *Commission) immutableTree() *iavl.ImmutableTree { + db := c.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) +} + +func (c *Commission) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + c.db.Store(immutableTree) +} + +func (c *Commission) Export(state *types.AppState) { + c.immutableTree().IterateRange([]byte{mainPrefix}, []byte{mainPrefix + 1}, true, func(key []byte, value []byte) bool { + height := binary.LittleEndian.Uint64(key[1:]) + prices := c.get(height) + if prices == nil { + return false + } + + for _, price := range prices { + p := Decode(price.Price) + for _, vote := range price.Votes { + state.PriceVotes = append(state.PriceVotes, types.PriceVotes{ + Height: height, + CandidateKey: vote, + PriceCommission: types.PriceCommission{ + Send: p.Send.String(), + // todo: add more txs + Coin: c.GetCommissions().Coin.String(), + }, + }) + } + } + + // todo: add current + // c.GetCommissions() + + return false + }) +} + +func (c *Commission) Commit(db *iavl.MutableTree) error { + c.lock.Lock() + if c.dirtyCurrent { + c.dirtyCurrent = false + db.Set([]byte{mainPrefix}, c.currentPrice.Encode()) + } + dirties := c.getOrderedDirty() + c.lock.Unlock() + for _, height := range dirties { + models := c.getFromMap(height) + + c.lock.Lock() + delete(c.dirty, height) + c.lock.Unlock() + + data, err := rlp.EncodeToBytes(models) + if err != nil { + return fmt.Errorf("can't encode object at %d: %v", height, err) + } + + db.Set(getPath(height), data) + } + + if c.forDelete != 0 { + path := getPath(c.forDelete) + db.Remove(path) + c.lock.Lock() + delete(c.list, c.forDelete) + c.forDelete = 0 + c.lock.Unlock() + } + + return nil +} + +func (c *Commission) GetVotes(height uint64) []*Model { + return c.get(height) +} + +func (c *Commission) GetCommissions() *Price { + c.lock.Lock() + defer c.lock.Unlock() + + if c.currentPrice != nil { + return c.currentPrice + } + _, value := c.immutableTree().Get([]byte{mainPrefix}) + if len(value) == 0 { + return &Price{ + Coin: types.GetBaseCoinID(), + PayloadByte: helpers.StringToBigInt("200000000000000000"), + Send: helpers.StringToBigInt("1000000000000000000"), + BuyBancor: helpers.StringToBigInt("10000000000000000000"), + SellBancor: helpers.StringToBigInt("10000000000000000000"), + SellAllBancor: helpers.StringToBigInt("10000000000000000000"), + BuyPoolBase: helpers.StringToBigInt("10000000000000000000"), + BuyPoolDelta: helpers.StringToBigInt("5000000000000000000"), + SellPoolBase: helpers.StringToBigInt("10000000000000000000"), + SellPoolDelta: helpers.StringToBigInt("5000000000000000000"), + SellAllPoolBase: helpers.StringToBigInt("10000000000000000000"), + SellAllPoolDelta: helpers.StringToBigInt("5000000000000000000"), + CreateTicker3: helpers.StringToBigInt("100000000000000000000000000"), + CreateTicker4: helpers.StringToBigInt("10000000000000000000000000"), + CreateTicker5: helpers.StringToBigInt("1000000000000000000000000"), + CreateTicker6: helpers.StringToBigInt("100000000000000000000000"), + CreateTicker7to10: helpers.StringToBigInt("10000000000000000000000"), + CreateCoin: helpers.StringToBigInt("0"), + CreateToken: helpers.StringToBigInt("0"), + RecreateCoin: helpers.StringToBigInt("1000000000000000000000000"), + RecreateToken: helpers.StringToBigInt("1000000000000000000000000"), + DeclareCandidacy: helpers.StringToBigInt("1000000000000000000000"), + Delegate: helpers.StringToBigInt("20000000000000000000"), + Unbond: helpers.StringToBigInt("20000000000000000000"), + RedeemCheck: helpers.StringToBigInt("3000000000000000000"), + SetCandidateOn: helpers.StringToBigInt("10000000000000000000"), + SetCandidateOff: helpers.StringToBigInt("10000000000000000000"), + CreateMultisig: helpers.StringToBigInt("10000000000000000000"), + MultisendBase: helpers.StringToBigInt("1000000000000000000"), + MultisendDelta: helpers.StringToBigInt("500000000000000000"), + EditCandidate: helpers.StringToBigInt("1000000000000000000000"), + SetHaltBlock: helpers.StringToBigInt("100000000000000000000"), + EditTickerOwner: helpers.StringToBigInt("1000000000000000000000000"), + EditMultisig: helpers.StringToBigInt("100000000000000000000"), + PriceVote: helpers.StringToBigInt("1000000000000000000"), + EditCandidatePublicKey: helpers.StringToBigInt("10000000000000000000000000"), + CreateSwapPool: helpers.StringToBigInt("100000000000000000000"), + AddLiquidity: helpers.StringToBigInt("10000000000000000000"), + RemoveLiquidity: helpers.StringToBigInt("10000000000000000000"), + EditCandidateCommission: helpers.StringToBigInt("1000000000000000000000"), + MoveStake: helpers.StringToBigInt("20000000000000000000"), + BurnToken: helpers.StringToBigInt("10000000000000000000"), + MintToken: helpers.StringToBigInt("10000000000000000000"), + VoteCommission: helpers.StringToBigInt("100000000000000000000"), + VoteUpdate: helpers.StringToBigInt("100000000000000000000"), + More: nil, + } + } + c.currentPrice = &Price{} + err := rlp.DecodeBytes(value, c.currentPrice) + if err != nil { + panic(err) + } + return c.currentPrice +} + +func (c *Commission) SetNewCommissions(prices []byte) { + c.lock.Lock() + defer c.lock.Unlock() + + c.dirtyCurrent = true + var newPrices Price + err := rlp.DecodeBytes(prices, &newPrices) + if err != nil { + panic(err) // todo: if update network after price vote, clean up following blocks + } + c.currentPrice = &newPrices +} + +func (c *Commission) getOrNew(height uint64, encode string) *Model { + prices := c.get(height) + + if len(prices) == 0 { + price := &Model{ + height: height, + Price: encode, + markDirty: c.markDirty(height), + } + c.setToMap(height, []*Model{price}) + return price + } + + for _, model := range prices { + if encode == model.Price { + return model + } + } + + return nil +} + +func (c *Commission) get(height uint64) []*Model { + if haltBlock := c.getFromMap(height); haltBlock != nil { + return haltBlock + } + + _, enc := c.immutableTree().Get(getPath(height)) + if len(enc) == 0 { + return nil + } + + var voteBlock []*Model + if err := rlp.DecodeBytes(enc, &voteBlock); err != nil { + panic(fmt.Sprintf("failed to decode halt blocks at height %d: %s", height, err)) + } + + c.setToMap(height, voteBlock) + + return voteBlock +} + +func (c *Commission) markDirty(height uint64) func() { + return func() { + c.lock.Lock() + defer c.lock.Unlock() + c.dirty[height] = struct{}{} + } +} + +func (c *Commission) getOrderedDirty() []uint64 { + keys := make([]uint64, 0, len(c.dirty)) + for k := range c.dirty { + keys = append(keys, k) + } + + sort.SliceStable(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + return keys +} + +func (c *Commission) IsVoteExists(height uint64, pubkey types.Pubkey) bool { + model := c.get(height) + if len(model) == 0 { + return false + } + + for _, price := range model { + for _, vote := range price.Votes { + if vote == pubkey { + return true + } + } + } + + return false +} + +func (c *Commission) AddVoice(height uint64, pubkey types.Pubkey, encode []byte) { + c.getOrNew(height, string(encode)).addVote(pubkey) +} + +func (c *Commission) Delete(height uint64) { + prices := c.get(height) + if len(prices) == 0 { + return + } + + c.lock.RLock() + defer c.lock.RUnlock() + + c.forDelete = height +} + +func (c *Commission) getFromMap(height uint64) []*Model { + c.lock.RLock() + defer c.lock.RUnlock() + + return c.list[height] +} + +func (c *Commission) setToMap(height uint64, model []*Model) { + c.lock.Lock() + defer c.lock.Unlock() + + c.list[height] = model +} + +func getPath(height uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, height) + + return append([]byte{mainPrefix}, b...) +} diff --git a/core/state/commission/model.go b/core/state/commission/model.go new file mode 100644 index 000000000..2e3d1303c --- /dev/null +++ b/core/state/commission/model.go @@ -0,0 +1,90 @@ +package commission + +import ( + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" +) + +type Price struct { + Coin types.CoinID + PayloadByte *big.Int + Send *big.Int + BuyBancor *big.Int + SellBancor *big.Int + SellAllBancor *big.Int + BuyPoolBase *big.Int + BuyPoolDelta *big.Int + SellPoolBase *big.Int + SellPoolDelta *big.Int + SellAllPoolBase *big.Int + SellAllPoolDelta *big.Int + CreateTicker3 *big.Int + CreateTicker4 *big.Int + CreateTicker5 *big.Int + CreateTicker6 *big.Int + CreateTicker7to10 *big.Int + CreateCoin *big.Int + CreateToken *big.Int + RecreateCoin *big.Int + RecreateToken *big.Int + DeclareCandidacy *big.Int + Delegate *big.Int + Unbond *big.Int + RedeemCheck *big.Int + SetCandidateOn *big.Int + SetCandidateOff *big.Int + CreateMultisig *big.Int + MultisendBase *big.Int + MultisendDelta *big.Int + EditCandidate *big.Int + SetHaltBlock *big.Int + EditTickerOwner *big.Int + EditMultisig *big.Int + PriceVote *big.Int + EditCandidatePublicKey *big.Int + CreateSwapPool *big.Int + AddLiquidity *big.Int + RemoveLiquidity *big.Int + EditCandidateCommission *big.Int + MoveStake *big.Int + BurnToken *big.Int + MintToken *big.Int + VoteCommission *big.Int + VoteUpdate *big.Int + More []*big.Int `rlp:"tail"` +} + +func (d *Price) Encode() []byte { + bytes, err := rlp.EncodeToBytes(d) + if err != nil { + panic(err) + } + return bytes +} +func Decode(s string) *Price { + var p Price + err := rlp.DecodeBytes([]byte(s), &p) + if err != nil { + panic(err) + } + return &p +} + +type Model struct { + Votes []types.Pubkey + Price string + + height uint64 + markDirty func() + + lock sync.Mutex +} + +func (m *Model) addVote(pubkey types.Pubkey) { + m.lock.Lock() + defer m.lock.Unlock() + m.Votes = append(m.Votes, pubkey) + m.markDirty() +} diff --git a/core/state/frozenfunds/bus.go b/core/state/frozenfunds/bus.go index e7bffbd05..4277e7f66 100644 --- a/core/state/frozenfunds/bus.go +++ b/core/state/frozenfunds/bus.go @@ -10,7 +10,7 @@ type Bus struct { } func (b *Bus) AddFrozenFund(height uint64, address types.Address, pubkey types.Pubkey, candidateID uint32, coin types.CoinID, value *big.Int) { - b.frozenfunds.AddFund(height, address, pubkey, candidateID, coin, value) + b.frozenfunds.AddFund(height, address, pubkey, candidateID, coin, value, nil) } func NewBus(frozenfunds *FrozenFunds) *Bus { diff --git a/core/state/frozenfunds/frozen_funds.go b/core/state/frozenfunds/frozen_funds.go index 0ad85ae15..85ef3bf7f 100644 --- a/core/state/frozenfunds/frozen_funds.go +++ b/core/state/frozenfunds/frozen_funds.go @@ -5,14 +5,14 @@ import ( "fmt" eventsdb "github.com/MinterTeam/minter-go-node/core/events" "github.com/MinterTeam/minter-go-node/core/state/bus" - "github.com/MinterTeam/minter-go-node/core/state/candidates" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" "math/big" "sort" "sync" + "sync/atomic" ) const mainPrefix = byte('f') @@ -26,45 +26,61 @@ type FrozenFunds struct { list map[uint64]*Model dirty map[uint64]interface{} - bus *bus.Bus - iavl tree.MTree + bus *bus.Bus + db atomic.Value lock sync.RWMutex } -func NewFrozenFunds(stateBus *bus.Bus, iavl tree.MTree) (*FrozenFunds, error) { - frozenfunds := &FrozenFunds{bus: stateBus, iavl: iavl, list: map[uint64]*Model{}, dirty: map[uint64]interface{}{}} - frozenfunds.bus.SetFrozenFunds(NewBus(frozenfunds)) +func NewFrozenFunds(stateBus *bus.Bus, db *iavl.ImmutableTree) *FrozenFunds { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } + frozenFunds := &FrozenFunds{bus: stateBus, db: immutableTree, list: map[uint64]*Model{}, dirty: map[uint64]interface{}{}} + frozenFunds.bus.SetFrozenFunds(NewBus(frozenFunds)) + + return frozenFunds +} - return frozenfunds, nil +func (f *FrozenFunds) immutableTree() *iavl.ImmutableTree { + db := f.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) } -func (f *FrozenFunds) Commit() error { +func (f *FrozenFunds) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + f.db.Store(immutableTree) +} +func (f *FrozenFunds) Commit(db *iavl.MutableTree) error { dirty := f.getOrderedDirty() for _, height := range dirty { ff := f.getFromMap(height) + path := getPath(height) f.lock.Lock() delete(f.dirty, height) - delete(f.list, height) f.lock.Unlock() - path := getPath(height) - + ff.lock.RLock() if ff.deleted { + f.lock.Lock() delete(f.list, height) f.lock.Unlock() - f.iavl.Remove(path) + db.Remove(path) } else { data, err := rlp.EncodeToBytes(ff) if err != nil { return fmt.Errorf("can't encode object at %d: %v", height, err) } - f.iavl.Set(path, data) + db.Set(path, data) } + ff.lock.RUnlock() } return nil @@ -81,6 +97,7 @@ func (f *FrozenFunds) PunishFrozenFundsWithID(fromHeight uint64, toHeight uint64 continue } + ff.lock.Lock() newList := make([]Item, len(ff.List)) for i, item := range ff.List { if item.CandidateID == candidateID { @@ -103,7 +120,7 @@ func (f *FrozenFunds) PunishFrozenFundsWithID(fromHeight uint64, toHeight uint64 f.bus.Checker().AddCoin(item.Coin, new(big.Int).Neg(slashed)) - f.bus.Events().AddEvent(uint32(fromHeight), &eventsdb.SlashEvent{ + f.bus.Events().AddEvent(&eventsdb.SlashEvent{ Address: item.Address, Amount: slashed.String(), Coin: uint64(item.Coin), @@ -115,8 +132,8 @@ func (f *FrozenFunds) PunishFrozenFundsWithID(fromHeight uint64, toHeight uint64 newList[i] = item } - ff.List = newList + ff.lock.Unlock() f.markDirty(cBlock) } @@ -140,7 +157,7 @@ func (f *FrozenFunds) get(height uint64) *Model { return ff } - _, enc := f.iavl.Get(getPath(height)) + _, enc := f.immutableTree().Get(getPath(height)) if len(enc) == 0 { return nil } @@ -159,14 +176,19 @@ func (f *FrozenFunds) get(height uint64) *Model { } func (f *FrozenFunds) markDirty(height uint64) { + f.lock.Lock() + defer f.lock.Unlock() + f.dirty[height] = struct{}{} } func (f *FrozenFunds) getOrderedDirty() []uint64 { + f.lock.Lock() keys := make([]uint64, 0, len(f.dirty)) for k := range f.dirty { keys = append(keys, k) } + f.lock.Unlock() sort.SliceStable(keys, func(i, j int) bool { return keys[i] < keys[j] @@ -175,8 +197,8 @@ func (f *FrozenFunds) getOrderedDirty() []uint64 { return keys } -func (f *FrozenFunds) AddFund(height uint64, address types.Address, pubkey types.Pubkey, candidateId uint32, coin types.CoinID, value *big.Int) { - f.GetOrNew(height).addFund(address, pubkey, candidateId, coin, value) +func (f *FrozenFunds) AddFund(height uint64, address types.Address, pubkey types.Pubkey, candidateId uint32, coin types.CoinID, value *big.Int, moveToCandidate *uint32) { + f.GetOrNew(height).addFund(address, pubkey, candidateId, coin, value, moveToCandidate) f.bus.Checker().AddCoin(coin, value) } @@ -194,12 +216,13 @@ func (f *FrozenFunds) Delete(height uint64) { } func (f *FrozenFunds) Export(state *types.AppState, height uint64) { - for i := height; i <= height+candidates.UnbondPeriod; i++ { + for i := height; i <= height+types.GetUnbondPeriod(); i++ { frozenFunds := f.get(i) if frozenFunds == nil { continue } + frozenFunds.lock.RLock() for _, frozenFund := range frozenFunds.List { state.FrozenFunds = append(state.FrozenFunds, types.FrozenFund{ Height: i, @@ -210,6 +233,7 @@ func (f *FrozenFunds) Export(state *types.AppState, height uint64) { Value: frozenFund.Value.String(), }) } + frozenFunds.lock.RUnlock() } } diff --git a/core/state/frozenfunds/frozen_funds_test.go b/core/state/frozenfunds/frozen_funds_test.go index 803f92481..30d99b431 100644 --- a/core/state/frozenfunds/frozen_funds_test.go +++ b/core/state/frozenfunds/frozen_funds_test.go @@ -12,30 +12,22 @@ import ( ) func TestFrozenFundsToAddModel(t *testing.T) { + t.Parallel() b := bus.NewBus() - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) - ff, err := NewFrozenFunds(b, mutableTree) - if err != nil { - t.Fatal(err) - } + ff := NewFrozenFunds(b, mutableTree.GetLastImmutable()) b.SetChecker(checker.NewChecker(b)) - coinsState, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + coinsState := coins.NewCoins(b, mutableTree.GetLastImmutable()) b.SetCoins(coins.NewBus(coinsState)) height, addr, pubkey, coin, val := uint64(1), types.Address{0}, types.Pubkey{0}, types.GetBaseCoinID(), big.NewInt(1e18) - ff.AddFund(height, addr, pubkey, 1, coin, val) - if err := ff.Commit(); err != nil { - t.Fatal(err) - } + ff.AddFund(height, addr, pubkey, 1, coin, val, nil) - _, _, err = mutableTree.SaveVersion() + _, _, err := mutableTree.Commit(ff) if err != nil { t.Fatal(err) } @@ -60,29 +52,21 @@ func TestFrozenFundsToAddModel(t *testing.T) { } func TestFrozenFundsToDeleteModel(t *testing.T) { + t.Parallel() b := bus.NewBus() - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - ff, err := NewFrozenFunds(b, mutableTree) - if err != nil { - t.Fatal(err) - } + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + ff := NewFrozenFunds(b, mutableTree.GetLastImmutable()) b.SetChecker(checker.NewChecker(b)) - coinsState, err := coins.NewCoins(b, mutableTree) - if err != nil { - t.Fatal(err) - } + coinsState := coins.NewCoins(b, mutableTree.GetLastImmutable()) b.SetCoins(coins.NewBus(coinsState)) height, addr, pubkey, coin, val := uint64(1), types.Address{0}, types.Pubkey{0}, types.GetBaseCoinID(), big.NewInt(1e18) - ff.AddFund(height, addr, pubkey, 1, coin, val) - if err := ff.Commit(); err != nil { - t.Fatal(err) - } + ff.AddFund(height, addr, pubkey, 1, coin, val, nil) - _, _, err = mutableTree.SaveVersion() + _, _, err := mutableTree.Commit(ff) if err != nil { t.Fatal(err) } @@ -93,11 +77,7 @@ func TestFrozenFundsToDeleteModel(t *testing.T) { ff.Delete(height) - if err := ff.Commit(); err != nil { - t.Fatal(err) - } - - _, _, err = mutableTree.SaveVersion() + _, _, err = mutableTree.Commit(ff) if err != nil { t.Fatal(err) } @@ -108,12 +88,10 @@ func TestFrozenFundsToDeleteModel(t *testing.T) { } func TestFrozenFundsToDeleteNotExistingFund(t *testing.T) { + t.Parallel() b := bus.NewBus() - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - ff, err := NewFrozenFunds(b, mutableTree) - if err != nil { - t.Fatal(err) - } + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + ff := NewFrozenFunds(b, mutableTree.GetLastImmutable()) ff.Delete(0) } diff --git a/core/state/frozenfunds/model.go b/core/state/frozenfunds/model.go index bb9dd5281..53db76f51 100644 --- a/core/state/frozenfunds/model.go +++ b/core/state/frozenfunds/model.go @@ -3,14 +3,16 @@ package frozenfunds import ( "github.com/MinterTeam/minter-go-node/core/types" "math/big" + "sync" ) type Item struct { - Address types.Address - CandidateKey *types.Pubkey - CandidateID uint32 - Coin types.CoinID - Value *big.Int + Address types.Address + CandidateKey *types.Pubkey + CandidateID uint32 + Coin types.CoinID + Value *big.Int + MoveToCandidate *uint32 `rlp:"nilList"` } type Model struct { @@ -19,21 +21,29 @@ type Model struct { height uint64 deleted bool markDirty func(height uint64) + lock sync.RWMutex } func (m *Model) delete() { + m.lock.Lock() + defer m.lock.Unlock() + m.deleted = true m.markDirty(m.height) } -func (m *Model) addFund(address types.Address, pubkey types.Pubkey, candidateID uint32, coin types.CoinID, value *big.Int) { +func (m *Model) addFund(address types.Address, pubkey types.Pubkey, candidateID uint32, coin types.CoinID, value *big.Int, moveToCandidateID *uint32) { + m.lock.Lock() m.List = append(m.List, Item{ - Address: address, - CandidateKey: &pubkey, - CandidateID: candidateID, - Coin: coin, - Value: value, + Address: address, + CandidateKey: &pubkey, + CandidateID: candidateID, + Coin: coin, + Value: value, + MoveToCandidate: moveToCandidateID, }) + m.lock.Unlock() + m.markDirty(m.height) } diff --git a/core/state/halts/halts.go b/core/state/halts/halts.go index 256a08e5b..8fd74091d 100644 --- a/core/state/halts/halts.go +++ b/core/state/halts/halts.go @@ -6,9 +6,10 @@ import ( "github.com/MinterTeam/minter-go-node/core/state/bus" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" "sort" "sync" + "sync/atomic" ) const mainPrefix = byte('h') @@ -23,50 +24,61 @@ type HaltBlocks struct { list map[uint64]*Model dirty map[uint64]struct{} - bus *bus.Bus - iavl tree.MTree + bus *bus.Bus + db atomic.Value lock sync.RWMutex } -func NewHalts(stateBus *bus.Bus, iavl tree.MTree) (*HaltBlocks, error) { +func NewHalts(stateBus *bus.Bus, db *iavl.ImmutableTree) *HaltBlocks { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } halts := &HaltBlocks{ bus: stateBus, - iavl: iavl, + db: immutableTree, list: map[uint64]*Model{}, dirty: map[uint64]struct{}{}, } halts.bus.SetHaltBlocks(NewBus(halts)) - return halts, nil + return halts +} + +func (hb *HaltBlocks) immutableTree() *iavl.ImmutableTree { + db := hb.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) +} + +func (hb *HaltBlocks) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + hb.db.Store(immutableTree) } -func (hb *HaltBlocks) Commit() error { +func (hb *HaltBlocks) Commit(db *iavl.MutableTree) error { dirty := hb.getOrderedDirty() for _, height := range dirty { haltBlock := hb.getFromMap(height) + path := getPath(height) hb.lock.Lock() delete(hb.dirty, height) - hb.lock.Unlock() - - path := getPath(height) - if haltBlock.deleted { - hb.lock.Lock() delete(hb.list, height) - hb.lock.Unlock() - - hb.iavl.Remove(path) + db.Remove(path) } else { data, err := rlp.EncodeToBytes(haltBlock) if err != nil { return fmt.Errorf("can't encode object at %d: %v", height, err) } - hb.iavl.Set(path, data) + db.Set(path, data) } + hb.lock.Unlock() } return nil @@ -94,7 +106,7 @@ func (hb *HaltBlocks) get(height uint64) *Model { return haltBlock } - _, enc := hb.iavl.Get(getPath(height)) + _, enc := hb.immutableTree().Get(getPath(height)) if len(enc) == 0 { return nil } @@ -113,14 +125,19 @@ func (hb *HaltBlocks) get(height uint64) *Model { } func (hb *HaltBlocks) markDirty(height uint64) { + hb.lock.Lock() + defer hb.lock.Unlock() + hb.dirty[height] = struct{}{} } func (hb *HaltBlocks) getOrderedDirty() []uint64 { + hb.lock.RLock() keys := make([]uint64, 0, len(hb.dirty)) for k := range hb.dirty { keys = append(keys, k) } + hb.lock.RUnlock() sort.SliceStable(keys, func(i, j int) bool { return keys[i] < keys[j] @@ -135,6 +152,9 @@ func (hb *HaltBlocks) IsHaltExists(height uint64, pubkey types.Pubkey) bool { return false } + model.lock.Lock() + defer model.lock.Unlock() + for _, halt := range model.List { if halt.Pubkey == pubkey { return true @@ -158,7 +178,7 @@ func (hb *HaltBlocks) Delete(height uint64) { } func (hb *HaltBlocks) Export(state *types.AppState) { - hb.iavl.Iterate(func(key []byte, value []byte) bool { + hb.immutableTree().Iterate(func(key []byte, value []byte) bool { if key[0] != mainPrefix { return false } diff --git a/core/state/halts/halts_test.go b/core/state/halts/halts_test.go index 21f0e1394..8f48f602e 100644 --- a/core/state/halts/halts_test.go +++ b/core/state/halts/halts_test.go @@ -9,20 +9,15 @@ import ( ) func TestHaltsToDeleteModel(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - h, err := NewHalts(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + h := NewHalts(bus.NewBus(), mutableTree.GetLastImmutable()) pubkey, height := types.Pubkey{0}, uint64(10) h.AddHaltBlock(height, pubkey) - if err := h.Commit(); err != nil { - t.Fatal(err) - } - _, _, err = mutableTree.SaveVersion() + _, _, err := mutableTree.Commit(h) if err != nil { t.Fatal(err) } @@ -32,11 +27,8 @@ func TestHaltsToDeleteModel(t *testing.T) { } h.Delete(height) - if err := h.Commit(); err != nil { - t.Fatal(err) - } - _, _, err = mutableTree.SaveVersion() + _, _, err = mutableTree.Commit(h) if err != nil { t.Fatal(err) } @@ -47,22 +39,16 @@ func TestHaltsToDeleteModel(t *testing.T) { } func TestBusToAddHaltBlock(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) - h, err := NewHalts(bus.NewBus(), mutableTree) - if err != nil { - t.Fatal(err) - } + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) + h := NewHalts(bus.NewBus(), mutableTree.GetLastImmutable()) pubkey, height := types.Pubkey{0}, uint64(10) hbBus := Bus{halts: h} hbBus.AddHaltBlock(height, pubkey) - if err := h.Commit(); err != nil { - t.Fatal(err) - } - - _, _, err = mutableTree.SaveVersion() + _, _, err := mutableTree.Commit(h) if err != nil { t.Fatal(err) } diff --git a/core/state/halts/model.go b/core/state/halts/model.go index 932ff6b4e..6f3be9e49 100644 --- a/core/state/halts/model.go +++ b/core/state/halts/model.go @@ -2,6 +2,7 @@ package halts import ( "github.com/MinterTeam/minter-go-node/core/types" + "sync" ) type Item struct { @@ -14,14 +15,22 @@ type Model struct { height uint64 deleted bool markDirty func(height uint64) + + lock sync.RWMutex } func (m *Model) delete() { + m.lock.Lock() + defer m.lock.Unlock() + m.deleted = true m.markDirty(m.height) } func (m *Model) addHaltBlock(pubkey types.Pubkey) { + m.lock.Lock() + defer m.lock.Unlock() + m.List = append(m.List, Item{ Pubkey: pubkey, }) diff --git a/core/state/state.go b/core/state/state.go index 3239e31ff..aaa2da244 100644 --- a/core/state/state.go +++ b/core/state/state.go @@ -2,7 +2,6 @@ package state import ( "encoding/hex" - "fmt" eventsdb "github.com/MinterTeam/minter-go-node/core/events" "github.com/MinterTeam/minter-go-node/core/state/accounts" "github.com/MinterTeam/minter-go-node/core/state/app" @@ -11,13 +10,16 @@ import ( "github.com/MinterTeam/minter-go-node/core/state/checker" "github.com/MinterTeam/minter-go-node/core/state/checks" "github.com/MinterTeam/minter-go-node/core/state/coins" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/state/frozenfunds" "github.com/MinterTeam/minter-go-node/core/state/halts" + "github.com/MinterTeam/minter-go-node/core/state/swap" "github.com/MinterTeam/minter-go-node/core/state/validators" "github.com/MinterTeam/minter-go-node/core/state/waitlist" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/helpers" "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" db "github.com/tendermint/tm-db" "log" "math/big" @@ -38,24 +40,20 @@ func NewCheckState(state *State) *CheckState { func (cs *CheckState) isValue_State() {} -func (cs *CheckState) Lock() { - cs.state.lock.Lock() -} - -func (cs *CheckState) Export(height uint64) types.AppState { - return cs.state.Export(height) -} - -func (cs *CheckState) Unlock() { - cs.state.lock.Unlock() -} - -func (cs *CheckState) RLock() { - cs.state.lock.RLock() -} +func (cs *CheckState) Export() types.AppState { + appState := new(types.AppState) + cs.App().Export(appState) + cs.Validators().Export(appState) + cs.Candidates().Export(appState) + cs.WaitList().Export(appState) + cs.FrozenFunds().Export(appState, uint64(cs.state.height)) + cs.Accounts().Export(appState) + cs.Coins().Export(appState) + cs.Checks().Export(appState) + cs.Halts().Export(appState) + cs.Swap().Export(appState) -func (cs *CheckState) RUnlock() { - cs.state.lock.RUnlock() + return *appState } func (cs *CheckState) Validators() validators.RValidators { @@ -85,44 +83,53 @@ func (cs *CheckState) Checks() checks.RChecks { func (cs *CheckState) WaitList() waitlist.RWaitList { return cs.state.Waitlist } -func (cs *CheckState) Tree() tree.ReadOnlyTree { - return cs.state.Tree() + +func (cs *CheckState) Swap() swap.RSwap { + return cs.state.Swap } -type State struct { - App *app.App - Validators *validators.Validators - Candidates *candidates.Candidates - FrozenFunds *frozenfunds.FrozenFunds - Halts *halts.HaltBlocks - Accounts *accounts.Accounts - Coins *coins.Coins - Checks *checks.Checks - Checker *checker.Checker - Waitlist *waitlist.WaitList +func (cs *CheckState) Commission() commission.RCommission { + return cs.state.Commission +} +type State struct { + App *app.App + Validators *validators.Validators + Candidates *candidates.Candidates + FrozenFunds *frozenfunds.FrozenFunds + Halts *halts.HaltBlocks + Accounts *accounts.Accounts + Coins *coins.Coins + Checks *checks.Checks + Checker *checker.Checker + Waitlist *waitlist.WaitList + Swap *swap.Swap + Commission *commission.Commission db db.DB events eventsdb.IEventsDB tree tree.MTree keepLastStates int64 - bus *bus.Bus - lock sync.RWMutex + bus *bus.Bus + lock sync.RWMutex + height int64 } func (s *State) isValue_State() {} -func NewState(height uint64, db db.DB, events eventsdb.IEventsDB, cacheSize int, keepLastStates int64) (*State, error) { - iavlTree, err := tree.NewMutableTree(height, db, cacheSize) +func NewState(height uint64, db db.DB, events eventsdb.IEventsDB, cacheSize int, keepLastStates int64, initialVersion uint64) (*State, error) { + iavlTree, err := tree.NewMutableTree(height, db, cacheSize, initialVersion) if err != nil { return nil, err } - state, err := newStateForTree(iavlTree, events, db, keepLastStates) + state, err := newStateForTree(iavlTree.GetLastImmutable(), events, db, keepLastStates) if err != nil { return nil, err } + state.tree = iavlTree + state.height = int64(height) state.Candidates.LoadCandidatesDeliver() state.Candidates.LoadStakes() state.Validators.LoadValidators() @@ -159,66 +166,25 @@ func (s *State) RUnlock() { } func (s *State) Check() error { - volumeDeltas := s.Checker.VolumeDeltas() - for coin, delta := range s.Checker.Deltas() { - volume := volumeDeltas[coin] - if volume == nil { - volume = big.NewInt(0) - } - - if delta.Cmp(volume) != 0 { - return fmt.Errorf("invariants error on coin %s: %s", coin.String(), big.NewInt(0).Sub(volume, delta).String()) - } - } - - return nil + return s.Checker.Check() } -const countBatchBlocksDelete = 60 - func (s *State) Commit() ([]byte, error) { s.Checker.Reset() - s.tree.GlobalLock() - defer s.tree.GlobalUnlock() - - if err := s.Accounts.Commit(); err != nil { - return nil, err - } - - if err := s.App.Commit(); err != nil { - return nil, err - } - - if err := s.Coins.Commit(); err != nil { - return nil, err - } - - if err := s.Candidates.Commit(); err != nil { - return nil, err - } - - if err := s.Validators.Commit(); err != nil { - return nil, err - } - - if err := s.Checks.Commit(); err != nil { - return nil, err - } - - if err := s.FrozenFunds.Commit(); err != nil { - return nil, err - } - - if err := s.Halts.Commit(); err != nil { - return nil, err - } - - if err := s.Waitlist.Commit(); err != nil { - return nil, err - } - - hash, version, err := s.tree.SaveVersion() + hash, version, err := s.tree.Commit( + s.Accounts, + s.App, + s.Coins, + s.Candidates, + s.Validators, + s.Checks, + s.FrozenFunds, + s.Halts, + s.Waitlist, + s.Swap, + s.Commission, + ) if err != nil { return hash, err } @@ -228,14 +194,16 @@ func (s *State) Commit() ([]byte, error) { return hash, nil } - if err := s.tree.DeleteVersionIfExists(versionToDelete); err != nil { + if err := s.tree.DeleteVersion(versionToDelete); err != nil { log.Printf("DeleteVersion %d error: %s\n", versionToDelete, err) } + s.height = version + return hash, nil } -func (s *State) Import(state types.AppState) error { +func (s *State) Import(state types.AppState, initialHeight uint64) error { s.App.SetMaxGas(state.MaxGas) totalSlash := helpers.StringToBigInt(state.TotalSlashed) s.App.SetTotalSlashed(totalSlash) @@ -296,7 +264,7 @@ func (s *State) Import(state types.AppState) error { s.Candidates.SetTotalStake(c.PubKey, helpers.StringToBigInt(c.TotalBipStake)) s.Candidates.SetStakes(c.PubKey, c.Stakes, c.Updates) } - s.Candidates.RecalculateStakes(state.StartHeight) + s.Candidates.RecalculateStakes(uint64(s.height)) for _, w := range state.Waitlist { value := helpers.StringToBigInt(w.Value) @@ -315,35 +283,36 @@ func (s *State) Import(state types.AppState) error { for _, ff := range state.FrozenFunds { coinID := types.CoinID(ff.Coin) value := helpers.StringToBigInt(ff.Value) - s.FrozenFunds.AddFund(ff.Height, ff.Address, *ff.CandidateKey, uint32(ff.CandidateID), coinID, value) + s.FrozenFunds.AddFund(ff.Height, ff.Address, *ff.CandidateKey, uint32(ff.CandidateID), coinID, value, nil) s.Checker.AddCoin(coinID, new(big.Int).Neg(value)) } + s.Swap.Import(&state) + return nil } -func (s *State) Export(height uint64) types.AppState { - state, err := NewCheckStateAtHeight(height, s.db) +func (s *State) Export() types.AppState { + state, err := NewCheckStateAtHeight(uint64(s.tree.Version()), s.db) if err != nil { - log.Panicf("Create new state at height %d failed: %s", height, err) + log.Panicf("Create new state at height %d failed: %s", s.tree.Version(), err) } - appState := new(types.AppState) - state.App().Export(appState, height) - state.Validators().Export(appState) - state.Candidates().Export(appState) - state.WaitList().Export(appState) - state.FrozenFunds().Export(appState, height) - state.Accounts().Export(appState) - state.Coins().Export(appState) - state.Checks().Export(appState) - state.Halts().Export(appState) + return state.Export() +} - return *appState +// Only for tests +func (s *State) ReloadFromDiskAndExport() types.AppState { + state, err := NewCheckStateAtHeight(uint64(s.tree.Version()), s.db) + if err != nil { + log.Panicf("Create new state at height %d failed: %s", s.tree.Version(), err) + } + + return state.Export() } -func newCheckStateForTree(iavlTree tree.MTree, events eventsdb.IEventsDB, db db.DB, keepLastStates int64) (*CheckState, error) { - stateForTree, err := newStateForTree(iavlTree, events, db, keepLastStates) +func newCheckStateForTree(immutableTree *iavl.ImmutableTree, events eventsdb.IEventsDB, db db.DB, keepLastStates int64) (*CheckState, error) { + stateForTree, err := newStateForTree(immutableTree, events, db, keepLastStates) if err != nil { return nil, err } @@ -351,56 +320,33 @@ func newCheckStateForTree(iavlTree tree.MTree, events eventsdb.IEventsDB, db db. return NewCheckState(stateForTree), nil } -func newStateForTree(iavlTree tree.MTree, events eventsdb.IEventsDB, db db.DB, keepLastStates int64) (*State, error) { +func newStateForTree(immutableTree *iavl.ImmutableTree, events eventsdb.IEventsDB, db db.DB, keepLastStates int64) (*State, error) { stateBus := bus.NewBus() stateBus.SetEvents(events) stateChecker := checker.NewChecker(stateBus) - candidatesState, err := candidates.NewCandidates(stateBus, iavlTree) - if err != nil { - return nil, err - } + candidatesState := candidates.NewCandidates(stateBus, immutableTree) - validatorsState, err := validators.NewValidators(stateBus, iavlTree) - if err != nil { - return nil, err - } + validatorsState := validators.NewValidators(stateBus, immutableTree) - appState, err := app.NewApp(stateBus, iavlTree) - if err != nil { - return nil, err - } + appState := app.NewApp(stateBus, immutableTree) - frozenFundsState, err := frozenfunds.NewFrozenFunds(stateBus, iavlTree) - if err != nil { - return nil, err - } + frozenFundsState := frozenfunds.NewFrozenFunds(stateBus, immutableTree) - accountsState, err := accounts.NewAccounts(stateBus, iavlTree) - if err != nil { - return nil, err - } + accountsState := accounts.NewAccounts(stateBus, immutableTree) - coinsState, err := coins.NewCoins(stateBus, iavlTree) - if err != nil { - return nil, err - } + coinsState := coins.NewCoins(stateBus, immutableTree) - checksState, err := checks.NewChecks(iavlTree) - if err != nil { - return nil, err - } + checksState := checks.NewChecks(immutableTree) - haltsState, err := halts.NewHalts(stateBus, iavlTree) - if err != nil { - return nil, err - } + haltsState := halts.NewHalts(stateBus, immutableTree) - waitlistState, err := waitlist.NewWaitList(stateBus, iavlTree) - if err != nil { - return nil, err - } + waitlistState := waitlist.NewWaitList(stateBus, immutableTree) + + swap := swap.New(stateBus, immutableTree) + + commission := commission.NewCommission(immutableTree) state := &State{ Validators: validatorsState, @@ -413,12 +359,13 @@ func newStateForTree(iavlTree tree.MTree, events eventsdb.IEventsDB, db db.DB, k Checker: stateChecker, Halts: haltsState, Waitlist: waitlistState, + Swap: swap, + Commission: commission, - bus: stateBus, - + height: immutableTree.Version(), + bus: stateBus, db: db, events: events, - tree: iavlTree, keepLastStates: keepLastStates, } diff --git a/core/state/state_test.go b/core/state/state_test.go index f1c26d535..28a7b7853 100644 --- a/core/state/state_test.go +++ b/core/state/state_test.go @@ -2,6 +2,7 @@ package state import ( "github.com/MinterTeam/minter-go-node/core/check" + eventsdb "github.com/MinterTeam/minter-go-node/core/events" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/crypto" "github.com/MinterTeam/minter-go-node/helpers" @@ -13,9 +14,10 @@ import ( ) func TestStateExport(t *testing.T) { + t.Parallel() height := uint64(0) - state, err := NewState(height, db.NewMemDB(), emptyEvents{}, 1, 1) + state, err := NewState(height, db.NewMemDB(), &eventsdb.MockEvents{}, 1, 2, 0) if err != nil { log.Panic("Cannot create state") } @@ -30,7 +32,7 @@ func TestStateExport(t *testing.T) { coinTestID, coinTest, "TEST", - helpers.BipToPip(big.NewInt(602)), + helpers.BipToPip(big.NewInt(701)), 10, helpers.BipToPip(big.NewInt(100)), helpers.BipToPip(big.NewInt(100)), @@ -41,7 +43,7 @@ func TestStateExport(t *testing.T) { coinTest2ID, coinTest2, "TEST2", - helpers.BipToPip(big.NewInt(1004)), + helpers.BipToPip(big.NewInt(1202)), 50, helpers.BipToPip(big.NewInt(200)), helpers.BipToPip(big.NewInt(200)), @@ -57,8 +59,8 @@ func TestStateExport(t *testing.T) { address2 := crypto.PubkeyToAddress(privateKey2.PublicKey) state.Accounts.AddBalance(address1, types.GetBaseCoinID(), helpers.BipToPip(big.NewInt(1))) - state.Accounts.AddBalance(address1, coinTestID, helpers.BipToPip(big.NewInt(1))) - state.Accounts.AddBalance(address2, coinTest2ID, helpers.BipToPip(big.NewInt(2))) + state.Accounts.AddBalance(address1, coinTestID, helpers.BipToPip(big.NewInt(100))) + state.Accounts.AddBalance(address2, coinTest2ID, helpers.BipToPip(big.NewInt(200))) candidatePubKey1 := [32]byte{} rand.Read(candidatePubKey1[:]) @@ -66,18 +68,34 @@ func TestStateExport(t *testing.T) { candidatePubKey2 := [32]byte{} rand.Read(candidatePubKey2[:]) - state.Candidates.Create(address1, address1, address1, candidatePubKey1, 10) - state.Candidates.Create(address2, address2, address2, candidatePubKey2, 30) + state.Candidates.Create(address1, address1, address1, candidatePubKey1, 10, 0) + state.Candidates.Create(address2, address2, address2, candidatePubKey2, 30, 0) state.Validators.Create(candidatePubKey1, helpers.BipToPip(big.NewInt(1))) - state.FrozenFunds.AddFund(height, address1, candidatePubKey1, state.Candidates.ID(candidatePubKey1), coinTestID, helpers.BipToPip(big.NewInt(100))) - state.FrozenFunds.AddFund(height+10, address1, candidatePubKey1, state.Candidates.ID(candidatePubKey1), types.GetBaseCoinID(), helpers.BipToPip(big.NewInt(3))) - state.FrozenFunds.AddFund(height+100, address2, candidatePubKey1, state.Candidates.ID(candidatePubKey1), coinTestID, helpers.BipToPip(big.NewInt(500))) - state.FrozenFunds.AddFund(height+150, address2, candidatePubKey1, state.Candidates.ID(candidatePubKey1), coinTest2ID, helpers.BipToPip(big.NewInt(1000))) + state.FrozenFunds.AddFund(height+110, address1, candidatePubKey1, state.Candidates.ID(candidatePubKey1), coinTestID, helpers.BipToPip(big.NewInt(100)), nil) + state.FrozenFunds.AddFund(height+120, address1, candidatePubKey1, state.Candidates.ID(candidatePubKey1), types.GetBaseCoinID(), helpers.BipToPip(big.NewInt(3)), nil) + state.FrozenFunds.AddFund(height+140, address2, candidatePubKey1, state.Candidates.ID(candidatePubKey1), coinTestID, helpers.BipToPip(big.NewInt(500)), nil) + state.FrozenFunds.AddFund(height+150, address2, candidatePubKey1, state.Candidates.ID(candidatePubKey1), coinTest2ID, helpers.BipToPip(big.NewInt(1000)), nil) - newCheck := &check.Check{ + newCheck0 := &check.Check{ Nonce: []byte("test nonce"), ChainID: types.CurrentChainID, - DueBlock: height + 1, + DueBlock: 1, + Coin: coinTestID, + Value: helpers.BipToPip(big.NewInt(100)), + GasCoin: coinTest2ID, + } + + err = newCheck0.Sign(privateKey1) + if err != nil { + log.Panicf("Cannot sign check: %s", err) + } + + state.Checks.UseCheck(newCheck0) + + newCheck := &check.Check{ + Nonce: []byte("test nonce 1"), + ChainID: types.CurrentChainID, + DueBlock: 999999, Coin: coinTestID, Value: helpers.BipToPip(big.NewInt(100)), GasCoin: coinTest2ID, @@ -105,15 +123,11 @@ func TestStateExport(t *testing.T) { log.Panicf("Cannot commit state: %s", err) } - newState := state.Export(height) + newState := state.Export() if err := newState.Verify(); err != nil { t.Error(err) } - if newState.StartHeight != height { - t.Fatalf("Wrong new state start height. Expected %d, got %d", height, newState.StartHeight) - } - if newState.MaxGas != state.App.GetMaxGas() { t.Fatalf("Wrong new state max gas. Expected %d, got %d", state.App.GetMaxGas(), newState.MaxGas) } @@ -126,12 +140,12 @@ func TestStateExport(t *testing.T) { t.Fatalf("Wrong new state coins size. Expected %d, got %d", 2, len(newState.Coins)) } - newStateCoin := newState.Coins[1] - newStateCoin1 := newState.Coins[0] + newStateCoin := newState.Coins[0] + newStateCoin1 := newState.Coins[1] if newStateCoin.Name != "TEST" || newStateCoin.Symbol != coinTest || - newStateCoin.Volume != helpers.BipToPip(big.NewInt(602)).String() || + newStateCoin.Volume != helpers.BipToPip(big.NewInt(701)).String() || newStateCoin.Reserve != helpers.BipToPip(big.NewInt(100)).String() || newStateCoin.MaxSupply != helpers.BipToPip(big.NewInt(100)).String() || newStateCoin.Crr != 10 { @@ -140,7 +154,7 @@ func TestStateExport(t *testing.T) { if newStateCoin1.Name != "TEST2" || newStateCoin1.Symbol != coinTest2 || - newStateCoin1.Volume != helpers.BipToPip(big.NewInt(1004)).String() || + newStateCoin1.Volume != helpers.BipToPip(big.NewInt(1202)).String() || newStateCoin1.Reserve != helpers.BipToPip(big.NewInt(200)).String() || newStateCoin1.MaxSupply != helpers.BipToPip(big.NewInt(200)).String() || newStateCoin1.Crr != 50 { @@ -156,7 +170,7 @@ func TestStateExport(t *testing.T) { funds2 := newState.FrozenFunds[2] funds3 := newState.FrozenFunds[3] - if funds.Height != height || + if funds.Height != height+110 || funds.Address != address1 || funds.Coin != uint64(coinTestID) || *funds.CandidateKey != types.Pubkey(candidatePubKey1) || @@ -164,7 +178,7 @@ func TestStateExport(t *testing.T) { t.Fatalf("Wrong new state frozen fund data") } - if funds1.Height != height+10 || + if funds1.Height != height+120 || funds1.Address != address1 || funds1.Coin != uint64(types.GetBaseCoinID()) || *funds1.CandidateKey != types.Pubkey(candidatePubKey1) || @@ -172,7 +186,7 @@ func TestStateExport(t *testing.T) { t.Fatalf("Wrong new state frozen fund data") } - if funds2.Height != height+100 || + if funds2.Height != height+140 || funds2.Address != address2 || funds2.Coin != uint64(coinTestID) || *funds2.CandidateKey != types.Pubkey(candidatePubKey1) || @@ -188,7 +202,7 @@ func TestStateExport(t *testing.T) { t.Fatalf("Wrong new state frozen fund data") } - if len(newState.UsedChecks) != 1 { + if len(newState.UsedChecks) != 2 { t.Fatalf("Wrong new state used checks size. Expected %d, got %d", 1, len(newState.UsedChecks)) } @@ -220,7 +234,7 @@ func TestStateExport(t *testing.T) { t.Fatal("Wrong new state account balances size") } - if account1.Balance[0].Coin != uint64(coinTestID) || account1.Balance[0].Value != helpers.BipToPip(big.NewInt(1)).String() { + if account1.Balance[0].Coin != uint64(coinTestID) || account1.Balance[0].Value != helpers.BipToPip(big.NewInt(100)).String() { t.Fatal("Wrong new state account balance data") } @@ -228,7 +242,7 @@ func TestStateExport(t *testing.T) { t.Fatal("Wrong new state account balance data") } - if account2.Balance[0].Coin != uint64(coinTest2ID) || account2.Balance[0].Value != helpers.BipToPip(big.NewInt(2)).String() { + if account2.Balance[0].Coin != uint64(coinTest2ID) || account2.Balance[0].Value != helpers.BipToPip(big.NewInt(200)).String() { t.Fatal("Wrong new state account balance data") } diff --git a/core/state/swap/swap.go b/core/state/swap/swap.go new file mode 100644 index 000000000..d1e4dc440 --- /dev/null +++ b/core/state/swap/swap.go @@ -0,0 +1,677 @@ +package swap + +import ( + "errors" + "fmt" + "github.com/MinterTeam/minter-go-node/core/state/bus" + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "github.com/cosmos/iavl" + "math/big" + "sort" + "strconv" + "sync" + "sync/atomic" +) + +var Bound = big.NewInt(minimumLiquidity) + +const minimumLiquidity = 1000 +const commission = 2 + +type EditableChecker interface { + IsExist() bool + CoinID() uint32 + AddLastSwapStep(amount0In, amount1Out *big.Int) EditableChecker + Revert() EditableChecker + Reserves() (reserve0 *big.Int, reserve1 *big.Int) + Amounts(liquidity, totalSupply *big.Int) (amount0 *big.Int, amount1 *big.Int) + CalculateBuyForSell(amount0In *big.Int) (amount1Out *big.Int) + CalculateSellForBuy(amount1Out *big.Int) (amount0In *big.Int) + CalculateAddLiquidity(amount0 *big.Int, supply *big.Int) (liquidity *big.Int, amount1 *big.Int) + CheckSwap(amount0In, amount1Out *big.Int) error + CheckMint(amount0, maxAmount1, totalSupply *big.Int) (err error) + CheckCreate(amount0, amount1 *big.Int) (err error) + CheckBurn(liquidity, minAmount0, minAmount1, totalSupply *big.Int) error +} + +type RSwap interface { + SwapPool(coin0, coin1 types.CoinID) (reserve0, reserve1 *big.Int, id uint32) + GetSwapper(coin0, coin1 types.CoinID) EditableChecker + SwapPoolExist(coin0, coin1 types.CoinID) bool + PairCalculateBuyForSell(coin0, coin1 types.CoinID, amount0In *big.Int) (amount1Out *big.Int, err error) + PairCalculateSellForBuy(coin0, coin1 types.CoinID, amount1Out *big.Int) (amount0In *big.Int, err error) + Export(state *types.AppState) +} + +type Swap struct { + muPairs sync.RWMutex + pairs map[pairKey]*Pair + dirties map[pairKey]struct{} + + muNextID sync.Mutex + nextID uint32 + dirtyNextID bool + + bus *bus.Bus + db atomic.Value +} + +func New(bus *bus.Bus, db *iavl.ImmutableTree) *Swap { + immutableTree := atomic.Value{} + immutableTree.Store(db) + return &Swap{pairs: map[pairKey]*Pair{}, bus: bus, db: immutableTree, dirties: map[pairKey]struct{}{}} +} + +func (s *Swap) immutableTree() *iavl.ImmutableTree { + return s.db.Load().(*iavl.ImmutableTree) +} + +func (s *Swap) Export(state *types.AppState) { + s.immutableTree().IterateRange([]byte{mainPrefix}, []byte{mainPrefix + 1}, true, func(key []byte, value []byte) bool { + if key[1] == 'i' { + if err := rlp.DecodeBytes(value, &s.nextID); err != nil { + panic(err) + } + return false + } + coin0 := types.BytesToCoinID(key[1:5]) + coin1 := types.BytesToCoinID(key[5:9]) + s.Pair(coin0, coin1) + return false + }) + + for key, pair := range s.pairs { + if pair == nil { + continue + } + reserve0, reserve1 := pair.Reserves() + swap := types.Swap{ + Coin0: uint64(key.Coin0), + Coin1: uint64(key.Coin1), + Reserve0: reserve0.String(), + Reserve1: reserve1.String(), + } + + state.Swap = append(state.Swap, swap) + } + + sort.Slice(state.Swap, func(i, j int) bool { + return strconv.Itoa(int(state.Swap[i].Coin0))+"-"+strconv.Itoa(int(state.Swap[i].Coin1)) < strconv.Itoa(int(state.Swap[j].Coin0))+"-"+strconv.Itoa(int(state.Swap[j].Coin1)) + }) +} + +func (s *Swap) Import(state *types.AppState) { + for _, swap := range state.Swap { + pair := s.ReturnPair(types.CoinID(swap.Coin0), types.CoinID(swap.Coin1)) + pair.Reserve0.Set(helpers.StringToBigInt(swap.Reserve0)) + pair.Reserve1.Set(helpers.StringToBigInt(swap.Reserve1)) + *pair.ID = uint32(swap.ID) + pair.markDirty() + } +} + +const mainPrefix = byte('s') + +type dirty struct{ isDirty bool } + +type pairData struct { + *sync.RWMutex + Reserve0 *big.Int + Reserve1 *big.Int + ID *uint32 + markDirty func() +} + +func (pd *pairData) Reserves() (reserve0 *big.Int, reserve1 *big.Int) { + pd.RLock() + defer pd.RUnlock() + return new(big.Int).Set(pd.Reserve0), new(big.Int).Set(pd.Reserve1) +} + +func (pd *pairData) Revert() *pairData { + return &pairData{ + RWMutex: pd.RWMutex, + Reserve0: pd.Reserve1, + Reserve1: pd.Reserve0, + ID: pd.ID, + markDirty: pd.markDirty, + } +} + +func (s *Swap) CheckSwap(coin0, coin1 types.CoinID, amount0In, amount1Out *big.Int) error { + return s.Pair(coin0, coin1).checkSwap(amount0In, big.NewInt(0), big.NewInt(0), amount1Out) +} +func (p *Pair) CheckSwap(amount0In, amount1Out *big.Int) error { + return p.checkSwap(amount0In, big.NewInt(0), big.NewInt(0), amount1Out) +} +func (p *Pair) IsExist() bool { + return p != nil +} +func (p *Pair) AddLastSwapStep(amount0In, amount1Out *big.Int) EditableChecker { + reserve0, reserve1 := p.Reserves() + return &Pair{pairData: &pairData{ + RWMutex: &sync.RWMutex{}, + Reserve0: reserve0.Add(reserve0, amount0In), + Reserve1: reserve1.Sub(reserve1, amount1Out), + ID: p.ID, + markDirty: func() {}, + }} +} +func (p *Pair) Revert() EditableChecker { + return &Pair{pairData: p.pairData.Revert()} +} + +func (s *Swap) Commit(db *iavl.MutableTree) error { + basePath := []byte{mainPrefix} + + s.muNextID.Lock() + if s.dirtyNextID { + s.dirtyNextID = false + bytes, err := rlp.EncodeToBytes(s.nextID) + if err != nil { + return err + } + db.Set([]byte{mainPrefix, 'i'}, bytes) + } + s.muNextID.Unlock() + + s.muPairs.RLock() + defer s.muPairs.RUnlock() + + for key := range s.dirties { + pair, _ := s.pairs[key] + pairDataBytes, err := rlp.EncodeToBytes(pair.pairData) + if err != nil { + return err + } + db.Set(append(basePath, key.Bytes()...), pairDataBytes) + } + s.dirties = map[pairKey]struct{}{} + return nil +} + +func (s *Swap) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + s.db.Store(immutableTree) +} + +func (s *Swap) SwapPoolExist(coin0, coin1 types.CoinID) bool { + return s.Pair(coin0, coin1) != nil +} + +func (s *Swap) pair(key pairKey) (*Pair, bool) { + pair, ok := s.pairs[key.sort()] + if pair == nil { + return nil, ok + } + if key.isSorted() { + return pair, true + } + return &Pair{ + pairData: pair.pairData.Revert(), + }, true +} + +func (s *Swap) SwapPool(coinA, coinB types.CoinID) (reserve0, reserve1 *big.Int, id uint32) { + pair := s.Pair(coinA, coinB) + if pair == nil { + return nil, nil, 0 + } + reserve0, reserve1 = pair.Reserves() + return reserve0, reserve1, *pair.ID +} + +func (s *Swap) GetSwapper(coinA, coinB types.CoinID) EditableChecker { + return s.Pair(coinA, coinB) + +} + +func (s *Swap) Pair(coin0, coin1 types.CoinID) *Pair { + s.muPairs.Lock() + defer s.muPairs.Unlock() + + key := pairKey{Coin0: coin0, Coin1: coin1} + pair, ok := s.pair(key) + if ok { + return pair + } + + pathPair := append([]byte{mainPrefix}, key.sort().Bytes()...) + _, data := s.immutableTree().Get(pathPair) + if len(data) == 0 { + s.pairs[key.sort()] = nil + return nil + } + + pair = s.addPair(key) + err := rlp.DecodeBytes(data, pair.pairData) + if err != nil { + panic(err) + } + + return pair +} + +func (s *Swap) PairCalculateSellForBuy(coin0, coin1 types.CoinID, amount1Out *big.Int) (amount0In *big.Int, err error) { + pair := s.Pair(coin0, coin1) + if pair == nil { + return nil, ErrorNotExist + } + value := pair.CalculateSellForBuy(amount1Out) + if value == nil { + return nil, ErrorInsufficientLiquidity + } + return value, nil +} + +func (s *Swap) PairCalculateBuyForSell(coin0, coin1 types.CoinID, amount0In *big.Int) (amount1Out *big.Int, err error) { + pair := s.Pair(coin0, coin1) + if pair == nil { + return nil, ErrorNotExist + } + value := pair.CalculateBuyForSell(amount0In) + if value == nil { + return nil, ErrorInsufficientLiquidity + } + return value, nil +} + +func (s *Swap) PairMint(coin0, coin1 types.CoinID, amount0, maxAmount1, totalSupply *big.Int) (*big.Int, *big.Int, *big.Int) { + pair := s.Pair(coin0, coin1) + oldReserve0, oldReserve1 := pair.Reserves() + liquidity := pair.Mint(amount0, maxAmount1, totalSupply) + newReserve0, newReserve1 := pair.Reserves() + + balance0 := new(big.Int).Sub(newReserve0, oldReserve0) + balance1 := new(big.Int).Sub(newReserve1, oldReserve1) + + s.bus.Checker().AddCoin(coin0, balance0) + s.bus.Checker().AddCoin(coin1, balance1) + + return balance0, balance1, liquidity +} + +func (s *Swap) PairCreate(coin0, coin1 types.CoinID, amount0, amount1 *big.Int) (*big.Int, *big.Int, *big.Int, uint32) { + pair := s.ReturnPair(coin0, coin1) + id := s.incID() + *pair.ID = id + oldReserve0, oldReserve1 := pair.Reserves() + liquidity := pair.Create(amount0, amount1) + newReserve0, newReserve1 := pair.Reserves() + + balance0 := new(big.Int).Sub(newReserve0, oldReserve0) + balance1 := new(big.Int).Sub(newReserve1, oldReserve1) + + s.bus.Checker().AddCoin(coin0, balance0) + s.bus.Checker().AddCoin(coin1, balance1) + + return balance0, balance1, liquidity, id +} + +func (s *Swap) PairBurn(coin0, coin1 types.CoinID, liquidity, minAmount0, minAmount1, totalSupply *big.Int) (*big.Int, *big.Int) { + pair := s.Pair(coin0, coin1) + oldReserve0, oldReserve1 := pair.Reserves() + _, _ = pair.Burn(liquidity, minAmount0, minAmount1, totalSupply) + newReserve0, newReserve1 := pair.Reserves() + + balance0 := new(big.Int).Sub(oldReserve0, newReserve0) + balance1 := new(big.Int).Sub(oldReserve1, newReserve1) + + s.bus.Checker().AddCoin(coin0, new(big.Int).Neg(balance0)) + s.bus.Checker().AddCoin(coin1, new(big.Int).Neg(balance1)) + + return balance0, balance1 +} + +func (s *Swap) PairSell(coin0, coin1 types.CoinID, amount0In, minAmount1Out *big.Int) (*big.Int, *big.Int, uint32) { + pair := s.Pair(coin0, coin1) + calculatedAmount1Out := pair.CalculateBuyForSell(amount0In) + if calculatedAmount1Out.Cmp(minAmount1Out) == -1 { + panic(fmt.Sprintf("calculatedAmount1Out %s less minAmount1Out %s", calculatedAmount1Out, minAmount1Out)) + } + balance0, balance1 := pair.Swap(amount0In, big.NewInt(0), big.NewInt(0), calculatedAmount1Out) + s.bus.Checker().AddCoin(coin0, balance0) + s.bus.Checker().AddCoin(coin1, balance1) + return balance0, new(big.Int).Neg(balance1), *pair.ID +} + +func (s *Swap) PairBuy(coin0, coin1 types.CoinID, maxAmount0In, amount1Out *big.Int) (*big.Int, *big.Int, uint32) { + pair := s.Pair(coin0, coin1) + calculatedAmount0In := pair.CalculateSellForBuy(amount1Out) + if calculatedAmount0In.Cmp(maxAmount0In) == 1 { + panic(fmt.Sprintf("calculatedAmount0In %s more maxAmount0In %s", calculatedAmount0In, maxAmount0In)) + } + balance0, balance1 := pair.Swap(calculatedAmount0In, big.NewInt(0), big.NewInt(0), amount1Out) + s.bus.Checker().AddCoin(coin0, balance0) + s.bus.Checker().AddCoin(coin1, balance1) + return balance0, new(big.Int).Neg(balance1), *pair.ID +} + +type pairKey struct { + Coin0, Coin1 types.CoinID +} + +func (pk pairKey) sort() pairKey { + if pk.isSorted() { + return pk + } + return pk.Revert() +} + +func (pk *pairKey) isSorted() bool { + return pk.Coin0 < pk.Coin1 +} + +func (pk *pairKey) Revert() pairKey { + return pairKey{Coin0: pk.Coin1, Coin1: pk.Coin0} +} + +func (pk pairKey) Bytes() []byte { + return append(pk.Coin0.Bytes(), pk.Coin1.Bytes()...) +} + +var ( + ErrorIdenticalAddresses = errors.New("IDENTICAL_ADDRESSES") +) + +func (s *Swap) ReturnPair(coin0, coin1 types.CoinID) *Pair { + if coin0 == coin1 { + panic(ErrorIdenticalAddresses) + } + + pair := s.Pair(coin0, coin1) + if pair != nil { + return pair + } + + s.muPairs.Lock() + defer s.muPairs.Unlock() + + key := pairKey{coin0, coin1} + pair = s.addPair(key) + + if !key.isSorted() { + return &Pair{ + pairData: pair.pairData.Revert(), + } + } + return pair +} + +func (s *Swap) loadBalanceFunc(key *pairKey) func(address types.Address) *Balance { + return func(address types.Address) *Balance { + _, balancesBytes := s.immutableTree().Get(append(append([]byte{mainPrefix}, key.Bytes()...), address.Bytes()...)) + if len(balancesBytes) == 0 { + return nil + } + + balance := new(Balance) + if err := rlp.DecodeBytes(balancesBytes, balance); err != nil { + panic(err) + } + + return balance + } +} + +func (s *Swap) markDirty(key pairKey) func() { + return func() { + s.muPairs.Lock() + defer s.muPairs.Unlock() + s.dirties[key] = struct{}{} + } +} + +func (s *Swap) addPair(key pairKey) *Pair { + data := &pairData{ + RWMutex: &sync.RWMutex{}, + Reserve0: big.NewInt(0), + Reserve1: big.NewInt(0), + ID: new(uint32), + markDirty: s.markDirty(key.sort()), + } + if !key.isSorted() { + key = key.Revert() + data = data.Revert() + } + pair := &Pair{ + pairData: data, + } + s.pairs[key] = pair + return pair +} + +func (s *Swap) incID() uint32 { + s.muNextID.Lock() + defer s.muNextID.Unlock() + id := s.loadNextID() + s.nextID = id + 1 + s.dirtyNextID = true + return id +} + +func (s *Swap) loadNextID() uint32 { + if s.nextID != 0 { + return s.nextID + } + _, value := s.immutableTree().Get([]byte{mainPrefix, 'i'}) + if len(value) == 0 { + return 1 + } + var id uint32 + if err := rlp.DecodeBytes(value, &id); err != nil { + panic(err) + } + return id +} + +var ( + ErrorInsufficientLiquidityMinted = errors.New("INSUFFICIENT_LIQUIDITY_MINTED") +) + +type Balance struct { + Liquidity *big.Int + isDirty bool +} + +type Pair struct { + *pairData +} + +func (p *Pair) CoinID() uint32 { + if p == nil { + return 0 + } + // if p.ID == nil { + // panic() + // return 0 + // } + return *p.ID +} + +func (p *Pair) CalculateAddLiquidity(amount0 *big.Int, totalSupply *big.Int) (liquidity *big.Int, amount1 *big.Int) { + reserve0, reserve1 := p.Reserves() + return new(big.Int).Div(new(big.Int).Mul(totalSupply, amount0), reserve0), new(big.Int).Div(new(big.Int).Mul(amount0, reserve1), reserve0) +} + +func (p *Pair) Mint(amount0, amount1, totalSupply *big.Int) (liquidity *big.Int) { + liquidity, amount1 = p.CalculateAddLiquidity(amount0, totalSupply) + if liquidity.Sign() != 1 { + panic(ErrorInsufficientLiquidityMinted) + } + p.update(amount0, amount1) + return new(big.Int).Set(liquidity) +} + +func (p *Pair) Create(amount0, amount1 *big.Int) (liquidity *big.Int) { + liquidity = startingSupply(amount0, amount1) + + if liquidity.Cmp(Bound) != 1 { + panic(ErrorInsufficientLiquidityMinted) + } + p.update(amount0, amount1) + return new(big.Int).Set(liquidity) +} + +func (p *Pair) CheckMint(amount0, maxAmount1, totalSupply *big.Int) (err error) { + + liquidity, amount1 := p.CalculateAddLiquidity(amount0, totalSupply) + if amount1.Cmp(maxAmount1) == 1 { + return ErrorInsufficientInputAmount + } + + if liquidity.Sign() != 1 { + return ErrorInsufficientLiquidityMinted + } + + return nil +} +func (p *Pair) CheckCreate(amount0, maxAmount1 *big.Int) (err error) { + liquidity := startingSupply(amount0, maxAmount1) + + if liquidity.Cmp(Bound) != 1 { + return ErrorInsufficientLiquidityMinted + } + + return nil +} + +var ( + ErrorInsufficientLiquidityBurned = errors.New("INSUFFICIENT_LIQUIDITY_BURNED") + ErrorInsufficientLiquidityBalance = errors.New("INSUFFICIENT_LIQUIDITY_BALANCE") + ErrorNotExist = errors.New("PAIR_NOT_EXISTS") +) + +func (p *Pair) Burn(liquidity, minAmount0, minAmount1, totalSupply *big.Int) (amount0, amount1 *big.Int) { + amount0, amount1 = p.Amounts(liquidity, totalSupply) + + if amount0.Cmp(minAmount0) == -1 || amount1.Cmp(minAmount1) == -1 { + panic(ErrorInsufficientLiquidityBurned) + } + + p.update(new(big.Int).Neg(amount0), new(big.Int).Neg(amount1)) + + return amount0, amount1 +} + +func (p *Pair) CheckBurn(liquidity, minAmount0, minAmount1, totalSupply *big.Int) error { + if p == nil { + return ErrorNotExist + } + amount0, amount1 := p.Amounts(liquidity, totalSupply) + + if amount0.Cmp(minAmount0) == -1 || amount1.Cmp(minAmount1) == -1 { + return ErrorInsufficientLiquidityBurned + } + + return nil +} + +var ( + ErrorK = errors.New("K") + ErrorInsufficientInputAmount = errors.New("INSUFFICIENT_INPUT_AMOUNT") + ErrorInsufficientOutputAmount = errors.New("INSUFFICIENT_OUTPUT_AMOUNT") + ErrorInsufficientLiquidity = errors.New("INSUFFICIENT_LIQUIDITY") +) + +func (p *Pair) CalculateBuyForSell(amount0In *big.Int) (amount1Out *big.Int) { + reserve0, reserve1 := p.Reserves() + kAdjusted := new(big.Int).Mul(new(big.Int).Mul(reserve0, reserve1), big.NewInt(1000000)) + balance0Adjusted := new(big.Int).Sub(new(big.Int).Mul(new(big.Int).Add(amount0In, reserve0), big.NewInt(1000)), new(big.Int).Mul(amount0In, big.NewInt(commission))) + amount1Out = new(big.Int).Sub(reserve1, new(big.Int).Quo(kAdjusted, new(big.Int).Mul(balance0Adjusted, big.NewInt(1000)))) + amount1Out = new(big.Int).Sub(amount1Out, big.NewInt(1)) + if amount1Out.Sign() != 1 { + return nil + } + return amount1Out +} + +func (p *Pair) CalculateSellForBuy(amount1Out *big.Int) (amount0In *big.Int) { + reserve0, reserve1 := p.Reserves() + if amount1Out.Cmp(reserve1) != -1 { + return nil + } + kAdjusted := new(big.Int).Mul(new(big.Int).Mul(reserve0, reserve1), big.NewInt(1000000)) + balance1Adjusted := new(big.Int).Mul(new(big.Int).Add(new(big.Int).Neg(amount1Out), reserve1), big.NewInt(1000)) + amount0In = new(big.Int).Quo(new(big.Int).Sub(new(big.Int).Quo(kAdjusted, balance1Adjusted), new(big.Int).Mul(reserve0, big.NewInt(1000))), big.NewInt(1000-commission)) + return new(big.Int).Add(amount0In, big.NewInt(1)) +} + +func (p *Pair) Swap(amount0In, amount1In, amount0Out, amount1Out *big.Int) (amount0, amount1 *big.Int) { + if amount0Out.Sign() != 1 && amount1Out.Sign() != 1 { + panic(ErrorInsufficientOutputAmount) + } + + reserve0, reserve1 := p.Reserves() + + if amount0Out.Cmp(reserve0) == 1 || amount1Out.Cmp(reserve1) == 1 { + panic(ErrorInsufficientLiquidity) + } + + amount0 = new(big.Int).Sub(amount0In, amount0Out) + amount1 = new(big.Int).Sub(amount1In, amount1Out) + + if amount0.Sign() != 1 && amount1.Sign() != 1 { + panic(ErrorInsufficientInputAmount) + } + + balance0Adjusted := new(big.Int).Sub(new(big.Int).Mul(new(big.Int).Add(amount0, reserve0), big.NewInt(1000)), new(big.Int).Mul(amount0In, big.NewInt(commission))) + balance1Adjusted := new(big.Int).Sub(new(big.Int).Mul(new(big.Int).Add(amount1, reserve1), big.NewInt(1000)), new(big.Int).Mul(amount1In, big.NewInt(commission))) + + if new(big.Int).Mul(balance0Adjusted, balance1Adjusted).Cmp(new(big.Int).Mul(new(big.Int).Mul(reserve0, reserve1), big.NewInt(1000000))) == -1 { + panic(ErrorK) + } + + p.update(amount0, amount1) + + return amount0, amount1 +} + +func (p *Pair) checkSwap(amount0In, amount1In, amount0Out, amount1Out *big.Int) (err error) { + reserve0, reserve1 := p.Reserves() + if amount0Out.Cmp(reserve0) == 1 || amount1Out.Cmp(reserve1) == 1 { + return ErrorInsufficientLiquidity + } + + if amount0Out.Sign() != 1 && amount1Out.Sign() != 1 { + return ErrorInsufficientOutputAmount + } + + amount0 := new(big.Int).Sub(amount0In, amount0Out) + amount1 := new(big.Int).Sub(amount1In, amount1Out) + + if amount0.Sign() != 1 && amount1.Sign() != 1 { + return ErrorInsufficientInputAmount + } + + balance0Adjusted := new(big.Int).Sub(new(big.Int).Mul(new(big.Int).Add(amount0, reserve0), big.NewInt(1000)), new(big.Int).Mul(amount0In, big.NewInt(commission))) + balance1Adjusted := new(big.Int).Sub(new(big.Int).Mul(new(big.Int).Add(amount1, reserve1), big.NewInt(1000)), new(big.Int).Mul(amount1In, big.NewInt(commission))) + + if new(big.Int).Mul(balance0Adjusted, balance1Adjusted).Cmp(new(big.Int).Mul(new(big.Int).Mul(reserve0, reserve1), big.NewInt(1000000))) == -1 { + return ErrorK + } + return nil +} + +func (p *Pair) update(amount0, amount1 *big.Int) { + p.pairData.Lock() + defer p.pairData.Unlock() + + p.markDirty() + p.Reserve0.Add(p.Reserve0, amount0) + p.Reserve1.Add(p.Reserve1, amount1) +} + +func (p *Pair) Amounts(liquidity, totalSupply *big.Int) (amount0 *big.Int, amount1 *big.Int) { + p.pairData.RLock() + defer p.pairData.RUnlock() + amount0 = new(big.Int).Div(new(big.Int).Mul(liquidity, p.Reserve0), totalSupply) + amount1 = new(big.Int).Div(new(big.Int).Mul(liquidity, p.Reserve1), totalSupply) + return amount0, amount1 +} + +func startingSupply(amount0 *big.Int, amount1 *big.Int) *big.Int { + mul := new(big.Int).Mul(amount0, amount1) + return new(big.Int).Sqrt(mul) +} diff --git a/core/state/swap/swap_test.go b/core/state/swap/swap_test.go new file mode 100644 index 000000000..24bfcd392 --- /dev/null +++ b/core/state/swap/swap_test.go @@ -0,0 +1,92 @@ +package swap + +import ( + "github.com/MinterTeam/minter-go-node/core/state/bus" + "github.com/MinterTeam/minter-go-node/core/state/checker" + "github.com/MinterTeam/minter-go-node/tree" + db "github.com/tendermint/tm-db" + "math/big" + "testing" +) + +func TestPair_load(t *testing.T) { + memDB := db.NewMemDB() + immutableTree, err := tree.NewMutableTree(0, memDB, 1024, 0) + if err != nil { + t.Fatal(err) + } + newBus := bus.NewBus() + checker.NewChecker(newBus) + swap := New(newBus, immutableTree.GetLastImmutable()) + r0 := big.NewInt(1e18) + r2 := big.NewInt(2e18) + swap.PairCreate(0, 2, r0, r2) + pair := swap.Pair(0, 2) + if pair == nil { + t.Fatal("not found") + } + _, _, err = immutableTree.Commit(swap) + if err != nil { + t.Fatal(err) + } + pair = swap.Pair(2, 0) + if pair == nil { + t.Fatal("not found") + } + if pair.Reserve0.Cmp(r2) != 0 { + t.Fatal("r2") + } + if pair.Reserve1.Cmp(r0) != 0 { + t.Fatal("r0") + } + swap = New(newBus, immutableTree.GetLastImmutable()) + pair = swap.Pair(0, 2) + if pair == nil { + t.Fatal("not found") + } + + if pair.Reserve0.Cmp(r0) != 0 { + t.Fatal("r0") + } + if pair.Reserve1.Cmp(r2) != 0 { + t.Fatal("r2") + } + pair = swap.Pair(2, 0) + if pair == nil { + t.Fatal("not found") + } + if pair.Reserve0.Cmp(r2) != 0 { + t.Fatal("r2") + } + if pair.Reserve1.Cmp(r0) != 0 { + t.Fatal("r0") + } +} + +func TestPair_commission(t *testing.T) { + memDB := db.NewMemDB() + immutableTree, err := tree.NewMutableTree(0, memDB, 1024, 0) + if err != nil { + t.Fatal(err) + } + newBus := bus.NewBus() + checker.NewChecker(newBus) + swap := New(newBus, immutableTree.GetLastImmutable()) + r0 := big.NewInt(1e18) + r1 := big.NewInt(1e18) + _, _, _, _ = swap.PairCreate(0, 1, r0, r1) + + valueSwap := big.NewInt(1e17) + { + swap.PairBuy(0, 1, big.NewInt(1e18), valueSwap) + // _ = swap.Pair(0, 1) + // t.Log(pair.Reserves()) + } + + { + swap.PairSell(1, 0, valueSwap, big.NewInt(0)) + // _ = swap.Pair(0, 1) + // t.Log(pair.Reserves()) + } + +} diff --git a/core/state/validators/model.go b/core/state/validators/model.go index 1d5c0a510..bc6f2f190 100644 --- a/core/state/validators/model.go +++ b/core/state/validators/model.go @@ -5,6 +5,7 @@ import ( "github.com/MinterTeam/minter-go-node/core/types" "github.com/tendermint/tendermint/crypto/ed25519" "math/big" + "sync" ) type Validator struct { @@ -21,7 +22,8 @@ type Validator struct { tmAddress types.TmAddress toDrop bool - bus *bus.Bus + lock sync.RWMutex + bus *bus.Bus } func NewValidator(pubKey types.Pubkey, absentTimes *types.BitArray, totalStake *big.Int, accumReward *big.Int, isDirty bool, isTotalStakeDirty bool, isAccumRewardDirty bool, bus *bus.Bus) *Validator { @@ -40,33 +42,55 @@ func NewValidator(pubKey types.Pubkey, absentTimes *types.BitArray, totalStake * } func (v *Validator) IsToDrop() bool { + v.lock.RLock() + defer v.lock.RUnlock() + return v.toDrop } func (v *Validator) SetAccumReward(value *big.Int) { - if v.accumReward.Cmp(value) != 0 { - v.isAccumRewardDirty = true + v.lock.Lock() + + if v.accumReward.Cmp(value) == 0 { + v.lock.Unlock() + return } - v.bus.Checker().AddCoin(types.GetBaseCoinID(), big.NewInt(0).Sub(value, v.accumReward), "reward") + v.isAccumRewardDirty = true + oldAcc := big.NewInt(0).Set(v.accumReward) v.accumReward = big.NewInt(0).Set(value) + v.lock.Unlock() + + v.bus.Checker().AddCoin(types.GetBaseCoinID(), big.NewInt(0).Sub(value, oldAcc), "reward") } func (v *Validator) GetAccumReward() *big.Int { + v.lock.RLock() + defer v.lock.RUnlock() + return big.NewInt(0).Set(v.accumReward) } // GetAddress returns tendermint-address of a validator func (v *Validator) GetAddress() types.TmAddress { + v.lock.RLock() + defer v.lock.RUnlock() + return v.tmAddress } // GetTotalBipStake returns total bip stake func (v *Validator) GetTotalBipStake() *big.Int { + v.lock.RLock() + defer v.lock.RUnlock() + return big.NewInt(0).Set(v.totalStake) } // SetTotalBipStake sets total bip stake func (v *Validator) SetTotalBipStake(value *big.Int) { + v.lock.Lock() + defer v.lock.Unlock() + if v.totalStake.Cmp(value) == 0 { return } @@ -75,16 +99,22 @@ func (v *Validator) SetTotalBipStake(value *big.Int) { } func (v *Validator) AddAccumReward(amount *big.Int) { - v.SetAccumReward(big.NewInt(0).Add(v.accumReward, amount)) + v.lock.Lock() + reward := big.NewInt(0).Set(v.accumReward) + v.lock.Unlock() + + v.SetAccumReward(big.NewInt(0).Add(reward, amount)) } func (v *Validator) CountAbsentTimes() int { count := 0 for i := 0; i < validatorMaxAbsentWindow; i++ { + v.lock.RLock() if v.AbsentTimes.GetIndex(i) { count++ } + v.lock.RUnlock() } return count @@ -92,17 +122,24 @@ func (v *Validator) CountAbsentTimes() int { func (v *Validator) setTmAddress() { // set tm address - var pubkey ed25519.PubKeyEd25519 - copy(pubkey[:], v.PubKey[:]) + v.lock.RLock() + add := ed25519.PubKey(v.PubKey[:]).Address() + v.lock.RUnlock() var address types.TmAddress - copy(address[:], pubkey.Address().Bytes()) + copy(address[:], add.Bytes()) + v.lock.Lock() v.tmAddress = address + v.lock.Unlock() } func (v *Validator) SetPresent(height uint64) { index := int(height) % validatorMaxAbsentWindow + + v.lock.Lock() + defer v.lock.Unlock() + if v.AbsentTimes.GetIndex(index) { v.isDirty = true } @@ -111,6 +148,10 @@ func (v *Validator) SetPresent(height uint64) { func (v *Validator) SetAbsent(height uint64) { index := int(height) % validatorMaxAbsentWindow + + v.lock.Lock() + defer v.lock.Unlock() + if !v.AbsentTimes.GetIndex(index) { v.isDirty = true } diff --git a/core/state/validators/validators.go b/core/state/validators/validators.go index 03102d498..74773f627 100644 --- a/core/state/validators/validators.go +++ b/core/state/validators/validators.go @@ -9,8 +9,10 @@ import ( "github.com/MinterTeam/minter-go-node/core/state/candidates" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" "github.com/MinterTeam/minter-go-node/upgrades" + "github.com/cosmos/iavl" + "sync" + "sync/atomic" "math/big" ) @@ -28,11 +30,13 @@ const ( // Validators struct is a store of Validators state type Validators struct { - list []*Validator - loaded bool + list []*Validator + removed map[types.Pubkey]struct{} + loaded bool - iavl tree.MTree + db atomic.Value bus *bus.Bus + lock sync.RWMutex } // RValidators interface represents Validator state @@ -45,47 +49,103 @@ type RValidators interface { } // NewValidators returns newly created Validators state with a given bus and iavl -func NewValidators(bus *bus.Bus, iavl tree.MTree) (*Validators, error) { - validators := &Validators{iavl: iavl, bus: bus} +func NewValidators(bus *bus.Bus, db *iavl.ImmutableTree) *Validators { + immutableTree := atomic.Value{} + loaded := false + if db != nil { + immutableTree.Store(db) + } else { + loaded = true + } + validators := &Validators{db: immutableTree, bus: bus, loaded: loaded} + + return validators +} + +func (v *Validators) immutableTree() *iavl.ImmutableTree { + db := v.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) +} - return validators, nil +func (v *Validators) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + if v.immutableTree() == nil && v.loaded { + v.loaded = false + } + v.db.Store(immutableTree) } // Commit writes changes to iavl, may return an error -func (v *Validators) Commit() error { +func (v *Validators) Commit(db *iavl.MutableTree) error { if v.hasDirtyValidators() { + v.lock.RLock() data, err := rlp.EncodeToBytes(v.list) + v.lock.RUnlock() if err != nil { return fmt.Errorf("can't encode validators: %v", err) } path := []byte{mainPrefix} - v.iavl.Set(path, data) + db.Set(path, data) } + v.lock.Lock() + defer v.lock.Unlock() + for _, val := range v.list { - if val.isDirty || val.isTotalStakeDirty { - val.isTotalStakeDirty = false + if v.IsDirtyOrDirtyTotalStake(val) { path := []byte{mainPrefix} - path = append(path, val.PubKey.Bytes()...) // todo: remove after + + val.lock.Lock() + val.isTotalStakeDirty = false + path = append(path, val.PubKey.Bytes()...) + val.lock.Unlock() + path = append(path, totalStakePrefix) - v.iavl.Set(path, val.GetTotalBipStake().Bytes()) + db.Set(path, val.GetTotalBipStake().Bytes()) } - if val.isDirty || val.isAccumRewardDirty { - val.isAccumRewardDirty = false + if v.IsDirtyOrDirtyAccumReward(val) { path := []byte{mainPrefix} - path = append(path, val.PubKey.Bytes()...) // todo: remove after + + val.lock.Lock() + val.isAccumRewardDirty = false + path = append(path, val.PubKey.Bytes()...) + val.lock.Unlock() + path = append(path, accumRewardPrefix) - v.iavl.Set(path, val.GetAccumReward().Bytes()) + db.Set(path, val.GetAccumReward().Bytes()) } } + for pubkey := range v.removed { + path := append([]byte{mainPrefix}, pubkey.Bytes()...) + db.Remove(append(path, totalStakePrefix)) + db.Remove(append(path, accumRewardPrefix)) + } + v.removed = map[types.Pubkey]struct{}{} + v.uncheckDirtyValidators() return nil } +func (v *Validators) IsDirtyOrDirtyAccumReward(val *Validator) bool { + val.lock.RLock() + defer val.lock.RUnlock() + + return val.isDirty || val.isAccumRewardDirty +} + +func (v *Validators) IsDirtyOrDirtyTotalStake(val *Validator) bool { + val.lock.RLock() + defer val.lock.RUnlock() + + return val.isDirty || val.isTotalStakeDirty +} + // SetValidatorPresent marks validator as present at current height func (v *Validators) SetValidatorPresent(height uint64, address types.TmAddress) { validator := v.GetByTmAddress(address) @@ -102,6 +162,7 @@ func (v *Validators) SetValidatorAbsent(height uint64, address types.TmAddress) if validator == nil { return } + validator.SetAbsent(height) if validator.CountAbsentTimes() > validatorMaxAbsentTimes { @@ -115,13 +176,23 @@ func (v *Validators) SetValidatorAbsent(height uint64, address types.TmAddress) // GetValidators returns list of validators func (v *Validators) GetValidators() []*Validator { + v.lock.RLock() + defer v.lock.RUnlock() + return v.list } // SetNewValidators updated validators list with new candidates -func (v *Validators) SetNewValidators(candidates []candidates.Candidate) { +func (v *Validators) SetNewValidators(candidates []*candidates.Candidate) { old := v.GetValidators() + oldValidatorsForRemove := map[types.Pubkey]struct{}{} + for _, oldVal := range old { + oldVal.lock.RLock() + oldValidatorsForRemove[oldVal.PubKey] = struct{}{} + oldVal.lock.RUnlock() + } + var newVals []*Validator for _, candidate := range candidates { accumReward := big.NewInt(0) @@ -129,8 +200,11 @@ func (v *Validators) SetNewValidators(candidates []candidates.Candidate) { for _, oldVal := range old { if oldVal.GetAddress() == candidate.GetTmAddress() { + oldVal.lock.RLock() accumReward = oldVal.accumReward absentTimes = oldVal.AbsentTimes + delete(oldValidatorsForRemove, oldVal.PubKey) + oldVal.lock.RUnlock() } } @@ -147,6 +221,10 @@ func (v *Validators) SetNewValidators(candidates []candidates.Candidate) { }) } + v.lock.Lock() + v.removed = oldValidatorsForRemove + v.lock.Unlock() + v.SetValidators(newVals) } @@ -157,8 +235,11 @@ func (v *Validators) PunishByzantineValidator(tmAddress [20]byte) { validator := v.GetByTmAddress(tmAddress) if validator != nil { validator.SetTotalBipStake(big.NewInt(0)) + + validator.lock.Lock() validator.toDrop = true validator.isDirty = true + validator.lock.Unlock() } } @@ -175,14 +256,18 @@ func (v *Validators) Create(pubkey types.Pubkey, stake *big.Int) { } val.setTmAddress() + + v.lock.RLock() + defer v.lock.RUnlock() v.list = append(v.list, val) } // PayRewards distributes accumulated rewards between validator, delegators, DAO and developers addresses -func (v *Validators) PayRewards(height uint64) { +func (v *Validators) PayRewards() { vals := v.GetValidators() + for _, validator := range vals { - if validator.GetAccumReward().Cmp(types.Big0) == 1 { + if validator.GetAccumReward().Sign() == 1 { candidate := v.bus.Candidates().GetCandidate(validator.PubKey) totalReward := big.NewInt(0).Set(validator.GetAccumReward()) @@ -194,7 +279,7 @@ func (v *Validators) PayRewards(height uint64) { DAOReward.Div(DAOReward, big.NewInt(100)) v.bus.Accounts().AddBalance(dao.Address, types.GetBaseCoinID(), DAOReward) remainder.Sub(remainder, DAOReward) - v.bus.Events().AddEvent(uint32(height), &eventsdb.RewardEvent{ + v.bus.Events().AddEvent(&eventsdb.RewardEvent{ Role: eventsdb.RoleDAO.String(), Address: dao.Address, Amount: DAOReward.String(), @@ -207,7 +292,7 @@ func (v *Validators) PayRewards(height uint64) { DevelopersReward.Div(DevelopersReward, big.NewInt(100)) v.bus.Accounts().AddBalance(developers.Address, types.GetBaseCoinID(), DevelopersReward) remainder.Sub(remainder, DevelopersReward) - v.bus.Events().AddEvent(uint32(height), &eventsdb.RewardEvent{ + v.bus.Events().AddEvent(&eventsdb.RewardEvent{ Role: eventsdb.RoleDevelopers.String(), Address: developers.Address, Amount: DevelopersReward.String(), @@ -224,7 +309,7 @@ func (v *Validators) PayRewards(height uint64) { totalReward.Sub(totalReward, validatorReward) v.bus.Accounts().AddBalance(candidate.RewardAddress, types.GetBaseCoinID(), validatorReward) remainder.Sub(remainder, validatorReward) - v.bus.Events().AddEvent(uint32(height), &eventsdb.RewardEvent{ + v.bus.Events().AddEvent(&eventsdb.RewardEvent{ Role: eventsdb.RoleValidator.String(), Address: candidate.RewardAddress, Amount: validatorReward.String(), @@ -233,7 +318,7 @@ func (v *Validators) PayRewards(height uint64) { stakes := v.bus.Candidates().GetStakes(validator.PubKey) for _, stake := range stakes { - if stake.BipValue.Cmp(big.NewInt(0)) == 0 { + if stake.BipValue.Sign() == 0 { continue } @@ -241,14 +326,14 @@ func (v *Validators) PayRewards(height uint64) { reward.Mul(reward, stake.BipValue) reward.Div(reward, validator.GetTotalBipStake()) - if reward.Cmp(types.Big0) < 1 { + if reward.Sign() < 1 { continue } v.bus.Accounts().AddBalance(stake.Owner, types.GetBaseCoinID(), reward) remainder.Sub(remainder, reward) - v.bus.Events().AddEvent(uint32(height), &eventsdb.RewardEvent{ + v.bus.Events().AddEvent(&eventsdb.RewardEvent{ Role: eventsdb.RoleDelegator.String(), Address: stake.Owner, Amount: reward.String(), @@ -258,7 +343,7 @@ func (v *Validators) PayRewards(height uint64) { validator.SetAccumReward(big.NewInt(0)) - if remainder.Cmp(big.NewInt(0)) > -1 { + if remainder.Sign() != -1 { v.bus.App().AddTotalSlashed(remainder) } else { panic(fmt.Sprintf("Negative remainder: %s", remainder.String())) @@ -269,10 +354,16 @@ func (v *Validators) PayRewards(height uint64) { // GetByTmAddress finds and returns validator with given tendermint-address func (v *Validators) GetByTmAddress(address types.TmAddress) *Validator { + v.lock.RLock() + defer v.lock.RUnlock() + for _, val := range v.list { + val.lock.RLock() if val.tmAddress == address { + val.lock.RUnlock() return val } + val.lock.RUnlock() } return nil @@ -280,10 +371,16 @@ func (v *Validators) GetByTmAddress(address types.TmAddress) *Validator { // GetByPublicKey finds and returns validator func (v *Validators) GetByPublicKey(pubKey types.Pubkey) *Validator { + v.lock.RLock() + defer v.lock.RUnlock() + for _, val := range v.list { + val.lock.RLock() if val.PubKey == pubKey { + val.lock.RUnlock() return val } + val.lock.RUnlock() } return nil @@ -291,6 +388,9 @@ func (v *Validators) GetByPublicKey(pubKey types.Pubkey) *Validator { // LoadValidators loads only list of validators (for read) func (v *Validators) LoadValidators() { + v.lock.Lock() + defer v.lock.Unlock() + if v.loaded { return } @@ -298,7 +398,7 @@ func (v *Validators) LoadValidators() { v.loaded = true path := []byte{mainPrefix} - _, enc := v.iavl.Get(path) + _, enc := v.immutableTree().Get(path) if len(enc) == 0 { v.list = nil return @@ -314,7 +414,7 @@ func (v *Validators) LoadValidators() { // load total stake path = append([]byte{mainPrefix}, validator.PubKey.Bytes()...) path = append(path, totalStakePrefix) - _, enc = v.iavl.Get(path) + _, enc = v.immutableTree().Get(path) if len(enc) == 0 { validator.totalStake = big.NewInt(0) } else { @@ -324,7 +424,7 @@ func (v *Validators) LoadValidators() { // load accum reward path = append([]byte{mainPrefix}, validator.PubKey.Bytes()...) path = append(path, accumRewardPrefix) - _, enc = v.iavl.Get(path) + _, enc = v.immutableTree().Get(path) if len(enc) == 0 { validator.accumReward = big.NewInt(0) } else { @@ -337,6 +437,9 @@ func (v *Validators) LoadValidators() { } func (v *Validators) hasDirtyValidators() bool { + v.lock.RLock() + defer v.lock.RUnlock() + for _, val := range v.list { if val.isDirty { return true @@ -348,7 +451,9 @@ func (v *Validators) hasDirtyValidators() bool { func (v *Validators) uncheckDirtyValidators() { for _, val := range v.list { + val.lock.Lock() val.isDirty = false + val.lock.Unlock() } } @@ -361,7 +466,9 @@ func (v *Validators) punishValidator(height uint64, tmAddress types.TmAddress) { // SetValidators updates validators list func (v *Validators) SetValidators(vals []*Validator) { + v.lock.Lock() v.list = vals + v.lock.Unlock() } // Export exports all data to the given state @@ -382,17 +489,22 @@ func (v *Validators) Export(state *types.AppState) { func (v *Validators) SetToDrop(pubkey types.Pubkey) { vals := v.GetValidators() for _, val := range vals { + val.lock.Lock() if val.PubKey == pubkey { val.toDrop = true } + val.lock.Unlock() } } func (v *Validators) turnValidatorOff(tmAddress types.TmAddress) { validator := v.GetByTmAddress(tmAddress) + + validator.lock.Lock() + defer validator.lock.Unlock() + validator.AbsentTimes = types.NewBitArray(validatorMaxAbsentWindow) validator.toDrop = true validator.isDirty = true - v.bus.Candidates().SetOffline(validator.PubKey) } diff --git a/core/state/validators/validators_test.go b/core/state/validators/validators_test.go index abf2a2c8b..a4a99d8a1 100644 --- a/core/state/validators/validators_test.go +++ b/core/state/validators/validators_test.go @@ -20,13 +20,11 @@ import ( ) func TestValidators_GetValidators(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) validators.Create([32]byte{1}, big.NewInt(1000000)) validators.Create([32]byte{2}, big.NewInt(2000000)) @@ -49,13 +47,11 @@ func TestValidators_GetValidators(t *testing.T) { } func TestValidators_GetByPublicKey(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) validators.Create([32]byte{1}, big.NewInt(1000000)) validator := validators.GetByPublicKey([32]byte{1}) @@ -71,13 +67,11 @@ func TestValidators_GetByPublicKey(t *testing.T) { } func TestValidators_GetByTmAddress(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) validators.Create([32]byte{1}, big.NewInt(1000000)) validator := validators.GetByPublicKey([32]byte{1}) @@ -95,13 +89,11 @@ func TestValidators_GetByTmAddress(t *testing.T) { } func TestValidators_PunishByzantineValidator(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) validators.Create([32]byte{1}, big.NewInt(1000000)) validator := validators.GetByPublicKey([32]byte{1}) @@ -117,13 +109,11 @@ func TestValidators_PunishByzantineValidator(t *testing.T) { } func TestValidators_LoadValidators(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) newValidator := NewValidator( [32]byte{1}, @@ -139,15 +129,12 @@ func TestValidators_LoadValidators(t *testing.T) { validators.Create([32]byte{2}, big.NewInt(2000000)) - err = validators.Commit() + _, _, err := mutableTree.Commit(validators) if err != nil { t.Fatal(err) } - validators, err = NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators = NewValidators(b, mutableTree.GetLastImmutable()) validators.LoadValidators() @@ -170,13 +157,11 @@ func TestValidators_LoadValidators(t *testing.T) { } func TestValidators_SetValidators(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) newValidator := NewValidator( [32]byte{1}, @@ -202,24 +187,17 @@ func TestValidators_SetValidators(t *testing.T) { } func TestValidators_PayRewards(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - accs, err := accounts.NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accs := accounts.NewAccounts(b, mutableTree.GetLastImmutable()) + b.SetAccounts(accounts.NewBus(accs)) b.SetChecker(checker.NewChecker(b)) b.SetEvents(eventsdb.NewEventsStore(db.NewMemDB())) - appBus, err := app.NewApp(b, mutableTree) - if err != nil { - t.Fatal(err) - } + appBus := app.NewApp(b, mutableTree.GetLastImmutable()) b.SetApp(appBus) - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) newValidator := NewValidator( [32]byte{4}, types.NewBitArray(validatorMaxAbsentWindow), @@ -235,11 +213,9 @@ func TestValidators_PayRewards(t *testing.T) { t.Fatal("validator not found") } validator.AddAccumReward(big.NewInt(90)) - candidatesS, err := candidates.NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } - candidatesS.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidatesS := candidates.NewCandidates(b, mutableTree.GetLastImmutable()) + + candidatesS.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidatesS.SetOnline([32]byte{4}) candidatesS.SetStakes([32]byte{4}, []types.Stake{ { @@ -252,7 +228,7 @@ func TestValidators_PayRewards(t *testing.T) { candidatesS.RecalculateStakes(0) validators.SetNewValidators(candidatesS.GetNewCandidates(1)) - validators.PayRewards(0) + validators.PayRewards() if accs.GetBalance([20]byte{1}, 0).String() != "72" { t.Fatal("delegate did not receive the award") @@ -270,24 +246,17 @@ func TestValidators_PayRewards(t *testing.T) { } func TestValidators_SetValidatorAbsent(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - accs, err := accounts.NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accs := accounts.NewAccounts(b, mutableTree.GetLastImmutable()) + b.SetAccounts(accounts.NewBus(accs)) b.SetChecker(checker.NewChecker(b)) b.SetEvents(eventsdb.NewEventsStore(db.NewMemDB())) - appBus, err := app.NewApp(b, mutableTree) - if err != nil { - t.Fatal(err) - } + appBus := app.NewApp(b, mutableTree.GetLastImmutable()) b.SetApp(appBus) - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) newValidator := NewValidator( [32]byte{4}, types.NewBitArray(validatorMaxAbsentWindow), @@ -299,11 +268,9 @@ func TestValidators_SetValidatorAbsent(t *testing.T) { b) validators.SetValidators([]*Validator{newValidator}) - candidatesS, err := candidates.NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } - candidatesS.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidatesS := candidates.NewCandidates(b, mutableTree.GetLastImmutable()) + + candidatesS.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidatesS.SetOnline([32]byte{4}) candidatesS.SetStakes([32]byte{4}, []types.Stake{ { @@ -328,13 +295,11 @@ func TestValidators_SetValidatorAbsent(t *testing.T) { } } func TestValidators_SetValidatorPresent(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) validators.Create([32]byte{4}, big.NewInt(1000000)) @@ -357,13 +322,11 @@ func TestValidators_SetValidatorPresent(t *testing.T) { } func TestValidators_SetToDrop(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) validators.Create([32]byte{4}, big.NewInt(1000000)) @@ -382,24 +345,18 @@ func TestValidators_SetToDrop(t *testing.T) { } func TestValidators_Export(t *testing.T) { - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + t.Parallel() + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) b := bus.NewBus() - accs, err := accounts.NewAccounts(b, mutableTree) - if err != nil { - t.Fatal(err) - } + accs := accounts.NewAccounts(b, mutableTree.GetLastImmutable()) + b.SetAccounts(accounts.NewBus(accs)) b.SetChecker(checker.NewChecker(b)) b.SetEvents(eventsdb.NewEventsStore(db.NewMemDB())) - appBus, err := app.NewApp(b, mutableTree) - if err != nil { - t.Fatal(err) - } + appBus := app.NewApp(b, mutableTree.GetLastImmutable()) + b.SetApp(appBus) - validators, err := NewValidators(b, mutableTree) - if err != nil { - t.Fatal(err) - } + validators := NewValidators(b, mutableTree.GetLastImmutable()) newValidator := NewValidator( [32]byte{4}, types.NewBitArray(validatorMaxAbsentWindow), @@ -411,11 +368,9 @@ func TestValidators_Export(t *testing.T) { b) validators.SetValidators([]*Validator{newValidator}) - candidatesS, err := candidates.NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } - candidatesS.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10) + candidatesS := candidates.NewCandidates(b, mutableTree.GetLastImmutable()) + + candidatesS.Create([20]byte{1}, [20]byte{2}, [20]byte{3}, [32]byte{4}, 10, 0) candidatesS.SetOnline([32]byte{4}) candidatesS.SetStakes([32]byte{4}, []types.Stake{ { @@ -428,15 +383,11 @@ func TestValidators_Export(t *testing.T) { candidatesS.RecalculateStakes(0) validators.SetNewValidators(candidatesS.GetNewCandidates(1)) - err = validators.Commit() + hash, version, err := mutableTree.Commit(validators) if err != nil { t.Fatal(err) } - hash, version, err := mutableTree.SaveVersion() - if err != nil { - t.Fatal(err) - } if version != 1 { t.Fatalf("version %d", version) } diff --git a/core/state/waitlist/model.go b/core/state/waitlist/model.go index c33d191e6..06765f742 100644 --- a/core/state/waitlist/model.go +++ b/core/state/waitlist/model.go @@ -3,6 +3,7 @@ package waitlist import ( "github.com/MinterTeam/minter-go-node/core/types" "math/big" + "sync" ) type Item struct { @@ -16,9 +17,12 @@ type Model struct { address types.Address markDirty func(address types.Address) + lock sync.RWMutex } func (m *Model) AddToList(candidateId uint32, coin types.CoinID, value *big.Int) { + m.lock.Lock() + defer m.lock.Unlock() m.List = append(m.List, Item{ CandidateId: candidateId, Coin: coin, diff --git a/core/state/waitlist/waitlist.go b/core/state/waitlist/waitlist.go index b87a8ed57..11ffb4090 100644 --- a/core/state/waitlist/waitlist.go +++ b/core/state/waitlist/waitlist.go @@ -6,11 +6,12 @@ import ( "github.com/MinterTeam/minter-go-node/core/state/bus" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/MinterTeam/minter-go-node/tree" + "github.com/cosmos/iavl" "log" "math/big" "sort" "sync" + "sync/atomic" ) const mainPrefix = byte('w') @@ -26,26 +27,43 @@ type WaitList struct { list map[types.Address]*Model dirty map[types.Address]interface{} - bus *bus.Bus - iavl tree.MTree + db atomic.Value + + bus *bus.Bus lock sync.RWMutex } -func NewWaitList(stateBus *bus.Bus, iavl tree.MTree) (*WaitList, error) { +func NewWaitList(stateBus *bus.Bus, db *iavl.ImmutableTree) *WaitList { + immutableTree := atomic.Value{} + if db != nil { + immutableTree.Store(db) + } waitlist := &WaitList{ bus: stateBus, - iavl: iavl, + db: immutableTree, list: map[types.Address]*Model{}, dirty: map[types.Address]interface{}{}, } waitlist.bus.SetWaitList(NewBus(waitlist)) - return waitlist, nil + return waitlist +} + +func (wl *WaitList) immutableTree() *iavl.ImmutableTree { + db := wl.db.Load() + if db == nil { + return nil + } + return db.(*iavl.ImmutableTree) +} + +func (wl *WaitList) SetImmutableTree(immutableTree *iavl.ImmutableTree) { + wl.db.Store(immutableTree) } func (wl *WaitList) Export(state *types.AppState) { - wl.iavl.Iterate(func(key []byte, value []byte) bool { + wl.immutableTree().Iterate(func(key []byte, value []byte) bool { if key[0] == mainPrefix { address := types.BytesToAddress(key[1:]) @@ -70,25 +88,27 @@ func (wl *WaitList) Export(state *types.AppState) { }) } -func (wl *WaitList) Commit() error { +func (wl *WaitList) Commit(db *iavl.MutableTree) error { dirty := wl.getOrderedDirty() for _, address := range dirty { w := wl.getFromMap(address) + path := append([]byte{mainPrefix}, address.Bytes()...) wl.lock.Lock() delete(wl.dirty, address) wl.lock.Unlock() - path := append([]byte{mainPrefix}, address.Bytes()...) - // if len(w.List) != 0 { - data, err := rlp.EncodeToBytes(w) - if err != nil { - return fmt.Errorf("can't encode object at %s: %v", address.String(), err) + w.lock.RLock() + if len(w.List) != 0 { + data, err := rlp.EncodeToBytes(w) + if err != nil { + return fmt.Errorf("can't encode object at %s: %v", address.String(), err) + } + db.Set(path, data) + } else { + db.Remove(path) } - wl.iavl.Set(path, data) - // } else { // todo: remove after - // wl.iavl.Remove(path) - // } + w.lock.RUnlock() } return nil @@ -165,6 +185,8 @@ func (wl *WaitList) Delete(address types.Address, pubkey types.Pubkey, coin type } value := big.NewInt(0) + + w.lock.RLock() items := make([]Item, 0, len(w.List)-1) for _, item := range w.List { if item.CandidateId != candidate.ID || item.Coin != coin { @@ -173,8 +195,9 @@ func (wl *WaitList) Delete(address types.Address, pubkey types.Pubkey, coin type value.Add(value, item.Value) } } - w.List = items + w.lock.RUnlock() + wl.markDirty(address) wl.setToMap(address, w) wl.bus.Checker().AddCoin(coin, big.NewInt(0).Neg(value)) @@ -196,7 +219,7 @@ func (wl *WaitList) get(address types.Address) *Model { } path := append([]byte{mainPrefix}, address.Bytes()...) - _, enc := wl.iavl.Get(path) + _, enc := wl.immutableTree().Get(path) if len(enc) == 0 { return nil } @@ -228,14 +251,18 @@ func (wl *WaitList) setToMap(address types.Address, model *Model) { } func (wl *WaitList) markDirty(address types.Address) { + wl.lock.Lock() + defer wl.lock.Unlock() wl.dirty[address] = struct{}{} } func (wl *WaitList) getOrderedDirty() []types.Address { + wl.lock.Lock() keys := make([]types.Address, 0, len(wl.dirty)) for k := range wl.dirty { keys = append(keys, k) } + wl.lock.Unlock() sort.SliceStable(keys, func(i, j int) bool { return bytes.Compare(keys[i].Bytes(), keys[j].Bytes()) == 1 diff --git a/core/state/waitlist/waitlist_test.go b/core/state/waitlist/waitlist_test.go index 49e517bb9..6a5f12cd1 100644 --- a/core/state/waitlist/waitlist_test.go +++ b/core/state/waitlist/waitlist_test.go @@ -12,34 +12,24 @@ import ( ) func TestWaitListToGetByAddressAndPubKey(t *testing.T) { + t.Parallel() b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) - wl, err := NewWaitList(b, mutableTree) - if err != nil { - t.Fatal(err) - } + wl := NewWaitList(b, mutableTree.GetLastImmutable()) - candidatesState, err := candidates.NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidatesState := candidates.NewCandidates(b, mutableTree.GetLastImmutable()) addr, pubkey, coin, val := types.Address{0}, types.Pubkey{0}, types.GetBaseCoinID(), big.NewInt(1e18) - candidatesState.Create(addr, addr, addr, pubkey, 10) + candidatesState.Create(addr, addr, addr, pubkey, 10, 0) wl.AddWaitList(addr, pubkey, coin, val) - if err := wl.Commit(); err != nil { - t.Fatal(err) - } - - _, _, err = mutableTree.SaveVersion() + _, _, err := mutableTree.Commit(wl) if err != nil { t.Fatal(err) } - items := wl.GetByAddressAndPubKey(addr, pubkey) if len(items) != 1 { t.Fatal("Incorrect amount of items in waitlist") @@ -56,39 +46,29 @@ func TestWaitListToGetByAddressAndPubKey(t *testing.T) { } func TestWaitListToPartialDelete(t *testing.T) { + t.Parallel() b := bus.NewBus() b.SetChecker(checker.NewChecker(b)) - mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024) + mutableTree, _ := tree.NewMutableTree(0, db.NewMemDB(), 1024, 0) - wl, err := NewWaitList(b, mutableTree) - if err != nil { - t.Fatal(err) - } + wl := NewWaitList(b, mutableTree.GetLastImmutable()) - candidatesState, err := candidates.NewCandidates(b, mutableTree) - if err != nil { - t.Fatal(err) - } + candidatesState := candidates.NewCandidates(b, mutableTree.GetLastImmutable()) addr, pubkey, coin, val := types.Address{0}, types.Pubkey{0}, types.GetBaseCoinID(), big.NewInt(1e18) - candidatesState.Create(addr, addr, addr, pubkey, 10) + candidatesState.Create(addr, addr, addr, pubkey, 10, 0) wl.AddWaitList(addr, pubkey, coin, val) wl.AddWaitList(addr, pubkey, 1, val) wl.AddWaitList(addr, pubkey, 2, val) - if err := wl.Commit(); err != nil { - t.Fatal(err) - } - - _, _, err = mutableTree.SaveVersion() + _, _, err := mutableTree.Commit(wl) if err != nil { t.Fatal(err) } - wl.Delete(addr, pubkey, 0) wl.Delete(addr, pubkey, 1) wl.AddWaitList(addr, pubkey, 1, big.NewInt(1e17)) - _, _, err = mutableTree.SaveVersion() + _, _, err = mutableTree.Commit(wl) if err != nil { t.Fatal(err) } diff --git a/core/transaction/add_liquidity.go b/core/transaction/add_liquidity.go new file mode 100644 index 000000000..cf56f15d3 --- /dev/null +++ b/core/transaction/add_liquidity.go @@ -0,0 +1,220 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/state/swap" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +type AddLiquidityData struct { + Coin0 types.CoinID + Coin1 types.CoinID + Volume0 *big.Int + MaximumVolume1 *big.Int +} + +func (data AddLiquidityData) Gas() int { + return gasAddLiquidity +} + +func (data AddLiquidityData) TxType() TxType { + return TypeAddLiquidity +} + +func (data AddLiquidityData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if data.Coin1 == data.Coin0 { + return &Response{ + Code: code.CrossConvert, + Log: "\"From\" coin equals to \"to\" coin", + Info: EncodeError(code.NewCrossConvert( + data.Coin0.String(), + data.Coin1.String(), "", "")), + } + } + + if !context.Swap().SwapPoolExist(data.Coin0, data.Coin1) { + return &Response{ + Code: code.PairNotExists, + Log: "swap pool not found", + Info: EncodeError(code.NewPairNotExists( + data.Coin0.String(), + data.Coin1.String())), + } + } + + coin0 := context.Coins().GetCoin(data.Coin0) + if coin0 == nil { + return &Response{ + Code: code.CoinNotExists, + Log: "Coin not exists", + Info: EncodeError(code.NewCoinNotExists("", data.Coin0.String())), + } + } + + coin1 := context.Coins().GetCoin(data.Coin1) + if coin1 == nil { + return &Response{ + Code: code.CoinNotExists, + Log: "Coin not exists", + Info: EncodeError(code.NewCoinNotExists("", data.Coin1.String())), + } + } + + return nil +} + +func (data AddLiquidityData) String() string { + return fmt.Sprintf("ADD SWAP POOL") +} + +func (data AddLiquidityData) CommissionData(price *commission.Price) *big.Int { + return price.AddLiquidity +} + +func (data AddLiquidityData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + neededAmount1 := new(big.Int).Set(data.MaximumVolume1) + + swapper := checkState.Swap().GetSwapper(data.Coin0, data.Coin1) + if isGasCommissionFromPoolSwap { + if tx.GasCoin == data.Coin0 && data.Coin1.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + if tx.GasCoin == data.Coin1 && data.Coin0.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commissionInBaseCoin, commission) + } + } + coinLiquidity := checkState.Coins().GetCoinBySymbol(LiquidityCoinSymbol(swapper.CoinID()), 0) + _, neededAmount1 = swapper.CalculateAddLiquidity(data.Volume0, coinLiquidity.Volume()) + if neededAmount1.Cmp(data.MaximumVolume1) == 1 { + return Response{ + Code: code.InsufficientInputAmount, + Log: fmt.Sprintf("You wanted to add %s %s, but currently you need to add %s %s to complete tx", data.Volume0, checkState.Coins().GetCoin(data.Coin0).GetFullSymbol(), neededAmount1, checkState.Coins().GetCoin(data.Coin1).GetFullSymbol()), + Info: EncodeError(code.NewInsufficientInputAmount(data.Coin0.String(), data.Volume0.String(), data.Coin1.String(), data.MaximumVolume1.String(), neededAmount1.String())), + } + } + + if err := swapper.CheckMint(data.Volume0, neededAmount1, coinLiquidity.Volume()); err != nil { + if err == swap.ErrorInsufficientLiquidityMinted { + amount0, amount1 := swapper.Amounts(big.NewInt(1), coinLiquidity.Volume()) + return Response{ + Code: code.InsufficientLiquidityMinted, + Log: fmt.Sprintf("You wanted to add less than one liquidity, you should add %s %s and %s %s or more", + amount0, checkState.Coins().GetCoin(data.Coin0).GetFullSymbol(), amount1, checkState.Coins().GetCoin(data.Coin1).GetFullSymbol()), + Info: EncodeError(code.NewInsufficientLiquidityMinted(data.Coin0.String(), amount0.String(), data.Coin1.String(), amount1.String())), + } + } else if err == swap.ErrorInsufficientInputAmount { + return Response{ + Code: code.InsufficientInputAmount, + Log: fmt.Sprintf("You wanted to add %s %s, but currently you need to add %s %s to complete tx", data.Volume0, checkState.Coins().GetCoin(data.Coin0).GetFullSymbol(), neededAmount1, checkState.Coins().GetCoin(data.Coin1).GetFullSymbol()), + Info: EncodeError(code.NewInsufficientInputAmount(data.Coin0.String(), data.Volume0.String(), data.Coin1.String(), data.MaximumVolume1.String(), neededAmount1.String())), + } + } + } + { + amount0 := new(big.Int).Set(data.Volume0) + if tx.GasCoin == data.Coin0 { + amount0.Add(amount0, commission) + } + if checkState.Accounts().GetBalance(sender, data.Coin0).Cmp(amount0) == -1 { + symbol := checkState.Coins().GetCoin(data.Coin0).GetFullSymbol() + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), amount0.String(), symbol), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), amount0.String(), symbol, data.Coin0.String())), + } + } + } + + { + maximumVolume1 := new(big.Int).Set(neededAmount1) + if tx.GasCoin == data.Coin1 { + maximumVolume1.Add(maximumVolume1, commission) + } + if checkState.Accounts().GetBalance(sender, data.Coin1).Cmp(maximumVolume1) == -1 { + symbol := checkState.Coins().GetCoin(data.Coin1).GetFullSymbol() + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), maximumVolume1.String(), symbol), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), maximumVolume1.String(), symbol, data.Coin1.String())), + } + } + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + amount0, amount1, liquidity := deliverState.Swap.PairMint(data.Coin0, data.Coin1, data.Volume0, data.MaximumVolume1, coinLiquidity.Volume()) + deliverState.Accounts.SubBalance(sender, data.Coin0, amount0) + deliverState.Accounts.SubBalance(sender, data.Coin1, amount1) + + deliverState.Coins.AddVolume(coinLiquidity.ID(), liquidity) + deliverState.Accounts.AddBalance(sender, coinLiquidity.ID(), liquidity) + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.volume1"), Value: []byte(amount1.String())}, + {Key: []byte("tx.liquidity"), Value: []byte(liquidity.String())}, + {Key: []byte("tx.pool_token"), Value: []byte(coinLiquidity.GetFullSymbol()), Index: true}, + {Key: []byte("tx.pool_token_id"), Value: []byte(coinLiquidity.ID().String()), Index: true}, + {Key: []byte("tx.pair_ids"), Value: []byte(liquidityCoinName(data.Coin0, data.Coin1))}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} + +func liquidityCoinName(c0, c1 types.CoinID) string { + if c0 < c1 { + return fmt.Sprintf("%d-%d", c0, c1) + } + return fmt.Sprintf("%d-%d", c1, c0) +} diff --git a/core/transaction/add_liquidity_test.go b/core/transaction/add_liquidity_test.go new file mode 100644 index 000000000..6399dd894 --- /dev/null +++ b/core/transaction/add_liquidity_test.go @@ -0,0 +1,478 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/state" + "math/big" + "sync" + "testing" + + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" +) + +func createNonReserveCoin(stateDB *state.State) types.CoinID { + volume := helpers.BipToPip(big.NewInt(100000)) + + id := stateDB.App.GetNextCoinID() + stateDB.Coins.CreateToken(id, types.StrToCoinSymbol("TOKEN"+id.String()), "TOKEN"+id.String(), true, true, volume, big.NewInt(0).Mul(volume, big.NewInt(10)), nil) + stateDB.App.SetCoinsCount(id.Uint32()) + stateDB.Accounts.AddBalance(types.Address{}, id, volume) + + return id +} + +func TestAddExchangeLiquidityTx_initialLiquidity(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999999000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(10)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(10)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestAddExchangeLiquidityTx_initialLiquidity_1(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999999000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(9)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(11)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestAddExchangeLiquidityTx_addLiquidity(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + privateKey2, _ := crypto.GenerateKey() + addr2 := crypto.PubkeyToAddress(privateKey2.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999999000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + cState.Accounts.AddBalance(addr2, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin1, helpers.BipToPip(big.NewInt(50000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: big.NewInt(10000), + Coin1: coin1, + Volume1: big.NewInt(10000), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + { + data := AddLiquidityData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(10)), + Coin1: coin1, + MaximumVolume1: helpers.BipToPip(big.NewInt(10)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeAddLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey2); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestAddExchangeLiquidityTx_addLiquidity_1(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + privateKey2, _ := crypto.GenerateKey() + addr2 := crypto.PubkeyToAddress(privateKey2.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999999000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + cState.Accounts.AddBalance(addr2, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin1, helpers.BipToPip(big.NewInt(50000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(10)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(10)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } + { + data := AddLiquidityData{ + Coin0: coin, + Volume0: big.NewInt(10000), + Coin1: coin1, + MaximumVolume1: big.NewInt(10000), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeAddLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey2); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestAddExchangeLiquidityTx_addLiquidity_2(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + privateKey2, _ := crypto.GenerateKey() + addr2 := crypto.PubkeyToAddress(privateKey2.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999999000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + cState.Accounts.AddBalance(addr2, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin1, helpers.BipToPip(big.NewInt(50000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(9)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(11)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } + { + data := AddLiquidityData{ + Coin0: coin, + Volume0: big.NewInt(9000), + Coin1: coin1, + MaximumVolume1: big.NewInt(11000), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeAddLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey2); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } +} diff --git a/core/transaction/burn_token.go b/core/transaction/burn_token.go new file mode 100644 index 000000000..ab887b586 --- /dev/null +++ b/core/transaction/burn_token.go @@ -0,0 +1,148 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +type BurnTokenData struct { + Coin types.CoinID + Value *big.Int +} + +func (data BurnTokenData) Gas() int { + return gasBurnToken +} +func (data BurnTokenData) TxType() TxType { + return TypeBurnToken +} + +func (data BurnTokenData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + coin := context.Coins().GetCoin(data.Coin) + if coin == nil { + return &Response{ + Code: code.CoinNotExists, + Log: "Coin not exists", + Info: EncodeError(code.NewCoinNotExists("", data.Coin.String())), + } + } + + if !coin.IsBurnable() { + return &Response{ + Code: code.CoinNotBurnable, + Log: "Coin not burnable", + Info: EncodeError(code.NewCoinIsNotBurnable(coin.GetFullSymbol(), data.Coin.String())), + } + } + + if big.NewInt(0).Sub(coin.Volume(), data.Value).Cmp(minTokenSupply) == -1 { + return &Response{ + Code: code.WrongCoinEmission, + Log: fmt.Sprintf("Coin volume should be more than %s", minTokenSupply), + Info: EncodeError(code.NewWrongCoinEmission(minTokenSupply.String(), coin.MaxSupply().String(), coin.Volume().String(), "", data.Value.String())), + } + } + + sender, _ := tx.Sender() + symbolInfo := context.Coins().GetSymbolInfo(coin.Symbol()) + if coin.Version() != 0 || symbolInfo == nil || symbolInfo.OwnerAddress().Compare(sender) != 0 { + var owner *string + if symbolInfo != nil && symbolInfo.OwnerAddress() != nil { + own := symbolInfo.OwnerAddress().String() + owner = &own + } + return &Response{ + Code: code.IsNotOwnerOfCoin, + Log: "Sender is not owner of coin", + Info: EncodeError(code.NewIsNotOwnerOfCoin(coin.Symbol().String(), owner)), + } + } + + return nil +} + +func (data BurnTokenData) String() string { + return fmt.Sprintf("BURN COIN: %d", data.Coin) +} + +func (data BurnTokenData) CommissionData(price *commission.Price) *big.Int { + return price.BurnToken +} + +func (data BurnTokenData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + value := big.NewInt(0).Set(data.Value) + if tx.GasCoin == data.Coin { + value.Add(value, commission) + } + + if checkState.Accounts().GetBalance(sender, data.Coin).Cmp(value) == -1 { + symbol := checkState.Coins().GetCoin(data.Coin).GetFullSymbol() + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), value.String(), symbol), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), value.String(), symbol, data.Coin.String())), + } + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + deliverState.Coins.SubVolume(data.Coin, data.Value) + deliverState.Accounts.SubBalance(sender, data.Coin, data.Value) + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_id"), Value: []byte(data.Coin.String()), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/burn_token_test.go b/core/transaction/burn_token_test.go new file mode 100644 index 000000000..94cb3f8c2 --- /dev/null +++ b/core/transaction/burn_token_test.go @@ -0,0 +1,188 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" + "testing" +) + +func TestBurnData_aaa(t *testing.T) { + t.Parallel() + cState := getState() + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + coin := types.GetBaseCoinID() + + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) + + toCreate := types.StrToCoinSymbol("TOKEN1") + + amount := helpers.BipToPip(big.NewInt(100)) + name := "My Test Coin" + { + data := CreateTokenData{ + Name: name, + Symbol: toCreate, + InitialAmount: amount, + MaxSupply: big.NewInt(0).Mul(amount, big.NewInt(10)), + Mintable: false, + Burnable: true, + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeCreateToken, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error %s", response.Log) + } + + _, _, err = cState.Tree().Commit(cState.Coins) + if err != nil { + t.Fatalf("Commit coins failed. Error %s", err) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + + targetBalance, _ := big.NewInt(0).SetString("999000000000000000000000", 10) + balance := cState.Accounts.GetBalance(addr, coin) + if balance.Cmp(targetBalance) != 0 { + t.Errorf("Target %s balance is not correct. Expected %s, got %s", coin, targetBalance, balance) + } + + stateCoin := cState.Coins.GetCoinBySymbol(toCreate, 0) + + if stateCoin == nil { + t.Fatalf("Coin %s not found in state", toCreate) + } + + if stateCoin.Volume().Cmp(amount) != 0 { + t.Fatalf("Volume in state is not correct. Expected %s, got %s", amount, stateCoin.MaxSupply()) + } + + if stateCoin.Name() != name { + t.Fatalf("Name in state is not correct. Expected %s, got %s", name, stateCoin.Name()) + } + + if stateCoin.Version() != 0 { + t.Fatalf("Version in state is not correct. Expected %d, got %d", 0, stateCoin.Version()) + } + + symbolInfo := cState.Coins.GetSymbolInfo(toCreate) + if symbolInfo == nil { + t.Fatalf("Symbol %s info not found in state", toCreate) + } + + if *symbolInfo.OwnerAddress() != addr { + t.Fatalf("Target owner address is not correct. Expected %s, got %s", addr.String(), symbolInfo.OwnerAddress().String()) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + stateCoin := cState.Coins.GetCoinBySymbol(toCreate, 0) + + if stateCoin == nil { + t.Fatalf("Coin %s not found in state", toCreate) + } + + if stateCoin.Volume().Cmp(amount) != 0 { + t.Fatalf("Volume in state is not correct. Expected %s, got %s", amount, stateCoin.Volume()) + } + + subVolume := big.NewInt(1e18) + data := BurnTokenData{ + Coin: stateCoin.ID(), + Value: big.NewInt(0).Set(subVolume), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeBurnToken, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error %s", response.Log) + } + + _, _, err = cState.Tree().Commit(cState.Coins) + if err != nil { + t.Fatalf("Commit coins failed. Error %s", err) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + + stateCoin = cState.Coins.GetCoinBySymbol(toCreate, 0) + + if stateCoin == nil { + t.Fatalf("Coin %s not found in state", toCreate) + } + + amount.Sub(amount, subVolume) + + if stateCoin.Volume().Cmp(amount) != 0 { + t.Errorf("Volume in state is not correct. Expected %s, got %s", amount, stateCoin.MaxSupply()) + } + + balance := cState.Accounts.GetBalance(addr, stateCoin.ID()) + if balance.Cmp(amount) != 0 { + t.Errorf("Target %s balance is not correct. Expected %s, got %s", stateCoin.ID(), amount, balance) + } + } +} diff --git a/core/transaction/buy_coin.go b/core/transaction/buy_coin.go index cc383fa93..a7a45304b 100644 --- a/core/transaction/buy_coin.go +++ b/core/transaction/buy_coin.go @@ -1,14 +1,13 @@ package transaction import ( - "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -19,322 +18,80 @@ type BuyCoinData struct { MaximumValueToSell *big.Int } +func (data BuyCoinData) Gas() int { + return gasBuyCoin +} +func (data BuyCoinData) TxType() TxType { + return TypeBuyCoin +} + func (data BuyCoinData) String() string { return fmt.Sprintf("BUY COIN sell:%s buy:%s %s", data.CoinToSell.String(), data.ValueToBuy.String(), data.CoinToBuy.String()) } -func (data BuyCoinData) Gas() int64 { - return commissions.ConvertTx +func (data BuyCoinData) CommissionData(price *commission.Price) *big.Int { + return price.BuyBancor } -func (data BuyCoinData) totalSpend(tx *Transaction, context *state.CheckState) (TotalSpends, - []conversion, *big.Int, *Response) { - total := TotalSpends{} - var conversions []conversion - - commissionInBaseCoin := tx.CommissionInBaseCoin() - commissionIncluded := false - - if !data.CoinToBuy.IsBaseCoin() { - coin := context.Coins().GetCoin(data.CoinToBuy) - - if errResp := CheckForCoinSupplyOverflow(coin, data.ValueToBuy); errResp != nil { - return nil, nil, nil, errResp - } - } - - var value *big.Int - - switch { - case data.CoinToSell.IsBaseCoin(): - coin := context.Coins().GetCoin(data.CoinToBuy) - value = formula.CalculatePurchaseAmount(coin.Volume(), coin.Reserve(), coin.Crr(), data.ValueToBuy) - - if value.Cmp(data.MaximumValueToSell) == 1 { - return nil, nil, nil, &Response{ - Code: code.MaximumValueToSellReached, - Log: fmt.Sprintf( - "You wanted to sell maximum %s, but currently you need to spend %s to complete tx", - data.MaximumValueToSell.String(), value.String()), - Info: EncodeError(code.NewMaximumValueToSellReached(data.MaximumValueToSell.String(), value.String(), coin.GetFullSymbol(), coin.ID().String())), - } - } - - if tx.GasCoin == data.CoinToBuy { - commissionIncluded = true - - nVolume := big.NewInt(0).Set(coin.Volume()) - nVolume.Add(nVolume, data.ValueToBuy) - - nReserveBalance := big.NewInt(0).Set(coin.Reserve()) - nReserveBalance.Add(nReserveBalance, value) - - if nReserveBalance.Cmp(commissionInBaseCoin) == -1 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coin.Reserve().String(), - types.GetBaseCoin(), - commissionInBaseCoin.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coin.GetFullSymbol(), - coin.ID().String(), - coin.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coin.Crr(), commissionInBaseCoin) - - total.Add(tx.GasCoin, commission) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - total.Add(data.CoinToSell, value) - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - ToCoin: data.CoinToBuy, - ToAmount: data.ValueToBuy, - ToReserve: value, - }) - case data.CoinToBuy.IsBaseCoin(): - valueToBuy := big.NewInt(0).Set(data.ValueToBuy) - - if tx.GasCoin == data.CoinToSell { - commissionIncluded = true - valueToBuy.Add(valueToBuy, commissionInBaseCoin) - } - - coin := context.Coins().GetCoin(data.CoinToSell) - value = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), valueToBuy) - - if value.Cmp(data.MaximumValueToSell) == 1 { - return nil, nil, nil, &Response{ - Code: code.MaximumValueToSellReached, - Log: fmt.Sprintf( - "You wanted to sell maximum %s, but currently you need to spend %s to complete tx", - data.MaximumValueToSell.String(), value.String()), - Info: EncodeError(code.NewMaximumValueToSellReached(data.MaximumValueToSell.String(), value.String(), coin.GetFullSymbol(), coin.ID().String())), - } - } - - total.Add(data.CoinToSell, value) - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - FromAmount: value, - FromReserve: valueToBuy, - ToCoin: data.CoinToBuy, - }) - default: - valueToBuy := big.NewInt(0).Set(data.ValueToBuy) - - coinFrom := context.Coins().GetCoin(data.CoinToSell) - coinTo := context.Coins().GetCoin(data.CoinToBuy) - baseCoinNeeded := formula.CalculatePurchaseAmount(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), valueToBuy) - - if coinFrom.Reserve().Cmp(baseCoinNeeded) < 0 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coinFrom.Reserve().String(), - types.GetBaseCoin(), - baseCoinNeeded.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coinFrom.GetFullSymbol(), - coinFrom.ID().String(), - coinFrom.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - if tx.GasCoin == data.CoinToBuy { - commissionIncluded = true - - nVolume := big.NewInt(0).Set(coinTo.Volume()) - nVolume.Add(nVolume, valueToBuy) - - nReserveBalance := big.NewInt(0).Set(coinTo.Reserve()) - nReserveBalance.Add(nReserveBalance, baseCoinNeeded) - - if nReserveBalance.Cmp(commissionInBaseCoin) == -1 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coinTo.Reserve().String(), - types.GetBaseCoin(), - commissionInBaseCoin.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coinTo.GetFullSymbol(), - coinTo.ID().String(), - coinTo.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coinTo.Crr(), commissionInBaseCoin) - - total.Add(tx.GasCoin, commission) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - value = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), baseCoinNeeded) - - if value.Cmp(data.MaximumValueToSell) == 1 { - return nil, nil, nil, &Response{ - Code: code.MaximumValueToSellReached, - Log: fmt.Sprintf("You wanted to sell maximum %s, but currently you need to spend %s to complete tx", - data.MaximumValueToSell.String(), value.String()), - Info: EncodeError(code.NewMaximumValueToSellReached(data.MaximumValueToSell.String(), value.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), - } - } - - if tx.GasCoin == data.CoinToSell { - commissionIncluded = true - - nVolume := big.NewInt(0).Set(coinFrom.Volume()) - nVolume.Sub(nVolume, value) - - nReserveBalance := big.NewInt(0).Set(coinFrom.Reserve()) - nReserveBalance.Sub(nReserveBalance, baseCoinNeeded) - - if nReserveBalance.Cmp(commissionInBaseCoin) == -1 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coinFrom.Reserve().String(), - types.GetBaseCoin(), - commissionInBaseCoin.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coinFrom.GetFullSymbol(), - coinFrom.ID().String(), - coinFrom.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coinFrom.Crr(), commissionInBaseCoin) - - total.Add(tx.GasCoin, commission) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - - totalValue := big.NewInt(0).Add(value, commission) - if totalValue.Cmp(data.MaximumValueToSell) == 1 { - return nil, nil, nil, &Response{ - Code: code.MaximumValueToSellReached, - Log: fmt.Sprintf("You wanted to sell maximum %s, but currently you need to spend %s to complete tx", data.MaximumValueToSell.String(), totalValue.String()), - Info: EncodeError(code.NewMaximumValueToSellReached(data.MaximumValueToSell.String(), value.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), - } - } - } - - total.Add(data.CoinToSell, value) - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - FromAmount: value, - FromReserve: baseCoinNeeded, - ToCoin: data.CoinToBuy, - ToAmount: valueToBuy, - ToReserve: baseCoinNeeded, - }) - } - - if !commissionIncluded { - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - coin := context.Coins().GetCoin(tx.GasCoin) - - if coin.Reserve().Cmp(commissionInBaseCoin) < 0 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coin.Reserve().String(), - types.GetBaseCoin(), - commissionInBaseCoin.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coin.GetFullSymbol(), - coin.ID().String(), - coin.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - commission = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), commissionInBaseCoin) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - total.Add(tx.GasCoin, commission) - } - - return total, conversions, value, nil -} - -func (data BuyCoinData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data BuyCoinData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.ValueToBuy == nil { return &Response{ Code: code.DecodeError, Log: "Incorrect tx data", Info: EncodeError(code.NewDecodeError()), } - } - if !context.Coins().Exists(data.CoinToSell) { + coinToSell := context.Coins().GetCoin(data.CoinToSell) + if coinToSell == nil { return &Response{ Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", data.CoinToSell), + Log: "Coin to sell not exists", Info: EncodeError(code.NewCoinNotExists("", data.CoinToSell.String())), } } - if !context.Coins().Exists(data.CoinToBuy) { + if !coinToSell.BaseOrHasReserve() { + return &Response{ + Code: code.CoinHasNotReserve, + Log: "sell coin has no reserve", + Info: EncodeError(code.NewCoinHasNotReserve( + coinToSell.GetFullSymbol(), + coinToSell.ID().String(), + )), + } + } + + coinToBuy := context.Coins().GetCoin(data.CoinToBuy) + if coinToBuy == nil { return &Response{ Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", data.CoinToBuy), + Log: "Coin to buy not exists", Info: EncodeError(code.NewCoinNotExists("", data.CoinToBuy.String())), } } + if !coinToBuy.BaseOrHasReserve() { + return &Response{ + Code: code.CoinHasNotReserve, + Log: "coin to buy has no reserve", + Info: EncodeError(code.NewCoinHasNotReserve( + coinToBuy.GetFullSymbol(), + coinToBuy.ID().String(), + )), + } + } + if data.CoinToSell == data.CoinToBuy { return &Response{ Code: code.CrossConvert, Log: "\"From\" coin equals to \"to\" coin", Info: EncodeError(code.NewCrossConvert( data.CoinToSell.String(), - context.Coins().GetCoin(data.CoinToSell).GetFullSymbol(), + coinToSell.GetFullSymbol(), data.CoinToBuy.String(), - context.Coins().GetCoin(data.CoinToBuy).GetFullSymbol()), + coinToBuy.GetFullSymbol()), ), } } @@ -342,101 +99,191 @@ func (data BuyCoinData) BasicCheck(tx *Transaction, context *state.CheckState) * return nil } -func (data BuyCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data BuyCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() - + var errResp *Response var checkState *state.CheckState var isCheck bool if checkState, isCheck = context.(*state.CheckState); !isCheck { checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - totalSpends, conversions, value, response := data.totalSpend(tx, checkState) - if response != nil { - return *response + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } - for _, ts := range totalSpends { - if checkState.Accounts().GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { - coin := checkState.Coins().GetCoin(ts.Coin) + coinToSell := data.CoinToSell + coinToBuy := data.CoinToBuy + var coinFrom, coinTo CalculateCoin + coinFrom = checkState.Coins().GetCoin(coinToSell) + coinTo = checkState.Coins().GetCoin(coinToBuy) - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", - sender.String(), - ts.Value.String(), - coin.GetFullSymbol()), - Info: EncodeError(code.NewInsufficientFunds(sender.String(), ts.Value.String(), coin.GetFullSymbol(), coin.ID().String())), + value := big.NewInt(0).Set(data.ValueToBuy) + + if isGasCommissionFromPoolSwap == false && !tx.GasCoin.IsBaseCoin() { + if tx.GasCoin == data.CoinToSell { + coinFrom = DummyCoin{ + id: gasCoin.ID(), + volume: big.NewInt(0).Sub(gasCoin.Volume(), commission), + reserve: big.NewInt(0).Sub(gasCoin.Reserve(), commissionInBaseCoin), + crr: gasCoin.Crr(), + fullSymbol: gasCoin.GetFullSymbol(), + maxSupply: gasCoin.MaxSupply(), + } + } else if tx.GasCoin == data.CoinToBuy { + coinTo = DummyCoin{ + id: gasCoin.ID(), + volume: big.NewInt(0).Sub(gasCoin.Volume(), commission), + reserve: big.NewInt(0).Sub(gasCoin.Reserve(), commissionInBaseCoin), + crr: gasCoin.Crr(), + fullSymbol: gasCoin.GetFullSymbol(), + maxSupply: gasCoin.MaxSupply(), } } } - errResp := checkConversionsReserveUnderflow(conversions, checkState) - if errResp != nil { - return *errResp + if !coinToBuy.IsBaseCoin() { + if errResp = CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { + return *errResp + } + value = formula.CalculatePurchaseAmount(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), value) } - - if deliverState, ok := context.(*state.State); ok { - for _, ts := range totalSpends { - deliverState.Accounts.SubBalance(sender, ts.Coin, ts.Value) + diffBipReserve := big.NewInt(0).Set(value) + if !coinToSell.IsBaseCoin() { + value, errResp = CalculateSaleAmountAndCheck(coinFrom, value) + if errResp != nil { + return *errResp } + } - for _, conversion := range conversions { - deliverState.Coins.SubVolume(conversion.FromCoin, conversion.FromAmount) - deliverState.Coins.SubReserve(conversion.FromCoin, conversion.FromReserve) + valueToSell := big.NewInt(0).Set(value) + if valueToSell.Cmp(data.MaximumValueToSell) == 1 { + return Response{ + Code: code.MaximumValueToSellReached, + Log: fmt.Sprintf( + "You wanted to sell maximum %s, but currently you need to spend %s to complete tx", + data.MaximumValueToSell.String(), valueToSell.String()), + Info: EncodeError(code.NewMaximumValueToSellReached(data.MaximumValueToSell.String(), valueToSell.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), + } + } - deliverState.Coins.AddVolume(conversion.ToCoin, conversion.ToAmount) - deliverState.Coins.AddReserve(conversion.ToCoin, conversion.ToReserve) + spendInGasCoin := big.NewInt(0).Set(commission) + if tx.GasCoin == coinToSell { + spendInGasCoin.Add(spendInGasCoin, value) + } else { + if checkState.Accounts().GetBalance(sender, data.CoinToSell).Cmp(value) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), value.String(), coinFrom.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), value.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), + } } + } - rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(spendInGasCoin) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), spendInGasCoin.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), spendInGasCoin.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + deliverState.Accounts.SubBalance(sender, data.CoinToSell, value) + if !data.CoinToSell.IsBaseCoin() { + deliverState.Coins.SubVolume(data.CoinToSell, value) + deliverState.Coins.SubReserve(data.CoinToSell, diffBipReserve) + } deliverState.Accounts.AddBalance(sender, data.CoinToBuy, data.ValueToBuy) + if !data.CoinToBuy.IsBaseCoin() { + deliverState.Coins.AddVolume(data.CoinToBuy, data.ValueToBuy) + deliverState.Coins.AddReserve(data.CoinToBuy, diffBipReserve) + } deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeBuyCoin)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.coin_to_buy"), Value: []byte(data.CoinToBuy.String())}, - kv.Pair{Key: []byte("tx.coin_to_sell"), Value: []byte(data.CoinToSell.String())}, - kv.Pair{Key: []byte("tx.return"), Value: []byte(value.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_to_buy"), Value: []byte(data.CoinToBuy.String()), Index: true}, + {Key: []byte("tx.coin_to_sell"), Value: []byte(data.CoinToSell.String()), Index: true}, + {Key: []byte("tx.return"), Value: []byte(value.String())}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } -func checkConversionsReserveUnderflow(conversions []conversion, context *state.CheckState) *Response { - var totalReserveCoins []types.CoinID - totalReserveSub := make(map[types.CoinID]*big.Int) - for _, conversion := range conversions { - if conversion.FromCoin.IsBaseCoin() { - continue - } - - if totalReserveSub[conversion.FromCoin] == nil { - totalReserveCoins = append(totalReserveCoins, conversion.FromCoin) - totalReserveSub[conversion.FromCoin] = big.NewInt(0) +func CalculateSaleAmountAndCheck(coinFrom CalculateCoin, value *big.Int) (*big.Int, *Response) { + if coinFrom.Reserve().Cmp(value) == -1 { + return nil, &Response{ + Code: code.CoinReserveNotSufficient, + Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", + coinFrom.Reserve().String(), + types.GetBaseCoin(), + value.String(), + types.GetBaseCoin()), + Info: EncodeError(code.NewCoinReserveNotSufficient( + coinFrom.GetFullSymbol(), + coinFrom.ID().String(), + coinFrom.Reserve().String(), + value.String(), + )), } + } + value = formula.CalculateSaleAmount(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), value) + if coinFrom.ID().IsBaseCoin() { + return value, nil + } - totalReserveSub[conversion.FromCoin].Add(totalReserveSub[conversion.FromCoin], conversion.FromReserve) + if errResp := CheckReserveUnderflow(coinFrom, value); errResp != nil { + return nil, errResp } - for _, coinSymbol := range totalReserveCoins { - errResp := CheckReserveUnderflow(context.Coins().GetCoin(coinSymbol), totalReserveSub[coinSymbol]) - if errResp != nil { - return errResp + return value, nil +} + +func CalculateSaleReturnAndCheck(coinFrom CalculateCoin, value *big.Int) (*big.Int, *Response) { + if coinFrom.Volume().Cmp(value) == -1 { + return nil, &Response{ + Code: code.CoinReserveNotSufficient, + Log: fmt.Sprintf("Coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", + coinFrom.Reserve().String(), + types.GetBaseCoin(), + value.String(), + types.GetBaseCoin()), + Info: EncodeError(code.NewCoinReserveNotSufficient( + coinFrom.GetFullSymbol(), + coinFrom.ID().String(), + coinFrom.Reserve().String(), + value.String(), + )), } } - - return nil + value = formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), value) + if errResp := CheckReserveUnderflow(coinFrom, value); errResp != nil { + return nil, errResp + } + return value, nil } diff --git a/core/transaction/buy_coin_test.go b/core/transaction/buy_coin_test.go index 5767fb2d2..da0117527 100644 --- a/core/transaction/buy_coin_test.go +++ b/core/transaction/buy_coin_test.go @@ -1,7 +1,12 @@ package transaction import ( + "bytes" "crypto/ecdsa" + "crypto/sha256" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/pkg/errors" + tmjson "github.com/tendermint/tendermint/libs/json" "log" "math/big" "math/rand" @@ -20,18 +25,66 @@ import ( ) var ( - rnd = rand.New(rand.NewSource(time.Now().Unix())) + rnd = rand.New(rand.NewSource(time.Now().Unix())) + commissionPrice = commission.Price{ + Coin: types.GetBaseCoinID(), + PayloadByte: helpers.StringToBigInt("2000000000000000"), + Send: helpers.StringToBigInt("10000000000000000"), + BuyBancor: helpers.StringToBigInt("100000000000000000"), + SellBancor: helpers.StringToBigInt("100000000000000000"), + SellAllBancor: helpers.StringToBigInt("100000000000000000"), + BuyPoolBase: helpers.StringToBigInt("100000000000000000"), + BuyPoolDelta: helpers.StringToBigInt("50000000000000000"), + SellPoolBase: helpers.StringToBigInt("100000000000000000"), + SellPoolDelta: helpers.StringToBigInt("50000000000000000"), + SellAllPoolBase: helpers.StringToBigInt("100000000000000000"), + SellAllPoolDelta: helpers.StringToBigInt("50000000000000000"), + CreateTicker3: helpers.StringToBigInt("1000000000000000000000000"), + CreateTicker4: helpers.StringToBigInt("100000000000000000000000"), + CreateTicker5: helpers.StringToBigInt("10000000000000000000000"), + CreateTicker6: helpers.StringToBigInt("1000000000000000000000"), + CreateTicker7to10: helpers.StringToBigInt("100000000000000000000"), + CreateCoin: helpers.StringToBigInt("0"), + CreateToken: helpers.StringToBigInt("0"), + RecreateCoin: helpers.StringToBigInt("10000000000000000000000"), + RecreateToken: helpers.StringToBigInt("10000000000000000000000"), + DeclareCandidacy: helpers.StringToBigInt("10000000000000000000"), + Delegate: helpers.StringToBigInt("200000000000000000"), + Unbond: helpers.StringToBigInt("200000000000000000"), + RedeemCheck: helpers.StringToBigInt("30000000000000000"), + SetCandidateOn: helpers.StringToBigInt("100000000000000000"), + SetCandidateOff: helpers.StringToBigInt("100000000000000000"), + CreateMultisig: helpers.StringToBigInt("100000000000000000"), + MultisendBase: helpers.StringToBigInt("10000000000000000"), + MultisendDelta: helpers.StringToBigInt("5000000000000000"), + EditCandidate: helpers.StringToBigInt("10000000000000000000"), + SetHaltBlock: helpers.StringToBigInt("1000000000000000000"), + EditTickerOwner: helpers.StringToBigInt("10000000000000000000000"), + EditMultisig: helpers.StringToBigInt("1000000000000000000"), + PriceVote: helpers.StringToBigInt("10000000000000000"), + EditCandidatePublicKey: helpers.StringToBigInt("100000000000000000000000"), + CreateSwapPool: helpers.StringToBigInt("1000000000000000000"), + AddLiquidity: helpers.StringToBigInt("100000000000000000"), + RemoveLiquidity: helpers.StringToBigInt("100000000000000000"), + EditCandidateCommission: helpers.StringToBigInt("10000000000000000000"), + MoveStake: helpers.StringToBigInt("200000000000000000"), + BurnToken: helpers.StringToBigInt("100000000000000000"), + MintToken: helpers.StringToBigInt("100000000000000000"), + VoteCommission: helpers.StringToBigInt("1000000000000000000"), + VoteUpdate: helpers.StringToBigInt("1000000000000000000"), + More: nil, + } ) func getState() *state.State { - s, err := state.NewState(0, db.NewMemDB(), nil, 1, 1) + s, err := state.NewState(0, db.NewMemDB(), nil, 1, 1, 0) if err != nil { panic(err) } s.Validators.Create(types.Pubkey{}, big.NewInt(1)) - s.Candidates.Create(types.Address{}, types.Address{}, types.Address{}, types.Pubkey{}, 10) - + s.Candidates.Create(types.Address{}, types.Address{}, types.Address{}, types.Pubkey{}, 10, 0) + s.Commission.SetNewCommissions(commissionPrice.Encode()) return s } @@ -66,7 +119,7 @@ func createTestCoinWithOwner(stateDB *state.State, owner types.Address) types.Co stateDB.App.SetCoinsCount(id.Uint32()) stateDB.Accounts.AddBalance(types.Address{}, id, volume) - err := stateDB.Coins.Commit() + _, _, err := stateDB.Tree().Commit(stateDB.Coins) if err != nil { log.Fatalf("failed to commit coins: %s", err) } @@ -74,19 +127,44 @@ func createTestCoinWithOwner(stateDB *state.State, owner types.Address) types.Co return id } -func checkState(t *testing.T, cState *state.State) { +func checkState(cState *state.State) error { if _, err := cState.Commit(); err != nil { - t.Fatal(err) + return err } - version := cState.Tree().Version() - exportedState := cState.Export(uint64(version)) + exportedState := cState.Export() if err := exportedState.Verify(); err != nil { - t.Fatalf("error export version %d: %s", version, err) + return errors.Wrapf(err, "error export version %d", cState.Tree().Version()) + } + + reloadExport := cState.ReloadFromDiskAndExport() + if err := reloadExport.Verify(); err != nil { + return errors.Wrapf(err, "error reload export version %d", cState.Tree().Version()) + } + + if bytes.Compare( + getStateSha256Hash(exportedState), + getStateSha256Hash(reloadExport), + ) != 0 { + return errors.New("current state and real from disk state not equal") } + + return nil +} + +func getStateSha256Hash(a types.AppState) []byte { + bytes, err := tmjson.Marshal(a) + if err != nil { + panic(err) + } + + h := sha256.New() + h.Write(bytes) + return h.Sum(nil) } func TestBuyCoinTxBaseToCustom(t *testing.T) { + t.Parallel() cState := getState() coinToBuyID := createTestCoin(cState) @@ -151,10 +229,13 @@ func TestBuyCoinTxBaseToCustom(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", getTestCoinSymbol(), toBuy, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() coinToBuyID := createTestCoin(cState) @@ -207,10 +288,13 @@ func TestBuyCoinTxInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxEqualCoins(t *testing.T) { + t.Parallel() cState := getState() coinID := createTestCoin(cState) @@ -255,10 +339,13 @@ func TestBuyCoinTxEqualCoins(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CrossConvert, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxNotExistsBuyCoin(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -301,10 +388,13 @@ func TestBuyCoinTxNotExistsBuyCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxNotExistsSellCoin(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -347,10 +437,13 @@ func TestBuyCoinTxNotExistsSellCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxNotExistsGasCoin(t *testing.T) { + t.Parallel() cState := getState() coinToSellID := createTestCoin(cState) @@ -395,10 +488,13 @@ func TestBuyCoinTxNotExistsGasCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxNotGasCoin(t *testing.T) { + t.Parallel() cState := getState() coinToSellID := createTestCoin(cState) @@ -449,10 +545,13 @@ func TestBuyCoinTxNotGasCoin(t *testing.T) { t.Fatalf("Response code is not 0. Error %s", response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxCustomToBase(t *testing.T) { + t.Parallel() cState := getState() coinToSellID := createTestCoin(cState) @@ -508,7 +607,7 @@ func TestBuyCoinTxCustomToBase(t *testing.T) { targetBalance, _ := big.NewInt(0).SetString("9999897985363348906133281", 10) balance := cState.Accounts.GetBalance(addr, coinToSellID) if balance.Cmp(targetBalance) != 0 { - t.Fatalf("Target %s balance is not correct. Expected %s, got %s", coinToSellID.String(), targetBalance, balance) + t.Errorf("Target %s balance is not correct. Expected %s, got %s", coinToSellID.String(), targetBalance, balance) } baseBalance := cState.Accounts.GetBalance(addr, types.GetBaseCoinID()) @@ -528,10 +627,13 @@ func TestBuyCoinTxCustomToBase(t *testing.T) { t.Fatalf("Target %s volume is not correct. Expected %s, got %s", coinToSellID.String(), targetVolume, coinData.Volume()) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() coinToSellID := createTestCoin(cState) @@ -584,10 +686,13 @@ func TestBuyCoinReserveUnderflow(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxBaseToCustomBaseCommission(t *testing.T) { + t.Parallel() // sell_coin: MNT // buy_coin: TEST // gas_coin: MNT @@ -631,7 +736,12 @@ func TestBuyCoinTxBaseToCustomBaseCommission(t *testing.T) { // check sold coins + commission sellCoinBalance := cState.Accounts.GetBalance(addr, coinToSell) estimatedSellCoinBalance := big.NewInt(0).Set(initialBalance) - estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, tx.CommissionInBaseCoin()) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, commissionInBaseCoin) estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, formula.CalculatePurchaseAmount(initialVolume, initialReserve, crr, toBuy)) if sellCoinBalance.Cmp(estimatedSellCoinBalance) != 0 { t.Fatalf("Sell coin balance is not correct") @@ -652,10 +762,13 @@ func TestBuyCoinTxBaseToCustomBaseCommission(t *testing.T) { t.Fatalf("Wrong coin supply") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxCustomToBaseBaseCommission(t *testing.T) { + t.Parallel() // sell_coin: TEST // buy_coin: MNT // gas_coin: MNT @@ -724,10 +837,13 @@ func TestBuyCoinTxCustomToBaseBaseCommission(t *testing.T) { t.Fatalf("Wrong coin supply") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxCustomToCustomBaseCommission(t *testing.T) { + t.Parallel() // sell_coin: TEST1 // buy_coin: TEST2 // gas_coin: MNT @@ -798,10 +914,13 @@ func TestBuyCoinTxCustomToCustomBaseCommission(t *testing.T) { t.Fatalf("Wrong coin supply") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxBaseToCustomCustomCommission(t *testing.T) { + t.Parallel() // sell_coin: MNT // buy_coin: TEST // gas_coin: TEST @@ -842,7 +961,13 @@ func TestBuyCoinTxBaseToCustomCustomCommission(t *testing.T) { estimatedBuyCoinBalance := big.NewInt(0).Set(toBuy) estimatedBuyCoinBalance.Add(estimatedBuyCoinBalance, initialGasBalance) toReserve := formula.CalculatePurchaseAmount(initialVolume, initialReserve, crr, toBuy) - commission := formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume, toBuy), big.NewInt(0).Add(initialReserve, toReserve), crr, tx.CommissionInBaseCoin()) + + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + commission := formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume, toBuy), big.NewInt(0).Add(initialReserve, toReserve), crr, commissionInBaseCoin) estimatedBuyCoinBalance.Sub(estimatedBuyCoinBalance, commission) if buyCoinBalance.Cmp(estimatedBuyCoinBalance) != 0 { t.Fatalf("Buy coin balance is not correct. Expected %s, got %s", estimatedBuyCoinBalance.String(), buyCoinBalance.String()) @@ -861,7 +986,7 @@ func TestBuyCoinTxBaseToCustomCustomCommission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve) estimatedReserve.Add(estimatedReserve, formula.CalculatePurchaseAmount(initialVolume, initialReserve, crr, toBuy)) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve") } @@ -873,10 +998,13 @@ func TestBuyCoinTxBaseToCustomCustomCommission(t *testing.T) { t.Fatalf("Wrong coin supply. Expected %s, got %s", estimatedSupply.String(), coinData.Volume().String()) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxCustomToBaseCustomCommission(t *testing.T) { + t.Parallel() // sell_coin: TEST // buy_coin: MNT // gas_coin: TEST @@ -919,10 +1047,15 @@ func TestBuyCoinTxCustomToBaseCustomCommission(t *testing.T) { // check sold coins sellCoinBalance := cState.Accounts.GetBalance(addr, coinToSellID) estimatedSellCoinBalance := big.NewInt(0).Set(initialBalance) - shouldGive := formula.CalculateSaleAmount(initialVolume, initialReserve, crr, big.NewInt(0).Add(toBuy, tx.CommissionInBaseCoin())) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + shouldGive := formula.CalculateSaleAmount(initialVolume, initialReserve, crr, big.NewInt(0).Add(toBuy, commissionInBaseCoin)) estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, shouldGive) if sellCoinBalance.Cmp(estimatedSellCoinBalance) != 0 { - t.Fatalf("Sell coin balance is not correct. Expected %s, got %s", estimatedSellCoinBalance.String(), sellCoinBalance.String()) + t.Errorf("Sell coin balance is not correct. Expected %s, got %s", estimatedSellCoinBalance.String(), sellCoinBalance.String()) } // check reserve and supply @@ -931,23 +1064,26 @@ func TestBuyCoinTxCustomToBaseCustomCommission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve) estimatedReserve.Sub(estimatedReserve, toBuy) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve. Expected %s, got %s", estimatedReserve.String(), coinData.Reserve().String()) } estimatedSupply := big.NewInt(0).Set(initialVolume) - estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(initialVolume, initialReserve, crr, big.NewInt(0).Add(toBuy, tx.CommissionInBaseCoin()))) - //estimatedSupply.Sub(estimatedSupply, commission) + estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(initialVolume, initialReserve, crr, big.NewInt(0).Add(toBuy, commissionInBaseCoin))) + // estimatedSupply.Sub(estimatedSupply, commission) if coinData.Volume().Cmp(estimatedSupply) != 0 { t.Fatalf("Wrong coin supply. Expected %s, got %s", estimatedSupply.String(), coinData.Volume().String()) } } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxCustomToCustomCustom1Commission(t *testing.T) { + t.Parallel() // sell_coin: TEST1 // buy_coin: TEST2 // gas_coin: TEST1 @@ -992,12 +1128,17 @@ func TestBuyCoinTxCustomToCustomCustom1Commission(t *testing.T) { sellCoinBalance := cState.Accounts.GetBalance(addr, coinToSellID) estimatedSellCoinBalance := big.NewInt(0).Set(initialBalance) toSellBaseCoin := formula.CalculatePurchaseAmount(initialVolume2, initialReserve2, crr2, toBuy) - toSell := formula.CalculateSaleAmount(initialVolume1, initialReserve1, crr1, toSellBaseCoin) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + toSell := formula.CalculateSaleAmount(initialVolume1, initialReserve1, crr1, big.NewInt(0).Add(toSellBaseCoin, commissionInBaseCoin)) estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, toSell) - commission := formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume1, toSell), big.NewInt(0).Sub(initialReserve1, toSellBaseCoin), crr1, tx.CommissionInBaseCoin()) - estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, commission) + // commission := formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume1, toSell), big.NewInt(0).Sub(initialReserve1, toSellBaseCoin), crr1, tx.CommissionInBaseCoin()) + // estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, commission) if sellCoinBalance.Cmp(estimatedSellCoinBalance) != 0 { - t.Fatalf("Sell coin balance is not correct. Expected %s, got %s", estimatedSellCoinBalance.String(), sellCoinBalance.String()) + t.Errorf("Sell coin balance is not correct. Expected %s, got %s", estimatedSellCoinBalance.String(), sellCoinBalance.String()) } // check reserve and supply @@ -1006,14 +1147,13 @@ func TestBuyCoinTxCustomToCustomCustom1Commission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve1) estimatedReserve.Sub(estimatedReserve, formula.CalculatePurchaseAmount(initialVolume2, initialReserve2, crr2, toBuy)) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve. Expected %s, got %s", estimatedReserve.String(), coinData.Reserve().String()) } estimatedSupply := big.NewInt(0).Set(initialVolume1) - estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(initialVolume1, initialReserve1, crr1, formula.CalculatePurchaseAmount(initialVolume2, initialReserve2, crr2, toBuy))) - estimatedSupply.Sub(estimatedSupply, commission) + estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(initialVolume1, initialReserve1, crr1, big.NewInt(0).Add(formula.CalculatePurchaseAmount(initialVolume2, initialReserve2, crr2, toBuy), commissionInBaseCoin))) if coinData.Volume().Cmp(estimatedSupply) != 0 { t.Fatalf("Wrong coin supply") } @@ -1035,10 +1175,13 @@ func TestBuyCoinTxCustomToCustomCustom1Commission(t *testing.T) { } } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxCustomToCustomCustom2Commission(t *testing.T) { + t.Parallel() // sell_coin: TEST1 // buy_coin: TEST2 // gas_coin: TEST2 @@ -1082,7 +1225,12 @@ func TestBuyCoinTxCustomToCustomCustom2Commission(t *testing.T) { buyCoinBalance := cState.Accounts.GetBalance(addr, coinToBuyID) buyCoinBalance.Sub(buyCoinBalance, initialGasBalance) toReserve := formula.CalculatePurchaseAmount(initialVolume2, initialReserve2, crr2, toBuy) - commission := formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume2, toBuy), big.NewInt(0).Add(initialReserve2, toReserve), crr2, tx.CommissionInBaseCoin()) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + commission := formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume2, toBuy), big.NewInt(0).Add(initialReserve2, toReserve), crr2, commissionInBaseCoin) buyCoinBalance.Add(buyCoinBalance, commission) if buyCoinBalance.Cmp(toBuy) != 0 { t.Fatalf("Buy coin balance is not correct. Expected %s, got %s", toBuy.String(), buyCoinBalance.String()) @@ -1120,7 +1268,7 @@ func TestBuyCoinTxCustomToCustomCustom2Commission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve2) estimatedReserve.Add(estimatedReserve, formula.CalculatePurchaseAmount(initialVolume2, initialReserve2, crr2, toBuy)) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve. Expected %s, got %s", estimatedReserve.String(), coinData.Reserve().String()) } @@ -1133,10 +1281,13 @@ func TestBuyCoinTxCustomToCustomCustom2Commission(t *testing.T) { } } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxToCoinSupplyOverflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID, sellCoinID := createTestCoin(cState), types.GetBaseCoinID() @@ -1158,16 +1309,19 @@ func TestBuyCoinTxToCoinSupplyOverflow(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinSupplyOverflow, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxToMaximumValueToSellReached(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID, sellCoinID := createTestCoin(cState), types.GetBaseCoinID() valueToBuy := big.NewInt(2e18) - //cState.Accounts.AddBalance(addr, sellCoinID, valueToBuy) + // cState.Accounts.AddBalance(addr, sellCoinID, valueToBuy) cState.Coins.AddVolume(sellCoinID, valueToBuy) data := BuyCoinData{ @@ -1204,7 +1358,7 @@ func TestBuyCoinTxToMaximumValueToSellReached(t *testing.T) { response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) if response.Code != code.MaximumValueToSellReached { - t.Fatalf("Response code is not %d. Error %s", code.MaximumValueToSellReached, response.Log) + t.Fatalf("Response code is not %d. Error %d %s", code.MaximumValueToSellReached, response.Code, response.Log) } cState.Accounts.AddBalance(addr, coinToBuyID, helpers.BipToPip(big.NewInt(100000))) @@ -1272,7 +1426,9 @@ func TestBuyCoinTxToMaximumValueToSellReached(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.MaximumValueToSellReached, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.MaximumValueToSell = big.NewInt(1000360064812986923) encodedData, err = rlp.EncodeToBytes(data) @@ -1293,13 +1449,16 @@ func TestBuyCoinTxToMaximumValueToSellReached(t *testing.T) { response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) if response.Code != code.MaximumValueToSellReached { - t.Fatalf("Response code is not %d. Error %s", code.MaximumValueToSellReached, response.Log) + t.Fatalf("Response code is not %d. Error %d %s", code.MaximumValueToSellReached, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestBuyCoinTxToCoinReserveNotSufficient(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID := createTestCoin(cState) @@ -1335,7 +1494,9 @@ func TestBuyCoinTxToCoinReserveNotSufficient(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinReserveNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // gas coin == coin to buy @@ -1352,11 +1513,13 @@ func TestBuyCoinTxToCoinReserveNotSufficient(t *testing.T) { } response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveNotSufficient { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveNotSufficient, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %d %s", code.CommissionCoinNotSufficient, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // gas coin == coin to sell @@ -1371,11 +1534,13 @@ func TestBuyCoinTxToCoinReserveNotSufficient(t *testing.T) { } response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveNotSufficient { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveNotSufficient, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %d %s", code.CommissionCoinNotSufficient, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // gas coin == coin to buy // sell coin == base coin @@ -1392,11 +1557,13 @@ func TestBuyCoinTxToCoinReserveNotSufficient(t *testing.T) { } response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveNotSufficient { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveNotSufficient, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %d %s", code.CommissionCoinNotSufficient, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func createBuyCoinTx(sellCoin, buyCoin, gasCoin types.CoinID, valueToBuy *big.Int, nonce uint64) *Transaction { diff --git a/core/transaction/buy_swap_pool.go b/core/transaction/buy_swap_pool.go new file mode 100644 index 000000000..d7309773e --- /dev/null +++ b/core/transaction/buy_swap_pool.go @@ -0,0 +1,307 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/state/swap" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" + "strings" +) + +type BuySwapPoolData struct { + Coins []types.CoinID + ValueToBuy *big.Int + MaximumValueToSell *big.Int +} + +func reverseCoinIds(a []types.CoinID) { + for i := len(a)/2 - 1; i >= 0; i-- { + opp := len(a) - 1 - i + a[i], a[opp] = a[opp], a[i] + } +} + +func reversePools(a []string) { + for i := len(a)/2 - 1; i >= 0; i-- { + opp := len(a) - 1 - i + a[i], a[opp] = a[opp], a[i] + } +} + +func (data BuySwapPoolData) Gas() int { + return gasBuySwapPool +} +func (data BuySwapPoolData) TxType() TxType { + return TypeBuySwapPool +} + +func (data BuySwapPoolData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if len(data.Coins) < 2 { + return &Response{ + Code: code.DecodeError, + Log: "Incorrect tx data", + Info: EncodeError(code.NewDecodeError()), + } + } + if len(data.Coins) > 5 { + return &Response{ + Code: code.TooLongSwapRoute, + Log: "maximum allowed length of the exchange chain is 5", + Info: EncodeError(code.NewCustomCode(code.TooLongSwapRoute)), + } + } + coin0 := data.Coins[0] + for _, coin1 := range data.Coins[1:] { + if coin0 == coin1 { + return &Response{ + Code: code.CrossConvert, + Log: "\"From\" coin equals to \"to\" coin", + Info: EncodeError(code.NewCrossConvert( + coin0.String(), "", + coin1.String(), "")), + } + } + if !context.Swap().SwapPoolExist(coin0, coin1) { + return &Response{ + Code: code.PairNotExists, + Log: fmt.Sprint("swap pool not exists"), + Info: EncodeError(code.NewPairNotExists(coin0.String(), coin1.String())), + } + } + coin0 = coin1 + } + return nil +} + +func (data BuySwapPoolData) String() string { + return fmt.Sprintf("SWAP POOL BUY") +} + +func (data BuySwapPoolData) CommissionData(price *commission.Price) *big.Int { + return new(big.Int).Add(price.BuyPoolBase, new(big.Int).Mul(price.BuyPoolDelta, big.NewInt(int64(len(data.Coins))-2))) +} + +func (data BuySwapPoolData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + reverseCoinIds(data.Coins) + + var calculatedAmountToSell *big.Int + resultCoin := data.Coins[len(data.Coins)-1] + { + coinToBuy := data.Coins[0] + coinToBuyModel := checkState.Coins().GetCoin(coinToBuy) + valueToBuy := big.NewInt(0).Set(data.ValueToBuy) + valueToSell := maxCoinSupply + for _, coinToSell := range data.Coins[1:] { + swapper := checkState.Swap().GetSwapper(coinToSell, coinToBuy) + if isGasCommissionFromPoolSwap { + if tx.GasCoin == coinToSell && coinToBuy.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + if tx.GasCoin == coinToSell && coinToBuy.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commissionInBaseCoin, commission) + } + } + + if coinToSell == resultCoin { + valueToSell = data.MaximumValueToSell + } + + coinToSellModel := checkState.Coins().GetCoin(coinToSell) + errResp = CheckSwap(swapper, coinToSellModel, coinToBuyModel, valueToSell, valueToBuy, true) + if errResp != nil { + return *errResp + } + + valueToBuyCalc := swapper.CalculateSellForBuy(valueToBuy) + if valueToBuyCalc == nil { + reserve0, reserve1 := swapper.Reserves() + return Response{ // todo + Code: code.SwapPoolUnknown, + Log: fmt.Sprintf("swap pool has reserves %s %s and %d %s, you wanted buy %s %s", reserve0, coinToSellModel.GetFullSymbol(), reserve1, coinToBuyModel.GetFullSymbol(), valueToBuy, coinToSellModel.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientLiquidity(coinToSellModel.ID().String(), valueToBuyCalc.String(), coinToBuyModel.ID().String(), valueToBuy.String(), reserve0.String(), reserve1.String())), + } + } + valueToBuy = valueToBuyCalc + coinToBuyModel = coinToSellModel + coinToBuy = coinToSell + } + calculatedAmountToSell = valueToBuy + } + + coinToSell := resultCoin + amount0 := new(big.Int).Set(calculatedAmountToSell) + if tx.GasCoin == coinToSell { + amount0.Add(amount0, commission) + } + if checkState.Accounts().GetBalance(sender, coinToSell).Cmp(amount0) == -1 { + symbol := checkState.Coins().GetCoin(coinToSell).GetFullSymbol() + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), amount0.String(), symbol), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), amount0.String(), symbol, coinToSell.String())), + } + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + coinToBuy := data.Coins[0] + resultCoin := data.Coins[len(data.Coins)-1] + valueToBuy := data.ValueToBuy + + var poolIDs []string + + for i, coinToSell := range data.Coins[1:] { + + amountIn, amountOut, poolID := deliverState.Swap.PairBuy(coinToSell, coinToBuy, maxCoinSupply, valueToBuy) + + poolIDs = append(poolIDs, fmt.Sprintf("%d:%d-%s:%d-%s", poolID, coinToSell, amountIn.String(), coinToBuy, amountOut.String())) + + if i == 0 { + deliverState.Accounts.AddBalance(sender, coinToBuy, amountOut) + } + + valueToBuy = amountIn + coinToBuy = coinToSell + + if coinToSell == resultCoin { + deliverState.Accounts.SubBalance(sender, coinToSell, amountIn) + } + } + reversePools(poolIDs) + amountIn := valueToBuy + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_to_buy"), Value: []byte(data.Coins[0].String()), Index: true}, + {Key: []byte("tx.coin_to_sell"), Value: []byte(resultCoin.String()), Index: true}, + {Key: []byte("tx.return"), Value: []byte(amountIn.String())}, + {Key: []byte("tx.pools"), Value: []byte(strings.Join(poolIDs, ","))}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} + +func CheckSwap(rSwap swap.EditableChecker, coinIn CalculateCoin, coinOut CalculateCoin, valueIn *big.Int, valueOut *big.Int, isBuy bool) *Response { + if isBuy { + calculatedAmountToSell := rSwap.CalculateSellForBuy(valueOut) + if calculatedAmountToSell == nil { + reserve0, reserve1 := rSwap.Reserves() + symbolIn := coinIn.GetFullSymbol() + symbolOut := coinOut.GetFullSymbol() + return &Response{ + Code: code.InsufficientLiquidity, + Log: fmt.Sprintf("You wanted to buy %s %s, but swap pool has reserve %s %s", valueOut, symbolOut, reserve0.String(), symbolIn), + Info: EncodeError(code.NewInsufficientLiquidity(coinIn.ID().String(), valueIn.String(), coinOut.ID().String(), valueOut.String(), reserve0.String(), reserve1.String())), + } + } + if calculatedAmountToSell.Cmp(valueIn) == 1 { + return &Response{ + Code: code.MaximumValueToSellReached, + Log: fmt.Sprintf( + "You wanted to sell maximum %s %s, but currently you need to spend %s %s to complete tx", + valueIn.String(), coinIn.GetFullSymbol(), calculatedAmountToSell.String(), coinOut.GetFullSymbol()), + Info: EncodeError(code.NewMaximumValueToSellReached(valueIn.String(), calculatedAmountToSell.String(), coinIn.GetFullSymbol(), coinIn.ID().String())), + } + } + valueIn = calculatedAmountToSell + } else { + calculatedAmountToBuy := rSwap.CalculateBuyForSell(valueIn) + if calculatedAmountToBuy == nil { + reserve0, reserve1 := rSwap.Reserves() + symbolIn := coinIn.GetFullSymbol() + symbolOut := coinOut.GetFullSymbol() + return &Response{ + Code: code.InsufficientLiquidity, + Log: fmt.Sprintf("You wanted to sell %s %s and get more than the swap pool has a reserve in %s", valueIn, symbolIn, symbolOut), + Info: EncodeError(code.NewInsufficientLiquidity(coinIn.ID().String(), valueIn.String(), coinOut.ID().String(), valueOut.String(), reserve0.String(), reserve1.String())), + } + } + if calculatedAmountToBuy.Cmp(valueOut) == -1 { + symbolOut := coinOut.GetFullSymbol() + return &Response{ + Code: code.MinimumValueToBuyReached, + Log: fmt.Sprintf( + "You wanted to buy minimum %s %s, but currently you buy only %s %s", + valueIn.String(), symbolOut, calculatedAmountToBuy.String(), symbolOut), + Info: EncodeError(code.NewMaximumValueToSellReached(valueIn.String(), calculatedAmountToBuy.String(), coinIn.GetFullSymbol(), coinIn.ID().String())), + } + } + valueOut = calculatedAmountToBuy + } + if err := rSwap.CheckSwap(valueIn, valueOut); err != nil { + if err == swap.ErrorK { + panic(swap.ErrorK) + } + if err == swap.ErrorInsufficientLiquidity { + reserve0, reserve1 := rSwap.Reserves() + symbolIn := coinIn.GetFullSymbol() + symbolOut := coinOut.GetFullSymbol() + return &Response{ + Code: code.InsufficientLiquidity, + Log: fmt.Sprintf("You wanted to exchange %s %s for %s %s, but pool reserve %s equal %s and reserve %s equal %s", valueIn, symbolIn, valueOut, symbolOut, reserve0.String(), symbolIn, reserve1.String(), symbolOut), + Info: EncodeError(code.NewInsufficientLiquidity(coinIn.ID().String(), valueIn.String(), coinOut.ID().String(), valueOut.String(), reserve0.String(), reserve1.String())), + } + } + if err == swap.ErrorInsufficientOutputAmount { + return &Response{ + Code: code.InsufficientOutputAmount, + Log: fmt.Sprintf("Enter a positive number of coins to exchange"), + Info: EncodeError(code.NewInsufficientOutputAmount(coinIn.ID().String(), valueIn.String(), coinOut.ID().String(), valueOut.String())), + } + } + return &Response{ + Code: code.SwapPoolUnknown, + Log: err.Error(), + } + } + return nil +} diff --git a/core/transaction/buy_swap_pool_test.go b/core/transaction/buy_swap_pool_test.go new file mode 100644 index 000000000..1bf1d9d24 --- /dev/null +++ b/core/transaction/buy_swap_pool_test.go @@ -0,0 +1,929 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" + "testing" +) + +func TestBuySwapPoolTx_0(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(100)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MaximumValueToSell: big.NewInt(10), + ValueToBuy: big.NewInt(99), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestBuySwapPoolTx_1(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(10)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(10)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin1, coin}, + MaximumValueToSell: big.NewInt(10), + ValueToBuy: big.NewInt(9), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MaximumValueToSell: big.NewInt(10), + ValueToBuy: big.NewInt(9), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 3, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestBuySwapPoolTx_2(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)), big.NewInt(0)), + Coin1: coin1, + Volume1: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)), big.NewInt(0)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MaximumValueToSell: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18)), big.NewInt(0)), + ValueToBuy: big.NewInt(996006981039903216), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestBuySwapPoolTx_3(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: new(big.Int).Add(new(big.Int).Mul(big.NewInt(5), big.NewInt(1e18)), big.NewInt(0)), + Coin1: coin1, + Volume1: new(big.Int).Add(new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)), big.NewInt(0)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MaximumValueToSell: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18)), big.NewInt(0)), + ValueToBuy: big.NewInt(1662497915624478906), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestBuySwapPoolTx_4(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: new(big.Int).Add(new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)), big.NewInt(0)), + Coin1: coin1, + Volume1: new(big.Int).Add(new(big.Int).Mul(big.NewInt(5), big.NewInt(1e18)), big.NewInt(0)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MaximumValueToSell: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18)), big.NewInt(0)), + ValueToBuy: big.NewInt(453305446940074565), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestBuySwapPoolTx_RouteYes(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(100)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + coin2 := createNonReserveCoin(cState) + cState.Accounts.SubBalance(types.Address{}, coin2, helpers.BipToPip(big.NewInt(1000))) + cState.Accounts.AddBalance(addr, coin2, helpers.BipToPip(big.NewInt(1000))) + { + data := CreateSwapPoolData{ + Coin0: coin1, + Volume0: helpers.BipToPip(big.NewInt(1000)), + Coin1: coin2, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin, coin1, coin2}, + MaximumValueToSell: big.NewInt(10), + ValueToBuy: big.NewInt(98), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 3, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestBuySwapPoolTx_Route(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(100)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MaximumValueToSell: big.NewInt(10), + ValueToBuy: big.NewInt(99), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + + coin2 := createNonReserveCoin(cState) + cState.Accounts.SubBalance(types.Address{}, coin2, helpers.BipToPip(big.NewInt(1000))) + cState.Accounts.AddBalance(addr, coin2, helpers.BipToPip(big.NewInt(1000))) + { + data := CreateSwapPoolData{ + Coin0: coin1, + Volume0: helpers.BipToPip(big.NewInt(1000)), + Coin1: coin2, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 3, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := BuySwapPoolData{ + Coins: []types.CoinID{coin1, coin2}, + MaximumValueToSell: big.NewInt(99), + ValueToBuy: big.NewInt(98), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 4, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeBuySwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} diff --git a/core/transaction/create_coin.go b/core/transaction/create_coin.go index e483d755f..2797b98ca 100644 --- a/core/transaction/create_coin.go +++ b/core/transaction/create_coin.go @@ -1,8 +1,8 @@ package transaction import ( - "encoding/hex" "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" "math/big" "regexp" "strconv" @@ -10,9 +10,8 @@ import ( "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" "github.com/MinterTeam/minter-go-node/helpers" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" ) const maxCoinNameBytes = 64 @@ -20,6 +19,7 @@ const allowedCoinSymbols = "^[A-Z0-9]{3,10}$" var ( minCoinSupply = helpers.BipToPip(big.NewInt(1)) + minTokenSupply = big.NewInt(1) minCoinReserve = helpers.BipToPip(big.NewInt(10000)) maxCoinSupply = big.NewInt(0).Exp(big.NewInt(10), big.NewInt(15+18), nil) allowedCoinSymbolsRegexpCompile, _ = regexp.Compile(allowedCoinSymbols) @@ -34,8 +34,15 @@ type CreateCoinData struct { MaxSupply *big.Int } -func (data CreateCoinData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { - if data.InitialReserve == nil || data.InitialAmount == nil || data.MaxSupply == nil { +func (data CreateCoinData) Gas() int { + return gasCreateCoin +} +func (data CreateCoinData) TxType() TxType { + return TypeCreateCoin +} + +func (data CreateCoinData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if data.InitialAmount == nil || data.MaxSupply == nil { return &Response{ Code: code.DecodeError, Log: "Incorrect tx data", @@ -67,19 +74,11 @@ func (data CreateCoinData) BasicCheck(tx *Transaction, context *state.CheckState } } - if data.ConstantReserveRatio < 10 || data.ConstantReserveRatio > 100 { - return &Response{ - Code: code.WrongCrr, - Log: "Constant Reserve Ratio should be between 10 and 100", - Info: EncodeError(code.NewWrongCrr("10", "100", strconv.Itoa(int(data.ConstantReserveRatio)))), - } - } - if data.MaxSupply.Cmp(maxCoinSupply) == 1 { return &Response{ Code: code.WrongCoinSupply, Log: fmt.Sprintf("Max coin supply should be less than %s", maxCoinSupply), - Info: EncodeError(code.NewWrongCoinSupply(maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), minCoinSupply.String(), data.MaxSupply.String(), data.InitialAmount.String())), + Info: EncodeError(code.NewWrongCoinSupply(minCoinSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), data.InitialAmount.String())), } } @@ -87,7 +86,7 @@ func (data CreateCoinData) BasicCheck(tx *Transaction, context *state.CheckState return &Response{ Code: code.WrongCoinSupply, Log: fmt.Sprintf("Coin supply should be between %s and %s", minCoinSupply.String(), data.MaxSupply.String()), - Info: EncodeError(code.NewWrongCoinSupply(maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), minCoinSupply.String(), data.MaxSupply.String(), data.InitialAmount.String())), + Info: EncodeError(code.NewWrongCoinSupply(minCoinSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), data.InitialAmount.String())), } } @@ -95,7 +94,24 @@ func (data CreateCoinData) BasicCheck(tx *Transaction, context *state.CheckState return &Response{ Code: code.WrongCoinSupply, Log: fmt.Sprintf("Coin reserve should be greater than or equal to %s", minCoinReserve.String()), - Info: EncodeError(code.NewWrongCoinSupply(maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), minCoinSupply.String(), data.MaxSupply.String(), data.InitialAmount.String())), + Info: EncodeError(map[string]string{ + "code": strconv.Itoa(int(code.WrongCoinSupply)), + "min_initial_reserve": minCoinReserve.String(), + "current_initial_reserve": data.InitialReserve.String(), + })} + } + if data.ConstantReserveRatio < 10 || data.ConstantReserveRatio > 100 { + return &Response{ + Code: code.WrongCrr, + Log: "Constant Reserve Ratio should be between 10 and 100", + Info: EncodeError(code.NewWrongCrr("10", "100", strconv.Itoa(int(data.ConstantReserveRatio)))), + } + } + if data.InitialReserve.Cmp(minCoinReserve) == -1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Coin reserve should be greater than or equal to %s", minCoinReserve.String()), + Info: EncodeError(code.NewWrongCoinSupply(minCoinSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), data.InitialAmount.String())), } } @@ -107,22 +123,23 @@ func (data CreateCoinData) String() string { data.Symbol.String(), data.InitialReserve, data.InitialAmount, data.ConstantReserveRatio) } -func (data CreateCoinData) Gas() int64 { +func (data CreateCoinData) CommissionData(price *commission.Price) *big.Int { + createTicker := new(big.Int).Set(price.CreateTicker7to10) switch len(data.Symbol.String()) { case 3: - return 1000000000 // 1mln bips + createTicker = price.CreateTicker3 case 4: - return 100000000 // 100k bips + createTicker = price.CreateTicker4 case 5: - return 10000000 // 10k bips + createTicker = price.CreateTicker5 case 6: - return 1000000 // 1k bips + createTicker = price.CreateTicker6 } - return 100000 // 100 bips + return big.NewInt(0).Add(createTicker, price.CreateCoin) } -func (data CreateCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data CreateCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -130,28 +147,21 @@ func (data CreateCoinData) Run(tx *Transaction, context state.Interface, rewardP if checkState, isCheck = context.(*state.CheckState); !isCheck { checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if tx.GasCoin != types.GetBaseCoinID() { - coin := checkState.Coins().GetCoin(tx.GasCoin) - - errResp := CheckReserveUnderflow(coin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), commissionInBaseCoin) + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } - if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - return Response{ Code: code.InsufficientFunds, Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), @@ -159,40 +169,31 @@ func (data CreateCoinData) Run(tx *Transaction, context state.Interface, rewardP } } - if checkState.Accounts().GetBalance(sender, types.GetBaseCoinID()).Cmp(data.InitialReserve) < 0 { + totalTxCost := big.NewInt(0).Set(data.InitialReserve) + if tx.GasCoin == types.GetBaseCoinID() { + totalTxCost.Add(totalTxCost, commissionInBaseCoin) + } + if checkState.Accounts().GetBalance(sender, types.GetBaseCoinID()).Cmp(totalTxCost) == -1 { + coin := checkState.Coins().GetCoin(types.GetBaseCoinID()) return Response{ Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), data.InitialReserve.String(), types.GetBaseCoin()), - Info: EncodeError(code.NewInsufficientFunds(sender.String(), data.InitialReserve.String(), types.GetBaseCoin().String(), types.GetBaseCoinID().String())), + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalTxCost.String(), coin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), totalTxCost.String(), coin.GetFullSymbol(), coin.ID().String())), } } - - if tx.GasCoin.IsBaseCoin() { - totalTxCost := big.NewInt(0) - totalTxCost.Add(totalTxCost, data.InitialReserve) - totalTxCost.Add(totalTxCost, commission) - - if checkState.Accounts().GetBalance(sender, types.GetBaseCoinID()).Cmp(totalTxCost) < 0 { - gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalTxCost.String(), gasCoin.GetFullSymbol()), - Info: EncodeError(code.NewInsufficientFunds(sender.String(), totalTxCost.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), - } - } - } - - var coinId = checkState.App().GetNextCoinID() + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - - deliverState.Accounts.SubBalance(sender, types.GetBaseCoinID(), data.InitialReserve) deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + deliverState.Accounts.SubBalance(sender, types.GetBaseCoinID(), data.InitialReserve) + coinId := checkState.App().GetNextCoinID() deliverState.Coins.Create( coinId, data.Symbol, @@ -207,19 +208,18 @@ func (data CreateCoinData) Run(tx *Transaction, context state.Interface, rewardP deliverState.App.SetCoinsCount(coinId.Uint32()) deliverState.Accounts.AddBalance(sender, coinId, data.InitialAmount) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeCreateCoin)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String())}, - kv.Pair{Key: []byte("tx.coin_id"), Value: []byte(coinId.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String()), Index: true}, + {Key: []byte("tx.coin_id"), Value: []byte(coinId.String()), Index: true}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/create_coin_test.go b/core/transaction/create_coin_test.go index fd042ee0a..e642acd5d 100644 --- a/core/transaction/create_coin_test.go +++ b/core/transaction/create_coin_test.go @@ -14,6 +14,7 @@ import ( ) func TestCreateCoinTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -68,17 +69,19 @@ func TestCreateCoinTx(t *testing.T) { t.Fatalf("Response code is not 0. Error %s", response.Log) } - err = cState.Coins.Commit() + _, _, err = cState.Tree().Commit(cState.Coins) if err != nil { t.Fatalf("Commit coins failed. Error %s", err) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } targetBalance, _ := big.NewInt(0).SetString("989000000000000000000000", 10) balance := cState.Accounts.GetBalance(addr, coin) if balance.Cmp(targetBalance) != 0 { - t.Fatalf("Target %s balance is not correct. Expected %s, got %s", coin, targetBalance, balance) + t.Errorf("Target %s balance is not correct. Expected %s, got %s", coin, targetBalance, balance) } stateCoin := cState.Coins.GetCoinBySymbol(toCreate, 0) @@ -116,10 +119,13 @@ func TestCreateCoinTx(t *testing.T) { t.Fatalf("Target owner address is not correct. Expected %s, got %s", addr.String(), symbolInfo.OwnerAddress().String()) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinWithIncorrectName(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -176,10 +182,13 @@ func TestCreateCoinWithIncorrectName(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InvalidCoinName, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinWithInvalidSymbol(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -234,10 +243,13 @@ func TestCreateCoinWithInvalidSymbol(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InvalidCoinSymbol, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinWithExistingSymbol(t *testing.T) { + t.Parallel() cState := getState() createTestCoin(cState) @@ -293,10 +305,13 @@ func TestCreateCoinWithExistingSymbol(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinAlreadyExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinWithWrongCrr(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -349,7 +364,9 @@ func TestCreateCoinWithWrongCrr(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCrr, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.ConstantReserveRatio = uint32(101) @@ -382,10 +399,13 @@ func TestCreateCoinWithWrongCrr(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCrr, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinWithWrongCoinSupply(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -437,7 +457,9 @@ func TestCreateCoinWithWrongCoinSupply(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCoinSupply, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.InitialAmount = helpers.BipToPip(big.NewInt(1000000)) encodedData, err = rlp.EncodeToBytes(data) @@ -469,7 +491,9 @@ func TestCreateCoinWithWrongCoinSupply(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCoinSupply, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.MaxSupply = big.NewInt(0).Exp(big.NewInt(100), big.NewInt(15+18), nil) encodedData, err = rlp.EncodeToBytes(data) @@ -514,40 +538,45 @@ func TestCreateCoinWithWrongCoinSupply(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCoinSupply, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinGas(t *testing.T) { + t.Parallel() + // t.SkipNow() data := CreateCoinData{ Symbol: types.StrToCoinSymbol("ABC"), } - if data.Gas() != 1000000000 { - t.Fatal("Gas for symbol with length 3 is not correct.") + if data.CommissionData(&commissionPrice).String() != "1000000000000000000000000" { + t.Fatal("CommissionData for symbol with length 3 is not correct.") } data.Symbol = types.StrToCoinSymbol("ABCD") - if data.Gas() != 100000000 { - t.Fatal("Gas for symbol with length 4 is not correct.") + if data.CommissionData(&commissionPrice).String() != "100000000000000000000000" { + t.Fatal("CommissionData for symbol with length 4 is not correct.") } data.Symbol = types.StrToCoinSymbol("ABCDE") - if data.Gas() != 10000000 { - t.Fatal("Gas for symbol with length 5 is not correct.") + if data.CommissionData(&commissionPrice).String() != "10000000000000000000000" { + t.Fatal("CommissionData for symbol with length 5 is not correct.") } data.Symbol = types.StrToCoinSymbol("ABCDEF") - if data.Gas() != 1000000 { - t.Fatal("Gas for symbol with length 6 is not correct.") + if data.CommissionData(&commissionPrice).String() != "1000000000000000000000" { + t.Fatal("CommissionData for symbol with length 6 is not correct.") } data.Symbol = types.StrToCoinSymbol("ABCDEFG") - if data.Gas() != 100000 { - t.Fatal("Gas for symbol with length 7 is not correct.") + if data.CommissionData(&commissionPrice).String() != "100000000000000000000" { + t.Fatal("CommissionData for symbol with length 7 is not correct.") } } func TestCreateCoinWithInsufficientFundsForGas(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -596,10 +625,13 @@ func TestCreateCoinWithInsufficientFundsForGas(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -652,14 +684,17 @@ func TestCreateCoinTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinToInsufficientFundsForGasCoin(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -707,13 +742,16 @@ func TestCreateCoinToInsufficientFundsForGasCoin(t *testing.T) { response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) if response.Code != code.InsufficientFunds { - t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) + t.Fatalf("Response code is not %d. Error %d %s", code.InsufficientFunds, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinToInsufficientFundsForInitialReserve(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -764,10 +802,13 @@ func TestCreateCoinToInsufficientFundsForInitialReserve(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateCoinToSameSymbolInOneBlock(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -823,7 +864,9 @@ func TestCreateCoinToSameSymbolInOneBlock(t *testing.T) { t.Fatalf("Response code is not success. Error %s", response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } if err := tx.Sign(privateKey2); err != nil { t.Fatal(err) @@ -839,5 +882,7 @@ func TestCreateCoinToSameSymbolInOneBlock(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinAlreadyExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/create_multisig.go b/core/transaction/create_multisig.go index 4b8da6b5c..6720df40a 100644 --- a/core/transaction/create_multisig.go +++ b/core/transaction/create_multisig.go @@ -3,16 +3,15 @@ package transaction import ( "encoding/hex" "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" "math/big" "strconv" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/state/accounts" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" ) type CreateMultisigData struct { @@ -21,7 +20,15 @@ type CreateMultisigData struct { Addresses []types.Address } -func (data CreateMultisigData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data CreateMultisigData) Gas() int { + return gasCreateMultisig +} + +func (data CreateMultisigData) TxType() TxType { + return TypeCreateMultisig +} + +func (data CreateMultisigData) basicCheck(tx *Transaction, context *state.CheckState) *Response { lenWeights := len(data.Weights) if lenWeights > 32 { return &Response{ @@ -69,11 +76,11 @@ func (data CreateMultisigData) String() string { return "CREATE MULTISIG" } -func (data CreateMultisigData) Gas() int64 { - return commissions.CreateMultisig +func (data CreateMultisigData) CommissionData(price *commission.Price) *big.Int { + return price.CreateMultisig } -func (data CreateMultisigData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data CreateMultisigData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -82,23 +89,17 @@ func (data CreateMultisigData) Run(tx *Transaction, context state.Interface, rew checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -118,29 +119,30 @@ func (data CreateMultisigData) Run(tx *Transaction, context state.Interface, rew Info: EncodeError(code.NewMultisigExists(msigAddress.String())), } } - + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubVolume(tx.GasCoin, commission) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) deliverState.Accounts.SetNonce(sender, tx.Nonce) deliverState.Accounts.CreateMultisig(data.Weights, data.Addresses, data.Threshold, msigAddress) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeCreateMultisig)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.created_multisig"), Value: []byte(hex.EncodeToString(msigAddress[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.created_multisig"), Value: []byte(hex.EncodeToString(msigAddress[:])), Index: true}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/create_multisig_test.go b/core/transaction/create_multisig_test.go index 645d26866..4955ff38f 100644 --- a/core/transaction/create_multisig_test.go +++ b/core/transaction/create_multisig_test.go @@ -16,6 +16,7 @@ import ( ) func TestCreateMultisigTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -107,10 +108,13 @@ func TestCreateMultisigTx(t *testing.T) { t.Fatalf("Threshold is not correct") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateMultisigFromExistingAccountTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -210,10 +214,13 @@ func TestCreateMultisigFromExistingAccountTx(t *testing.T) { t.Fatalf("Msig balance was not persisted") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateExistingMultisigTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -268,10 +275,13 @@ func TestCreateExistingMultisigTx(t *testing.T) { t.Fatalf("Response code is not %d. Got %d", code.MultisigExists, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateMultisigOwnersTxToNonExistAddress(t *testing.T) { + t.Parallel() cState := getState() addr := types.Address{0} @@ -309,15 +319,18 @@ func TestCreateMultisigOwnersTxToNonExistAddress(t *testing.T) { t.Fatal(err) } - response := data.BasicCheck(&tx, state.NewCheckState(cState)) + response := data.basicCheck(&tx, state.NewCheckState(cState)) if response.Code != code.MultisigNotExists { t.Fatalf("Response code is not %d. Error %s", code.MultisigNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateMultisigOwnersTxToTooLargeOwnersList(t *testing.T) { + t.Parallel() cState := getState() privateKey1, _ := crypto.GenerateKey() @@ -369,10 +382,13 @@ func TestCreateMultisigOwnersTxToTooLargeOwnersList(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.TooLargeOwnersList, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateMultisigOwnersTxIncorrectWeights(t *testing.T) { + t.Parallel() cState := getState() privateKey1, _ := crypto.GenerateKey() @@ -417,7 +433,9 @@ func TestCreateMultisigOwnersTxIncorrectWeights(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.DifferentCountAddressesAndWeights, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.Weights = []uint32{1, 2, 1024} encodedData, err = rlp.EncodeToBytes(data) @@ -440,10 +458,13 @@ func TestCreateMultisigOwnersTxIncorrectWeights(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.IncorrectWeights, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateMultisigOwnersTxToAddressDuplication(t *testing.T) { + t.Parallel() cState := getState() privateKey1, _ := crypto.GenerateKey() @@ -486,10 +507,13 @@ func TestCreateMultisigOwnersTxToAddressDuplication(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.DuplicatedAddresses, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateMultisigOwnersTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() privateKey1, _ := crypto.GenerateKey() @@ -534,10 +558,13 @@ func TestCreateMultisigOwnersTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestCreateMultisigTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey1, _ := crypto.GenerateKey() @@ -583,9 +610,11 @@ func TestCreateMultisigTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s, info %s", code.CommissionCoinNotSufficient, response.Log, response.Info) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/create_swap_pool.go b/core/transaction/create_swap_pool.go new file mode 100644 index 000000000..4d7dc1314 --- /dev/null +++ b/core/transaction/create_swap_pool.go @@ -0,0 +1,196 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/state/swap" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +type CreateSwapPoolData struct { + Coin0 types.CoinID + Coin1 types.CoinID + Volume0 *big.Int + Volume1 *big.Int +} + +func (data CreateSwapPoolData) Gas() int { + return gasCreateSwapPool +} +func (data CreateSwapPoolData) TxType() TxType { + return TypeCreateSwapPool +} + +func (data CreateSwapPoolData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if data.Coin1 == data.Coin0 { + return &Response{ + Code: code.CrossConvert, + Log: "\"From\" coin equals to \"to\" coin", + Info: EncodeError(code.NewCrossConvert( + data.Coin0.String(), + data.Coin1.String(), "", "")), + } + } + + if context.Swap().SwapPoolExist(data.Coin0, data.Coin1) { + return &Response{ + Code: code.PairAlreadyExists, + Log: "swap pool already exist", + Info: EncodeError(code.NewPairAlreadyExists( + data.Coin0.String(), + data.Coin1.String())), + } + } + + coin0 := context.Coins().GetCoin(data.Coin0) + if coin0 == nil { + return &Response{ + Code: code.CoinNotExists, + Log: "Coin not exists", + Info: EncodeError(code.NewCoinNotExists("", data.Coin0.String())), + } + } + + coin1 := context.Coins().GetCoin(data.Coin1) + if coin1 == nil { + return &Response{ + Code: code.CoinNotExists, + Log: "Coin not exists", + Info: EncodeError(code.NewCoinNotExists("", data.Coin1.String())), + } + } + + return nil +} + +func (data CreateSwapPoolData) String() string { + return fmt.Sprintf("CREATE SWAP POOL") +} + +func (data CreateSwapPoolData) CommissionData(price *commission.Price) *big.Int { + return price.CreateSwapPool +} + +func (data CreateSwapPoolData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if err := checkState.Swap().GetSwapper(data.Coin0, data.Coin1).CheckCreate(data.Volume0, data.Volume1); err != nil { + if err == swap.ErrorInsufficientLiquidityMinted { + return Response{ + Code: code.InsufficientLiquidityMinted, + Log: fmt.Sprintf("You wanted to add less than minimum liquidity, you should add %s %s and %s or more %s", + "10", checkState.Coins().GetCoin(data.Coin0).GetFullSymbol(), "10", checkState.Coins().GetCoin(data.Coin1).GetFullSymbol()), + Info: EncodeError(code.NewInsufficientLiquidityMinted(data.Coin0.String(), "10", data.Coin1.String(), "10")), + } + } + } + { + amount0 := new(big.Int).Set(data.Volume0) + if tx.GasCoin == data.Coin0 { + amount0.Add(amount0, commission) + } + if checkState.Accounts().GetBalance(sender, data.Coin0).Cmp(amount0) == -1 { + symbol := checkState.Coins().GetCoin(data.Coin0).GetFullSymbol() + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), amount0.String(), symbol), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), amount0.String(), symbol, data.Coin0.String())), + } + } + } + + { + totalAmount1 := new(big.Int).Set(data.Volume1) + if tx.GasCoin == data.Coin1 { + totalAmount1.Add(totalAmount1, commission) + } + if checkState.Accounts().GetBalance(sender, data.Coin1).Cmp(totalAmount1) == -1 { + symbol := checkState.Coins().GetCoin(data.Coin1).GetFullSymbol() + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), totalAmount1.String(), symbol), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), totalAmount1.String(), symbol, data.Coin1.String())), + } + } + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + amount0, amount1, liquidity, id := deliverState.Swap.PairCreate(data.Coin0, data.Coin1, data.Volume0, data.Volume1) + + deliverState.Accounts.SubBalance(sender, data.Coin0, amount0) + deliverState.Accounts.SubBalance(sender, data.Coin1, amount1) + + coins := liquidityCoinName(data.Coin0, data.Coin1) + coinID := checkState.App().GetNextCoinID() + + liquidityCoinSymbol := LiquidityCoinSymbol(id) + deliverState.Coins.CreateToken(coinID, liquidityCoinSymbol, "Swap Pool "+coins, true, true, big.NewInt(0).Set(liquidity), maxCoinSupply, nil) + deliverState.Accounts.AddBalance(sender, coinID, liquidity.Sub(liquidity, swap.Bound)) + deliverState.Accounts.AddBalance(types.Address{}, coinID, swap.Bound) + + deliverState.App.SetCoinsCount(coinID.Uint32()) + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.volume1"), Value: []byte(data.Volume1.String())}, + {Key: []byte("tx.liquidity"), Value: []byte(liquidity.String())}, + {Key: []byte("tx.pool_token"), Value: []byte(liquidityCoinSymbol.String()), Index: true}, + {Key: []byte("tx.pool_token_id"), Value: []byte(coinID.String()), Index: true}, + {Key: []byte("tx.pair_ids"), Value: []byte(coins), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} + +func LiquidityCoinSymbol(id uint32) types.CoinSymbol { + return types.StrToCoinSymbol(fmt.Sprintf("P-%d", id)) +} diff --git a/core/transaction/create_token.go b/core/transaction/create_token.go new file mode 100644 index 000000000..ae936c5cf --- /dev/null +++ b/core/transaction/create_token.go @@ -0,0 +1,165 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "math/big" + "strconv" + + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" +) + +type CreateTokenData struct { + Name string + Symbol types.CoinSymbol + InitialAmount *big.Int + MaxSupply *big.Int + Mintable bool + Burnable bool +} + +func (data CreateTokenData) Gas() int { + return gasCreateToken +} +func (data CreateTokenData) TxType() TxType { + return TypeCreateToken +} + +func (data CreateTokenData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if len(data.Name) > maxCoinNameBytes { + return &Response{ + Code: code.InvalidCoinName, + Log: fmt.Sprintf("Coin name is invalid. Allowed up to %d bytes.", maxCoinNameBytes), + Info: EncodeError(code.NewInvalidCoinName(strconv.Itoa(maxCoinNameBytes), strconv.Itoa(len(data.Name)))), + } + } + + if match := allowedCoinSymbolsRegexpCompile.MatchString(data.Symbol.String()); !match { + return &Response{ + Code: code.InvalidCoinSymbol, + Log: fmt.Sprintf("Invalid coin symbol. Should be %s", allowedCoinSymbols), + Info: EncodeError(code.NewInvalidCoinSymbol(allowedCoinSymbols, data.Symbol.String())), + } + } + + if context.Coins().ExistsBySymbol(data.Symbol) { + return &Response{ + Code: code.CoinAlreadyExists, + Log: "Coin already exists", + Info: EncodeError(code.NewCoinAlreadyExists(types.StrToCoinSymbol(data.Symbol.String()).String(), context.Coins().GetCoinBySymbol(data.Symbol, 0).ID().String())), + } + } + + if data.InitialAmount.Cmp(minTokenSupply) == -1 || data.InitialAmount.Cmp(data.MaxSupply) == 1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Coin amount should be between %s and %s", minTokenSupply.String(), data.MaxSupply.String()), + Info: EncodeError(code.NewWrongCoinSupply(minTokenSupply.String(), minTokenSupply.String(), data.MaxSupply.String(), "", "", data.InitialAmount.String())), + } + } + + if data.MaxSupply.Cmp(maxCoinSupply) == 1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Max coin supply should be less %s", maxCoinSupply.String()), + Info: EncodeError(code.NewWrongCoinSupply(minTokenSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), "", "", data.InitialAmount.String())), + } + } + + return nil +} + +func (data CreateTokenData) String() string { + return fmt.Sprintf("CREATE TOKEN symbol:%s emission:%s", + data.Symbol.String(), data.MaxSupply) +} + +func (data CreateTokenData) CommissionData(price *commission.Price) *big.Int { + createTicker := new(big.Int).Set(price.CreateTicker7to10) + switch len(data.Symbol.String()) { + case 3: + createTicker = price.CreateTicker3 + case 4: + createTicker = price.CreateTicker4 + case 5: + createTicker = price.CreateTicker5 + case 6: + createTicker = price.CreateTicker6 + } + + return big.NewInt(0).Add(createTicker, price.CreateToken) +} + +func (data CreateTokenData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + rewardPool.Add(rewardPool, commissionInBaseCoin) + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + + coinId := checkState.App().GetNextCoinID() + deliverState.Coins.CreateToken( + coinId, + data.Symbol, + data.Name, + data.Mintable, + data.Burnable, + data.InitialAmount, + data.MaxSupply, + &sender, + ) + + deliverState.App.SetCoinsCount(coinId.Uint32()) + deliverState.Accounts.AddBalance(sender, coinId, data.InitialAmount) + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String()), Index: true}, + {Key: []byte("tx.coin_id"), Value: []byte(coinId.String()), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/create_token_test.go b/core/transaction/create_token_test.go new file mode 100644 index 000000000..71b6883fb --- /dev/null +++ b/core/transaction/create_token_test.go @@ -0,0 +1,190 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" + "testing" +) + +func TestCreateTokenData_aaa(t *testing.T) { + t.Parallel() + cState := getState() + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + coin := types.GetBaseCoinID() + + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) + + toCreate := types.StrToCoinSymbol("TOKEN1") + amount := helpers.BipToPip(big.NewInt(100)) + name := "My Test Coin" + + maxSupply := big.NewInt(0).Mul(amount, big.NewInt(10)) + data := CreateTokenData{ + Name: name, + Symbol: toCreate, + InitialAmount: amount, + MaxSupply: maxSupply, + Mintable: true, + Burnable: false, + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeCreateToken, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error %s", response.Log) + } + + _, _, err = cState.Tree().Commit(cState.Coins) + if err != nil { + t.Fatalf("Commit coins failed. Error %s", err) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + + targetBalance, _ := big.NewInt(0).SetString("999000000000000000000000", 10) + balance := cState.Accounts.GetBalance(addr, coin) + if balance.Cmp(targetBalance) != 0 { + t.Errorf("Target %s balance is not correct. Expected %s, got %s", coin, targetBalance, balance) + } + + stateCoin := cState.Coins.GetCoinBySymbol(toCreate, 0) + + if stateCoin == nil { + t.Fatalf("Coin %s not found in state", toCreate) + } + + if stateCoin.MaxSupply().Cmp(maxSupply) != 0 { + t.Fatalf("MaxSupply in state is not correct. Expected %s, got %s", maxSupply, stateCoin.MaxSupply()) + } + + if stateCoin.Volume().Cmp(amount) != 0 { + t.Fatalf("Volume in state is not correct. Expected %s, got %s", amount, stateCoin.Volume()) + } + + if stateCoin.Name() != name { + t.Fatalf("Name in state is not correct. Expected %s, got %s", name, stateCoin.Name()) + } + + if stateCoin.Version() != 0 { + t.Fatalf("Version in state is not correct. Expected %d, got %d", 0, stateCoin.Version()) + } + + if stateCoin.IsBurnable() { + t.Errorf("IsBurnable in state is not correct. Expected %t, got %t", false, stateCoin.IsBurnable()) + } + + if !stateCoin.IsMintable() { + t.Errorf("IsMintable in state is not correct. Expected %t, got %t", true, stateCoin.IsMintable()) + } + + symbolInfo := cState.Coins.GetSymbolInfo(toCreate) + if symbolInfo == nil { + t.Fatalf("Symbol %s info not found in state", toCreate) + } + + if *symbolInfo.OwnerAddress() != addr { + t.Fatalf("Target owner address is not correct. Expected %s, got %s", addr.String(), symbolInfo.OwnerAddress().String()) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestCreateTokenData_bbb(t *testing.T) { + t.Parallel() + cState := getState() + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + coin := types.GetBaseCoinID() + + toCreate := types.StrToCoinSymbol("TOKEN1") + amount := helpers.BipToPip(big.NewInt(100)) + name := "My Test Coin" + + maxSupply := big.NewInt(0).Mul(amount, big.NewInt(10)) + data := CreateTokenData{ + Name: name, + Symbol: toCreate, + InitialAmount: amount, + MaxSupply: maxSupply, + Mintable: true, + Burnable: true, + } + cState.Accounts.AddBalance(addr, coin, big.NewInt(1e18-1)) + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeCreateToken, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != code.InsufficientFunds { + t.Fatalf("Response code is not %d. Error %d %s", code.InsufficientFunds, response.Code, response.Log) + } + + _, _, err = cState.Tree().Commit(cState.Coins) + if err != nil { + t.Fatalf("Commit coins failed. Error %s", err) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} diff --git a/core/transaction/declare_candidacy.go b/core/transaction/declare_candidacy.go index bdbbde914..47dbca81c 100644 --- a/core/transaction/declare_candidacy.go +++ b/core/transaction/declare_candidacy.go @@ -4,12 +4,11 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/core/validators" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -24,7 +23,14 @@ type DeclareCandidacyData struct { Stake *big.Int } -func (data DeclareCandidacyData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data DeclareCandidacyData) Gas() int { + return gasDeclareCandidacy +} +func (data DeclareCandidacyData) TxType() TxType { + return TypeDeclareCandidacy +} + +func (data DeclareCandidacyData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.Stake == nil { return &Response{ Code: code.DecodeError, @@ -33,7 +39,8 @@ func (data DeclareCandidacyData) BasicCheck(tx *Transaction, context *state.Chec } } - if !context.Coins().Exists(data.Coin) { + coin := context.Coins().GetCoin(data.Coin) + if coin == nil { return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", data.Coin), @@ -41,6 +48,17 @@ func (data DeclareCandidacyData) BasicCheck(tx *Transaction, context *state.Chec } } + if !coin.BaseOrHasReserve() { + return &Response{ + Code: code.CoinHasNotReserve, + Log: "coin has no reserve", + Info: EncodeError(code.NewCoinHasNotReserve( + coin.GetFullSymbol(), + coin.ID().String(), + )), + } + } + if context.Candidates().Exists(data.PubKey) { return &Response{ Code: code.CandidateExists, @@ -73,11 +91,11 @@ func (data DeclareCandidacyData) String() string { data.Address.String(), data.PubKey.String(), data.Commission) } -func (data DeclareCandidacyData) Gas() int64 { - return commissions.DeclareCandidacyTx +func (data DeclareCandidacyData) CommissionData(price *commission.Price) *big.Int { + return price.DeclareCandidacy } -func (data DeclareCandidacyData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data DeclareCandidacyData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -86,7 +104,7 @@ func (data DeclareCandidacyData) Run(tx *Transaction, context state.Interface, r checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } @@ -101,22 +119,16 @@ func (data DeclareCandidacyData) Run(tx *Transaction, context state.Interface, r } } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - coin := checkState.Coins().GetCoin(data.Coin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, data.Coin).Cmp(data.Stake) < 0 { + coin := checkState.Coins().GetCoin(data.Coin) return Response{ Code: code.InsufficientFunds, Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), data.Stake, coin.GetFullSymbol()), @@ -145,29 +157,32 @@ func (data DeclareCandidacyData) Run(tx *Transaction, context state.Interface, r } } } - + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) rewardPool.Add(rewardPool, commissionInBaseCoin) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - deliverState.Accounts.SubBalance(sender, data.Coin, data.Stake) - deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) - deliverState.Candidates.Create(data.Address, sender, sender, data.PubKey, data.Commission) + deliverState.Candidates.Create(data.Address, sender, sender, data.PubKey, data.Commission, currentBlock) deliverState.Candidates.Delegate(sender, data.PubKey, data.Coin, data.Stake, big.NewInt(0)) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeDeclareCandidacy)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/declare_candidacy_test.go b/core/transaction/declare_candidacy_test.go index 1373d8a51..09e76dd97 100644 --- a/core/transaction/declare_candidacy_test.go +++ b/core/transaction/declare_candidacy_test.go @@ -16,6 +16,7 @@ import ( ) func TestDeclareCandidacyTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -30,12 +31,12 @@ func TestDeclareCandidacyTx(t *testing.T) { var publicKey types.Pubkey copy(publicKey[:], publicKeyBytes) - commission := uint32(10) + percent := uint32(10) data := DeclareCandidacyData{ Address: addr, PubKey: publicKey, - Commission: commission, + Commission: percent, Coin: coin, Stake: helpers.BipToPip(big.NewInt(100)), } @@ -95,11 +96,11 @@ func TestDeclareCandidacyTx(t *testing.T) { t.Fatalf("Control address is not correct") } - if candidate.GetTotalBipStake() != nil && candidate.GetTotalBipStake().Cmp(types.Big0) != 0 { + if candidate.GetTotalBipStake() != nil && candidate.GetTotalBipStake().Sign() != 0 { t.Fatalf("Total stake is not correct") } - if candidate.Commission != commission { + if candidate.Commission != percent { t.Fatalf("Commission is not correct") } @@ -107,16 +108,19 @@ func TestDeclareCandidacyTx(t *testing.T) { t.Fatalf("Incorrect candidate status") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyTxOverflow(t *testing.T) { + t.Parallel() cState := getState() maxCandidatesCount := validators.GetCandidatesCountForBlock(0) for i := 0; i < maxCandidatesCount; i++ { pubkey := types.Pubkey{byte(i)} - cState.Candidates.Create(types.Address{}, types.Address{}, types.Address{}, pubkey, 10) + cState.Candidates.Create(types.Address{}, types.Address{}, types.Address{}, pubkey, 10, 0) cState.Candidates.Delegate(types.Address{}, pubkey, types.GetBaseCoinID(), helpers.BipToPip(big.NewInt(10)), helpers.BipToPip(big.NewInt(10))) } @@ -174,10 +178,13 @@ func TestDeclareCandidacyTxOverflow(t *testing.T) { t.Fatalf("Response code is not %d. Got %d", code.TooLowStake, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyTxWithBlockPybKey(t *testing.T) { + t.Parallel() cState := getState() pkey, _ := crypto.GenerateKey() @@ -185,7 +192,7 @@ func TestDeclareCandidacyTxWithBlockPybKey(t *testing.T) { var publicKey types.Pubkey copy(publicKey[:], publicKeyBytes) - cState.Candidates.Create(types.Address{}, types.Address{}, types.Address{}, publicKey, 10) + cState.Candidates.Create(types.Address{}, types.Address{}, types.Address{}, publicKey, 10, 0) pkeyNew, _ := crypto.GenerateKey() publicKeyNewBytes := crypto.FromECDSAPub(&pkeyNew.PublicKey)[:32] var publicKeyNew types.Pubkey @@ -200,12 +207,12 @@ func TestDeclareCandidacyTxWithBlockPybKey(t *testing.T) { cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) - commission := uint32(10) + percent := uint32(10) data := DeclareCandidacyData{ Address: addr, PubKey: publicKey, - Commission: commission, + Commission: percent, Coin: coin, Stake: helpers.BipToPip(big.NewInt(100)), } @@ -264,10 +271,13 @@ func TestDeclareCandidacyTxWithBlockPybKey(t *testing.T) { t.Fatalf("ControlAddress has changed") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyToNonExistCoin(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -282,12 +292,12 @@ func TestDeclareCandidacyToNonExistCoin(t *testing.T) { var publicKey types.Pubkey copy(publicKey[:], publicKeyBytes) - commission := uint32(10) + percent := uint32(10) data := DeclareCandidacyData{ Address: addr, PubKey: publicKey, - Commission: commission, + Commission: percent, Coin: 5, Stake: helpers.BipToPip(big.NewInt(100)), } @@ -321,10 +331,13 @@ func TestDeclareCandidacyToNonExistCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyToExistCandidate(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -339,14 +352,14 @@ func TestDeclareCandidacyToExistCandidate(t *testing.T) { var publicKey types.Pubkey copy(publicKey[:], publicKeyBytes) - cState.Candidates.Create(addr, addr, addr, publicKey, uint32(10)) + cState.Candidates.Create(addr, addr, addr, publicKey, uint32(10), 0) - commission := uint32(10) + persent := uint32(10) data := DeclareCandidacyData{ Address: addr, PubKey: publicKey, - Commission: commission, + Commission: persent, Coin: coin, Stake: helpers.BipToPip(big.NewInt(100)), } @@ -380,10 +393,13 @@ func TestDeclareCandidacyToExistCandidate(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CandidateExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyToDecodeError(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -427,15 +443,18 @@ func TestDeclareCandidacyToDecodeError(t *testing.T) { t.Fatal(err) } - response := data.Run(&tx, state.NewCheckState(cState), nil, 0) + response := data.Run(&tx, state.NewCheckState(cState), nil, 0, nil) if response.Code != code.DecodeError { t.Fatalf("Response code is not %d. Error %s", code.DecodeError, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyToWrongCommission(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -487,10 +506,13 @@ func TestDeclareCandidacyToWrongCommission(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCommission, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() addr := crypto.PubkeyToAddress(privateKey.PublicKey) @@ -539,7 +561,9 @@ func TestDeclareCandidacyToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } cState.Accounts.AddBalance(addr, coin, stake) cState.Commit() @@ -587,10 +611,13 @@ func TestDeclareCandidacyToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDeclareCandidacyTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() addr := crypto.PubkeyToAddress(privateKey.PublicKey) @@ -640,9 +667,11 @@ func TestDeclareCandidacyTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/decoder.go b/core/transaction/decoder.go index 0a1b8ae8b..057ebed3b 100644 --- a/core/transaction/decoder.go +++ b/core/transaction/decoder.go @@ -4,46 +4,85 @@ import ( "errors" "fmt" "github.com/MinterTeam/minter-go-node/rlp" - "reflect" ) -var TxDecoder = Decoder{ - registeredTypes: map[TxType]Data{}, -} - -func init() { - TxDecoder.RegisterType(TypeSend, SendData{}) - TxDecoder.RegisterType(TypeSellCoin, SellCoinData{}) - TxDecoder.RegisterType(TypeSellAllCoin, SellAllCoinData{}) - TxDecoder.RegisterType(TypeBuyCoin, BuyCoinData{}) - TxDecoder.RegisterType(TypeCreateCoin, CreateCoinData{}) - TxDecoder.RegisterType(TypeDeclareCandidacy, DeclareCandidacyData{}) - TxDecoder.RegisterType(TypeDelegate, DelegateData{}) - TxDecoder.RegisterType(TypeUnbond, UnbondData{}) - TxDecoder.RegisterType(TypeRedeemCheck, RedeemCheckData{}) - TxDecoder.RegisterType(TypeSetCandidateOnline, SetCandidateOnData{}) - TxDecoder.RegisterType(TypeSetCandidateOffline, SetCandidateOffData{}) - TxDecoder.RegisterType(TypeMultisend, MultisendData{}) - TxDecoder.RegisterType(TypeCreateMultisig, CreateMultisigData{}) - TxDecoder.RegisterType(TypeEditCandidate, EditCandidateData{}) - TxDecoder.RegisterType(TypeSetHaltBlock, SetHaltBlockData{}) - TxDecoder.RegisterType(TypeRecreateCoin, RecreateCoinData{}) - TxDecoder.RegisterType(TypeEditCoinOwner, EditCoinOwnerData{}) - TxDecoder.RegisterType(TypeEditMultisig, EditMultisigData{}) - TxDecoder.RegisterType(TypePriceVote, PriceVoteData{}) - TxDecoder.RegisterType(TypeEditCandidatePublicKey, EditCandidatePublicKeyData{}) -} - -type Decoder struct { - registeredTypes map[TxType]Data -} - -func (decoder *Decoder) RegisterType(t TxType, d Data) { - decoder.registeredTypes[t] = d +func getData(txType TxType) (Data, bool) { + switch txType { + case TypeSend: + return &SendData{}, true + case TypeSellCoin: + return &SellCoinData{}, true + case TypeSellAllCoin: + return &SellAllCoinData{}, true + case TypeBuyCoin: + return &BuyCoinData{}, true + case TypeCreateCoin: + return &CreateCoinData{}, true + case TypeDeclareCandidacy: + return &DeclareCandidacyData{}, true + case TypeDelegate: + return &DelegateData{}, true + case TypeUnbond: + return &UnbondData{}, true + case TypeRedeemCheck: + return &RedeemCheckData{}, true + case TypeSetCandidateOnline: + return &SetCandidateOnData{}, true + case TypeSetCandidateOffline: + return &SetCandidateOffData{}, true + case TypeMultisend: + return &MultisendData{}, true + case TypeCreateMultisig: + return &CreateMultisigData{}, true + case TypeEditCandidate: + return &EditCandidateData{}, true + case TypeSetHaltBlock: + return &SetHaltBlockData{}, true + case TypeRecreateCoin: + return &RecreateCoinData{}, true + case TypeEditCoinOwner: + return &EditCoinOwnerData{}, true + case TypeEditMultisig: + return &EditMultisigData{}, true + // case TypePriceVote: + // return &PriceVoteData{}, true + case TypeEditCandidatePublicKey: + return &EditCandidatePublicKeyData{}, true + case TypeAddLiquidity: + return &AddLiquidityData{}, true + case TypeRemoveLiquidity: + return &RemoveLiquidity{}, true + case TypeSellSwapPool: + return &SellSwapPoolData{}, true + case TypeBuySwapPool: + return &BuySwapPoolData{}, true + case TypeSellAllSwapPool: + return &SellAllSwapPoolData{}, true + case TypeEditCandidateCommission: + return &EditCandidateCommission{}, true + // case TypeMoveStake: + // return &MoveStakeData{}, true + case TypeMintToken: + return &MintTokenData{}, true + case TypeBurnToken: + return &BurnTokenData{}, true + case TypeCreateToken: + return &CreateTokenData{}, true + case TypeRecreateToken: + return &RecreateTokenData{}, true + case TypeVoteCommission: + return &VoteCommissionData{}, true + case TypeVoteUpdate: + return &VoteUpdateData{}, true + case TypeCreateSwapPool: + return &CreateSwapPoolData{}, true + default: + return nil, false + } } -func (decoder *Decoder) DecodeFromBytes(buf []byte) (*Transaction, error) { - tx, err := decoder.DecodeFromBytesWithoutSig(buf) +func DecodeFromBytes(buf []byte) (*Transaction, error) { + tx, err := DecodeFromBytesWithoutSig(buf) if err != nil { return nil, err } @@ -79,7 +118,7 @@ func DecodeSig(tx *Transaction) (*Transaction, error) { return tx, nil } -func (decoder *Decoder) DecodeFromBytesWithoutSig(buf []byte) (*Transaction, error) { +func DecodeFromBytesWithoutSig(buf []byte) (*Transaction, error) { var tx Transaction err := rlp.DecodeBytes(buf, &tx) @@ -91,13 +130,13 @@ func (decoder *Decoder) DecodeFromBytesWithoutSig(buf []byte) (*Transaction, err return nil, errors.New("incorrect tx data") } - d, ok := decoder.registeredTypes[tx.Type] + d, ok := getData(tx.Type) if !ok { return nil, fmt.Errorf("tx type %x is not registered", tx.Type) } - err = rlp.DecodeBytesForType(tx.Data, reflect.ValueOf(d).Type(), &d) + err = rlp.DecodeBytes(tx.Data, d) if err != nil { return nil, err diff --git a/core/transaction/decoder_test.go b/core/transaction/decoder_test.go index 15eb56ff2..296f6213c 100644 --- a/core/transaction/decoder_test.go +++ b/core/transaction/decoder_test.go @@ -8,6 +8,7 @@ import ( ) func TestDecodeFromBytesToInvalidSignature(t *testing.T) { + t.Parallel() data := SendData{Coin: 0, To: types.Address{0}, Value: big.NewInt(0)} encodedData, err := rlp.EncodeToBytes(data) if err != nil { @@ -29,13 +30,14 @@ func TestDecodeFromBytesToInvalidSignature(t *testing.T) { t.Fatal(err) } - _, err = TxDecoder.DecodeFromBytes(encodedTx) + _, err = DecodeFromBytes(encodedTx) if err == nil { t.Fatal("Expected the invalid signature error") } } func TestDecodeSigToInvalidMultiSignature(t *testing.T) { + t.Parallel() tx := Transaction{ Nonce: 1, GasPrice: 1, @@ -53,6 +55,7 @@ func TestDecodeSigToInvalidMultiSignature(t *testing.T) { } func TestDecodeSigToInvalidSingleSignature(t *testing.T) { + t.Parallel() tx := Transaction{ Nonce: 1, GasPrice: 1, @@ -70,6 +73,7 @@ func TestDecodeSigToInvalidSingleSignature(t *testing.T) { } func TestDecodeSigToUnknownSignatureType(t *testing.T) { + t.Parallel() tx := Transaction{ Nonce: 1, GasPrice: 1, @@ -87,6 +91,7 @@ func TestDecodeSigToUnknownSignatureType(t *testing.T) { } func TestDecodeFromBytesWithoutSigToInvalidData(t *testing.T) { + t.Parallel() tx := Transaction{ Nonce: 1, GasPrice: 1, @@ -102,7 +107,7 @@ func TestDecodeFromBytesWithoutSigToInvalidData(t *testing.T) { t.Fatal(err) } - _, err = TxDecoder.DecodeFromBytesWithoutSig(encodedTx) + _, err = DecodeFromBytesWithoutSig(encodedTx) if err == nil { t.Fatal("Expected tx type is not registered error") } @@ -113,7 +118,7 @@ func TestDecodeFromBytesWithoutSigToInvalidData(t *testing.T) { t.Fatal(err) } - _, err = TxDecoder.DecodeFromBytesWithoutSig(encodedTx) + _, err = DecodeFromBytesWithoutSig(encodedTx) if err == nil { t.Fatal("Expected invalid data error") } diff --git a/core/transaction/delegate.go b/core/transaction/delegate.go index d48f186b6..e274361d0 100644 --- a/core/transaction/delegate.go +++ b/core/transaction/delegate.go @@ -4,12 +4,11 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" "github.com/MinterTeam/minter-go-node/hexutil" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -19,7 +18,14 @@ type DelegateData struct { Value *big.Int } -func (data DelegateData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data DelegateData) Gas() int { + return gasDelegate +} +func (data DelegateData) TxType() TxType { + return TypeDelegate +} + +func (data DelegateData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.Value == nil { return &Response{ Code: code.DecodeError, @@ -28,19 +34,25 @@ func (data DelegateData) BasicCheck(tx *Transaction, context *state.CheckState) } } - if !context.Coins().Exists(tx.GasCoin) { + coin := context.Coins().GetCoin(data.Coin) + if coin == nil { return &Response{ Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", tx.GasCoin), - Info: EncodeError(code.NewCoinNotExists("", tx.GasCoin.String())), + Log: fmt.Sprintf("Coin %s not exists", data.Coin), + Info: EncodeError(code.NewCoinNotExists("", data.Coin.String())), } } - if !context.Coins().Exists(data.Coin) { + if !coin.BaseOrHasReserve() { return &Response{ - Code: code.CoinNotExists, - Log: fmt.Sprintf("Coin %s not exists", data.Coin), - Info: EncodeError(code.NewCoinNotExists("", data.Coin.String())), + Code: code.CoinReserveNotSufficient, + Log: "coin has no reserve", + Info: EncodeError(code.NewCoinReserveNotSufficient( + coin.GetFullSymbol(), + coin.ID().String(), + coin.Reserve().String(), + "", + )), } } @@ -50,7 +62,7 @@ func (data DelegateData) BasicCheck(tx *Transaction, context *state.CheckState) value.Add(value, waitList.Value) } - if value.Cmp(types.Big0) < 1 { + if value.Sign() < 1 { return &Response{ Code: code.StakeShouldBePositive, Log: "Stake should be positive", @@ -70,7 +82,7 @@ func (data DelegateData) BasicCheck(tx *Transaction, context *state.CheckState) return &Response{ Code: code.TooLowStake, Log: "Stake is too low", - Info: EncodeError(code.NewTooLowStake(sender.String(), data.PubKey.String(), value.String(), data.Coin.String(), context.Coins().GetCoin(data.Coin).GetFullSymbol())), + Info: EncodeError(code.NewTooLowStake(sender.String(), data.PubKey.String(), value.String(), data.Coin.String(), coin.GetFullSymbol())), } } @@ -82,11 +94,11 @@ func (data DelegateData) String() string { hexutil.Encode(data.PubKey[:])) } -func (data DelegateData) Gas() int64 { - return commissions.DelegateTx +func (data DelegateData) CommissionData(price *commission.Price) *big.Int { + return price.Delegate } -func (data DelegateData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data DelegateData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -95,24 +107,17 @@ func (data DelegateData) Run(tx *Transaction, context state.Interface, rewardPoo checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - coin := checkState.Coins().GetCoin(data.Coin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -124,6 +129,7 @@ func (data DelegateData) Run(tx *Transaction, context state.Interface, rewardPoo } if checkState.Accounts().GetBalance(sender, data.Coin).Cmp(data.Value) < 0 { + coin := checkState.Coins().GetCoin(data.Coin) return Response{ Code: code.InsufficientFunds, Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), data.Value, coin.GetFullSymbol()), @@ -144,14 +150,16 @@ func (data DelegateData) Run(tx *Transaction, context state.Interface, rewardPoo } } } - + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) deliverState.Accounts.SubBalance(sender, data.Coin, data.Value) value := big.NewInt(0).Set(data.Value) @@ -162,17 +170,17 @@ func (data DelegateData) Run(tx *Transaction, context state.Interface, rewardPoo deliverState.Candidates.Delegate(sender, data.PubKey, data.Coin, value, big.NewInt(0)) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeDelegate)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/delegate_test.go b/core/transaction/delegate_test.go index 6e06c68bc..eb4c4833e 100644 --- a/core/transaction/delegate_test.go +++ b/core/transaction/delegate_test.go @@ -20,12 +20,13 @@ func createTestCandidate(stateDB *state.State) types.Pubkey { pubkey := types.Pubkey{} rand.Read(pubkey[:]) - stateDB.Candidates.Create(address, address, address, pubkey, 10) + stateDB.Candidates.Create(address, address, address, pubkey, 10, 0) return pubkey } func TestDelegateTx(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -95,10 +96,13 @@ func TestDelegateTx(t *testing.T) { t.Fatalf("Stake value is not corrent. Expected %s, got %s", value, stake.Value) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateTxWithWaitlist(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) privateKey, _ := crypto.GenerateKey() @@ -161,10 +165,13 @@ func TestDelegateTxWithWaitlist(t *testing.T) { t.Fatalf("Waitlist is not deleted") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateTxToNonExistCoin(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -210,10 +217,13 @@ func TestDelegateTxToNonExistCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateTxToPositiveStake(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -259,10 +269,13 @@ func TestDelegateTxToPositiveStake(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.StakeShouldBePositive, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateTxToNonExistCandidate(t *testing.T) { + t.Parallel() cState := getState() pubkey := types.Pubkey{1} @@ -308,10 +321,13 @@ func TestDelegateTxToNonExistCandidate(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CandidateNotFound, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateTxToLowStake(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -366,10 +382,13 @@ func TestDelegateTxToLowStake(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.TooLowStake, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -414,7 +433,9 @@ func TestDelegateTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } cState.Accounts.AddBalance(addr, coin, big.NewInt(2e17)) @@ -460,10 +481,13 @@ func TestDelegateTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -504,14 +528,17 @@ func TestDelegateTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestDelegateData_addFromWaitlist(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -566,5 +593,7 @@ func TestDelegateData_addFromWaitlist(t *testing.T) { t.Fatal(err) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/edit_candidate.go b/core/transaction/edit_candidate.go index 3f7616b87..970a614f7 100644 --- a/core/transaction/edit_candidate.go +++ b/core/transaction/edit_candidate.go @@ -4,11 +4,10 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -23,11 +22,18 @@ type EditCandidateData struct { ControlAddress types.Address } +func (data EditCandidateData) Gas() int { + return gasEditCandidate +} +func (data EditCandidateData) TxType() TxType { + return TypeEditCandidate +} + func (data EditCandidateData) GetPubKey() types.Pubkey { return data.PubKey } -func (data EditCandidateData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data EditCandidateData) basicCheck(tx *Transaction, context *state.CheckState) *Response { return checkCandidateOwnership(data, tx, context) } @@ -36,11 +42,11 @@ func (data EditCandidateData) String() string { data.PubKey) } -func (data EditCandidateData) Gas() int64 { - return commissions.EditCandidate +func (data EditCandidateData) CommissionData(price *commission.Price) *big.Int { + return price.EditCandidate } -func (data EditCandidateData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data EditCandidateData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -49,23 +55,17 @@ func (data EditCandidateData) Run(tx *Transaction, context state.Interface, rewa checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -76,27 +76,31 @@ func (data EditCandidateData) Run(tx *Transaction, context state.Interface, rewa } } + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) rewardPool.Add(rewardPool, commissionInBaseCoin) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - - deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) deliverState.Candidates.Edit(data.PubKey, data.RewardAddress, data.OwnerAddress, data.ControlAddress) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeEditCandidate)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/edit_candidate_commission.go b/core/transaction/edit_candidate_commission.go new file mode 100644 index 000000000..af6d98610 --- /dev/null +++ b/core/transaction/edit_candidate_commission.go @@ -0,0 +1,129 @@ +package transaction + +import ( + "encoding/hex" + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" + "strconv" +) + +type EditCandidateCommission struct { + PubKey types.Pubkey + Commission uint32 +} + +func (data EditCandidateCommission) Gas() int { + return gasEditCandidateCommission +} +func (data EditCandidateCommission) TxType() TxType { + return TypeEditCandidateCommission +} + +func (data EditCandidateCommission) GetPubKey() types.Pubkey { + return data.PubKey +} + +func (data EditCandidateCommission) basicCheck(tx *Transaction, context *state.CheckState, block uint64) *Response { + errResp := checkCandidateOwnership(data, tx, context) + if errResp != nil { + return errResp + } + + candidate := context.Candidates().GetCandidate(data.PubKey) + + maxNewCommission, minNewCommission := candidate.Commission+10, candidate.Commission-10 + if maxNewCommission > maxCommission { + maxNewCommission = maxCommission + } + if minNewCommission < minCommission || minNewCommission > maxCommission { + minNewCommission = minCommission + } + if data.Commission < minNewCommission || data.Commission > maxNewCommission { + return &Response{ + Code: code.WrongCommission, + Log: fmt.Sprintf("You want change commission from %d to %d, but you can change no more than 10 units, because commission should be between %d and %d", candidate.Commission, data.Commission, minNewCommission, maxNewCommission), + Info: EncodeError(code.NewWrongCommission(fmt.Sprintf("%d", data.Commission), strconv.Itoa(int(minNewCommission)), strconv.Itoa(int(maxNewCommission)))), + } + } + + if candidate.LastEditCommissionHeight+3*types.GetUnbondPeriod() > block { + return &Response{ + Code: code.PeriodLimitReached, + Log: fmt.Sprintf("You cannot change the commission more than once every %d blocks, the last change was on block %d", 3*types.GetUnbondPeriod(), candidate.LastEditCommissionHeight), + Info: EncodeError(code.NewPeriodLimitReached(strconv.Itoa(int(candidate.LastEditCommissionHeight+3*types.GetUnbondPeriod())), strconv.Itoa(int(candidate.LastEditCommissionHeight)))), + } + } + + return nil +} + +func (data EditCandidateCommission) String() string { + return fmt.Sprintf("EDIT COMMISSION: %s", data.PubKey) +} + +func (data EditCandidateCommission) CommissionData(price *commission.Price) *big.Int { + return price.EditCandidateCommission +} + +func (data EditCandidateCommission) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState, currentBlock) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + deliverState.Candidates.EditCommission(data.PubKey, data.Commission, currentBlock) + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/edit_candidate_public_key.go b/core/transaction/edit_candidate_public_key.go index b19440c51..b5b414270 100644 --- a/core/transaction/edit_candidate_public_key.go +++ b/core/transaction/edit_candidate_public_key.go @@ -4,11 +4,10 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -17,11 +16,18 @@ type EditCandidatePublicKeyData struct { NewPubKey types.Pubkey } +func (data EditCandidatePublicKeyData) Gas() int { + return gasEditCandidatePublicKey +} +func (data EditCandidatePublicKeyData) TxType() TxType { + return TypeEditCandidatePublicKey +} + func (data EditCandidatePublicKeyData) GetPubKey() types.Pubkey { return data.PubKey } -func (data EditCandidatePublicKeyData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data EditCandidatePublicKeyData) basicCheck(tx *Transaction, context *state.CheckState) *Response { return checkCandidateOwnership(data, tx, context) } @@ -30,11 +36,11 @@ func (data EditCandidatePublicKeyData) String() string { data.PubKey, data.NewPubKey) } -func (data EditCandidatePublicKeyData) Gas() int64 { - return commissions.EditCandidatePublicKey +func (data EditCandidatePublicKeyData) CommissionData(price *commission.Price) *big.Int { + return price.EditCandidatePublicKey } -func (data EditCandidatePublicKeyData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data EditCandidatePublicKeyData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -43,7 +49,7 @@ func (data EditCandidatePublicKeyData) Run(tx *Transaction, context state.Interf checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } @@ -64,18 +70,12 @@ func (data EditCandidatePublicKeyData) Run(tx *Transaction, context state.Interf } } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -93,28 +93,31 @@ func (data EditCandidatePublicKeyData) Run(tx *Transaction, context state.Interf Info: EncodeError(code.NewPublicKeyInBlockList(data.NewPubKey.String())), } } - + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) rewardPool.Add(rewardPool, commissionInBaseCoin) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - - deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) deliverState.Candidates.ChangePubKey(data.PubKey, data.NewPubKey) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeEditCandidatePublicKey)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/edit_candidate_public_key_test.go b/core/transaction/edit_candidate_public_key_test.go index 7baec6da4..b1817f204 100644 --- a/core/transaction/edit_candidate_public_key_test.go +++ b/core/transaction/edit_candidate_public_key_test.go @@ -13,6 +13,7 @@ import ( ) func TestEditCandidateNewPublicKeyTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -23,7 +24,7 @@ func TestEditCandidateNewPublicKeyTx(t *testing.T) { pubkey := types.HexToPubkey("Mp11fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c611") newpubkey := types.HexToPubkey("Mp12fdfc072182654f163f5f0f9a621d729566c74d10037c4d7bbb0407d1e2c612") - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Candidates.SetStakes(pubkey, []types.Stake{ { Owner: addr, @@ -90,10 +91,13 @@ func TestEditCandidateNewPublicKeyTx(t *testing.T) { } cState.Validators.SetNewValidators(cState.Candidates.GetNewCandidates(1)) - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidatePublicKeyTxToNewPublicKey(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -104,7 +108,7 @@ func TestEditCandidatePublicKeyTxToNewPublicKey(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) data := EditCandidatePublicKeyData{ @@ -141,10 +145,13 @@ func TestEditCandidatePublicKeyTxToNewPublicKey(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.NewPublicKeyIsBad, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidatePublicKeyTxToNewPublicKeyInBlockList(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -157,7 +164,7 @@ func TestEditCandidatePublicKeyTxToNewPublicKeyInBlockList(t *testing.T) { pubkey2 := types.Pubkey{1} - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) cState.Candidates.AddToBlockPubKey(pubkey2) @@ -195,10 +202,13 @@ func TestEditCandidatePublicKeyTxToNewPublicKeyInBlockList(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.PublicKeyInBlockList, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidatePublicKeyTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -208,7 +218,7 @@ func TestEditCandidatePublicKeyTxToInsufficientFunds(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) data := EditCandidatePublicKeyData{ @@ -245,10 +255,13 @@ func TestEditCandidatePublicKeyTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidatePublicKeyTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -261,7 +274,7 @@ func TestEditCandidatePublicKeyTxToGasCoinReserveUnderflow(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) data := EditCandidatePublicKeyData{ @@ -294,14 +307,17 @@ func TestEditCandidatePublicKeyTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidatePublicKeyToNotExistCandidate(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -346,10 +362,13 @@ func TestEditCandidatePublicKeyToNotExistCandidate(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CandidateNotFound, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidatePublicKeyTxToCandidateOwnership(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -361,7 +380,7 @@ func TestEditCandidatePublicKeyTxToCandidateOwnership(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr2, addr2, addr2, pubkey, 10) + cState.Candidates.Create(addr2, addr2, addr2, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) data := EditCandidatePublicKeyData{ @@ -398,10 +417,13 @@ func TestEditCandidatePublicKeyTxToCandidateOwnership(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.IsNotOwnerOfCandidate, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidatePublicKeyData_Exists(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -414,10 +436,10 @@ func TestEditCandidatePublicKeyData_Exists(t *testing.T) { newpubkey := [32]byte{} rand.Read(newpubkey[:]) - cState.Candidates.Create(addr, addr, addr, newpubkey, 10) + cState.Candidates.Create(addr, addr, addr, newpubkey, 10, 0) cState.Validators.Create(newpubkey, helpers.BipToPip(big.NewInt(1))) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) candidate1 := cState.Candidates.GetCandidate(newpubkey) @@ -467,5 +489,7 @@ func TestEditCandidatePublicKeyData_Exists(t *testing.T) { t.Fatalf("Candidates pulic keys are equal") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/edit_candidate_test.go b/core/transaction/edit_candidate_test.go index 1d6d59f50..c4a1aa689 100644 --- a/core/transaction/edit_candidate_test.go +++ b/core/transaction/edit_candidate_test.go @@ -14,6 +14,7 @@ import ( ) func TestEditCandidateTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -24,7 +25,7 @@ func TestEditCandidateTx(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) newRewardAddress := types.Address{1} @@ -94,10 +95,13 @@ func TestEditCandidateTx(t *testing.T) { t.Fatalf("ControlAddress has not changed") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidateTxToNonExistCandidate(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -148,10 +152,13 @@ func TestEditCandidateTxToNonExistCandidate(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CandidateNotFound, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidateTxToCandidateOwnership(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -163,7 +170,7 @@ func TestEditCandidateTxToCandidateOwnership(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr2, addr2, addr2, pubkey, 10) + cState.Candidates.Create(addr2, addr2, addr2, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) newRewardAddress := types.Address{1} @@ -206,10 +213,13 @@ func TestEditCandidateTxToCandidateOwnership(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.IsNotOwnerOfCandidate, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidateTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -219,7 +229,7 @@ func TestEditCandidateTxToInsufficientFunds(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) newRewardAddress := types.Address{1} @@ -262,10 +272,13 @@ func TestEditCandidateTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCandidateTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -278,7 +291,7 @@ func TestEditCandidateTxToGasCoinReserveUnderflow(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) newRewardAddress := types.Address{1} @@ -317,9 +330,11 @@ func TestEditCandidateTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/edit_coin_owner.go b/core/transaction/edit_coin_owner.go index 7658d240c..2829285bf 100644 --- a/core/transaction/edit_coin_owner.go +++ b/core/transaction/edit_coin_owner.go @@ -1,14 +1,12 @@ package transaction import ( - "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -17,17 +15,17 @@ type EditCoinOwnerData struct { NewOwner types.Address } -func (data EditCoinOwnerData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data EditCoinOwnerData) Gas() int { + return gasEditCoinOwner +} +func (data EditCoinOwnerData) TxType() TxType { + return TypeEditCoinOwner +} + +func (data EditCoinOwnerData) basicCheck(tx *Transaction, context *state.CheckState) *Response { sender, _ := tx.Sender() - info := context.Coins().GetSymbolInfo(data.Symbol) - if info == nil { - // todo: change coin owner error message - // return &Response{ - // Code: code.IsNotOwnerOfCoin, - // Log: fmt.Sprintf("Sender is not owner of coin"), - // Info: EncodeError(code.NewIsNotOwnerOfCoin(data.Symbol.String(), nil)), - // } + if !context.Coins().ExistsBySymbol(data.Symbol) { return &Response{ Code: code.CoinNotExists, Log: fmt.Sprintf("Coin %s not exists", data.Symbol), @@ -35,6 +33,15 @@ func (data EditCoinOwnerData) BasicCheck(tx *Transaction, context *state.CheckSt } } + info := context.Coins().GetSymbolInfo(data.Symbol) + if info == nil { + return &Response{ + Code: code.IsNotOwnerOfCoin, + Log: fmt.Sprintf("Sender is not owner of coin"), + Info: EncodeError(code.NewIsNotOwnerOfCoin(data.Symbol.String(), nil)), + } + } + if info.OwnerAddress() == nil || *info.OwnerAddress() != sender { owner := info.OwnerAddress().String() return &Response{ @@ -51,11 +58,11 @@ func (data EditCoinOwnerData) String() string { return fmt.Sprintf("EDIT OWNER COIN symbol:%s new owner:%s", data.Symbol.String(), data.NewOwner.String()) } -func (data EditCoinOwnerData) Gas() int64 { - return commissions.EditOwner +func (data EditCoinOwnerData) CommissionData(price *commission.Price) *big.Int { + return price.EditTickerOwner } -func (data EditCoinOwnerData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data EditCoinOwnerData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -64,23 +71,17 @@ func (data EditCoinOwnerData) Run(tx *Transaction, context state.Interface, rewa checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if tx.GasCoin != types.GetBaseCoinID() { - gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -92,26 +93,30 @@ func (data EditCoinOwnerData) Run(tx *Transaction, context state.Interface, rewa Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), } } - + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { rewardPool.Add(rewardPool, commissionInBaseCoin) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) deliverState.Coins.ChangeOwner(data.Symbol, data.NewOwner) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeEditCoinOwner)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String()), Index: true}, + {Key: []byte("tx.coin_id"), Value: []byte(checkState.Coins().GetCoinBySymbol(data.Symbol, 0).ID().String()), Index: true}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/edit_coin_owner_test.go b/core/transaction/edit_coin_owner_test.go index 33a7f613d..92542afd6 100644 --- a/core/transaction/edit_coin_owner_test.go +++ b/core/transaction/edit_coin_owner_test.go @@ -2,7 +2,6 @@ package transaction import ( "crypto/ecdsa" - "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" @@ -18,7 +17,8 @@ import ( ) func TestEditOwnerTx(t *testing.T) { - cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1, 0) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -33,7 +33,7 @@ func TestEditOwnerTx(t *testing.T) { createDefaultValidator(cState) gasCoin := types.GetBaseCoinID() - cState.Accounts.AddBalance(addr, gasCoin, helpers.BipToPip(big.NewInt(10000))) + cState.Accounts.AddBalance(addr, gasCoin, helpers.BipToPip(big.NewInt(1000000))) data := EditCoinOwnerData{ Symbol: getTestCoinSymbol(), @@ -50,7 +50,7 @@ func TestEditOwnerTx(t *testing.T) { t.Fatalf("Response code is not 0. Error %s", response.Log) } - err = cState.Coins.Commit() + _, _, err = cState.Tree().Commit(cState.Coins) if err != nil { t.Fatalf("Failed to commit coins: %s", err) } @@ -70,11 +70,14 @@ func TestEditOwnerTx(t *testing.T) { t.Fatalf("Target owner address is not correct. Excpected %s, got %s", newOwner.String(), symbol.OwnerAddress().String()) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditOwnerTxWithWrongOwner(t *testing.T) { - cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1, 0) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -102,11 +105,14 @@ func TestEditOwnerTxWithWrongOwner(t *testing.T) { t.Fatalf("Response code is not 206. Error %s", response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditOwnerTxWithWrongSymbol(t *testing.T) { - cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1, 0) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -135,11 +141,14 @@ func TestEditOwnerTxWithWrongSymbol(t *testing.T) { t.Fatalf("Response code is not 102. Error %s", response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCOwnerTxWithInsufficientFunds(t *testing.T) { - cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(0, db.NewMemDB(), nil, 1, 1, 0) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -168,10 +177,13 @@ func TestEditCOwnerTxWithInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditCoinOwnerTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -213,11 +225,13 @@ func TestEditCoinOwnerTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 500000, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func makeTestEditOwnerTx(data EditCoinOwnerData, privateKey *ecdsa.PrivateKey) ([]byte, error) { @@ -250,5 +264,5 @@ func makeTestEditOwnerTx(data EditCoinOwnerData, privateKey *ecdsa.PrivateKey) ( func createDefaultValidator(cState *state.State) { cState.Validators.Create(types.Pubkey{0}, big.NewInt(0)) - cState.Candidates.Create(types.Address{0}, types.Address{0}, types.Address{0}, types.Pubkey{0}, 0) + cState.Candidates.Create(types.Address{0}, types.Address{0}, types.Address{0}, types.Pubkey{0}, 0, 0) } diff --git a/core/transaction/edit_multisig.go b/core/transaction/edit_multisig.go index a07229729..8622521d5 100644 --- a/core/transaction/edit_multisig.go +++ b/core/transaction/edit_multisig.go @@ -1,17 +1,15 @@ package transaction import ( - "encoding/hex" "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" "math/big" "strconv" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" ) type EditMultisigData struct { @@ -20,7 +18,14 @@ type EditMultisigData struct { Addresses []types.Address } -func (data EditMultisigData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data EditMultisigData) Gas() int { + return gasEditMultisig +} +func (data EditMultisigData) TxType() TxType { + return TypeEditMultisig +} + +func (data EditMultisigData) basicCheck(tx *Transaction, context *state.CheckState) *Response { sender, _ := tx.Sender() if !context.Accounts().GetAccount(sender).IsMultisig() { @@ -91,11 +96,11 @@ func (data EditMultisigData) String() string { return "EDIT MULTISIG OWNERS" } -func (data EditMultisigData) Gas() int64 { - return commissions.EditMultisigData +func (data EditMultisigData) CommissionData(price *commission.Price) *big.Int { + return price.EditMultisig } -func (data EditMultisigData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data EditMultisigData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -104,23 +109,17 @@ func (data EditMultisigData) Run(tx *Transaction, context state.Interface, rewar checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -131,28 +130,29 @@ func (data EditMultisigData) Run(tx *Transaction, context state.Interface, rewar } } + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubVolume(tx.GasCoin, commission) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) deliverState.Accounts.SetNonce(sender, tx.Nonce) deliverState.Accounts.EditMultisig(data.Threshold, data.Weights, data.Addresses, sender) - } - address := []byte(hex.EncodeToString(sender[:])) - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeEditMultisig)}))}, - kv.Pair{Key: []byte("tx.from"), Value: address}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/edit_multisig_test.go b/core/transaction/edit_multisig_test.go index d52c6c688..fd41052bf 100644 --- a/core/transaction/edit_multisig_test.go +++ b/core/transaction/edit_multisig_test.go @@ -16,6 +16,7 @@ import ( ) func TestEditMultisigTx(t *testing.T) { + t.Parallel() cState := getState() pubkey := [32]byte{} @@ -97,10 +98,13 @@ func TestEditMultisigTx(t *testing.T) { t.Fatalf("Threshold is not correct") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditMultisigTxToNonExistAddress(t *testing.T) { + t.Parallel() cState := getState() addr := types.Address{0} @@ -139,13 +143,14 @@ func TestEditMultisigTxToNonExistAddress(t *testing.T) { } checkState := state.NewCheckState(cState) - response := data.BasicCheck(&tx, checkState) + response := data.basicCheck(&tx, checkState) if response.Code != code.MultisigNotExists { t.Fatalf("Response code is not %d. Error %s", code.MultisigNotExists, response.Log) } } func TestEditMultisigTxToTooLargeOwnersList(t *testing.T) { + t.Parallel() cState := getState() addr := types.Address{0} @@ -202,10 +207,13 @@ func TestEditMultisigTxToTooLargeOwnersList(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.TooLargeOwnersList, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditMultisigTxIncorrectWeights(t *testing.T) { + t.Parallel() cState := getState() addr := types.Address{0} @@ -257,7 +265,9 @@ func TestEditMultisigTxIncorrectWeights(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.DifferentCountAddressesAndWeights, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.Weights = []uint32{1, 2, 1024} encodedData, err = rlp.EncodeToBytes(data) @@ -280,7 +290,9 @@ func TestEditMultisigTxIncorrectWeights(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.IncorrectWeights, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.Weights = []uint32{1, 2, 3} data.Threshold = 7 @@ -304,10 +316,13 @@ func TestEditMultisigTxIncorrectWeights(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.IncorrectTotalWeights, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditMultisigTxToAddressDuplication(t *testing.T) { + t.Parallel() cState := getState() addr := types.Address{0} @@ -359,10 +374,13 @@ func TestEditMultisigTxToAddressDuplication(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.DuplicatedAddresses, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditMultisigTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() addr := types.Address{0} @@ -414,10 +432,13 @@ func TestEditMultisigTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestEditMultisigTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() addr := types.Address{0} @@ -466,9 +487,11 @@ func TestEditMultisigTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/encoder/encoder.go b/core/transaction/encoder/encoder.go deleted file mode 100644 index fc6c921bf..000000000 --- a/core/transaction/encoder/encoder.go +++ /dev/null @@ -1,110 +0,0 @@ -package encoder - -import ( - "encoding/json" - "fmt" - - "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/core/transaction" - rpctypes "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "github.com/tendermint/tendermint/libs/bytes" - coretypes "github.com/tendermint/tendermint/rpc/core/types" -) - -type TxEncoderJSON struct { - context *state.CheckState -} - -type TransactionResponse struct { - Hash string `json:"hash"` - RawTx string `json:"raw_tx"` - Height int64 `json:"height"` - Index uint32 `json:"index"` - From string `json:"from"` - Nonce uint64 `json:"nonce"` - Gas int64 `json:"gas"` - GasPrice uint32 `json:"gas_price"` - GasCoin CoinResource `json:"gas_coin"` - Type uint8 `json:"type"` - Data json.RawMessage `json:"data"` - Payload []byte `json:"payload"` - Tags map[string]string `json:"tags"` - Code uint32 `json:"code,omitempty"` - Log string `json:"log,omitempty"` -} - -var resourcesConfig = map[transaction.TxType]TxDataResource{ - transaction.TypeSend: new(SendDataResource), - transaction.TypeSellCoin: new(SellCoinDataResource), - transaction.TypeSellAllCoin: new(SellAllCoinDataResource), - transaction.TypeBuyCoin: new(BuyCoinDataResource), - transaction.TypeCreateCoin: new(CreateCoinDataResource), - transaction.TypeDeclareCandidacy: new(DeclareCandidacyDataResource), - transaction.TypeDelegate: new(DelegateDataResource), - transaction.TypeUnbond: new(UnbondDataResource), - transaction.TypeRedeemCheck: new(RedeemCheckDataResource), - transaction.TypeSetCandidateOnline: new(SetCandidateOnDataResource), - transaction.TypeSetCandidateOffline: new(SetCandidateOffDataResource), - transaction.TypeCreateMultisig: new(CreateMultisigDataResource), - transaction.TypeMultisend: new(MultiSendDataResource), - transaction.TypeEditCandidate: new(EditCandidateDataResource), - transaction.TypeSetHaltBlock: new(SetHaltBlockDataResource), - transaction.TypeRecreateCoin: new(RecreateCoinDataResource), - transaction.TypeEditCoinOwner: new(EditCoinOwnerDataResource), - transaction.TypeEditMultisig: new(EditMultisigResource), - transaction.TypePriceVote: new(PriceVoteResource), - transaction.TypeEditCandidatePublicKey: new(EditCandidatePublicKeyResource), -} - -func NewTxEncoderJSON(context *state.CheckState) *TxEncoderJSON { - return &TxEncoderJSON{context} -} - -func (encoder *TxEncoderJSON) Encode(transaction *transaction.Transaction, tmTx *coretypes.ResultTx) (json.RawMessage, error) { - sender, _ := transaction.Sender() - - // prepare transaction data resource - data, err := encoder.EncodeData(transaction) - if err != nil { - return nil, err - } - - // prepare transaction tags - tags := make(map[string]string) - for _, tag := range tmTx.TxResult.Events[0].Attributes { - tags[string(tag.Key)] = string(tag.Value) - } - - gasCoin := encoder.context.Coins().GetCoin(transaction.GasCoin) - txGasCoin := CoinResource{gasCoin.ID().Uint32(), gasCoin.GetFullSymbol()} - - tx := TransactionResponse{ - Hash: bytes.HexBytes(tmTx.Tx.Hash()).String(), - RawTx: fmt.Sprintf("%x", []byte(tmTx.Tx)), - Height: tmTx.Height, - Index: tmTx.Index, - From: sender.String(), - Nonce: transaction.Nonce, - Gas: transaction.Gas(), - GasPrice: transaction.GasPrice, - GasCoin: txGasCoin, - Type: uint8(transaction.Type), - Data: data, - Payload: transaction.Payload, - Tags: tags, - Code: tmTx.TxResult.Code, - Log: tmTx.TxResult.Log, - } - - return json.Marshal(tx) -} - -func (encoder *TxEncoderJSON) EncodeData(decodedTx *transaction.Transaction) ([]byte, error) { - if resource, exists := resourcesConfig[decodedTx.Type]; exists { - return json.Marshal( - resource.Transform(decodedTx.GetDecodedData(), encoder.context), - ) - } - - return nil, rpctypes.RPCError{Code: 500, Message: "unknown tx type"} -} diff --git a/core/transaction/encoder/resources.go b/core/transaction/encoder/resources.go deleted file mode 100644 index c1302ab54..000000000 --- a/core/transaction/encoder/resources.go +++ /dev/null @@ -1,401 +0,0 @@ -package encoder - -import ( - "encoding/base64" - "strconv" - - "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/core/transaction" - "github.com/MinterTeam/minter-go-node/core/types" -) - -// TxDataResource is an interface for preparing JSON representation of TxData -type TxDataResource interface { - Transform(txData interface{}, context *state.CheckState) TxDataResource -} - -// CoinResource is a JSON representation of a coin -type CoinResource struct { - ID uint32 `json:"id"` - Symbol string `json:"symbol"` -} - -// SendDataResource is JSON representation of TxType 0x01 -type SendDataResource struct { - Coin CoinResource `json:"coin"` - To string `json:"to"` - Value string `json:"value"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (SendDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.SendData) - coin := context.Coins().GetCoin(data.Coin) - - return SendDataResource{ - To: data.To.String(), - Value: data.Value.String(), - Coin: CoinResource{coin.ID().Uint32(), coin.GetFullSymbol()}, - } -} - -// SellCoinDataResource is JSON representation of TxType 0x02 -type SellCoinDataResource struct { - CoinToSell CoinResource `json:"coin_to_sell"` - ValueToSell string `json:"value_to_sell"` - CoinToBuy CoinResource `json:"coin_to_buy"` - MinimumValueToBuy string `json:"minimum_value_to_buy"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (SellCoinDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.SellCoinData) - buyCoin := context.Coins().GetCoin(data.CoinToBuy) - sellCoin := context.Coins().GetCoin(data.CoinToSell) - - return SellCoinDataResource{ - ValueToSell: data.ValueToSell.String(), - MinimumValueToBuy: data.MinimumValueToBuy.String(), - CoinToBuy: CoinResource{buyCoin.ID().Uint32(), buyCoin.GetFullSymbol()}, - CoinToSell: CoinResource{sellCoin.ID().Uint32(), sellCoin.GetFullSymbol()}, - } -} - -// SellAllCoinDataResource is JSON representation of TxType 0x03 -type SellAllCoinDataResource struct { - CoinToSell CoinResource `json:"coin_to_sell"` - CoinToBuy CoinResource `json:"coin_to_buy"` - MinimumValueToBuy string `json:"minimum_value_to_buy"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (SellAllCoinDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.SellAllCoinData) - buyCoin := context.Coins().GetCoin(data.CoinToBuy) - sellCoin := context.Coins().GetCoin(data.CoinToSell) - - return SellAllCoinDataResource{ - MinimumValueToBuy: data.MinimumValueToBuy.String(), - CoinToBuy: CoinResource{buyCoin.ID().Uint32(), buyCoin.GetFullSymbol()}, - CoinToSell: CoinResource{sellCoin.ID().Uint32(), sellCoin.GetFullSymbol()}, - } -} - -// BuyCoinDataResource is JSON representation of TxType 0x04 -type BuyCoinDataResource struct { - CoinToBuy CoinResource `json:"coin_to_buy"` - ValueToBuy string `json:"value_to_buy"` - CoinToSell CoinResource `json:"coin_to_sell"` - MaximumValueToSell string `json:"maximum_value_to_sell"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (BuyCoinDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.BuyCoinData) - buyCoin := context.Coins().GetCoin(data.CoinToBuy) - sellCoin := context.Coins().GetCoin(data.CoinToSell) - - return BuyCoinDataResource{ - ValueToBuy: data.ValueToBuy.String(), - MaximumValueToSell: data.MaximumValueToSell.String(), - CoinToBuy: CoinResource{buyCoin.ID().Uint32(), buyCoin.GetFullSymbol()}, - CoinToSell: CoinResource{sellCoin.ID().Uint32(), sellCoin.GetFullSymbol()}, - } -} - -// CreateCoinDataResource is JSON representation of TxType 0x05 -type CreateCoinDataResource struct { - Name string `json:"name"` - Symbol string `json:"symbol"` - InitialAmount string `json:"initial_amount"` - InitialReserve string `json:"initial_reserve"` - ConstantReserveRatio string `json:"constant_reserve_ratio"` - MaxSupply string `json:"max_supply"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (CreateCoinDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.CreateCoinData) - - return CreateCoinDataResource{ - Name: data.Name, - Symbol: data.Symbol.String(), - InitialAmount: data.InitialAmount.String(), - InitialReserve: data.InitialReserve.String(), - ConstantReserveRatio: strconv.Itoa(int(data.ConstantReserveRatio)), - MaxSupply: data.MaxSupply.String(), - } -} - -// DeclareCandidacyDataResource is JSON representation of TxType 0x06 -type DeclareCandidacyDataResource struct { - Address string `json:"address"` - PubKey string `json:"pub_key"` - Commission string `json:"commission"` - Coin CoinResource `json:"coin"` - Stake string `json:"stake"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (DeclareCandidacyDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.DeclareCandidacyData) - coin := context.Coins().GetCoin(data.Coin) - - return DeclareCandidacyDataResource{ - Address: data.Address.String(), - PubKey: data.PubKey.String(), - Commission: strconv.Itoa(int(data.Commission)), - Stake: data.Stake.String(), - Coin: CoinResource{coin.ID().Uint32(), coin.GetFullSymbol()}, - } -} - -// DelegateDataResource is JSON representation of TxType 0x07 -type DelegateDataResource struct { - PubKey string `json:"pub_key"` - Coin CoinResource `json:"coin"` - Value string `json:"value"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (DelegateDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.DelegateData) - coin := context.Coins().GetCoin(data.Coin) - - return DelegateDataResource{ - PubKey: data.PubKey.String(), - Value: data.Value.String(), - Coin: CoinResource{coin.ID().Uint32(), coin.GetFullSymbol()}, - } -} - -// UnbondDataResource is JSON representation of TxType 0x08 -type UnbondDataResource struct { - PubKey string `json:"pub_key"` - Coin CoinResource `json:"coin"` - Value string `json:"value"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (UnbondDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.UnbondData) - coin := context.Coins().GetCoin(data.Coin) - - return UnbondDataResource{ - PubKey: data.PubKey.String(), - Value: data.Value.String(), - Coin: CoinResource{coin.ID().Uint32(), coin.GetFullSymbol()}, - } -} - -// RedeemCheckDataResource is JSON representation of TxType 0x09 -type RedeemCheckDataResource struct { - RawCheck string `json:"raw_check"` - Proof string `json:"proof"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (RedeemCheckDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.RedeemCheckData) - - return RedeemCheckDataResource{ - RawCheck: base64.StdEncoding.EncodeToString(data.RawCheck), - Proof: base64.StdEncoding.EncodeToString(data.Proof[:]), - } -} - -// SetCandidateOnDataResource is JSON representation of TxType 0x0A -type SetCandidateOnDataResource struct { - PubKey string `json:"pub_key"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (SetCandidateOnDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.SetCandidateOnData) - return SetCandidateOnDataResource{data.PubKey.String()} -} - -// SetCandidateOffDataResource is JSON representation of TxType 0x0B -type SetCandidateOffDataResource struct { - PubKey string `json:"pub_key"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (SetCandidateOffDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.SetCandidateOffData) - return SetCandidateOffDataResource{data.PubKey.String()} -} - -// CreateMultisigDataResource is JSON representation of TxType 0x0C -type CreateMultisigDataResource struct { - Threshold string `json:"threshold"` - Weights []string `json:"weights"` - Addresses []types.Address `json:"addresses"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (CreateMultisigDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.CreateMultisigData) - - var weights []string - for _, weight := range data.Weights { - weights = append(weights, strconv.Itoa(int(weight))) - } - - return CreateMultisigDataResource{ - Threshold: strconv.Itoa(int(data.Threshold)), - Weights: weights, - Addresses: data.Addresses, - } -} - -// MultiSendDataResource is JSON representation of TxType 0x0D -type MultiSendDataResource struct { - List []SendDataResource `json:"list"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (resource MultiSendDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.MultisendData) - - for _, v := range data.List { - coin := context.Coins().GetCoin(v.Coin) - - resource.List = append(resource.List, SendDataResource{ - Coin: CoinResource{coin.ID().Uint32(), coin.GetFullSymbol()}, - To: v.To.String(), - Value: v.Value.String(), - }) - } - - return resource -} - -// EditCandidateDataResource is JSON representation of TxType 0x0E -type EditCandidateDataResource struct { - PubKey string `json:"pub_key"` - RewardAddress string `json:"reward_address"` - OwnerAddress string `json:"owner_address"` - ControlAddress string `json:"control_address"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (EditCandidateDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.EditCandidateData) - return EditCandidateDataResource{ - PubKey: data.PubKey.String(), - RewardAddress: data.RewardAddress.String(), - OwnerAddress: data.OwnerAddress.String(), - ControlAddress: data.ControlAddress.String(), - } -} - -// SetHaltBlockDataResource is JSON representation of TxType 0x0F -type SetHaltBlockDataResource struct { - PubKey string `json:"pub_key"` - Height string `json:"height"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (SetHaltBlockDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.SetHaltBlockData) - - return SetHaltBlockDataResource{ - PubKey: data.PubKey.String(), - Height: strconv.FormatUint(data.Height, 10), - } -} - -// RecreateCoinDataResource is JSON representation of TxType 0x10 -type RecreateCoinDataResource struct { - Name string `json:"name"` - Symbol types.CoinSymbol `json:"symbol"` - InitialAmount string `json:"initial_amount"` - InitialReserve string `json:"initial_reserve"` - ConstantReserveRatio string `json:"constant_reserve_ratio"` - MaxSupply string `json:"max_supply"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (RecreateCoinDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.RecreateCoinData) - - return RecreateCoinDataResource{ - Name: data.Name, - Symbol: data.Symbol, - InitialAmount: data.InitialAmount.String(), - InitialReserve: data.InitialReserve.String(), - ConstantReserveRatio: strconv.Itoa(int(data.ConstantReserveRatio)), - MaxSupply: data.MaxSupply.String(), - } -} - -// EditCoinOwnerDataResource is JSON representation of TxType 0x11 -type EditCoinOwnerDataResource struct { - Symbol types.CoinSymbol `json:"symbol"` - NewOwner types.Address `json:"new_owner"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (EditCoinOwnerDataResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.EditCoinOwnerData) - - return EditCoinOwnerDataResource{ - Symbol: data.Symbol, - NewOwner: data.NewOwner, - } -} - -// EditMultisigResource is JSON representation of TxType 0x12 -type EditMultisigResource struct { - Threshold string `json:"threshold"` - Weights []string `json:"weights"` - Addresses []types.Address `json:"addresses"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (EditMultisigResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.EditMultisigData) - - resource := EditMultisigResource{ - Addresses: data.Addresses, - Threshold: strconv.Itoa(int(data.Threshold)), - } - - resource.Weights = make([]string, 0, len(data.Weights)) - for _, weight := range data.Weights { - resource.Weights = append(resource.Weights, strconv.Itoa(int(weight))) - } - - return resource -} - -// PriceVoteResource is JSON representation of TxType 0x13 -type PriceVoteResource struct { - Price uint32 `json:"price"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (PriceVoteResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.PriceVoteData) - - return PriceVoteResource{ - Price: uint32(data.Price), - } -} - -// EditCandidatePublicKeyResource is JSON representation of TxType 0x14 -type EditCandidatePublicKeyResource struct { - PubKey string `json:"pub_key"` - NewPubKey string `json:"new_pub_key"` -} - -// Transform returns TxDataResource from given txData. Used for JSON encoder. -func (EditCandidatePublicKeyResource) Transform(txData interface{}, context *state.CheckState) TxDataResource { - data := txData.(*transaction.EditCandidatePublicKeyData) - - return EditCandidatePublicKeyResource{ - PubKey: data.PubKey.String(), - NewPubKey: data.NewPubKey.String(), - } -} diff --git a/core/transaction/executor.go b/core/transaction/executor.go index 148f119a6..4926f91c7 100644 --- a/core/transaction/executor.go +++ b/core/transaction/executor.go @@ -1,8 +1,10 @@ package transaction import ( + "encoding/hex" "encoding/json" "fmt" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" "strconv" "sync" @@ -10,35 +12,29 @@ import ( "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/tendermint/tendermint/libs/kv" ) const ( - maxTxLength = 7168 - maxPayloadLength = 1024 + maxPayloadLength = 10000 + maxTxLength = 6144 + maxPayloadLength maxServiceDataLength = 128 stdGas = 5000 ) // Response represents standard response from tx delivery/check type Response struct { - Code uint32 `json:"code,omitempty"` - Data []byte `json:"data,omitempty"` - Log string `json:"log,omitempty"` - Info string `json:"-"` - GasWanted int64 `json:"gas_wanted,omitempty"` - GasUsed int64 `json:"gas_used,omitempty"` - Tags []kv.Pair `json:"tags,omitempty"` - GasPrice uint32 `json:"gas_price"` + Code uint32 `json:"code,omitempty"` + Data []byte `json:"data,omitempty"` + Log string `json:"log,omitempty"` + Info string `json:"-"` + GasWanted int64 `json:"gas_wanted,omitempty"` + GasUsed int64 `json:"gas_used,omitempty"` + Tags []abcTypes.EventAttribute `json:"tags,omitempty"` + GasPrice uint32 `json:"gas_price"` } // RunTx executes transaction in given context -func RunTx(context state.Interface, - rawTx []byte, - rewardPool *big.Int, - currentBlock uint64, - currentMempool *sync.Map, - minGasPrice uint32) Response { +func RunTx(context state.Interface, rawTx []byte, rewardPool *big.Int, currentBlock uint64, currentMempool *sync.Map, minGasPrice uint32) Response { lenRawTx := len(rawTx) if lenRawTx > maxTxLength { return Response{ @@ -48,7 +44,7 @@ func RunTx(context state.Interface, } } - tx, err := TxDecoder.DecodeFromBytes(rawTx) + tx, err := DecodeFromBytes(rawTx) if err != nil { return Response{ Code: code.DecodeError, @@ -193,19 +189,32 @@ func RunTx(context state.Interface, } } - response := tx.decodedData.Run(tx, context, rewardPool, currentBlock) + commissions := checkState.Commission().GetCommissions() + price := tx.Price(commissions) + coinCommission := abcTypes.EventAttribute{Key: []byte("tx.commission_price_coin"), Value: []byte(strconv.Itoa(int(commissions.Coin)))} + priceCommission := abcTypes.EventAttribute{Key: []byte("tx.commission_price"), Value: []byte(price.String())} + + if !commissions.Coin.IsBaseCoin() { + price = checkState.Swap().GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(price) + } + + response := tx.decodedData.Run(tx, context, rewardPool, currentBlock, price) if response.Code != code.TxFromSenderAlreadyInMempool && response.Code != code.OK { currentMempool.Delete(sender) } response.GasPrice = tx.GasPrice - - switch tx.Type { - case TypeCreateCoin, TypeEditCoinOwner, TypeRecreateCoin, TypeEditCandidatePublicKey: - response.GasUsed = stdGas - response.GasWanted = stdGas - } + gas := tx.Gas() + response.Tags = append(response.Tags, + coinCommission, + priceCommission, + abcTypes.EventAttribute{Key: []byte("tx.gas"), Value: []byte(strconv.Itoa(int(gas)))}, + abcTypes.EventAttribute{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:])), Index: true}, + abcTypes.EventAttribute{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(tx.decodedData.TxType())})), Index: true}, + ) + response.GasUsed = gas + response.GasWanted = gas return response } diff --git a/core/transaction/executor_test.go b/core/transaction/executor_test.go index 2794ebea9..c8deceb8f 100644 --- a/core/transaction/executor_test.go +++ b/core/transaction/executor_test.go @@ -14,7 +14,8 @@ import ( ) func TestTooLongTx(t *testing.T) { - fakeTx := make([]byte, 10000) + t.Parallel() + fakeTx := make([]byte, maxTxLength+1) cState := getState() response := RunTx(cState, fakeTx, big.NewInt(0), 0, &sync.Map{}, 0) @@ -22,10 +23,13 @@ func TestTooLongTx(t *testing.T) { t.Fatalf("Response code is not correct") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestIncorrectTx(t *testing.T) { + t.Parallel() fakeTx := make([]byte, 1) rand.Read(fakeTx) @@ -35,11 +39,14 @@ func TestIncorrectTx(t *testing.T) { t.Fatalf("Response code is not correct") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestTooLongPayloadTx(t *testing.T) { - payload := make([]byte, 1025) + t.Parallel() + payload := make([]byte, maxPayloadLength+1) rand.Read(payload) txData := SendData{ @@ -78,10 +85,13 @@ func TestTooLongPayloadTx(t *testing.T) { t.Fatalf("Response code is not correct. Expected %d, got %d", code.TxPayloadTooLarge, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestTooLongServiceDataTx(t *testing.T) { + t.Parallel() serviceData := make([]byte, 1025) rand.Read(serviceData) @@ -120,10 +130,13 @@ func TestTooLongServiceDataTx(t *testing.T) { t.Fatalf("Response code is not correct. Expected %d, got %d", code.TxServiceDataTooLarge, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnexpectedNonceTx(t *testing.T) { + t.Parallel() txData := SendData{ Coin: types.GetBaseCoinID(), To: types.Address{}, @@ -157,10 +170,13 @@ func TestUnexpectedNonceTx(t *testing.T) { t.Fatalf("Response code is not correct. Expected %d, got %d", code.WrongNonce, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestInvalidSigTx(t *testing.T) { + t.Parallel() txData := SendData{ Coin: types.GetBaseCoinID(), To: types.Address{}, @@ -198,10 +214,13 @@ func TestInvalidSigTx(t *testing.T) { t.Fatalf("Response code is not correct. Expected %d, got %d", code.DecodeError, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestNotExistMultiSigTx(t *testing.T) { + t.Parallel() txData := SendData{ Coin: types.GetBaseCoinID(), To: types.Address{}, @@ -240,10 +259,13 @@ func TestNotExistMultiSigTx(t *testing.T) { t.Fatalf("Response code is not correct. Expected %d, got %d", code.MultisigNotExists, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultiSigTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -286,10 +308,13 @@ func TestMultiSigTx(t *testing.T) { t.Fatalf("Error code is not 0. Error: %s", response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultiSigDoubleSignTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -336,10 +361,13 @@ func TestMultiSigDoubleSignTx(t *testing.T) { t.Fatalf("Error code is not %d, got %d", code.DuplicatedAddresses, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultiSigTooManySignsTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -389,10 +417,13 @@ func TestMultiSigTooManySignsTx(t *testing.T) { t.Fatalf("Error code is not %d, got %d", code.IncorrectMultiSignature, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultiSigNotEnoughTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -435,10 +466,13 @@ func TestMultiSigNotEnoughTx(t *testing.T) { t.Fatalf("Error code is not %d. Error: %d", code.NotEnoughMultisigVotes, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultiSigIncorrectSignsTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -482,5 +516,7 @@ func TestMultiSigIncorrectSignsTx(t *testing.T) { t.Fatalf("Error code is not %d, got %d", code.IncorrectMultiSignature, response.Code) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/mint_coin.go b/core/transaction/mint_coin.go new file mode 100644 index 000000000..0b389ec23 --- /dev/null +++ b/core/transaction/mint_coin.go @@ -0,0 +1,134 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +type MintTokenData struct { + Coin types.CoinID + Value *big.Int +} + +func (data MintTokenData) Gas() int { + return gasMintToken +} +func (data MintTokenData) TxType() TxType { + return TypeMintToken +} + +func (data MintTokenData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + coin := context.Coins().GetCoin(data.Coin) + if coin == nil { + return &Response{ + Code: code.CoinNotExists, + Log: "Coin not exists", + Info: EncodeError(code.NewCoinNotExists("", data.Coin.String())), + } + } + + if !coin.IsMintable() { + return &Response{ + Code: code.CoinNotMintable, + Log: "Coin not mintable", + Info: EncodeError(code.NewCoinIsNotMintable(coin.GetFullSymbol(), data.Coin.String())), + } + } + + if big.NewInt(0).Add(coin.Volume(), data.Value).Cmp(coin.MaxSupply()) == 1 { + return &Response{ + Code: code.WrongCoinEmission, + Log: fmt.Sprintf("Coin volume should be less than %s", coin.MaxSupply()), + Info: EncodeError(code.NewWrongCoinEmission("", coin.MaxSupply().String(), coin.Volume().String(), data.Value.String(), "")), + } + } + + sender, _ := tx.Sender() + symbolInfo := context.Coins().GetSymbolInfo(coin.Symbol()) + if coin.Version() != 0 || symbolInfo == nil || symbolInfo.OwnerAddress().Compare(sender) != 0 { + var owner *string + if symbolInfo != nil && symbolInfo.OwnerAddress() != nil { + own := symbolInfo.OwnerAddress().String() + owner = &own + } + return &Response{ + Code: code.IsNotOwnerOfCoin, + Log: "Sender is not owner of coin", + Info: EncodeError(code.NewIsNotOwnerOfCoin(coin.Symbol().String(), owner)), + } + } + + return nil +} + +func (data MintTokenData) String() string { + return fmt.Sprintf("MINT COIN: %d", data.Coin) +} + +func (data MintTokenData) CommissionData(price *commission.Price) *big.Int { + return price.MintToken +} + +func (data MintTokenData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + deliverState.Coins.AddVolume(data.Coin, data.Value) + deliverState.Accounts.AddBalance(sender, data.Coin, data.Value) + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_id"), Value: []byte(data.Coin.String()), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/mint_token_test.go b/core/transaction/mint_token_test.go new file mode 100644 index 000000000..b4445c27c --- /dev/null +++ b/core/transaction/mint_token_test.go @@ -0,0 +1,187 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" + "testing" +) + +func TestMintData_aaa(t *testing.T) { + t.Parallel() + cState := getState() + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + coin := types.GetBaseCoinID() + + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) + + toCreate := types.StrToCoinSymbol("TOKEN1") + amount := helpers.BipToPip(big.NewInt(100)) + name := "My Test Coin" + { + data := CreateTokenData{ + Name: name, + Symbol: toCreate, + InitialAmount: amount, + MaxSupply: big.NewInt(0).Mul(amount, big.NewInt(10)), + Mintable: true, + Burnable: false, + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeCreateToken, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error %s", response.Log) + } + + _, _, err = cState.Tree().Commit(cState.Coins) + if err != nil { + t.Fatalf("Commit coins failed. Error %s", err) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + + targetBalance, _ := big.NewInt(0).SetString("999000000000000000000000", 10) + balance := cState.Accounts.GetBalance(addr, coin) + if balance.Cmp(targetBalance) != 0 { + t.Errorf("Target %s balance is not correct. Expected %s, got %s", coin, targetBalance, balance) + } + + stateCoin := cState.Coins.GetCoinBySymbol(toCreate, 0) + + if stateCoin == nil { + t.Fatalf("Coin %s not found in state", toCreate) + } + + if stateCoin.Volume().Cmp(amount) != 0 { + t.Fatalf("Volume in state is not correct. Expected %s, got %s", amount, stateCoin.MaxSupply()) + } + + if stateCoin.Name() != name { + t.Fatalf("Name in state is not correct. Expected %s, got %s", name, stateCoin.Name()) + } + + if stateCoin.Version() != 0 { + t.Fatalf("Version in state is not correct. Expected %d, got %d", 0, stateCoin.Version()) + } + + symbolInfo := cState.Coins.GetSymbolInfo(toCreate) + if symbolInfo == nil { + t.Fatalf("Symbol %s info not found in state", toCreate) + } + + if *symbolInfo.OwnerAddress() != addr { + t.Fatalf("Target owner address is not correct. Expected %s, got %s", addr.String(), symbolInfo.OwnerAddress().String()) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + stateCoin := cState.Coins.GetCoinBySymbol(toCreate, 0) + + if stateCoin == nil { + t.Fatalf("Coin %s not found in state", toCreate) + } + + if stateCoin.Volume().Cmp(amount) != 0 { + t.Fatalf("Volume in state is not correct. Expected %s, got %s", amount, stateCoin.MaxSupply()) + } + + subVolume := big.NewInt(1e18) + data := MintTokenData{ + Coin: stateCoin.ID(), + Value: big.NewInt(0).Set(subVolume), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeMintToken, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error %s", response.Log) + } + + _, _, err = cState.Tree().Commit(cState.Coins) + if err != nil { + t.Fatalf("Commit coins failed. Error %s", err) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + + stateCoin = cState.Coins.GetCoinBySymbol(toCreate, 0) + + if stateCoin == nil { + t.Fatalf("Coin %s not found in state", toCreate) + } + + amount.Add(amount, subVolume) + + if stateCoin.Volume().Cmp(amount) != 0 { + t.Errorf("Volume in state is not correct. Expected %s, got %s", amount, stateCoin.MaxSupply()) + } + + balance := cState.Accounts.GetBalance(addr, stateCoin.ID()) + if balance.Cmp(amount) != 0 { + t.Errorf("Target %s balance is not correct. Expected %s, got %s", stateCoin.ID(), amount, balance) + } + } +} diff --git a/core/transaction/move_stake.go b/core/transaction/move_stake.go new file mode 100644 index 000000000..8b7802688 --- /dev/null +++ b/core/transaction/move_stake.go @@ -0,0 +1,159 @@ +package transaction + +import ( + "encoding/hex" + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +type MoveStakeData struct { + From, To types.Pubkey + Coin types.CoinID + Stake *big.Int +} + +func (data MoveStakeData) Gas() int { + return gasMoveStake +} +func (data MoveStakeData) TxType() TxType { + return TypeMoveStake +} + +func (data MoveStakeData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if !context.Coins().Exists(data.Coin) { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", data.Coin), + Info: EncodeError(code.NewCoinNotExists("", data.Coin.String())), + } + } + + if !context.Candidates().Exists(data.From) { + return &Response{ + Code: code.CandidateNotFound, + Log: fmt.Sprintf("Candidate with %s public key not found", data.From), + Info: EncodeError(code.NewCandidateNotFound(data.From.String())), + } + } + if !context.Candidates().Exists(data.To) { + return &Response{ + Code: code.CandidateNotFound, + Log: fmt.Sprintf("Candidate with %s public key not found", data.To), + Info: EncodeError(code.NewCandidateNotFound(data.To.String())), + } + } + + sender, _ := tx.Sender() + + if waitlist := context.WaitList().Get(sender, data.From, data.Coin); waitlist != nil { + if data.Stake.Cmp(waitlist.Value) == 1 { + return &Response{ + Code: code.InsufficientWaitList, + Log: "Insufficient amount at waitlist for sender account", + Info: EncodeError(code.NewInsufficientWaitList(waitlist.Value.String(), data.Stake.String())), + } + } + } else { + stake := context.Candidates().GetStakeValueOfAddress(data.From, sender, data.Coin) + + if stake == nil { + return &Response{ + Code: code.StakeNotFound, + Log: "Stake of current user not found", + Info: EncodeError(code.NewStakeNotFound(data.From.String(), sender.String(), data.Coin.String(), context.Coins().GetCoin(data.Coin).GetFullSymbol())), + } + } + + if stake.Cmp(data.Stake) == -1 { + return &Response{ + Code: code.InsufficientStake, + Log: "Insufficient stake for sender account", + Info: EncodeError(code.NewInsufficientStake(data.From.String(), sender.String(), data.Coin.String(), context.Coins().GetCoin(data.Coin).GetFullSymbol(), stake.String(), data.Stake.String())), + } + } + } + + return nil +} + +func (data MoveStakeData) String() string { + return fmt.Sprintf("MOVE STAKE") +} + +func (data MoveStakeData) CommissionData(price *commission.Price) *big.Int { + return price.MoveStake +} + +func (data MoveStakeData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + if waitList := deliverState.Waitlist.Get(sender, data.From, data.Coin); waitList != nil { + diffValue := big.NewInt(0).Sub(data.Stake, waitList.Value) + deliverState.Waitlist.Delete(sender, data.From, data.Coin) + if diffValue.Sign() == -1 { + deliverState.Waitlist.AddWaitList(sender, data.From, data.Coin, big.NewInt(0).Neg(diffValue)) + } + } else { + deliverState.Candidates.SubStake(sender, data.From, data.Coin, data.Stake) + } + + moveToCandidateId := deliverState.Candidates.ID(data.To) + deliverState.FrozenFunds.AddFund(currentBlock+types.GetUnbondPeriod(), sender, data.From, deliverState.Candidates.ID(data.From), data.Coin, data.Stake, &moveToCandidateId) + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/multisend.go b/core/transaction/multisend.go index 0d018cc74..399cb096e 100644 --- a/core/transaction/multisend.go +++ b/core/transaction/multisend.go @@ -4,23 +4,29 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" "math/big" "sort" "strings" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" ) type MultisendData struct { List []MultisendDataItem `json:"list"` } -func (data MultisendData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data MultisendData) Gas() int { + return gasMultisendBase + gasMultisendDelta*len(data.List) +} +func (data MultisendData) TxType() TxType { + return TypeMultisend +} + +func (data MultisendData) basicCheck(tx *Transaction, context *state.CheckState) *Response { quantity := len(data.List) if quantity < 1 || quantity > 100 { return &Response{ @@ -58,11 +64,11 @@ func (data MultisendData) String() string { return "MULTISEND" } -func (data MultisendData) Gas() int64 { - return commissions.SendTx + ((int64(len(data.List)) - 1) * commissions.MultisendDelta) +func (data MultisendData) CommissionData(price *commission.Price) *big.Int { + return big.NewInt(0).Add(price.MultisendBase, big.NewInt(0).Mul(big.NewInt(int64(len(data.List))-1), price.MultisendDelta)) } -func (data MultisendData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data MultisendData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -71,54 +77,55 @@ func (data MultisendData) Run(tx *Transaction, context state.Interface, rewardPo checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - coin := checkState.Coins().GetCoin(tx.GasCoin) - - errResp := CheckReserveUnderflow(coin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), commissionInBaseCoin) + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if errResp := checkBalances(checkState, sender, data.List, commission, tx.GasCoin); errResp != nil { return *errResp } + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubVolume(tx.GasCoin, commission) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) for _, item := range data.List { deliverState.Accounts.SubBalance(sender, item.Coin, item.Value) deliverState.Accounts.AddBalance(item.To, item.Coin, item.Value) } deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeMultisend)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.to"), Value: []byte(pluckRecipients(data.List))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + } + + for _, dataItem := range data.List { + tags = append(tags, abcTypes.EventAttribute{Key: []byte("tx.to"), Value: []byte(hex.EncodeToString(dataItem.To[:])), Index: true}) + } + + tags = append(tags, abcTypes.EventAttribute{Key: []byte("tx.to"), Value: []byte(pluckRecipients(data.List))}) } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/multisend_test.go b/core/transaction/multisend_test.go index e981bdf48..583e8cf5a 100644 --- a/core/transaction/multisend_test.go +++ b/core/transaction/multisend_test.go @@ -13,6 +13,7 @@ import ( ) func TestMultisendTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -78,10 +79,13 @@ func TestMultisendTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", to.String(), targetTestBalance, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultisendTxToInvalidDataLength(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -150,10 +154,13 @@ func TestMultisendTxToInvalidDataLength(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InvalidMultisendData, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultisendTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -201,10 +208,13 @@ func TestMultisendTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultisendToInvalidCoin(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -255,10 +265,13 @@ func TestMultisendToInvalidCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultisendToInsufficientReserve(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -309,10 +322,13 @@ func TestMultisendToInsufficientReserve(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestMultisendTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -362,9 +378,11 @@ func TestMultisendTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/price_vote.go b/core/transaction/price_vote.go index 4b354ae30..a7e2e8cca 100644 --- a/core/transaction/price_vote.go +++ b/core/transaction/price_vote.go @@ -4,18 +4,25 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) type PriceVoteData struct { - Price uint + Price uint32 } -func (data PriceVoteData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data PriceVoteData) Gas() int { + return gasPriceVote +} +func (data PriceVoteData) TxType() TxType { + return TypePriceVote +} + +func (data PriceVoteData) basicCheck(tx *Transaction, context *state.CheckState) *Response { return nil } @@ -23,11 +30,11 @@ func (data PriceVoteData) String() string { return fmt.Sprintf("PRICE VOTE price: %d", data.Price) } -func (data PriceVoteData) Gas() int64 { - return commissions.PriceVoteData +func (data PriceVoteData) CommissionData(price *commission.Price) *big.Int { + return price.PriceVote } -func (data PriceVoteData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data PriceVoteData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -36,23 +43,17 @@ func (data PriceVoteData) Run(tx *Transaction, context state.Interface, rewardPo checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -63,25 +64,28 @@ func (data PriceVoteData) Run(tx *Transaction, context state.Interface, rewardPo } } + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubVolume(tx.GasCoin, commission) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypePriceVote)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/price_vote_test.go b/core/transaction/price_vote_test.go index 05652ddbf..492b84448 100644 --- a/core/transaction/price_vote_test.go +++ b/core/transaction/price_vote_test.go @@ -1,127 +1,137 @@ package transaction -import ( - "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/helpers" - "github.com/MinterTeam/minter-go-node/rlp" - "math/big" - "sync" - "testing" -) - -func TestPriceVoteTx(t *testing.T) { - cState := getState() - privateKey, addr := getAccount() - - cState.Accounts.AddBalance(addr, types.GetBaseCoinID(), big.NewInt(1e18)) - - data := PriceVoteData{Price: 1} - encodedData, err := rlp.EncodeToBytes(data) - if err != nil { - t.Fatal(err) - } - - tx := Transaction{ - Nonce: 1, - GasPrice: 1, - ChainID: types.CurrentChainID, - GasCoin: types.GetBaseCoinID(), - Type: TypePriceVote, - Data: encodedData, - SignatureType: SigTypeSingle, - } - - if err := tx.Sign(privateKey); err != nil { - t.Fatal(err) - } - - encodedTx, err := rlp.EncodeToBytes(tx) - if err != nil { - t.Fatal(err) - } - - response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != 0 { - t.Fatalf("Response code is not 0. Error: %s", response.Log) - } - - checkState(t, cState) -} - -func TestPriceVoteTxToInsufficientFunds(t *testing.T) { - cState := getState() - privateKey, _ := getAccount() - - data := PriceVoteData{Price: 1} - encodedData, err := rlp.EncodeToBytes(data) - if err != nil { - t.Fatal(err) - } - - tx := Transaction{ - Nonce: 1, - GasPrice: 1, - ChainID: types.CurrentChainID, - GasCoin: types.GetBaseCoinID(), - Type: TypePriceVote, - Data: encodedData, - SignatureType: SigTypeSingle, - } - - if err := tx.Sign(privateKey); err != nil { - t.Fatal(err) - } - - encodedTx, err := rlp.EncodeToBytes(tx) - if err != nil { - t.Fatal(err) - } - - response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.InsufficientFunds { - t.Fatalf("Response code is not %d. Error: %s", code.InsufficientFunds, response.Log) - } - - checkState(t, cState) -} - -func TestPriceVoteTxToCoinReserveUnderflow(t *testing.T) { - cState := getState() - customCoin := createTestCoin(cState) - privateKey, _ := getAccount() - - cState.Coins.SubReserve(customCoin, helpers.BipToPip(big.NewInt(90000))) - - data := PriceVoteData{Price: 1} - encodedData, err := rlp.EncodeToBytes(data) - if err != nil { - t.Fatal(err) - } - - tx := Transaction{ - Nonce: 1, - GasPrice: 1, - ChainID: types.CurrentChainID, - GasCoin: customCoin, - Type: TypePriceVote, - Data: encodedData, - SignatureType: SigTypeSingle, - } - - if err := tx.Sign(privateKey); err != nil { - t.Fatal(err) - } - - encodedTx, err := rlp.EncodeToBytes(tx) - if err != nil { - t.Fatal(err) - } - - response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error: %s", code.CoinReserveUnderflow, response.Log) - } - - checkState(t, cState) -} +// +// import ( +// "github.com/MinterTeam/minter-go-node/core/code" +// "github.com/MinterTeam/minter-go-node/core/types" +// "github.com/MinterTeam/minter-go-node/helpers" +// "github.com/MinterTeam/minter-go-node/rlp" +// "math/big" +// "sync" +// "testing" +// ) +// +// func TestPriceVoteTx(t *testing.T) { +// t.Parallel() +// cState := getState() +// privateKey, addr := getAccount() +// +// cState.Accounts.AddBalance(addr, types.GetBaseCoinID(), big.NewInt(1e18)) +// +// data := PriceVoteData{Price: 1} +// encodedData, err := rlp.EncodeToBytes(data) +// if err != nil { +// t.Fatal(err) +// } +// +// tx := Transaction{ +// Nonce: 1, +// GasPrice: 1, +// ChainID: types.CurrentChainID, +// GasCoin: types.GetBaseCoinID(), +// Type: TypePriceVote, +// Data: encodedData, +// SignatureType: SigTypeSingle, +// } +// +// if err := tx.Sign(privateKey); err != nil { +// t.Fatal(err) +// } +// +// encodedTx, err := rlp.EncodeToBytes(tx) +// if err != nil { +// t.Fatal(err) +// } +// +// response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) +// if response.Code != 1 { +// t.Fatalf("Response code is not 0. Error: %s", response.Log) +// } +// +// if err := checkState(cState); err != nil { +// t.Error(err) +// } +// } +// +// func TestPriceVoteTxToInsufficientFunds(t *testing.T) { +// t.Parallel() +// cState := getState() +// privateKey, _ := getAccount() +// +// data := PriceVoteData{Price: 1} +// encodedData, err := rlp.EncodeToBytes(data) +// if err != nil { +// t.Fatal(err) +// } +// +// tx := Transaction{ +// Nonce: 1, +// GasPrice: 1, +// ChainID: types.CurrentChainID, +// GasCoin: types.GetBaseCoinID(), +// Type: TypePriceVote, +// Data: encodedData, +// SignatureType: SigTypeSingle, +// } +// +// if err := tx.Sign(privateKey); err != nil { +// t.Fatal(err) +// } +// +// encodedTx, err := rlp.EncodeToBytes(tx) +// if err != nil { +// t.Fatal(err) +// } +// +// response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) +// if response.Code != code.InsufficientFunds { +// t.Fatalf("Response code is not %d. Error: %s", code.InsufficientFunds, response.Log) +// } +// +// if err := checkState(cState); err != nil { +// t.Error(err) +// } +// } +// +// func TestPriceVoteTxToCoinReserveUnderflow(t *testing.T) { +// t.Parallel() +// cState := getState() +// customCoin := createTestCoin(cState) +// privateKey, _ := getAccount() +// +// cState.Coins.SubReserve(customCoin, helpers.BipToPip(big.NewInt(90000))) +// +// data := PriceVoteData{Price: 1} +// encodedData, err := rlp.EncodeToBytes(data) +// if err != nil { +// t.Fatal(err) +// } +// +// tx := Transaction{ +// Nonce: 1, +// GasPrice: 1, +// ChainID: types.CurrentChainID, +// GasCoin: customCoin, +// Type: TypePriceVote, +// Data: encodedData, +// SignatureType: SigTypeSingle, +// } +// +// if err := tx.Sign(privateKey); err != nil { +// t.Fatal(err) +// } +// +// encodedTx, err := rlp.EncodeToBytes(tx) +// if err != nil { +// t.Fatal(err) +// } +// +// response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) +// if response.Code != code.CommissionCoinNotSufficient { +// t.Fatalf("Response code is not %d. Error: %s", code.CommissionCoinNotSufficient, response.Log) +// } +// +// if err := checkState(cState); err != nil { +// t.Error(err) +// } +// } diff --git a/core/transaction/recreate_coin.go b/core/transaction/recreate_coin.go index 4ef0635a4..d395a22f8 100644 --- a/core/transaction/recreate_coin.go +++ b/core/transaction/recreate_coin.go @@ -1,17 +1,15 @@ package transaction import ( - "encoding/hex" "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" "math/big" "strconv" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" ) type RecreateCoinData struct { @@ -23,7 +21,14 @@ type RecreateCoinData struct { MaxSupply *big.Int } -func (data RecreateCoinData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data RecreateCoinData) Gas() int { + return gasRecreateCoin +} +func (data RecreateCoinData) TxType() TxType { + return TypeRecreateCoin +} + +func (data RecreateCoinData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.InitialReserve == nil || data.InitialAmount == nil || data.MaxSupply == nil { return &Response{ Code: code.DecodeError, @@ -40,19 +45,11 @@ func (data RecreateCoinData) BasicCheck(tx *Transaction, context *state.CheckSta } } - if data.ConstantReserveRatio < 10 || data.ConstantReserveRatio > 100 { - return &Response{ - Code: code.WrongCrr, - Log: "Constant Reserve Ratio should be between 10 and 100", - Info: EncodeError(code.NewWrongCrr("10", "100", strconv.Itoa(int(data.ConstantReserveRatio)))), - } - } - if data.InitialAmount.Cmp(minCoinSupply) == -1 || data.InitialAmount.Cmp(data.MaxSupply) == 1 { return &Response{ Code: code.WrongCoinSupply, Log: fmt.Sprintf("Coin supply should be between %s and %s", minCoinSupply.String(), data.MaxSupply.String()), - Info: EncodeError(code.NewWrongCoinSupply(maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), minCoinSupply.String(), data.MaxSupply.String(), data.InitialAmount.String())), + Info: EncodeError(code.NewWrongCoinSupply(minCoinSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), data.InitialAmount.String())), } } @@ -60,7 +57,7 @@ func (data RecreateCoinData) BasicCheck(tx *Transaction, context *state.CheckSta return &Response{ Code: code.WrongCoinSupply, Log: fmt.Sprintf("Max coin supply should be less than %s", maxCoinSupply), - Info: EncodeError(code.NewWrongCoinSupply(maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), minCoinSupply.String(), data.MaxSupply.String(), data.InitialAmount.String())), + Info: EncodeError(code.NewWrongCoinSupply(minCoinSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), data.InitialAmount.String())), } } @@ -68,7 +65,24 @@ func (data RecreateCoinData) BasicCheck(tx *Transaction, context *state.CheckSta return &Response{ Code: code.WrongCoinSupply, Log: fmt.Sprintf("Coin reserve should be greater than or equal to %s", minCoinReserve.String()), - Info: EncodeError(code.NewWrongCoinSupply(maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), minCoinSupply.String(), data.MaxSupply.String(), data.InitialAmount.String())), + Info: EncodeError(map[string]string{ + "code": strconv.Itoa(int(code.WrongCoinSupply)), + "min_initial_reserve": minCoinReserve.String(), + "current_initial_reserve": data.InitialReserve.String(), + })} + } + if data.ConstantReserveRatio < 10 || data.ConstantReserveRatio > 100 { + return &Response{ + Code: code.WrongCrr, + Log: "Constant Reserve Ratio should be between 10 and 100", + Info: EncodeError(code.NewWrongCrr("10", "100", strconv.Itoa(int(data.ConstantReserveRatio)))), + } + } + if data.InitialReserve.Cmp(minCoinReserve) == -1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Coin reserve should be greater than or equal to %s", minCoinReserve.String()), + Info: EncodeError(code.NewWrongCoinSupply(minCoinSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), minCoinReserve.String(), data.InitialReserve.String(), data.InitialAmount.String())), } } @@ -105,11 +119,11 @@ func (data RecreateCoinData) String() string { data.Symbol.String(), data.InitialReserve, data.InitialAmount, data.ConstantReserveRatio) } -func (data RecreateCoinData) Gas() int64 { - return commissions.RecreateCoin +func (data RecreateCoinData) CommissionData(price *commission.Price) *big.Int { + return price.RecreateCoin } -func (data RecreateCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data RecreateCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -118,23 +132,17 @@ func (data RecreateCoinData) Run(tx *Transaction, context state.Interface, rewar checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if tx.GasCoin != types.GetBaseCoinID() { - gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -170,16 +178,22 @@ func (data RecreateCoinData) Run(tx *Transaction, context state.Interface, rewar } } } - oldCoinID := checkState.Coins().GetCoinBySymbol(data.Symbol, 0).ID() - var coinId = checkState.App().GetNextCoinID() + + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { rewardPool.Add(rewardPool, commissionInBaseCoin) - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - - deliverState.Accounts.SubBalance(sender, types.GetBaseCoinID(), data.InitialReserve) + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + deliverState.Accounts.SubBalance(sender, types.GetBaseCoinID(), data.InitialReserve) + + oldCoinID := checkState.Coins().GetCoinBySymbol(data.Symbol, 0).ID() + coinId := checkState.App().GetNextCoinID() deliverState.Coins.Recreate( coinId, @@ -195,21 +209,19 @@ func (data RecreateCoinData) Run(tx *Transaction, context state.Interface, rewar deliverState.Accounts.AddBalance(sender, coinId, data.InitialAmount) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeRecreateCoin)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String())}, - kv.Pair{Key: []byte("tx.coin_id"), Value: []byte(coinId.String())}, - kv.Pair{Key: []byte("tx.old_coin_symbol"), Value: []byte(checkState.Coins().GetCoin(oldCoinID).GetFullSymbol())}, - kv.Pair{Key: []byte("tx.old_coin_id"), Value: []byte(oldCoinID.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String()), Index: true}, + {Key: []byte("tx.coin_id"), Value: []byte(coinId.String()), Index: true}, + {Key: []byte("tx.old_coin_symbol"), Value: []byte(checkState.Coins().GetCoin(oldCoinID).GetFullSymbol())}, + {Key: []byte("tx.old_coin_id"), Value: []byte(oldCoinID.String()), Index: true}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/recreate_coin_test.go b/core/transaction/recreate_coin_test.go index 7759e6360..c7b8684f4 100644 --- a/core/transaction/recreate_coin_test.go +++ b/core/transaction/recreate_coin_test.go @@ -15,6 +15,7 @@ import ( ) func TestRecreateCoinTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -48,7 +49,7 @@ func TestRecreateCoinTx(t *testing.T) { t.Fatalf("Response code is not 0. Error %s", response.Log) } - err = cState.Coins.Commit() + _, _, err = cState.Tree().Commit(cState.Coins) if err != nil { t.Fatalf("Commit coins failed. Error %s", err) } @@ -102,10 +103,13 @@ func TestRecreateCoinTx(t *testing.T) { t.Fatalf("Version in state is not correct. Expected %d, got %d", 1, stateCoin.Version()) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinTxWithWrongOwner(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -140,10 +144,13 @@ func TestRecreateCoinTxWithWrongOwner(t *testing.T) { t.Fatalf("Response code is not 206. Error %s", response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinTxWithWrongSymbol(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -176,10 +183,13 @@ func TestRecreateCoinTxWithWrongSymbol(t *testing.T) { t.Fatalf("Response code is not 102. Error %s", response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinWithIncorrectName(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -236,10 +246,13 @@ func TestRecreateCoinWithIncorrectName(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InvalidCoinName, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinWithWrongCrr(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -292,7 +305,9 @@ func TestRecreateCoinWithWrongCrr(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCrr, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.ConstantReserveRatio = uint32(101) encodedData, err = rlp.EncodeToBytes(data) @@ -324,10 +339,13 @@ func TestRecreateCoinWithWrongCrr(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCrr, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinWithWrongCoinSupply(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -379,7 +397,9 @@ func TestRecreateCoinWithWrongCoinSupply(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCoinSupply, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.InitialAmount = helpers.BipToPip(big.NewInt(1000000)) encodedData, err = rlp.EncodeToBytes(data) @@ -432,7 +452,9 @@ func TestRecreateCoinWithWrongCoinSupply(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCoinSupply, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.MaxSupply = maxCoinSupply data.InitialReserve = helpers.BipToPip(big.NewInt(1000)) @@ -456,10 +478,13 @@ func TestRecreateCoinWithWrongCoinSupply(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongCoinSupply, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinWithInsufficientFundsForGas(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -511,7 +536,9 @@ func TestRecreateCoinWithInsufficientFundsForGas(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } addr := crypto.PubkeyToAddress(privateKey.PublicKey) cState.Accounts.SetBalance(addr, types.GetBaseCoinID(), data.InitialReserve) @@ -532,10 +559,13 @@ func TestRecreateCoinWithInsufficientFundsForGas(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinToInsufficientFundsForInitialReserve(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -569,10 +599,13 @@ func TestRecreateCoinToInsufficientFundsForInitialReserve(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRecreateCoinToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -625,11 +658,13 @@ func TestRecreateCoinToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func makeTestRecreateCoinTx(data RecreateCoinData, privateKey *ecdsa.PrivateKey) ([]byte, error) { diff --git a/core/transaction/recreate_token.go b/core/transaction/recreate_token.go new file mode 100644 index 000000000..7be2066dc --- /dev/null +++ b/core/transaction/recreate_token.go @@ -0,0 +1,170 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "math/big" + "strconv" + + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" +) + +type RecreateTokenData struct { + Name string + Symbol types.CoinSymbol + InitialAmount *big.Int + MaxSupply *big.Int + Mintable bool + Burnable bool +} + +func (data RecreateTokenData) Gas() int { + return gasRecreateToken +} +func (data RecreateTokenData) TxType() TxType { + return TypeRecreateToken +} + +func (data RecreateTokenData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if len(data.Name) > maxCoinNameBytes { + return &Response{ + Code: code.InvalidCoinName, + Log: fmt.Sprintf("Coin name is invalid. Allowed up to %d bytes.", maxCoinNameBytes), + Info: EncodeError(code.NewInvalidCoinName(strconv.Itoa(maxCoinNameBytes), strconv.Itoa(len(data.Name)))), + } + } + + if (data.InitialAmount.Cmp(data.MaxSupply) != 0) != data.Mintable { + // todo + } + + if data.InitialAmount.Cmp(minTokenSupply) == -1 || data.InitialAmount.Cmp(data.MaxSupply) == 1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Coin amount should be between %s and %s", minTokenSupply.String(), data.MaxSupply.String()), + Info: EncodeError(code.NewWrongCoinSupply(minTokenSupply.String(), minTokenSupply.String(), data.MaxSupply.String(), "", "", data.InitialAmount.String())), + } + } + + if data.MaxSupply.Cmp(maxCoinSupply) == 1 { + return &Response{ + Code: code.WrongCoinSupply, + Log: fmt.Sprintf("Max coin supply should be less %s", maxCoinSupply.String()), + Info: EncodeError(code.NewWrongCoinSupply(minTokenSupply.String(), maxCoinSupply.String(), data.MaxSupply.String(), "", "", data.InitialAmount.String())), + } + } + + sender, _ := tx.Sender() + + coin := context.Coins().GetCoinBySymbol(data.Symbol, 0) + if coin == nil { + return &Response{ + Code: code.CoinNotExists, + Log: fmt.Sprintf("Coin %s not exists", data.Symbol), + Info: EncodeError(code.NewCoinNotExists(data.Symbol.String(), "")), + } + } + + symbolInfo := context.Coins().GetSymbolInfo(coin.Symbol()) + if symbolInfo == nil || symbolInfo.OwnerAddress() == nil || *symbolInfo.OwnerAddress() != sender { + var owner *string + if symbolInfo != nil && symbolInfo.OwnerAddress() != nil { + own := symbolInfo.OwnerAddress().String() + owner = &own + } + return &Response{ + Code: code.IsNotOwnerOfCoin, + Log: "Sender is not owner of coin", + Info: EncodeError(code.NewIsNotOwnerOfCoin(data.Symbol.String(), owner)), + } + } + + return nil +} + +func (data RecreateTokenData) String() string { + return fmt.Sprintf("RECREATE TOKEN symbol:%s emission:%s", + data.Symbol.String(), data.MaxSupply) +} + +func (data RecreateTokenData) CommissionData(price *commission.Price) *big.Int { + return price.RecreateToken +} + +func (data RecreateTokenData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + rewardPool.Add(rewardPool, commissionInBaseCoin) + + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + + oldCoinID := checkState.Coins().GetCoinBySymbol(data.Symbol, 0).ID() + coinId := checkState.App().GetNextCoinID() + deliverState.Coins.RecreateToken( + coinId, + data.Name, + data.Symbol, + data.Mintable, + data.Burnable, + data.InitialAmount, + data.MaxSupply, + ) + + deliverState.App.SetCoinsCount(coinId.Uint32()) + deliverState.Accounts.AddBalance(sender, coinId, data.InitialAmount) + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_symbol"), Value: []byte(data.Symbol.String()), Index: true}, + {Key: []byte("tx.coin_id"), Value: []byte(coinId.String()), Index: true}, + {Key: []byte("tx.old_coin_symbol"), Value: []byte(checkState.Coins().GetCoin(oldCoinID).GetFullSymbol())}, + {Key: []byte("tx.old_coin_id"), Value: []byte(oldCoinID.String()), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/redeem_check.go b/core/transaction/redeem_check.go index ff5c406a8..f40938ebf 100644 --- a/core/transaction/redeem_check.go +++ b/core/transaction/redeem_check.go @@ -4,18 +4,17 @@ import ( "bytes" "encoding/hex" "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" "math/big" "strconv" "github.com/MinterTeam/minter-go-node/core/check" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/crypto" - "github.com/MinterTeam/minter-go-node/formula" "github.com/MinterTeam/minter-go-node/rlp" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "golang.org/x/crypto/sha3" ) @@ -24,7 +23,14 @@ type RedeemCheckData struct { Proof [65]byte } -func (data RedeemCheckData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data RedeemCheckData) Gas() int { + return gasRedeemCheck +} +func (data RedeemCheckData) TxType() TxType { + return TypeRedeemCheck +} + +func (data RedeemCheckData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.RawCheck == nil { return &Response{ Code: code.DecodeError, @@ -49,11 +55,11 @@ func (data RedeemCheckData) String() string { return fmt.Sprintf("REDEEM CHECK proof: %x", data.Proof) } -func (data RedeemCheckData) Gas() int64 { - return commissions.RedeemCheckTx +func (data RedeemCheckData) CommissionData(price *commission.Price) *big.Int { + return price.RedeemCheck } -func (data RedeemCheckData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data RedeemCheckData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -62,7 +68,7 @@ func (data RedeemCheckData) Run(tx *Transaction, context state.Interface, reward checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } @@ -121,7 +127,7 @@ func (data RedeemCheckData) Run(tx *Transaction, context state.Interface, reward if tx.GasCoin != decodedCheck.GasCoin { return Response{ Code: code.WrongGasCoin, - Log: fmt.Sprintf("Gas coin for redeem check transaction can only be %s", decodedCheck.GasCoin), + Log: fmt.Sprintf("CommissionData coin for redeem check transaction can only be %s", decodedCheck.GasCoin), Info: EncodeError(code.NewWrongGasCoin(checkState.Coins().GetCoin(tx.GasCoin).GetFullSymbol(), tx.GasCoin.String(), checkState.Coins().GetCoin(decodedCheck.GasCoin).GetFullSymbol(), decodedCheck.GasCoin.String())), } } @@ -177,26 +183,22 @@ func (data RedeemCheckData) Run(tx *Transaction, context state.Interface, reward } } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } - gasCoin := checkState.Coins().GetCoin(decodedCheck.GasCoin) coin := checkState.Coins().GetCoin(decodedCheck.Coin) - if !decodedCheck.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) - } - if decodedCheck.Coin == decodedCheck.GasCoin { totalTxCost := big.NewInt(0).Add(decodedCheck.Value, commission) if checkState.Accounts().GetBalance(checkSender, decodedCheck.Coin).Cmp(totalTxCost) < 0 { return Response{ Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for check issuer account: %s %s. Wanted %s %s", decodedCheck.Coin, checkSender.String(), totalTxCost.String(), coin.GetFullSymbol()), + Log: fmt.Sprintf("Insufficient funds for check issuer account: %s %s. Wanted %s %s", decodedCheck.Value.String(), coin.GetFullSymbol(), totalTxCost.String(), coin.GetFullSymbol()), Info: EncodeError(code.NewInsufficientFunds(sender.String(), totalTxCost.String(), coin.GetFullSymbol(), coin.ID().String())), } } @@ -204,7 +206,7 @@ func (data RedeemCheckData) Run(tx *Transaction, context state.Interface, reward if checkState.Accounts().GetBalance(checkSender, decodedCheck.Coin).Cmp(decodedCheck.Value) < 0 { return Response{ Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for check issuer account: %s %s. Wanted %s %s", checkSender.String(), decodedCheck.Coin, decodedCheck.Value.String(), coin.GetFullSymbol()), + Log: fmt.Sprintf("Insufficient funds for check issuer account: %s %s. Wanted %s %s", decodedCheck.Value.String(), decodedCheck.Coin, decodedCheck.Value.String(), coin.GetFullSymbol()), Info: EncodeError(code.NewInsufficientFunds(checkSender.String(), decodedCheck.Value.String(), coin.GetFullSymbol(), coin.ID().String())), } } @@ -212,36 +214,37 @@ func (data RedeemCheckData) Run(tx *Transaction, context state.Interface, reward if checkState.Accounts().GetBalance(checkSender, decodedCheck.GasCoin).Cmp(commission) < 0 { return Response{ Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for check issuer account: %s %s. Wanted %s %s", checkSender.String(), decodedCheck.GasCoin, commission.String(), gasCoin.GetFullSymbol()), + Log: fmt.Sprintf("Insufficient funds for check issuer account: %s %s. Wanted %s %s", decodedCheck.Value.String(), decodedCheck.GasCoin, commission.String(), gasCoin.GetFullSymbol()), Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), } } } - + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { deliverState.Checks.UseCheck(decodedCheck) rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubVolume(decodedCheck.GasCoin, commission) - deliverState.Coins.SubReserve(decodedCheck.GasCoin, commissionInBaseCoin) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(checkSender, decodedCheck.GasCoin, commission) deliverState.Accounts.SubBalance(checkSender, decodedCheck.Coin, decodedCheck.Value) deliverState.Accounts.AddBalance(sender, decodedCheck.Coin, decodedCheck.Value) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeRedeemCheck)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(checkSender[:]))}, - kv.Pair{Key: []byte("tx.to"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.coin_id"), Value: []byte(decodedCheck.Coin.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.to"), Value: []byte(hex.EncodeToString(sender[:])), Index: true}, + {Key: []byte("tx.coin_id"), Value: []byte(decodedCheck.Coin.String()), Index: true}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/redeem_check_test.go b/core/transaction/redeem_check_test.go index bd422bae7..cd78d060c 100644 --- a/core/transaction/redeem_check_test.go +++ b/core/transaction/redeem_check_test.go @@ -17,6 +17,7 @@ import ( ) func TestRedeemCheckTx(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -119,10 +120,13 @@ func TestRedeemCheckTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", coin, checkValue, balance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToDecodeError(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -204,7 +208,7 @@ func TestRedeemCheckTxToDecodeError(t *testing.T) { t.Fatal(err) } - response := data.BasicCheck(&tx, state.NewCheckState(cState)) + response := data.basicCheck(&tx, state.NewCheckState(cState)) if response.Code != code.DecodeError { t.Fatalf("Response code is not %d. Error %s", code.DecodeError, response.Log) } @@ -226,10 +230,13 @@ func TestRedeemCheckTxToDecodeError(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.DecodeError, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToHighGasPrice(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -322,10 +329,13 @@ func TestRedeemCheckTxToHighGasPrice(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.TooHighGasPrice, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToWrongChainID(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -418,10 +428,13 @@ func TestRedeemCheckTxToWrongChainID(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongChainID, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToNonceLength(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -514,10 +527,13 @@ func TestRedeemCheckTxToNonceLength(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.TooLongNonce, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToCheckData(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -610,7 +626,9 @@ func TestRedeemCheckTxToCheckData(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } check.Coin = coin check.GasCoin = 5 @@ -651,7 +669,9 @@ func TestRedeemCheckTxToCheckData(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } check.GasCoin = coin lock, err = crypto.Sign(check.HashWithoutLock().Bytes(), passphrasePk) @@ -689,7 +709,9 @@ func TestRedeemCheckTxToCheckData(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.WrongGasCoin, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } check.DueBlock = 1 lock, err = crypto.Sign(check.HashWithoutLock().Bytes(), passphrasePk) @@ -726,10 +748,13 @@ func TestRedeemCheckTxToCheckData(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CheckExpired, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToUsed(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -824,10 +849,13 @@ func TestRedeemCheckTxToUsed(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CheckUsed, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() coin := types.GetBaseCoinID() @@ -918,10 +946,13 @@ func TestRedeemCheckTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() coin := createTestCoin(cState) cState.Coins.SubReserve(coin, helpers.BipToPip(big.NewInt(90000))) @@ -1009,14 +1040,17 @@ func TestRedeemCheckTxToCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToInsufficientFundsForCheckCoin(t *testing.T) { + t.Parallel() cState := getState() coin := createTestCoin(cState) @@ -1107,10 +1141,13 @@ func TestRedeemCheckTxToInsufficientFundsForCheckCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestRedeemCheckTxToInsufficientFundsForCheckGasCoin(t *testing.T) { + t.Parallel() cState := getState() coin := createTestCoin(cState) @@ -1204,5 +1241,7 @@ func TestRedeemCheckTxToInsufficientFundsForCheckGasCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/remove_liquidity.go b/core/transaction/remove_liquidity.go new file mode 100644 index 000000000..60159849b --- /dev/null +++ b/core/transaction/remove_liquidity.go @@ -0,0 +1,172 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/state/swap" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" +) + +type RemoveLiquidity struct { + Coin0 types.CoinID + Coin1 types.CoinID + Liquidity *big.Int + MinimumVolume0 *big.Int + MinimumVolume1 *big.Int +} + +func (data RemoveLiquidity) Gas() int { + return gasRemoveLiquidity +} +func (data RemoveLiquidity) TxType() TxType { + return TypeRemoveLiquidity +} + +func (data RemoveLiquidity) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if data.Liquidity.Sign() != 1 { + return &Response{ + Code: code.DecodeError, + Log: "Can't remove zero liquidity volume", + Info: EncodeError(code.NewDecodeError()), + } + } + + if data.Coin0 == data.Coin1 { + return &Response{ + Code: code.CrossConvert, + Log: "\"From\" coin equals to \"to\" coin", + Info: EncodeError(code.NewCrossConvert( + data.Coin0.String(), + data.Coin1.String(), "", "")), + } + } + + return nil +} + +func (data RemoveLiquidity) String() string { + return fmt.Sprintf("REMOVE SWAP POOL") +} + +func (data RemoveLiquidity) CommissionData(price *commission.Price) *big.Int { + return price.RemoveLiquidity +} + +func (data RemoveLiquidity) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + swapper := checkState.Swap().GetSwapper(data.Coin0, data.Coin1) + if !swapper.IsExist() { + return Response{ + Code: code.PairNotExists, + Log: "swap pool for pair not found", + Info: EncodeError(code.NewPairNotExists(data.Coin0.String(), data.Coin1.String())), + } + } + + coinLiquidity := checkState.Coins().GetCoinBySymbol(LiquidityCoinSymbol(swapper.CoinID()), 0) + balance := checkState.Accounts().GetBalance(sender, coinLiquidity.ID()) + if balance.Cmp(data.Liquidity) == -1 { + amount0, amount1 := swapper.Amounts(balance, coinLiquidity.Volume()) + symbol1 := checkState.Coins().GetCoin(data.Coin1).GetFullSymbol() + symbol0 := checkState.Coins().GetCoin(data.Coin0).GetFullSymbol() + return Response{ + Code: code.InsufficientLiquidityBalance, + Log: fmt.Sprintf("Insufficient balance for provider: %s liquidity tokens is equal %s %s and %s %s, but you want to get %s liquidity", balance, amount0, symbol0, amount1, symbol1, data.Liquidity), + Info: EncodeError(code.NewInsufficientLiquidityBalance(balance.String(), amount0.String(), data.Coin0.String(), amount1.String(), data.Coin1.String(), data.Liquidity.String())), + } + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if swapper.IsExist() { + if isGasCommissionFromPoolSwap { + if tx.GasCoin == data.Coin0 && data.Coin1.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + if tx.GasCoin == data.Coin1 && data.Coin0.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commissionInBaseCoin, commission) + } + } + } + + if err := swapper.CheckBurn(data.Liquidity, data.MinimumVolume0, data.MinimumVolume1, coinLiquidity.Volume()); err != nil { + wantAmount0, wantAmount1 := swapper.Amounts(data.Liquidity, coinLiquidity.Volume()) + if err == swap.ErrorInsufficientLiquidityBurned { + wantGetAmount0 := data.MinimumVolume0.String() + wantGetAmount1 := data.MinimumVolume1.String() + symbol0 := checkState.Coins().GetCoin(data.Coin0).GetFullSymbol() + symbol1 := checkState.Coins().GetCoin(data.Coin1).GetFullSymbol() + return Response{ + Code: code.InsufficientLiquidityBurned, + Log: fmt.Sprintf("You wanted to get more %s %s and more %s %s, but currently liquidity %s is equal %s %s and %s %s", wantGetAmount0, symbol0, wantGetAmount1, symbol1, data.Liquidity, wantAmount0, symbol0, wantAmount1, symbol1), + Info: EncodeError(code.NewInsufficientLiquidityBurned(wantGetAmount0, data.Coin0.String(), wantGetAmount1, data.Coin1.String(), data.Liquidity.String(), wantAmount0.String(), wantAmount1.String())), + } + } + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + amount0, amount1 := deliverState.Swap.PairBurn(data.Coin0, data.Coin1, data.Liquidity, data.MinimumVolume0, data.MinimumVolume1, coinLiquidity.Volume()) + deliverState.Accounts.AddBalance(sender, data.Coin0, amount0) + deliverState.Accounts.AddBalance(sender, data.Coin1, amount1) + + deliverState.Coins.SubVolume(coinLiquidity.ID(), data.Liquidity) + deliverState.Accounts.SubBalance(sender, coinLiquidity.ID(), data.Liquidity) + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.volume0"), Value: []byte(amount0.String())}, + {Key: []byte("tx.volume1"), Value: []byte(amount1.String())}, + {Key: []byte("tx.pool_token"), Value: []byte(coinLiquidity.GetFullSymbol()), Index: true}, + {Key: []byte("tx.pool_token_id"), Value: []byte(coinLiquidity.ID().String()), Index: true}, + {Key: []byte("tx.pair_ids"), Value: []byte(liquidityCoinName(data.Coin0, data.Coin1))}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/remove_liquidity_test.go b/core/transaction/remove_liquidity_test.go new file mode 100644 index 000000000..19f401372 --- /dev/null +++ b/core/transaction/remove_liquidity_test.go @@ -0,0 +1,427 @@ +package transaction + +import ( + "math/big" + "sync" + "testing" + + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" +) + +func TestRemoveExchangeLiquidityTx_one(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999998000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(10)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(10)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + + { + _, _, coinID := cState.Swap.SwapPool(coin, coin1) + data := RemoveLiquidity{ + Coin0: coin, + Coin1: coin1, + Liquidity: cState.Accounts.GetBalance(addr, cState.Coins.GetCoinBySymbol(LiquidityCoinSymbol(coinID), 0).ID()), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeRemoveLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestRemoveExchangeLiquidityTx_2(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + privateKey2, _ := crypto.GenerateKey() + addr2 := crypto.PubkeyToAddress(privateKey2.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999999000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + cState.Accounts.AddBalance(addr2, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin1, helpers.BipToPip(big.NewInt(50000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: big.NewInt(10000), + Coin1: coin1, + Volume1: big.NewInt(10000), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } + { + data := AddLiquidityData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(10)), + Coin1: coin1, + MaximumVolume1: helpers.BipToPip(big.NewInt(10)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeAddLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey2); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } + { + _, _, coinID := cState.Swap.SwapPool(coin, coin1) + data := RemoveLiquidity{ + Coin0: coin, + Coin1: coin1, + Liquidity: cState.Accounts.GetBalance(addr2, cState.Coins.GetCoinBySymbol(LiquidityCoinSymbol(coinID), 0).ID()), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeRemoveLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey2); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestRemoveExchangeLiquidityTx_3(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + privateKey2, _ := crypto.GenerateKey() + addr2 := crypto.PubkeyToAddress(privateKey2.PublicKey) + + cState.Checker.AddCoin(types.BasecoinID, helpers.StringToBigInt("-1099999999000000000000000")) + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + cState.Accounts.AddBalance(addr2, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(50000))) + cState.Accounts.AddBalance(addr2, coin1, helpers.BipToPip(big.NewInt(50000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: big.NewInt(9000), + Coin1: coin1, + Volume1: big.NewInt(11000), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } + { + data := AddLiquidityData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(9)), + Coin1: coin1, + MaximumVolume1: helpers.BipToPip(big.NewInt(11)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeAddLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey2); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } + { + _, _, coinID := cState.Swap.SwapPool(coin, coin1) + data := RemoveLiquidity{ + Coin0: coin, + Coin1: coin1, + Liquidity: cState.Accounts.GetBalance(addr2, cState.Coins.GetCoinBySymbol(LiquidityCoinSymbol(coinID), 0).ID()), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeRemoveLiquidity, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey2); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} diff --git a/core/transaction/sell_all_coin.go b/core/transaction/sell_all_coin.go index 234949b82..e71a6ce81 100644 --- a/core/transaction/sell_all_coin.go +++ b/core/transaction/sell_all_coin.go @@ -1,14 +1,13 @@ package transaction import ( - "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -18,128 +17,16 @@ type SellAllCoinData struct { MinimumValueToBuy *big.Int } -func (data SellAllCoinData) totalSpend(tx *Transaction, context *state.CheckState) (TotalSpends, []conversion, *big.Int, *Response) { - sender, _ := tx.Sender() - - total := TotalSpends{} - var conversions []conversion - - commissionInBaseCoin := tx.CommissionInBaseCoin() - available := context.Accounts().GetBalance(sender, data.CoinToSell) - var value *big.Int - - total.Add(data.CoinToSell, available) - - switch { - case data.CoinToSell.IsBaseCoin(): - amountToSell := big.NewInt(0).Set(available) - amountToSell.Sub(amountToSell, commissionInBaseCoin) - coin := context.Coins().GetCoin(data.CoinToBuy) - // todo: fix error message with negative value when converting all coins - // if amountToSell.Sign() != 1 { - // return nil, nil, nil, &Response{ - // Code: code.InsufficientFunds, - // Log: "Insufficient funds for sender account", - // Info: EncodeError(code.NewInsufficientFunds(sender.String(), commissionInBaseCoin.String(), coin.GetFullSymbol(), coin.ID().String())), - // } - // } - - value = formula.CalculatePurchaseReturn(coin.Volume(), coin.Reserve(), coin.Crr(), amountToSell) - - if value.Cmp(data.MinimumValueToBuy) == -1 { - return nil, nil, nil, &Response{ - Code: code.MinimumValueToBuyReached, - Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), - Info: EncodeError(code.NewMinimumValueToBuyReached(data.MinimumValueToBuy.String(), value.String(), coin.GetFullSymbol(), coin.ID().String())), - } - } - - if errResp := CheckForCoinSupplyOverflow(coin, value); errResp != nil { - return nil, nil, nil, errResp - } - - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - ToCoin: data.CoinToBuy, - ToAmount: value, - ToReserve: amountToSell, - }) - case data.CoinToBuy.IsBaseCoin(): - amountToSell := big.NewInt(0).Set(available) - - coin := context.Coins().GetCoin(data.CoinToSell) - ret := formula.CalculateSaleReturn(coin.Volume(), coin.Reserve(), coin.Crr(), amountToSell) - - if ret.Cmp(data.MinimumValueToBuy) == -1 { - return nil, nil, nil, &Response{ - Code: code.MinimumValueToBuyReached, - Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), ret.String()), - Info: EncodeError(code.NewMinimumValueToBuyReached(data.MinimumValueToBuy.String(), ret.String(), coin.GetFullSymbol(), coin.ID().String())), - } - } - - if ret.Cmp(commissionInBaseCoin) == -1 { - return nil, nil, nil, &Response{ - Code: code.InsufficientFunds, - Log: "Insufficient funds for sender account", - Info: EncodeError(code.NewInsufficientFunds(sender.String(), commissionInBaseCoin.String(), coin.GetFullSymbol(), coin.ID().String())), - } - } - - value = big.NewInt(0).Set(ret) - value.Sub(ret, commissionInBaseCoin) - - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - FromAmount: amountToSell, - FromReserve: ret, - ToCoin: data.CoinToBuy, - }) - default: - amountToSell := big.NewInt(0).Set(available) - - coinFrom := context.Coins().GetCoin(data.CoinToSell) - coinTo := context.Coins().GetCoin(data.CoinToBuy) - - basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), amountToSell) - if basecoinValue.Cmp(commissionInBaseCoin) == -1 { - return nil, nil, nil, &Response{ - Code: code.InsufficientFunds, - Log: "Insufficient funds for sender account", - Info: EncodeError(code.NewInsufficientFunds(sender.String(), commissionInBaseCoin.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), - } - } - - basecoinValue.Sub(basecoinValue, commissionInBaseCoin) - - value = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), basecoinValue) - if value.Cmp(data.MinimumValueToBuy) == -1 { - return nil, nil, nil, &Response{ - Code: code.MinimumValueToBuyReached, - Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), - Info: EncodeError(code.NewMinimumValueToBuyReached(data.MinimumValueToBuy.String(), value.String(), coinTo.GetFullSymbol(), coinTo.ID().String())), - } - } - - if errResp := CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { - return nil, nil, nil, errResp - } - - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - FromAmount: amountToSell, - FromReserve: big.NewInt(0).Add(basecoinValue, commissionInBaseCoin), - ToCoin: data.CoinToBuy, - ToAmount: value, - ToReserve: basecoinValue, - }) - } - - return total, conversions, value, nil +func (data SellAllCoinData) Gas() int { + return gasSellAllCoin +} +func (data SellAllCoinData) TxType() TxType { + return TypeSellAllCoin } -func (data SellAllCoinData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { - if !context.Coins().Exists(data.CoinToSell) { +func (data SellAllCoinData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + coinToSell := context.Coins().GetCoin(data.CoinToSell) + if coinToSell == nil { return &Response{ Code: code.CoinNotExists, Log: "Coin to sell not exists", @@ -147,7 +34,19 @@ func (data SellAllCoinData) BasicCheck(tx *Transaction, context *state.CheckStat } } - if !context.Coins().Exists(data.CoinToBuy) { + if !coinToSell.BaseOrHasReserve() { + return &Response{ + Code: code.CoinHasNotReserve, + Log: "sell coin has no reserve", + Info: EncodeError(code.NewCoinHasNotReserve( + coinToSell.GetFullSymbol(), + coinToSell.ID().String(), + )), + } + } + + coinToBuy := context.Coins().GetCoin(data.CoinToBuy) + if coinToBuy == nil { return &Response{ Code: code.CoinNotExists, Log: "Coin to buy not exists", @@ -155,15 +54,26 @@ func (data SellAllCoinData) BasicCheck(tx *Transaction, context *state.CheckStat } } + if !coinToBuy.BaseOrHasReserve() { + return &Response{ + Code: code.CoinHasNotReserve, + Log: "coin to buy has no reserve", + Info: EncodeError(code.NewCoinHasNotReserve( + coinToBuy.GetFullSymbol(), + coinToBuy.ID().String(), + )), + } + } + if data.CoinToSell == data.CoinToBuy { return &Response{ Code: code.CrossConvert, Log: "\"From\" coin equals to \"to\" coin", Info: EncodeError(code.NewCrossConvert( data.CoinToSell.String(), - context.Coins().GetCoin(data.CoinToSell).GetFullSymbol(), + coinToSell.GetFullSymbol(), data.CoinToBuy.String(), - context.Coins().GetCoin(data.CoinToBuy).GetFullSymbol()), + coinToBuy.GetFullSymbol()), ), } } @@ -176,80 +86,123 @@ func (data SellAllCoinData) String() string { data.CoinToSell.String(), data.CoinToBuy.String()) } -func (data SellAllCoinData) Gas() int64 { - return commissions.ConvertTx +func (data SellAllCoinData) CommissionData(price *commission.Price) *big.Int { + return price.SellAllBancor } -func (data SellAllCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data SellAllCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState var isCheck bool if checkState, isCheck = context.(*state.CheckState); !isCheck { checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - available := checkState.Accounts().GetBalance(sender, data.CoinToSell) - - totalSpends, conversions, value, response := data.totalSpend(tx, checkState) - if response != nil { - return *response + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(data.CoinToSell, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(data.CoinToSell) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } - for _, ts := range totalSpends { - if checkState.Accounts().GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { - coin := checkState.Coins().GetCoin(ts.Coin) + balance := checkState.Accounts().GetBalance(sender, data.CoinToSell) + if balance.Cmp(commission) != 1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("1Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", - sender.String(), - ts.Value.String(), - coin.GetFullSymbol()), - Info: EncodeError(code.NewInsufficientFunds(sender.String(), ts.Value.String(), coin.GetFullSymbol(), coin.ID().String())), - } + coinToSell := data.CoinToSell + coinToBuy := data.CoinToBuy + var coinFrom CalculateCoin + coinFrom = checkState.Coins().GetCoin(coinToSell) + coinTo := checkState.Coins().GetCoin(coinToBuy) + + if isGasCommissionFromPoolSwap == false && !data.CoinToSell.IsBaseCoin() { + coinFrom = DummyCoin{ + id: gasCoin.ID(), + volume: big.NewInt(0).Sub(gasCoin.Volume(), commission), + reserve: big.NewInt(0).Sub(gasCoin.Reserve(), commissionInBaseCoin), + crr: gasCoin.Crr(), + fullSymbol: gasCoin.GetFullSymbol(), + maxSupply: gasCoin.MaxSupply(), } } - errResp := checkConversionsReserveUnderflow(conversions, checkState) - if errResp != nil { - return *errResp + valueToSell := big.NewInt(0).Sub(balance, commission) + + value := big.NewInt(0).Set(valueToSell) + if value.Sign() != 1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), balance.String(), coinFrom.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), coinFrom.GetFullSymbol(), data.CoinToSell.String())), + } } + if !coinToSell.IsBaseCoin() { + value, errResp = CalculateSaleReturnAndCheck(coinFrom, value) + if errResp != nil { + return *errResp + } + } + diffBipReserve := big.NewInt(0).Set(value) + if !coinToBuy.IsBaseCoin() { + value = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), value) + if errResp := CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { + return *errResp + } + } + if value.Cmp(data.MinimumValueToBuy) == -1 { + return Response{ + Code: code.MinimumValueToBuyReached, + Log: fmt.Sprintf( + "You wanted to buy minimum %s, but currently you need to spend %s to complete tx", + data.MinimumValueToBuy.String(), value.String()), + Info: EncodeError(code.NewMaximumValueToSellReached(data.MinimumValueToBuy.String(), value.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), + } + } + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - for _, ts := range totalSpends { - deliverState.Accounts.SubBalance(sender, ts.Coin, ts.Value) + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(data.CoinToSell, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !data.CoinToSell.IsBaseCoin() { + deliverState.Coins.SubVolume(data.CoinToSell, commission) + deliverState.Coins.SubReserve(data.CoinToSell, commissionInBaseCoin) } - - for _, conversion := range conversions { - deliverState.Coins.SubVolume(conversion.FromCoin, conversion.FromAmount) - deliverState.Coins.SubReserve(conversion.FromCoin, conversion.FromReserve) - - deliverState.Coins.AddVolume(conversion.ToCoin, conversion.ToAmount) - deliverState.Coins.AddReserve(conversion.ToCoin, conversion.ToReserve) + rewardPool.Add(rewardPool, commissionInBaseCoin) + deliverState.Accounts.SubBalance(sender, data.CoinToSell, balance) + if !data.CoinToSell.IsBaseCoin() { + deliverState.Coins.SubVolume(data.CoinToSell, valueToSell) + deliverState.Coins.SubReserve(data.CoinToSell, diffBipReserve) } - - rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) deliverState.Accounts.AddBalance(sender, data.CoinToBuy, value) + if !data.CoinToBuy.IsBaseCoin() { + deliverState.Coins.AddVolume(data.CoinToBuy, value) + deliverState.Coins.AddReserve(data.CoinToBuy, diffBipReserve) + } deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeSellAllCoin)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.coin_to_buy"), Value: []byte(data.CoinToBuy.String())}, - kv.Pair{Key: []byte("tx.coin_to_sell"), Value: []byte(data.CoinToSell.String())}, - kv.Pair{Key: []byte("tx.return"), Value: []byte(value.String())}, - kv.Pair{Key: []byte("tx.sell_amount"), Value: []byte(available.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_to_buy"), Value: []byte(data.CoinToBuy.String()), Index: true}, + {Key: []byte("tx.coin_to_sell"), Value: []byte(data.CoinToSell.String()), Index: true}, + {Key: []byte("tx.return"), Value: []byte(value.String())}, + {Key: []byte("tx.sell_amount"), Value: []byte(balance.String())}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/sell_all_coin_test.go b/core/transaction/sell_all_coin_test.go index ae253a337..29ca09604 100644 --- a/core/transaction/sell_all_coin_test.go +++ b/core/transaction/sell_all_coin_test.go @@ -13,6 +13,7 @@ import ( ) func TestSellAllCoinTx(t *testing.T) { + t.Parallel() cState := getState() coinID := createTestCoin(cState) @@ -63,7 +64,7 @@ func TestSellAllCoinTx(t *testing.T) { } balance := cState.Accounts.GetBalance(addr, coin) - if balance.Cmp(types.Big0) != 0 { + if balance.Sign() != 0 { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", coin, types.Big0, balance) } @@ -73,10 +74,13 @@ func TestSellAllCoinTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", getTestCoinSymbol(), targetTestBalance, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellAllCoinTxWithSameCoins(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -121,10 +125,13 @@ func TestSellAllCoinTxWithSameCoins(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CrossConvert, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellAllCoinTxWithInvalidCoins(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -166,7 +173,9 @@ func TestSellAllCoinTxWithInvalidCoins(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } data.CoinToSell = types.GetBaseCoinID() data.CoinToBuy = types.CoinID(5) @@ -190,15 +199,18 @@ func TestSellAllCoinTxWithInvalidCoins(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellAllCoinTxWithMinimumValueToBuy(t *testing.T) { + t.Parallel() cState := getState() coinID := createTestCoin(cState) privateKey, _ := crypto.GenerateKey() coin := types.GetBaseCoinID() - + cState.Accounts.AddBalance(crypto.PubkeyToAddress(privateKey.PublicKey), coin, big.NewInt(9e18)) minValToBuy, _ := big.NewInt(0).SetString("151191152412701306252", 10) data := SellAllCoinData{ CoinToSell: coin, @@ -237,10 +249,13 @@ func TestSellAllCoinTxWithMinimumValueToBuy(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.MinimumValueToBuyReached, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellAllCoinTxWithInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() coinID := createTestCoin(cState) privateKey, _ := crypto.GenerateKey() @@ -286,7 +301,9 @@ func TestSellAllCoinTxWithInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } nextCoinID := cState.App.GetNextCoinID() cState.Coins.Create( @@ -325,13 +342,16 @@ func TestSellAllCoinTxWithInsufficientFunds(t *testing.T) { response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) if response.Code != code.InsufficientFunds { - t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) + t.Fatalf("Response code is not %d. Error %d %s", code.InsufficientFunds, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellAllCoinTxToCoinSupplyOverflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID, sellCoinID := createTestCoin(cState), types.GetBaseCoinID() @@ -376,7 +396,9 @@ func TestSellAllCoinTxToCoinSupplyOverflow(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinSupplyOverflow, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // custom buy and sell coins @@ -414,14 +436,17 @@ func TestSellAllCoinTxToCoinSupplyOverflow(t *testing.T) { } response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinSupplyOverflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinSupplyOverflow, response.Log) + if response.Code != code.CoinReserveUnderflow { + t.Fatalf("Response code is not %d. Error %d %s", code.CoinReserveUnderflow, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellAllCoinTxToMinimumValueToBuyReached(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID, sellCoinID := createTestCoin(cState), types.GetBaseCoinID() @@ -464,15 +489,18 @@ func TestSellAllCoinTxToMinimumValueToBuyReached(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.MinimumValueToBuyReached, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // coin to buy == base coin - - cState.Accounts.SubBalance(types.Address{}, coinToBuyID, big.NewInt(1)) - cState.Accounts.AddBalance(addr, coinToBuyID, big.NewInt(1)) - + // data.CoinToBuy = sellCoinID data.CoinToSell = coinToBuyID + + cState.Accounts.SubBalance(types.Address{}, data.CoinToSell, big.NewInt(10000004500002851)) + cState.Accounts.AddBalance(addr, data.CoinToSell, big.NewInt(10000004500002851)) + data.MinimumValueToBuy = big.NewInt(9e18) encodedData, err = rlp.EncodeToBytes(data) if err != nil { @@ -491,10 +519,12 @@ func TestSellAllCoinTxToMinimumValueToBuyReached(t *testing.T) { response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) if response.Code != code.MinimumValueToBuyReached { - t.Fatalf("Response code is not %d. Error %s", code.MinimumValueToBuyReached, response.Log) + t.Fatalf("Response code %d is not %d. Error %s: %s", response.Code, code.MinimumValueToBuyReached, response.Log, response.Info) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // custom buy and sell coins @@ -540,5 +570,7 @@ func TestSellAllCoinTxToMinimumValueToBuyReached(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.MinimumValueToBuyReached, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/sell_all_swap_pool.go b/core/transaction/sell_all_swap_pool.go new file mode 100644 index 000000000..769e3292e --- /dev/null +++ b/core/transaction/sell_all_swap_pool.go @@ -0,0 +1,319 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/state/swap" + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/formula" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" + "strings" +) + +type SellAllSwapPoolData struct { + Coins []types.CoinID + MinimumValueToBuy *big.Int +} + +func (data SellAllSwapPoolData) Gas() int { + return gasSellAllSwapPool +} + +func (data SellAllSwapPoolData) TxType() TxType { + return TypeSellAllSwapPool +} + +func (data SellAllSwapPoolData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if len(data.Coins) < 2 { + return &Response{ + Code: code.DecodeError, + Log: "Incorrect tx data", + Info: EncodeError(code.NewDecodeError()), + } + } + if len(data.Coins) > 5 { + return &Response{ + Code: code.TooLongSwapRoute, + Log: "maximum allowed length of the exchange chain is 5", + Info: EncodeError(code.NewCustomCode(code.TooLongSwapRoute)), + } + } + coin0 := data.Coins[0] + for _, coin1 := range data.Coins[1:] { + if coin0 == coin1 { + return &Response{ + Code: code.CrossConvert, + Log: "\"From\" coin equals to \"to\" coin", + Info: EncodeError(code.NewCrossConvert( + coin0.String(), "", + coin1.String(), "")), + } + } + if !context.Swap().SwapPoolExist(coin0, coin1) { + return &Response{ + Code: code.PairNotExists, + Log: fmt.Sprint("swap pool not exists"), + Info: EncodeError(code.NewPairNotExists(coin0.String(), coin1.String())), + } + } + coin0 = coin1 + } + return nil +} + +func (data SellAllSwapPoolData) String() string { + return fmt.Sprintf("SWAP POOL SELL ALL") +} + +func (data SellAllSwapPoolData) CommissionData(price *commission.Price) *big.Int { + return new(big.Int).Add(price.SellAllPoolBase, new(big.Int).Mul(price.SellAllPoolDelta, big.NewInt(int64(len(data.Coins))-2))) +} + +func (data SellAllSwapPoolData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + coinToSell := data.Coins[0] + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(coinToSell, types.GetBaseCoinID()) + sellCoin := checkState.Coins().GetCoin(coinToSell) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, sellCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + balance := checkState.Accounts().GetBalance(sender, coinToSell) + available := big.NewInt(0).Set(balance) + balance.Sub(available, commission) + + if balance.Sign() != 1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), balance.String(), sellCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), balance.String(), sellCoin.GetFullSymbol(), coinToSell.String())), + } + } + + { + coinToSell := data.Coins[0] + coinToSellModel := sellCoin + resultCoin := data.Coins[len(data.Coins)-1] + valueToSell := big.NewInt(0).Set(balance) + valueToBuy := big.NewInt(0) + for _, coinToBuy := range data.Coins[1:] { + swapper := checkState.Swap().GetSwapper(coinToSell, coinToBuy) + if isGasCommissionFromPoolSwap == true && coinToBuy.IsBaseCoin() { + swapper = commissionPoolSwapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + + if coinToBuy == resultCoin { + valueToBuy = data.MinimumValueToBuy + } + + coinToBuyModel := checkState.Coins().GetCoin(coinToBuy) + errResp = CheckSwap(swapper, coinToSellModel, coinToBuyModel, valueToSell, valueToBuy, false) + if errResp != nil { + return *errResp + } + + valueToSellCalc := swapper.CalculateBuyForSell(valueToSell) + if valueToSellCalc == nil { + reserve0, reserve1 := swapper.Reserves() + return Response{ // todo + Code: code.SwapPoolUnknown, + Log: fmt.Sprintf("swap pool has reserves %s %s and %d %s, you wanted sell %s %s", reserve0, coinToSellModel.GetFullSymbol(), reserve1, coinToBuyModel.GetFullSymbol(), valueToSell, coinToSellModel.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientLiquidity(coinToSellModel.ID().String(), valueToSell.String(), coinToBuyModel.ID().String(), valueToSellCalc.String(), reserve0.String(), reserve1.String())), + } + } + valueToSell = valueToSellCalc + coinToSellModel = coinToBuyModel + coinToSell = coinToBuy + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(sellCoin.ID(), types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !sellCoin.ID().IsBaseCoin() { + deliverState.Coins.SubVolume(sellCoin.ID(), commission) + deliverState.Coins.SubReserve(sellCoin.ID(), commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, sellCoin.ID(), commission) + + coinToSell := data.Coins[0] + resultCoin := data.Coins[len(data.Coins)-1] + valueToSell := big.NewInt(0).Set(balance) + + var poolIDs []string + + for i, coinToBuy := range data.Coins[1:] { + amountIn, amountOut, poolID := deliverState.Swap.PairSell(coinToSell, coinToBuy, valueToSell, big.NewInt(0)) + + poolIDs = append(poolIDs, fmt.Sprintf("%d:%d-%s:%d-%s", poolID, coinToSell, amountIn.String(), coinToBuy, amountOut.String())) + + if i == 0 { + deliverState.Accounts.SubBalance(sender, coinToSell, amountIn) + } + + valueToSell = amountOut + coinToSell = coinToBuy + + if resultCoin == coinToBuy { + deliverState.Accounts.AddBalance(sender, coinToBuy, amountOut) + } + } + + rewardPool.Add(rewardPool, commissionInBaseCoin) + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + amountOut := valueToSell + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_to_buy"), Value: []byte(resultCoin.String()), Index: true}, + {Key: []byte("tx.coin_to_sell"), Value: []byte(data.Coins[0].String()), Index: true}, + {Key: []byte("tx.return"), Value: []byte(amountOut.String())}, + {Key: []byte("tx.sell_amount"), Value: []byte(available.String())}, + {Key: []byte("tx.pools"), Value: []byte(strings.Join(poolIDs, ","))}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} + +type DummyCoin struct { + id types.CoinID + volume *big.Int + reserve *big.Int + crr uint32 + fullSymbol string + maxSupply *big.Int +} + +func NewDummyCoin(id types.CoinID, volume *big.Int, reserve *big.Int, crr uint32, fullSymbol string, maxSupply *big.Int) *DummyCoin { + return &DummyCoin{id: id, volume: volume, reserve: reserve, crr: crr, fullSymbol: fullSymbol, maxSupply: maxSupply} +} + +func (m DummyCoin) ID() types.CoinID { + return m.id +} + +func (m DummyCoin) BaseOrHasReserve() bool { + return m.ID().IsBaseCoin() || (m.Crr() > 0 && m.Reserve().Sign() == 1) +} + +func (m DummyCoin) Volume() *big.Int { + return m.volume +} + +func (m DummyCoin) Reserve() *big.Int { + return m.reserve +} + +func (m DummyCoin) Crr() uint32 { + return m.crr +} + +func (m DummyCoin) GetFullSymbol() string { + return m.fullSymbol +} +func (m DummyCoin) MaxSupply() *big.Int { + return m.maxSupply +} + +type CalculateCoin interface { + ID() types.CoinID + BaseOrHasReserve() bool + Volume() *big.Int + Reserve() *big.Int + Crr() uint32 + GetFullSymbol() string + MaxSupply() *big.Int +} +type gasMethod bool + +func (isGasCommissionFromPoolSwap gasMethod) String() string { + if isGasCommissionFromPoolSwap { + return "pool" + } + return "bancor" +} + +func CalculateCommission(checkState *state.CheckState, swapper swap.EditableChecker, gasCoin CalculateCoin, commissionInBaseCoin *big.Int) (commission *big.Int, poolSwap gasMethod, errResp *Response) { + if gasCoin.ID().IsBaseCoin() { + return new(big.Int).Set(commissionInBaseCoin), false, nil + } + commissionFromPool, responseFromPool := commissionFromPool(swapper, gasCoin, checkState.Coins().GetCoin(types.BasecoinID), commissionInBaseCoin) + commissionFromReserve, responseFromReserve := commissionFromReserve(gasCoin, commissionInBaseCoin) + + if responseFromPool != nil && responseFromReserve != nil { + return nil, false, &Response{ + Code: code.CommissionCoinNotSufficient, + Log: fmt.Sprintf("Not possible to pay commission in coin %s", gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewCommissionCoinNotSufficient(responseFromReserve.Log, responseFromPool.Log)), + } + } + + if responseFromPool == responseFromReserve { + if commissionFromReserve.Cmp(commissionFromPool) == -1 { + return commissionFromReserve, false, nil + } + return commissionFromPool, true, nil + } + + if responseFromPool == nil { + return commissionFromPool, true, nil + } + + return commissionFromReserve, false, nil +} + +func commissionFromPool(swapChecker swap.EditableChecker, coin CalculateCoin, baseCoin CalculateCoin, commissionInBaseCoin *big.Int) (*big.Int, *Response) { + if !swapChecker.IsExist() { + return nil, &Response{ + Code: code.PairNotExists, + Log: fmt.Sprintf("swap pool between coins %s and %s not exists", coin.GetFullSymbol(), types.GetBaseCoin()), + Info: EncodeError(code.NewPairNotExists(coin.ID().String(), types.GetBaseCoinID().String())), + } + } + commission := swapChecker.CalculateSellForBuy(commissionInBaseCoin) + if errResp := CheckSwap(swapChecker, coin, baseCoin, commission, commissionInBaseCoin, true); errResp != nil { + return nil, errResp + } + return commission, nil +} + +func commissionFromReserve(gasCoin CalculateCoin, commissionInBaseCoin *big.Int) (*big.Int, *Response) { + if !gasCoin.BaseOrHasReserve() { + return nil, &Response{ + Code: code.CoinHasNotReserve, + Log: "Gas coin has no reserve", + } + } + errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) + if errResp != nil { + return nil, errResp + } + + return formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin), nil +} diff --git a/core/transaction/sell_all_swap_pool_test.go b/core/transaction/sell_all_swap_pool_test.go new file mode 100644 index 000000000..8cac295c4 --- /dev/null +++ b/core/transaction/sell_all_swap_pool_test.go @@ -0,0 +1,263 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" + "testing" +) + +func TestSellAllWithCommissionFromBancor(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(50000)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(50000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellAllSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MinimumValueToBuy: big.NewInt(99), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeSellAllSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestSellAllWithCommissionFromPool(t *testing.T) { + t.Parallel() + cState := getState() + + coin1 := createTestCoin(cState) + coin := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(50000)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(50000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(1000)), + Coin1: types.GetBaseCoinID(), + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellAllSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + MinimumValueToBuy: big.NewInt(99), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 3, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeSellAllSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} diff --git a/core/transaction/sell_coin.go b/core/transaction/sell_coin.go index f3e6e891e..5846571bc 100644 --- a/core/transaction/sell_coin.go +++ b/core/transaction/sell_coin.go @@ -1,14 +1,13 @@ package transaction import ( - "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -19,247 +18,14 @@ type SellCoinData struct { MinimumValueToBuy *big.Int } -func (data SellCoinData) totalSpend(tx *Transaction, context *state.CheckState) (TotalSpends, []conversion, *big.Int, *Response) { - total := TotalSpends{} - var conversions []conversion - - commissionInBaseCoin := tx.CommissionInBaseCoin() - commissionIncluded := false - - var value *big.Int - - switch { - case data.CoinToSell.IsBaseCoin(): - coin := context.Coins().GetCoin(data.CoinToBuy) - value = formula.CalculatePurchaseReturn(coin.Volume(), coin.Reserve(), coin.Crr(), data.ValueToSell) - if value.Cmp(data.MinimumValueToBuy) == -1 { - return nil, nil, nil, &Response{ - Code: code.MinimumValueToBuyReached, - Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), - Info: EncodeError(code.NewMinimumValueToBuyReached(data.MinimumValueToBuy.String(), value.String(), coin.GetFullSymbol(), coin.ID().String())), - } - } - - if tx.GasCoin == data.CoinToBuy { - commissionIncluded = true - - nVolume := big.NewInt(0).Set(coin.Volume()) - nVolume.Add(nVolume, value) - - nReserveBalance := big.NewInt(0).Set(coin.Reserve()) - nReserveBalance.Add(nReserveBalance, data.ValueToSell) - - commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coin.Crr(), commissionInBaseCoin) - - total.Add(tx.GasCoin, commission) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - if errResp := CheckForCoinSupplyOverflow(coin, value); errResp != nil { - return nil, nil, nil, errResp - } - - total.Add(data.CoinToSell, data.ValueToSell) - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - ToCoin: data.CoinToBuy, - ToAmount: value, - ToReserve: data.ValueToSell, - }) - case data.CoinToBuy.IsBaseCoin(): - coin := context.Coins().GetCoin(data.CoinToSell) - value = formula.CalculateSaleReturn(coin.Volume(), coin.Reserve(), coin.Crr(), data.ValueToSell) - - if value.Cmp(data.MinimumValueToBuy) == -1 { - return nil, nil, nil, &Response{ - Code: code.MinimumValueToBuyReached, - Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), - Info: EncodeError(code.NewMinimumValueToBuyReached(data.MinimumValueToBuy.String(), value.String(), coin.GetFullSymbol(), coin.ID().String())), - } - } - - rValue := big.NewInt(0).Set(value) - valueToSell := data.ValueToSell - - if tx.GasCoin == data.CoinToSell { - commissionIncluded = true - - newVolume := big.NewInt(0).Set(coin.Volume()) - newReserve := big.NewInt(0).Set(coin.Reserve()) - - newVolume.Sub(newVolume, data.ValueToSell) - newReserve.Sub(newReserve, value) - - if newReserve.Cmp(commissionInBaseCoin) == -1 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coin.Reserve().String(), - types.GetBaseCoin(), - commissionInBaseCoin.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coin.GetFullSymbol(), - coin.ID().String(), - coin.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - c := formula.CalculateSaleAmount(newVolume, newReserve, coin.Crr(), commissionInBaseCoin) - - total.Add(tx.GasCoin, c) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: c, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - total.Add(data.CoinToSell, valueToSell) - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - FromAmount: valueToSell, - FromReserve: rValue, - ToCoin: data.CoinToBuy, - }) - default: - coinFrom := context.Coins().GetCoin(data.CoinToSell) - coinTo := context.Coins().GetCoin(data.CoinToBuy) - - valueToSell := big.NewInt(0).Set(data.ValueToSell) - - basecoinValue := formula.CalculateSaleReturn(coinFrom.Volume(), coinFrom.Reserve(), coinFrom.Crr(), data.ValueToSell) - fromReserve := big.NewInt(0).Set(basecoinValue) - - if tx.GasCoin == data.CoinToSell { - commissionIncluded = true - newVolume := big.NewInt(0).Set(coinFrom.Volume()) - newReserve := big.NewInt(0).Set(coinFrom.Reserve()) - - newVolume.Sub(newVolume, data.ValueToSell) - newReserve.Sub(newReserve, basecoinValue) - - if newReserve.Cmp(commissionInBaseCoin) == -1 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coinFrom.Reserve().String(), - types.GetBaseCoin(), - commissionInBaseCoin.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coinFrom.GetFullSymbol(), - coinFrom.ID().String(), - coinFrom.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - c := formula.CalculateSaleAmount(newVolume, newReserve, coinFrom.Crr(), commissionInBaseCoin) - - total.Add(tx.GasCoin, c) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: c, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - value = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), basecoinValue) - - if value.Cmp(data.MinimumValueToBuy) == -1 { - return nil, nil, nil, &Response{ - Code: code.MinimumValueToBuyReached, - Log: fmt.Sprintf("You wanted to get minimum %s, but currently you will get %s", data.MinimumValueToBuy.String(), value.String()), - Info: EncodeError(code.NewMinimumValueToBuyReached(data.MinimumValueToBuy.String(), value.String(), coinTo.GetFullSymbol(), coinTo.ID().String())), - } - } - - if tx.GasCoin == data.CoinToBuy { - commissionIncluded = true - - nVolume := big.NewInt(0).Set(coinTo.Volume()) - nVolume.Add(nVolume, value) - - nReserveBalance := big.NewInt(0).Set(coinTo.Reserve()) - nReserveBalance.Add(nReserveBalance, basecoinValue) - - commission := formula.CalculateSaleAmount(nVolume, nReserveBalance, coinTo.Crr(), commissionInBaseCoin) - - total.Add(tx.GasCoin, commission) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - if errResp := CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { - return nil, nil, nil, errResp - } - - total.Add(data.CoinToSell, valueToSell) - - conversions = append(conversions, conversion{ - FromCoin: data.CoinToSell, - FromAmount: valueToSell, - FromReserve: fromReserve, - ToCoin: data.CoinToBuy, - ToAmount: value, - ToReserve: basecoinValue, - }) - } - - if !commissionIncluded { - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - coin := context.Coins().GetCoin(tx.GasCoin) - - if coin.Reserve().Cmp(commissionInBaseCoin) < 0 { - return nil, nil, nil, &Response{ - Code: code.CoinReserveNotSufficient, - Log: fmt.Sprintf("Gas coin reserve balance is not sufficient for transaction. Has: %s %s, required %s %s", - coin.Reserve().String(), - types.GetBaseCoin(), - commissionInBaseCoin.String(), - types.GetBaseCoin()), - Info: EncodeError(code.NewCoinReserveNotSufficient( - coin.GetFullSymbol(), - coin.ID().String(), - coin.Reserve().String(), - commissionInBaseCoin.String(), - )), - } - } - - commission = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), commissionInBaseCoin) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - total.Add(tx.GasCoin, commission) - } - - return total, conversions, value, nil +func (data SellCoinData) Gas() int { + return gasSellCoin +} +func (data SellCoinData) TxType() TxType { + return TypeSellCoin } -func (data SellCoinData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data SellCoinData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.ValueToSell == nil { return &Response{ Code: code.DecodeError, @@ -268,31 +34,55 @@ func (data SellCoinData) BasicCheck(tx *Transaction, context *state.CheckState) } } - if !context.Coins().Exists(data.CoinToSell) { + coinToSell := context.Coins().GetCoin(data.CoinToSell) + if coinToSell == nil { return &Response{ Code: code.CoinNotExists, - Log: "Coin not exists", + Log: "Coin to sell not exists", Info: EncodeError(code.NewCoinNotExists("", data.CoinToSell.String())), } } - if !context.Coins().Exists(data.CoinToBuy) { + if !coinToSell.BaseOrHasReserve() { + return &Response{ + Code: code.CoinHasNotReserve, + Log: "sell coin has no reserve", + Info: EncodeError(code.NewCoinHasNotReserve( + coinToSell.GetFullSymbol(), + coinToSell.ID().String(), + )), + } + } + + coinToBuy := context.Coins().GetCoin(data.CoinToBuy) + if coinToBuy == nil { return &Response{ Code: code.CoinNotExists, - Log: "Coin not exists", + Log: "Coin to buy not exists", Info: EncodeError(code.NewCoinNotExists("", data.CoinToBuy.String())), } } + if !coinToBuy.BaseOrHasReserve() { + return &Response{ + Code: code.CoinHasNotReserve, + Log: "coin to buy has no reserve", + Info: EncodeError(code.NewCoinHasNotReserve( + coinToBuy.GetFullSymbol(), + coinToBuy.ID().String(), + )), + } + } + if data.CoinToSell == data.CoinToBuy { return &Response{ Code: code.CrossConvert, Log: "\"From\" coin equals to \"to\" coin", Info: EncodeError(code.NewCrossConvert( data.CoinToSell.String(), - context.Coins().GetCoin(data.CoinToSell).GetFullSymbol(), + coinToSell.GetFullSymbol(), data.CoinToBuy.String(), - context.Coins().GetCoin(data.CoinToBuy).GetFullSymbol()), + coinToBuy.GetFullSymbol()), ), } } @@ -305,78 +95,147 @@ func (data SellCoinData) String() string { data.ValueToSell.String(), data.CoinToBuy.String(), data.CoinToSell.String()) } -func (data SellCoinData) Gas() int64 { - return commissions.ConvertTx +func (data SellCoinData) CommissionData(price *commission.Price) *big.Int { + return price.SellBancor } -func (data SellCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data SellCoinData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() + var errResp *Response var checkState *state.CheckState var isCheck bool if checkState, isCheck = context.(*state.CheckState); !isCheck { checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - totalSpends, conversions, value, response := data.totalSpend(tx, checkState) - if response != nil { - return *response + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } - for _, ts := range totalSpends { - if checkState.Accounts().GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { - coin := checkState.Coins().GetCoin(ts.Coin) + coinToSell := data.CoinToSell + coinToBuy := data.CoinToBuy + var coinFrom, coinTo CalculateCoin + coinFrom = checkState.Coins().GetCoin(coinToSell) + coinTo = checkState.Coins().GetCoin(coinToBuy) - return Response{ - Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", - sender.String(), - ts.Value.String(), - coin.GetFullSymbol()), - Info: EncodeError(code.NewInsufficientFunds(sender.String(), ts.Value.String(), coin.GetFullSymbol(), coin.ID().String())), + value := big.NewInt(0).Set(data.ValueToSell) + + if isGasCommissionFromPoolSwap == false && !tx.GasCoin.IsBaseCoin() { + if tx.GasCoin == data.CoinToSell { + coinFrom = DummyCoin{ + id: gasCoin.ID(), + volume: big.NewInt(0).Sub(gasCoin.Volume(), commission), + reserve: big.NewInt(0).Sub(gasCoin.Reserve(), commissionInBaseCoin), + crr: gasCoin.Crr(), + fullSymbol: gasCoin.GetFullSymbol(), + maxSupply: gasCoin.MaxSupply(), + } + } else if tx.GasCoin == data.CoinToBuy { + coinTo = DummyCoin{ + id: gasCoin.ID(), + volume: big.NewInt(0).Sub(gasCoin.Volume(), commission), + reserve: big.NewInt(0).Sub(gasCoin.Reserve(), commissionInBaseCoin), + crr: gasCoin.Crr(), + fullSymbol: gasCoin.GetFullSymbol(), + maxSupply: gasCoin.MaxSupply(), } } } - errResp := checkConversionsReserveUnderflow(conversions, checkState) - if errResp != nil { - return *errResp + if !coinToSell.IsBaseCoin() { + value, errResp = CalculateSaleReturnAndCheck(coinFrom, value) + if errResp != nil { + return *errResp + } } - - if deliverState, ok := context.(*state.State); ok { - for _, ts := range totalSpends { - deliverState.Accounts.SubBalance(sender, ts.Coin, ts.Value) + diffBipReserve := big.NewInt(0).Set(value) + if !coinToBuy.IsBaseCoin() { + value = formula.CalculatePurchaseReturn(coinTo.Volume(), coinTo.Reserve(), coinTo.Crr(), value) + if errResp := CheckForCoinSupplyOverflow(coinTo, value); errResp != nil { + return *errResp } + } - for _, conversion := range conversions { - deliverState.Coins.SubVolume(conversion.FromCoin, conversion.FromAmount) - deliverState.Coins.SubReserve(conversion.FromCoin, conversion.FromReserve) - - deliverState.Coins.AddVolume(conversion.ToCoin, conversion.ToAmount) - deliverState.Coins.AddReserve(conversion.ToCoin, conversion.ToReserve) + if value.Cmp(data.MinimumValueToBuy) == -1 { + return Response{ + Code: code.MinimumValueToBuyReached, + Log: fmt.Sprintf( + "You wanted to buy minimum %s, but currently you need to spend %s to complete tx", + data.MinimumValueToBuy.String(), value.String()), + Info: EncodeError(code.NewMaximumValueToSellReached(data.MinimumValueToBuy.String(), value.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), } + } - rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) + spendInGasCoin := big.NewInt(0).Set(commission) + if tx.GasCoin == coinToSell { + spendInGasCoin.Add(spendInGasCoin, data.ValueToSell) + } else { + if checkState.Accounts().GetBalance(sender, data.CoinToSell).Cmp(value) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), value.String(), coinFrom.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), value.String(), coinFrom.GetFullSymbol(), coinFrom.ID().String())), + } + } + } + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(spendInGasCoin) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), spendInGasCoin.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), spendInGasCoin.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(spendInGasCoin) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), spendInGasCoin.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), spendInGasCoin.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + deliverState.Accounts.SubBalance(sender, data.CoinToSell, data.ValueToSell) + if !data.CoinToSell.IsBaseCoin() { + deliverState.Coins.SubVolume(data.CoinToSell, data.ValueToSell) + deliverState.Coins.SubReserve(data.CoinToSell, diffBipReserve) + } deliverState.Accounts.AddBalance(sender, data.CoinToBuy, value) + if !data.CoinToBuy.IsBaseCoin() { + deliverState.Coins.AddVolume(data.CoinToBuy, value) + deliverState.Coins.AddReserve(data.CoinToBuy, diffBipReserve) + } deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeSellCoin)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.coin_to_buy"), Value: []byte(data.CoinToBuy.String())}, - kv.Pair{Key: []byte("tx.coin_to_sell"), Value: []byte(data.CoinToSell.String())}, - kv.Pair{Key: []byte("tx.return"), Value: []byte(value.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_to_buy"), Value: []byte(data.CoinToBuy.String()), Index: true}, + {Key: []byte("tx.coin_to_sell"), Value: []byte(data.CoinToSell.String()), Index: true}, + {Key: []byte("tx.return"), Value: []byte(value.String())}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/sell_coin_test.go b/core/transaction/sell_coin_test.go index 651157395..cb98cb769 100644 --- a/core/transaction/sell_coin_test.go +++ b/core/transaction/sell_coin_test.go @@ -14,6 +14,7 @@ import ( ) func TestSellCoinTx(t *testing.T) { + t.Parallel() cState := getState() buyCoinID := createTestCoin(cState) @@ -76,10 +77,13 @@ func TestSellCoinTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", getTestCoinSymbol(), targetTestBalance, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxBaseToCustomBaseCommission(t *testing.T) { + t.Parallel() // sell_coin: MNT // buy_coin: TEST // gas_coin: MNT @@ -122,7 +126,12 @@ func TestSellCoinTxBaseToCustomBaseCommission(t *testing.T) { // check sold coins + commission sellCoinBalance := cState.Accounts.GetBalance(addr, coinToSell) estimatedSellCoinBalance := big.NewInt(0).Set(initialBalance) - estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, tx.CommissionInBaseCoin()) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, commissionInBaseCoin) estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, toSell) if sellCoinBalance.Cmp(estimatedSellCoinBalance) != 0 { t.Fatalf("Sell coin balance is not correct") @@ -143,10 +152,13 @@ func TestSellCoinTxBaseToCustomBaseCommission(t *testing.T) { t.Fatalf("Wrong coin supply") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxCustomToBaseBaseCommission(t *testing.T) { + t.Parallel() // sell_coin: TEST // buy_coin: MNT // gas_coin: MNT @@ -187,7 +199,12 @@ func TestSellCoinTxCustomToBaseBaseCommission(t *testing.T) { buyCoinBalance := cState.Accounts.GetBalance(addr, coinToBuy) estimatedBuyBalance := formula.CalculateSaleReturn(initialVolume, initialReserve, crr, toSell) estimatedBuyBalance.Add(estimatedBuyBalance, initialGasBalance) - estimatedBuyBalance.Sub(estimatedBuyBalance, tx.CommissionInBaseCoin()) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + estimatedBuyBalance.Sub(estimatedBuyBalance, commissionInBaseCoin) if buyCoinBalance.Cmp(estimatedBuyBalance) != 0 { t.Fatalf("Buy coin balance is not correct. Expected %s, got %s", estimatedBuyBalance.String(), buyCoinBalance.String()) } @@ -215,10 +232,13 @@ func TestSellCoinTxCustomToBaseBaseCommission(t *testing.T) { t.Fatalf("Wrong coin supply") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxCustomToCustomBaseCommission(t *testing.T) { + t.Parallel() // sell_coin: TEST1 // buy_coin: TEST2 // gas_coin: MNT @@ -304,10 +324,13 @@ func TestSellCoinTxCustomToCustomBaseCommission(t *testing.T) { } } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxBaseToCustomCustomCommission(t *testing.T) { + t.Parallel() // sell_coin: MNT // buy_coin: TEST // gas_coin: TEST @@ -348,7 +371,12 @@ func TestSellCoinTxBaseToCustomCustomCommission(t *testing.T) { estimatedReturn := formula.CalculatePurchaseReturn(initialVolume, initialReserve, crr, toSell) estimatedBuyBalance := big.NewInt(0).Set(estimatedReturn) estimatedBuyBalance.Add(estimatedBuyBalance, initialGasBalance) - estimatedBuyBalance.Sub(estimatedBuyBalance, formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume, estimatedReturn), big.NewInt(0).Add(initialReserve, toSell), crr, tx.CommissionInBaseCoin())) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + estimatedBuyBalance.Sub(estimatedBuyBalance, formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume, estimatedReturn), big.NewInt(0).Add(initialReserve, toSell), crr, commissionInBaseCoin)) if buyCoinBalance.Cmp(estimatedBuyBalance) != 0 { t.Fatalf("Buy coin balance is not correct. Expected %s, got %s", estimatedBuyBalance.String(), buyCoinBalance.String()) } @@ -366,22 +394,25 @@ func TestSellCoinTxBaseToCustomCustomCommission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve) estimatedReserve.Add(estimatedReserve, toSell) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve") } estimatedSupply := big.NewInt(0).Set(initialVolume) estimatedSupply.Add(estimatedSupply, formula.CalculatePurchaseReturn(initialVolume, initialReserve, crr, toSell)) - estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume, estimatedReturn), big.NewInt(0).Add(initialReserve, toSell), crr, tx.CommissionInBaseCoin())) + estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume, estimatedReturn), big.NewInt(0).Add(initialReserve, toSell), crr, commissionInBaseCoin)) if coinData.Volume().Cmp(estimatedSupply) != 0 { t.Fatalf("Wrong coin supply") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxCustomToBaseCustomCommission(t *testing.T) { + t.Parallel() // sell_coin: TEST // buy_coin: MNT // gas_coin: TEST @@ -415,6 +446,8 @@ func TestSellCoinTxCustomToBaseCustomCommission(t *testing.T) { t.Fatalf("Response code is not 0. Error %s", response.Log) } + // FIXME: incorrectly because sell+gas return more + // t.SkipNow() // check received coins buyCoinBalance := cState.Accounts.GetBalance(addr, coinToBuy) estimatedReturn := formula.CalculateSaleReturn(initialVolume, initialReserve, crr, toSell) @@ -427,7 +460,12 @@ func TestSellCoinTxCustomToBaseCustomCommission(t *testing.T) { sellCoinBalance := cState.Accounts.GetBalance(addr, coinToSellID) estimatedSellCoinBalance := big.NewInt(0).Set(initialBalance) estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, toSell) - estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume, toSell), big.NewInt(0).Sub(initialReserve, estimatedReturn), crr, tx.CommissionInBaseCoin())) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume, toSell), big.NewInt(0).Sub(initialReserve, estimatedReturn), crr, commissionInBaseCoin)) if sellCoinBalance.Cmp(estimatedSellCoinBalance) != 0 { t.Fatalf("Sell coin balance is not correct. Expected %s, got %s", estimatedSellCoinBalance.String(), sellCoinBalance.String()) } @@ -437,22 +475,26 @@ func TestSellCoinTxCustomToBaseCustomCommission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve) estimatedReserve.Sub(estimatedReserve, estimatedReturn) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve") } estimatedSupply := big.NewInt(0).Set(initialVolume) estimatedSupply.Sub(estimatedSupply, toSell) - estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume, toSell), big.NewInt(0).Sub(initialReserve, estimatedReturn), crr, tx.CommissionInBaseCoin())) + estimatedSupply.Sub(estimatedSupply, formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume, toSell), big.NewInt(0).Sub(initialReserve, estimatedReturn), crr, commissionInBaseCoin)) if coinData.Volume().Cmp(estimatedSupply) != 0 { t.Fatalf("Wrong coin supply") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxCustomToCustomCustom1Commission(t *testing.T) { + t.Parallel() // sell_coin: TEST1 // buy_coin: TEST2 // gas_coin: TEST1 @@ -487,6 +529,8 @@ func TestSellCoinTxCustomToCustomCustom1Commission(t *testing.T) { t.Fatalf("Response code is not 0. Error %s", response.Log) } + // FIXME: incorrectly because sell+gas return more + // t.SkipNow() // check received coins buyCoinBalance := cState.Accounts.GetBalance(addr, coinToBuyID) bipReturn := formula.CalculateSaleReturn(initialVolume1, initialReserve1, crr1, toSell) @@ -499,7 +543,12 @@ func TestSellCoinTxCustomToCustomCustom1Commission(t *testing.T) { sellCoinBalance := cState.Accounts.GetBalance(addr, coinToSellID) estimatedSellCoinBalance := big.NewInt(0).Set(initialBalance) estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, toSell) - commission := formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume1, toSell), big.NewInt(0).Sub(initialReserve1, bipReturn), crr1, tx.CommissionInBaseCoin()) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + commission := formula.CalculateSaleAmount(big.NewInt(0).Sub(initialVolume1, toSell), big.NewInt(0).Sub(initialReserve1, bipReturn), crr1, commissionInBaseCoin) estimatedSellCoinBalance.Sub(estimatedSellCoinBalance, commission) if sellCoinBalance.Cmp(estimatedSellCoinBalance) != 0 { t.Fatalf("Sell coin balance is not correct. Expected %s, got %s", estimatedSellCoinBalance.String(), sellCoinBalance.String()) @@ -511,7 +560,7 @@ func TestSellCoinTxCustomToCustomCustom1Commission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve1) estimatedReserve.Sub(estimatedReserve, formula.CalculateSaleReturn(initialVolume1, initialReserve1, crr1, toSell)) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve") } @@ -540,10 +589,13 @@ func TestSellCoinTxCustomToCustomCustom1Commission(t *testing.T) { } } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxCustomToCustomCustom2Commission(t *testing.T) { + t.Parallel() // sell_coin: TEST1 // buy_coin: TEST2 // gas_coin: TEST2 @@ -587,7 +639,12 @@ func TestSellCoinTxCustomToCustomCustom2Commission(t *testing.T) { buyCoinBalance := cState.Accounts.GetBalance(addr, coinToBuyID) bipReturn := formula.CalculateSaleReturn(initialVolume1, initialReserve1, crr1, toSell) estimatedReturn := formula.CalculatePurchaseReturn(initialVolume2, initialReserve2, crr2, bipReturn) - commission := formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume2, estimatedReturn), big.NewInt(0).Add(initialReserve2, bipReturn), crr2, tx.CommissionInBaseCoin()) + commissions := cState.Commission.GetCommissions() + commissionInBaseCoin := tx.Commission(tx.Price(commissions)) + if !commissions.Coin.IsBaseCoin() { + commissionInBaseCoin = cState.Swap.GetSwapper(types.GetBaseCoinID(), commissions.Coin).CalculateSellForBuy(commissionInBaseCoin) + } + commission := formula.CalculateSaleAmount(big.NewInt(0).Add(initialVolume2, estimatedReturn), big.NewInt(0).Add(initialReserve2, bipReturn), crr2, commissionInBaseCoin) estimatedBuyBalance := big.NewInt(0).Set(estimatedReturn) estimatedBuyBalance.Sub(estimatedBuyBalance, commission) @@ -626,7 +683,7 @@ func TestSellCoinTxCustomToCustomCustom2Commission(t *testing.T) { estimatedReserve := big.NewInt(0).Set(initialReserve2) estimatedReserve.Add(estimatedReserve, formula.CalculateSaleReturn(initialVolume1, initialReserve1, crr1, toSell)) - estimatedReserve.Sub(estimatedReserve, tx.CommissionInBaseCoin()) + estimatedReserve.Sub(estimatedReserve, commissionInBaseCoin) if coinData.Reserve().Cmp(estimatedReserve) != 0 { t.Fatalf("Wrong coin reserve") } @@ -639,10 +696,13 @@ func TestSellCoinTxCustomToCustomCustom2Commission(t *testing.T) { } } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxToCoinSupplyOverflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID, sellCoinID := createTestCoin(cState), types.GetBaseCoinID() @@ -667,7 +727,9 @@ func TestSellCoinTxToCoinSupplyOverflow(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinSupplyOverflow, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // custom buy and sell coins @@ -701,10 +763,13 @@ func TestSellCoinTxToCoinSupplyOverflow(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinSupplyOverflow, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxToMinimumValueToBuyReached(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID, sellCoinID := createTestCoin(cState), types.GetBaseCoinID() @@ -749,7 +814,9 @@ func TestSellCoinTxToMinimumValueToBuyReached(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.MinimumValueToBuyReached, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // coin to buy == base coin @@ -780,7 +847,9 @@ func TestSellCoinTxToMinimumValueToBuyReached(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.MinimumValueToBuyReached, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // custom buy and sell coins @@ -824,10 +893,13 @@ func TestSellCoinTxToMinimumValueToBuyReached(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.MinimumValueToBuyReached, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxToCoinReserveNotSufficient(t *testing.T) { + t.Parallel() cState := getState() privateKey, addr := getAccount() coinToBuyID, coinToSellID := createTestCoin(cState), types.GetBaseCoinID() @@ -861,10 +933,12 @@ func TestSellCoinTxToCoinReserveNotSufficient(t *testing.T) { response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) if response.Code != code.CoinReserveNotSufficient { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveNotSufficient, response.Log) + t.Fatalf("Response code is not %d. Error %d %s", code.CoinReserveNotSufficient, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } // gas coin == coin to sell @@ -881,14 +955,17 @@ func TestSellCoinTxToCoinReserveNotSufficient(t *testing.T) { } response = RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveNotSufficient { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveNotSufficient, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d %d. Error %s", code.CommissionCoinNotSufficient, response.Code, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() coinToBuyID, coinToSellID := createTestCoin(cState), types.GetBaseCoinID() @@ -914,10 +991,13 @@ func TestSellCoinTxInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxEqualCoins(t *testing.T) { + t.Parallel() cState := getState() coinID := createTestCoin(cState) privateKey, _ := crypto.GenerateKey() @@ -937,10 +1017,13 @@ func TestSellCoinTxEqualCoins(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CrossConvert, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSellCoinTxToNonExistCoins(t *testing.T) { + t.Parallel() cState := getState() coinID := createTestCoin(cState) privateKey, _ := crypto.GenerateKey() @@ -975,7 +1058,9 @@ func TestSellCoinTxToNonExistCoins(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } tx = createSellCoinTx(coinID, types.GetBaseCoinID(), 5, big.NewInt(1), 1) if err := tx.Sign(privateKey); err != nil { @@ -992,7 +1077,9 @@ func TestSellCoinTxToNonExistCoins(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func createSellCoinTx(sellCoin, buyCoin, gasCoin types.CoinID, valueToSell *big.Int, nonce uint64) *Transaction { diff --git a/core/transaction/sell_swap_pool.go b/core/transaction/sell_swap_pool.go new file mode 100644 index 000000000..3b9705815 --- /dev/null +++ b/core/transaction/sell_swap_pool.go @@ -0,0 +1,213 @@ +package transaction + +import ( + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" + "strings" +) + +type SellSwapPoolData struct { + Coins []types.CoinID + ValueToSell *big.Int + MinimumValueToBuy *big.Int +} + +func (data SellSwapPoolData) TxType() TxType { + return TypeSellSwapPool +} + +func (data SellSwapPoolData) Gas() int { + return gasSellSwapPool +} + +func (data SellSwapPoolData) basicCheck(tx *Transaction, context *state.CheckState) *Response { + if len(data.Coins) < 2 { + return &Response{ + Code: code.DecodeError, + Log: "Incorrect tx data", + Info: EncodeError(code.NewDecodeError()), + } + } + if len(data.Coins) > 5 { + return &Response{ + Code: code.TooLongSwapRoute, + Log: "maximum allowed length of the exchange chain is 5", + Info: EncodeError(code.NewCustomCode(code.TooLongSwapRoute)), + } + } + coin0 := data.Coins[0] + for _, coin1 := range data.Coins[1:] { + if coin0 == coin1 { + return &Response{ + Code: code.CrossConvert, + Log: "\"From\" coin equals to \"to\" coin", + Info: EncodeError(code.NewCrossConvert( + coin0.String(), "", + coin1.String(), "")), + } + } + if !context.Swap().SwapPoolExist(coin0, coin1) { + return &Response{ + Code: code.PairNotExists, + Log: fmt.Sprint("swap pool not exists"), + Info: EncodeError(code.NewPairNotExists(coin0.String(), coin1.String())), + } + } + coin0 = coin1 + } + + return nil +} + +func (data SellSwapPoolData) String() string { + return fmt.Sprintf("SWAP POOL SELL") +} + +func (data SellSwapPoolData) CommissionData(price *commission.Price) *big.Int { + return new(big.Int).Add(price.SellPoolBase, new(big.Int).Mul(price.SellPoolDelta, big.NewInt(int64(len(data.Coins))-2))) +} + +func (data SellSwapPoolData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + { + coinToSell := data.Coins[0] + coinToSellModel := checkState.Coins().GetCoin(coinToSell) + resultCoin := data.Coins[len(data.Coins)-1] + valueToSell := data.ValueToSell + valueToBuy := big.NewInt(0) + for _, coinToBuy := range data.Coins[1:] { + swapper := checkState.Swap().GetSwapper(coinToSell, coinToBuy) + if isGasCommissionFromPoolSwap { + if tx.GasCoin == coinToSell && coinToBuy.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commission, commissionInBaseCoin) + } + if tx.GasCoin == coinToBuy && coinToSell.IsBaseCoin() { + swapper = swapper.AddLastSwapStep(commissionInBaseCoin, commission) + } + } + + if coinToBuy == resultCoin { + valueToBuy = data.MinimumValueToBuy + } + + coinToBuyModel := checkState.Coins().GetCoin(coinToBuy) + errResp = CheckSwap(swapper, coinToSellModel, coinToBuyModel, valueToSell, valueToBuy, false) + if errResp != nil { + return *errResp + } + + valueToSellCalc := swapper.CalculateBuyForSell(valueToSell) + if valueToSellCalc == nil { + reserve0, reserve1 := swapper.Reserves() + return Response{ // todo + Code: code.SwapPoolUnknown, + Log: fmt.Sprintf("swap pool has reserves %s %s and %d %s, you wanted sell %s %s", reserve0, coinToSellModel.GetFullSymbol(), reserve1, coinToBuyModel.GetFullSymbol(), valueToSell, coinToSellModel.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientLiquidity(coinToSellModel.ID().String(), valueToSell.String(), coinToBuyModel.ID().String(), valueToSellCalc.String(), reserve0.String(), reserve1.String())), + } + } + valueToSell = valueToSellCalc + coinToSellModel = coinToBuyModel + coinToSell = coinToBuy + } + } + + coinToSell := data.Coins[0] + amount0 := new(big.Int).Set(data.ValueToSell) + if tx.GasCoin != coinToSell { + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) == -1 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + } else { + amount0.Add(amount0, commission) + } + if checkState.Accounts().GetBalance(sender, coinToSell).Cmp(amount0) == -1 { + symbol := checkState.Coins().GetCoin(coinToSell).GetFullSymbol() + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), amount0.String(), symbol), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), amount0.String(), symbol, coinToSell.String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + coinToSell := data.Coins[0] + resultCoin := data.Coins[len(data.Coins)-1] + valueToSell := data.ValueToSell + + var poolIDs []string + + for i, coinToBuy := range data.Coins[1:] { + amountIn, amountOut, poolID := deliverState.Swap.PairSell(coinToSell, coinToBuy, valueToSell, big.NewInt(0)) + + poolIDs = append(poolIDs, fmt.Sprintf("%d:%d-%s:%d-%s", poolID, coinToSell, amountIn.String(), coinToBuy, amountOut.String())) + + if i == 0 { + deliverState.Accounts.SubBalance(sender, coinToSell, amountIn) + } + + valueToSell = amountOut + coinToSell = coinToBuy + + if resultCoin == coinToBuy { + deliverState.Accounts.AddBalance(sender, coinToBuy, amountOut) + } + } + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + amountOut := valueToSell + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.coin_to_buy"), Value: []byte(resultCoin.String()), Index: true}, + {Key: []byte("tx.coin_to_sell"), Value: []byte(data.Coins[0].String()), Index: true}, + {Key: []byte("tx.return"), Value: []byte(amountOut.String())}, + {Key: []byte("tx.pools"), Value: []byte(strings.Join(poolIDs, ","))}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/sell_swap_pool_test.go b/core/transaction/sell_swap_pool_test.go new file mode 100644 index 000000000..6257e1833 --- /dev/null +++ b/core/transaction/sell_swap_pool_test.go @@ -0,0 +1,927 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" + "testing" +) + +func TestSellSwapPoolTx_0(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(100)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + ValueToSell: big.NewInt(10), + MinimumValueToBuy: big.NewInt(99), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestSellSwapPoolTx_1(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(10)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(10)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + ValueToSell: big.NewInt(10), + MinimumValueToBuy: big.NewInt(9), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin1, coin}, + ValueToSell: big.NewInt(10), + MinimumValueToBuy: big.NewInt(9), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 3, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestSellSwapPoolTx_2(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)), big.NewInt(0)), + Coin1: coin1, + Volume1: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1000), big.NewInt(1e18)), big.NewInt(0)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + ValueToSell: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18)), big.NewInt(0)), + MinimumValueToBuy: big.NewInt(996006981039903216), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestSellSwapPoolTx_3(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: new(big.Int).Add(new(big.Int).Mul(big.NewInt(5), big.NewInt(1e18)), big.NewInt(0)), + Coin1: coin1, + Volume1: new(big.Int).Add(new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)), big.NewInt(0)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + ValueToSell: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18)), big.NewInt(0)), + MinimumValueToBuy: big.NewInt(1662497915624478906), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestSellSwapPoolTx_4(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: new(big.Int).Add(new(big.Int).Mul(big.NewInt(10), big.NewInt(1e18)), big.NewInt(0)), + Coin1: coin1, + Volume1: new(big.Int).Add(new(big.Int).Mul(big.NewInt(5), big.NewInt(1e18)), big.NewInt(0)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + ValueToSell: new(big.Int).Add(new(big.Int).Mul(big.NewInt(1), big.NewInt(1e18)), big.NewInt(0)), + MinimumValueToBuy: big.NewInt(453305446940074565), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestSellSwapPoolTx_RouteYes(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + coin2 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin2, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin2, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(100)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := CreateSwapPoolData{ + Coin0: coin1, + Volume0: helpers.BipToPip(big.NewInt(1000)), + Coin1: coin2, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin, coin1, coin2}, + ValueToSell: big.NewInt(10), + MinimumValueToBuy: big.NewInt(98), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 3, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestSellSwapPoolTx_Route(t *testing.T) { + t.Parallel() + cState := getState() + + coin := createTestCoin(cState) + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + + { + data := CreateSwapPoolData{ + Coin0: coin, + Volume0: helpers.BipToPip(big.NewInt(100)), + Coin1: coin1, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + coin2 := createNonReserveCoin(cState) + cState.Accounts.SubBalance(types.Address{}, coin2, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin2, helpers.BipToPip(big.NewInt(100000))) + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin, coin1}, + ValueToSell: big.NewInt(10), + MinimumValueToBuy: big.NewInt(99), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := CreateSwapPoolData{ + Coin0: coin1, + Volume0: helpers.BipToPip(big.NewInt(1000)), + Coin1: coin2, + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 3, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SellSwapPoolData{ + Coins: []types.CoinID{coin1, coin2}, + ValueToSell: big.NewInt(99), + MinimumValueToBuy: big.NewInt(98), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 4, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeSellSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s, %s", response.Code, response.Log, response.Info) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} diff --git a/core/transaction/send.go b/core/transaction/send.go index f30418edf..8a60f9a3e 100644 --- a/core/transaction/send.go +++ b/core/transaction/send.go @@ -4,11 +4,10 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -18,42 +17,20 @@ type SendData struct { Value *big.Int } -type Coin struct { - ID uint32 `json:"id"` - Symbol string `json:"symbol"` +func (data SendData) TxType() TxType { + return TypeSend } -func (data SendData) totalSpend(tx *Transaction, context *state.CheckState) (TotalSpends, []conversion, *big.Int, *Response) { - total := TotalSpends{} - var conversions []conversion - - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - coin := context.Coins().GetCoin(tx.GasCoin) - - errResp := CheckReserveUnderflow(coin, commissionInBaseCoin) - if errResp != nil { - return nil, nil, nil, errResp - } - - commission = formula.CalculateSaleAmount(coin.Volume(), coin.Reserve(), coin.Crr(), commissionInBaseCoin) - conversions = append(conversions, conversion{ - FromCoin: tx.GasCoin, - FromAmount: commission, - FromReserve: commissionInBaseCoin, - ToCoin: types.GetBaseCoinID(), - }) - } - - total.Add(tx.GasCoin, commission) - total.Add(data.Coin, data.Value) +func (data SendData) Gas() int { + return gasSend +} - return total, conversions, nil, nil +type Coin struct { + ID uint32 `json:"id"` + Symbol string `json:"symbol"` } -func (data SendData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data SendData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.Value == nil { return &Response{ Code: code.DecodeError, @@ -78,11 +55,11 @@ func (data SendData) String() string { data.To.String(), data.Coin.String(), data.Value.String()) } -func (data SendData) Gas() int64 { - return commissions.SendTx +func (data SendData) CommissionData(price *commission.Price) *big.Int { + return price.Send } -func (data SendData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data SendData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -91,60 +68,64 @@ func (data SendData) Run(tx *Transaction, context state.Interface, rewardPool *b checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - totalSpends, conversions, _, response := data.totalSpend(tx, checkState) - if response != nil { - return *response + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } - for _, ts := range totalSpends { - if checkState.Accounts().GetBalance(sender, ts.Coin).Cmp(ts.Value) < 0 { - coin := checkState.Coins().GetCoin(ts.Coin) - + needValue := big.NewInt(0).Set(commission) + if tx.GasCoin == data.Coin { + needValue.Add(data.Value, needValue) + } else { + if checkState.Accounts().GetBalance(sender, data.Coin).Cmp(data.Value) < 0 { + coin := checkState.Coins().GetCoin(data.Coin) return Response{ Code: code.InsufficientFunds, - Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s.", - sender.String(), - ts.Value.String(), - coin.GetFullSymbol()), - Info: EncodeError(code.NewInsufficientFunds(sender.String(), ts.Value.String(), coin.GetFullSymbol(), coin.ID().String())), + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), data.Value.String(), coin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), data.Value.String(), coin.GetFullSymbol(), coin.ID().String())), } } } - - if deliverState, ok := context.(*state.State); ok { - for _, ts := range totalSpends { - deliverState.Accounts.SubBalance(sender, ts.Coin, ts.Value) + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(needValue) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), needValue.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), needValue.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), } - - for _, conversion := range conversions { - deliverState.Coins.SubVolume(conversion.FromCoin, conversion.FromAmount) - deliverState.Coins.SubReserve(conversion.FromCoin, conversion.FromReserve) - - deliverState.Coins.AddVolume(conversion.ToCoin, conversion.ToAmount) - deliverState.Coins.AddReserve(conversion.ToCoin, conversion.ToReserve) + } + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) } - - rewardPool.Add(rewardPool, tx.CommissionInBaseCoin()) + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + deliverState.Accounts.SubBalance(sender, data.Coin, data.Value) deliverState.Accounts.AddBalance(data.To, data.Coin, data.Value) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeSend)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, - kv.Pair{Key: []byte("tx.to"), Value: []byte(hex.EncodeToString(data.To[:]))}, - kv.Pair{Key: []byte("tx.coin_id"), Value: []byte(data.Coin.String())}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.to"), Value: []byte(hex.EncodeToString(data.To[:])), Index: true}, + {Key: []byte("tx.coin_id"), Value: []byte(data.Coin.String()), Index: true}, + } } return Response{ - Code: code.OK, - Tags: tags, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/send_test.go b/core/transaction/send_test.go index 88fe8af1c..b6fda7ac9 100644 --- a/core/transaction/send_test.go +++ b/core/transaction/send_test.go @@ -14,6 +14,7 @@ import ( ) func TestSendTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -72,10 +73,13 @@ func TestSendTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", to.String(), targetTestBalance, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSendMultisigTx(t *testing.T) { + t.Parallel() cState := getState() privateKey1, _ := crypto.GenerateKey() @@ -142,10 +146,13 @@ func TestSendMultisigTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", to.String(), targetTestBalance, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSendFailedMultisigTx(t *testing.T) { + t.Parallel() cState := getState() privateKey1, _ := crypto.GenerateKey() @@ -212,10 +219,13 @@ func TestSendFailedMultisigTx(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", to.String(), targetTestBalance, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSendWithNotExistedCoin(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -259,10 +269,13 @@ func TestSendWithNotExistedCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSendTxWithCustomCoin(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -322,10 +335,13 @@ func TestSendTxWithCustomCoin(t *testing.T) { t.Fatalf("Target %s balance is not correct. Expected %s, got %s", to.String(), targetTestBalance, testBalance) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSendTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -370,9 +386,11 @@ func TestSendTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error: %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error: %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/set_halt_block.go b/core/transaction/set_halt_block.go index 288a1da1b..32fc08f44 100644 --- a/core/transaction/set_halt_block.go +++ b/core/transaction/set_halt_block.go @@ -3,16 +3,15 @@ package transaction import ( "encoding/hex" "fmt" + "github.com/MinterTeam/minter-go-node/core/state/commission" "math/big" "strconv" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" "github.com/MinterTeam/minter-go-node/hexutil" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" ) type SetHaltBlockData struct { @@ -20,16 +19,24 @@ type SetHaltBlockData struct { Height uint64 } +func (data SetHaltBlockData) Gas() int { + return gasSetHaltBlock +} + +func (data SetHaltBlockData) TxType() TxType { + return TypeSetHaltBlock +} + func (data SetHaltBlockData) GetPubKey() types.Pubkey { return data.PubKey } -func (data SetHaltBlockData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { - if !context.Candidates().Exists(data.PubKey) { +func (data SetHaltBlockData) basicCheck(tx *Transaction, context *state.CheckState, block uint64) *Response { + if data.Height < block { return &Response{ - Code: code.CandidateNotFound, - Log: "Candidate with such public key not found", - Info: EncodeError(code.NewCandidateNotFound(data.PubKey.String())), + Code: code.VoiceExpired, + Log: fmt.Sprintf("Halt height should be equal or bigger than current: %d", block), + Info: EncodeError(code.NewVoiceExpired(strconv.FormatUint(data.Height, 10), data.GetPubKey().String())), } } @@ -37,7 +44,7 @@ func (data SetHaltBlockData) BasicCheck(tx *Transaction, context *state.CheckSta return &Response{ Code: code.HaltAlreadyExists, Log: "Halt with such public key and height already exists", - Info: EncodeError(code.NewWrongHaltHeight(strconv.FormatUint(data.Height, 10), data.GetPubKey().String())), + Info: EncodeError(code.NewHaltAlreadyExists(strconv.FormatUint(data.Height, 10), data.GetPubKey().String())), } } @@ -49,11 +56,11 @@ func (data SetHaltBlockData) String() string { hexutil.Encode(data.PubKey[:]), data.Height) } -func (data SetHaltBlockData) Gas() int64 { - return commissions.SetHaltBlock +func (data SetHaltBlockData) CommissionData(price *commission.Price) *big.Int { + return price.SetHaltBlock } -func (data SetHaltBlockData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data SetHaltBlockData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -62,31 +69,17 @@ func (data SetHaltBlockData) Run(tx *Transaction, context state.Interface, rewar checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState, currentBlock) if response != nil { return *response } - if data.Height < currentBlock { - return Response{ - Code: code.WrongHaltHeight, - Log: fmt.Sprintf("Halt height should be equal or bigger than current: %d", currentBlock), - Info: EncodeError(code.NewWrongHaltHeight(strconv.FormatUint(data.Height, 10), data.GetPubKey().String())), - } - } - - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - - if !tx.GasCoin.IsBaseCoin() { - gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -99,26 +92,29 @@ func (data SetHaltBlockData) Run(tx *Transaction, context state.Interface, rewar } } + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) deliverState.Halts.AddHaltBlock(data.Height, data.PubKey) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeSetHaltBlock)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/set_halt_block_test.go b/core/transaction/set_halt_block_test.go index a4bec088e..670dad320 100644 --- a/core/transaction/set_halt_block_test.go +++ b/core/transaction/set_halt_block_test.go @@ -17,7 +17,8 @@ import ( ) func TestSetHaltBlockTx(t *testing.T) { - cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1, 500000) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -30,9 +31,9 @@ func TestSetHaltBlockTx(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) - cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1))) + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(100))) data := SetHaltBlockData{ PubKey: pubkey, @@ -90,12 +91,15 @@ func TestSetHaltBlockTx(t *testing.T) { t.Fatalf("Wront halt block pubkey. Expected pubkey: %s, got %s", pubkey, haltBlock.Pubkey.String()+"asd") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSetHaltBlockTxWithWrongHeight(t *testing.T) { + t.Parallel() currentHeight := uint64(500000 + 5) - cState, err := state.NewState(currentHeight, db.NewMemDB(), nil, 1, 1) + cState, err := state.NewState(currentHeight, db.NewMemDB(), nil, 1, 1, currentHeight) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -108,7 +112,7 @@ func TestSetHaltBlockTxWithWrongHeight(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1))) @@ -143,8 +147,8 @@ func TestSetHaltBlockTxWithWrongHeight(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), currentHeight, &sync.Map{}, 0) - if response.Code != code.WrongHaltHeight { - t.Fatalf("Response code is not %d", code.WrongHaltHeight) + if response.Code != code.VoiceExpired { + t.Fatalf("Response code is not %d", code.VoiceExpired) } halts := cState.Halts.GetHaltBlocks(haltHeight) @@ -152,12 +156,15 @@ func TestSetHaltBlockTxWithWrongHeight(t *testing.T) { t.Fatalf("Halts found at height: %d", haltHeight) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSetHaltBlockTxWithWrongOwnership(t *testing.T) { + t.Parallel() currentHeight := uint64(500000 + 5) - cState, err := state.NewState(currentHeight, db.NewMemDB(), nil, 1, 1) + cState, err := state.NewState(currentHeight, db.NewMemDB(), nil, 1, 1, currentHeight) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -170,7 +177,7 @@ func TestSetHaltBlockTxWithWrongOwnership(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1))) @@ -215,11 +222,14 @@ func TestSetHaltBlockTxWithWrongOwnership(t *testing.T) { t.Fatalf("Halts found at height: %d", haltHeight) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSetHaltBlockTxToNonExistCandidate(t *testing.T) { - cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1, 500000) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -271,11 +281,14 @@ func TestSetHaltBlockTxToNonExistCandidate(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CandidateNotFound, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSetHaltBlockTxToInsufficientFunds(t *testing.T) { - cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1, 500000) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -288,7 +301,7 @@ func TestSetHaltBlockTxToInsufficientFunds(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) data := SetHaltBlockData{ @@ -326,11 +339,14 @@ func TestSetHaltBlockTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSetHaltBlockTxToGasCoinReserveUnderflow(t *testing.T) { - cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1, 500000) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -348,7 +364,7 @@ func TestSetHaltBlockTxToGasCoinReserveUnderflow(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) data := SetHaltBlockData{ @@ -381,15 +397,18 @@ func TestSetHaltBlockTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 500000, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSetHaltBlockTxToAlreadyExistenHalt(t *testing.T) { - cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1) + t.Parallel() + cState, err := state.NewState(500000, db.NewMemDB(), nil, 1, 1, 500000) if err != nil { t.Fatalf("Cannot load state. Error %s", err) } @@ -402,7 +421,7 @@ func TestSetHaltBlockTxToAlreadyExistenHalt(t *testing.T) { pubkey := [32]byte{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1))) cState.Halts.AddHaltBlock(haltHeight, pubkey) @@ -442,5 +461,7 @@ func TestSetHaltBlockTxToAlreadyExistenHalt(t *testing.T) { t.Fatalf("response code is not %d. Error %s", code.HaltAlreadyExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/switch_candidate_status.go b/core/transaction/switch_candidate_status.go index 54995e41f..62f7d447b 100644 --- a/core/transaction/switch_candidate_status.go +++ b/core/transaction/switch_candidate_status.go @@ -4,11 +4,10 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) @@ -16,11 +15,19 @@ type SetCandidateOnData struct { PubKey types.Pubkey } +func (data SetCandidateOnData) Gas() int { + return gasSetCandidateOnline +} + +func (data SetCandidateOnData) TxType() TxType { + return TypeSetCandidateOnline +} + func (data SetCandidateOnData) GetPubKey() types.Pubkey { return data.PubKey } -func (data SetCandidateOnData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data SetCandidateOnData) basicCheck(tx *Transaction, context *state.CheckState) *Response { return checkCandidateControl(data, tx, context) } @@ -29,11 +36,11 @@ func (data SetCandidateOnData) String() string { data.PubKey) } -func (data SetCandidateOnData) Gas() int64 { - return commissions.ToggleCandidateStatus +func (data SetCandidateOnData) CommissionData(price *commission.Price) *big.Int { + return price.SetCandidateOn } -func (data SetCandidateOnData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data SetCandidateOnData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -42,23 +49,17 @@ func (data SetCandidateOnData) Run(tx *Transaction, context state.Interface, rew checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -69,27 +70,31 @@ func (data SetCandidateOnData) Run(tx *Transaction, context state.Interface, rew } } - if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) deliverState.Candidates.SetOnline(data.PubKey) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeSetCandidateOnline)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } @@ -97,11 +102,19 @@ type SetCandidateOffData struct { PubKey types.Pubkey `json:"pub_key"` } +func (data SetCandidateOffData) Gas() int { + return gasSetCandidateOffline +} + +func (data SetCandidateOffData) TxType() TxType { + return TypeSetCandidateOffline +} + func (data SetCandidateOffData) GetPubKey() types.Pubkey { return data.PubKey } -func (data SetCandidateOffData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data SetCandidateOffData) basicCheck(tx *Transaction, context *state.CheckState) *Response { return checkCandidateControl(data, tx, context) } @@ -110,11 +123,11 @@ func (data SetCandidateOffData) String() string { data.PubKey) } -func (data SetCandidateOffData) Gas() int64 { - return commissions.ToggleCandidateStatus +func (data SetCandidateOffData) CommissionData(price *commission.Price) *big.Int { + return price.SetCandidateOff } -func (data SetCandidateOffData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data SetCandidateOffData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -123,22 +136,17 @@ func (data SetCandidateOffData) Run(tx *Transaction, context state.Interface, re checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -148,29 +156,31 @@ func (data SetCandidateOffData) Run(tx *Transaction, context state.Interface, re Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), } } - + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) - + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) deliverState.Candidates.SetOffline(data.PubKey) deliverState.Validators.SetToDrop(data.PubKey) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeSetCandidateOffline)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/switch_candidate_status_test.go b/core/transaction/switch_candidate_status_test.go index e43622dfa..d47d623c1 100644 --- a/core/transaction/switch_candidate_status_test.go +++ b/core/transaction/switch_candidate_status_test.go @@ -15,6 +15,7 @@ import ( ) func TestSwitchCandidateStatusTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -25,7 +26,7 @@ func TestSwitchCandidateStatusTx(t *testing.T) { pubkey := types.Pubkey{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) data := SetCandidateOnData{ PubKey: pubkey, @@ -76,10 +77,13 @@ func TestSwitchCandidateStatusTx(t *testing.T) { t.Fatalf("Status has not changed") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSetCandidateOffTx(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -90,7 +94,7 @@ func TestSetCandidateOffTx(t *testing.T) { pubkey := types.Pubkey{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) data := SetCandidateOffData{ PubKey: pubkey, @@ -141,10 +145,13 @@ func TestSetCandidateOffTx(t *testing.T) { t.Fatalf("Status has not changed") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSwitchCandidateStatusTxToNonExistCandidate(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -188,10 +195,13 @@ func TestSwitchCandidateStatusTxToNonExistCandidate(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CandidateNotFound, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSwitchCandidateStatusTxToCandidateOwnership(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -203,7 +213,7 @@ func TestSwitchCandidateStatusTxToCandidateOwnership(t *testing.T) { pubkey := types.Pubkey{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr2, addr2, addr2, pubkey, 10) + cState.Candidates.Create(addr2, addr2, addr2, pubkey, 10, 0) data := SetCandidateOnData{ PubKey: pubkey, @@ -238,10 +248,13 @@ func TestSwitchCandidateStatusTxToCandidateOwnership(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.IsNotOwnerOfCandidate, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSwitchCandidateStatusToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -255,7 +268,7 @@ func TestSwitchCandidateStatusToGasCoinReserveUnderflow(t *testing.T) { pubkey := types.Pubkey{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) data := SetCandidateOnData{ PubKey: pubkey, @@ -286,14 +299,17 @@ func TestSwitchCandidateStatusToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSwitchCandidateStatusToInsufficientFundsForGas(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -302,7 +318,7 @@ func TestSwitchCandidateStatusToInsufficientFundsForGas(t *testing.T) { pubkey := types.Pubkey{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) data := SetCandidateOnData{ PubKey: pubkey, @@ -337,7 +353,9 @@ func TestSwitchCandidateStatusToInsufficientFundsForGas(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } cState.Candidates.SetOnline(pubkey) @@ -356,10 +374,13 @@ func TestSwitchCandidateStatusToInsufficientFundsForGas(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestSwitchCandidateStatusToCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() privateKey, _ := crypto.GenerateKey() @@ -368,7 +389,7 @@ func TestSwitchCandidateStatusToCoinReserveUnderflow(t *testing.T) { pubkey := types.Pubkey{} rand.Read(pubkey[:]) - cState.Candidates.Create(addr, addr, addr, pubkey, 10) + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) cState.Candidates.SetOnline(pubkey) cState.Coins.AddVolume(coin, helpers.BipToPip(big.NewInt(1000000))) cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) @@ -403,9 +424,11 @@ func TestSwitchCandidateStatusToCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/transaction.go b/core/transaction/transaction.go index 25122f0de..b8e89ff10 100644 --- a/core/transaction/transaction.go +++ b/core/transaction/transaction.go @@ -5,9 +5,8 @@ import ( "errors" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/core/state/coins" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/crypto" "github.com/MinterTeam/minter-go-node/rlp" @@ -18,30 +17,87 @@ import ( // TxType of transaction is determined by a single byte. type TxType byte +const ( + TypeSend TxType = 0x01 + TypeSellCoin TxType = 0x02 + TypeSellAllCoin TxType = 0x03 + TypeBuyCoin TxType = 0x04 + TypeCreateCoin TxType = 0x05 + TypeDeclareCandidacy TxType = 0x06 + TypeDelegate TxType = 0x07 + TypeUnbond TxType = 0x08 + TypeRedeemCheck TxType = 0x09 + TypeSetCandidateOnline TxType = 0x0A + TypeSetCandidateOffline TxType = 0x0B + TypeCreateMultisig TxType = 0x0C + TypeMultisend TxType = 0x0D + TypeEditCandidate TxType = 0x0E + TypeSetHaltBlock TxType = 0x0F + TypeRecreateCoin TxType = 0x10 + TypeEditCoinOwner TxType = 0x11 + TypeEditMultisig TxType = 0x12 + TypePriceVote TxType = 0x13 + TypeEditCandidatePublicKey TxType = 0x14 + TypeAddLiquidity TxType = 0x15 + TypeRemoveLiquidity TxType = 0x16 + TypeSellSwapPool TxType = 0x17 + TypeBuySwapPool TxType = 0x18 + TypeSellAllSwapPool TxType = 0x19 + TypeEditCandidateCommission TxType = 0x1A + TypeMoveStake TxType = 0x1B + TypeMintToken TxType = 0x1C + TypeBurnToken TxType = 0x1D + TypeCreateToken TxType = 0x1E + TypeRecreateToken TxType = 0x1F + TypeVoteCommission TxType = 0x20 + TypeVoteUpdate TxType = 0x21 + TypeCreateSwapPool TxType = 0x22 +) +const ( + gasCustomCommission = 100 + + baseUnit = 10 + + gasSend = baseUnit + gasSellCoin = baseUnit * 2 + gasSellAllCoin = baseUnit * 2 + gasBuyCoin = baseUnit * 2 + gasCreateCoin = baseUnit * 10 + gasDeclareCandidacy = baseUnit * 15 + gasDelegate = baseUnit * 6 + gasUnbond = baseUnit * 4 + gasRedeemCheck = baseUnit * 10 + gasSetCandidateOnline = baseUnit * 5 + gasSetCandidateOffline = baseUnit * 5 + gasCreateMultisig = baseUnit * 10 + gasMultisendBase = 0 + gasMultisendDelta = baseUnit + gasEditCandidate = baseUnit * 5 + gasSetHaltBlock = baseUnit * 8 + gasRecreateCoin = baseUnit * 15 + gasEditCoinOwner = baseUnit * 8 + gasEditMultisig = baseUnit * 15 + gasPriceVote = baseUnit + gasEditCandidatePublicKey = baseUnit * 10 + gasAddLiquidity = baseUnit * 10 + gasRemoveLiquidity = baseUnit * 10 + gasSellSwapPool = baseUnit * 3 + gasBuySwapPool = baseUnit * 3 + gasSellAllSwapPool = baseUnit * 3 + gasEditCandidateCommission = baseUnit * 5 + gasMoveStake = baseUnit * 5 + gasMintToken = baseUnit * 5 + gasBurnToken = baseUnit * 5 + gasCreateToken = baseUnit * 10 + gasRecreateToken = baseUnit * 15 + gasVoteCommission = baseUnit * 15 + gasVoteUpdate = baseUnit * 5 + gasCreateSwapPool = baseUnit * 15 +) + type SigType byte const ( - TypeSend TxType = 0x01 - TypeSellCoin TxType = 0x02 - TypeSellAllCoin TxType = 0x03 - TypeBuyCoin TxType = 0x04 - TypeCreateCoin TxType = 0x05 - TypeDeclareCandidacy TxType = 0x06 - TypeDelegate TxType = 0x07 - TypeUnbond TxType = 0x08 - TypeRedeemCheck TxType = 0x09 - TypeSetCandidateOnline TxType = 0x0A - TypeSetCandidateOffline TxType = 0x0B - TypeCreateMultisig TxType = 0x0C - TypeMultisend TxType = 0x0D - TypeEditCandidate TxType = 0x0E - TypeSetHaltBlock TxType = 0x0F - TypeRecreateCoin TxType = 0x10 - TypeEditCoinOwner TxType = 0x11 - TypeEditMultisig TxType = 0x12 - TypePriceVote TxType = 0x13 - TypeEditCandidatePublicKey TxType = 0x14 - SigTypeSingle SigType = 0x01 SigTypeMulti SigType = 0x02 ) @@ -50,10 +106,6 @@ var ( ErrInvalidSig = errors.New("invalid transaction v, r, s values") ) -var ( - CommissionMultiplier = big.NewInt(10e14) -) - type Transaction struct { Nonce uint64 ChainID types.ChainID @@ -85,9 +137,9 @@ type SignatureMulti struct { type RawData []byte -type TotalSpends []totalSpend +type totalSpends []totalSpend -func (tss *TotalSpends) Add(coin types.CoinID, value *big.Int) { +func (tss *totalSpends) Add(coin types.CoinID, value *big.Int) { for i, t := range *tss { if t.Coin == coin { (*tss)[i].Value.Add((*tss)[i].Value, big.NewInt(0).Set(value)) @@ -117,8 +169,10 @@ type conversion struct { type Data interface { String() string - Gas() int64 - Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response + CommissionData(*commission.Price) *big.Int + Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response + TxType() TxType + Gas() int } func (tx *Transaction) Serialize() ([]byte, error) { @@ -126,18 +180,24 @@ func (tx *Transaction) Serialize() ([]byte, error) { } func (tx *Transaction) Gas() int64 { - return tx.decodedData.Gas() + tx.payloadGas() + // base := int64(tx.decodedData.Gas()) + // if tx.GasCoin != types.GetBaseCoinID() { + // base += gasCustomCommission + // } + // return int64(tx.decodedData.Gas()) + return 1 } -func (tx *Transaction) payloadGas() int64 { - return int64(len(tx.Payload)+len(tx.ServiceData)) * commissions.PayloadByte +func (tx *Transaction) Price(price *commission.Price) *big.Int { + return big.NewInt(0).Add(tx.decodedData.CommissionData(price), big.NewInt(0).Mul(big.NewInt(tx.payloadLen()), price.PayloadByte)) } -func (tx *Transaction) CommissionInBaseCoin() *big.Int { - commissionInBaseCoin := big.NewInt(0).Mul(big.NewInt(int64(tx.GasPrice)), big.NewInt(tx.Gas())) - commissionInBaseCoin.Mul(commissionInBaseCoin, CommissionMultiplier) +func (tx *Transaction) payloadLen() int64 { + return int64(len(tx.Payload) + len(tx.ServiceData)) +} - return commissionInBaseCoin +func (tx *Transaction) Commission(gas *big.Int) *big.Int { + return big.NewInt(0).Mul(big.NewInt(int64(tx.GasPrice)), gas) } func (tx *Transaction) String() string { @@ -302,14 +362,14 @@ func rlpHash(x interface{}) (h types.Hash) { return h } -func CheckForCoinSupplyOverflow(coin *coins.Model, delta *big.Int) *Response { +func CheckForCoinSupplyOverflow(coin CalculateCoin, delta *big.Int) *Response { total := big.NewInt(0).Set(coin.Volume()) total.Add(total, delta) - if total.Cmp(coin.MaxSupply()) != -1 { + if total.Cmp(coin.MaxSupply()) == 1 { return &Response{ Code: code.CoinSupplyOverflow, - Log: "coin supply overflow", + Log: "maximum supply reached", Info: EncodeError(code.NewCoinSupplyOverflow(delta.String(), coin.Volume().String(), total.String(), coin.MaxSupply().String(), coin.GetFullSymbol(), coin.ID().String())), } } @@ -317,7 +377,7 @@ func CheckForCoinSupplyOverflow(coin *coins.Model, delta *big.Int) *Response { return nil } -func CheckReserveUnderflow(coin *coins.Model, delta *big.Int) *Response { +func CheckReserveUnderflow(coin CalculateCoin, delta *big.Int) *Response { total := big.NewInt(0).Sub(coin.Reserve(), delta) if total.Cmp(minCoinReserve) == -1 { diff --git a/core/transaction/transaction_test.go b/core/transaction/transaction_test.go new file mode 100644 index 000000000..ef9c72811 --- /dev/null +++ b/core/transaction/transaction_test.go @@ -0,0 +1,215 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "sync" + "testing" +) + +func TestCommissionFromMin(t *testing.T) { + t.Parallel() + cState := getState() + + coin1 := createTestCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin1, + Volume0: helpers.BipToPip(big.NewInt(1000)), + Coin1: types.GetBaseCoinID(), + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SendData{ + To: types.Address{}, + Coin: types.GetBaseCoinID(), + Value: big.NewInt(1), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin1, + Type: TypeSend, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestCommissionFromPool(t *testing.T) { + t.Parallel() + cState := getState() + + coin1 := createNonReserveCoin(cState) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + + cState.Accounts.AddBalance(addr, types.BasecoinID, helpers.BipToPip(big.NewInt(1000000))) + + cState.Accounts.SubBalance(types.Address{}, coin1, helpers.BipToPip(big.NewInt(100000))) + cState.Accounts.AddBalance(addr, coin1, helpers.BipToPip(big.NewInt(100000))) + { + data := CreateSwapPoolData{ + Coin0: coin1, + Volume0: helpers.BipToPip(big.NewInt(1000)), + Coin1: types.GetBaseCoinID(), + Volume1: helpers.BipToPip(big.NewInt(1000)), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeCreateSwapPool, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + { + data := SendData{ + To: types.Address{}, + Coin: types.GetBaseCoinID(), + Value: big.NewInt(1), + } + + encodedData, err := rlp.EncodeToBytes(data) + + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin1, + Type: TypeSend, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + + if response.Code != 0 { + t.Fatalf("Response code %d is not 0. Error: %s", response.Code, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} diff --git a/core/transaction/unbond.go b/core/transaction/unbond.go index 5cfd062cd..2296ce37b 100644 --- a/core/transaction/unbond.go +++ b/core/transaction/unbond.go @@ -4,24 +4,28 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/core/code" - "github.com/MinterTeam/minter-go-node/core/commissions" "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" "github.com/MinterTeam/minter-go-node/core/types" - "github.com/MinterTeam/minter-go-node/formula" "github.com/MinterTeam/minter-go-node/hexutil" - "github.com/tendermint/tendermint/libs/kv" + abcTypes "github.com/tendermint/tendermint/abci/types" "math/big" ) -const unbondPeriod = 518400 - type UnbondData struct { PubKey types.Pubkey Coin types.CoinID Value *big.Int } -func (data UnbondData) BasicCheck(tx *Transaction, context *state.CheckState) *Response { +func (data UnbondData) Gas() int { + return gasUnbond +} +func (data UnbondData) TxType() TxType { + return TypeUnbond +} + +func (data UnbondData) basicCheck(tx *Transaction, context *state.CheckState) *Response { if data.Value == nil { return &Response{ Code: code.DecodeError, @@ -49,8 +53,7 @@ func (data UnbondData) BasicCheck(tx *Transaction, context *state.CheckState) *R sender, _ := tx.Sender() if waitlist := context.WaitList().Get(sender, data.PubKey, data.Coin); waitlist != nil { - value := big.NewInt(0).Sub(data.Value, waitlist.Value) - if value.Sign() < 1 { + if data.Value.Cmp(waitlist.Value) != 1 { return nil } return &Response{ @@ -86,11 +89,11 @@ func (data UnbondData) String() string { hexutil.Encode(data.PubKey[:])) } -func (data UnbondData) Gas() int64 { - return commissions.UnbondTx +func (data UnbondData) CommissionData(price *commission.Price) *big.Int { + return price.Unbond } -func (data UnbondData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64) Response { +func (data UnbondData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { sender, _ := tx.Sender() var checkState *state.CheckState @@ -99,23 +102,17 @@ func (data UnbondData) Run(tx *Transaction, context state.Interface, rewardPool checkState = state.NewCheckState(context.(*state.State)) } - response := data.BasicCheck(tx, checkState) + response := data.basicCheck(tx, checkState) if response != nil { return *response } - commissionInBaseCoin := tx.CommissionInBaseCoin() - commission := big.NewInt(0).Set(commissionInBaseCoin) - + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) gasCoin := checkState.Coins().GetCoin(tx.GasCoin) - - if !tx.GasCoin.IsBaseCoin() { - errResp := CheckReserveUnderflow(gasCoin, commissionInBaseCoin) - if errResp != nil { - return *errResp - } - - commission = formula.CalculateSaleAmount(gasCoin.Volume(), gasCoin.Reserve(), gasCoin.Crr(), commissionInBaseCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp } if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { @@ -126,16 +123,19 @@ func (data UnbondData) Run(tx *Transaction, context state.Interface, rewardPool } } + var tags []abcTypes.EventAttribute if deliverState, ok := context.(*state.State); ok { // now + 30 days - unbondAtBlock := currentBlock + unbondPeriod - - rewardPool.Add(rewardPool, commissionInBaseCoin) - - deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) - deliverState.Coins.SubVolume(tx.GasCoin, commission) + unbondAtBlock := currentBlock + types.GetUnbondPeriod() + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) if waitList := deliverState.Waitlist.Get(sender, data.PubKey, data.Coin); waitList != nil { diffValue := big.NewInt(0).Sub(data.Value, waitList.Value) @@ -147,19 +147,19 @@ func (data UnbondData) Run(tx *Transaction, context state.Interface, rewardPool deliverState.Candidates.SubStake(sender, data.PubKey, data.Coin, data.Value) } - deliverState.FrozenFunds.AddFund(unbondAtBlock, sender, data.PubKey, deliverState.Candidates.ID(data.PubKey), data.Coin, data.Value) + deliverState.FrozenFunds.AddFund(unbondAtBlock, sender, data.PubKey, deliverState.Candidates.ID(data.PubKey), data.Coin, data.Value, nil) deliverState.Accounts.SetNonce(sender, tx.Nonce) - } - tags := kv.Pairs{ - kv.Pair{Key: []byte("tx.type"), Value: []byte(hex.EncodeToString([]byte{byte(TypeUnbond)}))}, - kv.Pair{Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } } return Response{ - Code: code.OK, - GasUsed: tx.Gas(), - GasWanted: tx.Gas(), - Tags: tags, + Code: code.OK, + Tags: tags, } } diff --git a/core/transaction/unbond_test.go b/core/transaction/unbond_test.go index 557d0d68e..42fe232ba 100644 --- a/core/transaction/unbond_test.go +++ b/core/transaction/unbond_test.go @@ -3,7 +3,6 @@ package transaction import ( "github.com/MinterTeam/minter-go-node/core/code" "github.com/MinterTeam/minter-go-node/core/state" - "github.com/MinterTeam/minter-go-node/core/state/candidates" "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/crypto" "github.com/MinterTeam/minter-go-node/helpers" @@ -15,6 +14,7 @@ import ( ) func TestUnbondTx(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -78,14 +78,17 @@ func TestUnbondTx(t *testing.T) { stake := cState.Candidates.GetStakeOfAddress(pubkey, addr, coin) - if stake.Value.Cmp(types.Big0) != 0 { + if stake.Value.Sign() != 0 { t.Fatalf("Stake value is not corrent. Expected %s, got %s", types.Big0, stake.Value) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestFullUnbondTxWithWaitlist(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -135,7 +138,7 @@ func TestFullUnbondTxWithWaitlist(t *testing.T) { } cState.Candidates.RecalculateStakes(109000) - funds := cState.FrozenFunds.GetFrozenFunds(candidates.UnbondPeriod) + funds := cState.FrozenFunds.GetFrozenFunds(types.GetUnbondPeriod()) if funds == nil || len(funds.List) != 1 { t.Fatalf("Frozen funds are not correct") } @@ -154,10 +157,13 @@ func TestFullUnbondTxWithWaitlist(t *testing.T) { t.Fatalf("Waitlist is not deleted") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxWithWaitlist(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -207,7 +213,7 @@ func TestUnbondTxWithWaitlist(t *testing.T) { } cState.Candidates.RecalculateStakes(109000) - funds := cState.FrozenFunds.GetFrozenFunds(candidates.UnbondPeriod) + funds := cState.FrozenFunds.GetFrozenFunds(types.GetUnbondPeriod()) if funds == nil || len(funds.List) != 1 { t.Fatalf("Frozen funds are not correct") } @@ -226,10 +232,13 @@ func TestUnbondTxWithWaitlist(t *testing.T) { t.Fatalf("Waitlist is not correct") } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToDecodeError(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) @@ -267,15 +276,18 @@ func TestUnbondTxToDecodeError(t *testing.T) { t.Fatal(err) } - response := data.Run(&tx, state.NewCheckState(cState), nil, 1) + response := data.Run(&tx, state.NewCheckState(cState), nil, 1, nil) if response.Code != code.DecodeError { t.Fatalf("Response code is not %d. Error %s", code.DecodeError, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToNotExistCoin(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) privateKey, _ := crypto.GenerateKey() @@ -317,10 +329,13 @@ func TestUnbondTxToNotExistCoin(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CoinNotExists, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToNotExistCandidate(t *testing.T) { + t.Parallel() cState := getState() pubkey := types.Pubkey{1} privateKey, _ := crypto.GenerateKey() @@ -362,10 +377,13 @@ func TestUnbondTxToNotExistCandidate(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.CandidateNotFound, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToNotExistStake(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) privateKey, _ := crypto.GenerateKey() @@ -407,10 +425,13 @@ func TestUnbondTxToNotExistStake(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.StakeNotFound, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToInsufficientStake(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) privateKey, _ := crypto.GenerateKey() @@ -457,10 +478,13 @@ func TestUnbondTxToInsufficientStake(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientStake, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToInsufficientFunds(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) privateKey, _ := crypto.GenerateKey() @@ -506,10 +530,13 @@ func TestUnbondTxToInsufficientFunds(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientFunds, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToInsufficientAmountAtWaitlist(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) privateKey, _ := crypto.GenerateKey() @@ -554,10 +581,13 @@ func TestUnbondTxToInsufficientAmountAtWaitlist(t *testing.T) { t.Fatalf("Response code is not %d. Error %s", code.InsufficientWaitList, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } func TestUnbondTxToGasCoinReserveUnderflow(t *testing.T) { + t.Parallel() cState := getState() pubkey := createTestCandidate(cState) privateKey, _ := crypto.GenerateKey() @@ -602,9 +632,11 @@ func TestUnbondTxToGasCoinReserveUnderflow(t *testing.T) { } response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) - if response.Code != code.CoinReserveUnderflow { - t.Fatalf("Response code is not %d. Error %s", code.CoinReserveUnderflow, response.Log) + if response.Code != code.CommissionCoinNotSufficient { + t.Fatalf("Response code is not %d. Error %s", code.CommissionCoinNotSufficient, response.Log) } - checkState(t, cState) + if err := checkState(cState); err != nil { + t.Error(err) + } } diff --git a/core/transaction/update_network.go b/core/transaction/update_network.go new file mode 100644 index 000000000..a4ce4cc77 --- /dev/null +++ b/core/transaction/update_network.go @@ -0,0 +1,112 @@ +package transaction + +import ( + "encoding/hex" + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" + "strconv" +) + +type VoteUpdateData struct { + Version string + PubKey types.Pubkey + Height uint64 +} + +func (data VoteUpdateData) Gas() int { + return gasVoteUpdate +} +func (data VoteUpdateData) TxType() TxType { + return TypeVoteUpdate +} + +func (data VoteUpdateData) GetPubKey() types.Pubkey { + return data.PubKey +} + +func (data VoteUpdateData) basicCheck(tx *Transaction, context *state.CheckState, block uint64) *Response { + if len(data.Version) > 20 { + return &Response{ + Code: code.WrongUpdateVersionName, + Log: "wrong version name", + Info: EncodeError(code.NewCustomCode(code.WrongUpdateVersionName)), + } + } + if data.Height < block { + return &Response{ + Code: code.VoiceExpired, + Log: "voice is produced for the past state", + Info: EncodeError(code.NewVoiceExpired(strconv.Itoa(int(block)), strconv.Itoa(int(data.Height)))), + } + } + return checkCandidateOwnership(data, tx, context) +} + +func (data VoteUpdateData) String() string { + return fmt.Sprintf("UPDATE NETWORK on height: %d", data.Height) +} + +func (data VoteUpdateData) CommissionData(price *commission.Price) *big.Int { + return price.VoteUpdate +} + +func (data VoteUpdateData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState, currentBlock) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} diff --git a/core/transaction/vote_commission.go b/core/transaction/vote_commission.go new file mode 100644 index 000000000..ba63cbed8 --- /dev/null +++ b/core/transaction/vote_commission.go @@ -0,0 +1,237 @@ +package transaction + +import ( + "encoding/hex" + "fmt" + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/state" + "github.com/MinterTeam/minter-go-node/core/state/commission" + "github.com/MinterTeam/minter-go-node/core/types" + abcTypes "github.com/tendermint/tendermint/abci/types" + "math/big" + "strconv" +) + +type VoteCommissionData struct { + PubKey types.Pubkey + Height uint64 + Coin types.CoinID + PayloadByte *big.Int + Send *big.Int + BuyBancor *big.Int + SellBancor *big.Int + SellAllBancor *big.Int + BuyPoolBase *big.Int + BuyPoolDelta *big.Int + SellPoolBase *big.Int + SellPoolDelta *big.Int + SellAllPoolBase *big.Int + SellAllPoolDelta *big.Int + CreateTicker3 *big.Int + CreateTicker4 *big.Int + CreateTicker5 *big.Int + CreateTicker6 *big.Int + CreateTicker7to10 *big.Int + CreateCoin *big.Int + CreateToken *big.Int + RecreateCoin *big.Int + RecreateToken *big.Int + DeclareCandidacy *big.Int + Delegate *big.Int + Unbond *big.Int + RedeemCheck *big.Int + SetCandidateOn *big.Int + SetCandidateOff *big.Int + CreateMultisig *big.Int + MultisendBase *big.Int + MultisendDelta *big.Int + EditCandidate *big.Int + SetHaltBlock *big.Int + EditTickerOwner *big.Int + EditMultisig *big.Int + PriceVote *big.Int + EditCandidatePublicKey *big.Int + CreateSwapPool *big.Int + AddLiquidity *big.Int + RemoveLiquidity *big.Int + EditCandidateCommission *big.Int + MoveStake *big.Int + MintToken *big.Int + BurnToken *big.Int + VoteCommission *big.Int + VoteUpdate *big.Int + More []*big.Int `rlp:"tail"` +} + +func (data VoteCommissionData) TxType() TxType { + return TypeVoteCommission +} +func (data VoteCommissionData) Gas() int { + return gasVoteCommission +} + +func (data VoteCommissionData) GetPubKey() types.Pubkey { + return data.PubKey +} + +func (data VoteCommissionData) basicCheck(tx *Transaction, context *state.CheckState, block uint64) *Response { + if len(data.More) > 0 { // todo + return &Response{ + Code: code.DecodeError, + Log: "More parameters than expected", + Info: EncodeError(code.NewDecodeError()), + } + } + + if data.Height < block { + return &Response{ + Code: code.VoiceExpired, + Log: "voice is produced for the past state", + Info: EncodeError(code.NewVoiceExpired(strconv.Itoa(int(block)), strconv.Itoa(int(data.Height)))), + } + } + + if context.Commission().IsVoteExists(data.Height, data.PubKey) { + return &Response{ + Code: code.VoiceAlreadyExists, + Log: "Commission price vote with such public key and height already exists", + Info: EncodeError(code.NewVoiceAlreadyExists(strconv.FormatUint(data.Height, 10), data.GetPubKey().String())), + } + } + + coin := context.Coins().GetCoin(data.Coin) + if coin == nil { + return &Response{ + Code: code.CoinNotExists, + Log: "Coin to sell not exists", + Info: EncodeError(code.NewCoinNotExists("", data.Coin.String())), + } + } + + if !data.Coin.IsBaseCoin() && !context.Swap().SwapPoolExist(data.Coin, types.GetBaseCoinID()) { + return &Response{ + Code: code.PairNotExists, + Log: "swap pool not found", + Info: EncodeError(code.NewPairNotExists(data.Coin.String(), types.GetBaseCoinID().String())), + } + } + return checkCandidateOwnership(data, tx, context) +} + +func (data VoteCommissionData) String() string { + return fmt.Sprintf("PRICE COMMISSION in coin: %d", data.Coin) +} + +func (data VoteCommissionData) CommissionData(price *commission.Price) *big.Int { + return price.VoteCommission +} + +func (data VoteCommissionData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { + sender, _ := tx.Sender() + + var checkState *state.CheckState + var isCheck bool + if checkState, isCheck = context.(*state.CheckState); !isCheck { + checkState = state.NewCheckState(context.(*state.State)) + } + + response := data.basicCheck(tx, checkState, currentBlock) + if response != nil { + return *response + } + + commissionInBaseCoin := tx.Commission(price) + commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) + gasCoin := checkState.Coins().GetCoin(tx.GasCoin) + commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) + if errResp != nil { + return *errResp + } + + if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { + return Response{ + Code: code.InsufficientFunds, + Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), + Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), + } + } + + var tags []abcTypes.EventAttribute + if deliverState, ok := context.(*state.State); ok { + if isGasCommissionFromPoolSwap { + commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) + } else if !tx.GasCoin.IsBaseCoin() { + deliverState.Coins.SubVolume(tx.GasCoin, commission) + deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) + } + deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) + rewardPool.Add(rewardPool, commissionInBaseCoin) + + deliverState.Commission.AddVoice(data.Height, data.PubKey, data.price().Encode()) + + deliverState.Accounts.SetNonce(sender, tx.Nonce) + + tags = []abcTypes.EventAttribute{ + {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, + {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, + {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, + {Key: []byte("tx.public_key"), Value: []byte(hex.EncodeToString(data.PubKey[:])), Index: true}, + } + } + + return Response{ + Code: code.OK, + Tags: tags, + } +} + +func (data VoteCommissionData) price() *commission.Price { + return &commission.Price{ + Coin: data.Coin, + PayloadByte: data.PayloadByte, + Send: data.Send, + BuyBancor: data.BuyBancor, + SellBancor: data.SellBancor, + SellAllBancor: data.SellAllBancor, + BuyPoolBase: data.BuyPoolBase, + BuyPoolDelta: data.BuyPoolDelta, + SellPoolBase: data.SellPoolBase, + SellPoolDelta: data.SellPoolDelta, + SellAllPoolBase: data.SellAllPoolBase, + SellAllPoolDelta: data.SellAllPoolDelta, + CreateTicker3: data.CreateTicker3, + CreateTicker4: data.CreateTicker4, + CreateTicker5: data.CreateTicker5, + CreateTicker6: data.CreateTicker6, + CreateTicker7to10: data.CreateTicker7to10, + CreateCoin: data.CreateCoin, + CreateToken: data.CreateToken, + RecreateCoin: data.RecreateCoin, + RecreateToken: data.RecreateToken, + DeclareCandidacy: data.DeclareCandidacy, + Delegate: data.Delegate, + Unbond: data.Unbond, + RedeemCheck: data.RedeemCheck, + SetCandidateOn: data.SetCandidateOn, + SetCandidateOff: data.SetCandidateOff, + CreateMultisig: data.CreateMultisig, + MultisendBase: data.MultisendBase, + MultisendDelta: data.MultisendDelta, + EditCandidate: data.EditCandidate, + SetHaltBlock: data.SetHaltBlock, + EditTickerOwner: data.EditTickerOwner, + EditMultisig: data.EditMultisig, + PriceVote: data.PriceVote, + EditCandidatePublicKey: data.EditCandidatePublicKey, + CreateSwapPool: data.CreateSwapPool, + AddLiquidity: data.AddLiquidity, + RemoveLiquidity: data.RemoveLiquidity, + EditCandidateCommission: data.EditCandidateCommission, + MoveStake: data.MoveStake, + BurnToken: data.BurnToken, + MintToken: data.MintToken, + VoteCommission: data.VoteCommission, + VoteUpdate: data.VoteUpdate, + More: data.More, + } +} diff --git a/core/transaction/vote_commission_test.go b/core/transaction/vote_commission_test.go new file mode 100644 index 000000000..6c60ff34d --- /dev/null +++ b/core/transaction/vote_commission_test.go @@ -0,0 +1,772 @@ +package transaction + +import ( + "github.com/MinterTeam/minter-go-node/core/code" + "github.com/MinterTeam/minter-go-node/core/types" + "github.com/MinterTeam/minter-go-node/crypto" + "github.com/MinterTeam/minter-go-node/helpers" + "github.com/MinterTeam/minter-go-node/rlp" + "math/big" + "math/rand" + "sync" + "testing" +) + +func TestPriceCommissionTx(t *testing.T) { + t.Parallel() + cState := getState() + privateKey, addr := getAccount() + coin1 := createNonReserveCoin(cState) + cState.Accounts.SubBalance(types.Address{}, coin1, big.NewInt(1e18)) + + _, _, liquidity, id := cState.Swap.PairCreate(types.GetBaseCoinID(), coin1, big.NewInt(1e18), big.NewInt(1e18)) + coins := liquidityCoinName(coin1, types.GetBaseCoinID()) + liquidityCoinID := cState.App.GetNextCoinID() + cState.Coins.CreateToken(liquidityCoinID, LiquidityCoinSymbol(id), "Pool "+coins, true, true, big.NewInt(0).Set(liquidity), maxCoinSupply, nil) + cState.Accounts.AddBalance(addr, liquidityCoinID, liquidity) + cState.Accounts.AddBalance(addr, types.GetBaseCoinID(), big.NewInt(1e18)) + + pubkey := [32]byte{} + rand.Read(pubkey[:]) + + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) + cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) + { + data := VoteCommissionData{ + PubKey: pubkey, + Height: uint64(100500), + Coin: coin1, + PayloadByte: big.NewInt(1e18), + Send: big.NewInt(1e18), + BuyBancor: big.NewInt(1e18), + SellBancor: big.NewInt(1e18), + SellAllBancor: big.NewInt(1e18), + BuyPoolBase: big.NewInt(1e18), + BuyPoolDelta: big.NewInt(5e17), + SellPoolBase: big.NewInt(1e18), + SellPoolDelta: big.NewInt(5e17), + SellAllPoolBase: big.NewInt(1e18), + SellAllPoolDelta: big.NewInt(5e17), + CreateTicker3: big.NewInt(1e18), + CreateTicker4: big.NewInt(1e18), + CreateTicker5: big.NewInt(1e18), + CreateTicker6: big.NewInt(1e18), + CreateTicker7to10: big.NewInt(1e18), + CreateCoin: big.NewInt(1e18), + CreateToken: big.NewInt(1e18), + RecreateCoin: big.NewInt(1e18), + RecreateToken: big.NewInt(1e18), + DeclareCandidacy: big.NewInt(1e18), + Delegate: big.NewInt(1e18), + Unbond: big.NewInt(1e18), + RedeemCheck: big.NewInt(1e18), + SetCandidateOn: big.NewInt(1e18), + SetCandidateOff: big.NewInt(1e18), + CreateMultisig: big.NewInt(1e18), + MultisendBase: big.NewInt(1e18), + MultisendDelta: big.NewInt(1e18), + EditCandidate: big.NewInt(1e18), + SetHaltBlock: big.NewInt(1e18), + EditTickerOwner: big.NewInt(1e18), + EditMultisig: big.NewInt(1e18), + PriceVote: big.NewInt(1e18), + EditCandidatePublicKey: big.NewInt(1e18), + CreateSwapPool: big.NewInt(5e17), + AddLiquidity: big.NewInt(1e18), + RemoveLiquidity: big.NewInt(1e18), + EditCandidateCommission: big.NewInt(1e18), + MoveStake: big.NewInt(1e18), + MintToken: big.NewInt(1e18), + BurnToken: big.NewInt(1e18), + VoteCommission: big.NewInt(1e18), + VoteUpdate: big.NewInt(1e18), + More: nil, + } + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeVoteCommission, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + + { + data := VoteCommissionData{ + PayloadByte: big.NewInt(1e18), + Send: big.NewInt(1e18), + BuyBancor: big.NewInt(1e18), + SellBancor: big.NewInt(1e18), + SellAllBancor: big.NewInt(1e18), + BuyPoolBase: big.NewInt(1e18), + BuyPoolDelta: big.NewInt(5e17), + SellPoolBase: big.NewInt(1e18), + SellPoolDelta: big.NewInt(5e17), + SellAllPoolBase: big.NewInt(1e18), + SellAllPoolDelta: big.NewInt(5e17), + CreateTicker3: big.NewInt(1e18), + CreateTicker4: big.NewInt(1e18), + CreateTicker5: big.NewInt(1e18), + CreateTicker6: big.NewInt(1e18), + CreateTicker7to10: big.NewInt(1e18), + CreateCoin: big.NewInt(1e18), + CreateToken: big.NewInt(1e18), + RecreateCoin: big.NewInt(1e18), + RecreateToken: big.NewInt(1e18), + DeclareCandidacy: big.NewInt(1e18), + Delegate: big.NewInt(1e18), + Unbond: big.NewInt(1e18), + RedeemCheck: big.NewInt(1e18), + SetCandidateOn: big.NewInt(1e18), + SetCandidateOff: big.NewInt(1e18), + CreateMultisig: big.NewInt(1e18), + MultisendBase: big.NewInt(1e18), + MultisendDelta: big.NewInt(1e18), + EditCandidate: big.NewInt(1e18), + SetHaltBlock: big.NewInt(1e18), + EditTickerOwner: big.NewInt(1e18), + EditMultisig: big.NewInt(1e18), + PriceVote: big.NewInt(1e18), + EditCandidatePublicKey: big.NewInt(1e18), + AddLiquidity: big.NewInt(1e18), + RemoveLiquidity: big.NewInt(1e18), + EditCandidateCommission: big.NewInt(1e18), + MoveStake: big.NewInt(1e18), + MintToken: big.NewInt(1e18), + BurnToken: big.NewInt(1e18), + VoteCommission: big.NewInt(1e18), + VoteUpdate: big.NewInt(1e18), + Coin: coin1, + PubKey: pubkey, + Height: uint64(100500), + } + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeVoteCommission, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != code.VoiceAlreadyExists { + t.Fatalf("Response code is not %d. Error: %s", code.VoiceAlreadyExists, response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestPriceCommissionDeleteTx(t *testing.T) { + t.Parallel() + cState := getState() + privateKey, addr := getAccount() + coin1 := createNonReserveCoin(cState) + cState.Accounts.SubBalance(types.Address{}, coin1, big.NewInt(1e18)) + + _, _, liquidity, id := cState.Swap.PairCreate(types.GetBaseCoinID(), coin1, big.NewInt(1e18), big.NewInt(1e18)) + coins := liquidityCoinName(coin1, types.GetBaseCoinID()) + liquidityCoinID := cState.App.GetNextCoinID() + cState.Coins.CreateToken(liquidityCoinID, LiquidityCoinSymbol(id), "Pool "+coins, true, true, big.NewInt(0).Set(liquidity), maxCoinSupply, nil) + cState.Accounts.AddBalance(addr, liquidityCoinID, liquidity) + cState.Accounts.AddBalance(addr, types.GetBaseCoinID(), big.NewInt(2e18)) + + pubkey := [32]byte{} + rand.Read(pubkey[:]) + + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) + cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) + { + data := VoteCommissionData{ + PayloadByte: big.NewInt(1e18), + Send: big.NewInt(1e18), + BuyBancor: big.NewInt(1e18), + SellBancor: big.NewInt(1e18), + SellAllBancor: big.NewInt(1e18), + BuyPoolBase: big.NewInt(1e18), + BuyPoolDelta: big.NewInt(5e17), + SellPoolBase: big.NewInt(1e18), + SellPoolDelta: big.NewInt(5e17), + SellAllPoolBase: big.NewInt(1e18), + SellAllPoolDelta: big.NewInt(5e17), + CreateTicker3: big.NewInt(1e18), + CreateTicker4: big.NewInt(1e18), + CreateTicker5: big.NewInt(1e18), + CreateTicker6: big.NewInt(1e18), + CreateTicker7to10: big.NewInt(1e18), + CreateCoin: big.NewInt(1e18), + CreateToken: big.NewInt(1e18), + RecreateCoin: big.NewInt(1e18), + RecreateToken: big.NewInt(1e18), + DeclareCandidacy: big.NewInt(1e18), + Delegate: big.NewInt(1e18), + Unbond: big.NewInt(1e18), + RedeemCheck: big.NewInt(1e18), + SetCandidateOn: big.NewInt(1e18), + SetCandidateOff: big.NewInt(1e18), + CreateMultisig: big.NewInt(1e18), + MultisendBase: big.NewInt(1e18), + MultisendDelta: big.NewInt(1e18), + EditCandidate: big.NewInt(1e18), + SetHaltBlock: big.NewInt(1e18), + EditTickerOwner: big.NewInt(1e18), + EditMultisig: big.NewInt(1e18), + PriceVote: big.NewInt(1e18), + EditCandidatePublicKey: big.NewInt(1e18), + AddLiquidity: big.NewInt(1e18), + RemoveLiquidity: big.NewInt(1e18), + EditCandidateCommission: big.NewInt(1e18), + MoveStake: big.NewInt(1e18), + MintToken: big.NewInt(1e18), + BurnToken: big.NewInt(1e18), + VoteCommission: big.NewInt(1e18), + VoteUpdate: big.NewInt(1e18), + Coin: coin1, + PubKey: pubkey, + Height: uint64(100500), + } + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeVoteCommission, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + cState.Commission.Delete(100500) + if err := checkState(cState); err != nil { + t.Error(err) + } + { + data := VoteCommissionData{ + PayloadByte: big.NewInt(1e18), + Send: big.NewInt(1e18), + BuyBancor: big.NewInt(1e18), + SellBancor: big.NewInt(1e18), + SellAllBancor: big.NewInt(1e18), + BuyPoolBase: big.NewInt(1e18), + BuyPoolDelta: big.NewInt(5e17), + SellPoolBase: big.NewInt(1e18), + SellPoolDelta: big.NewInt(5e17), + SellAllPoolBase: big.NewInt(1e18), + SellAllPoolDelta: big.NewInt(5e17), + CreateTicker3: big.NewInt(1e18), + CreateTicker4: big.NewInt(1e18), + CreateTicker5: big.NewInt(1e18), + CreateTicker6: big.NewInt(1e18), + CreateTicker7to10: big.NewInt(1e18), + CreateCoin: big.NewInt(1e18), + CreateToken: big.NewInt(1e18), + RecreateCoin: big.NewInt(1e18), + RecreateToken: big.NewInt(1e18), + DeclareCandidacy: big.NewInt(1e18), + Delegate: big.NewInt(1e18), + Unbond: big.NewInt(1e18), + RedeemCheck: big.NewInt(1e18), + SetCandidateOn: big.NewInt(1e18), + SetCandidateOff: big.NewInt(1e18), + CreateMultisig: big.NewInt(1e18), + MultisendBase: big.NewInt(1e18), + MultisendDelta: big.NewInt(1e18), + EditCandidate: big.NewInt(1e18), + SetHaltBlock: big.NewInt(1e18), + EditTickerOwner: big.NewInt(1e18), + EditMultisig: big.NewInt(1e18), + PriceVote: big.NewInt(1e18), + EditCandidatePublicKey: big.NewInt(1e18), + AddLiquidity: big.NewInt(1e18), + RemoveLiquidity: big.NewInt(1e18), + EditCandidateCommission: big.NewInt(1e18), + MoveStake: big.NewInt(1e18), + MintToken: big.NewInt(1e18), + BurnToken: big.NewInt(1e18), + VoteCommission: big.NewInt(1e18), + VoteUpdate: big.NewInt(1e18), + Coin: coin1, + PubKey: pubkey, + Height: uint64(100500), + } + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 2, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeVoteCommission, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != code.OK { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestPriceCommissionAnyTx(t *testing.T) { + t.Parallel() + cState := getState() + coin1 := createNonReserveCoin(cState) + + { + privateKey, addr := getAccount() + cState.Accounts.SubBalance(types.Address{}, coin1, big.NewInt(1e18)) + + _, _, liquidity, id := cState.Swap.PairCreate(types.GetBaseCoinID(), coin1, big.NewInt(1e18), big.NewInt(1e18)) + coins := liquidityCoinName(coin1, types.GetBaseCoinID()) + liquidityCoinID := cState.App.GetNextCoinID() + cState.Coins.CreateToken(liquidityCoinID, LiquidityCoinSymbol(id), "Pool "+coins, true, true, big.NewInt(0).Set(liquidity), maxCoinSupply, nil) + cState.Accounts.AddBalance(addr, liquidityCoinID, liquidity) + cState.App.SetCoinsCount(liquidityCoinID.Uint32()) + cState.Accounts.AddBalance(addr, types.GetBaseCoinID(), big.NewInt(2e18)) + + pubkey := [32]byte{} + rand.Read(pubkey[:]) + + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) + cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) + + data := VoteCommissionData{ + PayloadByte: big.NewInt(1e18), + Send: big.NewInt(1e18), + BuyBancor: big.NewInt(1e18), + SellBancor: big.NewInt(1e18), + SellAllBancor: big.NewInt(1e18), + BuyPoolBase: big.NewInt(1e18), + BuyPoolDelta: big.NewInt(5e17), + SellPoolBase: big.NewInt(1e18), + SellPoolDelta: big.NewInt(5e17), + SellAllPoolBase: big.NewInt(1e18), + SellAllPoolDelta: big.NewInt(5e17), + CreateTicker3: big.NewInt(1e18), + CreateTicker4: big.NewInt(1e18), + CreateTicker5: big.NewInt(1e18), + CreateTicker6: big.NewInt(1e18), + CreateTicker7to10: big.NewInt(1e18), + CreateCoin: big.NewInt(1e18), + CreateToken: big.NewInt(1e18), + RecreateCoin: big.NewInt(1e18), + RecreateToken: big.NewInt(1e18), + DeclareCandidacy: big.NewInt(1e18), + Delegate: big.NewInt(1e18), + Unbond: big.NewInt(1e18), + RedeemCheck: big.NewInt(1e18), + SetCandidateOn: big.NewInt(1e18), + SetCandidateOff: big.NewInt(1e18), + CreateMultisig: big.NewInt(1e18), + MultisendBase: big.NewInt(1e18), + MultisendDelta: big.NewInt(1e18), + EditCandidate: big.NewInt(1e18), + SetHaltBlock: big.NewInt(1e18), + EditTickerOwner: big.NewInt(1e18), + EditMultisig: big.NewInt(1e18), + PriceVote: big.NewInt(1e18), + EditCandidatePublicKey: big.NewInt(1e18), + AddLiquidity: big.NewInt(1e18), + RemoveLiquidity: big.NewInt(1e18), + EditCandidateCommission: big.NewInt(1e18), + MoveStake: big.NewInt(1e18), + MintToken: big.NewInt(1e18), + BurnToken: big.NewInt(1e18), + VoteCommission: big.NewInt(1e18), + VoteUpdate: big.NewInt(1e18), + Coin: coin1, + PubKey: pubkey, + Height: uint64(100500), + } + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeVoteCommission, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } + if err := checkState(cState); err != nil { + t.Error(err) + } + { + privateKey, addr := getAccount() + cState.Accounts.SubBalance(types.Address{}, coin1, big.NewInt(1e18)) + + _, _, liquidity, id := cState.Swap.PairCreate(types.GetBaseCoinID(), coin1, big.NewInt(1e18), big.NewInt(1e18)) + coins := liquidityCoinName(coin1, types.GetBaseCoinID()) + liquidityCoinID := cState.App.GetNextCoinID() + cState.Coins.CreateToken(liquidityCoinID, LiquidityCoinSymbol(id), "Pool "+coins, true, true, big.NewInt(0).Set(liquidity), maxCoinSupply, nil) + cState.Accounts.AddBalance(addr, liquidityCoinID, liquidity) + cState.Accounts.AddBalance(addr, types.GetBaseCoinID(), big.NewInt(2e18)) + cState.App.SetCoinsCount(liquidityCoinID.Uint32()) + + pubkey := [32]byte{} + rand.Read(pubkey[:]) + + cState.Candidates.Create(addr, addr, addr, pubkey, 10, 0) + cState.Validators.Create(pubkey, helpers.BipToPip(big.NewInt(1))) + + data := VoteCommissionData{ + PayloadByte: big.NewInt(1e18), + Send: big.NewInt(1e18), + BuyBancor: big.NewInt(1e18), + SellBancor: big.NewInt(1e18), + SellAllBancor: big.NewInt(1e18), + BuyPoolBase: big.NewInt(1e18), + BuyPoolDelta: big.NewInt(5e17), + SellPoolBase: big.NewInt(1e18), + SellPoolDelta: big.NewInt(5e17), + SellAllPoolBase: big.NewInt(1e18), + SellAllPoolDelta: big.NewInt(5e17), + CreateTicker3: big.NewInt(1e18), + CreateTicker4: big.NewInt(1e18), + CreateTicker5: big.NewInt(1e18), + CreateTicker6: big.NewInt(1e18), + CreateTicker7to10: big.NewInt(1e18), + CreateCoin: big.NewInt(1e18), + CreateToken: big.NewInt(1e18), + RecreateCoin: big.NewInt(1e18), + RecreateToken: big.NewInt(1e18), + DeclareCandidacy: big.NewInt(1e18), + Delegate: big.NewInt(1e18), + Unbond: big.NewInt(1e18), + RedeemCheck: big.NewInt(1e18), + SetCandidateOn: big.NewInt(1e18), + SetCandidateOff: big.NewInt(1e18), + CreateMultisig: big.NewInt(1e18), + MultisendBase: big.NewInt(1e18), + MultisendDelta: big.NewInt(1e18), + EditCandidate: big.NewInt(1e18), + SetHaltBlock: big.NewInt(1e18), + EditTickerOwner: big.NewInt(1e18), + EditMultisig: big.NewInt(1e18), + PriceVote: big.NewInt(1e18), + EditCandidatePublicKey: big.NewInt(1e18), + AddLiquidity: big.NewInt(1e18), + RemoveLiquidity: big.NewInt(1e18), + EditCandidateCommission: big.NewInt(1e18), + MoveStake: big.NewInt(1e18), + MintToken: big.NewInt(1e18), + BurnToken: big.NewInt(1e18), + VoteCommission: big.NewInt(1e18), + VoteUpdate: big.NewInt(1e18), + Coin: coin1, + PubKey: pubkey, + Height: uint64(100500), + } + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: types.GetBaseCoinID(), + Type: TypeVoteCommission, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != code.OK { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } + } +} + +func TestCustomCommissionPriceCoin_sendTx(t *testing.T) { + t.Parallel() + cState := getState() + + usdCoinID := types.CoinID(666999) + cState.Coins.CreateToken(usdCoinID, types.StrToCoinSymbol("USD"), "USD Stable", true, true, helpers.BipToPip(big.NewInt(1e18)), maxCoinSupply, nil) + usdPool := helpers.BipToPip(big.NewInt(1e18)) + bipPool := big.NewInt(0).Sub(helpers.BipToPip(big.NewInt(1e18)), big.NewInt(0).Div(big.NewInt(0).Mul(helpers.BipToPip(big.NewInt(1e18)), big.NewInt(2)), big.NewInt(1000))) + _, _, liquidity, id := cState.Swap.PairCreate(usdCoinID, types.GetBaseCoinID(), usdPool, bipPool) + coins := liquidityCoinName(usdCoinID, types.GetBaseCoinID()) + coinID := cState.App.GetNextCoinID() + cState.Coins.CreateToken(coinID, LiquidityCoinSymbol(id), "Pool "+coins, true, true, big.NewInt(0).Set(liquidity), maxCoinSupply, nil) + cState.Accounts.AddBalance(types.Address{}, coinID, liquidity) + + price := cState.Commission.GetCommissions() + price.Coin = usdCoinID + cState.Commission.SetNewCommissions(price.Encode()) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + coin := types.GetBaseCoinID() + + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) + + value := helpers.BipToPip(big.NewInt(10)) + to := types.Address([20]byte{1}) + + data := SendData{ + Coin: coin, + To: to, + Value: value, + } + + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: coin, + Type: TypeSend, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + for _, tag := range response.Tags { + t.Log(string(tag.Key), string(tag.Value)) + } + + targetBalance, _ := big.NewInt(0).SetString("999989989999999999999999", 10) + balance := cState.Accounts.GetBalance(addr, coin) + if balance.Cmp(targetBalance) != 0 { + t.Fatalf("Target %s balance is not correct. Expected %s, got %s", addr.String(), targetBalance, balance) + } + + targetTestBalance, _ := big.NewInt(0).SetString("10000000000000000000", 10) + testBalance := cState.Accounts.GetBalance(to, coin) + if testBalance.Cmp(targetTestBalance) != 0 { + t.Fatalf("Target %s balance is not correct. Expected %s, got %s", to.String(), targetTestBalance, testBalance) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} + +func TestCustomCommissionPriceCoinAndGasCastomCoin_sendTx(t *testing.T) { + t.Parallel() + cState := getState() + + usdCoinID := types.CoinID(666999) + usdPool := helpers.BipToPip(big.NewInt(1e18)) + bipPool := big.NewInt(0).Sub(helpers.BipToPip(big.NewInt(1e18)), big.NewInt(0).Div(big.NewInt(0).Mul(helpers.BipToPip(big.NewInt(1e18)), big.NewInt(2)), big.NewInt(1000))) + cState.Coins.CreateToken(usdCoinID, types.StrToCoinSymbol("USD"), "USD Stable", true, true, big.NewInt(0).Add(big.NewInt(1e18), usdPool), maxCoinSupply, nil) + _, _, liquidity, id := cState.Swap.PairCreate(usdCoinID, types.GetBaseCoinID(), usdPool, bipPool) + coins := liquidityCoinName(usdCoinID, types.GetBaseCoinID()) + coinID := cState.App.GetNextCoinID() + cState.Coins.CreateToken(coinID, LiquidityCoinSymbol(id), "Pool "+coins, true, true, big.NewInt(0).Set(liquidity), maxCoinSupply, nil) + cState.Accounts.AddBalance(types.Address{}, coinID, liquidity) + cState.App.SetCoinsCount(coinID.Uint32()) + + price := cState.Commission.GetCommissions() + price.Coin = usdCoinID + cState.Commission.SetNewCommissions(price.Encode()) + + privateKey, _ := crypto.GenerateKey() + addr := crypto.PubkeyToAddress(privateKey.PublicKey) + coin := types.GetBaseCoinID() + + cState.Accounts.AddBalance(addr, coin, helpers.BipToPip(big.NewInt(1000000))) + cState.Accounts.AddBalance(addr, usdCoinID, big.NewInt(1e18)) + + value := helpers.BipToPip(big.NewInt(10)) + to := types.Address([20]byte{1}) + + data := SendData{ + Coin: coin, + To: to, + Value: value, + } + + encodedData, err := rlp.EncodeToBytes(data) + if err != nil { + t.Fatal(err) + } + + tx := Transaction{ + Nonce: 1, + GasPrice: 1, + ChainID: types.CurrentChainID, + GasCoin: usdCoinID, + Type: TypeSend, + Data: encodedData, + SignatureType: SigTypeSingle, + } + + if err := tx.Sign(privateKey); err != nil { + t.Fatal(err) + } + + encodedTx, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatal(err) + } + + response := RunTx(cState, encodedTx, big.NewInt(0), 0, &sync.Map{}, 0) + if response.Code != 0 { + t.Fatalf("Response code is not 0. Error: %s", response.Log) + } + + // for _, tag := range response.Tags { + // t.Log(string(tag.Key), string(tag.Value)) + // } + + targetBalance, _ := big.NewInt(0).SetString("999990000000000000000000", 10) + balance := cState.Accounts.GetBalance(addr, coin) + if balance.Cmp(targetBalance) != 0 { + t.Fatalf("Target %s balance is not correct. Expected %s, got %s", addr.String(), targetBalance, balance) + } + + targetGasBalance, _ := big.NewInt(0).SetString("989959879679198074", 10) + balanceGas := cState.Accounts.GetBalance(addr, usdCoinID) + if balanceGas.Cmp(targetGasBalance) != 0 { + t.Fatalf("Target %s balance is not correct. Expected %s, got %s", addr.String(), targetGasBalance, balanceGas) + } + + targetTestBalance, _ := big.NewInt(0).SetString("10000000000000000000", 10) + testBalance := cState.Accounts.GetBalance(to, coin) + if testBalance.Cmp(targetTestBalance) != 0 { + t.Fatalf("Target %s balance is not correct. Expected %s, got %s", to.String(), targetTestBalance, testBalance) + } + + if err := checkState(cState); err != nil { + t.Error(err) + } +} diff --git a/core/types/appstate.go b/core/types/appstate.go index 2bfcbd518..bf98e5be3 100644 --- a/core/types/appstate.go +++ b/core/types/appstate.go @@ -9,15 +9,16 @@ import ( type AppState struct { Note string `json:"note"` - StartHeight uint64 `json:"start_height"` Validators []Validator `json:"validators,omitempty"` Candidates []Candidate `json:"candidates,omitempty"` BlockListCandidates []Pubkey `json:"block_list_candidates,omitempty"` Waitlist []Waitlist `json:"waitlist,omitempty"` + Swap []Swap `json:"swap,omitempty"` Accounts []Account `json:"accounts,omitempty"` Coins []Coin `json:"coins,omitempty"` FrozenFunds []FrozenFund `json:"frozen_funds,omitempty"` HaltBlocks []HaltBlock `json:"halt_blocks,omitempty"` + PriceVotes []PriceVotes `json:"price_votes,omitempty"` UsedChecks []UsedCheck `json:"used_checks,omitempty"` MaxGas uint64 `json:"max_gas"` TotalSlashed string `json:"total_slashed"` @@ -145,6 +146,31 @@ func (s *AppState) Verify() error { // check coins' volume volume := big.NewInt(0) + + for _, account := range s.Accounts { + for _, bal := range account.Balance { + if bal.Coin == coin.ID { + volume.Add(volume, helpers.StringToBigInt(bal.Value)) + } + } + } + + for _, swap := range s.Swap { + if swap.Coin0 == coin.ID { + volume.Add(volume, helpers.StringToBigInt(swap.Reserve0)) + } + if swap.Coin1 == coin.ID { + volume.Add(volume, helpers.StringToBigInt(swap.Reserve1)) + } + } + + if coin.Crr == 0 { + if volume.Cmp(helpers.StringToBigInt(coin.Volume)) != 0 { + return fmt.Errorf("wrong token %s volume (%s)", coin.Symbol.String(), big.NewInt(0).Sub(volume, helpers.StringToBigInt(coin.Volume))) + } + continue + } + for _, ff := range s.FrozenFunds { if ff.Coin == coin.ID { volume.Add(volume, helpers.StringToBigInt(ff.Value)) @@ -165,14 +191,6 @@ func (s *AppState) Verify() error { } } - for _, account := range s.Accounts { - for _, bal := range account.Balance { - if bal.Coin == coin.ID { - volume.Add(volume, helpers.StringToBigInt(bal.Value)) - } - } - } - for _, wl := range s.Waitlist { if wl.Coin == coin.ID { volume.Add(volume, helpers.StringToBigInt(wl.Value)) @@ -277,26 +295,40 @@ type Waitlist struct { Coin uint64 `json:"coin"` Value string `json:"value"` } +type BalanceProvider struct { + Address Address `json:"address"` + Liquidity string `json:"liquidity"` +} +type Swap struct { + Coin0 uint64 `json:"coin0"` + Coin1 uint64 `json:"coin1"` + Reserve0 string `json:"reserve0"` + Reserve1 string `json:"reserve1"` + ID uint64 `json:"id"` +} type Coin struct { ID uint64 `json:"id"` Name string `json:"name"` Symbol CoinSymbol `json:"symbol"` Volume string `json:"volume"` - Crr uint64 `json:"crr"` - Reserve string `json:"reserve"` + Crr uint64 `json:"crr,omitempty"` + Reserve string `json:"reserve,omitempty"` MaxSupply string `json:"max_supply"` - Version uint64 `json:"version"` - OwnerAddress *Address `json:"owner_address"` + Version uint64 `json:"version,omitempty"` + OwnerAddress *Address `json:"owner_address,omitempty"` + Mintable bool `json:"mintable,omitempty"` + Burnable bool `json:"burnable,omitempty"` } type FrozenFund struct { - Height uint64 `json:"height"` - Address Address `json:"address"` - CandidateKey *Pubkey `json:"candidate_key,omitempty"` - CandidateID uint64 `json:"candidate_id"` - Coin uint64 `json:"coin"` - Value string `json:"value"` + Height uint64 `json:"height"` + Address Address `json:"address"` + CandidateKey *Pubkey `json:"candidate_key,omitempty"` + CandidateID uint64 `json:"candidate_id"` + Coin uint64 `json:"coin"` + Value string `json:"value"` + MoveToCandidateID *uint64 `json:"move_to_candidate_id,omitempty"` } type UsedCheck string @@ -323,3 +355,14 @@ type HaltBlock struct { Height uint64 `json:"height"` CandidateKey Pubkey `json:"candidate_key"` } +type PriceVotes struct { + Height uint64 `json:"height"` + CandidateKey Pubkey `json:"candidate_key"` + PriceCommission PriceCommission `json:"price_commission"` +} + +type PriceCommission struct { + Send string + + Coin string +} diff --git a/core/types/constants.go b/core/types/constants.go index e4272bd87..f5f69f61d 100644 --- a/core/types/constants.go +++ b/core/types/constants.go @@ -10,6 +10,15 @@ const ( ChainTestnet ChainID = 0x02 ) +const unbondPeriod = 518400 + +func GetUnbondPeriod() uint64 { + if CurrentChainID == ChainTestnet { + return 518400 / 2920 // 15min + } + return 518400 +} + // CurrentChainID is current ChainID of the network var CurrentChainID = ChainMainnet diff --git a/core/types/types.go b/core/types/types.go index d604e7628..70013bd90 100644 --- a/core/types/types.go +++ b/core/types/types.go @@ -6,6 +6,7 @@ import ( "encoding/hex" "fmt" "github.com/MinterTeam/minter-go-node/hexutil" + "github.com/tendermint/tendermint/crypto/ed25519" "math/big" "math/rand" "reflect" @@ -133,7 +134,7 @@ func (h UnprefixedHash) MarshalText() ([]byte, error) { return []byte(hex.EncodeToString(h[:])), nil } -/////////// Coin +// ///////// Coin // CoinSymbol represents the 10 byte coin symbol. type CoinSymbol [CoinSymbolLength]byte @@ -180,7 +181,7 @@ func StrToCoinSymbol(s string) CoinSymbol { // StrToCoinBaseSymbol converts give string to a coin base symbol func StrToCoinBaseSymbol(s string) CoinSymbol { delimiter := strings.Index(s, "-") - if delimiter != -1 { + if delimiter > 3 { return StrToCoinSymbol(s[:delimiter]) } @@ -193,6 +194,9 @@ func GetVersionFromSymbol(s string) CoinVersion { if len(parts) == 1 { return 0 } + if len(parts[0]) < 3 { + return 0 + } v, _ := strconv.ParseUint(parts[1], 10, 16) return CoinVersion(v) @@ -230,7 +234,7 @@ func BytesToCoinID(bytes []byte) CoinID { // CoinVersion represents coin version info type CoinVersion = uint16 -/////////// Address +// ///////// Address // Address represents 20-byte address in Minter Blockchain type Address [AddressLength]byte @@ -391,3 +395,14 @@ func (p Pubkey) Equals(p2 Pubkey) bool { // TmAddress represents Tendermint address type TmAddress [TendermintAddressLength]byte + +func GetTmAddress(publicKey Pubkey) TmAddress { + // set tm address + var pubkey ed25519.PubKey + copy(pubkey[:], publicKey[:]) + + var address TmAddress + copy(address[:], pubkey.Address().Bytes()) + + return address +} diff --git a/coreV2/transaction/move_stake.go b/coreV2/transaction/move_stake.go new file mode 100644 index 000000000..02737903b --- /dev/null +++ b/coreV2/transaction/move_stake.go @@ -0,0 +1,162 @@ +package transaction + +// +// import ( +// "encoding/hex" +// "fmt" +// "github.com/MinterTeam/minter-go-node/coreV2/code" +// "github.com/MinterTeam/minter-go-node/coreV2/state" +// "github.com/MinterTeam/minter-go-node/coreV2/state/commission" +// "github.com/MinterTeam/minter-go-node/coreV2/types" +// abcTypes "github.com/tendermint/tendermint/abci/types" +// "math/big" +// ) +// +// type MoveStakeData struct { +// From, To types.Pubkey +// Coin types.CoinID +// Stake *big.Int +// } +// +// func (data MoveStakeData) Gas() int { +// return gasMoveStake +// } +// func (data MoveStakeData) TxType() TxType { +// return TypeMoveStake +// } +// +// func (data MoveStakeData) basicCheck(tx *Transaction, context *state.CheckState) *Response { +// if !context.Coins().Exists(data.Coin) { +// return &Response{ +// Code: code.CoinNotExists, +// Log: fmt.Sprintf("Coin %s not exists", data.Coin), +// Info: EncodeError(code.NewCoinNotExists("", data.Coin.String())), +// } +// } +// +// if !context.Candidates().Exists(data.From) { +// return &Response{ +// Code: code.CandidateNotFound, +// Log: fmt.Sprintf("Candidate with %s public key not found", data.From), +// Info: EncodeError(code.NewCandidateNotFound(data.From.String())), +// } +// } +// if !context.Candidates().Exists(data.To) { +// return &Response{ +// Code: code.CandidateNotFound, +// Log: fmt.Sprintf("Candidate with %s public key not found", data.To), +// Info: EncodeError(code.NewCandidateNotFound(data.To.String())), +// } +// } +// +// sender, _ := tx.Sender() +// +// if waitlist := context.WaitList().Get(sender, data.From, data.Coin); waitlist != nil { +// if data.Stake.Cmp(waitlist.Value) == 1 { +// return &Response{ +// Code: code.InsufficientWaitList, +// Log: "Insufficient amount at waitlist for sender account", +// Info: EncodeError(code.NewInsufficientWaitList(waitlist.Value.String(), data.Stake.String())), +// } +// } +// } else { +// stake := context.Candidates().GetStakeValueOfAddress(data.From, sender, data.Coin) +// +// if stake == nil { +// return &Response{ +// Code: code.StakeNotFound, +// Log: "Stake of current user not found", +// Info: EncodeError(code.NewStakeNotFound(data.From.String(), sender.String(), data.Coin.String(), context.Coins().GetCoin(data.Coin).GetFullSymbol())), +// } +// } +// +// if stake.Cmp(data.Stake) == -1 { +// return &Response{ +// Code: code.InsufficientStake, +// Log: "Insufficient stake for sender account", +// Info: EncodeError(code.NewInsufficientStake(data.From.String(), sender.String(), data.Coin.String(), context.Coins().GetCoin(data.Coin).GetFullSymbol(), stake.String(), data.Stake.String())), +// } +// } +// } +// +// return nil +// } +// +// func (data MoveStakeData) String() string { +// return fmt.Sprintf("MOVE STAKE") +// } +// +// func (data MoveStakeData) CommissionData(price *commission.Price) *big.Int { +// return price.MoveStake +// } +// +// func (data MoveStakeData) Run(tx *Transaction, context state.Interface, rewardPool *big.Int, currentBlock uint64, price *big.Int) Response { +// sender, _ := tx.Sender() +// +// var checkState *state.CheckState +// var isCheck bool +// if checkState, isCheck = context.(*state.CheckState); !isCheck { +// checkState = state.NewCheckState(context.(*state.State)) +// } +// +// response := data.basicCheck(tx, checkState) +// if response != nil { +// return *response +// } +// +// commissionInBaseCoin := tx.Commission(price) +// commissionPoolSwapper := checkState.Swap().GetSwapper(tx.GasCoin, types.GetBaseCoinID()) +// gasCoin := checkState.Coins().GetCoin(tx.GasCoin) +// commission, isGasCommissionFromPoolSwap, errResp := CalculateCommission(checkState, commissionPoolSwapper, gasCoin, commissionInBaseCoin) +// if errResp != nil { +// return *errResp +// } +// +// if checkState.Accounts().GetBalance(sender, tx.GasCoin).Cmp(commission) < 0 { +// return Response{ +// Code: code.InsufficientFunds, +// Log: fmt.Sprintf("Insufficient funds for sender account: %s. Wanted %s %s", sender.String(), commission.String(), gasCoin.GetFullSymbol()), +// Info: EncodeError(code.NewInsufficientFunds(sender.String(), commission.String(), gasCoin.GetFullSymbol(), gasCoin.ID().String())), +// } +// } +// var tags []abcTypes.EventAttribute +// if deliverState, ok := context.(*state.State); ok { +// if isGasCommissionFromPoolSwap { +// commission, commissionInBaseCoin, _ = deliverState.Swap.PairSell(tx.GasCoin, types.GetBaseCoinID(), commission, commissionInBaseCoin) +// } else if !tx.GasCoin.IsBaseCoin() { +// deliverState.Coins.SubVolume(tx.GasCoin, commission) +// deliverState.Coins.SubReserve(tx.GasCoin, commissionInBaseCoin) +// } +// deliverState.Accounts.SubBalance(sender, tx.GasCoin, commission) +// rewardPool.Add(rewardPool, commissionInBaseCoin) +// +// if waitList := deliverState.Waitlist.Get(sender, data.From, data.Coin); waitList != nil { +// diffValue := big.NewInt(0).Sub(data.Stake, waitList.Value) +// deliverState.Waitlist.Delete(sender, data.From, data.Coin) +// if diffValue.Sign() == -1 { +// deliverState.Waitlist.AddWaitList(sender, data.From, data.Coin, big.NewInt(0).Neg(diffValue)) +// } +// } else { +// deliverState.Candidates.SubStake(sender, data.From, data.Coin, data.Stake) +// } +// +// moveToCandidateId := deliverState.Candidates.ID(data.To) +// deliverState.FrozenFunds.AddFund(currentBlock+types.GetUnbondPeriod(), sender, data.From, deliverState.Candidates.ID(data.From), data.Coin, data.Stake, &moveToCandidateId) +// +// deliverState.Accounts.SetNonce(sender, tx.Nonce) +// +// tags = []abcTypes.EventAttribute{ +// {Key: []byte("tx.commission_in_base_coin"), Value: []byte(commissionInBaseCoin.String())}, +// {Key: []byte("tx.commission_conversion"), Value: []byte(isGasCommissionFromPoolSwap.String())}, +// {Key: []byte("tx.commission_amount"), Value: []byte(commission.String())}, +// {Key: []byte("tx.from"), Value: []byte(hex.EncodeToString(sender[:]))}, +// {Key: []byte("tx.public_key_old"), Value: []byte(hex.EncodeToString(data.From[:])), Index: true}, +// {Key: []byte("tx.public_key_new"), Value: []byte(hex.EncodeToString(data.To[:])), Index: true}, +// } +// } +// +// return Response{ +// Code: code.OK, +// Tags: tags, +// } +// } diff --git a/crypto/bn256/cloudflare/example_test.go b/crypto/bn256/cloudflare/example_test.go index 6c285995c..05676789e 100644 --- a/crypto/bn256/cloudflare/example_test.go +++ b/crypto/bn256/cloudflare/example_test.go @@ -47,5 +47,5 @@ func TestExamplePair(t *testing.T) { require.Equal(t, k1, k2) require.Equal(t, k1, k3) - require.Equal(t, len(np), 4) //Avoid gometalinter varcheck err on np + require.Equal(t, len(np), 4) // Avoid gometalinter varcheck err on np } diff --git a/crypto/secp256k1/curve.go b/crypto/secp256k1/curve.go index 5409ee1d2..8fe876da7 100644 --- a/crypto/secp256k1/curve.go +++ b/crypto/secp256k1/curve.go @@ -100,19 +100,19 @@ func (BitCurve *BitCurve) Params() *elliptic.CurveParams { // IsOnCurve returns true if the given (x,y) lies on the BitCurve. func (BitCurve *BitCurve) IsOnCurve(x, y *big.Int) bool { // y² = x³ + b - y2 := new(big.Int).Mul(y, y) //y² - y2.Mod(y2, BitCurve.P) //y²%P + y2 := new(big.Int).Mul(y, y) // y² + y2.Mod(y2, BitCurve.P) // y²%P - x3 := new(big.Int).Mul(x, x) //x² - x3.Mul(x3, x) //x³ + x3 := new(big.Int).Mul(x, x) // x² + x3.Mul(x3, x) // x³ - x3.Add(x3, BitCurve.B) //x³+B - x3.Mod(x3, BitCurve.P) //(x³+B)%P + x3.Add(x3, BitCurve.B) // x³+B + x3.Mod(x3, BitCurve.P) // (x³+B)%P return x3.Cmp(y2) == 0 } -//TODO: double check if the function is okay +// TODO: double check if the function is okay // affineFromJacobian reverses the Jacobian transform. See the comment at the // top of the file. func (BitCurve *BitCurve) affineFromJacobian(x, y, z *big.Int) (xOut, yOut *big.Int) { @@ -209,30 +209,30 @@ func (BitCurve *BitCurve) Double(x1, y1 *big.Int) (*big.Int, *big.Int) { func (BitCurve *BitCurve) doubleJacobian(x, y, z *big.Int) (*big.Int, *big.Int, *big.Int) { // See http://hyperelliptic.org/EFD/g1p/auto-shortw-jacobian-0.html#doubling-dbl-2009-l - a := new(big.Int).Mul(x, x) //X1² - b := new(big.Int).Mul(y, y) //Y1² - c := new(big.Int).Mul(b, b) //B² + a := new(big.Int).Mul(x, x) // X1² + b := new(big.Int).Mul(y, y) // Y1² + c := new(big.Int).Mul(b, b) // B² - d := new(big.Int).Add(x, b) //X1+B - d.Mul(d, d) //(X1+B)² - d.Sub(d, a) //(X1+B)²-A - d.Sub(d, c) //(X1+B)²-A-C - d.Mul(d, big.NewInt(2)) //2*((X1+B)²-A-C) + d := new(big.Int).Add(x, b) // X1+B + d.Mul(d, d) // (X1+B)² + d.Sub(d, a) // (X1+B)²-A + d.Sub(d, c) // (X1+B)²-A-C + d.Mul(d, big.NewInt(2)) // 2*((X1+B)²-A-C) - e := new(big.Int).Mul(big.NewInt(3), a) //3*A - f := new(big.Int).Mul(e, e) //E² + e := new(big.Int).Mul(big.NewInt(3), a) // 3*A + f := new(big.Int).Mul(e, e) // E² - x3 := new(big.Int).Mul(big.NewInt(2), d) //2*D - x3.Sub(f, x3) //F-2*D + x3 := new(big.Int).Mul(big.NewInt(2), d) // 2*D + x3.Sub(f, x3) // F-2*D x3.Mod(x3, BitCurve.P) - y3 := new(big.Int).Sub(d, x3) //D-X3 - y3.Mul(e, y3) //E*(D-X3) - y3.Sub(y3, new(big.Int).Mul(big.NewInt(8), c)) //E*(D-X3)-8*C + y3 := new(big.Int).Sub(d, x3) // D-X3 + y3.Mul(e, y3) // E*(D-X3) + y3.Sub(y3, new(big.Int).Mul(big.NewInt(8), c)) // E*(D-X3)-8*C y3.Mod(y3, BitCurve.P) - z3 := new(big.Int).Mul(y, z) //Y1*Z1 - z3.Mul(big.NewInt(2), z3) //3*Y1*Z1 + z3 := new(big.Int).Mul(y, z) // Y1*Z1 + z3.Mul(big.NewInt(2), z3) // 3*Y1*Z1 z3.Mod(z3, BitCurve.P) return x3, y3, z3 diff --git a/crypto/secp256k1/libsecp256k1/.travis.yml b/crypto/secp256k1/libsecp256k1/.travis.yml index 243952924..253594a97 100644 --- a/crypto/secp256k1/libsecp256k1/.travis.yml +++ b/crypto/secp256k1/libsecp256k1/.travis.yml @@ -8,7 +8,7 @@ compiler: - gcc cache: directories: - - src/java/guava/ + - src/java/guava/ env: global: - FIELD=auto BIGNUM=auto SCALAR=auto ENDOMORPHISM=no STATICPRECOMPUTATION=yes ASM=no BUILD=check EXTRAFLAGS= HOST= ECDH=no RECOVERY=no EXPERIMENTAL=no @@ -63,7 +63,7 @@ before_install: mkdir -p `dirname $GUAVA_JAR` install: if [ ! -f $GUAVA_JAR ]; then wget $GUAVA_URL -O $GUAVA_JAR; fi before_script: ./autogen.sh script: - - if [ -n "$HOST" ]; then export USE_HOST="--host=$HOST"; fi - - if [ "x$HOST" = "xi686-linux-gnu" ]; then export CC="$CC -m32"; fi - - ./configure --enable-experimental=$EXPERIMENTAL --enable-endomorphism=$ENDOMORPHISM --with-field=$FIELD --with-bignum=$BIGNUM --with-scalar=$SCALAR --enable-ecmult-static-precomputation=$STATICPRECOMPUTATION --enable-module-ecdh=$ECDH --enable-module-recovery=$RECOVERY $EXTRAFLAGS $USE_HOST && make -j2 $BUILD + - if [ -n "$HOST" ]; then export USE_HOST="--host=$HOST"; fi + - if [ "x$HOST" = "xi686-linux-gnu" ]; then export CC="$CC -m32"; fi + - ./configure --enable-experimental=$EXPERIMENTAL --enable-endomorphism=$ENDOMORPHISM --with-field=$FIELD --with-bignum=$BIGNUM --with-scalar=$SCALAR --enable-ecmult-static-precomputation=$STATICPRECOMPUTATION --enable-module-ecdh=$ECDH --enable-module-recovery=$RECOVERY $EXTRAFLAGS $USE_HOST && make -j2 $BUILD os: linux diff --git a/crypto/secp256k1/libsecp256k1/README.md b/crypto/secp256k1/libsecp256k1/README.md index 8cd344ea8..850e654c0 100644 --- a/crypto/secp256k1/libsecp256k1/README.md +++ b/crypto/secp256k1/libsecp256k1/README.md @@ -8,6 +8,7 @@ Optimized C library for EC operations on curve secp256k1. This library is a work in progress and is being used to research best practices. Use at your own risk. Features: + * secp256k1 ECDSA signing/verification and key generation. * Adding/multiplying private/public keys. * Serialization/parsing of private keys, public keys, signatures. @@ -19,35 +20,39 @@ Implementation details ---------------------- * General - * No runtime heap allocation. - * Extensive testing infrastructure. - * Structured to facilitate review and analysis. - * Intended to be portable to any system with a C89 compiler and uint64_t support. - * Expose only higher level interfaces to minimize the API surface and improve application security. ("Be difficult to use insecurely.") + * No runtime heap allocation. + * Extensive testing infrastructure. + * Structured to facilitate review and analysis. + * Intended to be portable to any system with a C89 compiler and uint64_t support. + * Expose only higher level interfaces to minimize the API surface and improve application security. ("Be difficult + to use insecurely.") * Field operations - * Optimized implementation of arithmetic modulo the curve's field size (2^256 - 0x1000003D1). - * Using 5 52-bit limbs (including hand-optimized assembly for x86_64, by Diederik Huys). - * Using 10 26-bit limbs. - * Field inverses and square roots using a sliding window over blocks of 1s (by Peter Dettman). + * Optimized implementation of arithmetic modulo the curve's field size (2^256 - 0x1000003D1). + * Using 5 52-bit limbs (including hand-optimized assembly for x86_64, by Diederik Huys). + * Using 10 26-bit limbs. + * Field inverses and square roots using a sliding window over blocks of 1s (by Peter Dettman). * Scalar operations - * Optimized implementation without data-dependent branches of arithmetic modulo the curve's order. - * Using 4 64-bit limbs (relying on __int128 support in the compiler). - * Using 8 32-bit limbs. + * Optimized implementation without data-dependent branches of arithmetic modulo the curve's order. + * Using 4 64-bit limbs (relying on __int128 support in the compiler). + * Using 8 32-bit limbs. * Group operations - * Point addition formula specifically simplified for the curve equation (y^2 = x^3 + 7). - * Use addition between points in Jacobian and affine coordinates where possible. - * Use a unified addition/doubling formula where necessary to avoid data-dependent branches. - * Point/x comparison without a field inversion by comparison in the Jacobian coordinate space. + * Point addition formula specifically simplified for the curve equation (y^2 = x^3 + 7). + * Use addition between points in Jacobian and affine coordinates where possible. + * Use a unified addition/doubling formula where necessary to avoid data-dependent branches. + * Point/x comparison without a field inversion by comparison in the Jacobian coordinate space. * Point multiplication for verification (a*P + b*G). - * Use wNAF notation for point multiplicands. - * Use a much larger window for multiples of G, using precomputed multiples. - * Use Shamir's trick to do the multiplication with the public key and the generator simultaneously. - * Optionally (off by default) use secp256k1's efficiently-computable endomorphism to split the P multiplicand into 2 half-sized ones. + * Use wNAF notation for point multiplicands. + * Use a much larger window for multiples of G, using precomputed multiples. + * Use Shamir's trick to do the multiplication with the public key and the generator simultaneously. + * Optionally (off by default) use secp256k1's efficiently-computable endomorphism to split the P multiplicand into 2 + half-sized ones. * Point multiplication for signing - * Use a precomputed table of multiples of powers of 16 multiplied with the generator, so general multiplication becomes a series of additions. - * Access the table with branch-free conditional moves so memory access is uniform. - * No data-dependent branches - * The precomputed tables add and eventually subtract points for which no known scalar (private key) is known, preventing even an attacker with control over the private key used to control the data internally. + * Use a precomputed table of multiples of powers of 16 multiplied with the generator, so general multiplication + becomes a series of additions. + * Access the table with branch-free conditional moves so memory access is uniform. + * No data-dependent branches + * The precomputed tables add and eventually subtract points for which no known scalar (private key) is known, + preventing even an attacker with control over the private key used to control the data internally. Build steps ----------- diff --git a/docker-compose.yml b/docker-compose.yml index 20853b00e..39f0e7d2f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,12 +10,12 @@ services: - "127.0.0.1:3000:3000" restart: always healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8841/api/status"] + test: [ "CMD", "curl", "-f", "http://localhost:8841/api/status" ] interval: 5s timeout: 5s retries: 3 start_period: 30s volumes: - minter_data: - external: false + minter_data: + external: false diff --git a/formula/formula.go b/formula/formula.go index bd8769a72..8afa87687 100644 --- a/formula/formula.go +++ b/formula/formula.go @@ -1,7 +1,6 @@ package formula import ( - "github.com/MinterTeam/minter-go-node/core/types" "github.com/MinterTeam/minter-go-node/math" "math/big" ) @@ -13,7 +12,7 @@ const ( // CalculatePurchaseReturn calculates amount of coin that user will receive by depositing given amount of BIP // Return = supply * ((1 + deposit / reserve) ^ (crr / 100) - 1) func CalculatePurchaseReturn(supply *big.Int, reserve *big.Int, crr uint32, deposit *big.Int) *big.Int { - if deposit.Cmp(types.Big0) == 0 { + if deposit.Sign() == 0 { return big.NewInt(0) } @@ -41,7 +40,7 @@ func CalculatePurchaseReturn(supply *big.Int, reserve *big.Int, crr uint32, depo // CalculatePurchaseAmount is the reversed version of function CalculatePurchaseReturn // Deposit = reserve * (((wantReceive + supply) / supply)^(100/c) - 1) func CalculatePurchaseAmount(supply *big.Int, reserve *big.Int, crr uint32, wantReceive *big.Int) *big.Int { - if wantReceive.Cmp(types.Big0) == 0 { + if wantReceive.Sign() == 0 { return big.NewInt(0) } @@ -70,7 +69,7 @@ func CalculatePurchaseAmount(supply *big.Int, reserve *big.Int, crr uint32, want // Return = reserve * (1 - (1 - sellAmount / supply) ^ (100 / crr)) func CalculateSaleReturn(supply *big.Int, reserve *big.Int, crr uint32, sellAmount *big.Int) *big.Int { // special case for 0 sell amount - if sellAmount.Cmp(types.Big0) == 0 { + if sellAmount.Sign() == 0 { return big.NewInt(0) } @@ -104,7 +103,7 @@ func CalculateSaleReturn(supply *big.Int, reserve *big.Int, crr uint32, sellAmou // CalculateSaleAmount is the reversed version of function CalculateSaleReturn // Deposit = -(-1 + (-(wantReceive - reserve)/reserve)^(1/crr)) * supply func CalculateSaleAmount(supply *big.Int, reserve *big.Int, crr uint32, wantReceive *big.Int) *big.Int { - if wantReceive.Cmp(types.Big0) == 0 { + if wantReceive.Sign() == 0 { return big.NewInt(0) } diff --git a/go.mod b/go.mod index 0e42cc0b2..30d3008f6 100644 --- a/go.mod +++ b/go.mod @@ -3,40 +3,35 @@ module github.com/MinterTeam/minter-go-node go 1.15 require ( - github.com/MinterTeam/node-grpc-gateway v1.2.1 - github.com/btcsuite/btcd v0.20.1-beta + github.com/MinterTeam/node-grpc-gateway v1.2.2-0.20210217004251-74c4534aa659 + github.com/btcsuite/btcd v0.21.0-beta github.com/c-bata/go-prompt v0.2.3 + github.com/cosmos/iavl v0.15.3 github.com/go-kit/kit v0.10.0 github.com/golang/protobuf v1.4.3 - github.com/google/uuid v1.1.2 github.com/gorilla/handlers v1.4.2 - github.com/gorilla/websocket v1.4.2 github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.1.0 github.com/marcusolsson/tui-go v0.4.0 github.com/mattn/go-isatty v0.0.12 // indirect github.com/mattn/go-tty v0.0.3 // indirect github.com/pkg/errors v0.9.1 github.com/pkg/term v0.0.0-20190109203006-aa71e9d9e942 // indirect - github.com/prometheus/client_golang v1.5.1 + github.com/prometheus/client_golang v1.8.0 github.com/rakyll/statik v0.1.7 - github.com/rs/cors v1.7.0 - github.com/spf13/cobra v1.0.0 - github.com/spf13/viper v1.6.3 - github.com/stretchr/testify v1.6.1 + github.com/spf13/cobra v1.1.1 + github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.7.0 github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca github.com/tendermint/go-amino v0.15.1 - github.com/tendermint/iavl v0.14.3 - github.com/tendermint/tendermint v0.33.8 - github.com/tendermint/tm-db v0.5.2 - github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966 + github.com/tendermint/tendermint v0.34.3 + github.com/tendermint/tm-db v0.6.3 + github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 github.com/urfave/cli/v2 v2.0.0 - golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 - golang.org/x/net v0.0.0-20200822124328-c89045814202 - golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 + golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a - google.golang.org/grpc v1.33.1 + google.golang.org/grpc v1.35.0 google.golang.org/protobuf v1.25.0 - gopkg.in/errgo.v2 v2.1.0 ) diff --git a/go.sum b/go.sum index a49020131..922954bdf 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,7 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -36,9 +37,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/DataDog/zstd v1.4.1 h1:3oxKN3wbHibqx897utPC2LTQU4J+IHWWJO+glkAkpFM= +github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= -github.com/MinterTeam/node-grpc-gateway v1.2.1 h1:Y+86Sc3WpGbdiDZLI7DyEUS7Y3qaNUtXjYMzRBhsDmE= -github.com/MinterTeam/node-grpc-gateway v1.2.1/go.mod h1:oyBmm4OA4XyHpfbz7gHmP4j82qO3Xb2Z31hydzP192w= +github.com/MinterTeam/node-grpc-gateway v1.2.2-0.20210217004251-74c4534aa659 h1:LBzBdlcaJzpScR0CpySm/maSJAG5q14EPhzRkRwNfqE= +github.com/MinterTeam/node-grpc-gateway v1.2.2-0.20210217004251-74c4534aa659/go.mod h1:d4Rw0MtsZTrFNLJJWr/jcurTYcqRVITzWuGDGo7aHxM= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= @@ -52,6 +56,7 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= @@ -69,15 +74,20 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/btcsuite/btcd v0.20.1-beta h1:Ik4hyJqN8Jfyv3S4AGBOmyouMsYE3EdYODkMbQjwPGw= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.21.0-beta h1:At9hIZdJW0s9E/fAz28nrz6AmcNlSVucCH796ZteX1M= +github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/c-bata/go-prompt v0.2.3 h1:jjCS+QhG/sULBhAaBdjb2PlMRVaKXQgn+4yzaauvs2s= @@ -95,18 +105,31 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/confio/ics23/go v0.0.0-20200817220745-f173e6211efb/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= +github.com/confio/ics23/go v0.6.3 h1:PuGK2V1NJWZ8sSkNDq91jgT/cahFEW9RGp4Y5jxulf0= +github.com/confio/ics23/go v0.6.3/go.mod h1:E45NqnlpxGnpfTWL/xauN7MRwEE28T4Dd4uraToOaKg= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d h1:49RLWk1j44Xu4fjHb6JFYmeUnDORVwHNkDxaQ0ctCVU= github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= +github.com/cosmos/iavl v0.15.0-rc3.0.20201009144442-230e9bdf52cd/go.mod h1:3xOIaNNX19p0QrX0VqWa6voPRoJRGGYtny+DH8NEPvE= +github.com/cosmos/iavl v0.15.0-rc5/go.mod h1:WqoPL9yPTQ85QBMT45OOUzPxG/U/JcJoN7uMjgxke/I= +github.com/cosmos/iavl v0.15.3 h1:xE9r6HW8GeKeoYJN4zefpljZ1oukVScP/7M8oj6SUts= +github.com/cosmos/iavl v0.15.3/go.mod h1:OLjQiAQ4fGD2KDZooyJG9yz+p2ao2IAYSbke8mVvSA4= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= @@ -116,9 +139,19 @@ github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgraph-io/badger/v2 v2.2007.1/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= +github.com/dgraph-io/badger/v2 v2.2007.2 h1:EjjK0KqwaFMlPin1ajhP943VPENHJdEz1KLIegjaI3k= +github.com/dgraph-io/badger/v2 v2.2007.2/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de h1:t0UHb5vdojIDUqktM6+xJAfScFBsVpXZmqC9dsgJmeA= +github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -127,6 +160,8 @@ github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4s github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ= github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= @@ -164,12 +199,15 @@ github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -219,8 +257,8 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -256,23 +294,30 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 h1:z53tR0945TRRQO/fLEVPI6SMv7ZflF0TEaTAoU7tOzg= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.1/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2 h1:FlFbCRLd5Jr4iYXZufAvgWN6Ao0JrI5chLINnUXDDr0= github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.0 h1:bM6ZAFZmc/wPFaRDi0d5L7hGEZEx/2u+Tmr2evNHDiI= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1 h1:X2vfSnm1WC8HEo0MBHZg2TcuDUHJj6kd1TmEAQncnSA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.1/go.mod h1:oVMjMN64nzEcepv1kdZKgx1qNYt4Ro0Gqefiq2JWdis= +github.com/grpc-ecosystem/grpc-gateway v1.14.7/go.mod h1:oYZKL012gGh6LMyg/xA7Q2yq6j8bu0wa+9w14EEthWU= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.1.0 h1:EhTvIsn53GrBLl45YVHk25cUHQHwlJfq2y8b7W5IpVY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.1.0/go.mod h1:ly5QWKtiqC7tGfzgXYtpoZYmEWx5Z82/b18ASEL+yGc= github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f h1:8N8XWLZelZNibkhM1FuF+3Ad3YIbgirjdMiVA0eUkaM= github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -303,28 +348,33 @@ github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -365,8 +415,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643 h1:hLDRPB66XQT/8+wG9WsDpiCvZf1yKO7sz7scAjSlBa0= github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= -github.com/minio/highwayhash v1.0.0 h1:iMSDhgUILCr0TNm8LWlSjF8N0ZIj2qbO8WHp6Q/J2BA= -github.com/minio/highwayhash v1.0.0/go.mod h1:xQboMTeM9nY9v/LlAOxFctujiv5+Aq2hR5dxBpaMbdc= +github.com/minio/highwayhash v1.0.1 h1:dZ6IIu8Z14VlC0VpfKofAhCy74wu/Qb5gcn52yWoz/0= +github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -385,6 +435,7 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= @@ -392,6 +443,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= @@ -404,6 +457,7 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= @@ -424,6 +478,8 @@ github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= +github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -443,8 +499,9 @@ github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAy github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA= -github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.8.0 h1:zvJNkoCFAnYFNC24FV8nW4JdRJ3GIFcLbg65lL/JDcw= +github.com/prometheus/client_golang v1.8.0/go.mod h1:O9VU6huf47PktckDQfMTX0Y8tY0/7TSWwj+ITvv0TnM= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -459,8 +516,9 @@ github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= -github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.14.0 h1:RHRyE8UocrbjU+6UvRzwi6HjiDfxrrBU91TtbKzkGp4= +github.com/prometheus/common v0.14.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY= @@ -468,6 +526,9 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= @@ -480,10 +541,14 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sasha-s/go-deadlock v0.2.0 h1:lMqc+fUb7RrFS3gQLtoQsJ7/6TV/pAIFvBsqX73DK8Y= +github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -491,6 +556,8 @@ github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= @@ -500,27 +567,37 @@ github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPH github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= -github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -530,32 +607,31 @@ github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= -github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d h1:gZZadD8H+fF+n9CmNhYL1Y0dJB+kLOmKd7FbPJLeGHs= -github.com/syndtr/goleveldb v1.0.1-0.20190923125748-758128399b1d/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca h1:Ld/zXl5t4+D69SiV4JoN7kkfvJdOWlPpfxrzxpLMoUk= github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= -github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= github.com/tendermint/go-amino v0.15.1 h1:D2uk35eT4iTsvJd9jWIetzthE5C0/k2QmMFkCN+4JgQ= github.com/tendermint/go-amino v0.15.1/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= -github.com/tendermint/iavl v0.14.3 h1:tuiUAqJdA3OOyPU/9P3pMYnAcd+OL7BUdzNiE3ytUwQ= -github.com/tendermint/iavl v0.14.3/go.mod h1:vHLYxU/zuxBmxxr1v+5Vnd/JzcIsyK17n9P9RDubPVU= -github.com/tendermint/tendermint v0.33.5 h1:jYgRd9ImkzA9iOyhpmgreYsqSB6tpDa6/rXYPb8HKE8= -github.com/tendermint/tendermint v0.33.5/go.mod h1:0yUs9eIuuDq07nQql9BmI30FtYGcEC60Tu5JzB5IezM= -github.com/tendermint/tendermint v0.33.8 h1:Xxu4QhpqcomSE0iQDw1MqLgfsa8fqtPtWFJK6zZOVso= -github.com/tendermint/tendermint v0.33.8/go.mod h1:0yUs9eIuuDq07nQql9BmI30FtYGcEC60Tu5JzB5IezM= -github.com/tendermint/tm-db v0.5.1/go.mod h1:g92zWjHpCYlEvQXvy9M168Su8V1IBEeawpXVVBaK4f4= -github.com/tendermint/tm-db v0.5.2 h1:QG3IxQZBubWlr7kGQcYIavyTNmZRO+r//nENxoq0g34= -github.com/tendermint/tm-db v0.5.2/go.mod h1:VrPTx04QJhQ9d8TFUTc2GpPBvBf/U9vIdBIzkjBk7Lk= +github.com/tendermint/tendermint v0.34.0-rc4/go.mod h1:yotsojf2C1QBOw4dZrTcxbyxmPUrT4hNuOQWX9XUwB4= +github.com/tendermint/tendermint v0.34.0-rc6/go.mod h1:ugzyZO5foutZImv0Iyx/gOFCX6mjJTgbLHTwi17VDVg= +github.com/tendermint/tendermint v0.34.0/go.mod h1:Aj3PIipBFSNO21r+Lq3TtzQ+uKESxkbA3yo/INM4QwQ= +github.com/tendermint/tendermint v0.34.3 h1:9yEsf3WO5VAwPVwrmM+RffDMiijmNfWaBwNttHm0q5w= +github.com/tendermint/tendermint v0.34.3/go.mod h1:h57vnXeOlrdvvNFCqPBSaOrpOivl+2swWEtlUAqStYE= +github.com/tendermint/tm-db v0.6.2/go.mod h1:GYtQ67SUvATOcoY8/+x6ylk8Qo02BQyLrAs+yAcLvGI= +github.com/tendermint/tm-db v0.6.3 h1:ZkhQcKnB8/2jr5EaZwGndN4owkPsGezW2fSisS9zGbg= +github.com/tendermint/tm-db v0.6.3/go.mod h1:lfA1dL9/Y/Y8wwyPp2NMLyn5P5Ptr/gvDFNWtrCWSf8= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966 h1:j6JEOq5QWFker+d7mFQYOhjTZonQ7YkLTHm56dbn+yM= -github.com/tmc/grpc-websocket-proxy v0.0.0-20200427203606-3cfed13b9966/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= +github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -566,11 +642,12 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1: github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg= -go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.5 h1:XAzx9gjCb0Rxj7EoqcClPD1d5ZBxZJk0jbuoPHenBt0= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -590,6 +667,7 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -598,12 +676,13 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 h1:DOmugCavvUtnUD114C1Wh+UgTgQZ4pMLzXxi1pSt+/Y= -golang.org/x/crypto v0.0.0-20200406173513-056763e48d71/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88= -golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9 h1:phUcVbl53swtrUN8kQEXFhUxPlIlWyBfKmidCu7P95o= +golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -636,6 +715,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -658,6 +738,7 @@ golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8 golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -677,12 +758,14 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -694,6 +777,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -702,6 +787,7 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -712,17 +798,20 @@ golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e h1:N7DeIrjYszNmSW409R3frPPwglRwMkXSBzwVbkOjLLA= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82 h1:ywK/j/KkyTHcdyYSZNXGjMwgmDSfjglYZ3vStQ/gSCU= @@ -741,11 +830,16 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642 h1:B6caxRw+hozq68X2MY7jEpZh/cr4/aHLv9xU8Kkadrw= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a h1:e3IU37lwO4aq3uoRKINC7JikojFmE5gO7xhfxs8VC34= golang.org/x/sys v0.0.0-20201020230747-6e5568b54d1a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -779,6 +873,7 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -803,9 +898,11 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= @@ -863,16 +960,20 @@ google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154 h1:bFFRpT+e8JJVY7lMMfvezL1ZIwqiwmPl2bsE2yx4HqM= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201111145450-ac7456db90a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201119123407-9b1e624d6bc4/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210106152847-07624b53cd92 h1:jOTk2Z6KYaWoptUFqZ167cS8peoUPjFEXrsqfVkkCGc= +google.golang.org/genproto v0.0.0-20210106152847-07624b53cd92/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -887,13 +988,17 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0 h1:bO/TA4OxCOummhSf10siHuG7vJOiwh7SpRpFZDkOgl4= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.28.1 h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k= -google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1 h1:DGeFlSan2f+WEtCERJ4J9GJWk15TxUi8QGagfI87Xyc= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0 h1:raiipEjMOIC/TO2AvyTxP25XFdLxNIBwzDh3FM3XztI= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0 h1:TwIQcH3es+MojMVojxxfQ3l3OF2KzlRxML2xZq0kRo8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.0.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -916,6 +1021,9 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -937,6 +1045,7 @@ gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/minter-logo.svg b/minter-logo.svg index b01f98462..71a02b9ca 100644 --- a/minter-logo.svg +++ b/minter-logo.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/rlp/decode.go b/rlp/decode.go index 8eaab8bdd..5f3f5eedf 100644 --- a/rlp/decode.go +++ b/rlp/decode.go @@ -26,15 +26,16 @@ import ( "math/big" "reflect" "strings" + "sync" ) -// Set of Errors -var ( - // EOL is returned when the end of the current list - // has been reached during streaming. - EOL = errors.New("rlp: end of list") +//lint:ignore ST1012 EOL is not an error. + +// EOL is returned when the end of the current list +// has been reached during streaming. +var EOL = errors.New("rlp: end of list") - // Actual Errors +var ( ErrExpectedString = errors.New("rlp: expected String or Byte") ErrExpectedList = errors.New("rlp: expected List") ErrCanonInt = errors.New("rlp: non-canonical integer format") @@ -49,107 +50,49 @@ var ( errUintOverflow = errors.New("rlp: uint overflow") errNoPointer = errors.New("rlp: interface given to Decode must be a pointer") errDecodeIntoNil = errors.New("rlp: pointer given to Decode must not be nil") + + streamPool = sync.Pool{ + New: func() interface{} { return new(Stream) }, + } ) -// Decoder is implemented by types that require custom RLP -// decoding rules or need to decode into private fields. +// Decoder is implemented by types that require custom RLP decoding rules or need to decode +// into private fields. // -// The DecodeRLP method should read one value from the given -// Stream. It is not forbidden to read less or more, but it might -// be confusing. +// The DecodeRLP method should read one value from the given Stream. It is not forbidden to +// read less or more, but it might be confusing. type Decoder interface { DecodeRLP(*Stream) error } -// Decode parses RLP-encoded data from r and stores the result in the -// value pointed to by val. Val must be a non-nil pointer. If r does -// not implement ByteReader, Decode will do its own buffering. -// -// Decode uses the following type-dependent decoding rules: -// -// If the type implements the Decoder interface, decode calls -// DecodeRLP. -// -// To decode into a pointer, Decode will decode into the value pointed -// to. If the pointer is nil, a new value of the pointer's element -// type is allocated. If the pointer is non-nil, the existing value -// will be reused. -// -// To decode into a struct, Decode expects the input to be an RLP -// list. The decoded elements of the list are assigned to each public -// field in the order given by the struct's definition. The input list -// must contain an element for each decoded field. Decode returns an -// error if there are too few or too many elements. -// -// The decoding of struct fields honours certain struct tags, "tail", -// "nil" and "-". -// -// The "-" tag ignores fields. +// Decode parses RLP-encoded data from r and stores the result in the value pointed to by +// val. Please see package-level documentation for the decoding rules. Val must be a +// non-nil pointer. // -// For an explanation of "tail", see the example. +// If r does not implement ByteReader, Decode will do its own buffering. // -// The "nil" tag applies to pointer-typed fields and changes the decoding -// rules for the field such that input values of size zero decode as a nil -// pointer. This tag can be useful when decoding recursive types. -// -// type StructWithEmptyOK struct { -// Foo *[20]byte `rlp:"nil"` -// } -// -// To decode into a slice, the input must be a list and the resulting -// slice will contain the input elements in order. For byte slices, -// the input must be an RLP string. Array types decode similarly, with -// the additional restriction that the number of input elements (or -// bytes) must match the array's length. -// -// To decode into a Go string, the input must be an RLP string. The -// input bytes are taken as-is and will not necessarily be valid UTF-8. -// -// To decode into an unsigned integer type, the input must also be an RLP -// string. The bytes are interpreted as a big endian representation of -// the integer. If the RLP string is larger than the bit size of the -// type, Decode will return an error. Decode also supports *big.Int. -// There is no size limit for big integers. -// -// To decode into an interface value, Decode stores one of these -// in the value: -// -// []interface{}, for RLP lists -// []byte, for RLP strings -// -// Non-empty interface types are not supported, nor are booleans, -// signed integers, floating point numbers, maps, channels and -// functions. -// -// Note that Decode does not set an input limit for all readers -// and may be vulnerable to panics cause by huge value sizes. If -// you need an input limit, use +// Note that Decode does not set an input limit for all readers and may be vulnerable to +// panics cause by huge value sizes. If you need an input limit, use // // NewStream(r, limit).Decode(val) func Decode(r io.Reader, val interface{}) error { - // TODO: this could use a Stream from a pool. - return NewStream(r, 0).Decode(val) + stream := streamPool.Get().(*Stream) + defer streamPool.Put(stream) + + stream.Reset(r, 0) + return stream.Decode(val) } -// DecodeBytes parses RLP data from b into val. -// Please see the documentation of Decode for the decoding rules. -// The input must contain exactly one value and no trailing data. +// DecodeBytes parses RLP data from b into val. Please see package-level documentation for +// the decoding rules. The input must contain exactly one value and no trailing data. func DecodeBytes(b []byte, val interface{}) error { - // TODO: this could use a Stream from a pool. r := bytes.NewReader(b) - if err := NewStream(r, uint64(len(b))).Decode(val); err != nil { - return err - } - if r.Len() > 0 { - return ErrMoreThanOneValue - } - return nil -} -func DecodeBytesForType(b []byte, rt reflect.Type, val interface{}) error { - // TODO: this could use a Stream from a pool. - r := bytes.NewReader(b) - if err := NewStream(r, uint64(len(b))).DecodeForType(rt, val); err != nil { + stream := streamPool.Get().(*Stream) + defer streamPool.Put(stream) + + stream.Reset(r, uint64(len(b))) + if err := stream.Decode(val); err != nil { return err } if r.Len() > 0 { @@ -210,14 +153,14 @@ func makeDecoder(typ reflect.Type, tags tags) (dec decoder, err error) { switch { case typ == rawValueType: return decodeRawValue, nil - case typ.Implements(decoderInterface): - return decodeDecoder, nil - case kind != reflect.Ptr && reflect.PtrTo(typ).Implements(decoderInterface): - return decodeDecoderNoPtr, nil case typ.AssignableTo(reflect.PtrTo(bigInt)): return decodeBigInt, nil case typ.AssignableTo(bigInt): return decodeBigIntNoPtr, nil + case kind == reflect.Ptr: + return makePtrDecoder(typ, tags) + case reflect.PtrTo(typ).Implements(decoderInterface): + return decodeDecoder, nil case isUint(kind): return decodeUint, nil case kind == reflect.Bool: @@ -228,11 +171,6 @@ func makeDecoder(typ reflect.Type, tags tags) (dec decoder, err error) { return makeListDecoder(typ, tags) case kind == reflect.Struct: return makeStructDecoder(typ) - case kind == reflect.Ptr: - if tags.nilOK { - return makeOptionalPtrDecoder(typ) - } - return makePtrDecoder(typ) case kind == reflect.Interface: return decodeInterface, nil default: @@ -307,9 +245,9 @@ func makeListDecoder(typ reflect.Type, tag tags) (decoder, error) { } return decodeByteSlice, nil } - etypeinfo, err := cachedTypeInfo1(etype, tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(etype, tags{}) + if etypeinfo.decoderErr != nil { + return nil, etypeinfo.decoderErr } var dec decoder switch { @@ -447,6 +385,11 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) { if err != nil { return nil, err } + for _, f := range fields { + if f.info.decoderErr != nil { + return nil, structFieldError{typ, f.index, f.info.decoderErr} + } + } dec := func(s *Stream, val reflect.Value) (err error) { if _, err := s.List(); err != nil { return wrapStreamError(err, typ) @@ -464,15 +407,22 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) { return dec, nil } -// makePtrDecoder creates a decoder that decodes into -// the pointer's element type. -func makePtrDecoder(typ reflect.Type) (decoder, error) { +// makePtrDecoder creates a decoder that decodes into the pointer's element type. +func makePtrDecoder(typ reflect.Type, tag tags) (decoder, error) { etype := typ.Elem() - etypeinfo, err := cachedTypeInfo1(etype, tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(etype, tags{}) + switch { + case etypeinfo.decoderErr != nil: + return nil, etypeinfo.decoderErr + case !tag.nilOK: + return makeSimplePtrDecoder(etype, etypeinfo), nil + default: + return makeNilPtrDecoder(etype, etypeinfo, tag.nilKind), nil } - dec := func(s *Stream, val reflect.Value) (err error) { +} + +func makeSimplePtrDecoder(etype reflect.Type, etypeinfo *typeinfo) decoder { + return func(s *Stream, val reflect.Value) (err error) { newval := val if val.IsNil() { newval = reflect.New(etype) @@ -482,30 +432,35 @@ func makePtrDecoder(typ reflect.Type) (decoder, error) { } return err } - return dec, nil } -// makeOptionalPtrDecoder creates a decoder that decodes empty values -// as nil. Non-empty values are decoded into a value of the element type, -// just like makePtrDecoder does. +// makeNilPtrDecoder creates a decoder that decodes empty values as nil. Non-empty +// values are decoded into a value of the element type, just like makePtrDecoder does. // // This decoder is used for pointer-typed struct fields with struct tag "nil". -func makeOptionalPtrDecoder(typ reflect.Type) (decoder, error) { - etype := typ.Elem() - etypeinfo, err := cachedTypeInfo1(etype, tags{}) - if err != nil { - return nil, err - } - dec := func(s *Stream, val reflect.Value) (err error) { +func makeNilPtrDecoder(etype reflect.Type, etypeinfo *typeinfo, nilKind Kind) decoder { + typ := reflect.PtrTo(etype) + nilPtr := reflect.Zero(typ) + return func(s *Stream, val reflect.Value) (err error) { kind, size, err := s.Kind() - if err != nil || size == 0 && kind != Byte { + if err != nil { + val.Set(nilPtr) + return wrapStreamError(err, typ) + } + // Handle empty values as a nil pointer. + if kind != Byte && size == 0 { + if kind != nilKind { + return &decodeError{ + msg: fmt.Sprintf("wrong kind of empty value (got %v, want %v)", kind, nilKind), + typ: typ, + } + } // rearm s.Kind. This is important because the input // position must advance to the next value even though // we don't read anything. s.kind = -1 - // set the pointer to nil. - val.Set(reflect.Zero(typ)) - return err + val.Set(nilPtr) + return nil } newval := val if val.IsNil() { @@ -516,7 +471,6 @@ func makeOptionalPtrDecoder(typ reflect.Type) (decoder, error) { } return err } - return dec, nil } var ifsliceType = reflect.TypeOf([]interface{}{}) @@ -545,21 +499,8 @@ func decodeInterface(s *Stream, val reflect.Value) error { return nil } -// This decoder is used for non-pointer values of types -// that implement the Decoder interface using a pointer receiver. -func decodeDecoderNoPtr(s *Stream, val reflect.Value) error { - return val.Addr().Interface().(Decoder).DecodeRLP(s) -} - func decodeDecoder(s *Stream, val reflect.Value) error { - // Decoder instances are not handled using the pointer rule if the type - // implements Decoder with pointer receiver (i.e. always) - // because it might handle empty values specially. - // We need to allocate one here in this case, like makePtrDecoder does. - if val.Kind() == reflect.Ptr && val.IsNil() { - val.Set(reflect.New(val.Type().Elem())) - } - return val.Interface().(Decoder).DecodeRLP(s) + return val.Addr().Interface().(Decoder).DecodeRLP(s) } // Kind represents the kind of value contained in an RLP stream. @@ -815,12 +756,12 @@ func (s *Stream) Decode(val interface{}) error { if rval.IsNil() { return errDecodeIntoNil } - info, err := cachedTypeInfo(rtyp.Elem(), tags{}) + decoder, err := cachedDecoder(rtyp.Elem()) if err != nil { return err } - err = info.decoder(s, rval.Elem()) + err = decoder(s, rval.Elem()) if decErr, ok := err.(*decodeError); ok && len(decErr.ctx) > 0 { // add decode target type to error so context has more meaning decErr.ctx = append(decErr.ctx, fmt.Sprint("(", rtyp.Elem(), ")")) @@ -828,36 +769,6 @@ func (s *Stream) Decode(val interface{}) error { return err } -func (s *Stream) DecodeForType(rt reflect.Type, val interface{}) error { - if val == nil { - return errDecodeIntoNil - } - rval := reflect.ValueOf(val) - - if rval.Type().Kind() != reflect.Ptr { - return errNoPointer - } - if rval.IsNil() { - return errDecodeIntoNil - } - info, err := cachedTypeInfo(rt, tags{}) - if err != nil { - return err - } - - cPtrRv := reflect.New(rt) - crv := cPtrRv.Elem() - - err = info.decoder(s, crv) - if decErr, ok := err.(*decodeError); ok && len(decErr.ctx) > 0 { - // add decode target type to error so context has more meaning - decErr.ctx = append(decErr.ctx, fmt.Sprint("(", rt, ")")) - } - - rval.Elem().Set(cPtrRv) - return err -} - // Reset discards any information about the current decoding context // and starts reading from r. This method is meant to facilitate reuse // of a preallocated Stream across many decoding operations. @@ -896,6 +807,7 @@ func (s *Stream) Reset(r io.Reader, inputLimit uint64) { if s.uintbuf == nil { s.uintbuf = make([]byte, 8) } + s.byteval = 0 } // Kind returns the kind and size of the next value in the diff --git a/rlp/decode_test.go b/rlp/decode_test.go index 4d8abd001..167e9974b 100644 --- a/rlp/decode_test.go +++ b/rlp/decode_test.go @@ -327,6 +327,10 @@ type recstruct struct { Child *recstruct `rlp:"nil"` } +type invalidNilTag struct { + X []byte `rlp:"nil"` +} + type invalidTail1 struct { A uint `rlp:"tail"` B string @@ -347,6 +351,24 @@ type tailUint struct { Tail []uint `rlp:"tail"` } +type tailPrivateFields struct { + A uint + Tail []uint `rlp:"tail"` + x, y bool //lint:ignore U1000 unused fields required for testing purposes. +} + +type nilListUint struct { + X *uint `rlp:"nilList"` +} + +type nilStringSlice struct { + X *[]uint `rlp:"nilString"` +} + +type intField struct { + X int +} + var ( veryBigInt = big.NewInt(0).Add( big.NewInt(0).Lsh(big.NewInt(0xFFFFFFFFFFFFFF), 16), @@ -479,20 +501,20 @@ var decodeTests = []decodeTest{ error: "rlp: expected input string or byte for uint, decoding into (rlp.recstruct).Child.I", }, { - input: "C0", - ptr: new(invalidTail1), - error: "rlp: invalid struct tag \"tail\" for rlp.invalidTail1.A (must be on last field)", - }, - { - input: "C0", - ptr: new(invalidTail2), - error: "rlp: invalid struct tag \"tail\" for rlp.invalidTail2.B (field type is not slice)", + input: "C103", + ptr: new(intField), + error: "rlp: type int is not RLP-serializable (struct field rlp.intField.X)", }, { input: "C50102C20102", ptr: new(tailUint), error: "rlp: expected input string or byte for uint, decoding into (rlp.tailUint).Tail[1]", }, + { + input: "C0", + ptr: new(invalidNilTag), + error: `rlp: invalid struct tag "nil" for rlp.invalidNilTag.X (field is not a pointer)`, + }, // struct tag "tail" { @@ -510,6 +532,21 @@ var decodeTests = []decodeTest{ ptr: new(tailRaw), value: tailRaw{A: 1, Tail: []RawValue{}}, }, + { + input: "C3010203", + ptr: new(tailPrivateFields), + value: tailPrivateFields{A: 1, Tail: []uint{2, 3}}, + }, + { + input: "C0", + ptr: new(invalidTail1), + error: `rlp: invalid struct tag "tail" for rlp.invalidTail1.A (must be on last field)`, + }, + { + input: "C0", + ptr: new(invalidTail2), + error: `rlp: invalid struct tag "tail" for rlp.invalidTail2.B (field type is not slice)`, + }, // struct tag "-" { @@ -518,6 +555,43 @@ var decodeTests = []decodeTest{ value: hasIgnoredField{A: 1, C: 2}, }, + // struct tag "nilList" + { + input: "C180", + ptr: new(nilListUint), + error: "rlp: wrong kind of empty value (got String, want List) for *uint, decoding into (rlp.nilListUint).X", + }, + { + input: "C1C0", + ptr: new(nilListUint), + value: nilListUint{}, + }, + { + input: "C103", + ptr: new(nilListUint), + value: func() interface{} { + v := uint(3) + return nilListUint{X: &v} + }(), + }, + + // struct tag "nilString" + { + input: "C1C0", + ptr: new(nilStringSlice), + error: "rlp: wrong kind of empty value (got List, want String) for *[]uint, decoding into (rlp.nilStringSlice).X", + }, + { + input: "C180", + ptr: new(nilStringSlice), + value: nilStringSlice{}, + }, + { + input: "C2C103", + ptr: new(nilStringSlice), + value: nilStringSlice{X: &[]uint{3}}, + }, + // RawValue {input: "01", ptr: new(RawValue), value: RawValue(unhex("01"))}, {input: "82FFFF", ptr: new(RawValue), value: RawValue(unhex("82FFFF"))}, @@ -661,6 +735,22 @@ func TestDecodeDecoder(t *testing.T) { } } +func TestDecodeDecoderNilPointer(t *testing.T) { + var s struct { + T1 *testDecoder `rlp:"nil"` + T2 *testDecoder + } + if err := Decode(bytes.NewReader(unhex("C2C002")), &s); err != nil { + t.Fatalf("Decode error: %v", err) + } + if s.T1 != nil { + t.Errorf("decoder T1 allocated for empty input (called: %v)", s.T1.called) + } + if s.T2 == nil || !s.T2.called { + t.Errorf("decoder T2 not allocated/called") + } +} + type byteDecoder byte func (bd *byteDecoder) DecodeRLP(s *Stream) error { @@ -691,13 +781,33 @@ func TestDecoderInByteSlice(t *testing.T) { } } +type unencodableDecoder func() + +func (f *unencodableDecoder) DecodeRLP(s *Stream) error { + if _, err := s.List(); err != nil { + return err + } + if err := s.ListEnd(); err != nil { + return err + } + *f = func() {} + return nil +} + +func TestDecoderFunc(t *testing.T) { + var x func() + if err := DecodeBytes([]byte{0xC0}, (*unencodableDecoder)(&x)); err != nil { + t.Fatal(err) + } + x() +} + func ExampleDecode() { input, _ := hex.DecodeString("C90A1486666F6F626172") type example struct { - A, B uint - private uint // private fields are ignored - String string + A, B uint + String string } var s example @@ -708,7 +818,7 @@ func ExampleDecode() { fmt.Printf("Decoded value: %#v\n", s) } // Output: - // Decoded value: rlp.example{A:0xa, B:0x14, private:0x0, String:"foobar"} + // Decoded value: rlp.example{A:0xa, B:0x14, String:"foobar"} } func ExampleDecode_structTagNil() { diff --git a/rlp/doc.go b/rlp/doc.go index b3a81fe23..7e6ee8520 100644 --- a/rlp/doc.go +++ b/rlp/doc.go @@ -17,17 +17,114 @@ /* Package rlp implements the RLP serialization format. -The purpose of RLP (Recursive Linear Prefix) is to encode arbitrarily -nested arrays of binary data, and RLP is the main encoding method used -to serialize objects in Ethereum. The only purpose of RLP is to encode -structure; encoding specific atomic data types (eg. strings, ints, -floats) is left up to higher-order protocols; in Ethereum integers -must be represented in big endian binary form with no leading zeroes -(thus making the integer value zero equivalent to the empty byte -array). - -RLP values are distinguished by a type tag. The type tag precedes the -value in the input stream and defines the size and kind of the bytes -that follow. +The purpose of RLP (Recursive Linear Prefix) is to encode arbitrarily nested arrays of +binary data, and RLP is the main encoding method used to serialize objects in Ethereum. +The only purpose of RLP is to encode structure; encoding specific atomic data types (eg. +strings, ints, floats) is left up to higher-order protocols. In Ethereum integers must be +represented in big endian binary form with no leading zeroes (thus making the integer +value zero equivalent to the empty string). + +RLP values are distinguished by a type tag. The type tag precedes the value in the input +stream and defines the size and kind of the bytes that follow. + + +Encoding Rules + +Package rlp uses reflection and encodes RLP based on the Go type of the value. + +If the type implements the Encoder interface, Encode calls EncodeRLP. It does not +call EncodeRLP on nil pointer values. + +To encode a pointer, the value being pointed to is encoded. A nil pointer to a struct +type, slice or array always encodes as an empty RLP list unless the slice or array has +elememt type byte. A nil pointer to any other value encodes as the empty string. + +Struct values are encoded as an RLP list of all their encoded public fields. Recursive +struct types are supported. + +To encode slices and arrays, the elements are encoded as an RLP list of the value's +elements. Note that arrays and slices with element type uint8 or byte are always encoded +as an RLP string. + +A Go string is encoded as an RLP string. + +An unsigned integer value is encoded as an RLP string. Zero always encodes as an empty RLP +string. big.Int values are treated as integers. Signed integers (int, int8, int16, ...) +are not supported and will return an error when encoding. + +Boolean values are encoded as the unsigned integers zero (false) and one (true). + +An interface value encodes as the value contained in the interface. + +Floating point numbers, maps, channels and functions are not supported. + + +Decoding Rules + +Decoding uses the following type-dependent rules: + +If the type implements the Decoder interface, DecodeRLP is called. + +To decode into a pointer, the value will be decoded as the element type of the pointer. If +the pointer is nil, a new value of the pointer's element type is allocated. If the pointer +is non-nil, the existing value will be reused. Note that package rlp never leaves a +pointer-type struct field as nil unless one of the "nil" struct tags is present. + +To decode into a struct, decoding expects the input to be an RLP list. The decoded +elements of the list are assigned to each public field in the order given by the struct's +definition. The input list must contain an element for each decoded field. Decoding +returns an error if there are too few or too many elements for the struct. + +To decode into a slice, the input must be a list and the resulting slice will contain the +input elements in order. For byte slices, the input must be an RLP string. Array types +decode similarly, with the additional restriction that the number of input elements (or +bytes) must match the array's defined length. + +To decode into a Go string, the input must be an RLP string. The input bytes are taken +as-is and will not necessarily be valid UTF-8. + +To decode into an unsigned integer type, the input must also be an RLP string. The bytes +are interpreted as a big endian representation of the integer. If the RLP string is larger +than the bit size of the type, decoding will return an error. Decode also supports +*big.Int. There is no size limit for big integers. + +To decode into a boolean, the input must contain an unsigned integer of value zero (false) +or one (true). + +To decode into an interface value, one of these types is stored in the value: + + []interface{}, for RLP lists + []byte, for RLP strings + +Non-empty interface types are not supported when decoding. +Signed integers, floating point numbers, maps, channels and functions cannot be decoded into. + + +Struct Tags + +Package rlp honours certain struct tags: "-", "tail", "nil", "nilList" and "nilString". + +The "-" tag ignores fields. + +The "tail" tag, which may only be used on the last exported struct field, allows slurping +up any excess list elements into a slice. See examples for more details. + +The "nil" tag applies to pointer-typed fields and changes the decoding rules for the field +such that input values of size zero decode as a nil pointer. This tag can be useful when +decoding recursive types. + + type StructWithOptionalFoo struct { + Foo *[20]byte `rlp:"nil"` + } + +RLP supports two kinds of empty values: empty lists and empty strings. When using the +"nil" tag, the kind of empty value allowed for a type is chosen automatically. A struct +field whose Go type is a pointer to an unsigned integer, string, boolean or byte +array/slice expects an empty RLP string. Any other pointer field type encodes/decodes as +an empty RLP list. + +The choice of null value can be made explicit with the "nilList" and "nilString" struct +tags. Using these tags encodes/decodes a Go nil pointer value as the kind of empty +RLP value defined by the tag. */ package rlp diff --git a/rlp/encode.go b/rlp/encode.go index 445b4b5b2..77b591045 100644 --- a/rlp/encode.go +++ b/rlp/encode.go @@ -49,34 +49,7 @@ type Encoder interface { // perform many small writes in some cases. Consider making w // buffered. // -// Encode uses the following type-dependent encoding rules: -// -// If the type implements the Encoder interface, Encode calls -// EncodeRLP. This is true even for nil pointers, please see the -// documentation for Encoder. -// -// To encode a pointer, the value being pointed to is encoded. For nil -// pointers, Encode will encode the zero value of the type. A nil -// pointer to a struct type always encodes as an empty RLP list. -// A nil pointer to an array encodes as an empty list (or empty string -// if the array has element type byte). -// -// Struct values are encoded as an RLP list of all their encoded -// public fields. Recursive struct types are supported. -// -// To encode slices and arrays, the elements are encoded as an RLP -// list of the value's elements. Note that arrays and slices with -// element type uint8 or byte are always encoded as an RLP string. -// -// A Go string is encoded as an RLP string. -// -// An unsigned integer value is encoded as an RLP string. Zero always -// encodes as an empty RLP string. Encode also supports *big.Int. -// -// An interface value encodes as the value contained in the interface. -// -// Boolean values are not supported, nor are signed integers, floating -// point numbers, maps, channels and functions. +// Please see package-level documentation of encoding rules. func Encode(w io.Writer, val interface{}) error { if outer, ok := w.(*encbuf); ok { // Encode was called by some type's EncodeRLP. @@ -93,7 +66,7 @@ func Encode(w io.Writer, val interface{}) error { } // EncodeToBytes returns the RLP encoding of val. -// Please see the documentation of Encode for the encoding rules. +// Please see package-level documentation for the encoding rules. func EncodeToBytes(val interface{}) ([]byte, error) { eb := encbufPool.Get().(*encbuf) defer encbufPool.Put(eb) @@ -118,13 +91,6 @@ func EncodeToReader(val interface{}) (size int, r io.Reader, err error) { return eb.size(), &encReader{buf: eb}, nil } -type encbuf struct { - str []byte // string data, contains everything except list headers - lheads []*listhead // all list headers - lhsize int // sum of sizes of all encoded list headers - sizebuf []byte // 9-byte auxiliary buffer for uint encoding -} - type listhead struct { offset int // index of this header in string data size int // total size of encoded data (including list headers) @@ -157,19 +123,26 @@ func puthead(buf []byte, smalltag, largetag byte, size uint64) int { return sizesize + 1 } +type encbuf struct { + str []byte // string data, contains everything except list headers + lheads []listhead // all list headers + lhsize int // sum of sizes of all encoded list headers + sizebuf [9]byte // auxiliary buffer for uint encoding + bufvalue reflect.Value // used in writeByteArrayCopy +} + // encbufs are pooled. var encbufPool = sync.Pool{ - New: func() interface{} { return &encbuf{sizebuf: make([]byte, 9)} }, + New: func() interface{} { + var bytes []byte + return &encbuf{bufvalue: reflect.ValueOf(&bytes).Elem()} + }, } func (w *encbuf) reset() { w.lhsize = 0 - if w.str != nil { - w.str = w.str[:0] - } - if w.lheads != nil { - w.lheads = w.lheads[:0] - } + w.str = w.str[:0] + w.lheads = w.lheads[:0] } // encbuf implements io.Writer so it can be passed it into EncodeRLP. @@ -180,18 +153,17 @@ func (w *encbuf) Write(b []byte) (int, error) { func (w *encbuf) encode(val interface{}) error { rval := reflect.ValueOf(val) - ti, err := cachedTypeInfo(rval.Type(), tags{}) + writer, err := cachedWriter(rval.Type()) if err != nil { return err } - return ti.writer(rval, w) + return writer(rval, w) } func (w *encbuf) encodeStringHeader(size int) { if size < 56 { w.str = append(w.str, 0x80+byte(size)) } else { - // TODO: encode to w.str directly sizesize := putint(w.sizebuf[1:], uint64(size)) w.sizebuf[0] = 0xB7 + byte(sizesize) w.str = append(w.str, w.sizebuf[:sizesize+1]...) @@ -208,13 +180,29 @@ func (w *encbuf) encodeString(b []byte) { } } -func (w *encbuf) list() *listhead { - lh := &listhead{offset: len(w.str), size: w.lhsize} - w.lheads = append(w.lheads, lh) - return lh +func (w *encbuf) encodeUint(i uint64) { + if i == 0 { + w.str = append(w.str, 0x80) + } else if i < 128 { + // fits single byte + w.str = append(w.str, byte(i)) + } else { + s := putint(w.sizebuf[1:], i) + w.sizebuf[0] = 0x80 + byte(s) + w.str = append(w.str, w.sizebuf[:s+1]...) + } +} + +// list adds a new list header to the header stack. It returns the index +// of the header. The caller must call listEnd with this index after encoding +// the content of the list. +func (w *encbuf) list() int { + w.lheads = append(w.lheads, listhead{offset: len(w.str), size: w.lhsize}) + return len(w.lheads) - 1 } -func (w *encbuf) listEnd(lh *listhead) { +func (w *encbuf) listEnd(index int) { + lh := &w.lheads[index] lh.size = w.size() - lh.offset - lh.size if lh.size < 56 { w.lhsize++ // length encoded into kind tag @@ -257,7 +245,7 @@ func (w *encbuf) toWriter(out io.Writer) (err error) { } } // write the header - enc := head.encode(w.sizebuf) + enc := head.encode(w.sizebuf[:]) if _, err = out.Write(enc); err != nil { return err } @@ -323,7 +311,7 @@ func (r *encReader) next() []byte { return p } r.lhpos++ - return head.encode(r.buf.sizebuf) + return head.encode(r.buf.sizebuf[:]) case r.strpos < len(r.buf.str): // String data at the end, after all list headers. @@ -336,10 +324,7 @@ func (r *encReader) next() []byte { } } -var ( - encoderInterface = reflect.TypeOf(new(Encoder)).Elem() - big0 = big.NewInt(0) -) +var encoderInterface = reflect.TypeOf(new(Encoder)).Elem() // makeWriter creates a writer function for the given type. func makeWriter(typ reflect.Type, ts tags) (writer, error) { @@ -347,16 +332,14 @@ func makeWriter(typ reflect.Type, ts tags) (writer, error) { switch { case typ == rawValueType: return writeRawValue, nil - case typ.Implements(encoderInterface): - return writeEncoder, nil - case kind != reflect.Ptr && reflect.PtrTo(typ).Implements(encoderInterface): - return writeEncoderNoPtr, nil - case kind == reflect.Interface: - return writeInterface, nil case typ.AssignableTo(reflect.PtrTo(bigInt)): return writeBigIntPtr, nil case typ.AssignableTo(bigInt): return writeBigIntNoPtr, nil + case kind == reflect.Ptr: + return makePtrWriter(typ, ts) + case reflect.PtrTo(typ).Implements(encoderInterface): + return makeEncoderWriter(typ), nil case isUint(kind): return writeUint, nil case kind == reflect.Bool: @@ -366,40 +349,25 @@ func makeWriter(typ reflect.Type, ts tags) (writer, error) { case kind == reflect.Slice && isByte(typ.Elem()): return writeBytes, nil case kind == reflect.Array && isByte(typ.Elem()): - return writeByteArray, nil + return makeByteArrayWriter(typ), nil case kind == reflect.Slice || kind == reflect.Array: return makeSliceWriter(typ, ts) case kind == reflect.Struct: return makeStructWriter(typ) - case kind == reflect.Ptr: - return makePtrWriter(typ) + case kind == reflect.Interface: + return writeInterface, nil default: return nil, fmt.Errorf("rlp: type %v is not RLP-serializable", typ) } } -func isByte(typ reflect.Type) bool { - return typ.Kind() == reflect.Uint8 && !typ.Implements(encoderInterface) -} - func writeRawValue(val reflect.Value, w *encbuf) error { w.str = append(w.str, val.Bytes()...) return nil } func writeUint(val reflect.Value, w *encbuf) error { - i := val.Uint() - if i == 0 { - w.str = append(w.str, 0x80) - } else if i < 128 { - // fits single byte - w.str = append(w.str, byte(i)) - } else { - // TODO: encode int to w.str directly - s := putint(w.sizebuf[1:], i) - w.sizebuf[0] = 0x80 + byte(s) - w.str = append(w.str, w.sizebuf[:s+1]...) - } + w.encodeUint(val.Uint()) return nil } @@ -426,13 +394,32 @@ func writeBigIntNoPtr(val reflect.Value, w *encbuf) error { return writeBigInt(&i, w) } +// wordBytes is the number of bytes in a big.Word +const wordBytes = (32 << (uint64(^big.Word(0)) >> 63)) / 8 + func writeBigInt(i *big.Int, w *encbuf) error { - if cmp := i.Cmp(big0); cmp == -1 { + if i.Sign() == -1 { return fmt.Errorf("rlp: cannot encode negative *big.Int") - } else if cmp == 0 { - w.str = append(w.str, 0x80) - } else { - w.encodeString(i.Bytes()) + } + bitlen := i.BitLen() + if bitlen <= 64 { + w.encodeUint(i.Uint64()) + return nil + } + // Integer is larger than 64 bits, encode from i.Bits(). + // The minimal byte length is bitlen rounded up to the next + // multiple of 8, divided by 8. + length := ((bitlen + 7) & -8) >> 3 + w.encodeStringHeader(length) + w.str = append(w.str, make([]byte, length)...) + index := length + buf := w.str[len(w.str)-length:] + for _, d := range i.Bits() { + for j := 0; j < wordBytes && index > 0; j++ { + index-- + buf[index] = byte(d) + d >>= 8 + } } return nil } @@ -442,7 +429,52 @@ func writeBytes(val reflect.Value, w *encbuf) error { return nil } -func writeByteArray(val reflect.Value, w *encbuf) error { +var byteType = reflect.TypeOf(byte(0)) + +func makeByteArrayWriter(typ reflect.Type) writer { + length := typ.Len() + if length == 0 { + return writeLengthZeroByteArray + } else if length == 1 { + return writeLengthOneByteArray + } + if typ.Elem() != byteType { + return writeNamedByteArray + } + return func(val reflect.Value, w *encbuf) error { + writeByteArrayCopy(length, val, w) + return nil + } +} + +func writeLengthZeroByteArray(val reflect.Value, w *encbuf) error { + w.str = append(w.str, 0x80) + return nil +} + +func writeLengthOneByteArray(val reflect.Value, w *encbuf) error { + b := byte(val.Index(0).Uint()) + if b <= 0x7f { + w.str = append(w.str, b) + } else { + w.str = append(w.str, 0x81, b) + } + return nil +} + +// writeByteArrayCopy encodes byte arrays using reflect.Copy. This is +// the fast path for [N]byte where N > 1. +func writeByteArrayCopy(length int, val reflect.Value, w *encbuf) { + w.encodeStringHeader(length) + offset := len(w.str) + w.str = append(w.str, make([]byte, length)...) + w.bufvalue.SetBytes(w.str[offset:]) + reflect.Copy(w.bufvalue, val) +} + +// writeNamedByteArray encodes byte arrays with named element type. +// This exists because reflect.Copy can't be used with such types. +func writeNamedByteArray(val reflect.Value, w *encbuf) error { if !val.CanAddr() { // Slice requires the value to be addressable. // Make it addressable by copying. @@ -468,26 +500,6 @@ func writeString(val reflect.Value, w *encbuf) error { return nil } -func writeEncoder(val reflect.Value, w *encbuf) error { - return val.Interface().(Encoder).EncodeRLP(w) -} - -// writeEncoderNoPtr handles non-pointer values that implement Encoder -// with a pointer receiver. -func writeEncoderNoPtr(val reflect.Value, w *encbuf) error { - if !val.CanAddr() { - // We can't get the address. It would be possible to make the - // value addressable by creating a shallow copy, but this - // creates other problems so we're not doing it (yet). - // - // package json simply doesn't call MarshalJSON for cases like - // this, but encodes the value as if it didn't implement the - // interface. We don't want to handle it that way. - return fmt.Errorf("rlp: game over: unadressable value of type %v, EncodeRLP is pointer method", val.Type()) - } - return val.Addr().Interface().(Encoder).EncodeRLP(w) -} - func writeInterface(val reflect.Value, w *encbuf) error { if val.IsNil() { // Write empty list. This is consistent with the previous RLP @@ -497,17 +509,17 @@ func writeInterface(val reflect.Value, w *encbuf) error { return nil } eval := val.Elem() - ti, err := cachedTypeInfo(eval.Type(), tags{}) + writer, err := cachedWriter(eval.Type()) if err != nil { return err } - return ti.writer(eval, w) + return writer(eval, w) } func makeSliceWriter(typ reflect.Type, ts tags) (writer, error) { - etypeinfo, err := cachedTypeInfo1(typ.Elem(), tags{}) - if err != nil { - return nil, err + etypeinfo := cachedTypeInfo1(typ.Elem(), tags{}) + if etypeinfo.writerErr != nil { + return nil, etypeinfo.writerErr } writer := func(val reflect.Value, w *encbuf) error { if !ts.tail { @@ -529,6 +541,11 @@ func makeStructWriter(typ reflect.Type) (writer, error) { if err != nil { return nil, err } + for _, f := range fields { + if f.info.writerErr != nil { + return nil, structFieldError{typ, f.index, f.info.writerErr} + } + } writer := func(val reflect.Value, w *encbuf) error { lh := w.list() for _, f := range fields { @@ -542,42 +559,49 @@ func makeStructWriter(typ reflect.Type) (writer, error) { return writer, nil } -func makePtrWriter(typ reflect.Type) (writer, error) { - etypeinfo, err := cachedTypeInfo1(typ.Elem(), tags{}) - if err != nil { - return nil, err +func makePtrWriter(typ reflect.Type, ts tags) (writer, error) { + etypeinfo := cachedTypeInfo1(typ.Elem(), tags{}) + if etypeinfo.writerErr != nil { + return nil, etypeinfo.writerErr } - - // determine nil pointer handler - var nilfunc func(*encbuf) error - kind := typ.Elem().Kind() - switch { - case kind == reflect.Array && isByte(typ.Elem().Elem()): - nilfunc = func(w *encbuf) error { - w.str = append(w.str, 0x80) - return nil - } - case kind == reflect.Struct || kind == reflect.Array: - nilfunc = func(w *encbuf) error { - // encoding the zero value of a struct/array could trigger - // infinite recursion, avoid that. - w.listEnd(w.list()) - return nil - } - default: - zero := reflect.Zero(typ.Elem()) - nilfunc = func(w *encbuf) error { - return etypeinfo.writer(zero, w) - } + // Determine how to encode nil pointers. + var nilKind Kind + if ts.nilOK { + nilKind = ts.nilKind // use struct tag if provided + } else { + nilKind = defaultNilKind(typ.Elem()) } writer := func(val reflect.Value, w *encbuf) error { if val.IsNil() { - return nilfunc(w) + if nilKind == String { + w.str = append(w.str, 0x80) + } else { + w.listEnd(w.list()) + } + return nil } return etypeinfo.writer(val.Elem(), w) } - return writer, err + return writer, nil +} + +func makeEncoderWriter(typ reflect.Type) writer { + if typ.Implements(encoderInterface) { + return func(val reflect.Value, w *encbuf) error { + return val.Interface().(Encoder).EncodeRLP(w) + } + } + w := func(val reflect.Value, w *encbuf) error { + if !val.CanAddr() { + // package json simply doesn't call MarshalJSON for this case, but encodes the + // value as if it didn't implement the interface. We don't want to handle it that + // way. + return fmt.Errorf("rlp: unadressable value of type %v, EncodeRLP is pointer method", val.Type()) + } + return val.Addr().Interface().(Encoder).EncodeRLP(w) + } + return w } // putint writes i to the beginning of b in big endian byte diff --git a/rlp/encode_test.go b/rlp/encode_test.go index 827960f7c..15cd2c70f 100644 --- a/rlp/encode_test.go +++ b/rlp/encode_test.go @@ -1,341 +1,466 @@ -// Copyright 2014 The go-ethereum Authors -// This file is part of the go-ethereum library. +// // Copyright 2014 The go-ethereum Authors +// // This file is part of the go-ethereum library. +// // +// // The go-ethereum library is free software: you can redistribute it and/or modify +// // it under the terms of the GNU Lesser General Public License as published by +// // the Free Software Foundation, either version 3 of the License, or +// // (at your option) any later version. +// // +// // The go-ethereum library is distributed in the hope that it will be useful, +// // but WITHOUT ANY WARRANTY; without even the implied warranty of +// // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// // GNU Lesser General Public License for more details. +// // +// // You should have received a copy of the GNU Lesser General Public License +// // along with the go-ethereum library. If not, see . // -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - package rlp -import ( - "bytes" - "errors" - "fmt" - "io" - "io/ioutil" - "math/big" - "sync" - "testing" -) - -type testEncoder struct { - err error -} - -func (e *testEncoder) EncodeRLP(w io.Writer) error { - if e == nil { - w.Write([]byte{0, 0, 0, 0}) - } else if e.err != nil { - return e.err - } else { - w.Write([]byte{0, 1, 0, 1, 0, 1, 0, 1, 0, 1}) - } - return nil -} - -type byteEncoder byte - -func (e byteEncoder) EncodeRLP(w io.Writer) error { - w.Write(EmptyList) - return nil -} - -type encodableReader struct { - A, B uint -} - -func (e *encodableReader) Read(b []byte) (int, error) { - panic("called") -} - -type namedByteType byte - -var ( - _ = Encoder(&testEncoder{}) - _ = Encoder(byteEncoder(0)) - - reader io.Reader = &encodableReader{1, 2} -) - -type encTest struct { - val interface{} - output, error string -} - -var encTests = []encTest{ - // booleans - {val: true, output: "01"}, - {val: false, output: "80"}, - - // integers - {val: uint32(0), output: "80"}, - {val: uint32(127), output: "7F"}, - {val: uint32(128), output: "8180"}, - {val: uint32(256), output: "820100"}, - {val: uint32(1024), output: "820400"}, - {val: uint32(0xFFFFFF), output: "83FFFFFF"}, - {val: uint32(0xFFFFFFFF), output: "84FFFFFFFF"}, - {val: uint64(0xFFFFFFFF), output: "84FFFFFFFF"}, - {val: uint64(0xFFFFFFFFFF), output: "85FFFFFFFFFF"}, - {val: uint64(0xFFFFFFFFFFFF), output: "86FFFFFFFFFFFF"}, - {val: uint64(0xFFFFFFFFFFFFFF), output: "87FFFFFFFFFFFFFF"}, - {val: uint64(0xFFFFFFFFFFFFFFFF), output: "88FFFFFFFFFFFFFFFF"}, - - // big integers (should match uint for small values) - {val: big.NewInt(0), output: "80"}, - {val: big.NewInt(1), output: "01"}, - {val: big.NewInt(127), output: "7F"}, - {val: big.NewInt(128), output: "8180"}, - {val: big.NewInt(256), output: "820100"}, - {val: big.NewInt(1024), output: "820400"}, - {val: big.NewInt(0xFFFFFF), output: "83FFFFFF"}, - {val: big.NewInt(0xFFFFFFFF), output: "84FFFFFFFF"}, - {val: big.NewInt(0xFFFFFFFFFF), output: "85FFFFFFFFFF"}, - {val: big.NewInt(0xFFFFFFFFFFFF), output: "86FFFFFFFFFFFF"}, - {val: big.NewInt(0xFFFFFFFFFFFFFF), output: "87FFFFFFFFFFFFFF"}, - { - val: big.NewInt(0).SetBytes(unhex("102030405060708090A0B0C0D0E0F2")), - output: "8F102030405060708090A0B0C0D0E0F2", - }, - { - val: big.NewInt(0).SetBytes(unhex("0100020003000400050006000700080009000A000B000C000D000E01")), - output: "9C0100020003000400050006000700080009000A000B000C000D000E01", - }, - { - val: big.NewInt(0).SetBytes(unhex("010000000000000000000000000000000000000000000000000000000000000000")), - output: "A1010000000000000000000000000000000000000000000000000000000000000000", - }, - - // non-pointer big.Int - {val: *big.NewInt(0), output: "80"}, - {val: *big.NewInt(0xFFFFFF), output: "83FFFFFF"}, - - // negative ints are not supported - {val: big.NewInt(-1), error: "rlp: cannot encode negative *big.Int"}, - - // byte slices, strings - {val: []byte{}, output: "80"}, - {val: []byte{0x7E}, output: "7E"}, - {val: []byte{0x7F}, output: "7F"}, - {val: []byte{0x80}, output: "8180"}, - {val: []byte{1, 2, 3}, output: "83010203"}, - - {val: []namedByteType{1, 2, 3}, output: "83010203"}, - {val: [...]namedByteType{1, 2, 3}, output: "83010203"}, - - {val: "", output: "80"}, - {val: "\x7E", output: "7E"}, - {val: "\x7F", output: "7F"}, - {val: "\x80", output: "8180"}, - {val: "dog", output: "83646F67"}, - { - val: "Lorem ipsum dolor sit amet, consectetur adipisicing eli", - output: "B74C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C69", - }, - { - val: "Lorem ipsum dolor sit amet, consectetur adipisicing elit", - output: "B8384C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C6974", - }, - { - val: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat", - output: "B904004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E73656374657475722061646970697363696E6720656C69742E20437572616269747572206D6175726973206D61676E612C20737573636970697420736564207665686963756C61206E6F6E2C20696163756C697320666175636962757320746F72746F722E2050726F696E20737573636970697420756C74726963696573206D616C6573756164612E204475697320746F72746F7220656C69742C2064696374756D2071756973207472697374697175652065752C20756C7472696365732061742072697375732E204D6F72626920612065737420696D70657264696574206D6920756C6C616D636F7270657220616C6971756574207375736369706974206E6563206C6F72656D2E2041656E65616E2071756973206C656F206D6F6C6C69732C2076756C70757461746520656C6974207661726975732C20636F6E73657175617420656E696D2E204E756C6C6120756C74726963657320747572706973206A7573746F2C20657420706F73756572652075726E6120636F6E7365637465747572206E65632E2050726F696E206E6F6E20636F6E76616C6C6973206D657475732E20446F6E65632074656D706F7220697073756D20696E206D617572697320636F6E67756520736F6C6C696369747564696E2E20566573746962756C756D20616E746520697073756D207072696D697320696E206661756369627573206F726369206C756374757320657420756C74726963657320706F737565726520637562696C69612043757261653B2053757370656E646973736520636F6E76616C6C69732073656D2076656C206D617373612066617563696275732C2065676574206C6163696E6961206C616375732074656D706F722E204E756C6C61207175697320756C747269636965732070757275732E2050726F696E20617563746F722072686F6E637573206E69626820636F6E64696D656E74756D206D6F6C6C69732E20416C697175616D20636F6E73657175617420656E696D206174206D65747573206C75637475732C206120656C656966656E6420707572757320656765737461732E20437572616269747572206174206E696268206D657475732E204E616D20626962656E64756D2C206E6571756520617420617563746F72207472697374697175652C206C6F72656D206C696265726F20616C697175657420617263752C206E6F6E20696E74657264756D2074656C6C7573206C65637475732073697420616D65742065726F732E20437261732072686F6E6375732C206D65747573206163206F726E617265206375727375732C20646F6C6F72206A7573746F20756C747269636573206D657475732C20617420756C6C616D636F7270657220766F6C7574706174", - }, - - // slices - {val: []uint{}, output: "C0"}, - {val: []uint{1, 2, 3}, output: "C3010203"}, - { - // [ [], [[]], [ [], [[]] ] ] - val: []interface{}{[]interface{}{}, [][]interface{}{{}}, []interface{}{[]interface{}{}, [][]interface{}{{}}}}, - output: "C7C0C1C0C3C0C1C0", - }, - { - val: []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo"}, - output: "F83C836161618362626283636363836464648365656583666666836767678368686883696969836A6A6A836B6B6B836C6C6C836D6D6D836E6E6E836F6F6F", - }, - { - val: []interface{}{uint(1), uint(0xFFFFFF), []interface{}{[]uint{4, 5, 5}}, "abc"}, - output: "CE0183FFFFFFC4C304050583616263", - }, - { - val: [][]string{ - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - {"asdf", "qwer", "zxcv"}, - }, - output: "F90200CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376", - }, - - // RawValue - {val: RawValue(unhex("01")), output: "01"}, - {val: RawValue(unhex("82FFFF")), output: "82FFFF"}, - {val: []RawValue{unhex("01"), unhex("02")}, output: "C20102"}, - - // structs - {val: simplestruct{}, output: "C28080"}, - {val: simplestruct{A: 3, B: "foo"}, output: "C50383666F6F"}, - {val: &recstruct{5, nil}, output: "C205C0"}, - {val: &recstruct{5, &recstruct{4, &recstruct{3, nil}}}, output: "C605C404C203C0"}, - {val: &tailRaw{A: 1, Tail: []RawValue{unhex("02"), unhex("03")}}, output: "C3010203"}, - {val: &tailRaw{A: 1, Tail: []RawValue{unhex("02")}}, output: "C20102"}, - {val: &tailRaw{A: 1, Tail: []RawValue{}}, output: "C101"}, - {val: &tailRaw{A: 1, Tail: nil}, output: "C101"}, - {val: &hasIgnoredField{A: 1, B: 2, C: 3}, output: "C20103"}, - - // nil - {val: (*uint)(nil), output: "80"}, - {val: (*string)(nil), output: "80"}, - {val: (*[]byte)(nil), output: "80"}, - {val: (*[10]byte)(nil), output: "80"}, - {val: (*big.Int)(nil), output: "80"}, - {val: (*[]string)(nil), output: "C0"}, - {val: (*[10]string)(nil), output: "C0"}, - {val: (*[]interface{})(nil), output: "C0"}, - {val: (*[]struct{ uint })(nil), output: "C0"}, - {val: (*interface{})(nil), output: "C0"}, - - // interfaces - {val: []io.Reader{reader}, output: "C3C20102"}, // the contained value is a struct - - // Encoder - {val: (*testEncoder)(nil), output: "00000000"}, - {val: &testEncoder{}, output: "00010001000100010001"}, - {val: &testEncoder{errors.New("test error")}, error: "test error"}, - // verify that pointer method testEncoder.EncodeRLP is called for - // addressable non-pointer values. - {val: &struct{ TE testEncoder }{testEncoder{}}, output: "CA00010001000100010001"}, - {val: &struct{ TE testEncoder }{testEncoder{errors.New("test error")}}, error: "test error"}, - // verify the error for non-addressable non-pointer Encoder - {val: testEncoder{}, error: "rlp: game over: unadressable value of type rlp.testEncoder, EncodeRLP is pointer method"}, - // verify the special case for []byte - {val: []byteEncoder{0, 1, 2, 3, 4}, output: "C5C0C0C0C0C0"}, -} - -func runEncTests(t *testing.T, f func(val interface{}) ([]byte, error)) { - for i, test := range encTests { - output, err := f(test.val) - if err != nil && test.error == "" { - t.Errorf("test %d: unexpected error: %v\nvalue %#v\ntype %T", - i, err, test.val, test.val) - continue - } - if test.error != "" && fmt.Sprint(err) != test.error { - t.Errorf("test %d: error mismatch\ngot %v\nwant %v\nvalue %#v\ntype %T", - i, err, test.error, test.val, test.val) - continue - } - if err == nil && !bytes.Equal(output, unhex(test.output)) { - t.Errorf("test %d: output mismatch:\ngot %X\nwant %s\nvalue %#v\ntype %T", - i, output, test.output, test.val, test.val) - } - } -} - -func TestEncode(t *testing.T) { - runEncTests(t, func(val interface{}) ([]byte, error) { - b := new(bytes.Buffer) - err := Encode(b, val) - return b.Bytes(), err - }) -} - -func TestEncodeToBytes(t *testing.T) { - runEncTests(t, EncodeToBytes) -} - -func TestEncodeToReader(t *testing.T) { - runEncTests(t, func(val interface{}) ([]byte, error) { - _, r, err := EncodeToReader(val) - if err != nil { - return nil, err - } - return ioutil.ReadAll(r) - }) -} - -func TestEncodeToReaderPiecewise(t *testing.T) { - runEncTests(t, func(val interface{}) ([]byte, error) { - size, r, err := EncodeToReader(val) - if err != nil { - return nil, err - } - - // read output piecewise - output := make([]byte, size) - for start, end := 0, 0; start < size; start = end { - if remaining := size - start; remaining < 3 { - end += remaining - } else { - end = start + 3 - } - n, err := r.Read(output[start:end]) - end = start + n - if err == io.EOF { - break - } else if err != nil { - return nil, err - } - } - return output, nil - }) -} - -// This is a regression test verifying that encReader -// returns its encbuf to the pool only once. -func TestEncodeToReaderReturnToPool(t *testing.T) { - buf := make([]byte, 50) - wg := new(sync.WaitGroup) - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - for i := 0; i < 1000; i++ { - _, r, _ := EncodeToReader("foo") - ioutil.ReadAll(r) - r.Read(buf) - r.Read(buf) - r.Read(buf) - r.Read(buf) - } - wg.Done() - }() - } - wg.Wait() -} +// +// import ( +// "bytes" +// "errors" +// "fmt" +// "io" +// "io/ioutil" +// "math/big" +// "sync" +// "testing" +// +// "github.com/ethereum/go-ethereum/common/math" +// ) +// +// type testEncoder struct { +// err error +// } +// +// func (e *testEncoder) EncodeRLP(w io.Writer) error { +// if e == nil { +// panic("EncodeRLP called on nil value") +// } +// if e.err != nil { +// return e.err +// } else { +// w.Write([]byte{0, 1, 0, 1, 0, 1, 0, 1, 0, 1}) +// } +// return nil +// } +// +// type testEncoderValueMethod struct{} +// +// func (e testEncoderValueMethod) EncodeRLP(w io.Writer) error { +// w.Write([]byte{0xFA, 0xFE, 0xF0}) +// return nil +// } +// +// type byteEncoder byte +// +// func (e byteEncoder) EncodeRLP(w io.Writer) error { +// w.Write(EmptyList) +// return nil +// } +// +// type undecodableEncoder func() +// +// func (f undecodableEncoder) EncodeRLP(w io.Writer) error { +// w.Write([]byte{0xF5, 0xF5, 0xF5}) +// return nil +// } +// +// type encodableReader struct { +// A, B uint +// } +// +// func (e *encodableReader) Read(b []byte) (int, error) { +// panic("called") +// } +// +// type namedByteType byte +// +// var ( +// _ = Encoder(&testEncoder{}) +// _ = Encoder(byteEncoder(0)) +// +// reader io.Reader = &encodableReader{1, 2} +// ) +// +// type encTest struct { +// val interface{} +// output, error string +// } +// +// var encTests = []encTest{ +// // booleans +// {val: true, output: "01"}, +// {val: false, output: "80"}, +// +// // integers +// {val: uint32(0), output: "80"}, +// {val: uint32(127), output: "7F"}, +// {val: uint32(128), output: "8180"}, +// {val: uint32(256), output: "820100"}, +// {val: uint32(1024), output: "820400"}, +// {val: uint32(0xFFFFFF), output: "83FFFFFF"}, +// {val: uint32(0xFFFFFFFF), output: "84FFFFFFFF"}, +// {val: uint64(0xFFFFFFFF), output: "84FFFFFFFF"}, +// {val: uint64(0xFFFFFFFFFF), output: "85FFFFFFFFFF"}, +// {val: uint64(0xFFFFFFFFFFFF), output: "86FFFFFFFFFFFF"}, +// {val: uint64(0xFFFFFFFFFFFFFF), output: "87FFFFFFFFFFFFFF"}, +// {val: uint64(0xFFFFFFFFFFFFFFFF), output: "88FFFFFFFFFFFFFFFF"}, +// +// // big integers (should match uint for small values) +// {val: big.NewInt(0), output: "80"}, +// {val: big.NewInt(1), output: "01"}, +// {val: big.NewInt(127), output: "7F"}, +// {val: big.NewInt(128), output: "8180"}, +// {val: big.NewInt(256), output: "820100"}, +// {val: big.NewInt(1024), output: "820400"}, +// {val: big.NewInt(0xFFFFFF), output: "83FFFFFF"}, +// {val: big.NewInt(0xFFFFFFFF), output: "84FFFFFFFF"}, +// {val: big.NewInt(0xFFFFFFFFFF), output: "85FFFFFFFFFF"}, +// {val: big.NewInt(0xFFFFFFFFFFFF), output: "86FFFFFFFFFFFF"}, +// {val: big.NewInt(0xFFFFFFFFFFFFFF), output: "87FFFFFFFFFFFFFF"}, +// { +// val: big.NewInt(0).SetBytes(unhex("102030405060708090A0B0C0D0E0F2")), +// output: "8F102030405060708090A0B0C0D0E0F2", +// }, +// { +// val: big.NewInt(0).SetBytes(unhex("0100020003000400050006000700080009000A000B000C000D000E01")), +// output: "9C0100020003000400050006000700080009000A000B000C000D000E01", +// }, +// { +// val: big.NewInt(0).SetBytes(unhex("010000000000000000000000000000000000000000000000000000000000000000")), +// output: "A1010000000000000000000000000000000000000000000000000000000000000000", +// }, +// +// // non-pointer big.Int +// {val: *big.NewInt(0), output: "80"}, +// {val: *big.NewInt(0xFFFFFF), output: "83FFFFFF"}, +// +// // negative ints are not supported +// {val: big.NewInt(-1), error: "rlp: cannot encode negative *big.Int"}, +// +// // byte arrays +// {val: [0]byte{}, output: "80"}, +// {val: [1]byte{0}, output: "00"}, +// {val: [1]byte{1}, output: "01"}, +// {val: [1]byte{0x7F}, output: "7F"}, +// {val: [1]byte{0x80}, output: "8180"}, +// {val: [1]byte{0xFF}, output: "81FF"}, +// {val: [3]byte{1, 2, 3}, output: "83010203"}, +// {val: [57]byte{1, 2, 3}, output: "B839010203000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, +// +// // named byte type arrays +// {val: [0]namedByteType{}, output: "80"}, +// {val: [1]namedByteType{0}, output: "00"}, +// {val: [1]namedByteType{1}, output: "01"}, +// {val: [1]namedByteType{0x7F}, output: "7F"}, +// {val: [1]namedByteType{0x80}, output: "8180"}, +// {val: [1]namedByteType{0xFF}, output: "81FF"}, +// {val: [3]namedByteType{1, 2, 3}, output: "83010203"}, +// {val: [57]namedByteType{1, 2, 3}, output: "B839010203000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}, +// +// // byte slices +// {val: []byte{}, output: "80"}, +// {val: []byte{0}, output: "00"}, +// {val: []byte{0x7E}, output: "7E"}, +// {val: []byte{0x7F}, output: "7F"}, +// {val: []byte{0x80}, output: "8180"}, +// {val: []byte{1, 2, 3}, output: "83010203"}, +// +// // named byte type slices +// {val: []namedByteType{}, output: "80"}, +// {val: []namedByteType{0}, output: "00"}, +// {val: []namedByteType{0x7E}, output: "7E"}, +// {val: []namedByteType{0x7F}, output: "7F"}, +// {val: []namedByteType{0x80}, output: "8180"}, +// {val: []namedByteType{1, 2, 3}, output: "83010203"}, +// +// // strings +// {val: "", output: "80"}, +// {val: "\x7E", output: "7E"}, +// {val: "\x7F", output: "7F"}, +// {val: "\x80", output: "8180"}, +// {val: "dog", output: "83646F67"}, +// { +// val: "Lorem ipsum dolor sit amet, consectetur adipisicing eli", +// output: "B74C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C69", +// }, +// { +// val: "Lorem ipsum dolor sit amet, consectetur adipisicing elit", +// output: "B8384C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E7365637465747572206164697069736963696E6720656C6974", +// }, +// { +// val: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat", +// output: "B904004C6F72656D20697073756D20646F6C6F722073697420616D65742C20636F6E73656374657475722061646970697363696E6720656C69742E20437572616269747572206D6175726973206D61676E612C20737573636970697420736564207665686963756C61206E6F6E2C20696163756C697320666175636962757320746F72746F722E2050726F696E20737573636970697420756C74726963696573206D616C6573756164612E204475697320746F72746F7220656C69742C2064696374756D2071756973207472697374697175652065752C20756C7472696365732061742072697375732E204D6F72626920612065737420696D70657264696574206D6920756C6C616D636F7270657220616C6971756574207375736369706974206E6563206C6F72656D2E2041656E65616E2071756973206C656F206D6F6C6C69732C2076756C70757461746520656C6974207661726975732C20636F6E73657175617420656E696D2E204E756C6C6120756C74726963657320747572706973206A7573746F2C20657420706F73756572652075726E6120636F6E7365637465747572206E65632E2050726F696E206E6F6E20636F6E76616C6C6973206D657475732E20446F6E65632074656D706F7220697073756D20696E206D617572697320636F6E67756520736F6C6C696369747564696E2E20566573746962756C756D20616E746520697073756D207072696D697320696E206661756369627573206F726369206C756374757320657420756C74726963657320706F737565726520637562696C69612043757261653B2053757370656E646973736520636F6E76616C6C69732073656D2076656C206D617373612066617563696275732C2065676574206C6163696E6961206C616375732074656D706F722E204E756C6C61207175697320756C747269636965732070757275732E2050726F696E20617563746F722072686F6E637573206E69626820636F6E64696D656E74756D206D6F6C6C69732E20416C697175616D20636F6E73657175617420656E696D206174206D65747573206C75637475732C206120656C656966656E6420707572757320656765737461732E20437572616269747572206174206E696268206D657475732E204E616D20626962656E64756D2C206E6571756520617420617563746F72207472697374697175652C206C6F72656D206C696265726F20616C697175657420617263752C206E6F6E20696E74657264756D2074656C6C7573206C65637475732073697420616D65742065726F732E20437261732072686F6E6375732C206D65747573206163206F726E617265206375727375732C20646F6C6F72206A7573746F20756C747269636573206D657475732C20617420756C6C616D636F7270657220766F6C7574706174", +// }, +// +// // slices +// {val: []uint{}, output: "C0"}, +// {val: []uint{1, 2, 3}, output: "C3010203"}, +// { +// // [ [], [[]], [ [], [[]] ] ] +// val: []interface{}{[]interface{}{}, [][]interface{}{{}}, []interface{}{[]interface{}{}, [][]interface{}{{}}}}, +// output: "C7C0C1C0C3C0C1C0", +// }, +// { +// val: []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj", "kkk", "lll", "mmm", "nnn", "ooo"}, +// output: "F83C836161618362626283636363836464648365656583666666836767678368686883696969836A6A6A836B6B6B836C6C6C836D6D6D836E6E6E836F6F6F", +// }, +// { +// val: []interface{}{uint(1), uint(0xFFFFFF), []interface{}{[]uint{4, 5, 5}}, "abc"}, +// output: "CE0183FFFFFFC4C304050583616263", +// }, +// { +// val: [][]string{ +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// {"asdf", "qwer", "zxcv"}, +// }, +// output: "F90200CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376CF84617364668471776572847A786376", +// }, +// +// // RawValue +// {val: RawValue(unhex("01")), output: "01"}, +// {val: RawValue(unhex("82FFFF")), output: "82FFFF"}, +// {val: []RawValue{unhex("01"), unhex("02")}, output: "C20102"}, +// +// // structs +// {val: simplestruct{}, output: "C28080"}, +// {val: simplestruct{A: 3, B: "foo"}, output: "C50383666F6F"}, +// {val: &recstruct{5, nil}, output: "C205C0"}, +// {val: &recstruct{5, &recstruct{4, &recstruct{3, nil}}}, output: "C605C404C203C0"}, +// {val: &tailRaw{A: 1, Tail: []RawValue{unhex("02"), unhex("03")}}, output: "C3010203"}, +// {val: &tailRaw{A: 1, Tail: []RawValue{unhex("02")}}, output: "C20102"}, +// {val: &tailRaw{A: 1, Tail: []RawValue{}}, output: "C101"}, +// {val: &tailRaw{A: 1, Tail: nil}, output: "C101"}, +// {val: &hasIgnoredField{A: 1, B: 2, C: 3}, output: "C20103"}, +// {val: &intField{X: 3}, error: "rlp: type int is not RLP-serializable (struct field rlp.intField.X)"}, +// +// // nil +// {val: (*uint)(nil), output: "80"}, +// {val: (*string)(nil), output: "80"}, +// {val: (*[]byte)(nil), output: "80"}, +// {val: (*[10]byte)(nil), output: "80"}, +// {val: (*big.Int)(nil), output: "80"}, +// {val: (*[]string)(nil), output: "C0"}, +// {val: (*[10]string)(nil), output: "C0"}, +// {val: (*[]interface{})(nil), output: "C0"}, +// {val: (*[]struct{ uint })(nil), output: "C0"}, +// {val: (*interface{})(nil), output: "C0"}, +// +// // nil struct fields +// { +// val: struct { +// X *[]byte +// }{}, +// output: "C180", +// }, +// { +// val: struct { +// X *[2]byte +// }{}, +// output: "C180", +// }, +// { +// val: struct { +// X *uint64 +// }{}, +// output: "C180", +// }, +// { +// val: struct { +// X *uint64 `rlp:"nilList"` +// }{}, +// output: "C1C0", +// }, +// { +// val: struct { +// X *[]uint64 +// }{}, +// output: "C1C0", +// }, +// { +// val: struct { +// X *[]uint64 `rlp:"nilString"` +// }{}, +// output: "C180", +// }, +// +// // interfaces +// {val: []io.Reader{reader}, output: "C3C20102"}, // the contained value is a struct +// +// // Encoder +// {val: (*testEncoder)(nil), output: "C0"}, +// {val: &testEncoder{}, output: "00010001000100010001"}, +// {val: &testEncoder{errors.New("test error")}, error: "test error"}, +// {val: struct{ E testEncoderValueMethod }{}, output: "C3FAFEF0"}, +// {val: struct{ E *testEncoderValueMethod }{}, output: "C1C0"}, +// +// // Verify that the Encoder interface works for unsupported types like func(). +// {val: undecodableEncoder(func() {}), output: "F5F5F5"}, +// +// // Verify that pointer method testEncoder.EncodeRLP is called for +// // addressable non-pointer values. +// {val: &struct{ TE testEncoder }{testEncoder{}}, output: "CA00010001000100010001"}, +// {val: &struct{ TE testEncoder }{testEncoder{errors.New("test error")}}, error: "test error"}, +// +// // Verify the error for non-addressable non-pointer Encoder. +// {val: testEncoder{}, error: "rlp: unadressable value of type rlp.testEncoder, EncodeRLP is pointer method"}, +// +// // Verify Encoder takes precedence over []byte. +// {val: []byteEncoder{0, 1, 2, 3, 4}, output: "C5C0C0C0C0C0"}, +// } +// +// func runEncTests(t *testing.T, f func(val interface{}) ([]byte, error)) { +// for i, test := range encTests { +// output, err := f(test.val) +// if err != nil && test.error == "" { +// t.Errorf("test %d: unexpected error: %v\nvalue %#v\ntype %T", +// i, err, test.val, test.val) +// continue +// } +// if test.error != "" && fmt.Sprint(err) != test.error { +// t.Errorf("test %d: error mismatch\ngot %v\nwant %v\nvalue %#v\ntype %T", +// i, err, test.error, test.val, test.val) +// continue +// } +// if err == nil && !bytes.Equal(output, unhex(test.output)) { +// t.Errorf("test %d: output mismatch:\ngot %X\nwant %s\nvalue %#v\ntype %T", +// i, output, test.output, test.val, test.val) +// } +// } +// } +// +// func TestEncode(t *testing.T) { +// runEncTests(t, func(val interface{}) ([]byte, error) { +// b := new(bytes.Buffer) +// err := Encode(b, val) +// return b.Bytes(), err +// }) +// } +// +// func TestEncodeToBytes(t *testing.T) { +// runEncTests(t, EncodeToBytes) +// } +// +// func TestEncodeToReader(t *testing.T) { +// runEncTests(t, func(val interface{}) ([]byte, error) { +// _, r, err := EncodeToReader(val) +// if err != nil { +// return nil, err +// } +// return ioutil.ReadAll(r) +// }) +// } +// +// func TestEncodeToReaderPiecewise(t *testing.T) { +// runEncTests(t, func(val interface{}) ([]byte, error) { +// size, r, err := EncodeToReader(val) +// if err != nil { +// return nil, err +// } +// +// // read output piecewise +// output := make([]byte, size) +// for start, end := 0, 0; start < size; start = end { +// if remaining := size - start; remaining < 3 { +// end += remaining +// } else { +// end = start + 3 +// } +// n, err := r.Read(output[start:end]) +// end = start + n +// if err == io.EOF { +// break +// } else if err != nil { +// return nil, err +// } +// } +// return output, nil +// }) +// } +// +// // This is a regression test verifying that encReader +// // returns its encbuf to the pool only once. +// func TestEncodeToReaderReturnToPool(t *testing.T) { +// buf := make([]byte, 50) +// wg := new(sync.WaitGroup) +// for i := 0; i < 5; i++ { +// wg.Add(1) +// go func() { +// for i := 0; i < 1000; i++ { +// _, r, _ := EncodeToReader("foo") +// ioutil.ReadAll(r) +// r.Read(buf) +// r.Read(buf) +// r.Read(buf) +// r.Read(buf) +// } +// wg.Done() +// }() +// } +// wg.Wait() +// } +// +// var sink interface{} +// +// func BenchmarkIntsize(b *testing.B) { +// for i := 0; i < b.N; i++ { +// sink = intsize(0x12345678) +// } +// } +// +// func BenchmarkPutint(b *testing.B) { +// buf := make([]byte, 8) +// for i := 0; i < b.N; i++ { +// putint(buf, 0x12345678) +// sink = buf +// } +// } +// +// func BenchmarkEncodeBigInts(b *testing.B) { +// ints := make([]*big.Int, 200) +// for i := range ints { +// ints[i] = math.BigPow(2, int64(i)) +// } +// out := bytes.NewBuffer(make([]byte, 0, 4096)) +// b.ResetTimer() +// b.ReportAllocs() +// +// for i := 0; i < b.N; i++ { +// out.Reset() +// if err := Encode(out, ints); err != nil { +// b.Fatal(err) +// } +// } +// } diff --git a/rlp/encoder_example_test.go b/rlp/encoder_example_test.go index 1cffa241c..42c1c5c89 100644 --- a/rlp/encoder_example_test.go +++ b/rlp/encoder_example_test.go @@ -28,15 +28,7 @@ type MyCoolType struct { // EncodeRLP writes x as RLP list [a, b] that omits the Name field. func (x *MyCoolType) EncodeRLP(w io.Writer) (err error) { - // Note: the receiver can be a nil pointer. This allows you to - // control the encoding of nil, but it also means that you have to - // check for a nil receiver. - if x == nil { - err = Encode(w, []uint{0, 0}) - } else { - err = Encode(w, []uint{x.a, x.b}) - } - return err + return Encode(w, []uint{x.a, x.b}) } func ExampleEncoder() { @@ -49,6 +41,6 @@ func ExampleEncoder() { fmt.Printf("%v → %X\n", t, bytes) // Output: - // → C28080 + // → C0 // &{foobar 5 6} → C20506 } diff --git a/rlp/iterator.go b/rlp/iterator.go new file mode 100644 index 000000000..c28866dbc --- /dev/null +++ b/rlp/iterator.go @@ -0,0 +1,60 @@ +// Copyright 2019 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package rlp + +type listIterator struct { + data []byte + next []byte + err error +} + +// NewListIterator creates an iterator for the (list) represented by data +func NewListIterator(data RawValue) (*listIterator, error) { + k, t, c, err := readKind(data) + if err != nil { + return nil, err + } + if k != List { + return nil, ErrExpectedList + } + it := &listIterator{ + data: data[t : t+c], + } + return it, nil + +} + +// Next forwards the iterator one step, returns true if it was not at end yet +func (it *listIterator) Next() bool { + if len(it.data) == 0 { + return false + } + _, t, c, err := readKind(it.data) + it.next = it.data[:t+c] + it.data = it.data[t+c:] + it.err = err + return true +} + +// Value returns the current value +func (it *listIterator) Value() []byte { + return it.next +} + +func (it *listIterator) Err() error { + return it.err +} diff --git a/rlp/iterator_test.go b/rlp/iterator_test.go new file mode 100644 index 000000000..97fac7e27 --- /dev/null +++ b/rlp/iterator_test.go @@ -0,0 +1,60 @@ +// // Copyright 2019 The go-ethereum Authors +// // This file is part of the go-ethereum library. +// // +// // The go-ethereum library is free software: you can redistribute it and/or modify +// // it under the terms of the GNU Lesser General Public License as published by +// // the Free Software Foundation, either version 3 of the License, or +// // (at your option) any later version. +// // +// // The go-ethereum library is distributed in the hope that it will be useful, +// // but WITHOUT ANY WARRANTY; without even the implied warranty of +// // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// // GNU Lesser General Public License for more details. +// // +// // You should have received a copy of the GNU Lesser General Public License +// // along with the go-ethereum library. If not, see . +// +package rlp + +// +// import ( +// "testing" +// +// "github.com/ethereum/go-ethereum/common/hexutil" +// ) +// +// // TestIterator tests some basic things about the ListIterator. A more +// // comprehensive test can be found in core/rlp_test.go, where we can +// // use both types and rlp without dependency cycles +// func TestIterator(t *testing.T) { +// bodyRlpHex := "0xf902cbf8d6f869800182c35094000000000000000000000000000000000000aaaa808a000000000000000000001ba01025c66fad28b4ce3370222624d952c35529e602af7cbe04f667371f61b0e3b3a00ab8813514d1217059748fd903288ace1b4001a4bc5fbde2790debdc8167de2ff869010182c35094000000000000000000000000000000000000aaaa808a000000000000000000001ca05ac4cf1d19be06f3742c21df6c49a7e929ceb3dbaf6a09f3cfb56ff6828bd9a7a06875970133a35e63ac06d360aa166d228cc013e9b96e0a2cae7f55b22e1ee2e8f901f0f901eda0c75448377c0e426b8017b23c5f77379ecf69abc1d5c224284ad3ba1c46c59adaa00000000000000000000000000000000000000000000000000000000000000000940000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808080808080a00000000000000000000000000000000000000000000000000000000000000000880000000000000000" +// bodyRlp := hexutil.MustDecode(bodyRlpHex) +// +// it, err := NewListIterator(bodyRlp) +// if err != nil { +// t.Fatal(err) +// } +// // Check that txs exist +// if !it.Next() { +// t.Fatal("expected two elems, got zero") +// } +// txs := it.Value() +// // Check that uncles exist +// if !it.Next() { +// t.Fatal("expected two elems, got one") +// } +// txit, err := NewListIterator(txs) +// if err != nil { +// t.Fatal(err) +// } +// var i = 0 +// for txit.Next() { +// if txit.err != nil { +// t.Fatal(txit.err) +// } +// i++ +// } +// if exp := 2; i != exp { +// t.Errorf("count wrong, expected %d got %d", i, exp) +// } +// } diff --git a/rlp/raw.go b/rlp/raw.go index 2b3f328f6..c2a8517f6 100644 --- a/rlp/raw.go +++ b/rlp/raw.go @@ -57,6 +57,32 @@ func SplitString(b []byte) (content, rest []byte, err error) { return content, rest, nil } +// SplitUint64 decodes an integer at the beginning of b. +// It also returns the remaining data after the integer in 'rest'. +func SplitUint64(b []byte) (x uint64, rest []byte, err error) { + content, rest, err := SplitString(b) + if err != nil { + return 0, b, err + } + switch { + case len(content) == 0: + return 0, rest, nil + case len(content) == 1: + if content[0] == 0 { + return 0, b, ErrCanonInt + } + return uint64(content[0]), rest, nil + case len(content) > 8: + return 0, b, errUintOverflow + default: + x, err = readSize(content, byte(len(content))) + if err != nil { + return 0, b, ErrCanonInt + } + return x, rest, nil + } +} + // SplitList splits b into the content of a list and any remaining // bytes after the list. func SplitList(b []byte) (content, rest []byte, err error) { diff --git a/rlp/raw_test.go b/rlp/raw_test.go index 2aad04210..cdae4ff08 100644 --- a/rlp/raw_test.go +++ b/rlp/raw_test.go @@ -71,6 +71,49 @@ func TestSplitTypes(t *testing.T) { } } +func TestSplitUint64(t *testing.T) { + tests := []struct { + input string + val uint64 + rest string + err error + }{ + {"01", 1, "", nil}, + {"7FFF", 0x7F, "FF", nil}, + {"80FF", 0, "FF", nil}, + {"81FAFF", 0xFA, "FF", nil}, + {"82FAFAFF", 0xFAFA, "FF", nil}, + {"83FAFAFAFF", 0xFAFAFA, "FF", nil}, + {"84FAFAFAFAFF", 0xFAFAFAFA, "FF", nil}, + {"85FAFAFAFAFAFF", 0xFAFAFAFAFA, "FF", nil}, + {"86FAFAFAFAFAFAFF", 0xFAFAFAFAFAFA, "FF", nil}, + {"87FAFAFAFAFAFAFAFF", 0xFAFAFAFAFAFAFA, "FF", nil}, + {"88FAFAFAFAFAFAFAFAFF", 0xFAFAFAFAFAFAFAFA, "FF", nil}, + + // errors + {"", 0, "", io.ErrUnexpectedEOF}, + {"00", 0, "00", ErrCanonInt}, + {"81", 0, "81", ErrValueTooLarge}, + {"8100", 0, "8100", ErrCanonSize}, + {"8200FF", 0, "8200FF", ErrCanonInt}, + {"8103FF", 0, "8103FF", ErrCanonSize}, + {"89FAFAFAFAFAFAFAFAFAFF", 0, "89FAFAFAFAFAFAFAFAFAFF", errUintOverflow}, + } + + for i, test := range tests { + val, rest, err := SplitUint64(unhex(test.input)) + if val != test.val { + t.Errorf("test %d: val mismatch: got %x, want %x (input %q)", i, val, test.val, test.input) + } + if !bytes.Equal(rest, unhex(test.rest)) { + t.Errorf("test %d: rest mismatch: got %x, want %s (input %q)", i, rest, test.rest, test.input) + } + if err != test.err { + t.Errorf("test %d: error mismatch: got %q, want %q", i, err, test.err) + } + } +} + func TestSplit(t *testing.T) { tests := []struct { input string @@ -78,7 +121,9 @@ func TestSplit(t *testing.T) { val, rest string err error }{ + {input: "00FFFF", kind: Byte, val: "00", rest: "FFFF"}, {input: "01FFFF", kind: Byte, val: "01", rest: "FFFF"}, + {input: "7FFFFF", kind: Byte, val: "7F", rest: "FFFF"}, {input: "80FFFF", kind: String, val: "", rest: "FFFF"}, {input: "C3010203", kind: List, val: "010203"}, diff --git a/rlp/typecache.go b/rlp/typecache.go index 3df799e1e..6026e1a64 100644 --- a/rlp/typecache.go +++ b/rlp/typecache.go @@ -29,26 +29,34 @@ var ( ) type typeinfo struct { - decoder - writer + decoder decoder + decoderErr error // error from makeDecoder + writer writer + writerErr error // error from makeWriter } -// represents struct tags +// tags represents struct tags. type tags struct { // rlp:"nil" controls whether empty input results in a nil pointer. nilOK bool + + // This controls whether nil pointers are encoded/decoded as empty strings + // or empty lists. + nilKind Kind + // rlp:"tail" controls whether this field swallows additional list // elements. It can only be set for the last field, which must be // of slice type. tail bool + // rlp:"-" ignores fields. ignored bool } +// typekey is the key of a type in typeCache. It includes the struct tags because +// they might generate a different decoder. type typekey struct { reflect.Type - // the key must include the struct tags because they - // might generate a different decoder. tags } @@ -56,12 +64,22 @@ type decoder func(*Stream, reflect.Value) error type writer func(reflect.Value, *encbuf) error -func cachedTypeInfo(typ reflect.Type, tags tags) (*typeinfo, error) { +func cachedDecoder(typ reflect.Type) (decoder, error) { + info := cachedTypeInfo(typ, tags{}) + return info.decoder, info.decoderErr +} + +func cachedWriter(typ reflect.Type) (writer, error) { + info := cachedTypeInfo(typ, tags{}) + return info.writer, info.writerErr +} + +func cachedTypeInfo(typ reflect.Type, tags tags) *typeinfo { typeCacheMutex.RLock() info := typeCache[typekey{typ, tags}] typeCacheMutex.RUnlock() if info != nil { - return info, nil + return info } // not in the cache, need to generate info for this type. typeCacheMutex.Lock() @@ -69,25 +87,20 @@ func cachedTypeInfo(typ reflect.Type, tags tags) (*typeinfo, error) { return cachedTypeInfo1(typ, tags) } -func cachedTypeInfo1(typ reflect.Type, tags tags) (*typeinfo, error) { +func cachedTypeInfo1(typ reflect.Type, tags tags) *typeinfo { key := typekey{typ, tags} info := typeCache[key] if info != nil { // another goroutine got the write lock first - return info, nil + return info } - // put a dummmy value into the cache before generating. + // put a dummy value into the cache before generating. // if the generator tries to lookup itself, it will get // the dummy value and won't call itself recursively. - typeCache[key] = new(typeinfo) - info, err := genTypeInfo(typ, tags) - if err != nil { - // remove the dummy value if the generator fails - delete(typeCache, key) - return nil, err - } - *typeCache[key] = *info - return typeCache[key], err + info = new(typeinfo) + typeCache[key] = info + info.generate(typ, tags) + return info } type field struct { @@ -96,26 +109,43 @@ type field struct { } func structFields(typ reflect.Type) (fields []field, err error) { + lastPublic := lastPublicField(typ) for i := 0; i < typ.NumField(); i++ { if f := typ.Field(i); f.PkgPath == "" { // exported - tags, err := parseStructTag(typ, i) + tags, err := parseStructTag(typ, i, lastPublic) if err != nil { return nil, err } if tags.ignored { continue } - info, err := cachedTypeInfo1(f.Type, tags) - if err != nil { - return nil, err - } + info := cachedTypeInfo1(f.Type, tags) fields = append(fields, field{i, info}) } } return fields, nil } -func parseStructTag(typ reflect.Type, fi int) (tags, error) { +type structFieldError struct { + typ reflect.Type + field int + err error +} + +func (e structFieldError) Error() string { + return fmt.Sprintf("%v (struct field %v.%s)", e.err, e.typ, e.typ.Field(e.field).Name) +} + +type structTagError struct { + typ reflect.Type + field, tag, err string +} + +func (e structTagError) Error() string { + return fmt.Sprintf("rlp: invalid struct tag %q for %v.%s (%s)", e.tag, e.typ, e.field, e.err) +} + +func parseStructTag(typ reflect.Type, fi, lastPublic int) (tags, error) { f := typ.Field(fi) var ts tags for _, t := range strings.Split(f.Tag.Get("rlp"), ",") { @@ -123,15 +153,26 @@ func parseStructTag(typ reflect.Type, fi int) (tags, error) { case "": case "-": ts.ignored = true - case "nil": + case "nil", "nilString", "nilList": ts.nilOK = true + if f.Type.Kind() != reflect.Ptr { + return ts, structTagError{typ, f.Name, t, "field is not a pointer"} + } + switch t { + case "nil": + ts.nilKind = defaultNilKind(f.Type.Elem()) + case "nilString": + ts.nilKind = String + case "nilList": + ts.nilKind = List + } case "tail": ts.tail = true - if fi != typ.NumField()-1 { - return ts, fmt.Errorf(`rlp: invalid struct tag "tail" for %v.%s (must be on last field)`, typ, f.Name) + if fi != lastPublic { + return ts, structTagError{typ, f.Name, t, "must be on last field"} } if f.Type.Kind() != reflect.Slice { - return ts, fmt.Errorf(`rlp: invalid struct tag "tail" for %v.%s (field type is not slice)`, typ, f.Name) + return ts, structTagError{typ, f.Name, t, "field type is not slice"} } default: return ts, fmt.Errorf("rlp: unknown struct tag %q on %v.%s", t, typ, f.Name) @@ -140,17 +181,39 @@ func parseStructTag(typ reflect.Type, fi int) (tags, error) { return ts, nil } -func genTypeInfo(typ reflect.Type, tags tags) (info *typeinfo, err error) { - info = new(typeinfo) - if info.decoder, err = makeDecoder(typ, tags); err != nil { - return nil, err +func lastPublicField(typ reflect.Type) int { + last := 0 + for i := 0; i < typ.NumField(); i++ { + if typ.Field(i).PkgPath == "" { + last = i + } } - if info.writer, err = makeWriter(typ, tags); err != nil { - return nil, err + return last +} + +func (i *typeinfo) generate(typ reflect.Type, tags tags) { + i.decoder, i.decoderErr = makeDecoder(typ, tags) + i.writer, i.writerErr = makeWriter(typ, tags) +} + +// defaultNilKind determines whether a nil pointer to typ encodes/decodes +// as an empty string or empty list. +func defaultNilKind(typ reflect.Type) Kind { + k := typ.Kind() + if isUint(k) || k == reflect.String || k == reflect.Bool || isByteArray(typ) { + return String } - return info, nil + return List } func isUint(k reflect.Kind) bool { return k >= reflect.Uint && k <= reflect.Uintptr } + +func isByte(typ reflect.Type) bool { + return typ.Kind() == reflect.Uint8 && !typ.Implements(encoderInterface) +} + +func isByteArray(typ reflect.Type) bool { + return (typ.Kind() == reflect.Slice || typ.Kind() == reflect.Array) && isByte(typ.Elem()) +} diff --git a/rpc/lib/doc.go b/rpc/lib/doc.go deleted file mode 100644 index aa9638bfd..000000000 --- a/rpc/lib/doc.go +++ /dev/null @@ -1,85 +0,0 @@ -// HTTP RPC server supporting calls via uri params, jsonrpc, and jsonrpc over websockets -// -// Client Requests -// -// Suppose we want to expose the rpc function `HelloWorld(name string, num int)`. -// -// GET (URI) -// -// As a GET request, it would have URI encoded parameters, and look like: -// -// curl 'http://localhost:8008/hello_world?name="my_world"&num=5' -// -// Note the `'` around the url, which is just so bash doesn't ignore the quotes in `"my_world"`. -// This should also work: -// -// curl http://localhost:8008/hello_world?name=\"my_world\"&num=5 -// -// A GET request to `/` returns a list of available endpoints. -// For those which take arguments, the arguments will be listed in order, with `_` where the actual value should be. -// -// POST (JSONRPC) -// -// As a POST request, we use JSONRPC. For instance, the same request would have this as the body: -// -// { -// "jsonrpc": "2.0", -// "id": "anything", -// "method": "hello_world", -// "params": { -// "name": "my_world", -// "num": 5 -// } -// } -// -// With the above saved in file `data.json`, we can make the request with -// -// curl --data @data.json http://localhost:8008 -// -// -// WebSocket (JSONRPC) -// -// All requests are exposed over websocket in the same form as the POST JSONRPC. -// Websocket connections are available at their own endpoint, typically `/websocket`, -// though this is configurable when starting the server. -// -// Server Definition -// -// Define some types and routes: -// -// type ResultStatus struct { -// Value string -// } -// -// Define some routes -// -// var Routes = map[string]*rpcserver.RPCFunc{ -// "status": rpcserver.NewRPCFunc(Status, "arg"), -// } -// -// An rpc function: -// -// func Status(v string) (*ResultStatus, error) { -// return &ResultStatus{v}, nil -// } -// -// Now start the server: -// -// mux := http.NewServeMux() -// rpcserver.RegisterRPCFuncs(mux, Routes) -// wm := rpcserver.NewWebsocketManager(Routes) -// mux.HandleFunc("/websocket", wm.WebsocketHandler) -// logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) -// listener, err := rpc.Listen("0.0.0.0:8080", rpcserver.Config{}) -// if err != nil { panic(err) } -// go rpcserver.StartHTTPServer(listener, mux, logger) -// -// Note that unix sockets are supported as well (eg. `/path/to/socket` instead of `0.0.0.0:8008`) -// Now see all available endpoints by sending a GET request to `0.0.0.0:8008`. -// Each route is available as a GET request, as a JSONRPCv2 POST request, and via JSONRPCv2 over websockets. -// -// Examples -// -// - [Tendermint](https://github.com/tendermint/tendermint/blob/master/rpc/core/routes.go) -// - [tm-monitor](https://github.com/tendermint/tendermint/blob/master/tools/tm-monitor/rpc.go) -package rpc diff --git a/rpc/lib/rpc_test.go b/rpc/lib/rpc_test.go deleted file mode 100644 index d95f22094..000000000 --- a/rpc/lib/rpc_test.go +++ /dev/null @@ -1,377 +0,0 @@ -package rpc - -import ( - "bytes" - "context" - crand "crypto/rand" - "encoding/json" - "fmt" - tmbytes "github.com/tendermint/tendermint/libs/bytes" - "github.com/tendermint/tendermint/libs/rand" - "net/http" - "os" - "os/exec" - "testing" - "time" - - "github.com/go-kit/kit/log/term" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/libs/log" - - "github.com/tendermint/tendermint/rpc/jsonrpc/client" - "github.com/tendermint/tendermint/rpc/jsonrpc/server" - "github.com/tendermint/tendermint/rpc/jsonrpc/types" -) - -// Client and Server should work over tcp or unix sockets -const ( - tcpAddr = "tcp://0.0.0.0:47768" - - unixSocket = "/tmp/rpc_test.sock" - unixAddr = "unix://" + unixSocket - - websocketEndpoint = "/websocket/endpoint" -) - -type ResultEcho struct { - Value string `json:"value"` -} - -type ResultEchoInt struct { - Value int `json:"value"` -} - -type ResultEchoBytes struct { - Value []byte `json:"value"` -} - -type ResultEchoDataBytes struct { - Value tmbytes.HexBytes `json:"value"` -} - -// Define some routes -var Routes = map[string]*server.RPCFunc{ - "echo": server.NewRPCFunc(EchoResult, "arg"), - "echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"), - "echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"), - "echo_data_bytes": server.NewRPCFunc(EchoDataBytesResult, "arg"), - "echo_int": server.NewRPCFunc(EchoIntResult, "arg"), -} - -// Amino codec required to encode/decode everything above. -var RoutesCdc = amino.NewCodec() - -func EchoResult(ctx *types.Context, v string) (*ResultEcho, error) { - return &ResultEcho{v}, nil -} - -func EchoWSResult(ctx *types.Context, v string) (*ResultEcho, error) { - return &ResultEcho{v}, nil -} - -func EchoIntResult(ctx *types.Context, v int) (*ResultEchoInt, error) { - return &ResultEchoInt{v}, nil -} - -func EchoBytesResult(ctx *types.Context, v []byte) (*ResultEchoBytes, error) { - return &ResultEchoBytes{v}, nil -} - -func EchoDataBytesResult(ctx *types.Context, v tmbytes.HexBytes) (*ResultEchoDataBytes, error) { - return &ResultEchoDataBytes{v}, nil -} - -func TestMain(m *testing.M) { - setup() - code := m.Run() - os.Exit(code) -} - -var colorFn = func(keyvals ...interface{}) term.FgBgColor { - for i := 0; i < len(keyvals)-1; i += 2 { - if keyvals[i] == "socket" { - if keyvals[i+1] == "tcp" { - return term.FgBgColor{Fg: term.DarkBlue} - } else if keyvals[i+1] == "unix" { - return term.FgBgColor{Fg: term.DarkCyan} - } - } - } - return term.FgBgColor{} -} - -// launch unix and tcp servers -func setup() { - logger := log.NewTMLoggerWithColorFn(log.NewSyncWriter(os.Stdout), colorFn) - - cmd := exec.Command("rm", "-f", unixSocket) - err := cmd.Start() - if err != nil { - panic(err) - } - if err = cmd.Wait(); err != nil { - panic(err) - } - - tcpLogger := logger.With("socket", "tcp") - mux := http.NewServeMux() - server.RegisterRPCFuncs(mux, Routes, RoutesCdc, tcpLogger) - wm := server.NewWebsocketManager(Routes, RoutesCdc, server.ReadWait(5*time.Second), server.PingPeriod(1*time.Second)) - wm.SetLogger(tcpLogger) - mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) - config := server.DefaultConfig() - listener1, err := server.Listen(tcpAddr, config) - if err != nil { - panic(err) - } - go server.Serve(listener1, mux, tcpLogger, config) - - unixLogger := logger.With("socket", "unix") - mux2 := http.NewServeMux() - server.RegisterRPCFuncs(mux2, Routes, RoutesCdc, unixLogger) - wm = server.NewWebsocketManager(Routes, RoutesCdc) - wm.SetLogger(unixLogger) - mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) - listener2, err := server.Listen(unixAddr, config) - if err != nil { - panic(err) - } - go server.Serve(listener2, mux2, unixLogger, config) - - // wait for servers to start - time.Sleep(time.Second * 2) -} - -func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { - params := map[string]interface{}{ - "arg": val, - } - result := new(ResultEcho) - if _, err := cl.Call("echo", params, result); err != nil { - return "", err - } - return result.Value, nil -} - -func echoIntViaHTTP(cl client.HTTPClient, val int) (int, error) { - params := map[string]interface{}{ - "arg": val, - } - result := new(ResultEchoInt) - if _, err := cl.Call("echo_int", params, result); err != nil { - return 0, err - } - return result.Value, nil -} - -func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { - params := map[string]interface{}{ - "arg": bytes, - } - result := new(ResultEchoBytes) - if _, err := cl.Call("echo_bytes", params, result); err != nil { - return []byte{}, err - } - return result.Value, nil -} - -func echoDataBytesViaHTTP(cl client.HTTPClient, bytes tmbytes.HexBytes) (tmbytes.HexBytes, error) { - params := map[string]interface{}{ - "arg": bytes, - } - result := new(ResultEchoDataBytes) - if _, err := cl.Call("echo_data_bytes", params, result); err != nil { - return []byte{}, err - } - return result.Value, nil -} - -func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { - val := "acbd" - got, err := echoViaHTTP(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) - - val2 := randBytes(t) - got2, err := echoBytesViaHTTP(cl, val2) - require.Nil(t, err) - assert.Equal(t, got2, val2) - - val3 := tmbytes.HexBytes(randBytes(t)) - got3, err := echoDataBytesViaHTTP(cl, val3) - require.Nil(t, err) - assert.Equal(t, got3, val3) - - val4 := rand.Intn(10000) - got4, err := echoIntViaHTTP(cl, val4) - require.Nil(t, err) - assert.Equal(t, got4, val4) -} - -func echoViaWS(cl *client.WSClient, val string) (string, error) { - params := map[string]interface{}{ - "arg": val, - } - err := cl.Call(context.Background(), "echo", params) - if err != nil { - return "", err - } - - msg := <-cl.ResponsesCh - if msg.Error != nil { - return "", err - - } - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - if err != nil { - return "", nil - } - return result.Value, nil -} - -func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { - params := map[string]interface{}{ - "arg": bytes, - } - err := cl.Call(context.Background(), "echo_bytes", params) - if err != nil { - return []byte{}, err - } - - msg := <-cl.ResponsesCh - if msg.Error != nil { - return []byte{}, msg.Error - - } - result := new(ResultEchoBytes) - err = json.Unmarshal(msg.Result, result) - if err != nil { - return []byte{}, nil - } - return result.Value, nil -} - -func testWithWSClient(t *testing.T, cl *client.WSClient) { - val := "acbd" - got, err := echoViaWS(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) - - val2 := randBytes(t) - got2, err := echoBytesViaWS(cl, val2) - require.Nil(t, err) - assert.Equal(t, got2, val2) -} - -//------------- - -func TestServersAndClientsBasic(t *testing.T) { - serverAddrs := [...]string{tcpAddr, unixAddr} - for _, addr := range serverAddrs { - cl1, _ := client.NewURI(addr) - fmt.Printf("=== testing server on %s using URI client", addr) - testWithHTTPClient(t, cl1) - - cl2, _ := client.New(addr) - fmt.Printf("=== testing server on %s using JSONRPC client", addr) - testWithHTTPClient(t, cl2) - - cl3, _ := client.NewWS(addr, websocketEndpoint) - cl3.SetLogger(log.TestingLogger()) - err := cl3.Start() - require.Nil(t, err) - fmt.Printf("=== testing server on %s using WS client", addr) - testWithWSClient(t, cl3) - cl3.Stop() - } -} - -func TestHexStringArg(t *testing.T) { - cl, _ := client.NewURI(tcpAddr) - // should NOT be handled as hex - val := "0xabc" - got, err := echoViaHTTP(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) -} - -func TestQuotedStringArg(t *testing.T) { - cl, _ := client.NewURI(tcpAddr) - // should NOT be unquoted - val := "\"abc\"" - got, err := echoViaHTTP(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) -} - -func TestWSNewWSRPCFunc(t *testing.T) { - cl, _ := client.NewWS(tcpAddr, websocketEndpoint) - cl.SetLogger(log.TestingLogger()) - err := cl.Start() - require.Nil(t, err) - defer cl.Stop() - - val := "acbd" - params := map[string]interface{}{ - "arg": val, - } - err = cl.Call(context.Background(), "echo_ws", params) - require.Nil(t, err) - - msg := <-cl.ResponsesCh - if msg.Error != nil { - t.Fatal(err) - } - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - require.Nil(t, err) - got := result.Value - assert.Equal(t, got, val) -} - -func TestWSHandlesArrayParams(t *testing.T) { - cl, _ := client.NewWS(tcpAddr, websocketEndpoint) - cl.SetLogger(log.TestingLogger()) - err := cl.Start() - require.Nil(t, err) - defer cl.Stop() - - val := "acbd" - params := []interface{}{val} - err = cl.CallWithArrayParams(context.Background(), "echo_ws", params) - require.Nil(t, err) - - msg := <-cl.ResponsesCh - if msg.Error != nil { - t.Fatalf("%+v", err) - } - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - require.Nil(t, err) - got := result.Value - assert.Equal(t, got, val) -} - -// TestWSClientPingPong checks that a client & server exchange pings -// & pongs so connection stays alive. -func TestWSClientPingPong(t *testing.T) { - cl, _ := client.NewWS(tcpAddr, websocketEndpoint) - cl.SetLogger(log.TestingLogger()) - err := cl.Start() - require.Nil(t, err) - defer cl.Stop() - - time.Sleep(6 * time.Second) -} - -func randBytes(t *testing.T) []byte { - n := rand.Intn(10) + 2 - buf := make([]byte, n) - _, err := crand.Read(buf) - require.Nil(t, err) - return bytes.Replace(buf, []byte("="), []byte{100}, -1) -} diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go deleted file mode 100644 index 25cc4e374..000000000 --- a/rpc/lib/server/handlers.go +++ /dev/null @@ -1,849 +0,0 @@ -package rpcserver - -import ( - "bytes" - "context" - "encoding/hex" - "encoding/json" - "fmt" - "github.com/tendermint/tendermint/libs/service" - "io/ioutil" - "net/http" - "reflect" - "runtime/debug" - "sort" - "strings" - "time" - - "github.com/gorilla/websocket" - "github.com/pkg/errors" - - types "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/libs/log" -) - -// RegisterRPCFuncs adds a route for each function in the funcMap, as well as general jsonrpc and websocket handlers for all functions. -// "result" is the interface on which the result objects are registered, and is popualted with every RPCResponse -func RegisterRPCFuncs(mux *http.ServeMux, funcMap map[string]*RPCFunc, cdc *amino.Codec, logger log.Logger, middleware func(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request)) { - // HTTP endpoints - for funcName, rpcFunc := range funcMap { - handler := makeHTTPHandler(rpcFunc, cdc, logger) - if middleware != nil { - handler = middleware(handler) - } - mux.HandleFunc("/"+funcName, handler) - } - - // JSONRPC endpoints - mux.HandleFunc("/", handleInvalidJSONRPCPaths(makeJSONRPCHandler(funcMap, cdc, logger))) -} - -//------------------------------------- -// function introspection - -// RPCFunc contains the introspected type information for a function -type RPCFunc struct { - f reflect.Value // underlying rpc function - args []reflect.Type // type of each function arg - returns []reflect.Type // type of each return arg - argNames []string // name of each argument - ws bool // websocket only -} - -// NewRPCFunc wraps a function for introspection. -// f is the function, args are comma separated argument names -func NewRPCFunc(f interface{}, args string) *RPCFunc { - return newRPCFunc(f, args, false) -} - -// NewWSRPCFunc wraps a function for introspection and use in the websockets. -func NewWSRPCFunc(f interface{}, args string) *RPCFunc { - return newRPCFunc(f, args, true) -} - -func newRPCFunc(f interface{}, args string, ws bool) *RPCFunc { - var argNames []string - if args != "" { - argNames = strings.Split(args, ",") - } - return &RPCFunc{ - f: reflect.ValueOf(f), - args: funcArgTypes(f), - returns: funcReturnTypes(f), - argNames: argNames, - ws: ws, - } -} - -// return a function's argument types -func funcArgTypes(f interface{}) []reflect.Type { - t := reflect.TypeOf(f) - n := t.NumIn() - typez := make([]reflect.Type, n) - for i := 0; i < n; i++ { - typez[i] = t.In(i) - } - return typez -} - -// return a function's return types -func funcReturnTypes(f interface{}) []reflect.Type { - t := reflect.TypeOf(f) - n := t.NumOut() - typez := make([]reflect.Type, n) - for i := 0; i < n; i++ { - typez[i] = t.Out(i) - } - return typez -} - -// function introspection -//----------------------------------------------------------------------------- -// rpc.json - -// jsonrpc calls grab the given method's function info and runs reflect.Call -func makeJSONRPCHandler(funcMap map[string]*RPCFunc, cdc *amino.Codec, logger log.Logger) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - b, err := ioutil.ReadAll(r.Body) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(types.JSONRPCStringID(""), errors.Wrap(err, "Error reading request body"))) - return - } - // if its an empty request (like from a browser), - // just display a list of functions - if len(b) == 0 { - writeListOfEndpoints(w, r, funcMap) - return - } - - var request types.RPCRequest - err = json.Unmarshal(b, &request) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "Error unmarshalling request"))) - return - } - // A Notification is a Request object without an "id" member. - // The Server MUST NOT reply to a Notification, including those that are within a batch request. - if request.ID == types.JSONRPCStringID("") { - logger.Debug("HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") - return - } - if len(r.URL.Path) > 1 { - WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(request.ID, errors.Errorf("Path %s is invalid", r.URL.Path))) - return - } - rpcFunc := funcMap[request.Method] - if rpcFunc == nil || rpcFunc.ws { - WriteRPCResponseHTTP(w, types.RPCMethodNotFoundError(request.ID)) - return - } - var args []reflect.Value - if len(request.Params) > 0 { - args, err = jsonParamsToArgsRPC(rpcFunc, cdc, request.Params) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCInvalidParamsError(request.ID, errors.Wrap(err, "Error converting json params to arguments"))) - return - } - } - returns := rpcFunc.f.Call(args) - logger.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) - result, err := unreflectResult(returns) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCInternalError(request.ID, err)) - return - } - WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(cdc, request.ID, result)) - } -} - -func handleInvalidJSONRPCPaths(next http.HandlerFunc) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - // Since the pattern "/" matches all paths not matched by other registered patterns we check whether the path is indeed - // "/", otherwise return a 404 error - if r.URL.Path != "/" { - http.NotFound(w, r) - return - } - - next(w, r) - } -} - -func mapParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, params map[string]json.RawMessage, argsOffset int) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.argNames)) - for i, argName := range rpcFunc.argNames { - argType := rpcFunc.args[i+argsOffset] - - if p, ok := params[argName]; ok && p != nil && len(p) > 0 { - val := reflect.New(argType) - err := cdc.UnmarshalJSON(p, val.Interface()) - if err != nil { - return nil, err - } - values[i] = val.Elem() - } else { // use default for that type - values[i] = reflect.Zero(argType) - } - } - - return values, nil -} - -func arrayParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, params []json.RawMessage, argsOffset int) ([]reflect.Value, error) { - if len(rpcFunc.argNames) != len(params) { - return nil, errors.Errorf("Expected %v parameters (%v), got %v (%v)", - len(rpcFunc.argNames), rpcFunc.argNames, len(params), params) - } - - values := make([]reflect.Value, len(params)) - for i, p := range params { - argType := rpcFunc.args[i+argsOffset] - val := reflect.New(argType) - err := cdc.UnmarshalJSON(p, val.Interface()) - if err != nil { - return nil, err - } - values[i] = val.Elem() - } - return values, nil -} - -// `raw` is unparsed json (from json.RawMessage) encoding either a map or an array. -// `argsOffset` should be 0 for RPC calls, and 1 for WS requests, where len(rpcFunc.args) != len(rpcFunc.argNames). -// -// Example: -// rpcFunc.args = [rpctypes.WSRPCContext string] -// rpcFunc.argNames = ["arg"] -func jsonParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, raw []byte, argsOffset int) ([]reflect.Value, error) { - - // TODO: Make more efficient, perhaps by checking the first character for '{' or '['? - // First, try to get the map. - var m map[string]json.RawMessage - err := json.Unmarshal(raw, &m) - if err == nil { - return mapParamsToArgs(rpcFunc, cdc, m, argsOffset) - } - - // Otherwise, try an array. - var a []json.RawMessage - err = json.Unmarshal(raw, &a) - if err == nil { - return arrayParamsToArgs(rpcFunc, cdc, a, argsOffset) - } - - // Otherwise, bad format, we cannot parse - return nil, errors.Errorf("Unknown type for JSON params: %v. Expected map or array", err) -} - -// Convert a []interface{} OR a map[string]interface{} to properly typed values -func jsonParamsToArgsRPC(rpcFunc *RPCFunc, cdc *amino.Codec, params json.RawMessage) ([]reflect.Value, error) { - return jsonParamsToArgs(rpcFunc, cdc, params, 0) -} - -// Same as above, but with the first param the websocket connection -func jsonParamsToArgsWS(rpcFunc *RPCFunc, cdc *amino.Codec, params json.RawMessage, wsCtx types.WSRPCContext) ([]reflect.Value, error) { - values, err := jsonParamsToArgs(rpcFunc, cdc, params, 1) - if err != nil { - return nil, err - } - return append([]reflect.Value{reflect.ValueOf(wsCtx)}, values...), nil -} - -// rpc.json -//----------------------------------------------------------------------------- -// rpc.http - -// convert from a function name to the http handler -func makeHTTPHandler(rpcFunc *RPCFunc, cdc *amino.Codec, logger log.Logger) func(http.ResponseWriter, *http.Request) { - // Exception for websocket endpoints - if rpcFunc.ws { - return func(w http.ResponseWriter, r *http.Request) { - WriteRPCResponseHTTP(w, types.RPCMethodNotFoundError(types.JSONRPCStringID(""))) - } - } - // All other endpoints - return func(w http.ResponseWriter, r *http.Request) { - logger.Debug("HTTP HANDLER", "req", r) - args, err := httpParamsToArgs(rpcFunc, cdc, r) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCInvalidParamsError(types.JSONRPCStringID(""), errors.Wrap(err, "Error converting http params to arguments"))) - return - } - returns := rpcFunc.f.Call(args) - logger.Info("HTTPRestRPC", "method", r.URL.Path, "args", args, "returns", returns) - result, err := unreflectResult(returns) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCInternalError(types.JSONRPCStringID(""), err)) - return - } - WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(cdc, types.JSONRPCStringID(""), result)) - } -} - -// Covert an http query to a list of properly typed values. -// To be properly decoded the arg must be a concrete type from tendermint (if its an interface). -func httpParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, r *http.Request) ([]reflect.Value, error) { - values := make([]reflect.Value, len(rpcFunc.args)) - - for i, name := range rpcFunc.argNames { - argType := rpcFunc.args[i] - - values[i] = reflect.Zero(argType) // set default for that type - - arg := GetParam(r, name) - // log.Notice("param to arg", "argType", argType, "name", name, "arg", arg) - - if "" == arg { - continue - } - - v, err, ok := nonJSONStringToArg(cdc, argType, arg) - if err != nil { - return nil, err - } - if ok { - values[i] = v - continue - } - - values[i], err = jsonStringToArg(cdc, argType, arg) - if err != nil { - return nil, err - } - } - - return values, nil -} - -func jsonStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error) { - rv := reflect.New(rt) - err := cdc.UnmarshalJSON([]byte(arg), rv.Interface()) - if err != nil { - return rv, err - } - rv = rv.Elem() - return rv, nil -} - -func nonJSONStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error, bool) { - if rt.Kind() == reflect.Ptr { - rv_, err, ok := nonJSONStringToArg(cdc, rt.Elem(), arg) - if err != nil { - return reflect.Value{}, err, false - } else if ok { - rv := reflect.New(rt.Elem()) - rv.Elem().Set(rv_) - return rv, nil, true - } else { - return reflect.Value{}, nil, false - } - } else { - return _nonJSONStringToArg(cdc, rt, arg) - } -} - -// NOTE: rt.Kind() isn't a pointer. -func _nonJSONStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error, bool) { - isIntString := RE_INT.Match([]byte(arg)) - isQuotedString := strings.HasPrefix(arg, `"`) && strings.HasSuffix(arg, `"`) - isHexString := strings.HasPrefix(arg, "0x") || strings.HasPrefix(arg, "Mt") || - strings.HasPrefix(arg, "Mp") - - var expectingString, expectingByteSlice, expectingInt bool - switch rt.Kind() { - case reflect.Int, reflect.Uint, reflect.Int8, reflect.Uint8, reflect.Int16, reflect.Uint16, reflect.Int32, reflect.Uint32, reflect.Int64, reflect.Uint64: - expectingInt = true - case reflect.String: - expectingString = true - case reflect.Slice: - expectingByteSlice = rt.Elem().Kind() == reflect.Uint8 - } - - if isIntString && expectingInt { - qarg := `"` + arg + `"` - // jsonStringToArg - rv, err := jsonStringToArg(cdc, rt, qarg) - if err != nil { - return rv, err, false - } else { - return rv, nil, true - } - } - - if isHexString { - if !expectingString && !expectingByteSlice { - err := errors.Errorf("Got a hex string arg, but expected '%s'", - rt.Kind().String()) - return reflect.ValueOf(nil), err, false - } - - var value []byte - value, err := hex.DecodeString(arg[2:]) - if err != nil { - return reflect.ValueOf(nil), err, false - } - if rt.Kind() == reflect.String { - return reflect.ValueOf(string(value)), nil, true - } - return reflect.ValueOf([]byte(value)), nil, true - } - - if expectingString && !isQuotedString { - return reflect.ValueOf(arg), nil, true - } - - if isQuotedString && expectingByteSlice { - v := reflect.New(reflect.TypeOf("")) - err := cdc.UnmarshalJSON([]byte(arg), v.Interface()) - if err != nil { - return reflect.ValueOf(nil), err, false - } - v = v.Elem() - return reflect.ValueOf([]byte(v.String())), nil, true - } - - return reflect.ValueOf(nil), nil, false -} - -// rpc.http -//----------------------------------------------------------------------------- -// rpc.websocket - -const ( - defaultWSWriteChanCapacity = 1000 - defaultWSWriteWait = 10 * time.Second - defaultWSReadWait = 30 * time.Second - defaultWSPingPeriod = (defaultWSReadWait * 9) / 10 -) - -// A single websocket connection contains listener id, underlying ws -// connection, and the event switch for subscribing to events. -// -// In case of an error, the connection is stopped. -type wsConnection struct { - service.BaseService - - remoteAddr string - baseConn *websocket.Conn - writeChan chan types.RPCResponse - - funcMap map[string]*RPCFunc - cdc *amino.Codec - - // write channel capacity - writeChanCapacity int - - // each write times out after this. - writeWait time.Duration - - // Connection times out if we haven't received *anything* in this long, not even pings. - readWait time.Duration - - // Send pings to server with this period. Must be less than readWait, but greater than zero. - pingPeriod time.Duration - - // object that is used to subscribe / unsubscribe from events - eventSub types.EventSubscriber -} - -// NewWSConnection wraps websocket.Conn. -// -// See the commentary on the func(*wsConnection) functions for a detailed -// description of how to configure ping period and pong wait time. NOTE: if the -// write buffer is full, pongs may be dropped, which may cause clients to -// disconnect. see https://github.com/gorilla/websocket/issues/97 -func NewWSConnection( - baseConn *websocket.Conn, - funcMap map[string]*RPCFunc, - cdc *amino.Codec, - options ...func(*wsConnection), -) *wsConnection { - baseConn.SetReadLimit(maxBodyBytes) - wsc := &wsConnection{ - remoteAddr: baseConn.RemoteAddr().String(), - baseConn: baseConn, - funcMap: funcMap, - cdc: cdc, - writeWait: defaultWSWriteWait, - writeChanCapacity: defaultWSWriteChanCapacity, - readWait: defaultWSReadWait, - pingPeriod: defaultWSPingPeriod, - } - for _, option := range options { - option(wsc) - } - wsc.BaseService = *service.NewBaseService(nil, "wsConnection", wsc) - return wsc -} - -// EventSubscriber sets object that is used to subscribe / unsubscribe from -// events - not Goroutine-safe. If none given, default node's eventBus will be -// used. -func EventSubscriber(eventSub types.EventSubscriber) func(*wsConnection) { - return func(wsc *wsConnection) { - wsc.eventSub = eventSub - } -} - -// WriteWait sets the amount of time to wait before a websocket write times out. -// It should only be used in the constructor - not Goroutine-safe. -func WriteWait(writeWait time.Duration) func(*wsConnection) { - return func(wsc *wsConnection) { - wsc.writeWait = writeWait - } -} - -// WriteChanCapacity sets the capacity of the websocket write channel. -// It should only be used in the constructor - not Goroutine-safe. -func WriteChanCapacity(cap int) func(*wsConnection) { - return func(wsc *wsConnection) { - wsc.writeChanCapacity = cap - } -} - -// ReadWait sets the amount of time to wait before a websocket read times out. -// It should only be used in the constructor - not Goroutine-safe. -func ReadWait(readWait time.Duration) func(*wsConnection) { - return func(wsc *wsConnection) { - wsc.readWait = readWait - } -} - -// PingPeriod sets the duration for sending websocket pings. -// It should only be used in the constructor - not Goroutine-safe. -func PingPeriod(pingPeriod time.Duration) func(*wsConnection) { - return func(wsc *wsConnection) { - wsc.pingPeriod = pingPeriod - } -} - -// OnStart implements cmn.Service by starting the read and write routines. It -// blocks until the connection closes. -func (wsc *wsConnection) OnStart() error { - wsc.writeChan = make(chan types.RPCResponse, wsc.writeChanCapacity) - - // Read subscriptions/unsubscriptions to events - go wsc.readRoutine() - // Write responses, BLOCKING. - wsc.writeRoutine() - - return nil -} - -// OnStop implements cmn.Service by unsubscribing remoteAddr from all subscriptions. -func (wsc *wsConnection) OnStop() { - // Both read and write loops close the websocket connection when they exit their loops. - // The writeChan is never closed, to allow WriteRPCResponse() to fail. - if wsc.eventSub != nil { - wsc.eventSub.UnsubscribeAll(context.TODO(), wsc.remoteAddr) - } -} - -// GetRemoteAddr returns the remote address of the underlying connection. -// It implements WSRPCConnection -func (wsc *wsConnection) GetRemoteAddr() string { - return wsc.remoteAddr -} - -// GetEventSubscriber implements WSRPCConnection by returning event subscriber. -func (wsc *wsConnection) GetEventSubscriber() types.EventSubscriber { - return wsc.eventSub -} - -// WriteRPCResponse pushes a response to the writeChan, and blocks until it is accepted. -// It implements WSRPCConnection. It is Goroutine-safe. -func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { - select { - case <-wsc.Quit(): - return - case wsc.writeChan <- resp: - } -} - -// TryWriteRPCResponse attempts to push a response to the writeChan, but does not block. -// It implements WSRPCConnection. It is Goroutine-safe -func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { - select { - case <-wsc.Quit(): - return false - case wsc.writeChan <- resp: - return true - default: - return false - } -} - -// Codec returns an amino codec used to decode parameters and encode results. -// It implements WSRPCConnection. -func (wsc *wsConnection) Codec() *amino.Codec { - return wsc.cdc -} - -// Read from the socket and subscribe to or unsubscribe from events -func (wsc *wsConnection) readRoutine() { - defer func() { - if r := recover(); r != nil { - err, ok := r.(error) - if !ok { - err = fmt.Errorf("WSJSONRPC: %v", r) - } - wsc.Logger.Error("Panic in WSJSONRPC handler", "err", err, "stack", string(debug.Stack())) - wsc.WriteRPCResponse(types.RPCInternalError(types.JSONRPCStringID("unknown"), err)) - go wsc.readRoutine() - } else { - wsc.baseConn.Close() // nolint: errcheck - } - }() - - wsc.baseConn.SetPongHandler(func(m string) error { - return wsc.baseConn.SetReadDeadline(time.Now().Add(wsc.readWait)) - }) - - for { - select { - case <-wsc.Quit(): - return - default: - // reset deadline for every type of message (control or data) - if err := wsc.baseConn.SetReadDeadline(time.Now().Add(wsc.readWait)); err != nil { - wsc.Logger.Error("failed to set read deadline", "err", err) - } - var in []byte - _, in, err := wsc.baseConn.ReadMessage() - if err != nil { - if websocket.IsCloseError(err, websocket.CloseNormalClosure) { - wsc.Logger.Info("Client closed the connection") - } else { - wsc.Logger.Error("Failed to read request", "err", err) - } - wsc.Stop() - return - } - - var request types.RPCRequest - err = json.Unmarshal(in, &request) - if err != nil { - wsc.WriteRPCResponse(types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "Error unmarshaling request"))) - continue - } - - // A Notification is a Request object without an "id" member. - // The Server MUST NOT reply to a Notification, including those that are within a batch request. - if request.ID == types.JSONRPCStringID("") { - wsc.Logger.Debug("WSJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") - continue - } - - // Now, fetch the RPCFunc and execute it. - - rpcFunc := wsc.funcMap[request.Method] - if rpcFunc == nil { - wsc.WriteRPCResponse(types.RPCMethodNotFoundError(request.ID)) - continue - } - var args []reflect.Value - if rpcFunc.ws { - wsCtx := types.WSRPCContext{Request: request, WSRPCConnection: wsc} - if len(request.Params) > 0 { - args, err = jsonParamsToArgsWS(rpcFunc, wsc.cdc, request.Params, wsCtx) - } - } else if len(request.Params) > 0 { - args, err = jsonParamsToArgsRPC(rpcFunc, wsc.cdc, request.Params) - } - if err != nil { - wsc.WriteRPCResponse(types.RPCInternalError(request.ID, errors.Wrap(err, "Error converting json params to arguments"))) - continue - } - returns := rpcFunc.f.Call(args) - - // TODO: Need to encode args/returns to string if we want to log them - wsc.Logger.Info("WSJSONRPC", "method", request.Method) - - result, err := unreflectResult(returns) - if err != nil { - wsc.WriteRPCResponse(types.RPCInternalError(request.ID, err)) - continue - } - - wsc.WriteRPCResponse(types.NewRPCSuccessResponse(wsc.cdc, request.ID, result)) - } - } -} - -// receives on a write channel and writes out on the socket -func (wsc *wsConnection) writeRoutine() { - pingTicker := time.NewTicker(wsc.pingPeriod) - defer func() { - pingTicker.Stop() - if err := wsc.baseConn.Close(); err != nil { - wsc.Logger.Error("Error closing connection", "err", err) - } - }() - - // https://github.com/gorilla/websocket/issues/97 - pongs := make(chan string, 1) - wsc.baseConn.SetPingHandler(func(m string) error { - select { - case pongs <- m: - default: - } - return nil - }) - - for { - select { - case m := <-pongs: - err := wsc.writeMessageWithDeadline(websocket.PongMessage, []byte(m)) - if err != nil { - wsc.Logger.Info("Failed to write pong (client may disconnect)", "err", err) - } - case <-pingTicker.C: - err := wsc.writeMessageWithDeadline(websocket.PingMessage, []byte{}) - if err != nil { - wsc.Logger.Error("Failed to write ping", "err", err) - wsc.Stop() - return - } - case msg := <-wsc.writeChan: - jsonBytes, err := json.MarshalIndent(msg, "", " ") - if err != nil { - wsc.Logger.Error("Failed to marshal RPCResponse to JSON", "err", err) - } else if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { - wsc.Logger.Error("Failed to write response", "err", err) - wsc.Stop() - return - } - case <-wsc.Quit(): - return - } - } -} - -// All writes to the websocket must (re)set the write deadline. -// If some writes don't set it while others do, they may timeout incorrectly (https://github.com/tendermint/tendermint/issues/553) -func (wsc *wsConnection) writeMessageWithDeadline(msgType int, msg []byte) error { - if err := wsc.baseConn.SetWriteDeadline(time.Now().Add(wsc.writeWait)); err != nil { - return err - } - return wsc.baseConn.WriteMessage(msgType, msg) -} - -//---------------------------------------- - -// WebsocketManager provides a WS handler for incoming connections and passes a -// map of functions along with any additional params to new connections. -// NOTE: The websocket path is defined externally, e.g. in node/node.go -type WebsocketManager struct { - websocket.Upgrader - - funcMap map[string]*RPCFunc - cdc *amino.Codec - logger log.Logger - wsConnOptions []func(*wsConnection) -} - -// NewWebsocketManager returns a new WebsocketManager that passes a map of -// functions, connection options and logger to new WS connections. -func NewWebsocketManager(funcMap map[string]*RPCFunc, cdc *amino.Codec, wsConnOptions ...func(*wsConnection)) *WebsocketManager { - return &WebsocketManager{ - funcMap: funcMap, - cdc: cdc, - Upgrader: websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { - // TODO ??? - return true - }, - }, - logger: log.NewNopLogger(), - wsConnOptions: wsConnOptions, - } -} - -// SetLogger sets the logger. -func (wm *WebsocketManager) SetLogger(l log.Logger) { - wm.logger = l -} - -// WebsocketHandler upgrades the request/response (via http.Hijack) and starts -// the wsConnection. -func (wm *WebsocketManager) WebsocketHandler(w http.ResponseWriter, r *http.Request) { - wsConn, err := wm.Upgrade(w, r, nil) - if err != nil { - // TODO - return http error - wm.logger.Error("Failed to upgrade to websocket connection", "err", err) - return - } - - // register connection - con := NewWSConnection(wsConn, wm.funcMap, wm.cdc, wm.wsConnOptions...) - con.SetLogger(wm.logger.With("remote", wsConn.RemoteAddr())) - wm.logger.Info("New websocket connection", "remote", con.remoteAddr) - err = con.Start() // Blocking - if err != nil { - wm.logger.Error("Error starting connection", "err", err) - } -} - -// rpc.websocket -//----------------------------------------------------------------------------- - -// NOTE: assume returns is result struct and error. If error is not nil, return it -func unreflectResult(returns []reflect.Value) (interface{}, error) { - errV := returns[1] - if errV.Interface() != nil { - if txErr, ok := errV.Interface().(types.TxError); ok { - return nil, txErr - } - if rpcErr, ok := errV.Interface().(types.RPCError); ok { - return nil, rpcErr - } - return nil, errors.Errorf("%v", errV.Interface()) - } - rv := returns[0] - // the result is a registered interface, - // we need a pointer to it so we can marshal with type byte - rvp := reflect.New(rv.Type()) - rvp.Elem().Set(rv) - return rvp.Interface(), nil -} - -// writes a list of available rpc endpoints as an html page -func writeListOfEndpoints(w http.ResponseWriter, r *http.Request, funcMap map[string]*RPCFunc) { - noArgNames := []string{} - argNames := []string{} - for name, funcData := range funcMap { - if len(funcData.args) == 0 { - noArgNames = append(noArgNames, name) - } else { - argNames = append(argNames, name) - } - } - sort.Strings(noArgNames) - sort.Strings(argNames) - buf := new(bytes.Buffer) - buf.WriteString("") - buf.WriteString("
Available endpoints:
") - - for _, name := range noArgNames { - link := fmt.Sprintf("/%s", name) - buf.WriteString(fmt.Sprintf("%s
", link, link)) - } - - buf.WriteString("
Endpoints that require arguments:
") - for _, name := range argNames { - link := fmt.Sprintf("/%s?", name) - funcData := funcMap[name] - for i, argName := range funcData.argNames { - link += argName + "=_" - if i < len(funcData.argNames)-1 { - link += "&" - } - } - buf.WriteString(fmt.Sprintf("%s
", link, link)) - } - buf.WriteString("") - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(200) - w.Write(buf.Bytes()) // nolint: errcheck -} diff --git a/rpc/lib/server/handlers_test.go b/rpc/lib/server/handlers_test.go deleted file mode 100644 index a1e1a7184..000000000 --- a/rpc/lib/server/handlers_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package rpcserver_test - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - rs "github.com/MinterTeam/minter-go-node/rpc/lib/server" - types "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/libs/log" -) - -////////////////////////////////////////////////////////////////////////////// -// HTTP REST API -// TODO - -////////////////////////////////////////////////////////////////////////////// -// JSON-RPC over HTTP - -func testMux() *http.ServeMux { - funcMap := map[string]*rs.RPCFunc{ - "c": rs.NewRPCFunc(func(s string, i int) (string, error) { return "foo", nil }, "s,i"), - } - cdc := amino.NewCodec() - mux := http.NewServeMux() - buf := new(bytes.Buffer) - logger := log.NewTMLogger(buf) - rs.RegisterRPCFuncs(mux, funcMap, cdc, logger, nil) - - return mux -} - -func statusOK(code int) bool { return code >= 200 && code <= 299 } - -// Ensure that nefarious/unintended inputs to `params` -// do not crash our RPC handlers. -// See Issue https://github.com/tendermint/tendermint/issues/708. -func TestRPCParams(t *testing.T) { - mux := testMux() - tests := []struct { - payload string - wantErr string - expectedId interface{} - }{ - // bad - {`{"jsonrpc": "2.0", "id": "0"}`, "Method not found", types.JSONRPCStringID("0")}, - {`{"jsonrpc": "2.0", "method": "y", "id": "0"}`, "Method not found", types.JSONRPCStringID("0")}, - {`{"method": "c", "id": "0", "params": a}`, "invalid character", types.JSONRPCStringID("")}, // id not captured in JSON parsing failures - {`{"method": "c", "id": "0", "params": ["a"]}`, "got 1", types.JSONRPCStringID("0")}, - {`{"method": "c", "id": "0", "params": ["a", "b"]}`, "invalid character", types.JSONRPCStringID("0")}, - {`{"method": "c", "id": "0", "params": [1, 1]}`, "of type string", types.JSONRPCStringID("0")}, - - // good - {`{"jsonrpc": "2.0", "method": "c", "id": "0", "params": null}`, "", types.JSONRPCStringID("0")}, - {`{"method": "c", "id": "0", "params": {}}`, "", types.JSONRPCStringID("0")}, - {`{"method": "c", "id": "0", "params": ["a", "10"]}`, "", types.JSONRPCStringID("0")}, - } - - for i, tt := range tests { - req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload)) - rec := httptest.NewRecorder() - mux.ServeHTTP(rec, req) - res := rec.Result() - // Always expecting back a JSONRPCResponse - //assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) - blob, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Errorf("#%d: err reading body: %v", i, err) - continue - } - - recv := new(types.RPCResponse) - assert.Nil(t, json.Unmarshal(blob, recv), "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob) - assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i) - assert.Equal(t, tt.expectedId, recv.ID, "#%d: expected ID not matched in RPCResponse", i) - if tt.wantErr == "" { - assert.Nil(t, recv.Error, "#%d: not expecting an error", i) - } else { - assert.True(t, recv.Error.Code < 0, "#%d: not expecting a positive JSONRPC code", i) - // The wanted error is either in the message or the data - assert.Contains(t, recv.Error.Message+recv.Error.Data, tt.wantErr, "#%d: expected substring", i) - } - } -} - -func TestJSONRPCID(t *testing.T) { - mux := testMux() - tests := []struct { - payload string - wantErr bool - expectedId interface{} - }{ - // good id - {`{"jsonrpc": "2.0", "method": "c", "id": "0", "params": ["a", "10"]}`, false, types.JSONRPCStringID("0")}, - {`{"jsonrpc": "2.0", "method": "c", "id": "abc", "params": ["a", "10"]}`, false, types.JSONRPCStringID("abc")}, - {`{"jsonrpc": "2.0", "method": "c", "id": 0, "params": ["a", "10"]}`, false, types.JSONRPCIntID(0)}, - {`{"jsonrpc": "2.0", "method": "c", "id": 1, "params": ["a", "10"]}`, false, types.JSONRPCIntID(1)}, - {`{"jsonrpc": "2.0", "method": "c", "id": 1.3, "params": ["a", "10"]}`, false, types.JSONRPCIntID(1)}, - {`{"jsonrpc": "2.0", "method": "c", "id": -1, "params": ["a", "10"]}`, false, types.JSONRPCIntID(-1)}, - {`{"jsonrpc": "2.0", "method": "c", "id": null, "params": ["a", "10"]}`, false, nil}, - - // bad id - {`{"jsonrpc": "2.0", "method": "c", "id": {}, "params": ["a", "10"]}`, true, nil}, - {`{"jsonrpc": "2.0", "method": "c", "id": [], "params": ["a", "10"]}`, true, nil}, - } - - for i, tt := range tests { - req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload)) - rec := httptest.NewRecorder() - mux.ServeHTTP(rec, req) - res := rec.Result() - // Always expecting back a JSONRPCResponse - //assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) - blob, err := ioutil.ReadAll(res.Body) - if err != nil { - t.Errorf("#%d: err reading body: %v", i, err) - continue - } - - recv := new(types.RPCResponse) - err = json.Unmarshal(blob, recv) - assert.Nil(t, err, "#%d: expecting successful parsing of an RPCResponse:\nblob: %s", i, blob) - if !tt.wantErr { - assert.NotEqual(t, recv, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i) - assert.Equal(t, tt.expectedId, recv.ID, "#%d: expected ID not matched in RPCResponse", i) - assert.Nil(t, recv.Error, "#%d: not expecting an error", i) - } else { - assert.True(t, recv.Error.Code < 0, "#%d: not expecting a positive JSONRPC code", i) - } - } -} - -func TestRPCNotification(t *testing.T) { - mux := testMux() - body := strings.NewReader(`{"jsonrpc": "2.0", "id": ""}`) - req, _ := http.NewRequest("POST", "http://localhost/", body) - rec := httptest.NewRecorder() - mux.ServeHTTP(rec, req) - res := rec.Result() - - // Always expecting back a JSONRPCResponse - require.True(t, statusOK(res.StatusCode), "should always return 2XX") - blob, err := ioutil.ReadAll(res.Body) - require.Nil(t, err, "reading from the body should not give back an error") - require.Equal(t, len(blob), 0, "a notification SHOULD NOT be responded to by the server") -} - -func TestUnknownRPCPath(t *testing.T) { - mux := testMux() - req, _ := http.NewRequest("GET", "http://localhost/unknownrpcpath", nil) - rec := httptest.NewRecorder() - mux.ServeHTTP(rec, req) - res := rec.Result() - - // Always expecting back a 404 error - require.Equal(t, http.StatusNotFound, res.StatusCode, "should always return 404") -} - -////////////////////////////////////////////////////////////////////////////// -// JSON-RPC over WEBSOCKETS - -func TestWebsocketManagerHandler(t *testing.T) { - s := newWSServer() - defer s.Close() - - // check upgrader works - d := websocket.Dialer{} - c, dialResp, err := d.Dial("ws://"+s.Listener.Addr().String()+"/websocket", nil) - require.NoError(t, err) - - if got, want := dialResp.StatusCode, http.StatusSwitchingProtocols; got != want { - t.Errorf("dialResp.StatusCode = %q, want %q", got, want) - } - - // check basic functionality works - req, err := types.MapToRequest(amino.NewCodec(), types.JSONRPCStringID("TestWebsocketManager"), "c", map[string]interface{}{"s": "a", "i": 10}) - require.NoError(t, err) - err = c.WriteJSON(req) - require.NoError(t, err) - - var resp types.RPCResponse - err = c.ReadJSON(&resp) - require.NoError(t, err) - require.Nil(t, resp.Error) -} - -func newWSServer() *httptest.Server { - funcMap := map[string]*rs.RPCFunc{ - "c": rs.NewWSRPCFunc(func(wsCtx types.WSRPCContext, s string, i int) (string, error) { return "foo", nil }, "s,i"), - } - wm := rs.NewWebsocketManager(funcMap, amino.NewCodec()) - wm.SetLogger(log.TestingLogger()) - - mux := http.NewServeMux() - mux.HandleFunc("/websocket", wm.WebsocketHandler) - - return httptest.NewServer(mux) -} diff --git a/rpc/lib/server/http_params.go b/rpc/lib/server/http_params.go deleted file mode 100644 index 3c948c0ba..000000000 --- a/rpc/lib/server/http_params.go +++ /dev/null @@ -1,91 +0,0 @@ -package rpcserver - -import ( - "encoding/hex" - "net/http" - "regexp" - "strconv" - - "github.com/pkg/errors" -) - -var ( - // Parts of regular expressions - atom = "[A-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" - dotAtom = atom + `(?:\.` + atom + `)*` - domain = `[A-Z0-9.-]+\.[A-Z]{2,4}` - - RE_INT = regexp.MustCompile(`^-?[0-9]+$`) - RE_HEX = regexp.MustCompile(`^(?i)[a-f0-9]+$`) - RE_EMAIL = regexp.MustCompile(`^(?i)(` + dotAtom + `)@(` + dotAtom + `)$`) - RE_ADDRESS = regexp.MustCompile(`^(?i)[a-z0-9]{25,34}$`) - RE_HOST = regexp.MustCompile(`^(?i)(` + domain + `)$`) - - //RE_ID12 = regexp.MustCompile(`^[a-zA-Z0-9]{12}$`) -) - -func GetParam(r *http.Request, param string) string { - s := r.URL.Query().Get(param) - if s == "" { - s = r.FormValue(param) - } - return s -} - -func GetParamByteSlice(r *http.Request, param string) ([]byte, error) { - s := GetParam(r, param) - return hex.DecodeString(s) -} - -func GetParamInt64(r *http.Request, param string) (int64, error) { - s := GetParam(r, param) - i, err := strconv.ParseInt(s, 10, 64) - if err != nil { - return 0, errors.Errorf(param, err.Error()) - } - return i, nil -} - -func GetParamInt32(r *http.Request, param string) (int32, error) { - s := GetParam(r, param) - i, err := strconv.ParseInt(s, 10, 32) - if err != nil { - return 0, errors.Errorf(param, err.Error()) - } - return int32(i), nil -} - -func GetParamUint64(r *http.Request, param string) (uint64, error) { - s := GetParam(r, param) - i, err := strconv.ParseUint(s, 10, 64) - if err != nil { - return 0, errors.Errorf(param, err.Error()) - } - return i, nil -} - -func GetParamUint(r *http.Request, param string) (uint, error) { - s := GetParam(r, param) - i, err := strconv.ParseUint(s, 10, 64) - if err != nil { - return 0, errors.Errorf(param, err.Error()) - } - return uint(i), nil -} - -func GetParamRegexp(r *http.Request, param string, re *regexp.Regexp) (string, error) { - s := GetParam(r, param) - if !re.MatchString(s) { - return "", errors.Errorf(param, "Did not match regular expression %v", re.String()) - } - return s, nil -} - -func GetParamFloat64(r *http.Request, param string) (float64, error) { - s := GetParam(r, param) - f, err := strconv.ParseFloat(s, 64) - if err != nil { - return 0, errors.Errorf(param, err.Error()) - } - return f, nil -} diff --git a/rpc/lib/server/http_server.go b/rpc/lib/server/http_server.go deleted file mode 100644 index 2b9c1c1e4..000000000 --- a/rpc/lib/server/http_server.go +++ /dev/null @@ -1,213 +0,0 @@ -// Commons for HTTP handling -package rpcserver - -import ( - "bufio" - "encoding/json" - "fmt" - "net" - "net/http" - "runtime/debug" - "strings" - "time" - - "github.com/pkg/errors" - "golang.org/x/net/netutil" - - types "github.com/MinterTeam/minter-go-node/rpc/lib/types" - "github.com/tendermint/tendermint/libs/log" -) - -// Config is an RPC server configuration. -type Config struct { - MaxOpenConnections int -} - -const ( - // maxBodyBytes controls the maximum number of bytes the - // server will read parsing the request body. - maxBodyBytes = int64(1000000) // 1MB - - // same as the net/http default - maxHeaderBytes = 1 << 20 - - // Timeouts for reading/writing to the http connection. - // Public so handlers can read them - - // /broadcast_tx_commit has it's own timeout, which should - // be less than the WriteTimeout here. - // TODO: use a config instead. - ReadTimeout = 3 * time.Second - WriteTimeout = 20 * time.Second -) - -// StartHTTPServer takes a listener and starts an HTTP server with the given handler. -// It wraps handler with RecoverAndLogHandler. -// NOTE: This function blocks - you may want to call it in a go-routine. -func StartHTTPServer(listener net.Listener, handler http.Handler, logger log.Logger) error { - logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr())) - s := &http.Server{ - Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), - ReadTimeout: ReadTimeout, - WriteTimeout: WriteTimeout, - MaxHeaderBytes: maxHeaderBytes, - } - err := s.Serve(listener) - logger.Info("RPC HTTP server stopped", "err", err) - return err -} - -// StartHTTPAndTLSServer takes a listener and starts an HTTPS server with the given handler. -// It wraps handler with RecoverAndLogHandler. -// NOTE: This function blocks - you may want to call it in a go-routine. -func StartHTTPAndTLSServer( - listener net.Listener, - handler http.Handler, - certFile, keyFile string, - logger log.Logger, -) error { - logger.Info(fmt.Sprintf("Starting RPC HTTPS server on %s (cert: %q, key: %q)", - listener.Addr(), certFile, keyFile)) - s := &http.Server{ - Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), - ReadTimeout: ReadTimeout, - WriteTimeout: WriteTimeout, - MaxHeaderBytes: maxHeaderBytes, - } - err := s.ServeTLS(listener, certFile, keyFile) - - logger.Error("RPC HTTPS server stopped", "err", err) - return err -} - -func WriteRPCResponseHTTPError( - w http.ResponseWriter, - httpCode int, - res types.RPCResponse, -) { - jsonBytes, err := json.Marshal(res) - if err != nil { - panic(err) - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(httpCode) - w.Write(jsonBytes) // nolint: errcheck, gas -} - -func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { - jsonBytes, err := json.Marshal(res) - if err != nil { - panic(err) - } - w.Header().Set("Content-Type", "application/json") - status := 200 - if res.Error != nil && res.Error.Code != 0 { - if res.Error.Code > 0 { - status = res.Error.Code - } else { - status = 500 - } - } - w.WriteHeader(status) - w.Write(jsonBytes) // nolint: errcheck, gas -} - -//----------------------------------------------------------------------------- - -// Wraps an HTTP handler, adding error logging. -// If the inner function panics, the outer function recovers, logs, sends an -// HTTP 500 error response. -func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Wrap the ResponseWriter to remember the status - rww := &ResponseWriterWrapper{-1, w} - begin := time.Now() - - rww.Header().Set("X-Server-Time", fmt.Sprintf("%v", begin.Unix())) - rww.Header().Set("Deprecation", "version=\"v1\"") - // rww.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"successor-version\"","")) - // rww.Header().Set("Link", fmt.Sprintf("<%s>; rel=\"deprecation\"","")) - // rww.Header().Set("Sunset", time.Unix(0,0).Format(time.RFC1123)) - - defer func() { - // Send a 500 error if a panic happens during a handler. - // Without this, Chrome & Firefox were retrying aborted ajax requests, - // at least to my localhost. - if e := recover(); e != nil { - - // If RPCResponse - if res, ok := e.(types.RPCResponse); ok { - WriteRPCResponseHTTP(rww, res) - } else { - // For the rest, - logger.Error( - "Panic in RPC HTTP handler", "err", e, "stack", - string(debug.Stack()), - ) - WriteRPCResponseHTTPError(rww, http.StatusInternalServerError, types.RPCInternalError(types.JSONRPCStringID(""), e.(error))) - } - } - - // Finally, log. - durationMS := time.Since(begin).Nanoseconds() / 1000000 - if rww.Status == -1 { - rww.Status = 200 - } - logger.Info("Served RPC HTTP response", - "method", r.Method, "url", r.URL, - "status", rww.Status, "duration", durationMS, - "remoteAddr", r.RemoteAddr, - ) - }() - - handler.ServeHTTP(rww, r) - }) -} - -// Remember the status for logging -type ResponseWriterWrapper struct { - Status int - http.ResponseWriter -} - -func (w *ResponseWriterWrapper) WriteHeader(status int) { - w.Status = status - w.ResponseWriter.WriteHeader(status) -} - -// implements http.Hijacker -func (w *ResponseWriterWrapper) Hijack() (net.Conn, *bufio.ReadWriter, error) { - return w.ResponseWriter.(http.Hijacker).Hijack() -} - -type maxBytesHandler struct { - h http.Handler - n int64 -} - -func (h maxBytesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - r.Body = http.MaxBytesReader(w, r.Body, h.n) - h.h.ServeHTTP(w, r) -} - -// Listen starts a new net.Listener on the given address. -// It returns an error if the address is invalid or the call to Listen() fails. -func Listen(addr string, config Config) (listener net.Listener, err error) { - parts := strings.SplitN(addr, "://", 2) - if len(parts) != 2 { - return nil, errors.Errorf( - "Invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)", - addr, - ) - } - proto, addr := parts[0], parts[1] - listener, err = net.Listen(proto, addr) - if err != nil { - return nil, errors.Errorf("Failed to listen on %v: %v", addr, err) - } - if config.MaxOpenConnections > 0 { - listener = netutil.LimitListener(listener, config.MaxOpenConnections) - } - - return listener, nil -} diff --git a/rpc/lib/server/http_server_test.go b/rpc/lib/server/http_server_test.go deleted file mode 100644 index 6b852afae..000000000 --- a/rpc/lib/server/http_server_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package rpcserver - -import ( - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "sync" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/tendermint/tendermint/libs/log" -) - -func TestMaxOpenConnections(t *testing.T) { - const max = 5 // max simultaneous connections - - // Start the server. - var open int32 - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if n := atomic.AddInt32(&open, 1); n > int32(max) { - t.Errorf("%d open connections, want <= %d", n, max) - } - defer atomic.AddInt32(&open, -1) - time.Sleep(10 * time.Millisecond) - fmt.Fprint(w, "some body") - }) - l, err := Listen("tcp://127.0.0.1:0", Config{MaxOpenConnections: max}) - require.NoError(t, err) - defer l.Close() - go StartHTTPServer(l, mux, log.TestingLogger()) - - // Make N GET calls to the server. - attempts := max * 2 - var wg sync.WaitGroup - var failed int32 - for i := 0; i < attempts; i++ { - wg.Add(1) - go func() { - defer wg.Done() - c := http.Client{Timeout: 3 * time.Second} - r, err := c.Get("http://" + l.Addr().String()) - if err != nil { - t.Log(err) - atomic.AddInt32(&failed, 1) - return - } - defer r.Body.Close() - io.Copy(ioutil.Discard, r.Body) - }() - } - wg.Wait() - - // We expect some Gets to fail as the server's accept queue is filled, - // but most should succeed. - if int(failed) >= attempts/2 { - t.Errorf("%d requests failed within %d attempts", failed, attempts) - } -} - -func TestStartHTTPAndTLSServer(t *testing.T) { - // set up fixtures - listenerAddr := "tcp://0.0.0.0:0" - listener, err := Listen(listenerAddr, Config{MaxOpenConnections: 1}) - require.NoError(t, err) - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) - - // test failure - err = StartHTTPAndTLSServer(listener, mux, "", "", log.TestingLogger()) - require.IsType(t, (*os.PathError)(nil), err) - - // TODO: test that starting the server can actually work -} diff --git a/rpc/lib/server/parse_test.go b/rpc/lib/server/parse_test.go deleted file mode 100644 index 076fa943e..000000000 --- a/rpc/lib/server/parse_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package rpcserver - -import ( - "encoding/json" - "fmt" - "github.com/tendermint/tendermint/libs/bytes" - "net/http" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - amino "github.com/tendermint/go-amino" -) - -func TestParseJSONMap(t *testing.T) { - assert := assert.New(t) - - input := []byte(`{"value":"1234","height":22}`) - - // naive is float,string - var p1 map[string]interface{} - err := json.Unmarshal(input, &p1) - if assert.Nil(err) { - h, ok := p1["height"].(float64) - if assert.True(ok, "%#v", p1["height"]) { - assert.EqualValues(22, h) - } - v, ok := p1["value"].(string) - if assert.True(ok, "%#v", p1["value"]) { - assert.EqualValues("1234", v) - } - } - - // preloading map with values doesn't help - tmp := 0 - p2 := map[string]interface{}{ - "value": &bytes.HexBytes{}, - "height": &tmp, - } - err = json.Unmarshal(input, &p2) - if assert.Nil(err) { - h, ok := p2["height"].(float64) - if assert.True(ok, "%#v", p2["height"]) { - assert.EqualValues(22, h) - } - v, ok := p2["value"].(string) - if assert.True(ok, "%#v", p2["value"]) { - assert.EqualValues("1234", v) - } - } - - // preload here with *pointers* to the desired types - // struct has unknown types, but hard-coded keys - tmp = 0 - p3 := struct { - Value interface{} `json:"value"` - Height interface{} `json:"height"` - }{ - Height: &tmp, - Value: &bytes.HexBytes{}, - } - err = json.Unmarshal(input, &p3) - if assert.Nil(err) { - h, ok := p3.Height.(*int) - if assert.True(ok, "%#v", p3.Height) { - assert.Equal(22, *h) - } - v, ok := p3.Value.(*bytes.HexBytes) - if assert.True(ok, "%#v", p3.Value) { - assert.EqualValues([]byte{0x12, 0x34}, *v) - } - } - - // simplest solution, but hard-coded - p4 := struct { - Value bytes.HexBytes `json:"value"` - Height int `json:"height"` - }{} - err = json.Unmarshal(input, &p4) - if assert.Nil(err) { - assert.EqualValues(22, p4.Height) - assert.EqualValues([]byte{0x12, 0x34}, p4.Value) - } - - // so, let's use this trick... - // dynamic keys on map, and we can deserialize to the desired types - var p5 map[string]*json.RawMessage - err = json.Unmarshal(input, &p5) - if assert.Nil(err) { - var h int - err = json.Unmarshal(*p5["height"], &h) - if assert.Nil(err) { - assert.Equal(22, h) - } - - var v bytes.HexBytes - err = json.Unmarshal(*p5["value"], &v) - if assert.Nil(err) { - assert.Equal(bytes.HexBytes{0x12, 0x34}, v) - } - } -} - -func TestParseJSONArray(t *testing.T) { - assert := assert.New(t) - - input := []byte(`["1234",22]`) - - // naive is float,string - var p1 []interface{} - err := json.Unmarshal(input, &p1) - if assert.Nil(err) { - v, ok := p1[0].(string) - if assert.True(ok, "%#v", p1[0]) { - assert.EqualValues("1234", v) - } - h, ok := p1[1].(float64) - if assert.True(ok, "%#v", p1[1]) { - assert.EqualValues(22, h) - } - } - - // preloading map with values helps here (unlike map - p2 above) - tmp := 0 - p2 := []interface{}{&bytes.HexBytes{}, &tmp} - err = json.Unmarshal(input, &p2) - if assert.Nil(err) { - v, ok := p2[0].(*bytes.HexBytes) - if assert.True(ok, "%#v", p2[0]) { - assert.EqualValues([]byte{0x12, 0x34}, *v) - } - h, ok := p2[1].(*int) - if assert.True(ok, "%#v", p2[1]) { - assert.EqualValues(22, *h) - } - } -} - -func TestParseJSONRPC(t *testing.T) { - assert := assert.New(t) - - demo := func(height int, name string) {} - call := NewRPCFunc(demo, "height,name") - cdc := amino.NewCodec() - - cases := []struct { - raw string - height int64 - name string - fail bool - }{ - // should parse - {`["7", "flew"]`, 7, "flew", false}, - {`{"name": "john", "height": "22"}`, 22, "john", false}, - // defaults - {`{"name": "solo", "unused": "stuff"}`, 0, "solo", false}, - // should fail - wrong types/length - {`["flew", 7]`, 0, "", true}, - {`[7,"flew",100]`, 0, "", true}, - {`{"name": -12, "height": "fred"}`, 0, "", true}, - } - for idx, tc := range cases { - i := strconv.Itoa(idx) - data := []byte(tc.raw) - vals, err := jsonParamsToArgs(call, cdc, data, 0) - if tc.fail { - assert.NotNil(err, i) - } else { - assert.Nil(err, "%s: %+v", i, err) - if assert.Equal(2, len(vals), i) { - assert.Equal(tc.height, vals[0].Int(), i) - assert.Equal(tc.name, vals[1].String(), i) - } - } - - } -} - -func TestParseURI(t *testing.T) { - - demo := func(height int, name string) {} - call := NewRPCFunc(demo, "height,name") - cdc := amino.NewCodec() - - cases := []struct { - raw []string - height int64 - name string - fail bool - }{ - // can parse numbers unquoted and strings quoted - {[]string{"7", `"flew"`}, 7, "flew", false}, - {[]string{"22", `"john"`}, 22, "john", false}, - {[]string{"-10", `"bob"`}, -10, "bob", false}, - // can parse numbers quoted, too - {[]string{`"7"`, `"flew"`}, 7, "flew", false}, - {[]string{`"-10"`, `"bob"`}, -10, "bob", false}, - } - for idx, tc := range cases { - i := strconv.Itoa(idx) - // data := []byte(tc.raw) - url := fmt.Sprintf( - "test.com/method?height=%v&name=%v", - tc.raw[0], tc.raw[1]) - req, err := http.NewRequest("GET", url, nil) - assert.NoError(t, err) - vals, err := httpParamsToArgs(call, cdc, req) - if tc.fail { - assert.NotNil(t, err, i) - } else { - assert.Nil(t, err, "%s: %+v", i, err) - if assert.Equal(t, 2, len(vals), i) { - assert.Equal(t, tc.height, vals[0].Int(), i) - assert.Equal(t, tc.name, vals[1].String(), i) - } - } - - } -} diff --git a/rpc/lib/types/types.go b/rpc/lib/types/types.go deleted file mode 100644 index c34322bd7..000000000 --- a/rpc/lib/types/types.go +++ /dev/null @@ -1,296 +0,0 @@ -package rpctypes - -import ( - "context" - "encoding/json" - "fmt" - "github.com/pkg/errors" - "reflect" - - "github.com/tendermint/go-amino" - - tmpubsub "github.com/tendermint/tendermint/libs/pubsub" -) - -// a wrapper to emulate a sum type: jsonrpcid = string | int -// TODO: refactor when Go 2.0 arrives https://github.com/golang/go/issues/19412 -type jsonrpcid interface { - isJSONRPCID() -} - -// JSONRPCStringID a wrapper for JSON-RPC string IDs -type JSONRPCStringID string - -func (JSONRPCStringID) isJSONRPCID() {} - -// JSONRPCIntID a wrapper for JSON-RPC integer IDs -type JSONRPCIntID int - -func (JSONRPCIntID) isJSONRPCID() {} - -func idFromInterface(idInterface interface{}) (jsonrpcid, error) { - switch id := idInterface.(type) { - case string: - return JSONRPCStringID(id), nil - case float64: - // json.Unmarshal uses float64 for all numbers - // (https://golang.org/pkg/encoding/json/#Unmarshal), - // but the JSONRPC2.0 spec says the id SHOULD NOT contain - // decimals - so we truncate the decimals here. - return JSONRPCIntID(int(id)), nil - default: - typ := reflect.TypeOf(id) - return nil, fmt.Errorf("JSON-RPC ID (%v) is of unknown type (%v)", id, typ) - } -} - -//---------------------------------------- -// REQUEST - -type RPCRequest struct { - JSONRPC string `json:"jsonrpc"` - ID jsonrpcid `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{} -} - -// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int -func (request *RPCRequest) UnmarshalJSON(data []byte) error { - unsafeReq := &struct { - JSONRPC string `json:"jsonrpc"` - ID interface{} `json:"id"` - Method string `json:"method"` - Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{} - }{} - err := json.Unmarshal(data, &unsafeReq) - if err != nil { - return err - } - request.JSONRPC = unsafeReq.JSONRPC - request.Method = unsafeReq.Method - request.Params = unsafeReq.Params - if unsafeReq.ID == nil { - return nil - } - id, err := idFromInterface(unsafeReq.ID) - if err != nil { - return err - } - request.ID = id - return nil -} - -func NewRPCRequest(id jsonrpcid, method string, params json.RawMessage) RPCRequest { - return RPCRequest{ - JSONRPC: "2.0", - ID: id, - Method: method, - Params: params, - } -} - -func (req RPCRequest) String() string { - return fmt.Sprintf("[%s %s]", req.ID, req.Method) -} - -func MapToRequest(cdc *amino.Codec, id jsonrpcid, method string, params map[string]interface{}) (RPCRequest, error) { - var params_ = make(map[string]json.RawMessage, len(params)) - for name, value := range params { - valueJSON, err := cdc.MarshalJSON(value) - if err != nil { - return RPCRequest{}, err - } - params_[name] = valueJSON - } - payload, err := json.Marshal(params_) // NOTE: Amino doesn't handle maps yet. - if err != nil { - return RPCRequest{}, err - } - request := NewRPCRequest(id, method, payload) - return request, nil -} - -func ArrayToRequest(cdc *amino.Codec, id jsonrpcid, method string, params []interface{}) (RPCRequest, error) { - var params_ = make([]json.RawMessage, len(params)) - for i, value := range params { - valueJSON, err := cdc.MarshalJSON(value) - if err != nil { - return RPCRequest{}, err - } - params_[i] = valueJSON - } - payload, err := json.Marshal(params_) // NOTE: Amino doesn't handle maps yet. - if err != nil { - return RPCRequest{}, err - } - request := NewRPCRequest(id, method, payload) - return request, nil -} - -//---------------------------------------- -// RESPONSE - -type TxResult struct { - Code uint32 `json:"code"` - Log string `json:"log"` -} - -type RPCError struct { - Code int `json:"code"` - Message string `json:"message"` - Data string `json:"data,omitempty"` - - TxResult *TxResult `json:"tx_result,omitempty"` -} - -func (err RPCError) Error() string { - const baseFormat = "RPC error %v - %s" - if err.Data != "" { - return fmt.Sprintf(baseFormat+": %s", err.Code, err.Message, err.Data) - } - return fmt.Sprintf(baseFormat, err.Code, err.Message) -} - -type RPCResponse struct { - JSONRPC string `json:"jsonrpc"` - ID jsonrpcid `json:"id"` - Result json.RawMessage `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` -} - -// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int -func (response *RPCResponse) UnmarshalJSON(data []byte) error { - unsafeResp := &struct { - JSONRPC string `json:"jsonrpc"` - ID interface{} `json:"id"` - Result json.RawMessage `json:"result,omitempty"` - Error *RPCError `json:"error,omitempty"` - }{} - err := json.Unmarshal(data, &unsafeResp) - if err != nil { - return err - } - response.JSONRPC = unsafeResp.JSONRPC - response.Error = unsafeResp.Error - response.Result = unsafeResp.Result - if unsafeResp.ID == nil { - return nil - } - id, err := idFromInterface(unsafeResp.ID) - if err != nil { - return err - } - response.ID = id - return nil -} - -func NewRPCSuccessResponse(cdc *amino.Codec, id jsonrpcid, res interface{}) RPCResponse { - var rawMsg json.RawMessage - - if res != nil { - var js []byte - js, err := cdc.MarshalJSON(res) - if err != nil { - return RPCInternalError(id, errors.Wrap(err, "Error marshalling response")) - } - rawMsg = json.RawMessage(js) - } - - return RPCResponse{JSONRPC: "2.0", ID: id, Result: rawMsg} -} - -func NewRPCErrorResponse(id jsonrpcid, code int, msg string, data string) RPCResponse { - return RPCResponse{ - JSONRPC: "2.0", - ID: id, - Error: &RPCError{Code: code, Message: msg, Data: data}, - } -} - -func (resp RPCResponse) String() string { - if resp.Error == nil { - return fmt.Sprintf("[%s %v]", resp.ID, resp.Result) - } - return fmt.Sprintf("[%s %s]", resp.ID, resp.Error) -} - -func RPCParseError(id jsonrpcid, err error) RPCResponse { - return NewRPCErrorResponse(id, -32700, "Parse error. Invalid JSON", err.Error()) -} - -func RPCInvalidRequestError(id jsonrpcid, err error) RPCResponse { - return NewRPCErrorResponse(id, -32600, "Invalid Request", err.Error()) -} - -func RPCMethodNotFoundError(id jsonrpcid) RPCResponse { - return NewRPCErrorResponse(id, -32601, "Method not found", "") -} - -func RPCInvalidParamsError(id jsonrpcid, err error) RPCResponse { - return NewRPCErrorResponse(id, -32602, "Invalid params", err.Error()) -} - -func RPCInternalError(id jsonrpcid, err error) RPCResponse { - if txError, ok := err.(TxError); ok { - return RPCResponse{ - JSONRPC: "2.0", - ID: id, - Error: &RPCError{ - Code: 412, // HTTP Code: Precondition Failed - Message: "Check tx error", - Data: "", - TxResult: &TxResult{ - Code: txError.Code, - Log: txError.Log, - }, - }, - } - } - - if rpcError, ok := err.(RPCError); ok { - return RPCResponse{ - JSONRPC: "2.0", - ID: id, - Error: &rpcError, - } - } - - return NewRPCErrorResponse(id, -32603, "Internal error", err.Error()) -} - -func RPCServerError(id jsonrpcid, err error) RPCResponse { - return NewRPCErrorResponse(id, -32000, "Server error", err.Error()) -} - -//---------------------------------------- - -// *wsConnection implements this interface. -type WSRPCConnection interface { - GetRemoteAddr() string - WriteRPCResponse(resp RPCResponse) - TryWriteRPCResponse(resp RPCResponse) bool - GetEventSubscriber() EventSubscriber - Codec() *amino.Codec -} - -// EventSubscriber mirros tendermint/tendermint/types.EventBusSubscriber -type EventSubscriber interface { - Subscribe(ctx context.Context, subscriber string, query tmpubsub.Query, out chan<- interface{}) error - Unsubscribe(ctx context.Context, subscriber string, query tmpubsub.Query) error - UnsubscribeAll(ctx context.Context, subscriber string) error -} - -// websocket-only RPCFuncs take this as the first parameter. -type WSRPCContext struct { - Request RPCRequest - WSRPCConnection -} - -type TxError struct { - Code uint32 `json:"code"` - Log string `json:"log"` -} - -func (err TxError) Error() string { - return err.Log -} diff --git a/rpc/lib/types/types_test.go b/rpc/lib/types/types_test.go deleted file mode 100644 index 3e8851326..000000000 --- a/rpc/lib/types/types_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package rpctypes - -import ( - "encoding/json" - "testing" - - "fmt" - - "github.com/pkg/errors" - "github.com/stretchr/testify/assert" - "github.com/tendermint/go-amino" -) - -type SampleResult struct { - Value string -} - -type responseTest struct { - id jsonrpcid - expected string -} - -var responseTests = []responseTest{ - {JSONRPCStringID("1"), `"1"`}, - {JSONRPCStringID("alphabet"), `"alphabet"`}, - {JSONRPCStringID(""), `""`}, - {JSONRPCStringID("àáâ"), `"àáâ"`}, - {JSONRPCIntID(-1), "-1"}, - {JSONRPCIntID(0), "0"}, - {JSONRPCIntID(1), "1"}, - {JSONRPCIntID(100), "100"}, -} - -func TestResponses(t *testing.T) { - assert := assert.New(t) - cdc := amino.NewCodec() - for _, tt := range responseTests { - jsonid := tt.id - a := NewRPCSuccessResponse(cdc, jsonid, &SampleResult{"hello"}) - b, _ := json.Marshal(a) - s := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected) - assert.Equal(string(s), string(b)) - - d := RPCParseError(jsonid, errors.New("Hello world")) - e, _ := json.Marshal(d) - f := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}`, tt.expected) - assert.Equal(string(f), string(e)) - - g := RPCMethodNotFoundError(jsonid) - h, _ := json.Marshal(g) - i := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32601,"message":"Method not found"}}`, tt.expected) - assert.Equal(string(h), string(i)) - } -} - -func TestUnmarshallResponses(t *testing.T) { - assert := assert.New(t) - cdc := amino.NewCodec() - for _, tt := range responseTests { - response := &RPCResponse{} - err := json.Unmarshal([]byte(fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected)), response) - assert.Nil(err) - a := NewRPCSuccessResponse(cdc, tt.id, &SampleResult{"hello"}) - assert.Equal(*response, a) - } - response := &RPCResponse{} - err := json.Unmarshal([]byte(`{"jsonrpc":"2.0","id":true,"result":{"Value":"hello"}}`), response) - assert.NotNil(err) -} - -func TestRPCError(t *testing.T) { - assert.Equal(t, "RPC error 12 - Badness: One worse than a code 11", - fmt.Sprintf("%v", &RPCError{ - Code: 12, - Message: "Badness", - Data: "One worse than a code 11", - })) - - assert.Equal(t, "RPC error 12 - Badness", - fmt.Sprintf("%v", &RPCError{ - Code: 12, - Message: "Badness", - })) -} diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go deleted file mode 100644 index 40896e0a2..000000000 --- a/rpc/test/helpers.go +++ /dev/null @@ -1,139 +0,0 @@ -package rpctest - -import ( - "context" - "fmt" - "github.com/tendermint/tendermint/libs/rand" - "os" - "path/filepath" - "strings" - "time" - - "github.com/tendermint/tendermint/libs/log" - - abci "github.com/tendermint/tendermint/abci/types" - - cfg "github.com/tendermint/tendermint/config" - nm "github.com/tendermint/tendermint/node" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/proxy" - ctypes "github.com/tendermint/tendermint/rpc/core/types" - core_grpc "github.com/tendermint/tendermint/rpc/grpc" - rpcclient "github.com/tendermint/tendermint/rpc/jsonrpc/client" -) - -var globalConfig *cfg.Config - -func waitForRPC() { - laddr := GetConfig().RPC.ListenAddress - client, _ := rpcclient.New(laddr) - ctypes.RegisterAmino(client.Codec()) - result := new(ctypes.ResultStatus) - for { - _, err := client.Call("status", map[string]interface{}{}, result) - if err == nil { - return - } - - fmt.Println("error", err) - time.Sleep(time.Millisecond) - } -} - -func waitForGRPC() { - client := GetGRPCClient() - for { - _, err := client.Ping(context.Background(), &core_grpc.RequestPing{}) - if err == nil { - return - } - } -} - -// f**ing long, but unique for each test -func makePathname() string { - // get path - p, err := os.Getwd() - if err != nil { - panic(err) - } - // fmt.Println(p) - sep := string(filepath.Separator) - return strings.Replace(p, sep, "_", -1) -} - -func randPort() int { - return int(rand.Uint16()/2 + 10000) -} - -func makeAddrs() (string, string, string) { - start := randPort() - return fmt.Sprintf("tcp://0.0.0.0:%d", start), - fmt.Sprintf("tcp://0.0.0.0:%d", start+1), - fmt.Sprintf("tcp://0.0.0.0:%d", start+2) -} - -// GetConfig returns a config for the test cases as a singleton -func GetConfig() *cfg.Config { - if globalConfig == nil { - pathname := makePathname() - globalConfig = cfg.ResetTestRoot(pathname) - - // and we use random ports to run in parallel - tm, rpc, grpc := makeAddrs() - globalConfig.P2P.ListenAddress = tm - globalConfig.RPC.ListenAddress = rpc - globalConfig.RPC.CORSAllowedOrigins = []string{"https://tendermint.com/"} - globalConfig.RPC.GRPCListenAddress = grpc - globalConfig.TxIndex.IndexKeys = "app.creator,tx.height" // see kvstore application - } - return globalConfig -} - -func GetGRPCClient() core_grpc.BroadcastAPIClient { - grpcAddr := globalConfig.RPC.GRPCListenAddress - return core_grpc.StartGRPCClient(grpcAddr) -} - -// StartTendermint starts a test tendermint server in a go routine and returns when it is initialized -func StartTendermint(app abci.Application) *nm.Node { - node := NewTendermint(app) - err := node.Start() - if err != nil { - panic(err) - } - - // wait for rpc - waitForRPC() - waitForGRPC() - - fmt.Println("Tendermint running!") - - return node -} - -// NewTendermint creates a new tendermint server and sleeps forever -func NewTendermint(app abci.Application) *nm.Node { - // Create & start node - config := GetConfig() - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - logger = log.NewFilter(logger, log.AllowError()) - pvKeyFile := config.PrivValidatorKeyFile() - pvKeyStateFile := config.PrivValidatorStateFile() - pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) - papp := proxy.NewLocalClientCreator(app) - nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) - if err != nil { - panic(err) - } - node, err := nm.NewNode(config, pv, nodeKey, papp, - nm.DefaultGenesisDocProviderFunc(config), - nm.DefaultDBProvider, - nm.DefaultMetricsProvider(config.Instrumentation), - logger) - if err != nil { - panic(err) - } - return node -} diff --git a/tests/byz_test.go b/tests/byz_test.go index 58b3b1462..102985cbe 100644 --- a/tests/byz_test.go +++ b/tests/byz_test.go @@ -5,6 +5,8 @@ import ( "github.com/MinterTeam/minter-go-node/helpers" tmTypes "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/ed25519" + tmTypes1 "github.com/tendermint/tendermint/proto/tendermint/types" + "github.com/tendermint/tendermint/proto/tendermint/version" "math/big" "testing" "time" @@ -40,20 +42,19 @@ func TestBlockchain_ByzantineValidators(t *testing.T) { Status: 2, }) - var pubkey ed25519.PubKeyEd25519 - copy(pubkey[:], types.Pubkey{1}.Bytes()) var address types.TmAddress - copy(address[:], pubkey.Address().Bytes()) + bytes := [32]byte{1} + copy(address[:], ed25519.PubKey(bytes[:]).Address().Bytes()) app := CreateApp(state) // create application req := tmTypes.RequestBeginBlock{ Hash: nil, - Header: tmTypes.Header{ - Version: tmTypes.Version{}, + Header: tmTypes1.Header{ + Version: version.Consensus{}, ChainID: "", Height: 1, Time: time.Time{}, - LastBlockId: tmTypes.BlockID{}, + LastBlockId: tmTypes1.BlockID{}, LastCommitHash: nil, DataHash: nil, ValidatorsHash: nil, @@ -70,7 +71,7 @@ func TestBlockchain_ByzantineValidators(t *testing.T) { }, ByzantineValidators: []tmTypes.Evidence{ { - Type: "", + Type: tmTypes.EvidenceType_DUPLICATE_VOTE, Validator: tmTypes.Validator{ Address: address[:], Power: 10, diff --git a/tests/helpers.go b/tests/helpers_test.go similarity index 88% rename from tests/helpers.go rename to tests/helpers_test.go index 59007e0e8..7e1c9eb2d 100644 --- a/tests/helpers.go +++ b/tests/helpers_test.go @@ -11,32 +11,27 @@ import ( "github.com/MinterTeam/minter-go-node/rlp" "github.com/tendermint/go-amino" tmTypes "github.com/tendermint/tendermint/abci/types" - "os" - "path/filepath" + tmTypes1 "github.com/tendermint/tendermint/proto/tendermint/types" + "github.com/tendermint/tendermint/proto/tendermint/version" "time" ) // CreateApp creates and returns new Blockchain instance -// Recreates $HOME/.minter_test dir func CreateApp(state types.AppState) *minter.Blockchain { - utils.MinterHome = os.ExpandEnv(filepath.Join("$HOME", ".minter_test")) - _ = os.RemoveAll(utils.MinterHome) - jsonState, err := amino.MarshalJSON(state) if err != nil { panic(err) } - cfg := config.GetConfig() - app := minter.NewMinterBlockchain(cfg) + storage := utils.NewStorage("", "") + cfg := config.GetConfig(storage.GetMinterHome()) + cfg.DBBackend = "memdb" + app := minter.NewMinterBlockchain(storage, cfg, nil) app.InitChain(tmTypes.RequestInitChain{ Time: time.Now(), ChainId: "test", Validators: []tmTypes.ValidatorUpdate{ - { - PubKey: tmTypes.PubKey{}, - Power: 1, - }, + tmTypes.Ed25519ValidatorUpdate([]byte{}, 1), }, AppStateBytes: jsonState, }) @@ -53,12 +48,12 @@ func SendCommit(app *minter.Blockchain) tmTypes.ResponseCommit { func SendBeginBlock(app *minter.Blockchain) tmTypes.ResponseBeginBlock { return app.BeginBlock(tmTypes.RequestBeginBlock{ Hash: nil, - Header: tmTypes.Header{ - Version: tmTypes.Version{}, + Header: tmTypes1.Header{ + Version: version.Consensus{}, ChainID: "", Height: 1, Time: time.Time{}, - LastBlockId: tmTypes.BlockID{}, + LastBlockId: tmTypes1.BlockID{}, LastCommitHash: nil, DataHash: nil, ValidatorsHash: nil, @@ -136,7 +131,6 @@ func CreateAddress() (types.Address, *ecdsa.PrivateKey) { func DefaultAppState() types.AppState { return types.AppState{ Note: "", - StartHeight: 1, Validators: nil, Candidates: nil, BlockListCandidates: nil, diff --git a/tests/send_test.go b/tests/send_test.go index 3726475a4..cadefbc9c 100644 --- a/tests/send_test.go +++ b/tests/send_test.go @@ -20,7 +20,7 @@ func TestSend(t *testing.T) { Balance: []types.Balance{ { Coin: uint64(types.GetBaseCoinID()), - Value: helpers.BipToPip(big.NewInt(1)).String(), + Value: helpers.StringToBigInt("100000000000000000000").String(), }, }, Nonce: 0, @@ -60,8 +60,8 @@ func TestSend(t *testing.T) { // check sender's balance { balance := app.CurrentState().Accounts().GetBalance(address, types.GetBaseCoinID()) - if balance.String() != "989999999999999999" { - t.Fatalf("Recipient balance is not correct. Expected %s, got %s", "989999999999999999", balance) + if balance.String() != "98999999999999999999" { + t.Fatalf("Recipient balance is not correct. Expected %s, got %s", "98999999999999999999", balance) } } } diff --git a/tree/tree.go b/tree/tree.go index d424888a5..da7749620 100644 --- a/tree/tree.go +++ b/tree/tree.go @@ -1,52 +1,76 @@ package tree import ( - "github.com/tendermint/iavl" + "github.com/cosmos/iavl" dbm "github.com/tendermint/tm-db" "sync" ) -// ReadOnlyTree used for CheckState: API and CheckTx calls. Immutable. -type ReadOnlyTree interface { - Get(key []byte) (index int64, value []byte) - Version() int64 - Hash() []byte - Iterate(fn func(key []byte, value []byte) bool) (stopped bool) - AvailableVersions() []int +type saver interface { + Commit(db *iavl.MutableTree) error + SetImmutableTree(immutableTree *iavl.ImmutableTree) + // ModuleName() string // todo } // MTree mutable tree, used for txs delivery type MTree interface { - ReadOnlyTree - Set(key, value []byte) bool - Remove(key []byte) ([]byte, bool) - LoadVersion(targetVersion int64) (int64, error) - LazyLoadVersion(targetVersion int64) (int64, error) - SaveVersion() ([]byte, int64, error) - DeleteVersionIfExists(version int64) error + Commit(...saver) ([]byte, int64, error) + GetLastImmutable() *iavl.ImmutableTree + GetImmutableAtHeight(version int64) (*iavl.ImmutableTree, error) + + DeleteVersion(version int64) error DeleteVersionsRange(fromVersion, toVersion int64) error - GetImmutable() *ImmutableTree - GetImmutableAtHeight(version int64) (*ImmutableTree, error) - GlobalLock() - GlobalUnlock() + + AvailableVersions() []int + Version() int64 +} + +func (t *mutableTree) Commit(savers ...saver) (hash []byte, version int64, err error) { + t.lock.Lock() + defer t.lock.Unlock() + + for _, saver := range savers { + err := saver.Commit(t.tree) + if err != nil { + return nil, 0, err + // return nil, 0, errors.Wrap(err, saver.ModuleName()) + } + } + + hash, version, err = t.tree.SaveVersion() + if err != nil { + return nil, 0, err + } + + immutable, err := t.tree.GetImmutable(t.tree.Version()) + if err != nil { + return nil, 0, err + } + for _, saver := range savers { + saver.SetImmutableTree(immutable) + } + + return hash, version, err +} +func (t *mutableTree) MutableTree() *iavl.MutableTree { + return t.tree } // NewMutableTree creates and returns new MutableTree using given db. Panics on error. // If you want to get read-only state, you should use height = 0 and LazyLoadVersion (version), see NewImmutableTree -func NewMutableTree(height uint64, db dbm.DB, cacheSize int) (MTree, error) { - tree, err := iavl.NewMutableTree(db, cacheSize) +func NewMutableTree(height uint64, db dbm.DB, cacheSize int, initialVersion uint64) (MTree, error) { + tree, err := iavl.NewMutableTreeWithOpts(db, cacheSize, &iavl.Options{InitialVersion: initialVersion}) if err != nil { return nil, err } - m := &mutableTree{ tree: tree, } - if height == 0 { + if height <= initialVersion { return m, nil } - if _, err := m.tree.LoadVersionForOverwriting(int64(height)); err != nil { + if _, err := m.tree.LoadVersionForOverwriting(int64(height) + 1); err != nil { return nil, err } @@ -56,10 +80,9 @@ func NewMutableTree(height uint64, db dbm.DB, cacheSize int) (MTree, error) { type mutableTree struct { tree *iavl.MutableTree lock sync.RWMutex - sync.Mutex } -func (t *mutableTree) GetImmutableAtHeight(version int64) (*ImmutableTree, error) { +func (t *mutableTree) GetImmutableAtHeight(version int64) (*iavl.ImmutableTree, error) { t.lock.RLock() defer t.lock.RUnlock() @@ -68,31 +91,7 @@ func (t *mutableTree) GetImmutableAtHeight(version int64) (*ImmutableTree, error return nil, err } - return &ImmutableTree{ - tree: tree, - }, nil -} - -func (t *mutableTree) GlobalLock() { - t.Lock() -} - -func (t *mutableTree) GlobalUnlock() { - t.Unlock() -} - -func (t *mutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped bool) { - t.lock.RLock() - defer t.lock.RUnlock() - - return t.tree.Iterate(fn) -} - -func (t *mutableTree) Hash() []byte { - t.lock.RLock() - defer t.lock.RUnlock() - - return t.tree.Hash() + return tree, nil } func (t *mutableTree) Version() int64 { @@ -102,56 +101,16 @@ func (t *mutableTree) Version() int64 { return t.tree.Version() } -func (t *mutableTree) GetImmutable() *ImmutableTree { +func (t *mutableTree) GetLastImmutable() *iavl.ImmutableTree { t.lock.RLock() defer t.lock.RUnlock() - return &ImmutableTree{ - tree: t.tree.ImmutableTree, + immutable, err := t.tree.GetImmutable(t.tree.Version()) + if err != nil { + return iavl.NewImmutableTree(dbm.NewMemDB(), 0) } -} -func (t *mutableTree) Get(key []byte) (index int64, value []byte) { - t.lock.RLock() - defer t.lock.RUnlock() - - return t.tree.Get(key) -} - -func (t *mutableTree) Set(key, value []byte) bool { - t.lock.Lock() - defer t.lock.Unlock() - - return t.tree.Set(key, value) -} - -func (t *mutableTree) Remove(key []byte) ([]byte, bool) { - t.lock.Lock() - defer t.lock.Unlock() - - return t.tree.Remove(key) -} - -func (t *mutableTree) LoadVersion(targetVersion int64) (int64, error) { - t.lock.Lock() - defer t.lock.Unlock() - - return t.tree.LoadVersion(targetVersion) -} - -func (t *mutableTree) LazyLoadVersion(targetVersion int64) (int64, error) { - t.lock.Lock() - defer t.lock.Unlock() - - return t.tree.LazyLoadVersion(targetVersion) -} - -// Should use GlobalLock() and GlobalUnlock -func (t *mutableTree) SaveVersion() ([]byte, int64, error) { - t.lock.Lock() - defer t.lock.Unlock() - - return t.tree.SaveVersion() + return immutable } // Should use GlobalLock() and GlobalUnlock @@ -167,15 +126,10 @@ func (t *mutableTree) DeleteVersionsRange(fromVersion, toVersion int64) error { return nil } -// Should use GlobalLock() and GlobalUnlock -func (t *mutableTree) DeleteVersionIfExists(version int64) error { +func (t *mutableTree) DeleteVersion(version int64) error { t.lock.Lock() defer t.lock.Unlock() - if !t.tree.VersionExists(version) { - return nil - } - return t.tree.DeleteVersion(version) } @@ -186,41 +140,15 @@ func (t *mutableTree) AvailableVersions() []int { return t.tree.AvailableVersions() } -// ImmutableTree used for CheckState: API and CheckTx calls. -type ImmutableTree struct { - tree *iavl.ImmutableTree -} - -// NewImmutableTree returns MTree from given db at given height -// Warning: returns the MTree interface, but you should only use ReadOnlyTree -func NewImmutableTree(height uint64, db dbm.DB) (MTree, error) { - tree, _ := NewMutableTree(0, db, 1024) - _, err := tree.LazyLoadVersion(int64(height)) +// NewImmutableTree returns iavl.ImmutableTree from given db at given height +func NewImmutableTree(height uint64, db dbm.DB) (*iavl.ImmutableTree, error) { + tree, err := iavl.NewMutableTree(db, 1024) if err != nil { return nil, err } - return tree, nil -} - -// Iterate iterates over all keys of the tree, in order. The keys and values must not be modified, -// since they may point to data stored within IAVL. -func (t *ImmutableTree) Iterate(fn func(key []byte, value []byte) bool) (stopped bool) { - return t.tree.Iterate(fn) -} - -// Hash returns the root hash. -func (t *ImmutableTree) Hash() []byte { - return t.tree.Hash() -} - -// Version returns the version of the tree. -func (t *ImmutableTree) Version() int64 { - return t.tree.Version() -} - -// Get returns the index and value of the specified key if it exists, or nil and the next index -// otherwise. The returned value must not be modified, since it may point to data stored within -// IAVL. -func (t *ImmutableTree) Get(key []byte) (index int64, value []byte) { - return t.tree.Get(key) + immutableTree, err := tree.GetImmutable(int64(height)) + if err != nil { + return nil, err + } + return immutableTree, nil } diff --git a/upgrades/blocks.go b/upgrades/blocks.go index 6aad29a9b..cebe7a27a 100644 --- a/upgrades/blocks.go +++ b/upgrades/blocks.go @@ -1,6 +1,6 @@ package upgrades -//func IsUpgradeBlock(height uint64) bool { +// func IsUpgradeBlock(height uint64) bool { // upgradeBlocks := []uint64{} // fill this // // for _, block := range upgradeBlocks { @@ -10,4 +10,4 @@ package upgrades // } // // return false -//} +// } diff --git a/version/version.go b/version/version.go index 464ee0da0..591151fee 100755 --- a/version/version.go +++ b/version/version.go @@ -7,7 +7,7 @@ const ( var ( // Version must be a string because scripts like dist.sh read this file. - Version = "1.2.1" + Version = "2.0.0" // GitCommit is the current HEAD set using ldflags. GitCommit string