diff --git a/.github/workflows/contract-verify.yml b/.github/workflows/contract-verify.yml new file mode 100644 index 000000000..bbb8089b9 --- /dev/null +++ b/.github/workflows/contract-verify.yml @@ -0,0 +1,44 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: ContractVerify + +on: + push: + branches: [ "main", "develop" ] + paths: + - 'src/contracts/*.h' + - '!src/contracts/math_lib.h' + - '!src/contracts/qpi.h' + - '!src/contracts/TestExample*.h' + - '.github/workflows/contract-verify.yml' + pull_request: + branches: [ "main", "develop" ] + paths: + - 'src/contracts/*.h' + - '!src/contracts/math_lib.h' + - '!src/contracts/qpi.h' + - '!src/contracts/TestExample*.h' + - '.github/workflows/contract-verify.yml' + +jobs: + contract_verify_job: + runs-on: ubuntu-latest + timeout-minutes: 15 # Sometimes the parser can get stuck + name: Verify smart contract files + steps: + # Checkout repo to use files of the repo as input for container action + - name: Checkout + uses: actions/checkout@v4 + - name: Find all contract files to verify + id: filepaths + run: | + files=$(find src/contracts/ -maxdepth 1 -type f -name "*.h" ! -name "*TestExample*" ! -name "*math_lib*" ! -name "*qpi*" -printf "%p\n" | paste -sd, -) + echo "contract-filepaths=$files" >> "$GITHUB_OUTPUT" + - name: Contract verify action step + id: verify + uses: Franziska-Mueller/qubic-contract-verify@v1.0.0 + with: + filepaths: '${{ steps.filepaths.outputs.contract-filepaths }}' diff --git a/.gitignore b/.gitignore index bf4f57667..08ddaaaad 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,22 @@ x64/ .DS_Store .clang-format tmp + +# Build directories and temporary files +out/build/ +**/Testing/Temporary/ +**/_deps/googletest-src +test/CMakeLists.txt +test/CMakeLists.txt +comp.md +proposal.md +src/Qubic.vcxproj +.claude/settings.local.json +src/Qubic.vcxproj +test/CMakeLists.txt +ANALISIS_STATUS_3.md +.gitignore +node.md +report.md +claude.md +RESUMEN_REVISION.md diff --git a/doc/contracts.md b/doc/contracts.md index d738d2622..dbf3290c1 100644 --- a/doc/contracts.md +++ b/doc/contracts.md @@ -68,7 +68,8 @@ In order to develop a contract, follow these steps: - Design and implement the interfaces of your contract (the user procedures and user functions along with its inputs and outputs). The QPI available for implementing the contract is defined in `src/contracts/qpi.h`. - Implement the system procedures needed and remove all the system procedures that are not needed by your contract. - - Add the short form contract name as a prefix to all global constants (if any). + - Follow the general [qubic style guidelines](https://github.com/qubic/core/blob/main/doc/contributing.md#style-guidelines) when writing your code. + - Add the short form contract name as a prefix to all global constants, structs and classes (if any). - Make sure your code is efficient. Execution time will cost fees in the future. Think about the data structures you use, for example if you can use a hash map instead of an array with linear search. Check if you can optimize code in loops and especially in nested loops. @@ -92,8 +93,9 @@ In order to develop a contract, follow these steps: ## Review and tests Each contract must be validated with the following steps: -1. The contract is verified with a special software tool, ensuring that it complies with the formal requirements mentioned above, such as no use of forbidden C++ features. - (Currently, this tool has not been implemented yet. Thus, this check needs to be done during the review in point 3.) +1. The contract is verified with the [Qubic Contract Verification Tool](https://github.com/Franziska-Mueller/qubic-contract-verify), ensuring that it complies with the formal requirements mentioned above, such as no use of [forbidden C++ features](#restrictions-of-c-language-features). + In the `qubic/core` repository, the tool is run automatically as GitHub workflow for PRs to the `develop` and `main` branches (as well as for commits in these branches). + However, since workflow runs on PRs require maintainer approval, we highly recommend to either build the tool from source or use the GitHub action provided in the tool's repository to analyze your contract header file before opening your PR. 2. The features of the contract have to be extensively tested with automated tests implemented within the Qubic Core's GoogleTest framework. 3. The contract and testing code must be reviewed by at least one of the Qubic Core devs, ensuring it meets high quality standards. 4. Before integrating the contract in the official Qubic Core release, the features of the contract must be tested in a test network with multiple nodes, showing that the contract works well in practice and that the nodes run stable with the contract. @@ -105,7 +107,7 @@ After going through this validation process, a contract can be integrated in off Steps for deploying a contract: -1. Finish development, review, and tests as written above. +1. Finish development, review, and tests as written above. This includes waiting for approval of your PR by the core dev team. If you need to make any significant changes to the code after the computors accepted your proposal, you will need to make a second proposal. 2. A proposal for including your contract into the Qubic Core needs to be prepared. We recommend to add your proposal description to https://github.com/qubic/proposal/tree/main/SmartContracts via a pull request (this directory also contains files from other contracts added before, which can be used as a template). The proposal description should include a detailed description of your contract (see point 1 of the [Development section](#development)) and the final source code of the contract. @@ -547,8 +549,8 @@ In consequence, the procedure is executed but with `qpi.invocationReward() == 0` ## Restrictions of C++ Language Features -It is prohibited to locally instantiating objects or variables on the function call stack. -Instead, use the function and procedure definition macros with the postfix `_WITH_LOCALS` (see above). +It is prohibited to locally instantiate objects or variables on the function call stack. This includes loop index variables `for (int i = 0; ...)`. +Instead, use the function and procedure definition macros with the postfix `_WITH_LOCALS` (see above). In procedures you alternatively may store temporary variables permanently as members of the state. Defining, casting, and dereferencing pointers is forbidden. @@ -566,22 +568,25 @@ The division operator `/` and the modulo operator `%` are prohibited to prevent Use `div()` and `mod()` instead, which return zero in case of division by zero. Strings `"` and chars `'` are forbidden, because they can be used to jump to random memory fragments. +If you want to use `static_assert` you can do so via the `STATIC_ASSERT` macro defined in `qpi.h` which does not require a string literal. -Variadic arguments are prohibited (character combination `...`). +Variadic arguments, template parameter packs, and function parameter packs are prohibited (character combination `...`). Double underscores `__` must not be used in a contract, because these are reserved for internal functions and compiler macros that are prohibited to be used directly. For similar reasons, `QpiContext` and `const_cast` are prohibited too. The scope resolution operator `::` is also prohibited, except for structs, enums, and namespaces defined in contracts and `qpi.h`. -The keywords `typedef` and `union` are prohibited to make the code easier to read and prevent tricking code audits. +The keyword `union` is prohibited to make the code easier to read and prevent tricking code audits. +Similarly, the keywords `typedef` and `using` are only allowed in local scope, e.g. inside structs or functions. +The only exception is `using namespace QPI` which can be used at global scope. Global variables are not permitted. -Global constants must begin with the name of the contract state struct. +Global constants, structs and classes must begin with the name of the contract state struct. There is a limit for recursion and depth of nested contract function / procedure calls (the limit is 10 at the moment). -The input and output structs of contract user procedures and functions may only use integer and boolean types (such as `uint64`, `sint8`, `bit`) as well as `id`, `Array`, and `BitArray`. +The input and output structs of contract user procedures and functions may only use integer and boolean types (such as `uint64`, `sint8`, `bit`) as well as `id`, `Array`, and `BitArray`, and struct types containing only allowed types. Complex types that may have an inconsistent internal state, such as `Collection`, are forbidden in the public interface of a contract. @@ -627,3 +632,7 @@ The file `proposal.cpp` has a lot of examples showing how to use both functions. For example, `getProposalIndices()` shows how to call a contract function requiring input and providing output with `runContractFunction()`. An example use case of `makeContractTransaction()` can be found in `gqmpropSetProposal()`. The function `castVote()` is a more complex example combining both, calling a contract function and invoking a contract procedure. + + + + diff --git a/doc/contributing.md b/doc/contributing.md index fbd1aa55d..761a61449 100644 --- a/doc/contributing.md +++ b/doc/contributing.md @@ -1,5 +1,16 @@ # How to Contribute +## Table of contents + +1. [Contributing as an external developer](#contributing-as-an-external-developer) +2. [Development workflow / branches](#development-workflow--branches) +3. [Coding guidelines](#coding-guidelines) + 1. [Most important principles](#most-important-principles) + 2. [General guidelines](#general-guidelines) + 3. [Style guidelines](#style-guidelines) +4. [Version naming scheme](#version-naming-scheme) +5. [Profiling](#profiling) + ## Contributing as an external developer If you find bugs, typos, or other problems that can be fixed with a few changes, you are more than welcome to contribute these fixes with a pull request as follows. @@ -191,6 +202,8 @@ The code formatting rules are enforced using `clang-format`, ideally setup as a They are based on the "Microsoft" style with some custom modifications. Currently, the style guidelines are designed to improve consistency while minimizing the number of changes needed in the existing codebase. +#### Naming + The following naming rules are not strictly enforced, but should be followed at least in new code: - **Preprocessor symbols** must be defined `ALL_CAPS`. @@ -221,6 +234,21 @@ The following naming rules are not strictly enforced, but should be followed at - **Functions** are named following the same pattern as variables. They usually start with a verb. Examples: `getComputerDigest()`, `processExchangePublicPeers()`, `initContractExec()`. +#### Curly Braces Style + +Every opening curly brace should be on a new line. This applies to conditional blocks, loops, functions, classes, structs, etc. For example: + +``` +if (cond) +{ + // do something +} +else +{ + // do something else +} +``` + ## Version naming scheme @@ -373,3 +401,5 @@ Even when bound by serializing instructions, the system environment at the time [AMD64 Architecture Programmer's Manual, Volumes 1-5, 13.2.4 Time-Stamp Counter](https://www.amd.com/content/dam/amd/en/documents/processor-tech-docs/programmer-references/40332.pdf) Another rich source: [Intel® 64 and IA-32 Architectures Software Developer's Manual Combined Volumes: 1, 2A, 2B, 2C, 2D, 3A, 3B, 3C, 3D, and 4](https://cdrdv2.intel.com/v1/dl/getContent/671200) + + diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 9dd7a3aa6..7ed6eec52 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -23,7 +23,9 @@ + + @@ -39,6 +41,8 @@ + + @@ -50,6 +54,7 @@ + @@ -115,6 +120,7 @@ + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index 3d0f9ab18..abc3ab0bc 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -123,6 +123,12 @@ contracts + + contracts + + + contracts + contract_core @@ -270,6 +276,18 @@ platform + + contract_core + + + ticking + + + contracts + + + contracts + @@ -317,4 +335,4 @@ platform - + \ No newline at end of file diff --git a/src/assets/assets.h b/src/assets/assets.h index d0fc19589..793941669 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -245,6 +245,7 @@ static long long issueAsset(const m256i& issuerPublicKey, const char name[7], ch AssetIssuance assetIssuance; assetIssuance.issuerPublicKey = issuerPublicKey; assetIssuance.numberOfShares = numberOfShares; + assetIssuance.managingContractIndex = managingContractIndex; // any SC can call issueAsset now (eg: QBOND) not just QX *((unsigned long long*) assetIssuance.name) = *((unsigned long long*) name); // Order must be preserved! assetIssuance.numberOfDecimalPlaces = numberOfDecimalPlaces; // Order must be preserved! *((unsigned long long*) assetIssuance.unitOfMeasurement) = *((unsigned long long*) unitOfMeasurement); // Order must be preserved! @@ -417,8 +418,8 @@ static bool transferShareManagementRights(int sourceOwnershipIndex, int sourcePo logPM.possessionPublicKey = possessionPublicKey; logPM.ownershipPublicKey = ownershipPublicKey; logPM.issuerPublicKey = assets[issuanceIndex].varStruct.issuance.publicKey; - logOM.sourceContractIndex = assets[sourcePossessionIndex].varStruct.ownership.managingContractIndex; - logOM.destinationContractIndex = destinationPossessionManagingContractIndex; + logPM.sourceContractIndex = assets[sourcePossessionIndex].varStruct.ownership.managingContractIndex; + logPM.destinationContractIndex = destinationPossessionManagingContractIndex; logPM.numberOfShares = numberOfShares; *((unsigned long long*) & logPM.assetName) = *((unsigned long long*) & assets[assets[sourceOwnershipIndex].varStruct.ownership.issuanceIndex].varStruct.issuance.name); // possible with 7 byte array, because it is followed by memory reserved for terminator byte logger.logAssetPossessionManagingContractChange(logPM); diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index 9d4c41d94..5c19a8783 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -1,5 +1,5 @@ #pragma once - +#include "network_messages/common_def.h" #include "platform/m256.h" ////////// Smart contracts \\\\\\\\\\ @@ -194,8 +194,6 @@ struct __FunctionOrProcedureBeginEndGuard #define CONTRACT_STATE2_TYPE QSWAP2 #include "contracts/Qswap.h" -#ifndef NO_NOST - #undef CONTRACT_INDEX #undef CONTRACT_STATE_TYPE #undef CONTRACT_STATE2_TYPE @@ -206,7 +204,45 @@ struct __FunctionOrProcedureBeginEndGuard #define CONTRACT_STATE2_TYPE NOST2 #include "contracts/Nostromo.h" -#endif +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QDRAW_CONTRACT_INDEX 15 +#define CONTRACT_INDEX QDRAW_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QDRAW +#define CONTRACT_STATE2_TYPE QDRAW2 +#include "contracts/Qdraw.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define RL_CONTRACT_INDEX 16 +#define CONTRACT_INDEX RL_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE RL +#define CONTRACT_STATE2_TYPE RL2 +#include "contracts/RandomLottery.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QBOND_CONTRACT_INDEX 17 +#define CONTRACT_INDEX QBOND_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QBOND +#define CONTRACT_STATE2_TYPE QBOND2 +#include "contracts/QBond.h" + +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define VOTTUNBRIDGE_CONTRACT_INDEX 18 +#define CONTRACT_INDEX VOTTUNBRIDGE_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE VOTTUNBRIDGE +#define CONTRACT_STATE2_TYPE VOTTUNBRIDGE2 +#include "contracts/VottunBridge.h" // new contracts should be added above this line @@ -304,9 +340,11 @@ constexpr struct ContractDescription {"MSVAULT", 149, 10000, sizeof(MSVAULT)}, // proposal in epoch 147, IPO in 148, construction and first use in 149 {"QBAY", 154, 10000, sizeof(QBAY)}, // proposal in epoch 152, IPO in 153, construction and first use in 154 {"QSWAP", 171, 10000, sizeof(QSWAP)}, // proposal in epoch 169, IPO in 170, construction and first use in 171 -#ifndef NO_NOST {"NOST", 172, 10000, sizeof(NOST)}, // proposal in epoch 170, IPO in 171, construction and first use in 172 -#endif + {"QDRAW", 179, 10000, sizeof(QDRAW)}, // proposal in epoch 177, IPO in 178, construction and first use in 179 + {"RL", 182, 10000, sizeof(RL)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 + {"QBOND", 182, 10000, sizeof(QBOND)}, // proposal in epoch 180, IPO in 181, construction and first use in 182 + {"VBRIDGE", 184, 10000, sizeof(VOTTUNBRIDGE)}, // Vottun Bridge - Qubic <-> EVM bridge with multisig admin // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(IPO)}, @@ -409,9 +447,11 @@ static void initializeContracts() REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(MSVAULT); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBAY); REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QSWAP); -#ifndef NO_NOST REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(NOST); -#endif + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QDRAW); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(RL); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QBOND); + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(VOTTUNBRIDGE); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); diff --git a/src/contract_core/ipo.h b/src/contract_core/ipo.h index ab1b1ae55..2ae92336c 100644 --- a/src/contract_core/ipo.h +++ b/src/contract_core/ipo.h @@ -26,7 +26,7 @@ static long long bidInContractIPO(long long price, unsigned short quantity, cons ASSERT(spectrumIndex >= 0); ASSERT(spectrumIndex == ::spectrumIndex(sourcePublicKey)); ASSERT(contractIndex < contractCount); - ASSERT(system.epoch < contractDescriptions[contractIndex].constructionEpoch); + ASSERT(system.epoch == contractDescriptions[contractIndex].constructionEpoch - 1); long long registeredBids = -1; @@ -128,7 +128,7 @@ static void finishIPOs() { for (unsigned int contractIndex = 1; contractIndex < contractCount; contractIndex++) { - if (system.epoch < contractDescriptions[contractIndex].constructionEpoch && contractStates[contractIndex]) + if (system.epoch == (contractDescriptions[contractIndex].constructionEpoch - 1) && contractStates[contractIndex]) { contractStateLock[contractIndex].acquireRead(); IPO* ipo = (IPO*)contractStates[contractIndex]; diff --git a/src/contract_core/qpi_asset_impl.h b/src/contract_core/qpi_asset_impl.h index dc1520a05..07c4cba00 100644 --- a/src/contract_core/qpi_asset_impl.h +++ b/src/contract_core/qpi_asset_impl.h @@ -486,6 +486,13 @@ bool QPI::QpiContextProcedureCall::distributeDividends(long long amountPerShare) return false; } + // this part of code doesn't perform completed QuTransfers, instead it `decreaseEnergy` all QUs at once and `increaseEnergy` multiple times. + // Meanwhile, a QUTransfer requires a pair of both decrease & increase calls. + // This behavior will produce different numberOfOutgoingTransfers for the SC index. + // 3rd party software needs to catch the HINT message to know the distribute dividends operation + DummyCustomMessage dcm{ CUSTOM_MESSAGE_OP_START_DISTRIBUTE_DIVIDENDS }; + logger.logCustomMessage(dcm); + if (decreaseEnergy(index, amountPerShare * NUMBER_OF_COMPUTORS)) { ACQUIRE(universeLock); @@ -499,19 +506,22 @@ bool QPI::QpiContextProcedureCall::distributeDividends(long long amountPerShare) ASSERT(iter.possessionIndex() < ASSETS_CAPACITY); const auto& possession = assets[iter.possessionIndex()].varStruct.possession; - const long long dividend = amountPerShare * possession.numberOfShares; - increaseEnergy(possession.publicKey, dividend); + if (possession.numberOfShares) + { + const long long dividend = amountPerShare * possession.numberOfShares; + increaseEnergy(possession.publicKey, dividend); - if (!contractActionTracker.addQuTransfer(_currentContractId, possession.publicKey, dividend)) - __qpiAbort(ContractErrorTooManyActions); + if (!contractActionTracker.addQuTransfer(_currentContractId, possession.publicKey, dividend)) + __qpiAbort(ContractErrorTooManyActions); - __qpiNotifyPostIncomingTransfer(_currentContractId, possession.publicKey, dividend, TransferType::qpiDistributeDividends); + __qpiNotifyPostIncomingTransfer(_currentContractId, possession.publicKey, dividend, TransferType::qpiDistributeDividends); - const QuTransfer quTransfer = { _currentContractId, possession.publicKey, dividend }; - logger.logQuTransfer(quTransfer); + const QuTransfer quTransfer = { _currentContractId, possession.publicKey, dividend }; + logger.logQuTransfer(quTransfer); - totalShareCounter += possession.numberOfShares; + totalShareCounter += possession.numberOfShares; + } iter.next(); } @@ -520,7 +530,8 @@ bool QPI::QpiContextProcedureCall::distributeDividends(long long amountPerShare) RELEASE(universeLock); } - + dcm = DummyCustomMessage{ CUSTOM_MESSAGE_OP_END_DISTRIBUTE_DIVIDENDS }; + logger.logCustomMessage(dcm); return true; } diff --git a/src/contract_core/qpi_ipo_impl.h b/src/contract_core/qpi_ipo_impl.h index 5287f4c92..b2cc02dfa 100644 --- a/src/contract_core/qpi_ipo_impl.h +++ b/src/contract_core/qpi_ipo_impl.h @@ -12,7 +12,7 @@ QPI::sint64 QPI::QpiContextProcedureCall::bidInIPO(unsigned int IPOContractIndex return -1; } - if (system.epoch >= contractDescriptions[IPOContractIndex].constructionEpoch) // IPO is finished. + if (system.epoch != (contractDescriptions[IPOContractIndex].constructionEpoch - 1)) // IPO has not started yet or is finished. { return -1; } @@ -30,7 +30,7 @@ QPI::sint64 QPI::QpiContextProcedureCall::bidInIPO(unsigned int IPOContractIndex // Returns the ID of the entity who has made this IPO bid or NULL_ID if the ipoContractIndex or ipoBidIndex are invalid. QPI::id QPI::QpiContextFunctionCall::ipoBidId(QPI::uint32 ipoContractIndex, QPI::uint32 ipoBidIndex) const { - if (ipoContractIndex >= contractCount || system.epoch >= contractDescriptions[ipoContractIndex].constructionEpoch || ipoBidIndex >= NUMBER_OF_COMPUTORS) + if (ipoContractIndex >= contractCount || system.epoch != (contractDescriptions[ipoContractIndex].constructionEpoch - 1) || ipoBidIndex >= NUMBER_OF_COMPUTORS) { return NULL_ID; } @@ -51,7 +51,7 @@ QPI::sint64 QPI::QpiContextFunctionCall::ipoBidPrice(QPI::uint32 ipoContractInde return -1; } - if (system.epoch >= contractDescriptions[ipoContractIndex].constructionEpoch) + if (system.epoch != (contractDescriptions[ipoContractIndex].constructionEpoch - 1)) { return -2; } diff --git a/src/contract_core/qpi_mining_impl.h b/src/contract_core/qpi_mining_impl.h new file mode 100644 index 000000000..be4cf796c --- /dev/null +++ b/src/contract_core/qpi_mining_impl.h @@ -0,0 +1,27 @@ +#pragma once + +#include "contracts/qpi.h" +#include "score.h" + +static ScoreFunction< + NUMBER_OF_INPUT_NEURONS, + NUMBER_OF_OUTPUT_NEURONS, + NUMBER_OF_TICKS*2, + NUMBER_OF_NEIGHBORS, + POPULATION_THRESHOLD, + NUMBER_OF_MUTATIONS, + SOLUTION_THRESHOLD_DEFAULT, + 1 +>* score_qpi = nullptr; // NOTE: SC is single-threaded + +m256i QPI::QpiContextFunctionCall::computeMiningFunction(const m256i miningSeed, const m256i publicKey, const m256i nonce) const +{ + // Score's currentRandomSeed is initialized to zero by setMem(score_qpi, sizeof(*score_qpi), 0) + // If the mining seed changes, we must reinitialize it + if (miningSeed != score_qpi->currentRandomSeed) + { + score_qpi->initMiningData(miningSeed); + } + (*score_qpi)(0, publicKey, miningSeed, nonce); + return score_qpi->getLastOutput(0); +} diff --git a/src/contract_core/qpi_ticking_impl.h b/src/contract_core/qpi_ticking_impl.h index d64f043c1..d1c2f7f95 100644 --- a/src/contract_core/qpi_ticking_impl.h +++ b/src/contract_core/qpi_ticking_impl.h @@ -55,4 +55,19 @@ QPI::DateAndTime QPI::QpiContextFunctionCall::now() const result.second = etalonTick.second; result.millisecond = etalonTick.millisecond; return result; +} + +m256i QPI::QpiContextFunctionCall::getPrevSpectrumDigest() const +{ + return etalonTick.prevSpectrumDigest; +} + +m256i QPI::QpiContextFunctionCall::getPrevUniverseDigest() const +{ + return etalonTick.prevUniverseDigest; +} + +m256i QPI::QpiContextFunctionCall::getPrevComputerDigest() const +{ + return etalonTick.prevComputerDigest; } \ No newline at end of file diff --git a/src/contracts/ComputorControlledFund.h b/src/contracts/ComputorControlledFund.h index 4927f5089..fbd2175d6 100644 --- a/src/contracts/ComputorControlledFund.h +++ b/src/contracts/ComputorControlledFund.h @@ -143,7 +143,9 @@ struct CCF : public ContractBase struct GetProposal_output { bit okay; - uint8 _padding0[7]; + Array _padding0; + Array _padding1; + Array _padding2; id proposerPubicKey; ProposalDataT proposal; }; @@ -285,7 +287,7 @@ struct CCF : public ContractBase continue; // Option for transfer has been accepted? - if (locals.results.optionVoteCount.get(1) > QUORUM / 2) + if (locals.results.optionVoteCount.get(1) > div(QUORUM, 2U)) { // Prepare log entry and do transfer locals.transfer.destination = locals.proposal.transfer.destination; diff --git a/src/contracts/GeneralQuorumProposal.h b/src/contracts/GeneralQuorumProposal.h index 4055e1c1b..d7ff41981 100644 --- a/src/contracts/GeneralQuorumProposal.h +++ b/src/contracts/GeneralQuorumProposal.h @@ -279,7 +279,9 @@ struct GQMPROP : public ContractBase struct GetProposal_output { bit okay; - uint8 _padding0[7]; + Array _padding0; + Array _padding1; + Array _padding2; id proposerPubicKey; ProposalDataT proposal; }; @@ -410,7 +412,7 @@ struct GQMPROP : public ContractBase } // Option for changing status quo has been accepted? (option 0 is "no change") - if (locals.mostVotedOptionIndex > 0 && locals.mostVotedOptionVotes > QUORUM / 2) + if (locals.mostVotedOptionIndex > 0 && locals.mostVotedOptionVotes > div(QUORUM, 2U)) { // Set in revenueDonation table (cannot be done in END_EPOCH, because this may overwrite entries that // are still needed unchanged for this epoch for the revenue donation which is run after END_EPOCH) diff --git a/src/contracts/MsVault.h b/src/contracts/MsVault.h index f507978f0..2fcc8454f 100644 --- a/src/contracts/MsVault.h +++ b/src/contracts/MsVault.h @@ -6,12 +6,15 @@ constexpr uint64 MSVAULT_INITIAL_MAX_VAULTS = 131072ULL; // 2^17 constexpr uint64 MSVAULT_MAX_VAULTS = MSVAULT_INITIAL_MAX_VAULTS * X_MULTIPLIER; // MSVAULT asset name : 23727827095802701, using assetNameFromString("MSVAULT") utility in test_util.h static constexpr uint64 MSVAULT_ASSET_NAME = 23727827095802701; +constexpr uint64 MSVAULT_MAX_ASSET_TYPES = 8; // Max number of different asset types a vault can hold constexpr uint64 MSVAULT_REGISTERING_FEE = 5000000ULL; constexpr uint64 MSVAULT_RELEASE_FEE = 100000ULL; constexpr uint64 MSVAULT_RELEASE_RESET_FEE = 1000000ULL; constexpr uint64 MSVAULT_HOLDING_FEE = 500000ULL; constexpr uint64 MSVAULT_BURN_FEE = 0ULL; // Integer percentage from 1 -> 100 +constexpr uint64 MSVAULT_VOTE_FEE_CHANGE_FEE = 10000000ULL; // Deposit fee for adjusting other fees, and refund if shareholders +constexpr uint64 MSVAULT_REVOKE_FEE = 100ULL; // [TODO]: Turn this assert ON when MSVAULT_BURN_FEE > 0 //static_assert(MSVAULT_BURN_FEE > 0, "SC requires burning qu to operate, the burn fee must be higher than 0!"); @@ -25,19 +28,46 @@ struct MSVAULT2 struct MSVAULT : public ContractBase { public: + // Procedure Status Codes --- + // 0: GENEREAL_FAILURE + // 1: SUCCESS + // 2: FAILURE_INSUFFICIENT_FEE + // 3: FAILURE_INVALID_VAULT (Invalid vault ID or vault inactive) + // 4: FAILURE_NOT_AUTHORIZED (not owner, not shareholder, etc.) + // 5: FAILURE_INVALID_PARAMS (amount is 0, destination is NULL, etc.) + // 6: FAILURE_INSUFFICIENT_BALANCE + // 7: FAILURE_LIMIT_REACHED (max vaults, max asset types, etc.) + // 8: FAILURE_TRANSFER_FAILED + // 9: PENDING_APPROVAL + + struct AssetBalance + { + Asset asset; + uint64 balance; + }; + struct Vault { id vaultName; Array owners; Array releaseAmounts; Array releaseDestinations; - uint64 balance; + uint64 qubicBalance; uint8 numberOfOwners; uint8 requiredApprovals; bit isActive; }; - struct MsVaultFeeVote + struct VaultAssetPart + { + Array assetBalances; + uint8 numberOfAssetTypes; + Array releaseAssets; + Array releaseAssetAmounts; + Array releaseAssetDestinations; + }; + + struct MsVaultFeeVote { uint64 registeringFee; uint64 releaseFee; @@ -50,15 +80,19 @@ struct MSVAULT : public ContractBase struct MSVaultLogger { uint32 _contractIndex; - // 1: Invalid vault ID or vault inactive - // 2: Caller not an owner - // 3: Invalid parameters (e.g., amount=0, destination=NULL_ID) - // 4: Release successful - // 5: Insufficient balance - // 6: Release not fully approved - // 7: Reset release requests successful - uint32 _type; - uint64 vaultId; + // _type corresponds to Procedure Status Codes + // 0: GENEREAL_FAILURE + // 1: SUCCESS + // 2: FAILURE_INSUFFICIENT_FEE + // 3: FAILURE_INVALID_VAULT + // 4: FAILURE_NOT_AUTHORIZED + // 5: FAILURE_INVALID_PARAMS + // 6: FAILURE_INSUFFICIENT_BALANCE + // 7: FAILURE_LIMIT_REACHED + // 8: FAILURE_TRANSFER_FAILED + // 9: PENDING_APPROVAL + uint32 _type; + uint64 vaultId; id ownerID; uint64 amount; id destination; @@ -130,6 +164,16 @@ struct MSVAULT : public ContractBase uint64 result; }; + struct getManagedAssetBalance_input + { + Asset asset; + id owner; + }; + struct getManagedAssetBalance_output + { + sint64 balance; + }; + // Procedures and functions' structs struct registerVault_input { @@ -139,22 +183,25 @@ struct MSVAULT : public ContractBase }; struct registerVault_output { + uint64 status; }; struct registerVault_locals { + MSVaultLogger logger; uint64 ownerCount; uint64 i; sint64 ii; uint64 j; uint64 k; uint64 count; + uint64 found; sint64 slotIndex; Vault newVault; Vault tempVault; id proposedOwner; - + Vault newQubicVault; + VaultAssetPart newAssetVault; Array tempOwners; - resetReleaseRequests_input rr_in; resetReleaseRequests_output rr_out; resetReleaseRequests_locals rr_locals; @@ -166,13 +213,16 @@ struct MSVAULT : public ContractBase }; struct deposit_output { + uint64 status; }; struct deposit_locals { + MSVaultLogger logger; Vault vault; isValidVaultId_input iv_in; isValidVaultId_output iv_out; isValidVaultId_locals iv_locals; + uint64 amountToDeposit; }; struct releaseTo_input @@ -183,6 +233,7 @@ struct MSVAULT : public ContractBase }; struct releaseTo_output { + uint64 status; }; struct releaseTo_locals { @@ -218,6 +269,7 @@ struct MSVAULT : public ContractBase }; struct resetRelease_output { + uint64 status; }; struct resetRelease_locals { @@ -240,7 +292,7 @@ struct MSVAULT : public ContractBase isValidVaultId_locals iv_locals; }; - struct voteFeeChange_input + struct voteFeeChange_input { uint64 newRegisteringFee; uint64 newReleaseFee; @@ -251,9 +303,11 @@ struct MSVAULT : public ContractBase }; struct voteFeeChange_output { + uint64 status; }; struct voteFeeChange_locals { + MSVaultLogger logger; uint64 i; uint64 sumVote; bit needNewRecord; @@ -357,7 +411,7 @@ struct MSVAULT : public ContractBase uint64 burnedAmount; }; - struct getFees_input + struct getFees_input { }; struct getFees_output @@ -392,13 +446,238 @@ struct MSVAULT : public ContractBase uint64 requiredApprovals; }; + struct getFeeVotes_input + { + }; + struct getFeeVotes_output + { + uint64 status; + uint64 numberOfFeeVotes; + Array feeVotes; + }; + struct getFeeVotes_locals + { + uint64 i; + }; + + struct getFeeVotesOwner_input + { + }; + struct getFeeVotesOwner_output + { + uint64 status; + uint64 numberOfFeeVotes; + Array feeVotesOwner; + }; + struct getFeeVotesOwner_locals + { + uint64 i; + }; + + struct getFeeVotesScore_input + { + }; + struct getFeeVotesScore_output + { + uint64 status; + uint64 numberOfFeeVotes; + Array feeVotesScore; + }; + struct getFeeVotesScore_locals + { + uint64 i; + }; + + struct getUniqueFeeVotes_input + { + }; + struct getUniqueFeeVotes_output + { + uint64 status; + uint64 numberOfUniqueFeeVotes; + Array uniqueFeeVotes; + }; + struct getUniqueFeeVotes_locals + { + uint64 i; + }; + + struct getUniqueFeeVotesRanking_input + { + }; + struct getUniqueFeeVotesRanking_output + { + uint64 status; + uint64 numberOfUniqueFeeVotes; + Array uniqueFeeVotesRanking; + }; + struct getUniqueFeeVotesRanking_locals + { + uint64 i; + }; + + struct depositAsset_input + { + uint64 vaultId; + Asset asset; + uint64 amount; + }; + struct depositAsset_output + { + uint64 status; + }; + struct depositAsset_locals + { + MSVaultLogger logger; + Vault qubicVault; // Object for the Qubic-related part of the vault + VaultAssetPart assetVault; // Object for the Asset-related part of the vault + AssetBalance ab; + sint64 assetIndex; + uint64 i; + sint64 userAssetBalance; + sint64 tempShares; + sint64 transferResult; + sint64 transferedShares; + QX::TransferShareOwnershipAndPossession_input qx_in; + QX::TransferShareOwnershipAndPossession_output qx_out; + sint64 transferredNumberOfShares; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + }; + + struct revokeAssetManagementRights_input + { + Asset asset; + sint64 numberOfShares; + }; + struct revokeAssetManagementRights_output + { + sint64 transferredNumberOfShares; + uint64 status; + }; + struct revokeAssetManagementRights_locals + { + MSVaultLogger logger; + sint64 managedBalance; + sint64 result; + }; + + struct releaseAssetTo_input + { + uint64 vaultId; + Asset asset; + uint64 amount; + id destination; + }; + struct releaseAssetTo_output + { + uint64 status; + }; + struct releaseAssetTo_locals + { + Vault qubicVault; + VaultAssetPart assetVault; + MSVaultLogger logger; + sint64 ownerIndex; + uint64 approvals; + bit releaseApproved; + AssetBalance ab; + uint64 i; + sint64 assetIndex; + isOwnerOfVault_input io_in; + isOwnerOfVault_output io_out; + isOwnerOfVault_locals io_locals; + findOwnerIndexInVault_input fi_in; + findOwnerIndexInVault_output fi_out; + findOwnerIndexInVault_locals fi_locals; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + sint64 remainingShares; + sint64 releaseResult; + }; + + struct resetAssetRelease_input + { + uint64 vaultId; + }; + struct resetAssetRelease_output + { + uint64 status; + }; + struct resetAssetRelease_locals + { + Vault qubicVault; + VaultAssetPart assetVault; + sint64 ownerIndex; + MSVaultLogger logger; + isOwnerOfVault_input io_in; + isOwnerOfVault_output io_out; + isOwnerOfVault_locals io_locals; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + findOwnerIndexInVault_input fi_in; + findOwnerIndexInVault_output fi_out; + findOwnerIndexInVault_locals fi_locals; + uint64 i; + }; + + struct getAssetReleaseStatus_input + { + uint64 vaultId; + }; + struct getAssetReleaseStatus_output + { + uint64 status; + Array assets; + Array amounts; + Array destinations; + }; + struct getAssetReleaseStatus_locals + { + Vault qubicVault; + VaultAssetPart assetVault; + uint64 i; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + }; + + struct getVaultAssetBalances_input + { + uint64 vaultId; + }; + struct getVaultAssetBalances_output + { + uint64 status; + uint64 numberOfAssetTypes; + Array assetBalances; + }; + struct getVaultAssetBalances_locals + { + uint64 i; + Vault qubicVault; + VaultAssetPart assetVault; + isValidVaultId_input iv_in; + isValidVaultId_output iv_out; + isValidVaultId_locals iv_locals; + }; + struct END_EPOCH_locals { uint64 i; uint64 j; - Vault v; + uint64 k; + Vault qubicVault; + VaultAssetPart assetVault; sint64 amountToDistribute; uint64 feeToBurn; + AssetBalance ab; + QX::TransferShareOwnershipAndPossession_input qx_in; + QX::TransferShareOwnershipAndPossession_output qx_out; + id qxAdress; }; protected: @@ -426,6 +705,8 @@ struct MSVAULT : public ContractBase uint64 liveDepositFee; uint64 liveBurnFee; + Array vaultAssetParts; + // Helper Functions PRIVATE_FUNCTION_WITH_LOCALS(isValidVaultId) { @@ -473,68 +754,71 @@ struct MSVAULT : public ContractBase // Procedures and functions PUBLIC_PROCEDURE_WITH_LOCALS(registerVault) { - // [TODO]: Change this to - // if (qpi.invocationReward() < state.liveRegisteringFee) - if (qpi.invocationReward() < MSVAULT_REGISTERING_FEE) + output.status = 0; + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.vaultId = -1; // Not yet created + + if (qpi.invocationReward() < (sint64)state.liveRegisteringFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } + if (qpi.invocationReward() > (sint64)state.liveRegisteringFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveRegisteringFee); + } + locals.ownerCount = 0; for (locals.i = 0; locals.i < MSVAULT_MAX_OWNERS; locals.i = locals.i + 1) { locals.proposedOwner = input.owners.get(locals.i); if (locals.proposedOwner != NULL_ID) { - locals.tempOwners.set(locals.ownerCount, locals.proposedOwner); - locals.ownerCount = locals.ownerCount + 1; + // Check for duplicates + locals.found = false; + for (locals.j = 0; locals.j < locals.ownerCount; locals.j++) + { + if (locals.tempOwners.get(locals.j) == locals.proposedOwner) + { + locals.found = true; + break; + } + } + if (!locals.found) + { + locals.tempOwners.set(locals.ownerCount, locals.proposedOwner); + locals.ownerCount++; + } } } if (locals.ownerCount <= 1) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - if (locals.ownerCount > MSVAULT_MAX_OWNERS) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - for (locals.i = locals.ownerCount; locals.i < MSVAULT_MAX_OWNERS; locals.i = locals.i + 1) - { - locals.tempOwners.set(locals.i, NULL_ID); - } - - // Check if requiredApprovals is valid: must be <= numberOfOwners, > 1 + // requiredApprovals must be > 1 and <= numberOfOwners if (input.requiredApprovals <= 1 || input.requiredApprovals > locals.ownerCount) { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } - - // Find empty slot - locals.slotIndex = -1; - for (locals.ii = 0; locals.ii < MSVAULT_MAX_VAULTS; locals.ii++) - { - locals.tempVault = state.vaults.get(locals.ii); - if (!locals.tempVault.isActive && locals.tempVault.numberOfOwners == 0 && locals.tempVault.balance == 0) - { - locals.slotIndex = locals.ii; - break; - } - } - - if (locals.slotIndex == -1) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - for (locals.i = 0; locals.i < locals.ownerCount; locals.i++) + // Check co-ownership limits + for (locals.i = 0; locals.i < locals.ownerCount; locals.i = locals.i + 1) { locals.proposedOwner = locals.tempOwners.get(locals.i); locals.count = 0; @@ -548,101 +832,408 @@ struct MSVAULT : public ContractBase if (locals.tempVault.owners.get(locals.k) == locals.proposedOwner) { locals.count++; - if (locals.count >= MSVAULT_MAX_COOWNER) - { - qpi.transfer(qpi.invocator(), qpi.invocationReward()); - return; - } } } } } + if (locals.count >= MSVAULT_MAX_COOWNER) + { + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 7; // FAILURE_LIMIT_REACHED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + } + + // Find empty slot + locals.slotIndex = -1; + for (locals.ii = 0; locals.ii < MSVAULT_MAX_VAULTS; locals.ii++) + { + locals.tempVault = state.vaults.get(locals.ii); + if (!locals.tempVault.isActive && locals.tempVault.numberOfOwners == 0 && locals.tempVault.qubicBalance == 0) + { + locals.slotIndex = locals.ii; + break; + } } - locals.newVault.vaultName = input.vaultName; - locals.newVault.numberOfOwners = (uint8)locals.ownerCount; - locals.newVault.requiredApprovals = (uint8)input.requiredApprovals; - locals.newVault.balance = 0; - locals.newVault.isActive = true; + if (locals.slotIndex == -1) + { + qpi.transfer(qpi.invocator(), (sint64)state.liveRegisteringFee); + output.status = 7; // FAILURE_LIMIT_REACHED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } - locals.rr_in.vault = locals.newVault; - resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); - locals.newVault = locals.rr_out.vault; + // Initialize the new vault + locals.newQubicVault.vaultName = input.vaultName; + locals.newQubicVault.numberOfOwners = (uint8)locals.ownerCount; + locals.newQubicVault.requiredApprovals = (uint8)input.requiredApprovals; + locals.newQubicVault.qubicBalance = 0; + locals.newQubicVault.isActive = true; + // Set owners for (locals.i = 0; locals.i < locals.ownerCount; locals.i++) { - locals.newVault.owners.set(locals.i, locals.tempOwners.get(locals.i)); + locals.newQubicVault.owners.set(locals.i, locals.tempOwners.get(locals.i)); + } + // Clear remaining owner slots + for (locals.i = locals.ownerCount; locals.i < MSVAULT_MAX_OWNERS; locals.i++) + { + locals.newQubicVault.owners.set(locals.i, NULL_ID); } - state.vaults.set((uint64)locals.slotIndex, locals.newVault); + // Reset release requests for both Qubic and Assets + locals.rr_in.vault = locals.newQubicVault; + resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); + locals.newQubicVault = locals.rr_out.vault; - // [TODO]: Change this to - //if (qpi.invocationReward() > state.liveRegisteringFee) - //{ - // qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.liveRegisteringFee); - // } - if (qpi.invocationReward() > MSVAULT_REGISTERING_FEE) + // Init the Asset part of the vault + locals.newAssetVault.numberOfAssetTypes = 0; + for (locals.i = 0; locals.i < locals.ownerCount; locals.i++) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - MSVAULT_REGISTERING_FEE); + locals.newAssetVault.releaseAssets.set(locals.i, { NULL_ID, 0 }); + locals.newAssetVault.releaseAssetAmounts.set(locals.i, 0); + locals.newAssetVault.releaseAssetDestinations.set(locals.i, NULL_ID); } + state.vaults.set((uint64)locals.slotIndex, locals.newQubicVault); + state.vaultAssetParts.set((uint64)locals.slotIndex, locals.newAssetVault); + state.numberOfActiveVaults++; - // [TODO]: Change this to - //state.totalRevenue += state.liveRegisteringFee; - state.totalRevenue += MSVAULT_REGISTERING_FEE; + state.totalRevenue += state.liveRegisteringFee; + output.status = 1; // SUCCESS + locals.logger.vaultId = locals.slotIndex; + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); } PUBLIC_PROCEDURE_WITH_LOCALS(deposit) { - locals.iv_in.vaultId = input.vaultId; - isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + output.status = 0; // FAILURE_GENERAL - if (!locals.iv_out.result) + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.vaultId = input.vaultId; + + if (qpi.invocationReward() < (sint64)state.liveDepositFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - locals.vault = state.vaults.get(input.vaultId); - if (!locals.vault.isActive) + // calculate the actual amount to deposit into the vault + locals.amountToDeposit = qpi.invocationReward() - state.liveDepositFee; + + // make sure the deposit amount is greater than zero + if (locals.amountToDeposit == 0) { + // The user only send the exact fee amount, with nothing left to deposit + // this is an invalid operation, so we refund everything qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); return; } - locals.vault.balance += qpi.invocationReward(); - state.vaults.set(input.vaultId, locals.vault); - } - - PUBLIC_PROCEDURE_WITH_LOCALS(releaseTo) + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.vault = state.vaults.get(input.vaultId); + if (!locals.vault.isActive) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // add the collected fee to the total revenue + state.totalRevenue += state.liveDepositFee; + + // add the remaining amount to the specified vault's balance + locals.vault.qubicBalance += locals.amountToDeposit; + + state.vaults.set(input.vaultId, locals.vault); + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(revokeAssetManagementRights) { - // [TODO]: Change this to - //if (qpi.invocationReward() > state.liveReleaseFee) - //{ - // qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.liveReleaseFee); - //} - if (qpi.invocationReward() > MSVAULT_RELEASE_FEE) + // This procedure allows a user to revoke asset management rights from MsVault + // and transfer them back to QX, which is the default manager for trading. + + output.status = 0; // FAILURE_GENERAL + output.transferredNumberOfShares = 0; + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.amount = input.numberOfShares; + + if (qpi.invocationReward() < (sint64)MSVAULT_REVOKE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.transferredNumberOfShares = 0; + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)MSVAULT_REVOKE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)MSVAULT_REVOKE_FEE); + } + + // must transfer a positive number of shares. + if (input.numberOfShares <= 0) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - MSVAULT_RELEASE_FEE); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.transferredNumberOfShares = 0; + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; } - // [TODO]: Change this to - //state.totalRevenue += state.liveReleaseFee; - state.totalRevenue += MSVAULT_RELEASE_FEE; + + // Check if MsVault actually manages the specified number of shares for the caller. + locals.managedBalance = qpi.numberOfShares( + input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX } + ); + + if (locals.managedBalance < input.numberOfShares) + { + // The user is trying to revoke more shares than are managed by MsVault. + output.transferredNumberOfShares = 0; + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + else + { + // The balance check passed. Proceed to release the management rights. + locals.result = qpi.releaseShares( + input.asset, + qpi.invocator(), // owner + qpi.invocator(), // possessor + input.numberOfShares, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + MSVAULT_REVOKE_FEE + ); + + if (locals.result < 0) + { + output.transferredNumberOfShares = 0; + output.status = 8; // FAILURE_TRANSFER_FAILED + } + else + { + output.transferredNumberOfShares = input.numberOfShares; + output.status = 1; // SUCCESS + } + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + } + + PUBLIC_PROCEDURE_WITH_LOCALS(depositAsset) + { + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + locals.logger.vaultId = input.vaultId; + locals.logger.amount = input.amount; + + if (qpi.invocationReward() < (sint64)state.liveDepositFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)state.liveDepositFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveDepositFee); + } + + locals.userAssetBalance = qpi.numberOfShares(input.asset, + { qpi.invocator(), SELF_INDEX }, + { qpi.invocator(), SELF_INDEX }); + + if (locals.userAssetBalance < (sint64)input.amount || input.amount == 0) + { + // User does not have enough shares, or is trying to deposit zero. Abort and refund the fee. + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // check if vault id is valid and the vault is active + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + if (!locals.iv_out.result) + { + output.status = 3; // FAILURE_INVALID_VAULT + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; // invalid vault id + } + + locals.qubicVault = state.vaults.get(input.vaultId); + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + if (!locals.qubicVault.isActive) + { + output.status = 3; // FAILURE_INVALID_VAULT + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; // vault is not active + } + + // check if the vault has room for a new asset type + locals.assetIndex = -1; + for (locals.i = 0; locals.i < locals.assetVault.numberOfAssetTypes; locals.i++) + { + locals.ab = locals.assetVault.assetBalances.get(locals.i); + if (locals.ab.asset.assetName == input.asset.assetName && locals.ab.asset.issuer == input.asset.issuer) + { + locals.assetIndex = locals.i; + break; + } + } + + // if the asset is new to this vault, check if there's an empty slot. + if (locals.assetIndex == -1 && locals.assetVault.numberOfAssetTypes >= MSVAULT_MAX_ASSET_TYPES) + { + // no more new asset + output.status = 7; // FAILURE_LIMIT_REACHED + qpi.transfer(qpi.invocator(), state.liveDepositFee); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // All checks passed, now perform the transfer of ownership. + state.totalRevenue += state.liveDepositFee; + + locals.tempShares = qpi.numberOfShares( + input.asset, + { SELF, SELF_INDEX }, + { SELF, SELF_INDEX } + ); + + locals.qx_in.assetName = input.asset.assetName; + locals.qx_in.issuer = input.asset.issuer; + locals.qx_in.numberOfShares = input.amount; + locals.qx_in.newOwnerAndPossessor = SELF; + + locals.transferResult = qpi.transferShareOwnershipAndPossession(input.asset.assetName, input.asset.issuer, qpi.invocator(), qpi.invocator(), input.amount, SELF); + + if (locals.transferResult < 0) + { + output.status = 8; // FAILURE_TRANSFER_FAILED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.transferedShares = qpi.numberOfShares(input.asset, { SELF, SELF_INDEX }, { SELF, SELF_INDEX }) - locals.tempShares; + + if (locals.transferedShares != (sint64)input.amount) + { + output.status = 8; // FAILURE_TRANSFER_FAILED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // If the transfer succeeds, update the vault's internal accounting. + if (locals.assetIndex != -1) + { + // Asset type exists, update balance + locals.ab = locals.assetVault.assetBalances.get(locals.assetIndex); + locals.ab.balance += input.amount; + locals.assetVault.assetBalances.set(locals.assetIndex, locals.ab); + } + else + { + // Add the new asset type to the vault's balance list + locals.ab.asset = input.asset; + locals.ab.balance = input.amount; + locals.assetVault.assetBalances.set(locals.assetVault.numberOfAssetTypes, locals.ab); + locals.assetVault.numberOfAssetTypes++; + } + + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(releaseTo) + { + output.status = 0; // GENEREAL_FAILURE locals.logger._contractIndex = CONTRACT_INDEX; - locals.logger._type = 0; locals.logger.vaultId = input.vaultId; locals.logger.ownerID = qpi.invocator(); locals.logger.amount = input.amount; locals.logger.destination = input.destination; + if (qpi.invocationReward() < (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseFee); + } + + state.totalRevenue += state.liveReleaseFee; + locals.iv_in.vaultId = input.vaultId; isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); if (!locals.iv_out.result) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -651,7 +1242,9 @@ struct MSVAULT : public ContractBase if (!locals.vault.isActive) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -661,21 +1254,27 @@ struct MSVAULT : public ContractBase isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); if (!locals.io_out.result) { - locals.logger._type = 2; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } if (input.amount == 0 || input.destination == NULL_ID) { - locals.logger._type = 3; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } - if (locals.vault.balance < input.amount) + if (locals.vault.qubicBalance < input.amount) { - locals.logger._type = 5; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -688,9 +1287,9 @@ struct MSVAULT : public ContractBase locals.vault.releaseAmounts.set(locals.ownerIndex, input.amount); locals.vault.releaseDestinations.set(locals.ownerIndex, input.destination); + // Check for approvals locals.approvals = 0; - locals.totalOwners = (uint64)locals.vault.numberOfOwners; - for (locals.i = 0; locals.i < locals.totalOwners; locals.i++) + for (locals.i = 0; locals.i < (uint64)locals.vault.numberOfOwners; locals.i++) { if (locals.vault.releaseAmounts.get(locals.i) == input.amount && locals.vault.releaseDestinations.get(locals.i) == input.destination) @@ -708,10 +1307,10 @@ struct MSVAULT : public ContractBase if (locals.releaseApproved) { // Still need to re-check the balance before releasing funds - if (locals.vault.balance >= input.amount) + if (locals.vault.qubicBalance >= input.amount) { qpi.transfer(input.destination, input.amount); - locals.vault.balance -= input.amount; + locals.vault.qubicBalance -= input.amount; locals.rr_in.vault = locals.vault; resetReleaseRequests(qpi, state, locals.rr_in, locals.rr_out, locals.rr_locals); @@ -719,51 +1318,278 @@ struct MSVAULT : public ContractBase state.vaults.set(input.vaultId, locals.vault); - locals.logger._type = 4; + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } else { - locals.logger._type = 5; + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } } else { state.vaults.set(input.vaultId, locals.vault); - locals.logger._type = 6; + output.status = 9; // PENDING_APPROVAL + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } } - PUBLIC_PROCEDURE_WITH_LOCALS(resetRelease) + PUBLIC_PROCEDURE_WITH_LOCALS(releaseAssetTo) { - // [TODO]: Change this to - //if (qpi.invocationReward() > state.liveReleaseResetFee) - //{ - // qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.liveReleaseResetFee); - //} - if (qpi.invocationReward() > MSVAULT_RELEASE_RESET_FEE) + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.vaultId = input.vaultId; + locals.logger.ownerID = qpi.invocator(); + locals.logger.amount = input.amount; + locals.logger.destination = input.destination; + + if (qpi.invocationReward() < (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (qpi.invocationReward() > (sint64)state.liveReleaseFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseFee); + } + + state.totalRevenue += state.liveReleaseFee; + + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - MSVAULT_RELEASE_RESET_FEE); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.qubicVault = state.vaults.get(input.vaultId); + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + + if (!locals.qubicVault.isActive) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.io_in.vault = locals.qubicVault; + locals.io_in.ownerID = qpi.invocator(); + isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); + if (!locals.io_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; } - // [TODO]: Change this to - //state.totalRevenue += state.liveReleaseResetFee; - state.totalRevenue += MSVAULT_RELEASE_RESET_FEE; + + if (locals.qubicVault.qubicBalance < MSVAULT_REVOKE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + if (input.amount == 0 || input.destination == NULL_ID) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // Find the asset in the vault + locals.assetIndex = -1; + for (locals.i = 0; locals.i < locals.assetVault.numberOfAssetTypes; locals.i++) + { + locals.ab = locals.assetVault.assetBalances.get(locals.i); + if (locals.ab.asset.assetName == input.asset.assetName && locals.ab.asset.issuer == input.asset.issuer) + { + locals.assetIndex = locals.i; + break; + } + } + if (locals.assetIndex == -1) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 5; // FAILURE_INVALID_PARAMS + locals.logger._type = (uint32)output.status; // Asset not found + LOG_INFO(locals.logger); + return; + } + + if (locals.assetVault.assetBalances.get(locals.assetIndex).balance < input.amount) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + // Record the release request + locals.fi_in.vault = locals.qubicVault; + locals.fi_in.ownerID = qpi.invocator(); + findOwnerIndexInVault(qpi, state, locals.fi_in, locals.fi_out, locals.fi_locals); + locals.ownerIndex = locals.fi_out.index; + + locals.assetVault.releaseAssets.set(locals.ownerIndex, input.asset); + locals.assetVault.releaseAssetAmounts.set(locals.ownerIndex, input.amount); + locals.assetVault.releaseAssetDestinations.set(locals.ownerIndex, input.destination); + + // Check for approvals + locals.approvals = 0; + for (locals.i = 0; locals.i < (uint64)locals.qubicVault.numberOfOwners; locals.i++) + { + if (locals.assetVault.releaseAssetAmounts.get(locals.i) == input.amount && + locals.assetVault.releaseAssetDestinations.get(locals.i) == input.destination && + locals.assetVault.releaseAssets.get(locals.i).assetName == input.asset.assetName && + locals.assetVault.releaseAssets.get(locals.i).issuer == input.asset.issuer) + { + locals.approvals++; + } + } + + locals.releaseApproved = false; + if (locals.approvals >= (uint64)locals.qubicVault.requiredApprovals) + { + locals.releaseApproved = true; + } + + if (locals.releaseApproved) + { + // Re-check balance before transfer + if (locals.assetVault.assetBalances.get(locals.assetIndex).balance >= input.amount) + { + locals.remainingShares = qpi.transferShareOwnershipAndPossession( + input.asset.assetName, + input.asset.issuer, + SELF, // owner + SELF, // possessor + input.amount, + input.destination // new owner & possessor + ); + if (locals.remainingShares >= 0) + { + // Update internal asset balance + locals.ab = locals.assetVault.assetBalances.get(locals.assetIndex); + locals.ab.balance -= input.amount; + locals.assetVault.assetBalances.set(locals.assetIndex, locals.ab); + + // Release management rights from MsVault to QX for the recipient + locals.releaseResult = qpi.releaseShares( + input.asset, + input.destination, // new owner + input.destination, // new possessor + input.amount, + QX_CONTRACT_INDEX, + QX_CONTRACT_INDEX, + MSVAULT_REVOKE_FEE + ); + + if (locals.releaseResult >= 0) + { + // Deduct the fee from the vault's balance upon success + locals.qubicVault.qubicBalance -= MSVAULT_REVOKE_FEE; + output.status = 1; // SUCCESS + } + else + { + // Log an error if management rights transfer fails + output.status = 8; // FAILURE_TRANSFER_FAILED + } + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + + // Reset all asset release requests + for (locals.i = 0; locals.i < MSVAULT_MAX_OWNERS; locals.i++) + { + locals.assetVault.releaseAssets.set(locals.i, { NULL_ID, 0 }); + locals.assetVault.releaseAssetAmounts.set(locals.i, 0); + locals.assetVault.releaseAssetDestinations.set(locals.i, NULL_ID); + } + } + else + { + output.status = 8; // FAILURE_TRANSFER_FAILED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + } + else + { + output.status = 6; // FAILURE_INSUFFICIENT_BALANCE + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + } + else + { + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + output.status = 9; // PENDING_APPROVAL + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + } + + PUBLIC_PROCEDURE_WITH_LOCALS(resetRelease) + { + output.status = 0; // GENEREAL_FAILURE locals.logger._contractIndex = CONTRACT_INDEX; - locals.logger._type = 0; locals.logger.vaultId = input.vaultId; locals.logger.ownerID = qpi.invocator(); locals.logger.amount = 0; locals.logger.destination = NULL_ID; + if (qpi.invocationReward() < (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + if (qpi.invocationReward() > (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseResetFee); + } + + state.totalRevenue += state.liveReleaseResetFee; + locals.iv_in.vaultId = input.vaultId; isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); if (!locals.iv_out.result) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -772,7 +1598,9 @@ struct MSVAULT : public ContractBase if (!locals.vault.isActive) { - locals.logger._type = 1; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -782,7 +1610,9 @@ struct MSVAULT : public ContractBase isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); if (!locals.io_out.result) { - locals.logger._type = 2; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); return; } @@ -797,121 +1627,231 @@ struct MSVAULT : public ContractBase state.vaults.set(input.vaultId, locals.vault); - locals.logger._type = 7; + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + } + + PUBLIC_PROCEDURE_WITH_LOCALS(resetAssetRelease) + { + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.vaultId = input.vaultId; + locals.logger.ownerID = qpi.invocator(); + locals.logger.amount = 0; + locals.logger.destination = NULL_ID; + + if (qpi.invocationReward() < (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + if (qpi.invocationReward() > (sint64)state.liveReleaseResetFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - (sint64)state.liveReleaseResetFee); + } + + state.totalRevenue += state.liveReleaseResetFee; + + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.qubicVault = state.vaults.get(input.vaultId); + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + + if (!locals.qubicVault.isActive) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 3; // FAILURE_INVALID_VAULT + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.io_in.vault = locals.qubicVault; + locals.io_in.ownerID = qpi.invocator(); + isOwnerOfVault(qpi, state, locals.io_in, locals.io_out, locals.io_locals); + if (!locals.io_out.result) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.fi_in.vault = locals.qubicVault; + locals.fi_in.ownerID = qpi.invocator(); + findOwnerIndexInVault(qpi, state, locals.fi_in, locals.fi_out, locals.fi_locals); + locals.ownerIndex = locals.fi_out.index; + + locals.assetVault.releaseAssets.set(locals.ownerIndex, { NULL_ID, 0 }); + locals.assetVault.releaseAssetAmounts.set(locals.ownerIndex, 0); + locals.assetVault.releaseAssetDestinations.set(locals.ownerIndex, NULL_ID); + + state.vaults.set(input.vaultId, locals.qubicVault); + state.vaultAssetParts.set(input.vaultId, locals.assetVault); + + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; LOG_INFO(locals.logger); } - // [TODO]: Uncomment this to enable live fee update PUBLIC_PROCEDURE_WITH_LOCALS(voteFeeChange) { - // locals.ish_in.candidate = qpi.invocator(); - // isShareHolder(qpi, state, locals.ish_in, locals.ish_out, locals.ish_locals); - // if (!locals.ish_out.result) - // { - // return; - // } - // - // qpi.transfer(qpi.invocator(), qpi.invocationReward()); - // locals.nShare = qpi.numberOfPossessedShares(MSVAULT_ASSET_NAME, id::zero(), qpi.invocator(), qpi.invocator(), MSVAULT_CONTRACT_INDEX, MSVAULT_CONTRACT_INDEX); - // - // locals.fs.registeringFee = input.newRegisteringFee; - // locals.fs.releaseFee = input.newReleaseFee; - // locals.fs.releaseResetFee = input.newReleaseResetFee; - // locals.fs.holdingFee = input.newHoldingFee; - // locals.fs.depositFee = input.newDepositFee; - // // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 - // //locals.fs.burnFee = input.burnFee; - // - // locals.needNewRecord = true; - // for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) - // { - // locals.currentAddr = state.feeVotesOwner.get(locals.i); - // locals.realScore = qpi.numberOfPossessedShares(MSVAULT_ASSET_NAME, id::zero(), locals.currentAddr, locals.currentAddr, MSVAULT_CONTRACT_INDEX, MSVAULT_CONTRACT_INDEX); - // state.feeVotesScore.set(locals.i, locals.realScore); - // if (locals.currentAddr == qpi.invocator()) - // { - // locals.needNewRecord = false; - // } - // } - // if (locals.needNewRecord) - // { - // state.feeVotes.set(state.feeVotesAddrCount, locals.fs); - // state.feeVotesOwner.set(state.feeVotesAddrCount, qpi.invocator()); - // state.feeVotesScore.set(state.feeVotesAddrCount, locals.nShare); - // state.feeVotesAddrCount = state.feeVotesAddrCount + 1; - // } - // - // locals.sumVote = 0; - // for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) - // { - // locals.sumVote = locals.sumVote + state.feeVotesScore.get(locals.i); - // } - // if (locals.sumVote < QUORUM) - // { - // return; - // } - // - // state.uniqueFeeVotesCount = 0; - // for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) - // { - // locals.currentVote = state.feeVotes.get(locals.i); - // locals.found = false; - // locals.uniqueIndex = 0; - // locals.j; - // for (locals.j = 0; locals.j < state.uniqueFeeVotesCount; locals.j = locals.j + 1) - // { - // locals.uniqueVote = state.uniqueFeeVotes.get(locals.j); - // if (locals.uniqueVote.registeringFee == locals.currentVote.registeringFee && - // locals.uniqueVote.releaseFee == locals.currentVote.releaseFee && - // locals.uniqueVote.releaseResetFee == locals.currentVote.releaseResetFee && - // locals.uniqueVote.holdingFee == locals.currentVote.holdingFee && - // locals.uniqueVote.depositFee == locals.currentVote.depositFee - // // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 - // //&& locals.uniqueVote.burnFee == locals.currentVote.burnFee - // ) - // { - // locals.found = true; - // locals.uniqueIndex = locals.j; - // break; - // } - // } - // if (locals.found) - // { - // locals.currentRank = state.uniqueFeeVotesRanking.get(locals.uniqueIndex); - // state.uniqueFeeVotesRanking.set(locals.uniqueIndex, locals.currentRank + state.feeVotesScore.get(locals.i)); - // } - // else - // { - // state.uniqueFeeVotes.set(state.uniqueFeeVotesCount, locals.currentVote); - // state.uniqueFeeVotesRanking.set(state.uniqueFeeVotesCount, state.feeVotesScore.get(locals.i)); - // state.uniqueFeeVotesCount = state.uniqueFeeVotesCount + 1; - // } - // } - // - // for (locals.i = 0; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) - // { - // if (state.uniqueFeeVotesRanking.get(locals.i) >= QUORUM) - // { - // state.liveRegisteringFee = state.uniqueFeeVotes.get(locals.i).registeringFee; - // state.liveReleaseFee = state.uniqueFeeVotes.get(locals.i).releaseFee; - // state.liveReleaseResetFee = state.uniqueFeeVotes.get(locals.i).releaseResetFee; - // state.liveHoldingFee = state.uniqueFeeVotes.get(locals.i).holdingFee; - // state.liveDepositFee = state.uniqueFeeVotes.get(locals.i).depositFee; - // // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 - // //state.liveBurnFee = state.uniqueFeeVotes.get(locals.i).burnFee; - - // state.feeVotesAddrCount = 0; - // state.uniqueFeeVotesCount = 0; - // return; - // } - // } + output.status = 0; // GENEREAL_FAILURE + + locals.logger._contractIndex = CONTRACT_INDEX; + locals.logger.ownerID = qpi.invocator(); + + if (qpi.invocationReward() < (sint64)MSVAULT_VOTE_FEE_CHANGE_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + output.status = 2; // FAILURE_INSUFFICIENT_FEE + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + locals.ish_in.candidate = qpi.invocator(); + isShareHolder(qpi, state, locals.ish_in, locals.ish_out, locals.ish_locals); + if (!locals.ish_out.result) + { + output.status = 4; // FAILURE_NOT_AUTHORIZED + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + locals.nShare = qpi.numberOfShares({ NULL_ID, MSVAULT_ASSET_NAME }, AssetOwnershipSelect::byOwner(qpi.invocator()), AssetPossessionSelect::byPossessor(qpi.invocator())); + + locals.fs.registeringFee = input.newRegisteringFee; + locals.fs.releaseFee = input.newReleaseFee; + locals.fs.releaseResetFee = input.newReleaseResetFee; + locals.fs.holdingFee = input.newHoldingFee; + locals.fs.depositFee = input.newDepositFee; + // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 + //locals.fs.burnFee = input.burnFee; + + locals.needNewRecord = true; + for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + locals.currentAddr = state.feeVotesOwner.get(locals.i); + locals.realScore = qpi.numberOfShares({ NULL_ID, MSVAULT_ASSET_NAME }, AssetOwnershipSelect::byOwner(locals.currentAddr), AssetPossessionSelect::byPossessor(locals.currentAddr)); + state.feeVotesScore.set(locals.i, locals.realScore); + if (locals.currentAddr == qpi.invocator()) + { + locals.needNewRecord = false; + state.feeVotes.set(locals.i, locals.fs); // Update existing vote + } + } + if (locals.needNewRecord && state.feeVotesAddrCount < MSVAULT_MAX_FEE_VOTES) + { + state.feeVotes.set(state.feeVotesAddrCount, locals.fs); + state.feeVotesOwner.set(state.feeVotesAddrCount, qpi.invocator()); + state.feeVotesScore.set(state.feeVotesAddrCount, locals.nShare); + state.feeVotesAddrCount = state.feeVotesAddrCount + 1; + } + + locals.sumVote = 0; + for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + locals.sumVote = locals.sumVote + state.feeVotesScore.get(locals.i); + } + if (locals.sumVote < QUORUM) + { + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + + state.uniqueFeeVotesCount = 0; + // Reset unique vote ranking + for (locals.i = 0; locals.i < MSVAULT_MAX_FEE_VOTES; locals.i = locals.i + 1) + { + state.uniqueFeeVotesRanking.set(locals.i, 0); + } + + for (locals.i = 0; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + locals.currentVote = state.feeVotes.get(locals.i); + locals.found = false; + locals.uniqueIndex = 0; + for (locals.j = 0; locals.j < state.uniqueFeeVotesCount; locals.j = locals.j + 1) + { + locals.uniqueVote = state.uniqueFeeVotes.get(locals.j); + if (locals.uniqueVote.registeringFee == locals.currentVote.registeringFee && + locals.uniqueVote.releaseFee == locals.currentVote.releaseFee && + locals.uniqueVote.releaseResetFee == locals.currentVote.releaseResetFee && + locals.uniqueVote.holdingFee == locals.currentVote.holdingFee && + locals.uniqueVote.depositFee == locals.currentVote.depositFee + // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 + //&& locals.uniqueVote.burnFee == locals.currentVote.burnFee + ) + { + locals.found = true; + locals.uniqueIndex = locals.j; + break; + } + } + if (locals.found) + { + locals.currentRank = state.uniqueFeeVotesRanking.get(locals.uniqueIndex); + state.uniqueFeeVotesRanking.set(locals.uniqueIndex, locals.currentRank + state.feeVotesScore.get(locals.i)); + } + else if (state.uniqueFeeVotesCount < MSVAULT_MAX_FEE_VOTES) + { + state.uniqueFeeVotes.set(state.uniqueFeeVotesCount, locals.currentVote); + state.uniqueFeeVotesRanking.set(state.uniqueFeeVotesCount, state.feeVotesScore.get(locals.i)); + state.uniqueFeeVotesCount = state.uniqueFeeVotesCount + 1; + } + } + + for (locals.i = 0; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) + { + if (state.uniqueFeeVotesRanking.get(locals.i) >= QUORUM) + { + state.liveRegisteringFee = state.uniqueFeeVotes.get(locals.i).registeringFee; + state.liveReleaseFee = state.uniqueFeeVotes.get(locals.i).releaseFee; + state.liveReleaseResetFee = state.uniqueFeeVotes.get(locals.i).releaseResetFee; + state.liveHoldingFee = state.uniqueFeeVotes.get(locals.i).holdingFee; + state.liveDepositFee = state.uniqueFeeVotes.get(locals.i).depositFee; + // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 + //state.liveBurnFee = state.uniqueFeeVotes.get(locals.i).burnFee; + + state.feeVotesAddrCount = 0; + state.uniqueFeeVotesCount = 0; + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); + return; + } + } + output.status = 1; // SUCCESS + locals.logger._type = (uint32)output.status; + LOG_INFO(locals.logger); } PUBLIC_FUNCTION_WITH_LOCALS(getVaults) { output.numberOfVaults = 0ULL; locals.count = 0ULL; - for (locals.i = 0ULL; locals.i < MSVAULT_MAX_VAULTS; locals.i++) + for (locals.i = 0ULL; locals.i < MSVAULT_MAX_VAULTS && locals.count < MSVAULT_MAX_COOWNER; locals.i++) { locals.v = state.vaults.get(locals.i); if (locals.v.isActive) @@ -956,6 +1896,33 @@ struct MSVAULT : public ContractBase output.status = 1ULL; } + PUBLIC_FUNCTION_WITH_LOCALS(getAssetReleaseStatus) + { + output.status = 0ULL; + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + return; // output.status = false + } + + locals.qubicVault = state.vaults.get(input.vaultId); + if (!locals.qubicVault.isActive) + { + return; // output.status = false + } + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + + for (locals.i = 0; locals.i < (uint64)locals.qubicVault.numberOfOwners; locals.i++) + { + output.assets.set(locals.i, locals.assetVault.releaseAssets.get(locals.i)); + output.amounts.set(locals.i, locals.assetVault.releaseAssetAmounts.get(locals.i)); + output.destinations.set(locals.i, locals.assetVault.releaseAssetDestinations.get(locals.i)); + } + output.status = 1ULL; + } + PUBLIC_FUNCTION_WITH_LOCALS(getBalanceOf) { output.status = 0ULL; @@ -972,7 +1939,32 @@ struct MSVAULT : public ContractBase { return; // output.status = false } - output.balance = locals.vault.balance; + output.balance = locals.vault.qubicBalance; + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getVaultAssetBalances) + { + output.status = 0ULL; + locals.iv_in.vaultId = input.vaultId; + isValidVaultId(qpi, state, locals.iv_in, locals.iv_out, locals.iv_locals); + + if (!locals.iv_out.result) + { + return; // output.status = false + } + + locals.qubicVault = state.vaults.get(input.vaultId); + if (!locals.qubicVault.isActive) + { + return; // output.status = false + } + locals.assetVault = state.vaultAssetParts.get(input.vaultId); + output.numberOfAssetTypes = locals.assetVault.numberOfAssetTypes; + for (locals.i = 0; locals.i < locals.assetVault.numberOfAssetTypes; locals.i++) + { + output.assetBalances.set(locals.i, locals.assetVault.assetBalances.get(locals.i)); + } output.status = 1ULL; } @@ -1003,23 +1995,19 @@ struct MSVAULT : public ContractBase output.totalDistributedToShareholders = state.totalDistributedToShareholders; // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 //output.burnedAmount = state.burnedAmount; + output.burnedAmount = 0; } PUBLIC_FUNCTION(getFees) { - output.registeringFee = MSVAULT_REGISTERING_FEE; - output.releaseFee = MSVAULT_RELEASE_FEE; - output.releaseResetFee = MSVAULT_RELEASE_RESET_FEE; - output.holdingFee = MSVAULT_HOLDING_FEE; - output.depositFee = 0ULL; - // [TODO]: Change this to: - //output.registeringFee = state.liveRegisteringFee; - //output.releaseFee = state.liveReleaseFee; - //output.releaseResetFee = state.liveReleaseResetFee; - //output.holdingFee = state.liveHoldingFee; - //output.depositFee = state.liveDepositFee; + output.registeringFee = state.liveRegisteringFee; + output.releaseFee = state.liveReleaseFee; + output.releaseResetFee = state.liveReleaseResetFee; + output.holdingFee = state.liveHoldingFee; + output.depositFee = state.liveDepositFee; // [TODO]: Turn this ON when MSVAULT_BURN_FEE > 0 //output.burnFee = state.liveBurnFee; + output.burnFee = MSVAULT_BURN_FEE; } PUBLIC_FUNCTION_WITH_LOCALS(getVaultOwners) @@ -1053,17 +2041,93 @@ struct MSVAULT : public ContractBase output.status = 1ULL; } - // [TODO]: Uncomment this to enable live fee update PUBLIC_FUNCTION_WITH_LOCALS(isShareHolder) { - // if (qpi.numberOfPossessedShares(MSVAULT_ASSET_NAME, id::zero(), input.candidate, input.candidate, MSVAULT_CONTRACT_INDEX, MSVAULT_CONTRACT_INDEX) > 0) - // { - // output.result = 1ULL; - // } - // else - // { - // output.result = 0ULL; - // } + if (qpi.numberOfShares({ NULL_ID, MSVAULT_ASSET_NAME }, AssetOwnershipSelect::byOwner(input.candidate), + AssetPossessionSelect::byPossessor(input.candidate)) > 0) + { + output.result = 1ULL; + } + else + { + output.result = 0ULL; + } + } + + PUBLIC_FUNCTION_WITH_LOCALS(getFeeVotes) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + output.feeVotes.set(locals.i, state.feeVotes.get(locals.i)); + } + + output.numberOfFeeVotes = state.feeVotesAddrCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getFeeVotesOwner) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + output.feeVotesOwner.set(locals.i, state.feeVotesOwner.get(locals.i)); + } + output.numberOfFeeVotes = state.feeVotesAddrCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getFeeVotesScore) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.feeVotesAddrCount; locals.i = locals.i + 1) + { + output.feeVotesScore.set(locals.i, state.feeVotesScore.get(locals.i)); + } + output.numberOfFeeVotes = state.feeVotesAddrCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getUniqueFeeVotes) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) + { + output.uniqueFeeVotes.set(locals.i, state.uniqueFeeVotes.get(locals.i)); + } + output.numberOfUniqueFeeVotes = state.uniqueFeeVotesCount; + + output.status = 1ULL; + } + + PUBLIC_FUNCTION(getManagedAssetBalance) + { + // Get management rights balance the owner transferred to MsVault + output.balance = qpi.numberOfShares( + input.asset, + { input.owner, SELF_INDEX }, + { input.owner, SELF_INDEX } + ); + } + + PUBLIC_FUNCTION_WITH_LOCALS(getUniqueFeeVotesRanking) + { + output.status = 0ULL; + + for (locals.i = 0ULL; locals.i < state.uniqueFeeVotesCount; locals.i = locals.i + 1) + { + output.uniqueFeeVotesRanking.set(locals.i, state.uniqueFeeVotesRanking.get(locals.i)); + } + output.numberOfUniqueFeeVotes = state.uniqueFeeVotesCount; + + output.status = 1ULL; } INITIALIZE() @@ -1082,47 +2146,72 @@ struct MSVAULT : public ContractBase END_EPOCH_WITH_LOCALS() { + locals.qxAdress = id(QX_CONTRACT_INDEX, 0, 0, 0); for (locals.i = 0ULL; locals.i < MSVAULT_MAX_VAULTS; locals.i++) { - locals.v = state.vaults.get(locals.i); - if (locals.v.isActive) + locals.qubicVault = state.vaults.get(locals.i); + if (locals.qubicVault.isActive) { - // [TODO]: Change this to - //if (locals.v.balance >= state.liveHoldingFee) - //{ - // locals.v.balance -= state.liveHoldingFee; - // state.totalRevenue += state.liveHoldingFee; - // state.vaults.set(locals.i, locals.v); - //} - if (locals.v.balance >= MSVAULT_HOLDING_FEE) + if (locals.qubicVault.qubicBalance >= state.liveHoldingFee) { - locals.v.balance -= MSVAULT_HOLDING_FEE; - state.totalRevenue += MSVAULT_HOLDING_FEE; - state.vaults.set(locals.i, locals.v); + locals.qubicVault.qubicBalance -= state.liveHoldingFee; + state.totalRevenue += state.liveHoldingFee; + state.vaults.set(locals.i, locals.qubicVault); } else { // Not enough funds to pay holding fee - if (locals.v.balance > 0) + if (locals.qubicVault.qubicBalance > 0) + { + state.totalRevenue += locals.qubicVault.qubicBalance; + } + + locals.assetVault = state.vaultAssetParts.get(locals.i); + for (locals.k = 0; locals.k < locals.assetVault.numberOfAssetTypes; locals.k++) + { + locals.ab = locals.assetVault.assetBalances.get(locals.k); + if (locals.ab.balance > 0) + { + // Prepare the transfer request to QX + locals.qx_in.assetName = locals.ab.asset.assetName; + locals.qx_in.issuer = locals.ab.asset.issuer; + locals.qx_in.numberOfShares = locals.ab.balance; + locals.qx_in.newOwnerAndPossessor = locals.qxAdress; + + INVOKE_OTHER_CONTRACT_PROCEDURE(QX, TransferShareOwnershipAndPossession, locals.qx_in, locals.qx_out, 0); + } + } + locals.qubicVault.isActive = false; + locals.qubicVault.qubicBalance = 0; + locals.qubicVault.requiredApprovals = 0; + locals.qubicVault.vaultName = NULL_ID; + locals.qubicVault.numberOfOwners = 0; + for (locals.j = 0; locals.j < MSVAULT_MAX_OWNERS; locals.j++) { - state.totalRevenue += locals.v.balance; + locals.qubicVault.owners.set(locals.j, NULL_ID); + locals.qubicVault.releaseAmounts.set(locals.j, 0); + locals.qubicVault.releaseDestinations.set(locals.j, NULL_ID); + } + + // clear asset release proposals + locals.assetVault.numberOfAssetTypes = 0; + for (locals.j = 0; locals.j < MSVAULT_MAX_ASSET_TYPES; locals.j++) + { + locals.assetVault.assetBalances.set(locals.j, { { NULL_ID, 0 }, 0 }); } - locals.v.isActive = false; - locals.v.balance = 0; - locals.v.requiredApprovals = 0; - locals.v.vaultName = NULL_ID; - locals.v.numberOfOwners = 0; for (locals.j = 0; locals.j < MSVAULT_MAX_OWNERS; locals.j++) { - locals.v.owners.set(locals.j, NULL_ID); - locals.v.releaseAmounts.set(locals.j, 0); - locals.v.releaseDestinations.set(locals.j, NULL_ID); + locals.assetVault.releaseAssets.set(locals.j, { NULL_ID, 0 }); + locals.assetVault.releaseAssetAmounts.set(locals.j, 0); + locals.assetVault.releaseAssetDestinations.set(locals.j, NULL_ID); } + if (state.numberOfActiveVaults > 0) { state.numberOfActiveVaults--; } - state.vaults.set(locals.i, locals.v); + state.vaults.set(locals.i, locals.qubicVault); + state.vaultAssetParts.set(locals.i, locals.assetVault); } } } @@ -1165,5 +2254,29 @@ struct MSVAULT : public ContractBase REGISTER_USER_FUNCTION(getVaultOwners, 11); REGISTER_USER_FUNCTION(isShareHolder, 12); REGISTER_USER_PROCEDURE(voteFeeChange, 13); + REGISTER_USER_FUNCTION(getFeeVotes, 14); + REGISTER_USER_FUNCTION(getFeeVotesOwner, 15); + REGISTER_USER_FUNCTION(getFeeVotesScore, 16); + REGISTER_USER_FUNCTION(getUniqueFeeVotes, 17); + REGISTER_USER_FUNCTION(getUniqueFeeVotesRanking, 18); + // New asset-related functions and procedures + REGISTER_USER_PROCEDURE(depositAsset, 19); + REGISTER_USER_PROCEDURE(releaseAssetTo, 20); + REGISTER_USER_PROCEDURE(resetAssetRelease, 21); + REGISTER_USER_FUNCTION(getVaultAssetBalances, 22); + REGISTER_USER_FUNCTION(getAssetReleaseStatus, 23); + REGISTER_USER_FUNCTION(getManagedAssetBalance, 24); + REGISTER_USER_PROCEDURE(revokeAssetManagementRights, 25); + + } + + PRE_ACQUIRE_SHARES() + { + output.requestedFee = 0; + output.allowTransfer = true; + } + + POST_ACQUIRE_SHARES() + { } }; diff --git a/src/contracts/Nostromo.h b/src/contracts/Nostromo.h index 2fe184712..4b0480036 100644 --- a/src/contracts/Nostromo.h +++ b/src/contracts/Nostromo.h @@ -572,6 +572,14 @@ struct NOST : public ContractBase } return ; } + if (QUOTTERY::checkValidQtryDateTime(locals.firstPhaseStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.firstPhaseEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.secondPhaseStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.secondPhaseEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.thirdPhaseStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.thirdPhaseEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.listingStartDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.cliffEndDate) == 0 || QUOTTERY::checkValidQtryDateTime(locals.vestingEndDate) == 0) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (input.stepOfVesting == 0 || input.stepOfVesting > 12 || input.TGE > 50 || input.threshold > 50 || input.indexOfProject >= state.numberOfCreatedProject) { @@ -923,6 +931,14 @@ struct NOST : public ContractBase } locals.tmpFundraising.raisedFunds += qpi.invocationReward(); } + else + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return ; + } if (locals.minCap <= locals.tmpFundraising.raisedFunds && locals.tmpFundraising.isCreatedToken == 0) { locals.input.assetName = state.projects.get(locals.tmpFundraising.indexOfProject).tokenName; @@ -1131,7 +1147,6 @@ struct NOST : public ContractBase { // success output.transferredNumberOfShares = input.numberOfShares; - qpi.transfer(id(QX_CONTRACT_INDEX, 0, 0, 0), state.transferRightsFee); if (qpi.invocationReward() > state.transferRightsFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.transferRightsFee); diff --git a/src/contracts/QBond.h b/src/contracts/QBond.h new file mode 100644 index 000000000..10488fafe --- /dev/null +++ b/src/contracts/QBond.h @@ -0,0 +1,1338 @@ +using namespace QPI; + +constexpr uint64 QBOND_MAX_EPOCH_COUNT = 1024ULL; +constexpr uint64 QBOND_MBOND_PRICE = 1000000ULL; +constexpr uint64 QBOND_MAX_QUEUE_SIZE = 10ULL; +constexpr uint64 QBOND_MIN_MBONDS_TO_STAKE = 10ULL; +constexpr sint64 QBOND_MBONDS_EMISSION = 1000000000LL; +constexpr uint16 QBOND_START_EPOCH = 182; + +constexpr uint64 QBOND_STAKE_FEE_PERCENT = 50; // 0.5% +constexpr uint64 QBOND_TRADE_FEE_PERCENT = 3; // 0.03% +constexpr uint64 QBOND_MBOND_TRANSFER_FEE = 100; + +constexpr uint64 QBOND_QVAULT_DISTRIBUTION_PERCENT = 9900; // 99% + +struct QBOND2 +{ +}; + +struct QBOND : public ContractBase +{ +public: + struct StakeEntry + { + id staker; + sint64 amount; + }; + + struct MBondInfo + { + uint64 name; + sint64 stakersAmount; + sint64 totalStaked; + }; + + struct Stake_input + { + sint64 quMillions; + }; + struct Stake_output + { + }; + + struct TransferMBondOwnershipAndPossession_input + { + id newOwnerAndPossessor; + sint64 epoch; + sint64 numberOfMBonds; + }; + struct TransferMBondOwnershipAndPossession_output + { + sint64 transferredMBonds; + }; + + struct AddAskOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct AddAskOrder_output + { + sint64 addedMBondsAmount; + }; + + struct RemoveAskOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct RemoveAskOrder_output + { + sint64 removedMBondsAmount; + }; + + struct AddBidOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct AddBidOrder_output + { + sint64 addedMBondsAmount; + }; + + struct RemoveBidOrder_input + { + sint64 epoch; + sint64 price; + sint64 numberOfMBonds; + }; + struct RemoveBidOrder_output + { + sint64 removedMBondsAmount; + }; + + struct BurnQU_input + { + sint64 amount; + }; + struct BurnQU_output + { + sint64 amount; + }; + + struct UpdateCFA_input + { + id user; + bit operation; // 0 to remove, 1 to add + }; + struct UpdateCFA_output + { + bit result; + }; + + struct GetFees_input + { + }; + struct GetFees_output + { + uint64 stakeFeePercent; + uint64 tradeFeePercent; + uint64 transferFee; + }; + + struct GetEarnedFees_input + { + }; + struct GetEarnedFees_output + { + uint64 stakeFees; + uint64 tradeFees; + }; + + struct GetInfoPerEpoch_input + { + sint64 epoch; + }; + struct GetInfoPerEpoch_output + { + uint64 stakersAmount; + sint64 totalStaked; + sint64 apy; + }; + + struct GetOrders_input + { + sint64 epoch; + sint64 askOrdersOffset; + sint64 bidOrdersOffset; + }; + struct GetOrders_output + { + struct Order + { + id owner; + sint64 epoch; + sint64 numberOfMBonds; + sint64 price; + }; + + Array askOrders; + Array bidOrders; + }; + + struct GetUserOrders_input + { + id owner; + sint64 askOrdersOffset; + sint64 bidOrdersOffset; + }; + struct GetUserOrders_output + { + struct Order + { + id owner; + sint64 epoch; + sint64 numberOfMBonds; + sint64 price; + }; + + Array askOrders; + Array bidOrders; + }; + + struct GetMBondsTable_input + { + }; + struct GetMBondsTable_output + { + struct TableEntry + { + sint64 epoch; + sint64 totalStakedQBond; + sint64 totalStakedQEarn; + uint64 apy; + }; + Array info; + }; + + struct GetUserMBonds_input + { + id owner; + }; + struct GetUserMBonds_output + { + sint64 totalMBondsAmount; + struct MBondEntity + { + sint64 epoch; + sint64 amount; + uint64 apy; + }; + Array mbonds; + }; + + struct GetCFA_input + { + }; + struct GetCFA_output + { + Array commissionFreeAddresses; + }; + +protected: + Array _stakeQueue; + HashMap _epochMbondInfoMap; + HashMap _userTotalStakedMap; + HashSet _commissionFreeAddresses; + uint64 _qearnIncomeAmount; + uint64 _totalEarnedAmount; + uint64 _earnedAmountFromTrade; + uint64 _distributedAmount; + id _adminAddress; + id _devAddress; + + struct _Order + { + id owner; + sint64 epoch; + sint64 numberOfMBonds; + }; + Collection<_Order, 1048576> _askOrders; + Collection<_Order, 1048576> _bidOrders; + + struct _NumberOfReservedMBonds_input + { + id owner; + sint64 epoch; + } _numberOfReservedMBonds_input; + + struct _NumberOfReservedMBonds_output + { + sint64 amount; + } _numberOfReservedMBonds_output; + + struct _NumberOfReservedMBonds_locals + { + sint64 elementIndex; + id mbondIdentity; + _Order order; + MBondInfo tempMbondInfo; + }; + + PRIVATE_FUNCTION_WITH_LOCALS(_NumberOfReservedMBonds) + { + output.amount = 0; + if (!state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX) + { + locals.order = state._askOrders.element(locals.elementIndex); + if (locals.order.epoch == input.epoch && locals.order.owner == input.owner) + { + output.amount += locals.order.numberOfMBonds; + } + + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + } + + struct Stake_locals + { + sint64 amountInQueue; + sint64 userMBondsAmount; + sint64 tempAmount; + uint64 counter; + sint64 amountToStake; + uint64 amountAndFee; + StakeEntry tempStakeEntry; + MBondInfo tempMbondInfo; + QEARN::lock_input lock_input; + QEARN::lock_output lock_output; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(Stake) + { + locals.amountAndFee = sadd(smul((uint64) input.quMillions, QBOND_MBOND_PRICE), div(smul(smul((uint64) input.quMillions, QBOND_MBOND_PRICE), QBOND_STAKE_FEE_PERCENT), 10000ULL)); + + if (input.quMillions <= 0 + || input.quMillions >= MAX_AMOUNT + || !state._epochMbondInfoMap.get(qpi.epoch(), locals.tempMbondInfo) + || qpi.invocationReward() < 0 + || (uint64) qpi.invocationReward() < locals.amountAndFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if ((uint64) qpi.invocationReward() > locals.amountAndFee) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.amountAndFee); + } + + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), div(smul((uint64) input.quMillions, QBOND_MBOND_PRICE) * QBOND_STAKE_FEE_PERCENT, 10000ULL)); + } + else + { + state._totalEarnedAmount += div(smul((uint64) input.quMillions, QBOND_MBOND_PRICE) * QBOND_STAKE_FEE_PERCENT, 10000ULL); + } + + locals.amountInQueue = input.quMillions; + for (locals.counter = 0; locals.counter < QBOND_MAX_QUEUE_SIZE; locals.counter++) + { + if (state._stakeQueue.get(locals.counter).staker != NULL_ID) + { + locals.amountInQueue += state._stakeQueue.get(locals.counter).amount; + } + else + { + locals.tempStakeEntry.staker = qpi.invocator(); + locals.tempStakeEntry.amount = input.quMillions; + state._stakeQueue.set(locals.counter, locals.tempStakeEntry); + break; + } + } + + if (locals.amountInQueue < QBOND_MIN_MBONDS_TO_STAKE) + { + return; + } + + locals.tempStakeEntry.staker = NULL_ID; + locals.tempStakeEntry.amount = 0; + locals.amountToStake = 0; + for (locals.counter = 0; locals.counter < QBOND_MAX_QUEUE_SIZE; locals.counter++) + { + if (state._stakeQueue.get(locals.counter).staker == NULL_ID) + { + break; + } + + if (state._userTotalStakedMap.get(state._stakeQueue.get(locals.counter).staker, locals.userMBondsAmount)) + { + state._userTotalStakedMap.replace(state._stakeQueue.get(locals.counter).staker, locals.userMBondsAmount + state._stakeQueue.get(locals.counter).amount); + } + else + { + state._userTotalStakedMap.set(state._stakeQueue.get(locals.counter).staker, state._stakeQueue.get(locals.counter).amount); + } + + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, state._stakeQueue.get(locals.counter).staker, state._stakeQueue.get(locals.counter).staker, SELF_INDEX, SELF_INDEX) <= 0) + { + locals.tempMbondInfo.stakersAmount++; + } + qpi.transferShareOwnershipAndPossession(locals.tempMbondInfo.name, SELF, SELF, SELF, state._stakeQueue.get(locals.counter).amount, state._stakeQueue.get(locals.counter).staker); + locals.amountToStake += state._stakeQueue.get(locals.counter).amount; + state._stakeQueue.set(locals.counter, locals.tempStakeEntry); + } + + locals.tempMbondInfo.totalStaked += locals.amountToStake; + state._epochMbondInfoMap.replace(qpi.epoch(), locals.tempMbondInfo); + + INVOKE_OTHER_CONTRACT_PROCEDURE(QEARN, lock, locals.lock_input, locals.lock_output, locals.amountToStake * QBOND_MBOND_PRICE); + } + + struct TransferMBondOwnershipAndPossession_locals + { + MBondInfo tempMbondInfo; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(TransferMBondOwnershipAndPossession) + { + if (input.numberOfMBonds >= MAX_AMOUNT || input.numberOfMBonds <= 0 || qpi.invocationReward() < QBOND_MBOND_TRANSFER_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if (qpi.invocationReward() > QBOND_MBOND_TRANSFER_FEE) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QBOND_MBOND_TRANSFER_FEE); + } + + state._numberOfReservedMBonds_input.epoch = input.epoch; + state._numberOfReservedMBonds_input.owner = qpi.invocator(); + CALL(_NumberOfReservedMBonds, state._numberOfReservedMBonds_input, state._numberOfReservedMBonds_output); + + if (state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo) + && qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) - state._numberOfReservedMBonds_output.amount < input.numberOfMBonds) + { + output.transferredMBonds = 0; + qpi.transfer(qpi.invocator(), QBOND_MBOND_TRANSFER_FEE); + } + else + { + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, input.newOwnerAndPossessor, input.newOwnerAndPossessor, SELF_INDEX, SELF_INDEX) <= 0) + { + locals.tempMbondInfo.stakersAmount++; + } + output.transferredMBonds = qpi.transferShareOwnershipAndPossession(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), input.numberOfMBonds, input.newOwnerAndPossessor) < 0 ? 0 : input.numberOfMBonds; + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) <= 0) + { + locals.tempMbondInfo.stakersAmount--; + } + state._epochMbondInfoMap.replace((uint16)input.epoch, locals.tempMbondInfo); + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), QBOND_MBOND_TRANSFER_FEE); + } + else + { + state._totalEarnedAmount += QBOND_MBOND_TRANSFER_FEE; + } + } + } + + struct AddAskOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + sint64 nextElementIndex; + sint64 fee; + _Order tempAskOrder; + _Order tempBidOrder; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(AddAskOrder) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (input.price <= 0 || input.price >= MAX_AMOUNT || input.numberOfMBonds <= 0 || input.numberOfMBonds >= MAX_AMOUNT || !state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + output.addedMBondsAmount = 0; + return; + } + + state._numberOfReservedMBonds_input.epoch = input.epoch; + state._numberOfReservedMBonds_input.owner = qpi.invocator(); + CALL(_NumberOfReservedMBonds, state._numberOfReservedMBonds_input, state._numberOfReservedMBonds_output); + if (qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) - state._numberOfReservedMBonds_output.amount < input.numberOfMBonds) + { + output.addedMBondsAmount = 0; + return; + } + + output.addedMBondsAmount = input.numberOfMBonds; + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price > state._bidOrders.priority(locals.elementIndex)) + { + break; + } + + locals.nextElementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + + locals.tempBidOrder = state._bidOrders.element(locals.elementIndex); + if (input.numberOfMBonds <= locals.tempBidOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + qpi.invocator(), + qpi.invocator(), + input.numberOfMBonds, + locals.tempBidOrder.owner); + + locals.fee = div(input.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(qpi.invocator(), input.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) - locals.fee); + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), locals.fee); + } + else + { + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + + if (input.numberOfMBonds < locals.tempBidOrder.numberOfMBonds) + { + locals.tempBidOrder.numberOfMBonds -= input.numberOfMBonds; + state._bidOrders.replace(locals.elementIndex, locals.tempBidOrder); + } + else if (input.numberOfMBonds == locals.tempBidOrder.numberOfMBonds) + { + state._bidOrders.remove(locals.elementIndex); + } + return; + } + else if (input.numberOfMBonds > locals.tempBidOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + qpi.invocator(), + qpi.invocator(), + locals.tempBidOrder.numberOfMBonds, + locals.tempBidOrder.owner); + + locals.fee = div(locals.tempBidOrder.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(qpi.invocator(), locals.tempBidOrder.numberOfMBonds * state._bidOrders.priority(locals.elementIndex) - locals.fee); + if (state._commissionFreeAddresses.getElementIndex(qpi.invocator()) != NULL_INDEX) + { + qpi.transfer(qpi.invocator(), locals.fee); + } + else + { + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + state._bidOrders.remove(locals.elementIndex); + input.numberOfMBonds -= locals.tempBidOrder.numberOfMBonds; + } + + locals.elementIndex = locals.nextElementIndex; + } + + if (state._askOrders.population(locals.mbondIdentity) == 0) + { + locals.tempAskOrder.epoch = input.epoch; + locals.tempAskOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempAskOrder.owner = qpi.invocator(); + state._askOrders.add(locals.mbondIdentity, locals.tempAskOrder, -input.price); + return; + } + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price < -state._askOrders.priority(locals.elementIndex)) + { + locals.tempAskOrder.epoch = input.epoch; + locals.tempAskOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempAskOrder.owner = qpi.invocator(); + state._askOrders.add(locals.mbondIdentity, locals.tempAskOrder, -input.price); + break; + } + else if (input.price == -state._askOrders.priority(locals.elementIndex)) + { + if (state._askOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + locals.tempAskOrder = state._askOrders.element(locals.elementIndex); + locals.tempAskOrder.numberOfMBonds += input.numberOfMBonds; + state._askOrders.replace(locals.elementIndex, locals.tempAskOrder); + break; + } + } + + if (state._askOrders.nextElementIndex(locals.elementIndex) == NULL_INDEX) + { + locals.tempAskOrder.epoch = input.epoch; + locals.tempAskOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempAskOrder.owner = qpi.invocator(); + state._askOrders.add(locals.mbondIdentity, locals.tempAskOrder, -input.price); + break; + } + + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + } + + struct RemoveAskOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + _Order order; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(RemoveAskOrder) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.removedMBondsAmount = 0; + + if (input.price <= 0 || input.price >= MAX_AMOUNT || input.numberOfMBonds <= 0 || input.numberOfMBonds >= MAX_AMOUNT || !state._epochMbondInfoMap.get((uint16) input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price == -state._askOrders.priority(locals.elementIndex) && state._askOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + if (state._askOrders.element(locals.elementIndex).numberOfMBonds <= input.numberOfMBonds) + { + output.removedMBondsAmount = state._askOrders.element(locals.elementIndex).numberOfMBonds; + state._askOrders.remove(locals.elementIndex); + } + else + { + locals.order = state._askOrders.element(locals.elementIndex); + locals.order.numberOfMBonds -= input.numberOfMBonds; + state._askOrders.replace(locals.elementIndex, locals.order); + output.removedMBondsAmount = input.numberOfMBonds; + } + break; + } + + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + } + + struct AddBidOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + sint64 nextElementIndex; + sint64 fee; + _Order tempAskOrder; + _Order tempBidOrder; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(AddBidOrder) + { + if (qpi.invocationReward() < smul(input.numberOfMBonds, input.price) + || input.price <= 0 + || input.price >= MAX_AMOUNT + || input.numberOfMBonds <= 0 + || input.numberOfMBonds >= MAX_AMOUNT + || !state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + output.addedMBondsAmount = 0; + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + if (qpi.invocationReward() > smul(input.numberOfMBonds, input.price)) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - smul(input.numberOfMBonds, input.price)); + } + + output.addedMBondsAmount = input.numberOfMBonds; + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price < -state._askOrders.priority(locals.elementIndex)) + { + break; + } + + locals.nextElementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + + locals.tempAskOrder = state._askOrders.element(locals.elementIndex); + if (input.numberOfMBonds <= locals.tempAskOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + locals.tempAskOrder.owner, + locals.tempAskOrder.owner, + input.numberOfMBonds, + qpi.invocator()); + + if (state._commissionFreeAddresses.getElementIndex(locals.tempAskOrder.owner) != NULL_INDEX) + { + qpi.transfer(locals.tempAskOrder.owner, -(input.numberOfMBonds * state._askOrders.priority(locals.elementIndex))); + } + else + { + locals.fee = div(input.numberOfMBonds * -state._askOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(locals.tempAskOrder.owner, -(input.numberOfMBonds * state._askOrders.priority(locals.elementIndex)) - locals.fee); + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + + if (input.price > -state._askOrders.priority(locals.elementIndex)) + { + qpi.transfer(qpi.invocator(), input.numberOfMBonds * (input.price + state._askOrders.priority(locals.elementIndex))); // ask orders priotiry is always negative + } + + if (input.numberOfMBonds < locals.tempAskOrder.numberOfMBonds) + { + locals.tempAskOrder.numberOfMBonds -= input.numberOfMBonds; + state._askOrders.replace(locals.elementIndex, locals.tempAskOrder); + } + else if (input.numberOfMBonds == locals.tempAskOrder.numberOfMBonds) + { + state._askOrders.remove(locals.elementIndex); + } + return; + } + else if (input.numberOfMBonds > locals.tempAskOrder.numberOfMBonds) + { + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + locals.tempAskOrder.owner, + locals.tempAskOrder.owner, + locals.tempAskOrder.numberOfMBonds, + qpi.invocator()); + + if (state._commissionFreeAddresses.getElementIndex(locals.tempAskOrder.owner) != NULL_INDEX) + { + qpi.transfer(locals.tempAskOrder.owner, -(locals.tempAskOrder.numberOfMBonds * state._askOrders.priority(locals.elementIndex))); + } + else + { + locals.fee = div(locals.tempAskOrder.numberOfMBonds * -state._askOrders.priority(locals.elementIndex) * QBOND_TRADE_FEE_PERCENT, 10000ULL); + qpi.transfer(locals.tempAskOrder.owner, -(locals.tempAskOrder.numberOfMBonds * state._askOrders.priority(locals.elementIndex)) - locals.fee); + state._totalEarnedAmount += locals.fee; + state._earnedAmountFromTrade += locals.fee; + } + + if (input.price > -state._askOrders.priority(locals.elementIndex)) + { + qpi.transfer(qpi.invocator(), locals.tempAskOrder.numberOfMBonds * (input.price + state._askOrders.priority(locals.elementIndex))); // ask orders priotiry is always negative + } + + state._askOrders.remove(locals.elementIndex); + input.numberOfMBonds -= locals.tempAskOrder.numberOfMBonds; + } + + locals.elementIndex = locals.nextElementIndex; + } + + if (state._bidOrders.population(locals.mbondIdentity) == 0) + { + locals.tempBidOrder.epoch = input.epoch; + locals.tempBidOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempBidOrder.owner = qpi.invocator(); + state._bidOrders.add(locals.mbondIdentity, locals.tempBidOrder, input.price); + return; + } + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price > state._bidOrders.priority(locals.elementIndex)) + { + locals.tempBidOrder.epoch = input.epoch; + locals.tempBidOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempBidOrder.owner = qpi.invocator(); + state._bidOrders.add(locals.mbondIdentity, locals.tempBidOrder, input.price); + break; + } + else if (input.price == state._bidOrders.priority(locals.elementIndex)) + { + if (state._bidOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + locals.tempBidOrder = state._bidOrders.element(locals.elementIndex); + locals.tempBidOrder.numberOfMBonds += input.numberOfMBonds; + state._bidOrders.replace(locals.elementIndex, locals.tempBidOrder); + break; + } + } + + if (state._bidOrders.nextElementIndex(locals.elementIndex) == NULL_INDEX) + { + locals.tempBidOrder.epoch = input.epoch; + locals.tempBidOrder.numberOfMBonds = input.numberOfMBonds; + locals.tempBidOrder.owner = qpi.invocator(); + state._bidOrders.add(locals.mbondIdentity, locals.tempBidOrder, input.price); + break; + } + + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + } + } + + struct RemoveBidOrder_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + _Order order; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(RemoveBidOrder) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + output.removedMBondsAmount = 0; + + if (input.price <= 0 || input.price >= MAX_AMOUNT || input.numberOfMBonds <= 0 || input.numberOfMBonds >= MAX_AMOUNT || !state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + if (input.price == state._bidOrders.priority(locals.elementIndex) && state._bidOrders.element(locals.elementIndex).owner == qpi.invocator()) + { + if (state._bidOrders.element(locals.elementIndex).numberOfMBonds <= input.numberOfMBonds) + { + output.removedMBondsAmount = state._bidOrders.element(locals.elementIndex).numberOfMBonds; + state._bidOrders.remove(locals.elementIndex); + } + else + { + locals.order = state._bidOrders.element(locals.elementIndex); + locals.order.numberOfMBonds -= input.numberOfMBonds; + state._bidOrders.replace(locals.elementIndex, locals.order); + output.removedMBondsAmount = input.numberOfMBonds; + } + qpi.transfer(qpi.invocator(), output.removedMBondsAmount * input.price); + break; + } + + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + } + } + + PUBLIC_PROCEDURE(BurnQU) + { + if (input.amount <= 0 || input.amount >= MAX_AMOUNT || qpi.invocationReward() < input.amount) + { + output.amount = -1; + if (input.amount == 0) + { + output.amount = 0; + } + + if (qpi.invocationReward() > 0 && qpi.invocationReward() < MAX_AMOUNT) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + if (qpi.invocationReward() > input.amount) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.amount); + } + + qpi.burn(input.amount); + output.amount = input.amount; + } + + PUBLIC_PROCEDURE(UpdateCFA) + { + if (qpi.invocationReward() > 0 && qpi.invocationReward() <= MAX_AMOUNT) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + + if (qpi.invocator() != state._adminAddress) + { + return; + } + + if (input.operation == 0) + { + output.result = state._commissionFreeAddresses.remove(input.user); + } + else + { + output.result = state._commissionFreeAddresses.add(input.user); + } + } + + struct GetInfoPerEpoch_locals + { + sint64 index; + QEARN::getLockInfoPerEpoch_input tempInput; + QEARN::getLockInfoPerEpoch_output tempOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetInfoPerEpoch) + { + output.totalStaked = 0; + output.stakersAmount = 0; + output.apy = 0; + + locals.index = state._epochMbondInfoMap.getElementIndex((uint16)input.epoch); + + if (locals.index == NULL_INDEX) + { + return; + } + + locals.tempInput.Epoch = (uint32) input.epoch; + CALL_OTHER_CONTRACT_FUNCTION(QEARN, getLockInfoPerEpoch, locals.tempInput, locals.tempOutput); + + output.totalStaked = state._epochMbondInfoMap.value(locals.index).totalStaked; + output.stakersAmount = state._epochMbondInfoMap.value(locals.index).stakersAmount; + output.apy = locals.tempOutput.yield; + } + + PUBLIC_FUNCTION(GetFees) + { + output.stakeFeePercent = QBOND_STAKE_FEE_PERCENT; + output.tradeFeePercent = QBOND_TRADE_FEE_PERCENT; + output.transferFee = QBOND_MBOND_TRANSFER_FEE; + } + + PUBLIC_FUNCTION(GetEarnedFees) + { + output.stakeFees = state._totalEarnedAmount - state._earnedAmountFromTrade; + output.tradeFees = state._earnedAmountFromTrade; + } + + struct GetOrders_locals + { + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex; + sint64 arrayElementIndex; + GetOrders_output::Order tempOrder; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetOrders) + { + if (!state._epochMbondInfoMap.get((uint16)input.epoch, locals.tempMbondInfo)) + { + return; + } + + locals.arrayElementIndex = 0; + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex != NULL_INDEX && locals.arrayElementIndex < 256) + { + if (input.askOrdersOffset > 0) + { + input.askOrdersOffset--; + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + continue; + } + + locals.tempOrder.owner = state._askOrders.element(locals.elementIndex).owner; + locals.tempOrder.epoch = state._askOrders.element(locals.elementIndex).epoch; + locals.tempOrder.numberOfMBonds = state._askOrders.element(locals.elementIndex).numberOfMBonds; + locals.tempOrder.price = -state._askOrders.priority(locals.elementIndex); + output.askOrders.set(locals.arrayElementIndex, locals.tempOrder); + locals.arrayElementIndex++; + locals.elementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + } + + locals.arrayElementIndex = 0; + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX && locals.arrayElementIndex < 256) + { + if (input.bidOrdersOffset > 0) + { + input.bidOrdersOffset--; + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + continue; + } + + locals.tempOrder.owner = state._bidOrders.element(locals.elementIndex).owner; + locals.tempOrder.epoch = state._bidOrders.element(locals.elementIndex).epoch; + locals.tempOrder.numberOfMBonds = state._bidOrders.element(locals.elementIndex).numberOfMBonds; + locals.tempOrder.price = state._bidOrders.priority(locals.elementIndex); + output.bidOrders.set(locals.arrayElementIndex, locals.tempOrder); + locals.arrayElementIndex++; + locals.elementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + } + } + + struct GetUserOrders_locals + { + sint64 epoch; + MBondInfo tempMbondInfo; + id mbondIdentity; + sint64 elementIndex1; + sint64 arrayElementIndex1; + sint64 elementIndex2; + sint64 arrayElementIndex2; + GetUserOrders_output::Order tempOrder; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserOrders) + { + for (locals.epoch = QBOND_START_EPOCH; locals.epoch <= qpi.epoch(); locals.epoch++) + { + if (!state._epochMbondInfoMap.get((uint16)locals.epoch, locals.tempMbondInfo)) + { + continue; + } + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex1 = state._askOrders.headIndex(locals.mbondIdentity, 0); + while (locals.elementIndex1 != NULL_INDEX && locals.arrayElementIndex1 < 256) + { + if (state._askOrders.element(locals.elementIndex1).owner != input.owner) + { + locals.elementIndex1 = state._askOrders.nextElementIndex(locals.elementIndex1); + continue; + } + + if (input.askOrdersOffset > 0) + { + input.askOrdersOffset--; + locals.elementIndex1 = state._askOrders.nextElementIndex(locals.elementIndex1); + continue; + } + + locals.tempOrder.owner = input.owner; + locals.tempOrder.epoch = state._askOrders.element(locals.elementIndex1).epoch; + locals.tempOrder.numberOfMBonds = state._askOrders.element(locals.elementIndex1).numberOfMBonds; + locals.tempOrder.price = -state._askOrders.priority(locals.elementIndex1); + output.askOrders.set(locals.arrayElementIndex1, locals.tempOrder); + locals.arrayElementIndex1++; + locals.elementIndex1 = state._askOrders.nextElementIndex(locals.elementIndex1); + } + + locals.elementIndex2 = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex2 != NULL_INDEX && locals.arrayElementIndex2 < 256) + { + if (state._bidOrders.element(locals.elementIndex2).owner != input.owner) + { + locals.elementIndex2 = state._bidOrders.nextElementIndex(locals.elementIndex2); + continue; + } + + if (input.bidOrdersOffset > 0) + { + input.bidOrdersOffset--; + locals.elementIndex2 = state._bidOrders.nextElementIndex(locals.elementIndex2); + continue; + } + + locals.tempOrder.owner = input.owner; + locals.tempOrder.epoch = state._bidOrders.element(locals.elementIndex2).epoch; + locals.tempOrder.numberOfMBonds = state._bidOrders.element(locals.elementIndex2).numberOfMBonds; + locals.tempOrder.price = state._bidOrders.priority(locals.elementIndex2); + output.bidOrders.set(locals.arrayElementIndex2, locals.tempOrder); + locals.arrayElementIndex2++; + locals.elementIndex2 = state._bidOrders.nextElementIndex(locals.elementIndex2); + } + } + } + + struct GetMBondsTable_locals + { + sint64 epoch; + sint64 index; + MBondInfo tempMBondInfo; + GetMBondsTable_output::TableEntry tempTableEntry; + QEARN::getLockInfoPerEpoch_input tempInput; + QEARN::getLockInfoPerEpoch_output tempOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetMBondsTable) + { + for (locals.epoch = QBOND_START_EPOCH; locals.epoch <= qpi.epoch(); locals.epoch++) + { + if (state._epochMbondInfoMap.get((uint16)locals.epoch, locals.tempMBondInfo)) + { + locals.tempInput.Epoch = (uint32) locals.epoch; + CALL_OTHER_CONTRACT_FUNCTION(QEARN, getLockInfoPerEpoch, locals.tempInput, locals.tempOutput); + locals.tempTableEntry.epoch = locals.epoch; + locals.tempTableEntry.totalStakedQBond = locals.tempMBondInfo.totalStaked * QBOND_MBOND_PRICE; + locals.tempTableEntry.totalStakedQEarn = locals.tempOutput.currentLockedAmount; + locals.tempTableEntry.apy = locals.tempOutput.yield; + output.info.set(locals.index, locals.tempTableEntry); + locals.index++; + } + } + } + + struct GetUserMBonds_locals + { + GetUserMBonds_output::MBondEntity tempMbondEntity; + sint64 epoch; + sint64 index; + sint64 mbondsAmount; + MBondInfo tempMBondInfo; + QEARN::getLockInfoPerEpoch_input tempInput; + QEARN::getLockInfoPerEpoch_output tempOutput; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetUserMBonds) + { + output.totalMBondsAmount = 0; + if (state._userTotalStakedMap.get(input.owner, locals.mbondsAmount)) + { + output.totalMBondsAmount = locals.mbondsAmount; + } + + for (locals.epoch = QBOND_START_EPOCH; locals.epoch <= qpi.epoch(); locals.epoch++) + { + if (!state._epochMbondInfoMap.get((uint16)locals.epoch, locals.tempMBondInfo)) + { + continue; + } + + locals.mbondsAmount = qpi.numberOfPossessedShares(locals.tempMBondInfo.name, SELF, input.owner, input.owner, SELF_INDEX, SELF_INDEX); + if (locals.mbondsAmount <= 0) + { + continue; + } + + locals.tempInput.Epoch = (uint32) locals.epoch; + CALL_OTHER_CONTRACT_FUNCTION(QEARN, getLockInfoPerEpoch, locals.tempInput, locals.tempOutput); + + locals.tempMbondEntity.epoch = locals.epoch; + locals.tempMbondEntity.amount = locals.mbondsAmount; + locals.tempMbondEntity.apy = locals.tempOutput.yield; + output.mbonds.set(locals.index, locals.tempMbondEntity); + locals.index++; + } + } + + struct GetCFA_locals + { + sint64 index; + sint64 counter; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(GetCFA) + { + locals.index = state._commissionFreeAddresses.nextElementIndex(NULL_INDEX); + while (locals.index != NULL_INDEX) + { + output.commissionFreeAddresses.set(locals.counter, state._commissionFreeAddresses.key(locals.index)); + locals.counter++; + locals.index = state._commissionFreeAddresses.nextElementIndex(locals.index); + } + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(Stake, 1); + REGISTER_USER_PROCEDURE(TransferMBondOwnershipAndPossession, 2); + REGISTER_USER_PROCEDURE(AddAskOrder, 3); + REGISTER_USER_PROCEDURE(RemoveAskOrder, 4); + REGISTER_USER_PROCEDURE(AddBidOrder, 5); + REGISTER_USER_PROCEDURE(RemoveBidOrder, 6); + REGISTER_USER_PROCEDURE(BurnQU, 7); + REGISTER_USER_PROCEDURE(UpdateCFA, 8); + + REGISTER_USER_FUNCTION(GetFees, 1); + REGISTER_USER_FUNCTION(GetEarnedFees, 2); + REGISTER_USER_FUNCTION(GetInfoPerEpoch, 3); + REGISTER_USER_FUNCTION(GetOrders, 4); + REGISTER_USER_FUNCTION(GetUserOrders, 5); + REGISTER_USER_FUNCTION(GetMBondsTable, 6); + REGISTER_USER_FUNCTION(GetUserMBonds, 7); + REGISTER_USER_FUNCTION(GetCFA, 8); + } + + INITIALIZE() + { + state._devAddress = ID(_B, _O, _N, _D, _D, _J, _N, _U, _H, _O, _G, _Y, _L, _A, _A, _A, _C, _V, _X, _C, _X, _F, _G, _F, _R, _C, _S, _D, _C, _U, _W, _C, _Y, _U, _N, _K, _M, _P, _G, _O, _I, _F, _E, _P, _O, _E, _M, _Y, _T, _L, _Q, _L, _F, _C, _S, _B); + state._adminAddress = ID(_B, _O, _N, _D, _A, _A, _F, _B, _U, _G, _H, _E, _L, _A, _N, _X, _G, _H, _N, _L, _M, _S, _U, _I, _V, _B, _K, _B, _H, _A, _Y, _E, _Q, _S, _Q, _B, _V, _P, _V, _N, _B, _H, _L, _F, _J, _I, _A, _Z, _F, _Q, _C, _W, _W, _B, _V, _E); + state._commissionFreeAddresses.add(state._adminAddress); + } + + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } + + struct BEGIN_EPOCH_locals + { + sint8 chunk; + uint64 currentName; + StakeEntry emptyEntry; + sint64 totalReward; + sint64 rewardPerMBond; + Asset tempAsset; + MBondInfo tempMbondInfo; + AssetOwnershipIterator assetIt; + id mbondIdentity; + sint64 elementIndex; + sint64 nextElementIndex; + }; + + BEGIN_EPOCH_WITH_LOCALS() + { + if (state._qearnIncomeAmount > 0 && state._epochMbondInfoMap.get((uint16) (qpi.epoch() - 53), locals.tempMbondInfo)) + { + locals.totalReward = state._qearnIncomeAmount - locals.tempMbondInfo.totalStaked * QBOND_MBOND_PRICE; + locals.rewardPerMBond = QPI::div(locals.totalReward, locals.tempMbondInfo.totalStaked); + + locals.tempAsset.assetName = locals.tempMbondInfo.name; + locals.tempAsset.issuer = SELF; + locals.assetIt.begin(locals.tempAsset); + while (!locals.assetIt.reachedEnd()) + { + qpi.transfer(locals.assetIt.owner(), (QBOND_MBOND_PRICE + locals.rewardPerMBond) * locals.assetIt.numberOfOwnedShares()); + qpi.transferShareOwnershipAndPossession( + locals.tempMbondInfo.name, + SELF, + locals.assetIt.owner(), + locals.assetIt.owner(), + locals.assetIt.numberOfOwnedShares(), + NULL_ID); + locals.assetIt.next(); + } + state._qearnIncomeAmount = 0; + + locals.mbondIdentity = SELF; + locals.mbondIdentity.u64._3 = locals.tempMbondInfo.name; + + locals.elementIndex = state._askOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + locals.nextElementIndex = state._askOrders.nextElementIndex(locals.elementIndex); + state._askOrders.remove(locals.elementIndex); + locals.elementIndex = locals.nextElementIndex; + } + + locals.elementIndex = state._bidOrders.headIndex(locals.mbondIdentity); + while (locals.elementIndex != NULL_INDEX) + { + locals.nextElementIndex = state._bidOrders.nextElementIndex(locals.elementIndex); + state._bidOrders.remove(locals.elementIndex); + locals.elementIndex = locals.nextElementIndex; + } + } + + locals.currentName = 1145979469ULL; // MBND + + locals.chunk = (sint8) (48 + mod(div((uint64)qpi.epoch(), 100ULL), 10ULL)); + locals.currentName |= (uint64)locals.chunk << (4 * 8); + + locals.chunk = (sint8) (48 + mod(div((uint64)qpi.epoch(), 10ULL), 10ULL)); + locals.currentName |= (uint64)locals.chunk << (5 * 8); + + locals.chunk = (sint8) (48 + mod((uint64)qpi.epoch(), 10ULL)); + locals.currentName |= (uint64)locals.chunk << (6 * 8); + + if (qpi.issueAsset(locals.currentName, SELF, 0, QBOND_MBONDS_EMISSION, 0) == QBOND_MBONDS_EMISSION) + { + locals.tempMbondInfo.name = locals.currentName; + locals.tempMbondInfo.totalStaked = 0; + locals.tempMbondInfo.stakersAmount = 0; + state._epochMbondInfoMap.set(qpi.epoch(), locals.tempMbondInfo); + } + + locals.emptyEntry.staker = NULL_ID; + locals.emptyEntry.amount = 0; + state._stakeQueue.setAll(locals.emptyEntry); + } + + struct POST_INCOMING_TRANSFER_locals + { + MBondInfo tempMbondInfo; + }; + + POST_INCOMING_TRANSFER_WITH_LOCALS() + { + if (input.sourceId == id(QEARN_CONTRACT_INDEX, 0, 0, 0) && state._epochMbondInfoMap.get(qpi.epoch() - 52, locals.tempMbondInfo)) + { + state._qearnIncomeAmount = input.amount; + } + } + + struct END_EPOCH_locals + { + sint64 availableMbonds; + MBondInfo tempMbondInfo; + sint64 counter; + StakeEntry tempStakeEntry; + sint64 amountToQvault; + sint64 amountToDev; + }; + + END_EPOCH_WITH_LOCALS() + { + locals.amountToQvault = div((state._totalEarnedAmount - state._distributedAmount) * QBOND_QVAULT_DISTRIBUTION_PERCENT, 10000ULL); + locals.amountToDev = state._totalEarnedAmount - state._distributedAmount - locals.amountToQvault; + qpi.transfer(id(QVAULT_CONTRACT_INDEX, 0, 0, 0), locals.amountToQvault); + qpi.transfer(state._devAddress, locals.amountToDev); + state._distributedAmount += locals.amountToQvault; + state._distributedAmount += locals.amountToDev; + + locals.tempStakeEntry.staker = NULL_ID; + locals.tempStakeEntry.amount = 0; + for (locals.counter = 0; locals.counter < QBOND_MAX_QUEUE_SIZE; locals.counter++) + { + if (state._stakeQueue.get(locals.counter).staker == NULL_ID) + { + break; + } + + qpi.transfer(state._stakeQueue.get(locals.counter).staker, state._stakeQueue.get(locals.counter).amount * QBOND_MBOND_PRICE); + state._stakeQueue.set(locals.counter, locals.tempStakeEntry); + } + + if (state._epochMbondInfoMap.get(qpi.epoch(), locals.tempMbondInfo)) + { + locals.availableMbonds = qpi.numberOfPossessedShares(locals.tempMbondInfo.name, SELF, SELF, SELF, SELF_INDEX, SELF_INDEX); + qpi.transferShareOwnershipAndPossession(locals.tempMbondInfo.name, SELF, SELF, SELF, locals.availableMbonds, NULL_ID); + } + + state._commissionFreeAddresses.cleanupIfNeeded(); + state._askOrders.cleanupIfNeeded(); + state._bidOrders.cleanupIfNeeded(); + } +}; diff --git a/src/contracts/QUtil.h b/src/contracts/QUtil.h index d9e957bb0..7d23f1c2d 100644 --- a/src/contracts/QUtil.h +++ b/src/contracts/QUtil.h @@ -32,30 +32,34 @@ constexpr uint64 QUTIL_MAX_ASSETS_PER_POLL = 16; // Maximum assets per poll constexpr sint64 QUTIL_VOTE_FEE = 100LL; // Fee for voting, burnt 100% constexpr sint64 QUTIL_POLL_CREATION_FEE = 10000000LL; // Fee for poll creation to prevent spam constexpr uint16 QUTIL_POLL_GITHUB_URL_MAX_SIZE = 256; // Max String Length for Poll's Github URLs -constexpr uint64 QUTIL_MAX_NEW_POLL = QUTIL_MAX_POLL / 4; // Max number of new poll per epoch +constexpr uint64 QUTIL_MAX_NEW_POLL = div(QUTIL_MAX_POLL, 4ULL); // Max number of new poll per epoch // Voting log types enum -const uint64 QutilLogTypePollCreated = 5; // Poll created successfully -const uint64 QutilLogTypeInsufficientFundsForPoll = 6; // Insufficient funds for poll creation -const uint64 QutilLogTypeInvalidPollType = 7; // Invalid poll type -const uint64 QutilLogTypeInvalidNumAssetsQubic = 8; // Invalid number of assets for Qubic poll -const uint64 QutilLogTypeInvalidNumAssetsAsset = 9; // Invalid number of assets for Asset poll -const uint64 QutilLogTypeVoteCast = 10; // Vote cast successfully -const uint64 QutilLogTypeInsufficientFundsForVote = 11; // Insufficient funds for voting -const uint64 QutilLogTypeInvalidPollId = 12; // Invalid poll ID -const uint64 QutilLogTypePollInactive = 13; // Poll is inactive -const uint64 QutilLogTypeInsufficientBalance = 14; // Insufficient voter balance -const uint64 QutilLogTypeInvalidOption = 15; // Invalid voting option -const uint64 QutilLogTypeInvalidPollIdResult = 16; // Invalid poll ID in GetCurrentResult -const uint64 QutilLogTypePollInactiveResult = 17; // Poll inactive in GetCurrentResult -const uint64 QutilLogTypeNoPollsByCreator = 18; // No polls found in GetPollsByCreator -const uint64 QutilLogTypePollCancelled = 19; // Poll cancelled successfully -const uint64 QutilLogTypeNotAuthorized = 20; // Not authorized to cancel the poll -const uint64 QutilLogTypeInsufficientFundsForCancel = 21; // Not have enough funds for poll calcellation -const uint64 QutilLogTypeMaxPollsReached = 22; // Max epoch per epoch reached - -struct QUtilLogger +constexpr uint64 QUTILLogTypePollCreated = 5; // Poll created successfully +constexpr uint64 QUTILLogTypeInsufficientFundsForPoll = 6; // Insufficient funds for poll creation +constexpr uint64 QUTILLogTypeInvalidPollType = 7; // Invalid poll type +constexpr uint64 QUTILLogTypeInvalidNumAssetsQubic = 8; // Invalid number of assets for Qubic poll +constexpr uint64 QUTILLogTypeInvalidNumAssetsAsset = 9; // Invalid number of assets for Asset poll +constexpr uint64 QUTILLogTypeVoteCast = 10; // Vote cast successfully +constexpr uint64 QUTILLogTypeInsufficientFundsForVote = 11; // Insufficient funds for voting +constexpr uint64 QUTILLogTypeInvalidPollId = 12; // Invalid poll ID +constexpr uint64 QUTILLogTypePollInactive = 13; // Poll is inactive +constexpr uint64 QUTILLogTypeInsufficientBalance = 14; // Insufficient voter balance +constexpr uint64 QUTILLogTypeInvalidOption = 15; // Invalid voting option +constexpr uint64 QUTILLogTypeInvalidPollIdResult = 16; // Invalid poll ID in GetCurrentResult +constexpr uint64 QUTILLogTypePollInactiveResult = 17; // Poll inactive in GetCurrentResult +constexpr uint64 QUTILLogTypeNoPollsByCreator = 18; // No polls found in GetPollsByCreator +constexpr uint64 QUTILLogTypePollCancelled = 19; // Poll cancelled successfully +constexpr uint64 QUTILLogTypeNotAuthorized = 20; // Not authorized to cancel the poll +constexpr uint64 QUTILLogTypeInsufficientFundsForCancel = 21; // Not have enough funds for poll calcellation +constexpr uint64 QUTILLogTypeMaxPollsReached = 22; // Max epoch per epoch reached + +// Fee per shareholder for DistributeQuToShareholders() +constexpr sint64 QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER = 5; + + +struct QUTILLogger { uint32 contractId; // to distinguish bw SCs uint32 padding; @@ -64,11 +68,25 @@ struct QUtilLogger sint64 amt; uint32 logtype; // Other data go here - char _terminator; // Only data before "_terminator" are logged + sint8 _terminator; // Only data before "_terminator" are logged }; +// Deactivate logger for delay function +#if 0 +struct QUTILDFLogger +{ + uint32 contractId; // to distinguish bw SCs + uint32 padding; + id dfNonce; + id dfPubkey; + id dfMiningSeed; + id result; + sint8 _terminator; // Only data before "_terminator" are logged +}; +#endif + // poll and voter structs -struct QUtilPoll { +struct QUTILPoll { id poll_name; uint64 poll_type; // QUTIL_POLL_TYPE_QUBIC or QUTIL_POLL_TYPE_ASSET uint64 min_amount; // Minimum Qubic/asset amount for eligibility @@ -78,7 +96,7 @@ struct QUtilPoll { uint64 num_assets; // Number of assets in allowed assets }; -struct QUtilVoter { +struct QUTILVoter { id address; uint64 amount; uint64 chosen_option; // Limited to 0-63 by vote procedure @@ -98,14 +116,18 @@ struct QUTIL : public ContractBase sint64 total; // Voting state - Array polls; - Array voters; // 1d array for all voters + Array polls; + Array voters; // 1d array for all voters Array poll_ids; Array voter_counts; // tracks number of voters per poll Array, QUTIL_MAX_POLL> poll_links; // github links for polls uint64 current_poll_id; uint64 new_polls_this_epoch; + // DF function variables + m256i dfMiningSeed; + m256i dfCurrentState; + // Get Qubic Balance struct get_qubic_balance_input { id address; @@ -128,7 +150,7 @@ struct QUTIL : public ContractBase struct get_asset_balance_locals { }; - // Get QUtilVoter Balance Helper + // Get QUTILVoter Balance Helper struct get_voter_balance_input { uint64 poll_idx; id address; @@ -150,7 +172,7 @@ struct QUTIL : public ContractBase get_asset_balance_locals gab_locals; }; - // Swap QUtilVoter to the end of the array helper + // Swap QUTILVoter to the end of the array helper struct swap_voter_to_end_input { uint64 poll_idx; uint64 i; @@ -161,7 +183,7 @@ struct QUTIL : public ContractBase struct swap_voter_to_end_locals { uint64 voter_index_i; uint64 voter_index_end; - QUtilVoter temp_voter; + QUTILVoter temp_voter; }; public: @@ -181,7 +203,7 @@ struct QUTIL : public ContractBase }; struct SendToManyV1_locals { - QUtilLogger logger; + QUTILLogger logger; }; struct GetSendToManyV1Fee_input @@ -209,7 +231,8 @@ struct QUTIL : public ContractBase id currentId; sint64 t; uint64 useNext; - QUtilLogger logger; + uint64 totalNumTransfers; + QUTILLogger logger; }; struct BurnQubic_input @@ -240,10 +263,10 @@ struct QUTIL : public ContractBase struct CreatePoll_locals { uint64 idx; - QUtilPoll new_poll; - QUtilVoter default_voter; + QUTILPoll new_poll; + QUTILVoter default_voter; uint64 i; - QUtilLogger logger; + QUTILLogger logger; }; struct Vote_input @@ -271,11 +294,11 @@ struct QUTIL : public ContractBase swap_voter_to_end_locals sve_locals; uint64 i; uint64 voter_index; - QUtilVoter temp_voter; + QUTILVoter temp_voter; uint64 real_vote; uint64 end_idx; uint64 max_balance; - QUtilLogger logger; + QUTILLogger logger; }; struct CancelPoll_input @@ -291,8 +314,8 @@ struct QUTIL : public ContractBase struct CancelPoll_locals { uint64 idx; - QUtilPoll current_poll; - QUtilLogger logger; + QUTILPoll current_poll; + QUTILLogger logger; }; struct GetCurrentResult_input @@ -310,10 +333,10 @@ struct QUTIL : public ContractBase uint64 idx; uint64 poll_type; uint64 effective_amount; - QUtilVoter voter; + QUTILVoter voter; uint64 i; uint64 voter_index; - QUtilLogger logger; + QUTILLogger logger; }; struct GetPollsByCreator_input @@ -328,7 +351,7 @@ struct QUTIL : public ContractBase struct GetPollsByCreator_locals { uint64 idx; - QUtilLogger logger; + QUTILLogger logger; }; struct GetCurrentPollId_input @@ -353,28 +376,19 @@ struct QUTIL : public ContractBase struct GetPollInfo_output { uint64 found; // 1 if exists, 0 ig not - QUtilPoll poll_info; + QUTILPoll poll_info; Array poll_link; }; struct GetPollInfo_locals { uint64 idx; - QUtilPoll default_poll; // default values if not found + QUTILPoll default_poll; // default values if not found }; struct END_EPOCH_locals { uint64 i; - QUtilPoll current_poll; - }; - - struct BEGIN_EPOCH_locals - { - uint64 i; - uint64 j; - QUtilPoll default_poll; - QUtilVoter default_voter; - Array zero_link; + QUTILPoll current_poll; }; /**************************************/ @@ -432,7 +446,7 @@ struct QUTIL : public ContractBase state.voters.set(locals.voter_index_end, locals.temp_voter); } - // Calculate QUtilVoter Index + // Calculate QUTILVoter Index inline static uint64 calculate_voter_index(uint64 poll_idx, uint64 voter_idx) { return poll_idx * QUTIL_MAX_VOTERS_PER_POLL + voter_idx; @@ -440,30 +454,25 @@ struct QUTIL : public ContractBase static inline bit check_github_prefix(const Array& github_link) { - return github_link.get(0) == 'h' && - github_link.get(1) == 't' && - github_link.get(2) == 't' && - github_link.get(3) == 'p' && - github_link.get(4) == 's' && - github_link.get(5) == ':' && - github_link.get(6) == '/' && - github_link.get(7) == '/' && - github_link.get(8) == 'g' && - github_link.get(9) == 'i' && - github_link.get(10) == 't' && - github_link.get(11) == 'h' && - github_link.get(12) == 'u' && - github_link.get(13) == 'b' && - github_link.get(14) == '.' && - github_link.get(15) == 'c' && - github_link.get(16) == 'o' && - github_link.get(17) == 'm' && - github_link.get(18) == '/' && - github_link.get(19) == 'q' && - github_link.get(20) == 'u' && - github_link.get(21) == 'b' && - github_link.get(22) == 'i' && - github_link.get(23) == 'c'; + return github_link.get(0) == 104 && // 'h' + github_link.get(1) == 116 && // 't' + github_link.get(2) == 116 && // 't' + github_link.get(3) == 112 && // 'p' + github_link.get(4) == 115 && // 's' + github_link.get(5) == 58 && // ':' + github_link.get(6) == 47 && // '/' + github_link.get(7) == 47 && // '/' + github_link.get(8) == 103 && // 'g' + github_link.get(9) == 105 && // 'i' + github_link.get(10) == 116 && // 't' + github_link.get(11) == 104 && // 'h' + github_link.get(12) == 117 && // 'u' + github_link.get(13) == 98 && // 'b' + github_link.get(14) == 46 && // '.' + github_link.get(15) == 99 && // 'c' + github_link.get(16) == 111 && // 'o' + github_link.get(17) == 109 && // 'm' + github_link.get(18) == 47; // '/' } /**************************************/ @@ -485,13 +494,37 @@ struct QUTIL : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(SendToManyV1) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; LOG_INFO(locals.logger); - state.total = input.amt0 + input.amt1 + input.amt2 + input.amt3 + input.amt4 + input.amt5 + input.amt6 + input.amt7 + input.amt8 + input.amt9 + input.amt10 + input.amt11 + input.amt12 + input.amt13 + input.amt14 + input.amt15 + input.amt16 + input.amt17 + input.amt18 + input.amt19 + input.amt20 + input.amt21 + input.amt22 + input.amt23 + input.amt24 + QUTIL_STM1_INVOCATION_FEE; - // invalid amount (<0), return fund and exit - if ((input.amt0 < 0) || (input.amt1 < 0) || (input.amt2 < 0) || (input.amt3 < 0) || (input.amt4 < 0) || (input.amt5 < 0) || (input.amt6 < 0) || (input.amt7 < 0) || (input.amt8 < 0) || (input.amt9 < 0) || (input.amt10 < 0) || (input.amt11 < 0) || (input.amt12 < 0) || (input.amt13 < 0) || (input.amt14 < 0) || (input.amt15 < 0) || (input.amt16 < 0) || (input.amt17 < 0) || (input.amt18 < 0) || (input.amt19 < 0) || (input.amt20 < 0) || (input.amt21 < 0) || (input.amt22 < 0) || (input.amt23 < 0) || (input.amt24 < 0)) - { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; + + // invalid amount (<0 or >= MAX_AMOUNT), return fund and exit + if ((input.amt0 < 0) || (input.amt0 >= MAX_AMOUNT) + || (input.amt1 < 0) || (input.amt1 >= MAX_AMOUNT) + || (input.amt2 < 0) || (input.amt2 >= MAX_AMOUNT) + || (input.amt3 < 0) || (input.amt3 >= MAX_AMOUNT) + || (input.amt4 < 0) || (input.amt4 >= MAX_AMOUNT) + || (input.amt5 < 0) || (input.amt5 >= MAX_AMOUNT) + || (input.amt6 < 0) || (input.amt6 >= MAX_AMOUNT) + || (input.amt7 < 0) || (input.amt7 >= MAX_AMOUNT) + || (input.amt8 < 0) || (input.amt8 >= MAX_AMOUNT) + || (input.amt9 < 0) || (input.amt9 >= MAX_AMOUNT) + || (input.amt10 < 0) || (input.amt10 >= MAX_AMOUNT) + || (input.amt11 < 0) || (input.amt11 >= MAX_AMOUNT) + || (input.amt12 < 0) || (input.amt12 >= MAX_AMOUNT) + || (input.amt13 < 0) || (input.amt13 >= MAX_AMOUNT) + || (input.amt14 < 0) || (input.amt14 >= MAX_AMOUNT) + || (input.amt15 < 0) || (input.amt15 >= MAX_AMOUNT) + || (input.amt16 < 0) || (input.amt16 >= MAX_AMOUNT) + || (input.amt17 < 0) || (input.amt17 >= MAX_AMOUNT) + || (input.amt18 < 0) || (input.amt18 >= MAX_AMOUNT) + || (input.amt19 < 0) || (input.amt19 >= MAX_AMOUNT) + || (input.amt20 < 0) || (input.amt20 >= MAX_AMOUNT) + || (input.amt21 < 0) || (input.amt21 >= MAX_AMOUNT) + || (input.amt22 < 0) || (input.amt22 >= MAX_AMOUNT) + || (input.amt23 < 0) || (input.amt23 >= MAX_AMOUNT) + || (input.amt24 < 0) || (input.amt24 >= MAX_AMOUNT)) + { + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; output.returnCode = QUTIL_STM1_INVALID_AMOUNT_NUMBER; LOG_INFO(locals.logger); if (qpi.invocationReward() > 0) @@ -499,10 +532,41 @@ struct QUTIL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } } + + // Make sure that the sum of all amounts does not overflow and is equal to qpi.invocationReward() + state.total = qpi.invocationReward(); + state.total -= input.amt0; if (state.total < 0) goto exit; + state.total -= input.amt1; if (state.total < 0) goto exit; + state.total -= input.amt2; if (state.total < 0) goto exit; + state.total -= input.amt3; if (state.total < 0) goto exit; + state.total -= input.amt4; if (state.total < 0) goto exit; + state.total -= input.amt5; if (state.total < 0) goto exit; + state.total -= input.amt6; if (state.total < 0) goto exit; + state.total -= input.amt7; if (state.total < 0) goto exit; + state.total -= input.amt8; if (state.total < 0) goto exit; + state.total -= input.amt9; if (state.total < 0) goto exit; + state.total -= input.amt10; if (state.total < 0) goto exit; + state.total -= input.amt11; if (state.total < 0) goto exit; + state.total -= input.amt12; if (state.total < 0) goto exit; + state.total -= input.amt13; if (state.total < 0) goto exit; + state.total -= input.amt14; if (state.total < 0) goto exit; + state.total -= input.amt15; if (state.total < 0) goto exit; + state.total -= input.amt16; if (state.total < 0) goto exit; + state.total -= input.amt17; if (state.total < 0) goto exit; + state.total -= input.amt18; if (state.total < 0) goto exit; + state.total -= input.amt19; if (state.total < 0) goto exit; + state.total -= input.amt20; if (state.total < 0) goto exit; + state.total -= input.amt21; if (state.total < 0) goto exit; + state.total -= input.amt22; if (state.total < 0) goto exit; + state.total -= input.amt23; if (state.total < 0) goto exit; + state.total -= input.amt24; if (state.total < 0) goto exit; + state.total -= QUTIL_STM1_INVOCATION_FEE; if (state.total < 0) goto exit; + // insufficient or too many qubic transferred, return fund and exit (we don't want to return change) - if (qpi.invocationReward() != state.total) + if (state.total != 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_WRONG_FUND }; + exit: + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_WRONG_FUND }; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_WRONG_FUND; if (qpi.invocationReward() > 0) @@ -514,155 +578,155 @@ struct QUTIL : public ContractBase if (input.dst0 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst0, input.amt0, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst0, input.amt0, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst0, input.amt0); } if (input.dst1 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst1, input.amt1, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst1, input.amt1, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst1, input.amt1); } if (input.dst2 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst2, input.amt2, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst2, input.amt2, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst2, input.amt2); } if (input.dst3 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst3, input.amt3, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst3, input.amt3, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst3, input.amt3); } if (input.dst4 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst4, input.amt4, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst4, input.amt4, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst4, input.amt4); } if (input.dst5 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst5, input.amt5, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst5, input.amt5, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst5, input.amt5); } if (input.dst6 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst6, input.amt6, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst6, input.amt6, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst6, input.amt6); } if (input.dst7 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst7, input.amt7, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst7, input.amt7, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst7, input.amt7); } if (input.dst8 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst8, input.amt8, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst8, input.amt8, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst8, input.amt8); } if (input.dst9 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst9, input.amt9, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst9, input.amt9, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst9, input.amt9); } if (input.dst10 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst10, input.amt10, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst10, input.amt10, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst10, input.amt10); } if (input.dst11 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst11, input.amt11, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst11, input.amt11, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst11, input.amt11); } if (input.dst12 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst12, input.amt12, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst12, input.amt12, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst12, input.amt12); } if (input.dst13 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst13, input.amt13, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst13, input.amt13, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst13, input.amt13); } if (input.dst14 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst14, input.amt14, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst14, input.amt14, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst14, input.amt14); } if (input.dst15 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst15, input.amt15, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst15, input.amt15, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst15, input.amt15); } if (input.dst16 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst16, input.amt16, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst16, input.amt16, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst16, input.amt16); } if (input.dst17 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst17, input.amt17, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst17, input.amt17, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst17, input.amt17); } if (input.dst18 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst18, input.amt18, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst18, input.amt18, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst18, input.amt18); } if (input.dst19 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst19, input.amt19, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst19, input.amt19, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst19, input.amt19); } if (input.dst20 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst20, input.amt20, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst20, input.amt20, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst20, input.amt20); } if (input.dst21 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst21, input.amt21, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst21, input.amt21, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst21, input.amt21); } if (input.dst22 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst22, input.amt22, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst22, input.amt22, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst22, input.amt22); } if (input.dst23 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst23, input.amt23, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst23, input.amt23, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst23, input.amt23); } if (input.dst24 != NULL_ID) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), input.dst24, input.amt24, QUTIL_STM1_SEND_FUND }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), input.dst24, input.amt24, QUTIL_STM1_SEND_FUND }; LOG_INFO(locals.logger); qpi.transfer(input.dst24, input.amt24); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, state.total, QUTIL_STM1_SUCCESS }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_SUCCESS}; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_SUCCESS; qpi.burn(QUTIL_STM1_INVOCATION_FEE); @@ -677,31 +741,32 @@ struct QUTIL : public ContractBase */ PUBLIC_PROCEDURE_WITH_LOCALS(SendToManyBenchmark) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_TRIGGERED }; LOG_INFO(locals.logger); output.total = 0; // Number of addresses and transfers is > 0 and total transfers do not exceed limit (including 2 transfers from invocator to contract and contract to invocator) - if (input.dstCount <= 0 || input.numTransfersEach <= 0 || input.dstCount * input.numTransfersEach + 2 > CONTRACT_ACTION_TRACKER_SIZE) + locals.totalNumTransfers = smul((uint64)input.dstCount, (uint64)input.numTransfersEach); + if (input.dstCount <= 0 || input.numTransfersEach <= 0 || locals.totalNumTransfers > CONTRACT_ACTION_TRACKER_SIZE - 2) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_INVALID_AMOUNT_NUMBER; return; } // Check the fund is enough - if (qpi.invocationReward() < input.dstCount * input.numTransfersEach) + if ((uint64)qpi.invocationReward() < locals.totalNumTransfers) { if (qpi.invocationReward() > 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTIL_STM1_INVALID_AMOUNT_NUMBER }; LOG_INFO(locals.logger); output.returnCode = QUTIL_STM1_INVALID_AMOUNT_NUMBER; return; @@ -716,7 +781,7 @@ struct QUTIL : public ContractBase locals.currentId = qpi.nextId(locals.currentId); else locals.currentId = qpi.prevId(locals.currentId); - if (locals.currentId == m256i::zero()) + if (locals.currentId == id::zero()) { locals.currentId = qpi.invocator(); locals.useNext = 1 - locals.useNext; @@ -737,7 +802,7 @@ struct QUTIL : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward() - output.total); } - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, output.total, QUTIL_STM1_SUCCESS }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, output.total, QUTIL_STM1_SUCCESS }; LOG_INFO(locals.logger); } @@ -782,7 +847,7 @@ struct QUTIL : public ContractBase // max new poll exceeded if (state.new_polls_this_epoch >= QUTIL_MAX_NEW_POLL) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeMaxPollsReached }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeMaxPollsReached }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -791,7 +856,7 @@ struct QUTIL : public ContractBase // insufficient fund if (qpi.invocationReward() < QUTIL_POLL_CREATION_FEE) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QutilLogTypeInsufficientFundsForPoll }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTILLogTypeInsufficientFundsForPoll }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -800,7 +865,7 @@ struct QUTIL : public ContractBase // invalid poll type if (input.poll_type != QUTIL_POLL_TYPE_QUBIC && input.poll_type != QUTIL_POLL_TYPE_ASSET) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollType }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollType }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -809,7 +874,7 @@ struct QUTIL : public ContractBase // invalid number of assets in Qubic poll if (input.poll_type == QUTIL_POLL_TYPE_QUBIC && input.num_assets != 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidNumAssetsQubic }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidNumAssetsQubic }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -818,7 +883,7 @@ struct QUTIL : public ContractBase // invalid number of assets in Asset poll if (input.poll_type == QUTIL_POLL_TYPE_ASSET && (input.num_assets == 0 || input.num_assets > QUTIL_MAX_ASSETS_PER_POLL)) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidNumAssetsAsset }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidNumAssetsAsset }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -826,7 +891,7 @@ struct QUTIL : public ContractBase if (!check_github_prefix(input.github_link)) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollType }; // reusing existing log type for invalid GitHub link + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollType }; // reusing existing log type for invalid GitHub link LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -862,7 +927,7 @@ struct QUTIL : public ContractBase state.new_polls_this_epoch++; - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, QUTIL_POLL_CREATION_FEE, QutilLogTypePollCreated }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, QUTIL_POLL_CREATION_FEE, QUTILLogTypePollCreated }; LOG_INFO(locals.logger); } @@ -874,7 +939,7 @@ struct QUTIL : public ContractBase output.success = false; if (qpi.invocationReward() < QUTIL_VOTE_FEE) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QutilLogTypeInsufficientFundsForVote }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTILLogTypeInsufficientFundsForVote }; LOG_INFO(locals.logger); return; } @@ -885,13 +950,13 @@ struct QUTIL : public ContractBase if (state.poll_ids.get(locals.idx) != input.poll_id) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollId }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollId }; LOG_INFO(locals.logger); return; } if (state.polls.get(locals.idx).is_active == 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypePollInactive }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypePollInactive }; LOG_INFO(locals.logger); return; } @@ -905,13 +970,13 @@ struct QUTIL : public ContractBase if (locals.max_balance < state.polls.get(locals.idx).min_amount || locals.max_balance < input.amount) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInsufficientBalance }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInsufficientBalance }; LOG_INFO(locals.logger); return; } if (input.chosen_option >= QUTIL_MAX_OPTIONS) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidOption }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidOption }; LOG_INFO(locals.logger); return; } @@ -923,14 +988,14 @@ struct QUTIL : public ContractBase if (state.voters.get(locals.voter_index).address == input.address) { // Update existing voter - state.voters.set(locals.voter_index, QUtilVoter{ input.address, input.amount, input.chosen_option }); + state.voters.set(locals.voter_index, QUTILVoter{ input.address, input.amount, input.chosen_option }); output.success = true; break; } else if (state.voters.get(locals.voter_index).address == NULL_ID) { // Add new voter in empty slot - state.voters.set(locals.voter_index, QUtilVoter{ input.address, input.amount, input.chosen_option }); + state.voters.set(locals.voter_index, QUTILVoter{ input.address, input.amount, input.chosen_option }); state.voter_counts.set(locals.idx, state.voter_counts.get(locals.idx) + 1); output.success = true; break; @@ -985,7 +1050,7 @@ struct QUTIL : public ContractBase if (locals.max_balance < state.polls.get(locals.idx).min_amount) { // Mark as invalid by setting address to NULL_ID - state.voters.set(locals.voter_index, QUtilVoter{ NULL_ID, 0, 0 }); + state.voters.set(locals.voter_index, QUTILVoter{ NULL_ID, 0, 0 }); // Swap with the last valid voter while (locals.end_idx > locals.i && state.voters.get(calculate_voter_index(locals.idx, locals.end_idx)).address == NULL_ID) { @@ -1016,7 +1081,7 @@ struct QUTIL : public ContractBase if (output.success) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, QUTIL_VOTE_FEE, QutilLogTypeVoteCast }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, QUTIL_VOTE_FEE, QUTILLogTypeVoteCast }; LOG_INFO(locals.logger); } } @@ -1025,7 +1090,7 @@ struct QUTIL : public ContractBase { if (qpi.invocationReward() < QUTIL_POLL_CREATION_FEE) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QutilLogTypeInsufficientFundsForCancel }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, qpi.invocationReward(), QUTILLogTypeInsufficientFundsForCancel }; LOG_INFO(locals.logger); qpi.transfer(qpi.invocator(), qpi.invocationReward()); output.success = false; @@ -1036,7 +1101,7 @@ struct QUTIL : public ContractBase if (state.poll_ids.get(locals.idx) != input.poll_id) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollId }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollId }; LOG_INFO(locals.logger); output.success = false; qpi.transfer(qpi.invocator(), qpi.invocationReward()); @@ -1047,7 +1112,7 @@ struct QUTIL : public ContractBase if (locals.current_poll.creator != qpi.invocator()) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeNotAuthorized }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeNotAuthorized }; LOG_INFO(locals.logger); output.success = false; qpi.transfer(qpi.invocator(), qpi.invocationReward()); @@ -1056,7 +1121,7 @@ struct QUTIL : public ContractBase if (locals.current_poll.is_active == 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypePollInactive }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypePollInactive }; LOG_INFO(locals.logger); output.success = false; qpi.transfer(qpi.invocator(), qpi.invocationReward()); @@ -1066,7 +1131,7 @@ struct QUTIL : public ContractBase locals.current_poll.is_active = 0; state.polls.set(locals.idx, locals.current_poll); - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypePollCancelled }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypePollCancelled }; LOG_INFO(locals.logger); output.success = true; @@ -1082,7 +1147,7 @@ struct QUTIL : public ContractBase locals.idx = mod(input.poll_id, QUTIL_MAX_POLL); if (state.poll_ids.get(locals.idx) != input.poll_id) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeInvalidPollIdResult }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeInvalidPollIdResult }; LOG_INFO(locals.logger); return; } @@ -1107,7 +1172,7 @@ struct QUTIL : public ContractBase output.count = 0; for (locals.idx = 0; locals.idx < QUTIL_MAX_POLL; locals.idx++) { - if (state.polls.get(locals.idx).is_active != 0 && state.polls.get(locals.idx).creator == input.creator) + if (state.polls.get(locals.idx).creator != NULL_ID && state.polls.get(locals.idx).creator == input.creator) { output.poll_ids.set(output.count, state.poll_ids.get(locals.idx)); output.count++; @@ -1115,7 +1180,7 @@ struct QUTIL : public ContractBase } if (output.count == 0) { - locals.logger = QUtilLogger{ 0, 0, qpi.invocator(), SELF, 0, QutilLogTypeNoPollsByCreator }; + locals.logger = QUTILLogger{ 0, 0, qpi.invocator(), SELF, 0, QUTILLogTypeNoPollsByCreator }; LOG_INFO(locals.logger); } } @@ -1171,6 +1236,32 @@ struct QUTIL : public ContractBase state.new_polls_this_epoch = 0; } + BEGIN_EPOCH() + { + state.dfMiningSeed = qpi.getPrevSpectrumDigest(); + } + + // Deactivate delay function + #if 0 + struct BEGIN_TICK_locals + { + m256i dfPubkey, dfNonce; + QUTILDFLogger logger; + }; + /* + * A deterministic delay function + */ + BEGIN_TICK_WITH_LOCALS() + { + locals.dfPubkey = qpi.getPrevSpectrumDigest(); + locals.dfNonce = qpi.getPrevComputerDigest(); + state.dfCurrentState = qpi.computeMiningFunction(state.dfMiningSeed, locals.dfPubkey, locals.dfNonce); + + locals.logger = QUTILDFLogger{ 0, 0, locals.dfNonce, locals.dfPubkey, state.dfMiningSeed, state.dfCurrentState}; + LOG_INFO(locals.logger); + } + #endif + /* * @return Return total number of shares that currently exist of the asset given as input */ @@ -1179,6 +1270,79 @@ struct QUTIL : public ContractBase output = qpi.numberOfShares(input); } + struct DistributeQuToShareholders_input + { + Asset asset; + }; + struct DistributeQuToShareholders_output + { + sint64 shareholders; + sint64 totalShares; + sint64 amountPerShare; + sint64 fees; + }; + struct DistributeQuToShareholders_locals + { + AssetPossessionIterator iter; + sint64 payBack; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(DistributeQuToShareholders) + { + // 1. Compute fee (increases linear with number of shareholders) + // 1.1. Count shareholders and shares + for (locals.iter.begin(input.asset); !locals.iter.reachedEnd(); locals.iter.next()) + { + if (locals.iter.numberOfPossessedShares() > 0) + { + ++output.shareholders; + output.totalShares += locals.iter.numberOfPossessedShares(); + } + } + + // 1.2. Cancel if there are no shareholders + if (output.shareholders == 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // 1.3. Compute fee (proportional to number of shareholders) + output.fees = output.shareholders * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + + // 1.4. Compute QU per share + output.amountPerShare = div(qpi.invocationReward() - output.fees, output.totalShares); + + // 1.5. Cancel if amount is not sufficient to pay fees and at least one QU per share + if (output.amountPerShare <= 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + return; + } + + // 1.6. compute payback QU (remainder of distribution) + locals.payBack = qpi.invocationReward() - output.totalShares * output.amountPerShare - output.fees; + ASSERT(locals.payBack >= 0); + + // 2. Distribute to shareholders + for (locals.iter.begin(input.asset); !locals.iter.reachedEnd(); locals.iter.next()) + { + if (locals.iter.numberOfPossessedShares() > 0) + { + qpi.transfer(locals.iter.possessor(), locals.iter.numberOfPossessedShares() * output.amountPerShare); + } + } + + // 3. Burn fee + qpi.burn(output.fees); + + // 4. pay back QU that cannot be evenly distributed + if (locals.payBack > 0) + { + qpi.transfer(qpi.invocator(), locals.payBack); + } + } + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { REGISTER_USER_FUNCTION(GetSendToManyV1Fee, 1); @@ -1194,5 +1358,6 @@ struct QUTIL : public ContractBase REGISTER_USER_PROCEDURE(CreatePoll, 4); REGISTER_USER_PROCEDURE(Vote, 5); REGISTER_USER_PROCEDURE(CancelPoll, 6); + REGISTER_USER_PROCEDURE(DistributeQuToShareholders, 7); } }; diff --git a/src/contracts/Qbay.h b/src/contracts/Qbay.h index c352da7df..15f668dea 100644 --- a/src/contracts/Qbay.h +++ b/src/contracts/Qbay.h @@ -55,7 +55,7 @@ struct QBAYLogger { uint32 _contractIndex; uint32 _type; // Assign a random unique (per contract) number to distinguish messages of different types - char _terminator; // Only data before "_terminator" are logged + sint8 _terminator; // Only data before "_terminator" are logged }; struct QBAY2 @@ -491,14 +491,6 @@ struct QBAY : public ContractBase Array NFTs; - /** - * @return Current date from core node system - */ - - inline static void getCurrentDate(const QPI::QpiContextFunctionCall& qpi, uint32& res) - { - QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), res); - } struct settingCFBAndQubicPrice_locals { @@ -966,7 +958,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1033,7 +1025,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1109,7 +1101,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1247,7 +1239,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1322,7 +1314,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.possessedNFT).endTimeOfAuction || locals.curDate <= state.NFTs.get(input.anotherNFT).endTimeOfAuction) { @@ -1411,7 +1403,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.possessedNFT).endTimeOfAuction || locals.curDate <= state.NFTs.get(input.anotherNFT).endTimeOfAuction) { @@ -1477,7 +1469,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1629,7 +1621,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1728,7 +1720,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate <= state.NFTs.get(input.NFTid).endTimeOfAuction) { @@ -1824,7 +1816,7 @@ struct QBAY : public ContractBase return; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.startDate <= locals.curDate || locals.endDate <= locals.startDate) { @@ -1923,7 +1915,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if(locals.curDate < state.NFTs.get(input.NFTId).startTimeOfAuction || locals.curDate > state.NFTs.get(input.NFTId).endTimeOfAuction) { @@ -2179,7 +2171,6 @@ struct QBAY : public ContractBase { // success output.transferredNumberOfShares = input.numberOfShares; - qpi.transfer(id(QX_CONTRACT_INDEX, 0, 0, 0), state.transferRightsFee); if (qpi.invocationReward() > state.transferRightsFee) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - state.transferRightsFee); @@ -2200,7 +2191,7 @@ struct QBAY : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(getNumberOfNFTForUser) { - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); output.numberOfNFT = 0; @@ -2222,7 +2213,7 @@ struct QBAY : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(getInfoOfNFTUserPossessed) { - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.cnt = 0; @@ -2349,7 +2340,7 @@ struct QBAY : public ContractBase return ; } - getCurrentDate(qpi, locals.curDate); + QUOTTERY::packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.cnt = 0; locals._r = 0; @@ -2510,6 +2501,11 @@ struct QBAY : public ContractBase } + BEGIN_EPOCH() + { + state.transferRightsFee = 100; + } + struct END_EPOCH_locals { QX::TransferShareManagementRights_input transferShareManagementRights_input; diff --git a/src/contracts/Qdraw.h b/src/contracts/Qdraw.h new file mode 100644 index 000000000..46c34d111 --- /dev/null +++ b/src/contracts/Qdraw.h @@ -0,0 +1,216 @@ +using namespace QPI; + +constexpr sint64 QDRAW_TICKET_PRICE = 1000000LL; +constexpr uint64 QDRAW_MAX_PARTICIPANTS = 1024 * X_MULTIPLIER; + +struct QDRAW2 +{ +}; + +struct QDRAW : public ContractBase +{ +public: + struct buyTicket_input + { + uint64 ticketCount; + }; + struct buyTicket_output + { + }; + + struct getInfo_input + { + }; + struct getInfo_output + { + sint64 pot; + uint64 participantCount; + id lastWinner; + sint64 lastWinAmount; + uint8 lastDrawHour; + uint8 currentHour; + uint8 nextDrawHour; + }; + + + struct getParticipants_input + { + }; + struct getParticipants_output + { + uint64 participantCount; + uint64 uniqueParticipantCount; + Array participants; + Array ticketCounts; + }; + +protected: + Array _participants; + uint64 _participantCount; + sint64 _pot; + uint8 _lastDrawHour; + id _lastWinner; + sint64 _lastWinAmount; + id _owner; + + struct buyTicket_locals + { + uint64 available; + sint64 totalCost; + uint64 i; + }; + + struct getParticipants_locals + { + uint64 uniqueCount; + uint64 i; + uint64 j; + bool found; + id p; + }; + + struct BEGIN_TICK_locals + { + uint8 currentHour; + id only; + id rand; + id winner; + uint64 loopIndex; + }; + + inline static bool isMonopoly(const Array& arr, uint64 count, uint64 loopIndex) + { + if (count != QDRAW_MAX_PARTICIPANTS) + { + return false; + } + for (loopIndex = 1; loopIndex < count; ++loopIndex) + { + if (arr.get(loopIndex) != arr.get(0)) + { + return false; + } + } + return true; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(buyTicket) + { + locals.available = QDRAW_MAX_PARTICIPANTS - state._participantCount; + if (QDRAW_MAX_PARTICIPANTS == state._participantCount || input.ticketCount == 0 || input.ticketCount > locals.available) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + locals.totalCost = (sint64)input.ticketCount * (sint64)QDRAW_TICKET_PRICE; + if (qpi.invocationReward() < locals.totalCost) + { + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + for (locals.i = 0; locals.i < input.ticketCount; ++locals.i) + { + state._participants.set(state._participantCount + locals.i, qpi.invocator()); + } + state._participantCount += input.ticketCount; + state._pot += locals.totalCost; + if (qpi.invocationReward() > locals.totalCost) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.totalCost); + } + } + + PUBLIC_FUNCTION(getInfo) + { + output.pot = state._pot; + output.participantCount = state._participantCount; + output.lastDrawHour = state._lastDrawHour; + output.currentHour = qpi.hour(); + output.nextDrawHour = (uint8)(mod(qpi.hour() + 1, 24)); + output.lastWinner = state._lastWinner; + output.lastWinAmount = state._lastWinAmount; + } + + PUBLIC_FUNCTION_WITH_LOCALS(getParticipants) + { + locals.uniqueCount = 0; + for (locals.i = 0; locals.i < state._participantCount; ++locals.i) + { + locals.p = state._participants.get(locals.i); + locals.found = false; + for (locals.j = 0; locals.j < locals.uniqueCount; ++locals.j) + { + if (output.participants.get(locals.j) == locals.p) + { + output.ticketCounts.set(locals.j, output.ticketCounts.get(locals.j) + 1); + locals.found = true; + break; + } + } + if (!locals.found) + { + output.participants.set(locals.uniqueCount, locals.p); + output.ticketCounts.set(locals.uniqueCount, 1); + ++locals.uniqueCount; + } + } + output.participantCount = state._participantCount; + output.uniqueParticipantCount = locals.uniqueCount; + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(buyTicket, 1); + REGISTER_USER_FUNCTION(getInfo, 2); + REGISTER_USER_FUNCTION(getParticipants, 3); + } + + INITIALIZE() + { + state._participantCount = 0; + state._pot = 0; + state._lastWinAmount = 0; + state._lastWinner = NULL_ID; + state._lastDrawHour = qpi.hour(); + state._owner = ID(_Q, _D, _R, _A, _W, _U, _R, _A, _L, _C, _L, _P, _P, _E, _Q, _O, _G, _Q, _C, _U, _J, _N, _F, _B, _B, _B, _A, _A, _F, _X, _W, _Y, _Y, _M, _M, _C, _U, _C, _U, _K, _T, _C, _R, _Q, _B, _S, _M, _Z, _U, _D, _M, _V, _X, _P, _N, _F); + } + + BEGIN_TICK_WITH_LOCALS() + { + locals.currentHour = qpi.hour(); + if (locals.currentHour != state._lastDrawHour) + { + state._lastDrawHour = locals.currentHour; + if (state._participantCount > 0) + { + if (isMonopoly(state._participants, state._participantCount, locals.loopIndex)) + { + locals.only = state._participants.get(0); + qpi.burn(QDRAW_TICKET_PRICE); + qpi.transfer(locals.only, QDRAW_TICKET_PRICE); + qpi.transfer(state._owner, state._pot - QDRAW_TICKET_PRICE - QDRAW_TICKET_PRICE); + state._lastWinner = locals.only; + state._lastWinAmount = QDRAW_TICKET_PRICE; + } + else + { + locals.rand = qpi.K12(qpi.getPrevSpectrumDigest()); + locals.winner = state._participants.get(mod(locals.rand.u64._0, state._participantCount)); + qpi.transfer(locals.winner, state._pot); + state._lastWinner = locals.winner; + state._lastWinAmount = state._pot; + } + state._participantCount = 0; + state._pot = 0; + } + } + } +}; + + diff --git a/src/contracts/Qearn.h b/src/contracts/Qearn.h index 407085d31..15c32f812 100644 --- a/src/contracts/Qearn.h +++ b/src/contracts/Qearn.h @@ -45,7 +45,7 @@ constexpr sint32 QEARN_UNLOCK_SUCCESS = 5; constexpr sint32 QEARN_OVERFLOW_USER = 6; constexpr sint32 QEARN_LIMIT_LOCK = 7; -enum QearnLogInfo { +enum QEARNLogInfo { QearnSuccessLocking = 0, QearnFailedTransfer = 1, QearnLimitLocking = 2, @@ -54,14 +54,14 @@ enum QearnLogInfo { QearnSuccessEarlyUnlocking = 5, QearnSuccessFullyUnlocking = 6, }; -struct QearnLogger +struct QEARNLogger { uint32 _contractIndex; id sourcePublicKey; id destinationPublicKey; sint64 amount; uint32 _type; - char _terminator; + sint8 _terminator; }; struct QEARN2 @@ -308,7 +308,7 @@ struct QEARN : public ContractBase output.currentLockedAmount = state._currentRoundInfo.get(input.Epoch)._totalLockedAmount; if(state._currentRoundInfo.get(input.Epoch)._totalLockedAmount) { - output.yield = state._currentRoundInfo.get(input.Epoch)._epochBonusAmount * 10000000ULL / state._currentRoundInfo.get(input.Epoch)._totalLockedAmount; + output.yield = div(state._currentRoundInfo.get(input.Epoch)._epochBonusAmount * 10000000ULL, state._currentRoundInfo.get(input.Epoch)._totalLockedAmount); } else { @@ -482,7 +482,7 @@ struct QEARN : public ContractBase LockInfo newLocker; RoundInfo updatedRoundInfo; EpochIndexInfo tmpIndex; - QearnLogger log; + QEARNLogger log; uint32 t; uint32 endIndex; @@ -605,7 +605,7 @@ struct QEARN : public ContractBase LockInfo updatedUserInfo; HistoryInfo unlockerInfo; StatsInfo tmpStats; - QearnLogger log; + QEARNLogger log; uint64 amountOfUnlocking; uint64 amountOfReward; @@ -938,7 +938,7 @@ struct QEARN : public ContractBase RoundInfo INITIALIZE_ROUNDINFO; EpochIndexInfo tmpEpochIndex; StatsInfo tmpStats; - QearnLogger log; + QEARNLogger log; uint64 _rewardPercent; uint64 _rewardAmount; diff --git a/src/contracts/Qswap.h b/src/contracts/Qswap.h index 193ff3965..13672be8d 100644 --- a/src/contracts/Qswap.h +++ b/src/contracts/Qswap.h @@ -1,10 +1,20 @@ using namespace QPI; +// Log types enum for QSWAP contract +enum QSWAPLogInfo { + QSWAPAddLiquidity = 4, + QSWAPRemoveLiquidity = 5, + QSWAPSwapExactQuForAsset = 6, + QSWAPSwapQuForExactAsset = 7, + QSWAPSwapExactAssetForQu = 8, + QSWAPSwapAssetForExactQu = 9 +}; + // FIXED CONSTANTS constexpr uint64 QSWAP_INITIAL_MAX_POOL = 16384; constexpr uint64 QSWAP_MAX_POOL = QSWAP_INITIAL_MAX_POOL * X_MULTIPLIER; constexpr uint64 QSWAP_MAX_USER_PER_POOL = 256; -constexpr sint64 QSWAP_MIN_LIQUDITY = 1000; +constexpr sint64 QSWAP_MIN_LIQUIDITY = 1000; constexpr uint32 QSWAP_SWAP_FEE_BASE = 10000; constexpr uint32 QSWAP_FEE_BASE_100 = 100; @@ -12,6 +22,39 @@ struct QSWAP2 { }; +// Logging message structures for QSWAP procedures +struct QSWAPAddLiquidityMessage +{ + uint32 _contractIndex; + uint32 _type; + id assetIssuer; + uint64 assetName; + sint64 userIncreaseLiquidity; + sint64 quAmount; + sint64 assetAmount; + sint8 _terminator; +}; + +struct QSWAPRemoveLiquidityMessage +{ + uint32 _contractIndex; + uint32 _type; + sint64 quAmount; + sint64 assetAmount; + sint8 _terminator; +}; + +struct QSWAPSwapMessage +{ + uint32 _contractIndex; + uint32 _type; + id assetIssuer; + uint64 assetName; + sint64 assetAmountIn; + sint64 assetAmountOut; + sint8 _terminator; +}; + struct QSWAP : public ContractBase { public: @@ -57,18 +100,18 @@ struct QSWAP : public ContractBase sint64 poolExists; sint64 reservedQuAmount; sint64 reservedAssetAmount; - sint64 totalLiqudity; + sint64 totalLiquidity; }; - struct GetLiqudityOf_input + struct GetLiquidityOf_input { id assetIssuer; uint64 assetName; id account; }; - struct GetLiqudityOf_output + struct GetLiquidityOf_output { - sint64 liqudity; + sint64 liquidity; }; struct QuoteExactQuInput_input @@ -154,7 +197,7 @@ struct QSWAP : public ContractBase * @param quAmountMin Bounds the extent to which the B/A price can go up before the transaction reverts. Must be <= amountADesired. * @param assetAmountMin Bounds the extent to which the A/B price can go up before the transaction reverts. Must be <= amountBDesired. */ - struct AddLiqudity_input + struct AddLiquidity_input { id assetIssuer; uint64 assetName; @@ -162,23 +205,23 @@ struct QSWAP : public ContractBase sint64 quAmountMin; sint64 assetAmountMin; }; - struct AddLiqudity_output + struct AddLiquidity_output { - sint64 userIncreaseLiqudity; + sint64 userIncreaseLiquidity; sint64 quAmount; sint64 assetAmount; }; - struct RemoveLiqudity_input + struct RemoveLiquidity_input { id assetIssuer; uint64 assetName; - sint64 burnLiqudity; + sint64 burnLiquidity; sint64 quAmountMin; sint64 assetAmountMin; }; - struct RemoveLiqudity_output + struct RemoveLiquidity_output { sint64 quAmount; sint64 assetAmount; @@ -230,6 +273,17 @@ struct QSWAP : public ContractBase sint64 assetAmountIn; }; + struct TransferShareManagementRights_input + { + Asset asset; + sint64 numberOfShares; + uint32 newManagingContractIndex; + }; + struct TransferShareManagementRights_output + { + sint64 transferredNumberOfShares; + }; + protected: uint32 swapFeeRate; // e.g. 30: 0.3% (base: 10_000) uint32 teamFeeRate; // e.g. 20: 20% (base: 100) @@ -248,17 +302,17 @@ struct QSWAP : public ContractBase id poolID; sint64 reservedQuAmount; sint64 reservedAssetAmount; - sint64 totalLiqudity; + sint64 totalLiquidity; }; - struct LiqudityInfo + struct LiquidityInfo { id entity; - sint64 liqudity; + sint64 liquidity; }; Array mPoolBasicStates; - Collection mLiquditys; + Collection mLiquidities; inline static sint64 min(sint64 a, sint64 b) { @@ -425,7 +479,7 @@ struct QSWAP : public ContractBase PUBLIC_FUNCTION_WITH_LOCALS(GetPoolBasicState) { output.poolExists = 0; - output.totalLiqudity = -1; + output.totalLiquidity = -1; output.reservedAssetAmount = -1; output.reservedQuAmount = -1; @@ -459,32 +513,32 @@ struct QSWAP : public ContractBase output.reservedQuAmount = locals.poolBasicState.reservedQuAmount; output.reservedAssetAmount = locals.poolBasicState.reservedAssetAmount; - output.totalLiqudity = locals.poolBasicState.totalLiqudity; + output.totalLiquidity = locals.poolBasicState.totalLiquidity; } - struct GetLiqudityOf_locals + struct GetLiquidityOf_locals { id poolID; sint64 liqElementIndex; }; - PUBLIC_FUNCTION_WITH_LOCALS(GetLiqudityOf) + PUBLIC_FUNCTION_WITH_LOCALS(GetLiquidityOf) { - output.liqudity = 0; + output.liquidity = 0; locals.poolID = input.assetIssuer; locals.poolID.u64._3 = input.assetName; - locals.liqElementIndex = state.mLiquditys.headIndex(locals.poolID, 0); + locals.liqElementIndex = state.mLiquidities.headIndex(locals.poolID, 0); while (locals.liqElementIndex != NULL_INDEX) { - if (state.mLiquditys.element(locals.liqElementIndex).entity == input.account) + if (state.mLiquidities.element(locals.liqElementIndex).entity == input.account) { - output.liqudity = state.mLiquditys.element(locals.liqElementIndex).liqudity; + output.liquidity = state.mLiquidities.element(locals.liqElementIndex).liquidity; return; } - locals.liqElementIndex = state.mLiquditys.nextElementIndex(locals.liqElementIndex); + locals.liqElementIndex = state.mLiquidities.nextElementIndex(locals.liqElementIndex); } } @@ -528,8 +582,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -586,8 +640,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -649,8 +703,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -718,8 +772,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // no liqudity in the pool - if (locals.poolBasicState.totalLiqudity == 0) + // no liquidity in the pool + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -879,7 +933,7 @@ struct QSWAP : public ContractBase locals.poolBasicState.poolID = locals.poolID; locals.poolBasicState.reservedAssetAmount = 0; locals.poolBasicState.reservedQuAmount = 0; - locals.poolBasicState.totalLiqudity = 0; + locals.poolBasicState.totalLiquidity = 0; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); @@ -893,21 +947,22 @@ struct QSWAP : public ContractBase } - struct AddLiqudity_locals + struct AddLiquidity_locals { + QSWAPAddLiquidityMessage addLiquidityMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; - LiqudityInfo tmpLiqudity; + LiquidityInfo tmpLiquidity; - sint64 userLiqudityElementIndex; + sint64 userLiquidityElementIndex; sint64 quAmountDesired; sint64 quTransferAmount; sint64 assetTransferAmount; sint64 quOptimalAmount; sint64 assetOptimalAmount; - sint64 increaseLiqudity; + sint64 increaseLiquidity; sint64 reservedAssetAmountBefore; sint64 reservedAssetAmountAfter; @@ -918,13 +973,13 @@ struct QSWAP : public ContractBase uint128 i1, i2, i3; }; - PUBLIC_PROCEDURE_WITH_LOCALS(AddLiqudity) + PUBLIC_PROCEDURE_WITH_LOCALS(AddLiquidity) { - output.userIncreaseLiqudity = 0; + output.userIncreaseLiquidity = 0; output.assetAmount = 0; output.quAmount = 0; - // add liqudity must stake both qu and asset + // add liquidity must stake both qu and asset if (qpi.invocationReward() <= 0) { return; @@ -965,7 +1020,7 @@ struct QSWAP : public ContractBase // check if pool state meet the input condition before desposit // and confirm the final qu and asset amount to stake - if (locals.poolBasicState.totalLiqudity == 0) + if (locals.poolBasicState.totalLiquidity == 0) { locals.quTransferAmount = locals.quAmountDesired; locals.assetTransferAmount = input.assetAmountDesired; @@ -1046,11 +1101,11 @@ struct QSWAP : public ContractBase } // for pool's initial mint - if (locals.poolBasicState.totalLiqudity == 0) + if (locals.poolBasicState.totalLiquidity == 0) { - locals.increaseLiqudity = sqrt(locals.quTransferAmount, locals.assetTransferAmount, locals.i1, locals.i2, locals.i3); + locals.increaseLiquidity = sqrt(locals.quTransferAmount, locals.assetTransferAmount, locals.i1, locals.i2, locals.i3); - if (locals.increaseLiqudity < QSWAP_MIN_LIQUDITY ) + if (locals.increaseLiquidity < QSWAP_MIN_LIQUIDITY ) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1088,22 +1143,22 @@ struct QSWAP : public ContractBase } // permanently lock the first MINIMUM_LIQUIDITY tokens - locals.tmpLiqudity.entity = SELF; - locals.tmpLiqudity.liqudity = QSWAP_MIN_LIQUDITY; - state.mLiquditys.add(locals.poolID, locals.tmpLiqudity, 0); + locals.tmpLiquidity.entity = SELF; + locals.tmpLiquidity.liquidity = QSWAP_MIN_LIQUIDITY; + state.mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); - locals.tmpLiqudity.entity = qpi.invocator(); - locals.tmpLiqudity.liqudity = locals.increaseLiqudity - QSWAP_MIN_LIQUDITY; - state.mLiquditys.add(locals.poolID, locals.tmpLiqudity, 0); + locals.tmpLiquidity.entity = qpi.invocator(); + locals.tmpLiquidity.liquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; + state.mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); output.quAmount = locals.quTransferAmount; output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiqudity = locals.increaseLiqudity - QSWAP_MIN_LIQUDITY; + output.userIncreaseLiquidity = locals.increaseLiquidity - QSWAP_MIN_LIQUIDITY; } else { locals.tmpIncLiq0 = div( - uint128(locals.quTransferAmount) * uint128(locals.poolBasicState.totalLiqudity), + uint128(locals.quTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), uint128(locals.poolBasicState.reservedQuAmount) ); if (locals.tmpIncLiq0.high != 0 || locals.tmpIncLiq0.low > 0x7FFFFFFFFFFFFFFF) @@ -1112,7 +1167,7 @@ struct QSWAP : public ContractBase return; } locals.tmpIncLiq1 = div( - uint128(locals.assetTransferAmount) * uint128(locals.poolBasicState.totalLiqudity), + uint128(locals.assetTransferAmount) * uint128(locals.poolBasicState.totalLiquidity), uint128(locals.poolBasicState.reservedAssetAmount) ); if (locals.tmpIncLiq1.high != 0 || locals.tmpIncLiq1.low > 0x7FFFFFFFFFFFFFFF) @@ -1125,29 +1180,29 @@ struct QSWAP : public ContractBase // quTransferAmount * totalLiquity / reserveQuAmount, // assetTransferAmount * totalLiquity / reserveAssetAmount // ); - locals.increaseLiqudity = min(sint64(locals.tmpIncLiq0.low), sint64(locals.tmpIncLiq1.low)); + locals.increaseLiquidity = min(sint64(locals.tmpIncLiq0.low), sint64(locals.tmpIncLiq1.low)); // maybe too little input - if (locals.increaseLiqudity == 0) + if (locals.increaseLiquidity == 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - // find user liqudity index - locals.userLiqudityElementIndex = state.mLiquditys.headIndex(locals.poolID, 0); - while (locals.userLiqudityElementIndex != NULL_INDEX) + // find user liquidity index + locals.userLiquidityElementIndex = state.mLiquidities.headIndex(locals.poolID, 0); + while (locals.userLiquidityElementIndex != NULL_INDEX) { - if(state.mLiquditys.element(locals.userLiqudityElementIndex).entity == qpi.invocator()) + if(state.mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) { break; } - locals.userLiqudityElementIndex = state.mLiquditys.nextElementIndex(locals.userLiqudityElementIndex); + locals.userLiquidityElementIndex = state.mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); } - // no more space for new liqudity item - if ((locals.userLiqudityElementIndex == NULL_INDEX) && ( state.mLiquditys.population() == state.mLiquditys.capacity())) + // no more space for new liquidity item + if ((locals.userLiquidityElementIndex == NULL_INDEX) && ( state.mLiquidities.population() == state.mLiquidities.capacity())) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1186,50 +1241,61 @@ struct QSWAP : public ContractBase return; } - if (locals.userLiqudityElementIndex == NULL_INDEX) + if (locals.userLiquidityElementIndex == NULL_INDEX) { - locals.tmpLiqudity.entity = qpi.invocator(); - locals.tmpLiqudity.liqudity = locals.increaseLiqudity; - state.mLiquditys.add(locals.poolID, locals.tmpLiqudity, 0); + locals.tmpLiquidity.entity = qpi.invocator(); + locals.tmpLiquidity.liquidity = locals.increaseLiquidity; + state.mLiquidities.add(locals.poolID, locals.tmpLiquidity, 0); } else { - locals.tmpLiqudity = state.mLiquditys.element(locals.userLiqudityElementIndex); - locals.tmpLiqudity.liqudity += locals.increaseLiqudity; - state.mLiquditys.replace(locals.userLiqudityElementIndex, locals.tmpLiqudity); + locals.tmpLiquidity = state.mLiquidities.element(locals.userLiquidityElementIndex); + locals.tmpLiquidity.liquidity += locals.increaseLiquidity; + state.mLiquidities.replace(locals.userLiquidityElementIndex, locals.tmpLiquidity); } output.quAmount = locals.quTransferAmount; output.assetAmount = locals.assetTransferAmount; - output.userIncreaseLiqudity = locals.increaseLiqudity; + output.userIncreaseLiquidity = locals.increaseLiquidity; } locals.poolBasicState.reservedQuAmount += locals.quTransferAmount; locals.poolBasicState.reservedAssetAmount += locals.assetTransferAmount; - locals.poolBasicState.totalLiqudity += locals.increaseLiqudity; + locals.poolBasicState.totalLiquidity += locals.increaseLiquidity; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + // Log AddLiquidity procedure + locals.addLiquidityMessage._contractIndex = SELF_INDEX; + locals.addLiquidityMessage._type = QSWAPAddLiquidity; + locals.addLiquidityMessage.assetIssuer = input.assetIssuer; + locals.addLiquidityMessage.assetName = input.assetName; + locals.addLiquidityMessage.userIncreaseLiquidity = output.userIncreaseLiquidity; + locals.addLiquidityMessage.quAmount = output.quAmount; + locals.addLiquidityMessage.assetAmount = output.assetAmount; + LOG_INFO(locals.addLiquidityMessage); + if (qpi.invocationReward() > locals.quTransferAmount) { qpi.transfer(qpi.invocator(), qpi.invocationReward() - locals.quTransferAmount); } } - struct RemoveLiqudity_locals + struct RemoveLiquidity_locals { + QSWAPRemoveLiquidityMessage removeLiquidityMessage; id poolID; PoolBasicState poolBasicState; - sint64 userLiqudityElementIndex; + sint64 userLiquidityElementIndex; sint64 poolSlot; - LiqudityInfo userLiqudity; + LiquidityInfo userLiquidity; sint64 burnQuAmount; sint64 burnAssetAmount; uint32 i0; }; - PUBLIC_PROCEDURE_WITH_LOCALS(RemoveLiqudity) + PUBLIC_PROCEDURE_WITH_LOCALS(RemoveLiquidity) { output.quAmount = 0; output.assetAmount = 0; @@ -1268,45 +1334,45 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - locals.userLiqudityElementIndex = state.mLiquditys.headIndex(locals.poolID, 0); - while (locals.userLiqudityElementIndex != NULL_INDEX) + locals.userLiquidityElementIndex = state.mLiquidities.headIndex(locals.poolID, 0); + while (locals.userLiquidityElementIndex != NULL_INDEX) { - if(state.mLiquditys.element(locals.userLiqudityElementIndex).entity == qpi.invocator()) + if(state.mLiquidities.element(locals.userLiquidityElementIndex).entity == qpi.invocator()) { break; } - locals.userLiqudityElementIndex = state.mLiquditys.nextElementIndex(locals.userLiqudityElementIndex); + locals.userLiquidityElementIndex = state.mLiquidities.nextElementIndex(locals.userLiquidityElementIndex); } - if (locals.userLiqudityElementIndex == NULL_INDEX) + if (locals.userLiquidityElementIndex == NULL_INDEX) { return; } - locals.userLiqudity = state.mLiquditys.element(locals.userLiqudityElementIndex); + locals.userLiquidity = state.mLiquidities.element(locals.userLiquidityElementIndex); - // not enough liqudity for burning - if (locals.userLiqudity.liqudity < input.burnLiqudity ) + // not enough liquidity for burning + if (locals.userLiquidity.liquidity < input.burnLiquidity ) { return; } - if (locals.poolBasicState.totalLiqudity < input.burnLiqudity ) + if (locals.poolBasicState.totalLiquidity < input.burnLiquidity ) { return; } - // since burnLiqudity < totalLiqudity, so there will be no overflow risk + // since burnLiquidity < totalLiquidity, so there will be no overflow risk locals.burnQuAmount = sint64(div( - uint128(input.burnLiqudity) * uint128(locals.poolBasicState.reservedQuAmount), - uint128(locals.poolBasicState.totalLiqudity) + uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedQuAmount), + uint128(locals.poolBasicState.totalLiquidity) ).low); - // since burnLiqudity < totalLiqudity, so there will be no overflow risk + // since burnLiquidity < totalLiquidity, so there will be no overflow risk locals.burnAssetAmount = sint64(div( - uint128(input.burnLiqudity) * uint128(locals.poolBasicState.reservedAssetAmount), - uint128(locals.poolBasicState.totalLiqudity) + uint128(input.burnLiquidity) * uint128(locals.poolBasicState.reservedAssetAmount), + uint128(locals.poolBasicState.totalLiquidity) ).low); @@ -1329,27 +1395,35 @@ struct QSWAP : public ContractBase output.quAmount = locals.burnQuAmount; output.assetAmount = locals.burnAssetAmount; - // modify invocator's liqudity info - locals.userLiqudity.liqudity -= input.burnLiqudity; - if (locals.userLiqudity.liqudity == 0) + // modify invocator's liquidity info + locals.userLiquidity.liquidity -= input.burnLiquidity; + if (locals.userLiquidity.liquidity == 0) { - state.mLiquditys.remove(locals.userLiqudityElementIndex); + state.mLiquidities.remove(locals.userLiquidityElementIndex); } else { - state.mLiquditys.replace(locals.userLiqudityElementIndex, locals.userLiqudity); + state.mLiquidities.replace(locals.userLiquidityElementIndex, locals.userLiquidity); } - // modify the pool's liqudity info - locals.poolBasicState.totalLiqudity -= input.burnLiqudity; + // modify the pool's liquidity info + locals.poolBasicState.totalLiquidity -= input.burnLiquidity; locals.poolBasicState.reservedQuAmount -= locals.burnQuAmount; locals.poolBasicState.reservedAssetAmount -= locals.burnAssetAmount; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log RemoveLiquidity procedure + locals.removeLiquidityMessage._contractIndex = SELF_INDEX; + locals.removeLiquidityMessage._type = QSWAPRemoveLiquidity; + locals.removeLiquidityMessage.quAmount = output.quAmount; + locals.removeLiquidityMessage.assetAmount = output.assetAmount; + LOG_INFO(locals.removeLiquidityMessage); } struct SwapExactQuForAsset_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; sint64 quAmountIn; @@ -1404,8 +1478,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check the liqudity validity - if (locals.poolBasicState.totalLiqudity == 0) + // check the liquidity validity + if (locals.poolBasicState.totalLiquidity == 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1466,10 +1540,20 @@ struct QSWAP : public ContractBase locals.poolBasicState.reservedQuAmount += locals.quAmountIn - sint64(locals.feeToTeam.low) - sint64(locals.feeToProtocol.low); locals.poolBasicState.reservedAssetAmount -= locals.assetAmountOut; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapExactQuForAsset procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapExactQuForAsset; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = locals.quAmountIn; + locals.swapMessage.assetAmountOut = output.assetAmountOut; + LOG_INFO(locals.swapMessage); } struct SwapQuForExactAsset_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; @@ -1522,8 +1606,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check if there is liqudity in the poool - if (locals.poolBasicState.totalLiqudity == 0) + // check if there is liquidity in the poool + if (locals.poolBasicState.totalLiquidity == 0) { qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1601,10 +1685,20 @@ struct QSWAP : public ContractBase locals.poolBasicState.reservedQuAmount += locals.quAmountIn - sint64(locals.feeToTeam.low) - sint64(locals.feeToProtocol.low); locals.poolBasicState.reservedAssetAmount -= input.assetAmountOut; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapQuForExactAsset procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapQuForExactAsset; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = output.quAmountIn; + locals.swapMessage.assetAmountOut = input.assetAmountOut; + LOG_INFO(locals.swapMessage); } struct SwapExactAssetForQu_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; @@ -1656,8 +1750,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check the liqudity validity - if (locals.poolBasicState.totalLiqudity == 0) + // check the liquidity validity + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -1755,10 +1849,20 @@ struct QSWAP : public ContractBase state.protocolEarnedFee += locals.feeToProtocol.low; state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapExactAssetForQu procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapExactAssetForQu; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = input.assetAmountIn; + locals.swapMessage.assetAmountOut = output.quAmountOut; + LOG_INFO(locals.swapMessage); } struct SwapAssetForExactQu_locals { + QSWAPSwapMessage swapMessage; id poolID; sint64 poolSlot; PoolBasicState poolBasicState; @@ -1807,8 +1911,8 @@ struct QSWAP : public ContractBase locals.poolBasicState = state.mPoolBasicStates.get(locals.poolSlot); - // check the liqudity validity - if (locals.poolBasicState.totalLiqudity == 0) + // check the liquidity validity + if (locals.poolBasicState.totalLiquidity == 0) { return; } @@ -1903,6 +2007,15 @@ struct QSWAP : public ContractBase locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToTeam.low); locals.poolBasicState.reservedQuAmount -= sint64(locals.feeToProtocol.low); state.mPoolBasicStates.set(locals.poolSlot, locals.poolBasicState); + + // Log SwapAssetForExactQu procedure + locals.swapMessage._contractIndex = SELF_INDEX; + locals.swapMessage._type = QSWAPSwapAssetForExactQu; + locals.swapMessage.assetIssuer = input.assetIssuer; + locals.swapMessage.assetName = input.assetName; + locals.swapMessage.assetAmountIn = output.assetAmountIn; + locals.swapMessage.assetAmountOut = input.quAmountOut; + LOG_INFO(locals.swapMessage); } struct TransferShareOwnershipAndPossession_locals @@ -1982,12 +2095,52 @@ struct QSWAP : public ContractBase output.success = true; } + PUBLIC_PROCEDURE(TransferShareManagementRights) + { + if (qpi.invocationReward() < QSWAP_FEE_BASE_100) + { + return ; + } + + if (qpi.numberOfPossessedShares(input.asset.assetName, input.asset.issuer,qpi.invocator(), qpi.invocator(), SELF_INDEX, SELF_INDEX) < input.numberOfShares) + { + // not enough shares available + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + if (qpi.releaseShares(input.asset, qpi.invocator(), qpi.invocator(), input.numberOfShares, + input.newManagingContractIndex, input.newManagingContractIndex, QSWAP_FEE_BASE_100) < 0) + { + // error + output.transferredNumberOfShares = 0; + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + } + else + { + // success + output.transferredNumberOfShares = input.numberOfShares; + if (qpi.invocationReward() > QSWAP_FEE_BASE_100) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward() - QSWAP_FEE_BASE_100); + } + } + } + } + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() { // functions REGISTER_USER_FUNCTION(Fees, 1); REGISTER_USER_FUNCTION(GetPoolBasicState, 2); - REGISTER_USER_FUNCTION(GetLiqudityOf, 3); + REGISTER_USER_FUNCTION(GetLiquidityOf, 3); REGISTER_USER_FUNCTION(QuoteExactQuInput, 4); REGISTER_USER_FUNCTION(QuoteExactQuOutput, 5); REGISTER_USER_FUNCTION(QuoteExactAssetInput, 6); @@ -1998,13 +2151,14 @@ struct QSWAP : public ContractBase REGISTER_USER_PROCEDURE(IssueAsset, 1); REGISTER_USER_PROCEDURE(TransferShareOwnershipAndPossession, 2); REGISTER_USER_PROCEDURE(CreatePool, 3); - REGISTER_USER_PROCEDURE(AddLiqudity, 4); - REGISTER_USER_PROCEDURE(RemoveLiqudity, 5); + REGISTER_USER_PROCEDURE(AddLiquidity, 4); + REGISTER_USER_PROCEDURE(RemoveLiquidity, 5); REGISTER_USER_PROCEDURE(SwapExactQuForAsset, 6); REGISTER_USER_PROCEDURE(SwapQuForExactAsset, 7); REGISTER_USER_PROCEDURE(SwapExactAssetForQu, 8); REGISTER_USER_PROCEDURE(SwapAssetForExactQu, 9); REGISTER_USER_PROCEDURE(SetTeamInfo, 10); + REGISTER_USER_PROCEDURE(TransferShareManagementRights, 11); } INITIALIZE() @@ -2036,4 +2190,8 @@ struct QSWAP : public ContractBase } } } + PRE_ACQUIRE_SHARES() + { + output.allowTransfer = true; + } }; diff --git a/src/contracts/Quottery.h b/src/contracts/Quottery.h index a76e0dc59..cd6c06a8b 100644 --- a/src/contracts/Quottery.h +++ b/src/contracts/Quottery.h @@ -14,14 +14,13 @@ constexpr unsigned long long QUOTTERY_MIN_AMOUNT_PER_BET_SLOT_ = 10000ULL; constexpr unsigned long long QUOTTERY_SHAREHOLDER_FEE_ = 1000; // 10% constexpr unsigned long long QUOTTERY_GAME_OPERATOR_FEE_ = 50; // 0.5% constexpr unsigned long long QUOTTERY_BURN_FEE_ = 200; // 2% -static_assert(QUOTTERY_BURN_FEE_ > 0, "SC requires burning qu to operate, the burn fee must be higher than 0!"); - +STATIC_ASSERT(QUOTTERY_BURN_FEE_ > 0, BurningRequiredToOperate); constexpr unsigned long long QUOTTERY_TICK_TO_KEEP_AFTER_END = 100ULL; -enum QuotteryLogInfo { +enum QUOTTERYLogInfo { invalidMaxBetSlotPerOption=0, invalidOption = 1, invalidBetAmount = 2, @@ -38,12 +37,12 @@ enum QuotteryLogInfo { betIsAlreadyFinalized = 13, totalError = 14 }; -struct QuotteryLogger +struct QUOTTERYLogger { uint32 _contractIndex; uint32 _type; // Assign a random unique (per contract) number to distinguish messages of different types // Other data go here - char _terminator; // Only data before "_terminator" are logged + sint8 _terminator; // Only data before "_terminator" are logged }; struct QUOTTERY2 @@ -196,6 +195,7 @@ struct QUOTTERY : public ContractBase uint64 baseId0, baseId1; sint32 numberOP, requiredVote, winOption, totalOption, voteCounter, numberOfSlot, currentState; uint64 amountPerSlot, totalBetSlot, potAmountTotal, feeChargedAmount, transferredAmount, fee, profitPerBetSlot, nWinBet; + QUOTTERYLogger log; }; /**************************************/ @@ -223,24 +223,24 @@ struct QUOTTERY : public ContractBase Array mBetResultWonOption; Array mBetResultOPId; - //static assert for developing: - static_assert(sizeof(mBetID) == (sizeof(uint32) * QUOTTERY_MAX_BET), "bet id array"); - static_assert(sizeof(mCreator) == (sizeof(id) * QUOTTERY_MAX_BET), "creator array"); - static_assert(sizeof(mBetDesc) == (sizeof(id) * QUOTTERY_MAX_BET), "desc array"); - static_assert(sizeof(mOptionDesc) == (sizeof(id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_OPTION), "option desc array"); - static_assert(sizeof(mBetAmountPerSlot) == (sizeof(uint64) * QUOTTERY_MAX_BET), "bet amount per slot array"); - static_assert(sizeof(mMaxNumberOfBetSlotPerOption) == (sizeof(uint32) * QUOTTERY_MAX_BET), "number of bet slot per option array"); - static_assert(sizeof(mOracleProvider) == (sizeof(QPI::id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), "oracle providers"); - static_assert(sizeof(mOracleFees) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), "oracle providers fees"); - static_assert(sizeof(mCurrentBetState) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), "bet states"); - static_assert(sizeof(mNumberOption) == (sizeof(uint8) * QUOTTERY_MAX_BET), "number of options"); - static_assert(sizeof(mOpenDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), "open date"); - static_assert(sizeof(mCloseDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), "close date"); - static_assert(sizeof(mEndDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), "end date"); - static_assert(sizeof(mBetResultWonOption) == (QUOTTERY_MAX_BET * 8), "won option array"); - static_assert(sizeof(mBetResultOPId) == (QUOTTERY_MAX_BET * 8), "op id array"); - static_assert(sizeof(mBettorID) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(id)), "bettor array"); - static_assert(sizeof(mBettorBetOption) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(uint8)), "bet option"); + // static assert for developing: + STATIC_ASSERT(sizeof(mBetID) == (sizeof(uint32) * QUOTTERY_MAX_BET), BetIdArray); + STATIC_ASSERT(sizeof(mCreator) == (sizeof(id) * QUOTTERY_MAX_BET), CreatorArray); + STATIC_ASSERT(sizeof(mBetDesc) == (sizeof(id) * QUOTTERY_MAX_BET), DescArray); + STATIC_ASSERT(sizeof(mOptionDesc) == (sizeof(id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_OPTION), OptionDescArray); + STATIC_ASSERT(sizeof(mBetAmountPerSlot) == (sizeof(uint64) * QUOTTERY_MAX_BET), BetAmountPerSlotArray); + STATIC_ASSERT(sizeof(mMaxNumberOfBetSlotPerOption) == (sizeof(uint32) * QUOTTERY_MAX_BET), NumberOfBetSlotPerOptionArray); + STATIC_ASSERT(sizeof(mOracleProvider) == (sizeof(QPI::id) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), OracleProviders); + STATIC_ASSERT(sizeof(mOracleFees) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), OracleProvidersFees); + STATIC_ASSERT(sizeof(mCurrentBetState) == (sizeof(uint32) * QUOTTERY_MAX_BET * QUOTTERY_MAX_ORACLE_PROVIDER), BetStates); + STATIC_ASSERT(sizeof(mNumberOption) == (sizeof(uint8) * QUOTTERY_MAX_BET), NumberOfOptions); + STATIC_ASSERT(sizeof(mOpenDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), OpenDate); + STATIC_ASSERT(sizeof(mCloseDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), CloseDate); + STATIC_ASSERT(sizeof(mEndDate) == (sizeof(uint8) * 4 * QUOTTERY_MAX_BET), EndDate); + STATIC_ASSERT(sizeof(mBetResultWonOption) == (QUOTTERY_MAX_BET * 8), WonOptionArray); + STATIC_ASSERT(sizeof(mBetResultOPId) == (QUOTTERY_MAX_BET * 8), OpIdArray); + STATIC_ASSERT(sizeof(mBettorID) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(id)), BettorArray); + STATIC_ASSERT(sizeof(mBettorBetOption) == (QUOTTERY_MAX_BET * QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET * QUOTTERY_MAX_OPTION * sizeof(uint8)), BetOption); // other stats uint32 mCurrentBetID; @@ -335,13 +335,6 @@ struct QUOTTERY : public ContractBase _second = qtryGetSecond(data); //6bits } - /** - * @return Current date from core node system - */ - inline static void getCurrentDate(const QPI::QpiContextProcedureCall& qpi, uint32& res) { - packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), res); - } - inline static void accumulatedDay(sint32 month, uint64& res) { switch (month) @@ -609,8 +602,8 @@ struct QUOTTERY : public ContractBase } else { - QuotteryLogger log{ 0,QuotteryLogInfo::notEnoughVote,0 }; - LOG_INFO(log); + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::notEnoughVote,0 }; + LOG_INFO(locals.log); } } /**************************************/ @@ -849,7 +842,7 @@ struct QUOTTERY : public ContractBase checkAndCleanMemorySlots_input _checkAndCleanMemorySlots_input; checkAndCleanMemorySlots_output _checkAndCleanMemorySlots_output; checkAndCleanMemorySlots_locals _checkAndCleanMemorySlots_locals; - QuotteryLogger log; + QUOTTERYLogger log; }; PUBLIC_PROCEDURE_WITH_LOCALS(issueBet) @@ -860,10 +853,10 @@ struct QUOTTERY : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); if (!checkValidQtryDateTime(input.closeDate) || !checkValidQtryDateTime(input.endDate)) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidDate,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidDate,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -872,28 +865,28 @@ struct QUOTTERY : public ContractBase if (dateCompare(input.closeDate, input.endDate, locals.i0) == 1 || dateCompare(locals.curDate, input.closeDate, locals.i0) == 1) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidDate,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidDate,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } if (input.amountPerSlot < state.mMinAmountPerBetSlot) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetAmount,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetAmount,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } if (input.numberOfOption > QUOTTERY_MAX_OPTION || input.numberOfOption < 2) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidOption,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidOption,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } if (input.maxBetSlotPerOption == 0 || input.maxBetSlotPerOption > QUOTTERY_MAX_SLOT_PER_OPTION_PER_BET) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidMaxBetSlotPerOption,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidMaxBetSlotPerOption,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -907,7 +900,7 @@ struct QUOTTERY : public ContractBase // fee is higher than sent amount, exit if (locals.fee > qpi.invocationReward()) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::insufficientFund,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::insufficientFund,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -934,7 +927,7 @@ struct QUOTTERY : public ContractBase //out of bet storage, exit if (locals.slotId == -1) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::outOfStorage,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::outOfStorage,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -974,7 +967,7 @@ struct QUOTTERY : public ContractBase } if (locals.numberOP == 0) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidNumberOfOracleProvider,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidNumberOfOracleProvider,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1011,7 +1004,7 @@ struct QUOTTERY : public ContractBase sint64 amountPerSlot, fee; uint32 availableSlotForBet; sint64 slotId; - QuotteryLogger log; + QUOTTERYLogger log; }; /** * Join a bet @@ -1041,7 +1034,7 @@ struct QUOTTERY : public ContractBase if (locals.slotId == -1) { // can't find betId - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetId,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1053,14 +1046,14 @@ struct QUOTTERY : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.closeDate = state.mCloseDate.get(locals.slotId); if (dateCompare(locals.curDate, locals.closeDate, locals.i0) > 0) { // bet is closed for betting - QuotteryLogger log{ 0,QuotteryLogInfo::expiredBet,0 }; - LOG_INFO(log); + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::expiredBet,0 }; + LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; } @@ -1069,7 +1062,7 @@ struct QUOTTERY : public ContractBase if (input.option >= state.mNumberOption.get(locals.slotId)) { // bet is closed for betting - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidOption,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidOption,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1086,7 +1079,7 @@ struct QUOTTERY : public ContractBase if (locals.numberOfSlot == 0) { // out of slot - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::outOfSlot,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::outOfSlot,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1096,7 +1089,7 @@ struct QUOTTERY : public ContractBase if (locals.fee > qpi.invocationReward()) { // not send enough amount - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::insufficientFund,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::insufficientFund,0 }; LOG_INFO(locals.log); qpi.transfer(qpi.invocator(), qpi.invocationReward()); return; @@ -1136,7 +1129,7 @@ struct QUOTTERY : public ContractBase uint64 baseId0; sint64 slotId, writeId; sint8 opId; - QuotteryLogger log; + QUOTTERYLogger log; tryFinalizeBet_locals tfb; tryFinalizeBet_input _tryFinalizeBet_input; tryFinalizeBet_output _tryFinalizeBet_output; @@ -1166,7 +1159,7 @@ struct QUOTTERY : public ContractBase if (locals.slotId == -1) { // can't find betId - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetId,0 }; LOG_INFO(locals.log); return; } @@ -1174,11 +1167,11 @@ struct QUOTTERY : public ContractBase if (state.mIsOccupied.get(locals.slotId) == 0) { // the bet is over - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::expiredBet,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::expiredBet,0 }; LOG_INFO(locals.log); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.endDate = state.mEndDate.get(locals.slotId); // endDate is counted as 23:59 of that day if (dateCompare(locals.curDate, locals.endDate, locals.i0) <= 0) @@ -1201,7 +1194,7 @@ struct QUOTTERY : public ContractBase if (locals.opId == -1) { // is not oracle provider - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidOPId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidOPId,0 }; LOG_INFO(locals.log); return; } @@ -1212,7 +1205,7 @@ struct QUOTTERY : public ContractBase if (state.mBetEndTick.get(locals.slotId) != 0) { // is already finalized - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::betIsAlreadyFinalized,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::betIsAlreadyFinalized,0 }; LOG_INFO(locals.log); return; } @@ -1271,7 +1264,7 @@ struct QUOTTERY : public ContractBase uint64 amountPerSlot; uint64 duration, u64_0, u64_1; sint64 slotId; - QuotteryLogger log; + QUOTTERYLogger log; cleanMemorySlot_locals cms; cleanMemorySlot_input _cleanMemorySlot_input; cleanMemorySlot_output _cleanMemorySlot_output; @@ -1287,7 +1280,7 @@ struct QUOTTERY : public ContractBase // all funds will be returned if (qpi.invocator() != state.mGameOperatorId) { - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::notGameOperator,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::notGameOperator,0 }; LOG_INFO(locals.log); return; } @@ -1303,7 +1296,7 @@ struct QUOTTERY : public ContractBase if (locals.slotId == -1) { // can't find betId - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::invalidBetId,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::invalidBetId,0 }; LOG_INFO(locals.log); return; } @@ -1311,11 +1304,11 @@ struct QUOTTERY : public ContractBase if (state.mIsOccupied.get(locals.slotId) == 0) { // the bet is over - locals.log = QuotteryLogger{ 0,QuotteryLogInfo::expiredBet,0 }; + locals.log = QUOTTERYLogger{ 0,QUOTTERYLogInfo::expiredBet,0 }; LOG_INFO(locals.log); return; } - getCurrentDate(qpi, locals.curDate); + packQuotteryDate(qpi.year(), qpi.month(), qpi.day(), qpi.hour(), qpi.minute(), qpi.second(), locals.curDate); locals.endDate = state.mEndDate.get(locals.slotId); // endDate is counted as 23:59 of that day diff --git a/src/contracts/Qx.h b/src/contracts/Qx.h index 601a374a4..0379f696d 100644 --- a/src/contracts/Qx.h +++ b/src/contracts/Qx.h @@ -220,7 +220,7 @@ struct QX : public ContractBase sint64 price; sint64 numberOfShares; - char _terminator; + sint8 _terminator; } _tradeMessage; struct _NumberOfReservedShares_input @@ -533,8 +533,8 @@ struct QX : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (input.price <= 0 - || input.numberOfShares <= 0) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT) { output.addedNumberOfShares = 0; } @@ -625,7 +625,7 @@ struct QX : public ContractBase state._elementIndex2 = state._entityOrders.nextElementIndex(state._elementIndex2); } - state._fee = (state._price * state._assetOrder.numberOfShares * state._tradeFee / 1000000000UL) + 1; + state._fee = div(state._price * state._assetOrder.numberOfShares * state._tradeFee, 1000000000LL) + 1; state._earnedAmount += state._fee; qpi.transfer(qpi.invocator(), state._price * state._assetOrder.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, qpi.invocator(), qpi.invocator(), state._assetOrder.numberOfShares, state._assetOrder.entity); @@ -659,7 +659,7 @@ struct QX : public ContractBase state._elementIndex = state._entityOrders.nextElementIndex(state._elementIndex); } - state._fee = (state._price * input.numberOfShares * state._tradeFee / 1000000000UL) + 1; + state._fee = div(state._price * input.numberOfShares * state._tradeFee, 1000000000LL) + 1; state._earnedAmount += state._fee; qpi.transfer(qpi.invocator(), state._price * input.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, qpi.invocator(), qpi.invocator(), input.numberOfShares, state._assetOrder.entity); @@ -694,9 +694,9 @@ struct QX : public ContractBase PUBLIC_PROCEDURE(AddToBidOrder) { - if (input.price <= 0 - || input.numberOfShares <= 0 - || qpi.invocationReward() < input.price * input.numberOfShares) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT + || qpi.invocationReward() < smul(input.price, input.numberOfShares)) { output.addedNumberOfShares = 0; @@ -707,9 +707,9 @@ struct QX : public ContractBase } else { - if (qpi.invocationReward() > input.price * input.numberOfShares) + if (qpi.invocationReward() > smul(input.price, input.numberOfShares)) { - qpi.transfer(qpi.invocator(), qpi.invocationReward() - input.price * input.numberOfShares); + qpi.transfer(qpi.invocator(), qpi.invocationReward() - smul(input.price, input.numberOfShares)); } output.addedNumberOfShares = input.numberOfShares; @@ -788,7 +788,7 @@ struct QX : public ContractBase state._elementIndex2 = state._entityOrders.nextElementIndex(state._elementIndex2); } - state._fee = (state._price * state._assetOrder.numberOfShares * state._tradeFee / 1000000000UL) + 1; + state._fee = div(state._price * state._assetOrder.numberOfShares * state._tradeFee, 1000000000LL) + 1; state._earnedAmount += state._fee; qpi.transfer(state._assetOrder.entity, state._price * state._assetOrder.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, state._assetOrder.entity, state._assetOrder.entity, state._assetOrder.numberOfShares, qpi.invocator()); @@ -826,7 +826,7 @@ struct QX : public ContractBase state._elementIndex = state._entityOrders.nextElementIndex(state._elementIndex); } - state._fee = (state._price * input.numberOfShares * state._tradeFee / 1000000000UL) + 1; + state._fee = div(state._price * input.numberOfShares * state._tradeFee, 1000000000LL) + 1; state._earnedAmount += state._fee; qpi.transfer(state._assetOrder.entity, state._price * input.numberOfShares - state._fee); qpi.transferShareOwnershipAndPossession(input.assetName, input.issuer, state._assetOrder.entity, state._assetOrder.entity, input.numberOfShares, qpi.invocator()); @@ -869,8 +869,8 @@ struct QX : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (input.price <= 0 - || input.numberOfShares <= 0) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT) { output.removedNumberOfShares = 0; } @@ -956,8 +956,8 @@ struct QX : public ContractBase qpi.transfer(qpi.invocator(), qpi.invocationReward()); } - if (input.price <= 0 - || input.numberOfShares <= 0) + if (input.price <= 0 || input.price >= MAX_AMOUNT + || input.numberOfShares <= 0 || input.numberOfShares >= MAX_AMOUNT) { output.removedNumberOfShares = 0; } diff --git a/src/contracts/RandomLottery.h b/src/contracts/RandomLottery.h new file mode 100644 index 000000000..448cf0c96 --- /dev/null +++ b/src/contracts/RandomLottery.h @@ -0,0 +1,521 @@ +/** + * @file RandomLottery.h + * @brief Random Lottery contract definition: state, data structures, and user / internal + * procedures. + * + * This header declares the RL (Random Lottery) contract which: + * - Sells tickets during a SELLING epoch. + * - Draws a pseudo-random winner when the epoch ends. + * - Distributes fees (team, distribution, burn, winner). + * - Records winners' history in a ring-like buffer. + */ + +using namespace QPI; + +/// Maximum number of players allowed in the lottery. +constexpr uint16 RL_MAX_NUMBER_OF_PLAYERS = 1024; + +/// Maximum number of winners kept in the on-chain winners history buffer. +constexpr uint16 RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY = 1024; + +/// Placeholder structure for future extensions. +struct RL2 +{ +}; + +/** + * @brief Main contract class implementing the random lottery mechanics. + * + * Lifecycle: + * 1. INITIALIZE sets defaults (fees, ticket price, state LOCKED). + * 2. BEGIN_EPOCH opens ticket selling (SELLING). + * 3. Users call BuyTicket while SELLING. + * 4. END_EPOCH closes, computes fees, selects winner, distributes, burns rest. + * 5. Players list is cleared for next epoch. + */ +struct RL : public ContractBase +{ +public: + /** + * @brief High-level finite state of the lottery. + * SELLING: tickets can be purchased. + * LOCKED: purchases closed; waiting for epoch transition. + */ + enum class EState : uint8 + { + SELLING, + LOCKED + }; + + /** + * @brief Standardized return / error codes for procedures. + */ + enum class EReturnCode : uint8 + { + SUCCESS = 0, + // Ticket-related errors + TICKET_INVALID_PRICE = 1, + TICKET_ALREADY_PURCHASED = 2, + TICKET_ALL_SOLD_OUT = 3, + TICKET_SELLING_CLOSED = 4, + // Access-related errors + ACCESS_DENIED = 5, + // Fee-related errors + FEE_INVALID_PERCENT_VALUE = 6, + // Fallback + UNKNOW_ERROR = UINT8_MAX + }; + + //---- User-facing I/O structures ------------------------------------------------------------- + + struct BuyTicket_input + { + }; + + struct BuyTicket_output + { + uint8 returnCode = static_cast(EReturnCode::SUCCESS); + }; + + struct GetFees_input + { + }; + + struct GetFees_output + { + uint8 teamFeePercent = 0; + uint8 distributionFeePercent = 0; + uint8 winnerFeePercent = 0; + uint8 burnPercent = 0; + uint8 returnCode = static_cast(EReturnCode::SUCCESS); + }; + + struct GetPlayers_input + { + }; + + struct GetPlayers_output + { + Array players; + uint16 numberOfPlayers = 0; + uint8 returnCode = static_cast(EReturnCode::SUCCESS); + }; + + struct GetPlayers_locals + { + uint64 arrayIndex = 0; + sint64 i = 0; + }; + + /** + * @brief Stored winner snapshot for an epoch. + */ + struct WinnerInfo + { + id winnerAddress = id::zero(); + uint64 revenue = 0; + uint16 epoch = 0; + uint32 tick = 0; + }; + + struct FillWinnersInfo_input + { + id winnerAddress = id::zero(); + uint64 revenue = 0; + }; + + struct FillWinnersInfo_output + { + }; + + struct FillWinnersInfo_locals + { + WinnerInfo winnerInfo = {}; + }; + + struct GetWinner_input + { + }; + + struct GetWinner_output + { + id winnerAddress = id::zero(); + uint64 index = 0; + }; + + struct GetWinner_locals + { + uint64 randomNum = 0; + sint64 i = 0; + uint64 j = 0; + }; + + struct GetWinners_input + { + }; + + struct GetWinners_output + { + Array winners; + uint64 numberOfWinners = 0; + uint8 returnCode = static_cast(EReturnCode::SUCCESS); + }; + + struct ReturnAllTickets_input + { + }; + struct ReturnAllTickets_output + { + }; + + struct ReturnAllTickets_locals + { + sint64 i = 0; + }; + + struct END_EPOCH_locals + { + GetWinner_input getWinnerInput = {}; + GetWinner_output getWinnerOutput = {}; + GetWinner_locals getWinnerLocals = {}; + + FillWinnersInfo_input fillWinnersInfoInput = {}; + FillWinnersInfo_output fillWinnersInfoOutput = {}; + FillWinnersInfo_locals fillWinnersInfoLocals = {}; + + ReturnAllTickets_input returnAllTicketsInput = {}; + ReturnAllTickets_output returnAllTicketsOutput = {}; + ReturnAllTickets_locals returnAllTicketsLocals = {}; + + uint64 teamFee = 0; + uint64 distributionFee = 0; + uint64 winnerAmount = 0; + uint64 burnedAmount = 0; + + uint64 revenue = 0; + Entity entity = {}; + + sint32 i = 0; + }; + +public: + /** + * @brief Registers all externally callable functions and procedures with their numeric + * identifiers. Mapping numbers must remain stable to preserve external interface compatibility. + */ + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_FUNCTION(GetFees, 1); + REGISTER_USER_FUNCTION(GetPlayers, 2); + REGISTER_USER_FUNCTION(GetWinners, 3); + REGISTER_USER_PROCEDURE(BuyTicket, 1); + } + + /** + * @brief Contract initialization hook. + * Sets default fees, ticket price, addresses, and locks the lottery (no selling yet). + */ + INITIALIZE() + { + // Addresses + state.teamAddress = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, + _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + // Owner address (currently identical to developer address; can be split in future revisions). + state.ownerAddress = state.teamAddress; + + // Default fee percentages (sum <= 100; winner percent derived) + state.teamFeePercent = 10; + state.distributionFeePercent = 20; + state.burnPercent = 2; + state.winnerFeePercent = 100 - state.teamFeePercent - state.distributionFeePercent - state.burnPercent; + + // Default ticket price + state.ticketPrice = 1000000; + + // Start locked + state.currentState = EState::LOCKED; + } + + /** + * @brief Opens ticket selling for a new epoch. + */ + BEGIN_EPOCH() { state.currentState = EState::SELLING; } + + /** + * @brief Closes epoch: computes revenue, selects winner (if >1 player), + * distributes fees, burns leftover, records winner, then clears players. + */ + END_EPOCH_WITH_LOCALS() + { + state.currentState = EState::LOCKED; + + // Single-player edge case: refund instead of drawing. + if (state.players.population() == 1) + { + ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); + } + else if (state.players.population() > 1) + { + qpi.getEntity(SELF, locals.entity); + locals.revenue = locals.entity.incomingAmount - locals.entity.outgoingAmount; + + // Winner selection (pseudo-random). + GetWinner(qpi, state, locals.getWinnerInput, locals.getWinnerOutput, locals.getWinnerLocals); + + if (locals.getWinnerOutput.winnerAddress != id::zero()) + { + // Fee splits + locals.winnerAmount = div(locals.revenue * state.winnerFeePercent, 100ULL); + locals.teamFee = div(locals.revenue * state.teamFeePercent, 100ULL); + locals.distributionFee = div(locals.revenue * state.distributionFeePercent, 100ULL); + locals.burnedAmount = div(locals.revenue * state.burnPercent, 100ULL); + + // Team fee + if (locals.teamFee > 0) + { + qpi.transfer(state.teamAddress, locals.teamFee); + } + + // Distribution fee + if (locals.distributionFee > 0) + { + qpi.distributeDividends(div(locals.distributionFee, uint64(NUMBER_OF_COMPUTORS))); + } + + // Winner payout + if (locals.winnerAmount > 0) + { + qpi.transfer(locals.getWinnerOutput.winnerAddress, locals.winnerAmount); + } + + // Burn remainder + if (locals.burnedAmount > 0) + { + qpi.burn(locals.burnedAmount); + } + + // Persist winner record + locals.fillWinnersInfoInput.winnerAddress = locals.getWinnerOutput.winnerAddress; + locals.fillWinnersInfoInput.revenue = locals.winnerAmount; + FillWinnersInfo(qpi, state, locals.fillWinnersInfoInput, locals.fillWinnersInfoOutput, locals.fillWinnersInfoLocals); + } + else + { + // Return funds to players if no winner could be selected (should be impossible). + ReturnAllTickets(qpi, state, locals.returnAllTicketsInput, locals.returnAllTicketsOutput, locals.returnAllTicketsLocals); + } + } + + // Prepare for next epoch. + state.players.reset(); + } + + /** + * @brief Returns currently configured fee percentages. + */ + PUBLIC_FUNCTION(GetFees) + { + output.teamFeePercent = state.teamFeePercent; + output.distributionFeePercent = state.distributionFeePercent; + output.winnerFeePercent = state.winnerFeePercent; + output.burnPercent = state.burnPercent; + } + + /** + * @brief Retrieves the active players list for the ongoing epoch. + */ + PUBLIC_FUNCTION_WITH_LOCALS(GetPlayers) + { + locals.arrayIndex = 0; + + locals.i = state.players.nextElementIndex(NULL_INDEX); + while (locals.i != NULL_INDEX) + { + output.players.set(locals.arrayIndex++, state.players.key(locals.i)); + locals.i = state.players.nextElementIndex(locals.i); + }; + + output.numberOfPlayers = static_cast(locals.arrayIndex); + } + + /** + * @brief Returns historical winners (ring buffer segment). + */ + PUBLIC_FUNCTION(GetWinners) + { + output.winners = state.winners; + output.numberOfWinners = state.winnersInfoNextEmptyIndex; + } + + /** + * @brief Attempts to buy a ticket (must send exact price unless zero is forbidden; state must + * be SELLING). Reverts with proper return codes for invalid cases. + */ + PUBLIC_PROCEDURE(BuyTicket) + { + // Selling closed + if (state.currentState == EState::LOCKED) + { + output.returnCode = static_cast(EReturnCode::TICKET_SELLING_CLOSED); + if (qpi.invocationReward() > 0) + { + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + } + return; + } + + // Already purchased + if (state.players.contains(qpi.invocator())) + { + output.returnCode = static_cast(EReturnCode::TICKET_ALREADY_PURCHASED); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + return; + } + + // Capacity full + if (state.players.add(qpi.invocator()) == NULL_INDEX) + { + output.returnCode = static_cast(EReturnCode::TICKET_ALL_SOLD_OUT); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + + return; + } + + // Price mismatch + if (qpi.invocationReward() != state.ticketPrice && qpi.invocationReward() > 0) + { + output.returnCode = static_cast(EReturnCode::TICKET_INVALID_PRICE); + qpi.transfer(qpi.invocator(), qpi.invocationReward()); + state.players.remove(qpi.invocator()); + + state.players.cleanupIfNeeded(80); + return; + } + } + +private: + /** + * @brief Internal: records a winner into the cyclic winners array. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(FillWinnersInfo) + { + if (input.winnerAddress == id::zero()) + { + return; + } + + state.winnersInfoNextEmptyIndex = mod(state.winnersInfoNextEmptyIndex, state.winners.capacity()); + + locals.winnerInfo.winnerAddress = input.winnerAddress; + locals.winnerInfo.revenue = input.revenue; + locals.winnerInfo.epoch = qpi.epoch(); + locals.winnerInfo.tick = qpi.tick(); + state.winners.set(state.winnersInfoNextEmptyIndex++, locals.winnerInfo); + } + + /** + * @brief Internal: pseudo-random selection of a winner index using hardware RNG. + */ + PRIVATE_PROCEDURE_WITH_LOCALS(GetWinner) + { + if (state.players.population() == 0) + { + return; + } + + locals.randomNum = mod(qpi.K12(qpi.getPrevSpectrumDigest()).u64._0, state.players.population()); + + locals.j = 0; + locals.i = state.players.nextElementIndex(NULL_INDEX); + while (locals.i != NULL_INDEX) + { + if (locals.j++ == locals.randomNum) + { + output.winnerAddress = state.players.key(locals.i); + output.index = locals.i; + break; + } + + locals.i = state.players.nextElementIndex(locals.i); + }; + } + + PRIVATE_PROCEDURE_WITH_LOCALS(ReturnAllTickets) + { + locals.i = state.players.nextElementIndex(NULL_INDEX); + while (locals.i != NULL_INDEX) + { + qpi.transfer(state.players.key(locals.i), state.ticketPrice); + + locals.i = state.players.nextElementIndex(locals.i); + }; + } + +protected: + /** + * @brief Address of the team managing the lottery contract. + * Initialized to a zero address. + */ + id teamAddress = id::zero(); + + /** + * @brief Address of the owner of the lottery contract. + * Initialized to a zero address. + */ + id ownerAddress = id::zero(); + + /** + * @brief Percentage of the revenue allocated to the team. + * Value is between 0 and 100. + */ + uint8 teamFeePercent = 0; + + /** + * @brief Percentage of the revenue allocated for distribution. + * Value is between 0 and 100. + */ + uint8 distributionFeePercent = 0; + + /** + * @brief Percentage of the revenue allocated to the winner. + * Automatically calculated as the remainder after other fees. + */ + uint8 winnerFeePercent = 0; + + /** + * @brief Percentage of the revenue to be burned. + * Value is between 0 and 100. + */ + uint8 burnPercent = 0; + + /** + * @brief Price of a single lottery ticket. + * Value is in the smallest currency unit (e.g., cents). + */ + uint64 ticketPrice = 0; + + /** + * @brief Set of players participating in the current lottery epoch. + * Maximum capacity is defined by RL_MAX_NUMBER_OF_PLAYERS. + */ + HashSet players = {}; + + /** + * @brief Circular buffer storing the history of winners. + * Maximum capacity is defined by RL_MAX_NUMBER_OF_WINNERS_IN_HISTORY. + */ + Array winners = {}; + + /** + * @brief Index pointing to the next empty slot in the winners array. + * Used for maintaining the circular buffer of winners. + */ + uint64 winnersInfoNextEmptyIndex = 0; + + /** + * @brief Current state of the lottery contract. + * Can be either SELLING (tickets available) or LOCKED (epoch closed). + */ + EState currentState = EState::LOCKED; +}; diff --git a/src/contracts/TestExampleD.h b/src/contracts/TestExampleD.h index d78a054de..3cb3e0caf 100644 --- a/src/contracts/TestExampleD.h +++ b/src/contracts/TestExampleD.h @@ -19,7 +19,7 @@ struct TESTEXD : public ContractBase locals.balance = locals.entity.incomingAmount - locals.entity.outgoingAmount; if (locals.balance > NUMBER_OF_COMPUTORS) { - qpi.distributeDividends(locals.balance / NUMBER_OF_COMPUTORS); + qpi.distributeDividends(div(locals.balance, NUMBER_OF_COMPUTORS)); } } diff --git a/src/contracts/VottunBridge.h b/src/contracts/VottunBridge.h new file mode 100644 index 000000000..2396005d1 --- /dev/null +++ b/src/contracts/VottunBridge.h @@ -0,0 +1,1908 @@ +using namespace QPI; + +struct VOTTUNBRIDGE2 +{ +}; + +struct VOTTUNBRIDGE : public ContractBase +{ +public: + // Bridge Order Structure + struct BridgeOrder + { + id qubicSender; // Sender address on Qubic + id qubicDestination; // Destination address on Qubic + Array ethAddress; // Destination Ethereum address + uint64 orderId; // Unique ID for the order + uint64 amount; // Amount to transfer + uint8 orderType; // Type of order (e.g., mint, transfer) + uint8 status; // Order status (e.g., Created, Pending, Refunded) + bit fromQubicToEthereum; // Direction of transfer + bit tokensReceived; // Flag to indicate if tokens have been received + bit tokensLocked; // Flag to indicate if tokens are in locked state + }; + + // Input and Output Structs + struct createOrder_input + { + id qubicDestination; // Destination address on Qubic (for EVM to Qubic orders) + uint64 amount; + Array ethAddress; + bit fromQubicToEthereum; + }; + + struct createOrder_output + { + uint8 status; + uint64 orderId; + }; + + struct addManager_input + { + id address; + }; + + struct addManager_output + { + uint8 status; + }; + + struct removeManager_input + { + id address; + }; + + struct removeManager_output + { + uint8 status; + }; + + struct getTotalReceivedTokens_input + { + uint64 amount; + }; + + struct getTotalReceivedTokens_output + { + uint64 totalTokens; + }; + + struct completeOrder_input + { + uint64 orderId; + }; + + struct completeOrder_output + { + uint8 status; + }; + + struct refundOrder_input + { + uint64 orderId; + }; + + struct refundOrder_output + { + uint8 status; + }; + + struct transferToContract_input + { + uint64 amount; + uint64 orderId; + }; + + struct transferToContract_output + { + uint8 status; + }; + + // Withdraw Fees structures + struct withdrawFees_input + { + uint64 amount; + }; + + struct withdrawFees_output + { + uint8 status; + }; + + // Get Available Fees structures + struct getAvailableFees_input + { + // No parameters + }; + + struct getAvailableFees_output + { + uint64 availableFees; + uint64 totalEarnedFees; + uint64 totalDistributedFees; + }; + + // Order Response Structure + struct OrderResponse + { + id originAccount; // Origin account + Array destinationAccount; // Destination account + uint64 orderId; // Order ID as uint64 + uint64 amount; // Amount as uint64 + Array memo; // Notes or metadata + uint32 sourceChain; // Source chain identifier + id qubicDestination; + uint8 status; // Order status (0=pending, 1=completed, 2=refunded) + }; + + struct getOrder_input + { + uint64 orderId; + }; + + struct getOrder_output + { + uint8 status; + OrderResponse order; // Updated response format + Array message; + }; + + struct getContractInfo_input + { + // No parameters + }; + + struct getContractInfo_output + { + Array managers; + uint64 nextOrderId; + uint64 lockedTokens; + uint64 totalReceivedTokens; + uint64 earnedFees; + uint32 tradeFeeBillionths; + uint32 sourceChain; + // Debug info + Array firstOrders; // First 16 orders + uint64 totalOrdersFound; // How many non-empty orders exist + uint64 emptySlots; + // Multisig info + Array multisigAdmins; // List of multisig admins + uint8 numberOfAdmins; // Number of active admins + uint8 requiredApprovals; // Required approvals threshold + uint64 totalProposals; // Total number of active proposals + }; + + // Logger structures + struct EthBridgeLogger + { + uint32 _contractIndex; // Index of the contract + uint32 _errorCode; // Error code + uint64 _orderId; // Order ID if applicable + uint64 _amount; // Amount involved in the operation + sint8 _terminator; // Marks the end of the logged data + }; + + struct AddressChangeLogger + { + id _newAdminAddress; + uint32 _contractIndex; + uint8 _eventCode; // Event code 'adminchanged' + sint8 _terminator; + }; + + struct TokensLogger + { + uint32 _contractIndex; + uint64 _lockedTokens; // Balance tokens locked + uint64 _totalReceivedTokens; // Balance total receivedTokens + sint8 _terminator; + }; + + struct getTotalLockedTokens_locals + { + EthBridgeLogger log; + }; + + struct getTotalLockedTokens_input + { + // No input parameters + }; + + struct getTotalLockedTokens_output + { + uint64 totalLockedTokens; + }; + + // Enum for error codes + enum EthBridgeError + { + onlyManagersCanCompleteOrders = 1, + invalidAmount = 2, + insufficientTransactionFee = 3, + orderNotFound = 4, + invalidOrderState = 5, + insufficientLockedTokens = 6, + transferFailed = 7, + maxManagersReached = 8, + notAuthorized = 9, + onlyManagersCanRefundOrders = 10, + proposalNotFound = 11, + proposalAlreadyExecuted = 12, + proposalAlreadyApproved = 13, + notOwner = 14, + maxProposalsReached = 15 + }; + + // Enum for proposal types + enum ProposalType + { + PROPOSAL_SET_ADMIN = 1, + PROPOSAL_ADD_MANAGER = 2, + PROPOSAL_REMOVE_MANAGER = 3, + PROPOSAL_WITHDRAW_FEES = 4, + PROPOSAL_CHANGE_THRESHOLD = 5 + }; + + // Admin proposal structure for multisig + struct AdminProposal + { + uint64 proposalId; + uint8 proposalType; // Type from ProposalType enum + id targetAddress; // For setAdmin/addManager/removeManager (new admin address) + id oldAddress; // For setAdmin: which admin to replace + uint64 amount; // For withdrawFees or changeThreshold + Array approvals; // Array of owner IDs who approved + uint8 approvalsCount; // Count of approvals + bit executed; // Whether proposal was executed + bit active; // Whether proposal is active (not cancelled) + }; + +public: + // Contract State + Array orders; + id feeRecipient; // Specific wallet to receive fees + Array managers; // Managers list + uint64 nextOrderId; // Counter for order IDs + uint64 lockedTokens; // Total locked tokens in the contract (balance) + uint64 totalReceivedTokens; // Total tokens received + uint32 sourceChain; // Source chain identifier (e.g., Ethereum=1, Qubic=0) + uint32 _tradeFeeBillionths; // Trade fee in billionths (e.g., 0.5% = 5,000,000) + uint64 _earnedFees; // Accumulated fees from trades + uint64 _distributedFees; // Fees already distributed to shareholders + uint64 _earnedFeesQubic; // Accumulated fees from Qubic trades + uint64 _distributedFeesQubic; // Fees already distributed to Qubic shareholders + + // Multisig state + Array admins; // List of multisig admins + uint8 numberOfAdmins; // Number of active admins + uint8 requiredApprovals; // Threshold: number of approvals needed (2 of 3) + Array proposals; // Pending admin proposals + uint64 nextProposalId; // Counter for proposal IDs + + // Internal methods for admin/manager permissions + typedef id isManager_input; + typedef bit isManager_output; + + struct isManager_locals + { + uint64 i; + }; + + PRIVATE_FUNCTION_WITH_LOCALS(isManager) + { + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + if (state.managers.get(locals.i) == input) + { + output = true; + return; + } + } + output = false; + } + + typedef id isMultisigAdmin_input; + typedef bit isMultisigAdmin_output; + + struct isMultisigAdmin_locals + { + uint64 i; + }; + + PRIVATE_FUNCTION_WITH_LOCALS(isMultisigAdmin) + { + for (locals.i = 0; locals.i < (uint64)state.numberOfAdmins; ++locals.i) + { + if (state.admins.get(locals.i) == input) + { + output = true; + return; + } + } + output = false; + } + +public: + // Create a new order and lock tokens + struct createOrder_locals + { + BridgeOrder newOrder; + EthBridgeLogger log; + uint64 i; + uint64 j; + bit slotFound; + uint64 cleanedSlots; // Counter for cleaned slots + BridgeOrder emptyOrder; // Empty order to clean slots + uint64 requiredFeeEth; + uint64 requiredFeeQubic; + uint64 totalRequiredFee; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(createOrder) + { + // Validate the input + if (input.amount == 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + 0, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 1; // Error + return; + } + + // Calculate fees as percentage of amount (0.5% each, 1% total) + locals.requiredFeeEth = div(input.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.requiredFeeQubic = div(input.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.totalRequiredFee = locals.requiredFeeEth + locals.requiredFeeQubic; + + // Verify that the fee paid is sufficient for both fees + if (qpi.invocationReward() < static_cast(locals.totalRequiredFee)) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientTransactionFee, + 0, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientTransactionFee; + return; + } + + // Create the order + locals.newOrder.orderId = state.nextOrderId++; + locals.newOrder.qubicSender = qpi.invocator(); + + // Set qubicDestination according to the direction + if (!input.fromQubicToEthereum) + { + // EVM TO QUBIC + locals.newOrder.qubicDestination = input.qubicDestination; + + // Verify that there are enough locked tokens for EVM to Qubic orders + if (state.lockedTokens < input.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientLockedTokens, + 0, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientLockedTokens; // Error + return; + } + } + else + { + // QUBIC TO EVM + locals.newOrder.qubicDestination = qpi.invocator(); + } + + for (locals.i = 0; locals.i < 42; ++locals.i) + { + locals.newOrder.ethAddress.set(locals.i, input.ethAddress.get(locals.i)); + } + locals.newOrder.amount = input.amount; + locals.newOrder.orderType = 0; // Default order type + locals.newOrder.status = 0; // Created + locals.newOrder.fromQubicToEthereum = input.fromQubicToEthereum; + locals.newOrder.tokensReceived = false; + locals.newOrder.tokensLocked = false; + + // Store the order + locals.slotFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).status == 255) + { // Empty slot + state.orders.set(locals.i, locals.newOrder); + locals.slotFound = true; + + // Accumulate fees only after order is successfully created + state._earnedFees += locals.requiredFeeEth; + state._earnedFeesQubic += locals.requiredFeeQubic; + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + locals.newOrder.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; // Success + output.orderId = locals.newOrder.orderId; + return; + } + } + + // No available slots - attempt cleanup of completed orders + if (!locals.slotFound) + { + // Clean up completed and refunded orders to free slots + locals.cleanedSlots = 0; + for (locals.j = 0; locals.j < state.orders.capacity(); ++locals.j) + { + if (state.orders.get(locals.j).status == 1 || state.orders.get(locals.j).status == 2) // Completed or Refunded + { + // Create empty order to overwrite + locals.emptyOrder.status = 255; // Mark as empty + locals.emptyOrder.orderId = 0; + locals.emptyOrder.amount = 0; + // Clear other fields as needed + state.orders.set(locals.j, locals.emptyOrder); + locals.cleanedSlots++; + } + } + + // If we cleaned some slots, try to find a slot again + if (locals.cleanedSlots > 0) + { + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).status == 255) + { // Empty slot + state.orders.set(locals.i, locals.newOrder); + locals.slotFound = true; + + // Accumulate fees only after order is successfully created + state._earnedFees += locals.requiredFeeEth; + state._earnedFeesQubic += locals.requiredFeeQubic; + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + locals.newOrder.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; // Success + output.orderId = locals.newOrder.orderId; + return; + } + } + } + + // If still no slots available after cleanup + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 99, // Custom error code for "no available slots" + 0, // No orderId + locals.cleanedSlots, // Number of slots cleaned + 0 }; // Terminator + LOG_INFO(locals.log); + output.status = 3; // Error: no available slots + return; + } + } + + // Retrieve an order + struct getOrder_locals + { + EthBridgeLogger log; + BridgeOrder order; + OrderResponse orderResp; + uint64 i; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getOrder) + { + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + locals.order = state.orders.get(locals.i); + if (locals.order.orderId == input.orderId && locals.order.status != 255) + { + // Populate OrderResponse with BridgeOrder data + locals.orderResp.orderId = locals.order.orderId; + locals.orderResp.originAccount = locals.order.qubicSender; + locals.orderResp.destinationAccount = locals.order.ethAddress; + locals.orderResp.amount = locals.order.amount; + locals.orderResp.sourceChain = state.sourceChain; + locals.orderResp.qubicDestination = locals.order.qubicDestination; + locals.orderResp.status = locals.order.status; + + output.status = 0; // Success + output.order = locals.orderResp; + return; + } + } + + // If order not found + output.status = 1; // Error + } + + // Multisig Proposal Functions + + // Create proposal structures + struct createProposal_input + { + uint8 proposalType; // Type of proposal + id targetAddress; // Target address (new admin/manager address) + id oldAddress; // Old address (for setAdmin: which admin to replace) + uint64 amount; // Amount (for withdrawFees or changeThreshold) + }; + + struct createProposal_output + { + uint8 status; + uint64 proposalId; + }; + + struct createProposal_locals + { + EthBridgeLogger log; + id invocatorAddress; + uint64 i; + uint64 j; + bit slotFound; + AdminProposal newProposal; + AdminProposal emptyProposal; + bit isMultisigAdminResult; + uint64 freeSlots; + uint64 cleanedSlots; + }; + + // Approve proposal structures + struct approveProposal_input + { + uint64 proposalId; + }; + + struct approveProposal_output + { + uint8 status; + bit executed; + }; + + struct approveProposal_locals + { + EthBridgeLogger log; + id invocatorAddress; + AddressChangeLogger adminLog; + AdminProposal proposal; + uint64 i; + bit found; + bit alreadyApproved; + bit isMultisigAdminResult; + uint64 proposalIndex; + uint64 availableFees; + bit adminAdded; + }; + + // Get proposal structures + struct getProposal_input + { + uint64 proposalId; + }; + + struct getProposal_output + { + uint8 status; + AdminProposal proposal; + }; + + struct getProposal_locals + { + uint64 i; + }; + + // Create a new proposal (only multisig admins can create) + PUBLIC_PROCEDURE_WITH_LOCALS(createProposal) + { + // Verify that the invocator is a multisig admin + locals.invocatorAddress = qpi.invocator(); + CALL(isMultisigAdmin, locals.invocatorAddress, locals.isMultisigAdminResult); + if (!locals.isMultisigAdminResult) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notOwner, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notOwner; + return; + } + + // Validate proposal type + if (input.proposalType < PROPOSAL_SET_ADMIN || input.proposalType > PROPOSAL_CHANGE_THRESHOLD) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, // Reusing error code + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Count free slots and find an empty slot for the proposal + locals.slotFound = false; + locals.freeSlots = 0; + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (!state.proposals.get(locals.i).active && state.proposals.get(locals.i).proposalId == 0) + { + locals.freeSlots++; + if (!locals.slotFound) + { + locals.slotFound = true; + // Don't break, continue counting free slots + } + } + } + + // If found slot but less than 5 free slots, cleanup executed proposals + if (locals.slotFound && locals.freeSlots < 5) + { + locals.cleanedSlots = 0; + for (locals.j = 0; locals.j < state.proposals.capacity(); ++locals.j) + { + if (state.proposals.get(locals.j).executed) + { + // Clear executed proposal + locals.emptyProposal.proposalId = 0; + locals.emptyProposal.proposalType = 0; + locals.emptyProposal.approvalsCount = 0; + locals.emptyProposal.executed = false; + locals.emptyProposal.active = false; + state.proposals.set(locals.j, locals.emptyProposal); + locals.cleanedSlots++; + } + } + } + + // If no slot found at all, try cleanup and search again + if (!locals.slotFound) + { + // Attempt cleanup of executed proposals + locals.cleanedSlots = 0; + for (locals.j = 0; locals.j < state.proposals.capacity(); ++locals.j) + { + if (state.proposals.get(locals.j).executed) + { + // Clear executed proposal + locals.emptyProposal.proposalId = 0; + locals.emptyProposal.proposalType = 0; + locals.emptyProposal.approvalsCount = 0; + locals.emptyProposal.executed = false; + locals.emptyProposal.active = false; + state.proposals.set(locals.j, locals.emptyProposal); + locals.cleanedSlots++; + } + } + + // Try to find slot again after cleanup + if (locals.cleanedSlots > 0) + { + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (!state.proposals.get(locals.i).active && state.proposals.get(locals.i).proposalId == 0) + { + locals.slotFound = true; + break; + } + } + } + + // If still no slot available + if (!locals.slotFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::maxProposalsReached, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::maxProposalsReached; + return; + } + } + + // Create the new proposal + locals.newProposal.proposalId = state.nextProposalId++; + locals.newProposal.proposalType = input.proposalType; + locals.newProposal.targetAddress = input.targetAddress; + locals.newProposal.oldAddress = input.oldAddress; + locals.newProposal.amount = input.amount; + locals.newProposal.approvalsCount = 1; // Creator automatically approves + locals.newProposal.executed = false; + locals.newProposal.active = true; + + // Set creator as first approver + locals.newProposal.approvals.set(0, qpi.invocator()); + + // Store the proposal + state.proposals.set(locals.i, locals.newProposal); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + locals.newProposal.proposalId, + input.amount, + 0 }; + LOG_INFO(locals.log); + + output.status = 0; // Success + output.proposalId = locals.newProposal.proposalId; + } + + // Approve a proposal (only multisig admins can approve) + PUBLIC_PROCEDURE_WITH_LOCALS(approveProposal) + { + // Verify that the invocator is a multisig admin + locals.invocatorAddress = qpi.invocator(); + CALL(isMultisigAdmin, locals.invocatorAddress, locals.isMultisigAdminResult); + if (!locals.isMultisigAdminResult) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notOwner, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notOwner; + output.executed = false; + return; + } + + // Find the proposal + locals.found = false; + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + locals.proposal = state.proposals.get(locals.i); + if (locals.proposal.proposalId == input.proposalId && locals.proposal.active) + { + locals.found = true; + locals.proposalIndex = locals.i; + break; + } + } + + if (!locals.found) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalNotFound, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalNotFound; + output.executed = false; + return; + } + + // Check if already executed + if (locals.proposal.executed) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalAlreadyExecuted, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalAlreadyExecuted; + output.executed = false; + return; + } + + // Check if this owner has already approved + locals.alreadyApproved = false; + for (locals.i = 0; locals.i < (uint64)locals.proposal.approvalsCount; ++locals.i) + { + if (locals.proposal.approvals.get(locals.i) == qpi.invocator()) + { + locals.alreadyApproved = true; + break; + } + } + + if (locals.alreadyApproved) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::proposalAlreadyApproved, + input.proposalId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::proposalAlreadyApproved; + output.executed = false; + return; + } + + // Add approval + locals.proposal.approvals.set((uint64)locals.proposal.approvalsCount, qpi.invocator()); + locals.proposal.approvalsCount++; + + // Check if threshold reached and execute + if (locals.proposal.approvalsCount >= state.requiredApprovals) + { + // Execute the proposal based on type + if (locals.proposal.proposalType == PROPOSAL_SET_ADMIN) + { + // Replace existing admin with new admin (max 3 admins: 2 of 3 multisig) + // oldAddress specifies which admin to replace + locals.adminAdded = false; + for (locals.i = 0; locals.i < state.admins.capacity(); ++locals.i) + { + if (state.admins.get(locals.i) == locals.proposal.oldAddress) + { + // Replace the old admin with the new one + state.admins.set(locals.i, locals.proposal.targetAddress); + locals.adminAdded = true; + locals.adminLog = AddressChangeLogger{ + locals.proposal.targetAddress, + CONTRACT_INDEX, + 1, // Admin changed + 0 }; + LOG_INFO(locals.adminLog); + break; + } + } + // numberOfAdmins stays the same (we're replacing, not adding) + } + else if (locals.proposal.proposalType == PROPOSAL_ADD_MANAGER) + { + // Find empty slot in managers + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + if (state.managers.get(locals.i) == NULL_ID) + { + state.managers.set(locals.i, locals.proposal.targetAddress); + locals.adminLog = AddressChangeLogger{ + locals.proposal.targetAddress, + CONTRACT_INDEX, + 2, // Manager added + 0 }; + LOG_INFO(locals.adminLog); + break; + } + } + } + else if (locals.proposal.proposalType == PROPOSAL_REMOVE_MANAGER) + { + // Find and remove manager + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + if (state.managers.get(locals.i) == locals.proposal.targetAddress) + { + state.managers.set(locals.i, NULL_ID); + locals.adminLog = AddressChangeLogger{ + locals.proposal.targetAddress, + CONTRACT_INDEX, + 3, // Manager removed + 0 }; + LOG_INFO(locals.adminLog); + break; + } + } + } + else if (locals.proposal.proposalType == PROPOSAL_WITHDRAW_FEES) + { + locals.availableFees = state._earnedFees - state._distributedFees; + if (locals.proposal.amount <= locals.availableFees && locals.proposal.amount > 0) + { + if (qpi.transfer(state.feeRecipient, locals.proposal.amount) >= 0) + { + state._distributedFees += locals.proposal.amount; + } + } + } + else if (locals.proposal.proposalType == PROPOSAL_CHANGE_THRESHOLD) + { + // Amount field is used to store new threshold + // Hard limit: minimum threshold is 2 to maintain multisig security + if (locals.proposal.amount >= 2 && locals.proposal.amount <= (uint64)state.numberOfAdmins) + { + state.requiredApprovals = (uint8)locals.proposal.amount; + } + } + + locals.proposal.executed = true; + output.executed = true; + } + else + { + output.executed = false; + } + + // Update the proposal + state.proposals.set(locals.proposalIndex, locals.proposal); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + input.proposalId, + locals.proposal.approvalsCount, + 0 }; + LOG_INFO(locals.log); + + output.status = 0; // Success + } + + // Get proposal details + PUBLIC_FUNCTION_WITH_LOCALS(getProposal) + { + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (state.proposals.get(locals.i).proposalId == input.proposalId) + { + output.proposal = state.proposals.get(locals.i); + output.status = 0; // Success + return; + } + } + + output.status = EthBridgeError::proposalNotFound; + } + + // Admin Functions (now deprecated - use multisig proposals) + struct addManager_locals + { + EthBridgeLogger log; + AddressChangeLogger managerLog; + uint64 i; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(addManager) + { + // DEPRECATED: Use createProposal/approveProposal with PROPOSAL_ADD_MANAGER instead + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + struct removeManager_locals + { + EthBridgeLogger log; + AddressChangeLogger managerLog; + uint64 i; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(removeManager) + { + // DEPRECATED: Use createProposal/approveProposal with PROPOSAL_REMOVE_MANAGER instead + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + struct getTotalReceivedTokens_locals + { + EthBridgeLogger log; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getTotalReceivedTokens) + { + output.totalTokens = state.totalReceivedTokens; + } + + struct completeOrder_locals + { + EthBridgeLogger log; + id invocatorAddress; + bit isManagerOperating; + bit orderFound; + BridgeOrder order; + TokensLogger logTokens; + uint64 i; + uint64 netAmount; + }; + + // Complete an order and release tokens + PUBLIC_PROCEDURE_WITH_LOCALS(completeOrder) + { + locals.invocatorAddress = qpi.invocator(); + locals.isManagerOperating = false; + CALL(isManager, locals.invocatorAddress, locals.isManagerOperating); + + // Verify that the invocator is a manager + if (!locals.isManagerOperating) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::onlyManagersCanCompleteOrders, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::onlyManagersCanCompleteOrders; // Error: not a manager + return; + } + + // Check if the order exists + locals.orderFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).orderId == input.orderId) + { + locals.order = state.orders.get(locals.i); + locals.orderFound = true; + break; + } + } + + // Order not found + if (!locals.orderFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::orderNotFound, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::orderNotFound; // Error + return; + } + + // Check order status + if (locals.order.status != 0) + { // Check it is not completed or refunded already + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; // Error + return; + } + + // Use full amount without deducting commission (commission was already charged in createOrder) + locals.netAmount = locals.order.amount; + + // Handle order based on transfer direction + if (locals.order.fromQubicToEthereum) + { + // Verify that tokens were received + if (!locals.order.tokensReceived || !locals.order.tokensLocked) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Tokens are already in lockedTokens from transferToContract + // No need to modify lockedTokens here + } + else + { + // Ensure sufficient tokens are locked for the order + if (state.lockedTokens < locals.order.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientLockedTokens, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientLockedTokens; // Error + return; + } + + // Transfer tokens back to the user + if (qpi.transfer(locals.order.qubicDestination, locals.netAmount) < 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::transferFailed, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::transferFailed; // Error + return; + } + + state.lockedTokens -= locals.order.amount; + locals.logTokens = TokensLogger{ + CONTRACT_INDEX, + state.lockedTokens, + state.totalReceivedTokens, + 0 }; + LOG_INFO(locals.logTokens); + } + + // Mark the order as completed + locals.order.status = 1; // Completed + state.orders.set(locals.i, locals.order); // Use the loop index + + output.status = 0; // Success + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + } + + // Refund an order and unlock tokens + struct refundOrder_locals + { + EthBridgeLogger log; + id invocatorAddress; + bit isManagerOperating; + bit orderFound; + BridgeOrder order; + uint64 i; + uint64 feeEth; + uint64 feeQubic; + uint64 totalRefund; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(refundOrder) + { + locals.invocatorAddress = qpi.invocator(); + locals.isManagerOperating = false; + CALL(isManager, locals.invocatorAddress, locals.isManagerOperating); + + // Check if the order is handled by a manager + if (!locals.isManagerOperating) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::onlyManagersCanRefundOrders, + input.orderId, + 0, // No amount involved + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::onlyManagersCanRefundOrders; // Error + return; + } + + // Retrieve the order + locals.orderFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).orderId == input.orderId) + { + locals.order = state.orders.get(locals.i); + locals.orderFound = true; + break; + } + } + + // Order not found + if (!locals.orderFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::orderNotFound, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::orderNotFound; // Error + return; + } + + // Check order status + if (locals.order.status != 0) + { // Check it is not completed or refunded already + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; // Error + return; + } + + // Handle refund based on transfer direction + if (locals.order.fromQubicToEthereum) + { + // Only refund if tokens were received + if (!locals.order.tokensReceived) + { + // No tokens to return, but refund fees + // Calculate fees to refund + locals.feeEth = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.feeQubic = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.totalRefund = locals.feeEth + locals.feeQubic; + + // Deduct fees from earned fees (return them to user) + state._earnedFees -= locals.feeEth; + state._earnedFeesQubic -= locals.feeQubic; + + // Transfer fees back to user + if (qpi.transfer(locals.order.qubicSender, locals.totalRefund) < 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::transferFailed, + input.orderId, + locals.totalRefund, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::transferFailed; + return; + } + + // Mark order as refunded + locals.order.status = 2; + state.orders.set(locals.i, locals.order); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, + input.orderId, + locals.totalRefund, + 0 }; + LOG_INFO(locals.log); + output.status = 0; + return; + } + + // Tokens were received and are in lockedTokens + if (!locals.order.tokensLocked) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Verify sufficient locked tokens + if (state.lockedTokens < locals.order.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::insufficientLockedTokens, + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::insufficientLockedTokens; + return; + } + + // Calculate fees to refund + locals.feeEth = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + locals.feeQubic = div(locals.order.amount * state._tradeFeeBillionths, 1000000000ULL); + + // Deduct fees from earned fees (return them to user) + state._earnedFees -= locals.feeEth; + state._earnedFeesQubic -= locals.feeQubic; + + // Calculate total refund: amount + fees + locals.totalRefund = locals.order.amount + locals.feeEth + locals.feeQubic; + + // Return tokens + fees to original sender + if (qpi.transfer(locals.order.qubicSender, locals.totalRefund) < 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::transferFailed, + input.orderId, + locals.totalRefund, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::transferFailed; + return; + } + + // Update locked tokens balance + state.lockedTokens -= locals.order.amount; + } + // Note: For EVM to Qubic orders, tokens were already transferred in completeOrder + // No refund needed on Qubic side (fees were paid on Ethereum side) + + // Mark as refunded + locals.order.status = 2; + state.orders.set(locals.i, locals.order); + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + input.orderId, + locals.order.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; // Success + } + + // Transfer tokens to the contract + struct transferToContract_locals + { + EthBridgeLogger log; + TokensLogger logTokens; + BridgeOrder order; + bit orderFound; + uint64 i; + uint64 depositAmount; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(transferToContract) + { + if (input.amount == 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Find the order + locals.orderFound = false; + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).orderId == input.orderId) + { + locals.order = state.orders.get(locals.i); + locals.orderFound = true; + break; + } + } + + if (!locals.orderFound) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::orderNotFound, + input.orderId, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::orderNotFound; + return; + } + + // Verify sender is the original order creator + if (locals.order.qubicSender != qpi.invocator()) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + // Verify order state + if (locals.order.status != 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Verify tokens not already received + if (locals.order.tokensReceived) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidOrderState, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidOrderState; + return; + } + + // Verify amount matches order + if (input.amount != locals.order.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Only for Qubic-to-Ethereum orders need to receive tokens + if (locals.order.fromQubicToEthereum) + { + // Tokens must be provided with the invocation (invocationReward) + locals.depositAmount = qpi.invocationReward(); + if (locals.depositAmount != input.amount) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Tokens go directly to lockedTokens for this order + state.lockedTokens += locals.depositAmount; + state.totalReceivedTokens += locals.depositAmount; + + // Mark tokens as received AND locked + locals.order.tokensReceived = true; + locals.order.tokensLocked = true; + state.orders.set(locals.i, locals.order); + + locals.logTokens = TokensLogger{ + CONTRACT_INDEX, + state.lockedTokens, + state.totalReceivedTokens, + 0 }; + LOG_INFO(locals.logTokens); + } + + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, + input.orderId, + input.amount, + 0 }; + LOG_INFO(locals.log); + output.status = 0; + } + + struct withdrawFees_locals + { + EthBridgeLogger log; + uint64 availableFees; + }; + + PUBLIC_PROCEDURE_WITH_LOCALS(withdrawFees) + { + // DEPRECATED: Use createProposal/approveProposal with PROPOSAL_WITHDRAW_FEES instead + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, + 0, + 0 }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + + PUBLIC_FUNCTION_WITH_LOCALS(getTotalLockedTokens) + { + output.totalLockedTokens = state.lockedTokens; + } + + // Structure for the input of the getOrderByDetails function + struct getOrderByDetails_input + { + Array ethAddress; // Ethereum address + uint64 amount; // Transaction amount + uint8 status; // Order status (0 = created, 1 = completed, 2 = refunded) + }; + + // Structure for the output of the getOrderByDetails function + struct getOrderByDetails_output + { + uint8 status; // Operation status (0 = success, other = error) + uint64 orderId; // ID of the found order + id qubicDestination; // Destination address on Qubic (for EVM to Qubic orders) + }; + + // Function to search for an order by details + struct getOrderByDetails_locals + { + uint64 i; + uint64 j; + bit addressMatch; // Flag to check if addresses match + BridgeOrder order; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getOrderByDetails) + { + // Validate input parameters + if (input.amount == 0) + { + output.status = 2; // Error: invalid amount + output.orderId = 0; + return; + } + + // Iterate through all orders + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + locals.order = state.orders.get(locals.i); + + // Check if the order matches the criteria + if (locals.order.status == 255) // Empty slot + continue; + + // Compare ethAddress arrays element by element + locals.addressMatch = true; + for (locals.j = 0; locals.j < 42; ++locals.j) + { + if (locals.order.ethAddress.get(locals.j) != input.ethAddress.get(locals.j)) + { + locals.addressMatch = false; + break; + } + } + + // Verify exact match + if (locals.addressMatch && + locals.order.amount == input.amount && + locals.order.status == input.status) + { + // Found an exact match + output.status = 0; // Success + output.orderId = locals.order.orderId; + return; + } + } + + // If no matching order was found + output.status = 1; // Not found + output.orderId = 0; + } + + // Add Liquidity structures + struct addLiquidity_input + { + // No input parameters - amount comes from qpi.invocationReward() + }; + + struct addLiquidity_output + { + uint8 status; // Operation status (0 = success, other = error) + uint64 addedAmount; // Amount of tokens added to liquidity + uint64 totalLocked; // Total locked tokens after addition + }; + + struct addLiquidity_locals + { + EthBridgeLogger log; + id invocatorAddress; + bit isManagerOperating; + bit isMultisigAdminResult; + uint64 depositAmount; + }; + + // Add liquidity to the bridge (for managers or multisig admins to provide initial/additional liquidity) + PUBLIC_PROCEDURE_WITH_LOCALS(addLiquidity) + { + locals.invocatorAddress = qpi.invocator(); + locals.isManagerOperating = false; + CALL(isManager, locals.invocatorAddress, locals.isManagerOperating); + + locals.isMultisigAdminResult = false; + CALL(isMultisigAdmin, locals.invocatorAddress, locals.isMultisigAdminResult); + + // Verify that the invocator is a manager or multisig admin + if (!locals.isManagerOperating && !locals.isMultisigAdminResult) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::notAuthorized, + 0, // No order ID involved + 0, // No amount involved + 0 + }; + LOG_INFO(locals.log); + output.status = EthBridgeError::notAuthorized; + return; + } + + // Get the amount of tokens sent with this call + locals.depositAmount = qpi.invocationReward(); + + // Validate that some tokens were sent + if (locals.depositAmount == 0) + { + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + EthBridgeError::invalidAmount, + 0, // No order ID involved + 0, // No amount involved + 0 + }; + LOG_INFO(locals.log); + output.status = EthBridgeError::invalidAmount; + return; + } + + // Add the deposited tokens to the locked tokens pool + state.lockedTokens += locals.depositAmount; + state.totalReceivedTokens += locals.depositAmount; + + // Log the successful liquidity addition + locals.log = EthBridgeLogger{ + CONTRACT_INDEX, + 0, // No error + 0, // No order ID involved + locals.depositAmount, // Amount added + 0 + }; + LOG_INFO(locals.log); + + // Set output values + output.status = 0; // Success + output.addedAmount = locals.depositAmount; + output.totalLocked = state.lockedTokens; + } + + + PUBLIC_FUNCTION(getAvailableFees) + { + output.availableFees = state._earnedFees - state._distributedFees; + output.totalEarnedFees = state._earnedFees; + output.totalDistributedFees = state._distributedFees; + } + + + struct getContractInfo_locals + { + uint64 i; + }; + + PUBLIC_FUNCTION_WITH_LOCALS(getContractInfo) + { + output.managers = state.managers; + output.nextOrderId = state.nextOrderId; + output.lockedTokens = state.lockedTokens; + output.totalReceivedTokens = state.totalReceivedTokens; + output.earnedFees = state._earnedFees; + output.tradeFeeBillionths = state._tradeFeeBillionths; + output.sourceChain = state.sourceChain; + + + output.totalOrdersFound = 0; + output.emptySlots = 0; + + for (locals.i = 0; locals.i < 16 && locals.i < state.orders.capacity(); ++locals.i) + { + output.firstOrders.set(locals.i, state.orders.get(locals.i)); + } + + // Count real orders vs empty ones + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + if (state.orders.get(locals.i).status == 255) + { + output.emptySlots++; + } + else + { + output.totalOrdersFound++; + } + } + + // Multisig info + output.multisigAdmins = state.admins; + output.numberOfAdmins = state.numberOfAdmins; + output.requiredApprovals = state.requiredApprovals; + + // Count active proposals + output.totalProposals = 0; + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + if (state.proposals.get(locals.i).active && state.proposals.get(locals.i).proposalId > 0) + { + output.totalProposals++; + } + } + } + + // Called at the end of every tick to distribute earned fees + struct END_TICK_locals + { + uint64 feesToDistributeInThisTick; + uint64 amountPerComputor; + uint64 vottunFeesToDistribute; + }; + + END_TICK_WITH_LOCALS() + { + locals.feesToDistributeInThisTick = state._earnedFeesQubic - state._distributedFeesQubic; + + if (locals.feesToDistributeInThisTick > 0) + { + // Distribute fees to computors holding shares of this contract. + // NUMBER_OF_COMPUTORS is a Qubic global constant (typically 676). + locals.amountPerComputor = div(locals.feesToDistributeInThisTick, (uint64)NUMBER_OF_COMPUTORS); + + if (locals.amountPerComputor > 0) + { + if (qpi.distributeDividends(locals.amountPerComputor)) + { + state._distributedFeesQubic += locals.amountPerComputor * NUMBER_OF_COMPUTORS; + } + } + } + + // Distribution of Vottun fees to feeRecipient + locals.vottunFeesToDistribute = state._earnedFees - state._distributedFees; + + if (locals.vottunFeesToDistribute > 0 && state.feeRecipient != 0) + { + if (qpi.transfer(state.feeRecipient, locals.vottunFeesToDistribute)) + { + state._distributedFees += locals.vottunFeesToDistribute; + } + } + } + + // Register Functions and Procedures + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_FUNCTION(getOrder, 1); + REGISTER_USER_FUNCTION(isManager, 2); + REGISTER_USER_FUNCTION(getTotalReceivedTokens, 3); + REGISTER_USER_FUNCTION(getTotalLockedTokens, 4); + REGISTER_USER_FUNCTION(getOrderByDetails, 5); + REGISTER_USER_FUNCTION(getContractInfo, 6); + REGISTER_USER_FUNCTION(getAvailableFees, 7); + REGISTER_USER_FUNCTION(getProposal, 8); + + REGISTER_USER_PROCEDURE(createOrder, 1); + REGISTER_USER_PROCEDURE(addManager, 2); + REGISTER_USER_PROCEDURE(removeManager, 3); + REGISTER_USER_PROCEDURE(completeOrder, 4); + REGISTER_USER_PROCEDURE(refundOrder, 5); + REGISTER_USER_PROCEDURE(transferToContract, 6); + REGISTER_USER_PROCEDURE(withdrawFees, 7); + REGISTER_USER_PROCEDURE(addLiquidity, 8); + REGISTER_USER_PROCEDURE(createProposal, 9); + REGISTER_USER_PROCEDURE(approveProposal, 10); + } + + // Initialize the contract with SECURE ADMIN CONFIGURATION + struct INITIALIZE_locals + { + uint64 i; + BridgeOrder emptyOrder; + AdminProposal emptyProposal; + }; + + INITIALIZE_WITH_LOCALS() + { + //Initialize the wallet that receives fees (REPLACE WITH YOUR WALLET) + // state.feeRecipient = ID(_YOUR, _WALLET, _HERE, _PLACEHOLDER, _UNTIL, _YOU, _PUT, _THE, _REAL, _WALLET, _ADDRESS, _FROM, _VOTTUN, _TO, _RECEIVE, _THE, _BRIDGE, _FEES, _BETWEEN, _QUBIC, _AND, _ETHEREUM, _WITH, _HALF, _PERCENT, _COMMISSION, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S, _T, _U, _V); + + // Initialize the orders array. Good practice to zero first. + locals.emptyOrder = {}; // Sets all fields to 0 (including orderId and status). + locals.emptyOrder.status = 255; // Then set your status for empty. + + for (locals.i = 0; locals.i < state.orders.capacity(); ++locals.i) + { + state.orders.set(locals.i, locals.emptyOrder); + } + + // Initialize the managers array with NULL_ID to mark slots as empty + for (locals.i = 0; locals.i < state.managers.capacity(); ++locals.i) + { + state.managers.set(locals.i, NULL_ID); + } + + // Add the initial manager + state.managers.set(0, ID(_X, _A, _B, _E, _F, _A, _B, _I, _H, _W, _R, _W, _B, _A, _I, _J, _Q, _J, _P, _W, _T, _I, _I, _Q, _B, _U, _C, _B, _H, _B, _V, _W, _Y, _Y, _G, _F, _F, _J, _A, _D, _Q, _B, _K, _W, _F, _B, _O, _R, _R, _V, _X, _W, _S, _C, _V, _B)); + + // Initialize the rest of the state variables + state.nextOrderId = 1; // Start from 1 to avoid ID 0 + state.lockedTokens = 0; + state.totalReceivedTokens = 0; + state.sourceChain = 0; // Arbitrary number. No-EVM chain + + // Initialize fee variables + state._tradeFeeBillionths = 5000000; // 0.5% == 5,000,000 / 1,000,000,000 + state._earnedFees = 0; + state._distributedFees = 0; + + state._earnedFeesQubic = 0; + state._distributedFeesQubic = 0; + + // Initialize multisig admins (3 admins, requires 2 approvals) + state.numberOfAdmins = 3; + state.requiredApprovals = 2; // 2 of 3 threshold + + // Initialize admins array (REPLACE WITH ACTUAL ADMIN ADDRESSES) + state.admins.set(0, ID(_X, _A, _B, _E, _F, _A, _B, _I, _H, _W, _R, _W, _B, _A, _I, _J, _Q, _J, _P, _W, _T, _I, _I, _Q, _B, _U, _C, _B, _H, _B, _V, _W, _Y, _Y, _G, _F, _F, _J, _A, _D, _Q, _B, _K, _W, _F, _B, _O, _R, _R, _V, _X, _W, _S, _C, _V, _B)); // Admin 1 + state.admins.set(1, ID(_E, _Q, _M, _B, _B, _V, _Y, _G, _Z, _O, _F, _U, _I, _H, _E, _X, _F, _O, _X, _K, _T, _F, _T, _A, _N, _E, _K, _B, _X, _L, _B, _X, _H, _A, _Y, _D, _F, _F, _M, _R, _E, _E, _M, _R, _Q, _E, _V, _A, _D, _Y, _M, _M, _E, _W, _A, _C)); // Admin 2 (Manager) + state.admins.set(2, ID(_H, _Y, _J, _X, _E, _Z, _S, _E, _C, _W, _S, _K, _O, _D, _J, _A, _L, _R, _C, _K, _S, _L, _K, _V, _Y, _U, _E, _B, _M, _A, _H, _D, _O, _D, _Y, _Z, _U, _J, _I, _I, _Y, _D, _P, _A, _G, _F, _K, _L, _M, _O, _T, _H, _T, _J, _X, _E)); // Admin 3 (User) + + // Initialize remaining admin slots + for (locals.i = 3; locals.i < state.admins.capacity(); ++locals.i) + { + state.admins.set(locals.i, NULL_ID); + } + + // Initialize proposals array properly (like orders array) + state.nextProposalId = 1; + + // Initialize emptyProposal fields explicitly (avoid memset) + locals.emptyProposal.proposalId = 0; + locals.emptyProposal.proposalType = 0; + locals.emptyProposal.targetAddress = NULL_ID; + locals.emptyProposal.amount = 0; + locals.emptyProposal.approvalsCount = 0; + locals.emptyProposal.executed = false; + locals.emptyProposal.active = false; + // Initialize approvals array with NULL_ID + for (locals.i = 0; locals.i < locals.emptyProposal.approvals.capacity(); ++locals.i) + { + locals.emptyProposal.approvals.set(locals.i, NULL_ID); + } + + // Set all proposal slots with the empty proposal + for (locals.i = 0; locals.i < state.proposals.capacity(); ++locals.i) + { + state.proposals.set(locals.i, locals.emptyProposal); + } + } +}; diff --git a/src/contracts/math_lib.h b/src/contracts/math_lib.h index 2eaeafd8a..e54499dc1 100644 --- a/src/contracts/math_lib.h +++ b/src/contracts/math_lib.h @@ -49,4 +49,21 @@ inline static unsigned char divUp(unsigned char a, unsigned char b) return b ? ((a + b - 1) / b) : 0; } +inline constexpr unsigned long long findNextPowerOf2(unsigned long long num) +{ + if (num == 0) + return 1; + + num--; + num |= num >> 1; + num |= num >> 2; + num |= num >> 4; + num |= num >> 8; + num |= num >> 16; + num |= num >> 32; + num++; + + return num; +} + } diff --git a/src/contracts/qpi.h b/src/contracts/qpi.h index 652ba9cfd..d963e1f60 100644 --- a/src/contracts/qpi.h +++ b/src/contracts/qpi.h @@ -52,62 +52,74 @@ namespace QPI typedef uint128_t uint128; typedef m256i id; +#define STATIC_ASSERT(condition, identifier) static_assert(condition, #identifier); + #define NULL_ID id::zero() + constexpr sint64 NULL_INDEX = -1; constexpr sint64 INVALID_AMOUNT = 0x8000000000000000; -#define _A 0 -#define _B 1 -#define _C 2 -#define _D 3 -#define _E 4 -#define _F 5 -#define _G 6 -#define _H 7 -#define _I 8 -#define _J 9 -#define _K 10 -#define _L 11 -#define _M 12 -#define _N 13 -#define _O 14 -#define _P 15 -#define _Q 16 -#define _R 17 -#define _S 18 -#define _T 19 -#define _U 20 -#define _V 21 -#define _W 22 -#define _X 23 -#define _Y 24 -#define _Z 25 -#define ID(_00, _01, _02, _03, _04, _05, _06, _07, _08, _09, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55) _mm256_set_epi64x(((((((((((((((uint64)_55) * 26 + _54) * 26 + _53) * 26 + _52) * 26 + _51) * 26 + _50) * 26 + _49) * 26 + _48) * 26 + _47) * 26 + _46) * 26 + _45) * 26 + _44) * 26 + _43) * 26 + _42, ((((((((((((((uint64)_41) * 26 + _40) * 26 + _39) * 26 + _38) * 26 + _37) * 26 + _36) * 26 + _35) * 26 + _34) * 26 + _33) * 26 + _32) * 26 + _31) * 26 + _30) * 26 + _29) * 26 + _28, ((((((((((((((uint64)_27) * 26 + _26) * 26 + _25) * 26 + _24) * 26 + _23) * 26 + _22) * 26 + _21) * 26 + _20) * 26 + _19) * 26 + _18) * 26 + _17) * 26 + _16) * 26 + _15) * 26 + _14, ((((((((((((((uint64)_13) * 26 + _12) * 26 + _11) * 26 + _10) * 26 + _09) * 26 + _08) * 26 + _07) * 26 + _06) * 26 + _05) * 26 + _04) * 26 + _03) * 26 + _02) * 26 + _01) * 26 + _00) + constexpr long long _A = 0; + constexpr long long _B = 1; + constexpr long long _C = 2; + constexpr long long _D = 3; + constexpr long long _E = 4; + constexpr long long _F = 5; + constexpr long long _G = 6; + constexpr long long _H = 7; + constexpr long long _I = 8; + constexpr long long _J = 9; + constexpr long long _K = 10; + constexpr long long _L = 11; + constexpr long long _M = 12; + constexpr long long _N = 13; + constexpr long long _O = 14; + constexpr long long _P = 15; + constexpr long long _Q = 16; + constexpr long long _R = 17; + constexpr long long _S = 18; + constexpr long long _T = 19; + constexpr long long _U = 20; + constexpr long long _V = 21; + constexpr long long _W = 22; + constexpr long long _X = 23; + constexpr long long _Y = 24; + constexpr long long _Z = 25; + + inline id ID(long long _00, long long _01, long long _02, long long _03, long long _04, long long _05, long long _06, long long _07, long long _08, long long _09, + long long _10, long long _11, long long _12, long long _13, long long _14, long long _15, long long _16, long long _17, long long _18, long long _19, + long long _20, long long _21, long long _22, long long _23, long long _24, long long _25, long long _26, long long _27, long long _28, long long _29, + long long _30, long long _31, long long _32, long long _33, long long _34, long long _35, long long _36, long long _37, long long _38, long long _39, + long long _40, long long _41, long long _42, long long _43, long long _44, long long _45, long long _46, long long _47, long long _48, long long _49, + long long _50, long long _51, long long _52, long long _53, long long _54, long long _55) + { + return _mm256_set_epi64x(((((((((((((((uint64)_55) * 26 + _54) * 26 + _53) * 26 + _52) * 26 + _51) * 26 + _50) * 26 + _49) * 26 + _48) * 26 + _47) * 26 + _46) * 26 + _45) * 26 + _44) * 26 + _43) * 26 + _42, ((((((((((((((uint64)_41) * 26 + _40) * 26 + _39) * 26 + _38) * 26 + _37) * 26 + _36) * 26 + _35) * 26 + _34) * 26 + _33) * 26 + _32) * 26 + _31) * 26 + _30) * 26 + _29) * 26 + _28, ((((((((((((((uint64)_27) * 26 + _26) * 26 + _25) * 26 + _24) * 26 + _23) * 26 + _22) * 26 + _21) * 26 + _20) * 26 + _19) * 26 + _18) * 26 + _17) * 26 + _16) * 26 + _15) * 26 + _14, ((((((((((((((uint64)_13) * 26 + _12) * 26 + _11) * 26 + _10) * 26 + _09) * 26 + _08) * 26 + _07) * 26 + _06) * 26 + _05) * 26 + _04) * 26 + _03) * 26 + _02) * 26 + _01) * 26 + _00); + } #define NUMBER_OF_COMPUTORS 676 #define QUORUM (NUMBER_OF_COMPUTORS * 2 / 3 + 1) -#define JANUARY 1 -#define FEBRUARY 2 -#define MARCH 3 -#define APRIL 4 -#define MAY 5 -#define JUNE 6 -#define JULY 7 -#define AUGUST 8 -#define SEPTEMBER 9 -#define OCTOBER 10 -#define NOVEMBER 11 -#define DECEMBER 12 - -#define WEDNESDAY 0 -#define THURSDAY 1 -#define FRIDAY 2 -#define SATURDAY 3 -#define SUNDAY 4 -#define MONDAY 5 -#define TUESDAY 6 + constexpr int JANUARY = 1; + constexpr int FEBRUARY = 2; + constexpr int MARCH = 3; + constexpr int APRIL = 4; + constexpr int MAY = 5; + constexpr int JUNE = 6; + constexpr int JULY = 7; + constexpr int AUGUST = 8; + constexpr int SEPTEMBER = 9; + constexpr int OCTOBER = 10; + constexpr int NOVEMBER = 11; + constexpr int DECEMBER = 12; + + constexpr int WEDNESDAY = 0; + constexpr int THURSDAY = 1; + constexpr int FRIDAY = 2; + constexpr int SATURDAY = 3; + constexpr int SUNDAY = 4; + constexpr int MONDAY = 5; + constexpr int TUESDAY = 6; constexpr unsigned long long X_MULTIPLIER = 1ULL; @@ -883,17 +895,114 @@ namespace QPI }; ////////// + // safety multiplying a and b and then clamp + + inline static sint64 smul(sint64 a, sint64 b) + { + sint64 hi, lo; + lo = _mul128(a, b, &hi); + if (hi != (lo >> 63)) + { + return ((a > 0) == (b > 0)) ? INT64_MAX : INT64_MIN; + } + return lo; + } + + inline static uint64 smul(uint64 a, uint64 b) + { + uint64 hi, lo; + lo = _umul128(a, b, &hi); + if (hi != 0) + { + return UINT64_MAX; + } + return lo; + } + + inline static sint32 smul(sint32 a, sint32 b) + { + sint64 r = (sint64)(a) * (sint64)(b); + if (r < INT32_MIN) + { + return INT32_MIN; + } + else if (r > INT32_MAX) + { + return INT32_MAX; + } + else + { + return (sint32)r; + } + } + + inline static uint32 smul(uint32 a, uint32 b) + { + uint64 r = (uint64)(a) * (uint64)(b); + if (r > UINT32_MAX) + { + return UINT32_MAX; + } + return (uint32)r; + } + + ////////// + // safety adding a and b and then clamp + + inline static sint64 sadd(sint64 a, sint64 b) + { + sint64 sum = a + b; + if (a < 0 && b < 0 && sum > 0) // negative overflow + return INT64_MIN; + if (a > 0 && b > 0 && sum < 0) // positive overflow + return INT64_MAX; + return sum; + } + + inline static uint64 sadd(uint64 a, uint64 b) + { + if (UINT64_MAX - a < b) + return UINT64_MAX; + return a + b; + } + + inline static sint32 sadd(sint32 a, sint32 b) + { + sint64 sum = (sint64)(a) + (sint64)(b); + if (sum < INT32_MIN) + { + return INT32_MIN; + } + else if (sum > INT32_MAX) + { + return INT32_MAX; + } + else + { + return (sint32)sum; + } + } + + inline static uint32 sadd(uint32 a, uint32 b) + { + uint64 sum = (uint64)(a) + (uint64)(b); + if (sum > UINT32_MAX) + { + return UINT32_MAX; + } + return (uint32)sum; + } // Divide a by b, but return 0 if b is 0 (rounding to lower magnitude in case of integers) template - inline static T div(T a, T b) + inline static constexpr T div(T a, T b) { return b ? (a / b) : T(0); } // Return remainder of dividing a by b, but return 0 if b is 0 (requires modulo % operator) template - inline static T mod(T a, T b) + inline static constexpr T mod(T a, T b) { return b ? (a % b) : 0; } @@ -1768,6 +1877,18 @@ namespace QPI // return current datetime (year, month, day, hour, minute, second, millisec) inline DateAndTime now() const; + // return last spectrum digest on etalonTick + inline m256i getPrevSpectrumDigest() const; + + // return last universe digest on etalonTick + inline m256i getPrevUniverseDigest() const; + + // return last computer digest on etalonTick + inline m256i getPrevComputerDigest() const; + + // run the score function (in qubic mining) and return first 256 bit of output + inline m256i computeMiningFunction(const m256i miningSeed, const m256i publicKey, const m256i nonce) const; + inline bit signatureValidity( const id& entity, const id& digest, diff --git a/src/four_q.h b/src/four_q.h index dcfffd6d3..3e65680ac 100644 --- a/src/four_q.h +++ b/src/four_q.h @@ -644,27 +644,49 @@ static void table_lookup_fixed_base(point_precomp_t P, unsigned int digit, unsig static void multiply(const unsigned long long* a, const unsigned long long* b, unsigned long long* c) { - unsigned long long u, v, uv; + unsigned long long u, v, uv, tmp; + // The intended operation is: _addcarry_u64(0, _umul128(a[0], b[1], &uv), u, &c[1]) + uv. + // However, MSVC (VC2022 17.14 specifically) does not strictly preserve left-to-right evaluation order. + // A temporary variable is introduced to ensure that 'uv' is _umul128 before addition. + // The same behavior are applied for all following code c[0] = _umul128(a[0], b[0], &u); - u = _addcarry_u64(0, _umul128(a[0], b[1], &uv), u, &c[1]) + uv; - u = _addcarry_u64(0, _umul128(a[0], b[2], &uv), u, &c[2]) + uv; - c[4] = _addcarry_u64(0, _umul128(a[0], b[3], &uv), u, &c[3]) + uv; - - u = _addcarry_u64(0, c[1], _umul128(a[1], b[0], &uv), &c[1]) + uv; - u = _addcarry_u64(0, _umul128(a[1], b[1], &uv), u, &v) + uv; - u = _addcarry_u64(_addcarry_u64(0, c[2], v, &c[2]), _umul128(a[1], b[2], &uv), u, &v) + uv; - c[5] = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), _umul128(a[1], b[3], &uv), u, &v) + uv + _addcarry_u64(0, c[4], v, &c[4]); - - u = _addcarry_u64(0, c[2], _umul128(a[2], b[0], &uv), &c[2]) + uv; - u = _addcarry_u64(0, _umul128(a[2], b[1], &uv), u, &v) + uv; - u = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), _umul128(a[2], b[2], &uv), u, &v) + uv; - c[6] = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), _umul128(a[2], b[3], &uv), u, &v) + uv + _addcarry_u64(0, c[5], v, &c[5]); - - u = _addcarry_u64(0, c[3], _umul128(a[3], b[0], &uv), &c[3]) + uv; - u = _addcarry_u64(0, _umul128(a[3], b[1], &uv), u, &v) + uv; - u = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), _umul128(a[3], b[2], &uv), u, &v) + uv; - c[7] = _addcarry_u64(_addcarry_u64(0, c[5], v, &c[5]), _umul128(a[3], b[3], &uv), u, &v) + uv + _addcarry_u64(0, c[6], v, &c[6]); + tmp = _umul128(a[0], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &c[1]) + uv; + tmp = _umul128(a[0], b[2], &uv); + u = _addcarry_u64(0, tmp, u, &c[2]) + uv; + tmp = _umul128(a[0], b[3], &uv); + c[4] = _addcarry_u64(0, tmp, u, &c[3]) + uv; + + tmp = _umul128(a[1], b[0], &uv); + u = _addcarry_u64(0, c[1], tmp, &c[1]) + uv; + tmp = _umul128(a[1], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; + tmp = _umul128(a[1], b[2], &uv); + u = _addcarry_u64(_addcarry_u64(0, c[2], v, &c[2]), tmp, u, &v) + uv; + tmp = _umul128(a[1], b[3], &uv); + tmp = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), tmp, u, &v); + c[5] = tmp + uv + _addcarry_u64(0, c[4], v, &c[4]); + + tmp = _umul128(a[2], b[0], &uv); + u = _addcarry_u64(0, c[2], tmp, &c[2]) + uv; + tmp = _umul128(a[2], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; + tmp = _umul128(a[2], b[2], &uv); + u = _addcarry_u64(_addcarry_u64(0, c[3], v, &c[3]), tmp, u, &v) + uv; + tmp = _umul128(a[2], b[3], &uv); + tmp = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), tmp, u, &v); + c[6] = tmp + uv + _addcarry_u64(0, c[5], v, &c[5]); + + tmp = _umul128(a[3], b[0], &uv); + u = _addcarry_u64(0, c[3], tmp, &c[3]) + uv; + tmp = _umul128(a[3], b[1], &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; + tmp = _umul128(a[3], b[2], &uv); + u = _addcarry_u64(_addcarry_u64(0, c[4], v, &c[4]), tmp, u, &v) + uv; + tmp = _umul128(a[3], b[3], &uv); + tmp = _addcarry_u64(_addcarry_u64(0, c[5], v, &c[5]), tmp, u, &v); + c[7] = tmp + uv + _addcarry_u64(0, c[6], v, &c[6]); } static void Montgomery_multiply_mod_order(const unsigned long long* ma, const unsigned long long* mb, unsigned long long* mc) @@ -683,16 +705,21 @@ static void Montgomery_multiply_mod_order(const unsigned long long* ma, const un multiply(ma, mb, P); // P = ma * mb } - unsigned long long u, v, uv; + unsigned long long u, v, uv, tmp; Q[0] = _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_0, &u); - u = _addcarry_u64(0, _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_1, &uv), u, &Q[1]) + uv; - u = _addcarry_u64(0, _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_2, &uv), u, &Q[2]) + uv; + tmp = _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_1, &uv); + u = _addcarry_u64(0, tmp, u, &Q[1]) + uv; + tmp = _umul128(P[0], MONTGOMERY_SMALL_R_PRIME_2, &uv); + u = _addcarry_u64(0, tmp, u, &Q[2]) + uv; _addcarry_u64(0, P[0] * MONTGOMERY_SMALL_R_PRIME_3, u, &Q[3]); - u = _addcarry_u64(0, Q[1], _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_0, &uv), &Q[1]) + uv; - u = _addcarry_u64(0, _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_1, &uv), u, &v) + uv; + tmp = _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_0, &uv); + u = _addcarry_u64(0, Q[1], tmp, &Q[1]) + uv; + tmp = _umul128(P[1], MONTGOMERY_SMALL_R_PRIME_1, &uv); + u = _addcarry_u64(0, tmp, u, &v) + uv; _addcarry_u64(_addcarry_u64(0, Q[2], v, &Q[2]), P[1] * MONTGOMERY_SMALL_R_PRIME_2, u, &v); _addcarry_u64(0, Q[3], v, &Q[3]); - u = _addcarry_u64(0, Q[2], _umul128(P[2], MONTGOMERY_SMALL_R_PRIME_0, &uv), &Q[2]) + uv; + tmp = _umul128(P[2], MONTGOMERY_SMALL_R_PRIME_0, &uv); + u = _addcarry_u64(0, Q[2], tmp, &Q[2]) + uv; _addcarry_u64(0, P[2] * MONTGOMERY_SMALL_R_PRIME_1, u, &v); _addcarry_u64(0, Q[3], v, &Q[3]); _addcarry_u64(0, Q[3], P[3] * MONTGOMERY_SMALL_R_PRIME_0, &Q[3]); diff --git a/src/logging/logging.h b/src/logging/logging.h index c200a0ad1..f7a39c4c4 100644 --- a/src/logging/logging.h +++ b/src/logging/logging.h @@ -58,6 +58,9 @@ struct Peer; #define ASSET_POSSESSION_MANAGING_CONTRACT_CHANGE 12 #define CUSTOM_MESSAGE 255 +#define CUSTOM_MESSAGE_OP_START_DISTRIBUTE_DIVIDENDS 6217575821008262227ULL // STA_DDIV +#define CUSTOM_MESSAGE_OP_END_DISTRIBUTE_DIVIDENDS 6217575821008457285ULL //END_DDIV + /* * STRUCTS FOR LOGGING */ @@ -73,6 +76,7 @@ struct AssetIssuance { m256i issuerPublicKey; long long numberOfShares; + long long managingContractIndex; char name[7]; char numberOfDecimalPlaces; char unitOfMeasurement[7]; @@ -576,12 +580,14 @@ class qLogger #endif } + // updateTick is called right after _tick is processed static void updateTick(unsigned int _tick) { #if ENABLED_LOGGING ASSERT((_tick == lastUpdatedTick + 1) || (_tick == tickBegin)); + ASSERT(_tick >= tickBegin); #if LOG_STATE_DIGEST - unsigned long long index = lastUpdatedTick - tickBegin; + unsigned long long index = _tick - tickBegin; XKCP::KangarooTwelve_Final(&k12, digests[index].m256i_u8, (const unsigned char*)"", 0); XKCP::KangarooTwelve_Initialize(&k12, 128, 32); // init new k12 XKCP::KangarooTwelve_Update(&k12, digests[index].m256i_u8, 32); // feed the prev hash back to this diff --git a/src/mining/mining.h b/src/mining/mining.h index bef2fe872..293af781f 100644 --- a/src/mining/mining.h +++ b/src/mining/mining.h @@ -52,48 +52,63 @@ struct CustomMiningSolutionTransaction : public Transaction } }; -struct CustomMiningTask -{ - unsigned long long taskIndex; // ever increasing number (unix timestamp in ms) - unsigned short firstComputorIndex; // the first computor index assigned by this task - unsigned short lastComputorIndex; // the last computor index assigned by this task - unsigned int padding; - - unsigned char blob[408]; // Job data from pool - unsigned long long size; // length of the blob - unsigned long long target; // Pool difficulty - unsigned long long height; // Block height - unsigned char seed[32]; // Seed hash for XMR -}; - struct CustomMiningTaskV2 { unsigned long long taskIndex; unsigned char m_template[896]; - unsigned long long m_extraNonceOffset; + unsigned long long m_extraNonceOffset;// offset to place extra nonce unsigned long long m_size; unsigned long long m_target; unsigned long long m_height; unsigned char m_seed[32]; }; -struct CustomMiningSolution -{ - unsigned long long taskIndex; // should match the index from task - unsigned short firstComputorIndex; // should match the index from task - unsigned short lastComputorIndex; // should match the index from task - unsigned int nonce; // xmrig::JobResult.nonce - m256i result; // xmrig::JobResult.result, 32 bytes -}; - struct CustomMiningSolutionV2 { - unsigned long long taskIndex; - unsigned long long nonce; // (extraNonce<<32) | nonce - unsigned long long reserve0; - unsigned long long reserve1; - unsigned long long reserve2; + unsigned long long taskIndex; // should match the index from task + unsigned long long nonce; // (extraNonce<<32) | nonce + unsigned long long encryptionLevel; // 0 = no encryption. `0` is allowed for legacy purposes + unsigned long long computorRandom; // random number which fullfils the condition computorRandom % 676 == ComputorIndex.`0` is allowed for legacy purposes + unsigned long long reserve2; // reserved m256i result; }; +static unsigned short customMiningGetComputorID(const CustomMiningSolutionV2* pSolution) +{ + // Check the computor idx of this solution. + unsigned short computorID = 0; + if (pSolution->encryptionLevel == 0) + { + computorID = (unsigned short)((pSolution->nonce >> 32ULL) % (unsigned long long)NUMBER_OF_COMPUTORS); + } + else + { + computorID = (unsigned short)(pSolution->computorRandom % (unsigned long long)NUMBER_OF_COMPUTORS); + } + return computorID; +} + +static CustomMiningSolutionV2 customMiningVerificationRequestToSolution(RequestedCustomMiningSolutionVerification* pRequest) +{ + CustomMiningSolutionV2 solution; + solution.taskIndex = pRequest->taskIndex; + solution.nonce = pRequest->nonce; + solution.encryptionLevel = pRequest->encryptionLevel; + solution.computorRandom = pRequest->computorRandom; + solution.reserve2 = pRequest->reserve2; + return solution; +} + +static RespondCustomMiningSolutionVerification customMiningVerificationRequestToRespond(RequestedCustomMiningSolutionVerification* pRequest) +{ + RespondCustomMiningSolutionVerification respond; + respond.taskIndex = pRequest->taskIndex; + respond.nonce = pRequest->nonce; + respond.encryptionLevel = pRequest->encryptionLevel; + respond.computorRandom = pRequest->computorRandom; + respond.reserve2 = pRequest->reserve2; + + return respond; +} + #define CUSTOM_MINING_SHARES_COUNT_SIZE_IN_BYTES 848 #define CUSTOM_MINING_SOLUTION_NUM_BIT_PER_COMP 10 #define TICK_VOTE_COUNTER_PUBLICATION_OFFSET 2 // Must be 2 @@ -115,7 +130,7 @@ struct BroadcastCustomMiningTransaction bool isBroadcasted; }; -BroadcastCustomMiningTransaction gCustomMiningBroadcastTxBuffer[NUMBER_OF_COMPUTORS]; +static BroadcastCustomMiningTransaction gCustomMiningBroadcastTxBuffer[NUMBER_OF_COMPUTORS]; class CustomMiningSharesCounter { @@ -368,9 +383,8 @@ class CustomMininingCache return retVal; } - // Try to fetch data from cacheIndex, also checking a few following entries in case of collisions (may update cacheIndex), - // increments counter of hits, misses, or collisions - int tryFetchingAndUpdate(T& rData, int updateCondition) + // Try to fetch data from cacheIndex, also checking a few following entries in case of collisions, + bool tryFetchingAndUpdateHitData(T& rData) { int retVal; unsigned int tryFetchIdx = rData.getHashIndex() % capacity(); @@ -380,16 +394,12 @@ class CustomMininingCache const T& cacheData = cache[tryFetchIdx]; if (cacheData.isEmpty()) { - // miss: data not available in cache yet (entry is empty) - misses++; retVal = CUSTOM_MINING_CACHE_MISS; break; } if (cacheData.isMatched(rData)) { - // hit: data available in cache -> return score - hits++; retVal = CUSTOM_MINING_CACHE_HIT; break; } @@ -398,19 +408,15 @@ class CustomMininingCache retVal = CUSTOM_MINING_CACHE_COLLISION; tryFetchIdx = (tryFetchIdx + 1) % capacity(); } - if (retVal == updateCondition) + + // This allow update data with additional field beside the key + if (retVal == CUSTOM_MINING_CACHE_HIT) { cache[tryFetchIdx] = rData; } RELEASE(lock); - if (retVal == CUSTOM_MINING_CACHE_COLLISION) - { - ACQUIRE(lock); - collisions++; - RELEASE(lock); - } - return retVal; + return (retVal == CUSTOM_MINING_CACHE_HIT); } @@ -522,103 +528,6 @@ class CustomMininingCache unsigned int invalid; }; -class CustomMiningSolutionCacheEntry -{ -public: - void reset() - { - _solution.taskIndex = 0; - _isHashed = false; - _isVerification = false; - _isValid = true; - } - - void set(const CustomMiningSolution* pCustomMiningSolution) - { - reset(); - _solution = *pCustomMiningSolution; - } - - void set(const unsigned long long taskIndex, unsigned int nonce, unsigned short firstComputorIndex, unsigned short lastComputorIndex) - { - reset(); - _solution.taskIndex = taskIndex; - _solution.nonce = nonce; - _solution.firstComputorIndex = firstComputorIndex; - _solution.lastComputorIndex = lastComputorIndex; - } - - void get(CustomMiningSolution& rCustomMiningSolution) - { - rCustomMiningSolution = _solution; - } - - bool isEmpty() const - { - return (_solution.taskIndex == 0); - } - bool isMatched(const CustomMiningSolutionCacheEntry& rOther) const - { - return (_solution.taskIndex == rOther.getTaskIndex()) && (_solution.nonce == rOther.getNonce()); - } - unsigned long long getHashIndex() - { - // TODO: reserve each computor ID a limited slot. - // This will avoid them spawning invalid solutions without verification - if (!_isHashed) - { - copyMem(_buffer, &_solution.taskIndex, sizeof(_solution.taskIndex)); - copyMem(_buffer + sizeof(_solution.taskIndex), &_solution.nonce, sizeof(_solution.nonce)); - KangarooTwelve(_buffer, sizeof(_solution.taskIndex) + sizeof(_solution.nonce), &_digest, sizeof(_digest)); - _isHashed = true; - } - return _digest; - } - - unsigned long long getTaskIndex() const - { - return _solution.taskIndex; - } - - unsigned long long getNonce() const - { - return _solution.nonce; - } - - bool isValid() - { - return _isValid; - } - - bool isVerified() - { - return _isVerification; - } - - void setValid(bool val) - { - _isValid = val; - } - - void setVerified(bool val) - { - _isVerification = true; - } - - void setEmpty() - { - _solution.taskIndex = 0; - } - -private: - CustomMiningSolution _solution; - unsigned long long _digest; - unsigned char _buffer[sizeof(_solution.taskIndex) + sizeof(_solution.nonce)]; - bool _isHashed; - bool _isVerification; - bool _isValid; -}; - class CustomMiningSolutionV2CacheEntry { public: @@ -709,11 +618,10 @@ class CustomMiningSolutionV2CacheEntry // In charge of storing custom mining -constexpr unsigned int NUMBER_OF_TASK_PARTITIONS = 4; -constexpr unsigned long long MAX_NUMBER_OF_CUSTOM_MINING_SOLUTIONS = (200ULL << 20) / NUMBER_OF_TASK_PARTITIONS / sizeof(CustomMiningSolutionCacheEntry); +constexpr unsigned long long MAX_NUMBER_OF_CUSTOM_MINING_SOLUTIONS = (200ULL << 20) / sizeof(CustomMiningSolutionV2CacheEntry); constexpr unsigned long long CUSTOM_MINING_INVALID_INDEX = 0xFFFFFFFFFFFFFFFFULL; constexpr unsigned long long CUSTOM_MINING_TASK_STORAGE_COUNT = 60 * 60 * 24 * 8 / 2 / 10; // All epoch tasks in 7 (+1) days, 10s per task, idle phases only -constexpr unsigned long long CUSTOM_MINING_TASK_STORAGE_SIZE = CUSTOM_MINING_TASK_STORAGE_COUNT * sizeof(CustomMiningTask); // ~16.6MB +constexpr unsigned long long CUSTOM_MINING_TASK_STORAGE_SIZE = CUSTOM_MINING_TASK_STORAGE_COUNT * sizeof(CustomMiningTaskV2); // ~16.6MB constexpr unsigned long long CUSTOM_MINING_SOLUTION_STORAGE_COUNT = MAX_NUMBER_OF_CUSTOM_MINING_SOLUTIONS; constexpr unsigned long long CUSTOM_MINING_STORAGE_PROCESSOR_MAX_STORAGE = 10 * 1024 * 1024; // 10MB constexpr unsigned long long CUSTOM_MINING_RESPOND_MESSAGE_MAX_SIZE = 1 * 1024 * 1024; // 1MB @@ -756,18 +664,12 @@ struct CustomMiningStats long long maxCollisionShareCount; // Max number of shares that are not save in cached because of collision // Stats of current custom mining phase - Counter phase[NUMBER_OF_TASK_PARTITIONS]; Counter phaseV2; // Asume at begining of epoch. void epochReset() { lastPhases.reset(); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - phase[i].reset(); - } - phaseV2.reset(); ATOMIC_STORE64(maxOverflowShareCount, 0); ATOMIC_STORE64(maxCollisionShareCount, 0); @@ -782,14 +684,11 @@ struct CustomMiningStats long long valid = 0; long long invalid = 0; long long duplicated = 0; - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - tasks += ATOMIC_LOAD64(phase[i].tasks); - shares += ATOMIC_LOAD64(phase[i].shares); - valid += ATOMIC_LOAD64(phase[i].valid); - invalid += ATOMIC_LOAD64(phase[i].invalid); - duplicated += ATOMIC_LOAD64(phase[i].duplicated); - } + tasks += ATOMIC_LOAD64(phaseV2.tasks); + shares += ATOMIC_LOAD64(phaseV2.shares); + valid += ATOMIC_LOAD64(phaseV2.valid); + invalid += ATOMIC_LOAD64(phaseV2.invalid); + duplicated += ATOMIC_LOAD64(phaseV2.duplicated); // Accumulate the phase into last phases ATOMIC_ADD64(lastPhases.tasks, tasks); @@ -798,11 +697,6 @@ struct CustomMiningStats ATOMIC_ADD64(lastPhases.invalid, invalid); ATOMIC_ADD64(lastPhases.duplicated, duplicated); - // Reset phase number - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - phase[i].reset(); - } phaseV2.reset(); } @@ -813,14 +707,11 @@ struct CustomMiningStats long long customMiningValidShares = 0; long long customMiningInvalidShares = 0; long long customMiningDuplicated = 0; - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - customMiningTasks += ATOMIC_LOAD64(phase[i].tasks); - customMiningShares += ATOMIC_LOAD64(phase[i].shares); - customMiningValidShares += ATOMIC_LOAD64(phase[i].valid); - customMiningInvalidShares += ATOMIC_LOAD64(phase[i].invalid); - customMiningDuplicated += ATOMIC_LOAD64(phase[i].duplicated); - } + customMiningTasks = ATOMIC_LOAD64(phaseV2.tasks); + customMiningShares = ATOMIC_LOAD64(phaseV2.shares); + customMiningValidShares = ATOMIC_LOAD64(phaseV2.valid); + customMiningInvalidShares = ATOMIC_LOAD64(phaseV2.invalid); + customMiningDuplicated = ATOMIC_LOAD64(phaseV2.duplicated); appendText(message, L"Phase:"); appendText(message, L" Tasks: "); @@ -834,25 +725,13 @@ struct CustomMiningStats appendText(message, L" | Duplicated: "); appendNumber(message, customMiningDuplicated, true); - appendText(message, L"Phase V2:"); - appendText(message, L" Tasks: "); - appendNumber(message, phaseV2.tasks, true); - appendText(message, L" | Shares: "); - appendNumber(message, phaseV2.shares, true); - appendText(message, L" | Valid: "); - appendNumber(message, phaseV2.valid, true); - appendText(message, L" | InValid: "); - appendNumber(message, phaseV2.invalid, true); - appendText(message, L" | Duplicated: "); - appendNumber(message, phaseV2.duplicated, true); - long long customMiningEpochTasks = customMiningTasks + ATOMIC_LOAD64(lastPhases.tasks); long long customMiningEpochShares = customMiningShares + ATOMIC_LOAD64(lastPhases.shares); long long customMiningEpochInvalidShares = customMiningInvalidShares + ATOMIC_LOAD64(lastPhases.invalid); long long customMiningEpochValidShares = customMiningValidShares + ATOMIC_LOAD64(lastPhases.valid); long long customMiningEpochDuplicated = customMiningDuplicated + ATOMIC_LOAD64(lastPhases.duplicated); - appendText(message, L". Epoch (not count v2):"); + appendText(message, L". Epoch:"); appendText(message, L" Tasks: "); appendNumber(message, customMiningEpochTasks, false); appendText(message, L" | Shares: "); @@ -1231,7 +1110,6 @@ struct CustomMiningSolutionStorageEntry unsigned long long cacheEntryIndex; }; -typedef CustomMiningSortedStorage CustomMiningTaskStorage; typedef CustomMiningSortedStorage CustomMiningSolutionStorage; typedef CustomMiningSortedStorage CustomMiningTaskV2Storage; @@ -1241,11 +1119,6 @@ class CustomMiningStorage public: void init() { - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _taskStorage[i].init(); - _solutionStorage[i].init(); - } _taskV2Storage.init(); _solutionV2Storage.init(); // Buffer allocation for each processors. It is limited to 10MB each @@ -1260,11 +1133,6 @@ class CustomMiningStorage } void deinit() { - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _taskStorage[i].deinit(); - _solutionStorage[i].deinit(); - } _taskV2Storage.deinit(); _solutionV2Storage.deinit(); for (unsigned int i = 0; i < MAX_NUMBER_OF_PROCESSORS; i++) @@ -1276,18 +1144,10 @@ class CustomMiningStorage void reset() { ACQUIRE(gCustomMiningTaskStorageLock); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _taskStorage[i].reset(); - } _taskV2Storage.reset(); RELEASE(gCustomMiningTaskStorageLock); ACQUIRE(gCustomMiningSolutionStorageLock); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; ++i) - { - _solutionStorage[i].reset(); - } _solutionV2Storage.reset(); RELEASE(gCustomMiningSolutionStorageLock); @@ -1302,32 +1162,30 @@ class CustomMiningStorage unsigned char* packedData = _dataBuffer[processorNumber]; CustomMiningRespondDataHeader* packedHeader = (CustomMiningRespondDataHeader*)packedData; packedHeader->respondType = RespondCustomMiningData::taskType; - packedHeader->itemSize = sizeof(CustomMiningTask); + packedHeader->itemSize = sizeof(CustomMiningTaskV2); packedHeader->fromTimeStamp = fromTimeStamp; packedHeader->toTimeStamp = toTimeStamp; packedHeader->itemCount = 0; unsigned char* traverseData = packedData + sizeof(CustomMiningRespondDataHeader); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) + unsigned char* data = _taskV2Storage.getSerializedData(fromTimeStamp, toTimeStamp, processorNumber); + if (data != NULL) { - unsigned char* data = _taskStorage[i].getSerializedData(fromTimeStamp, toTimeStamp, processorNumber); - if (data != NULL) + CustomMiningRespondDataHeader* customMiningInternalHeader = (CustomMiningRespondDataHeader*)data; + ASSERT(packedHeader->itemSize == customMiningInternalHeader->itemSize); + unsigned long long dataSize = customMiningInternalHeader->itemCount * sizeof(CustomMiningTaskV2); + if (customMiningInternalHeader->itemCount > 0 && remainedSize >= dataSize) { - CustomMiningRespondDataHeader* customMiningInternalHeader = (CustomMiningRespondDataHeader*)data; - ASSERT(packedHeader->itemSize == customMiningInternalHeader->itemSize); - unsigned long long dataSize = customMiningInternalHeader->itemCount * sizeof(CustomMiningTask); - if (customMiningInternalHeader->itemCount > 0 && remainedSize >= dataSize) - { - packedHeader->itemCount += customMiningInternalHeader->itemCount; - // Copy data - copyMem(traverseData, data + sizeof(CustomMiningRespondDataHeader), dataSize); + packedHeader->itemCount += customMiningInternalHeader->itemCount; + // Copy data + copyMem(traverseData, data + sizeof(CustomMiningRespondDataHeader), dataSize); - // Update pointer and size - traverseData += dataSize; - remainedSize -= dataSize; - } + // Update pointer and size + traverseData += dataSize; + remainedSize -= dataSize; } } + return packedData; } @@ -1354,115 +1212,30 @@ class CustomMiningStorage } unsigned long long _last3TaskV2Indexes[3]; // [0] is max, [2] is min - CustomMiningTaskStorage _taskStorage[NUMBER_OF_TASK_PARTITIONS]; CustomMiningTaskV2Storage _taskV2Storage; - CustomMiningSolutionStorage _solutionStorage[NUMBER_OF_TASK_PARTITIONS]; CustomMiningSolutionStorage _solutionV2Storage; - - // Buffer can accessed from multiple threads unsigned char* _dataBuffer[MAX_NUMBER_OF_PROCESSORS]; }; -// Describe how a task is divided into groups -// The task is for computorID in range [firstComputorIdx, lastComputorIdx] -// domainSize determine the range of nonce that computor should work on -// Currenly, it is designed as -// domainSize = (2 ^ 32)/ (lastComputorIndex - firstComputorIndex + 1); -// myNonceOffset = (myComputorIndex - firstComputorIndex) * domainSize; -// myNonce = myNonceOffset + x; x = [0, domainSize - 1] -// For computorID calculation, -// computorID = myNonce / domainSize + firstComputorIndex -struct CustomMiningTaskPartition -{ - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int domainSize; -}; - -#define SOLUTION_CACHE_DYNAMIC_MEM 0 - -static CustomMiningTaskPartition gTaskPartition[NUMBER_OF_TASK_PARTITIONS]; - -#if SOLUTION_CACHE_DYNAMIC_MEM -static CustomMininingCache* gSystemCustomMiningSolutionCache = NULL; -#else -static CustomMininingCache gSystemCustomMiningSolutionCache[NUMBER_OF_TASK_PARTITIONS]; -#endif - static CustomMininingCache gSystemCustomMiningSolutionV2Cache; static CustomMiningStorage gCustomMiningStorage; static CustomMiningStats gCustomMiningStats; -// Get the part ID -int customMiningGetPartitionID(unsigned short firstComputorIndex, unsigned short lastComputorIndex) -{ - int partitionID = -1; - for (int k = 0; k < NUMBER_OF_TASK_PARTITIONS; k++) - { - if (firstComputorIndex == gTaskPartition[k].firstComputorIdx - && lastComputorIndex == gTaskPartition[k].lastComputorIdx) - { - partitionID = k; - break; - } - } - return partitionID; -} - - -// Generate computor task partition -int customMiningInitTaskPartitions() -{ - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - // Currently the task is partitioned evenly - gTaskPartition[i].firstComputorIdx = i * NUMBER_OF_COMPUTORS / NUMBER_OF_TASK_PARTITIONS; - gTaskPartition[i].lastComputorIdx = gTaskPartition[i].firstComputorIdx + NUMBER_OF_COMPUTORS / NUMBER_OF_TASK_PARTITIONS - 1; - ASSERT(gTaskPartition[i].lastComputorIdx > gTaskPartition[i].firstComputorIdx + 1); - gTaskPartition[i].domainSize = (unsigned int)((1ULL << 32) / ((unsigned long long)gTaskPartition[i].lastComputorIdx - gTaskPartition[i].firstComputorIdx + 1)); - } - return 0; -} - -// Get computor ids -int customMiningGetComputorID(unsigned int nonce, int partId) -{ - return nonce / gTaskPartition[partId].domainSize + gTaskPartition[partId].firstComputorIdx; -} -int customMiningInitialize() +static int customMiningInitialize() { gCustomMiningStorage.init(); -#if SOLUTION_CACHE_DYNAMIC_MEM - allocPoolWithErrorLog(L"gSystemCustomMiningSolutionCache", - NUMBER_OF_TASK_PARTITIONS * sizeof(CustomMininingCache), - (void**)&gSystemCustomMiningSolutionCache, - __LINE__); -#endif - setMem((unsigned char*)gSystemCustomMiningSolutionCache, NUMBER_OF_TASK_PARTITIONS * sizeof(CustomMininingCache), 0); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - gSystemCustomMiningSolutionCache[i].init(); - } gSystemCustomMiningSolutionV2Cache.init(); - customMiningInitTaskPartitions(); return 0; } -int customMiningDeinitialize() +static int customMiningDeinitialize() { -#if SOLUTION_CACHE_DYNAMIC_MEM - if (gSystemCustomMiningSolutionCache) - { - freePool(gSystemCustomMiningSolutionCache); - gSystemCustomMiningSolutionCache = NULL; - } -#endif gCustomMiningStorage.deinit(); return 0; } @@ -1470,37 +1243,24 @@ int customMiningDeinitialize() #ifdef NO_UEFI #else // Save score cache to SCORE_CACHE_FILE_NAME -void saveCustomMiningCache(int epoch, CHAR16* directory = NULL) +static void saveCustomMiningCache(int epoch, CHAR16* directory = NULL) { logToConsole(L"Saving custom mining cache file..."); CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 4] = epoch / 100 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 3] = (epoch % 100) / 10 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 2] = epoch % 10 + L'0'; - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 8] = i / 100 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 7] = (i % 100) / 10 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 6] = i % 10 + L'0'; - gSystemCustomMiningSolutionCache[i].save(CUSTOM_MINING_CACHE_FILE_NAME, directory); - } + gSystemCustomMiningSolutionV2Cache.save(CUSTOM_MINING_CACHE_FILE_NAME, directory); } // Update score cache filename with epoch and try to load file -bool loadCustomMiningCache(int epoch) +static bool loadCustomMiningCache(int epoch) { logToConsole(L"Loading custom mining cache..."); bool success = true; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 4] = epoch / 100 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 3] = (epoch % 100) / 10 + L'0'; CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 2] = epoch % 10 + L'0'; - // TODO: Support later - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 8] = i / 100 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 7] = (i % 100) / 10 + L'0'; - CUSTOM_MINING_CACHE_FILE_NAME[sizeof(CUSTOM_MINING_CACHE_FILE_NAME) / sizeof(CUSTOM_MINING_CACHE_FILE_NAME[0]) - 6] = i % 10 + L'0'; - success &= gSystemCustomMiningSolutionCache[i].load(CUSTOM_MINING_CACHE_FILE_NAME); - } + success &= gSystemCustomMiningSolutionV2Cache.load(CUSTOM_MINING_CACHE_FILE_NAME); return success; } #endif diff --git a/src/network_core/peers.h b/src/network_core/peers.h index 0150fb7b8..cbce9806d 100644 --- a/src/network_core/peers.h +++ b/src/network_core/peers.h @@ -182,6 +182,23 @@ static bool isWhiteListPeer(unsigned char address[4]) } */ +static bool isPrivateIp(const unsigned char address[4]) +{ + int total = min(int(sizeof(knownPublicPeers)/sizeof(knownPublicPeers[0])), NUMBER_OF_PRIVATE_IP); + for (int i = 0; i < total; i++) + { + const auto& privateIp = knownPublicPeers[i]; + if (address[0] == privateIp[0] + && address[1] == privateIp[1] + && address[2] == privateIp[2] + && address[3] == privateIp[3]) + { + return true; + } + } + return false; +} + static void closePeer(Peer* peer) { PROFILE_SCOPE(); @@ -403,6 +420,7 @@ static void enqueueResponse(Peer* peer, unsigned int dataSize, unsigned char typ */ static bool isBogonAddress(const IPv4Address& address) { + return false; return (!address.u8[0]) || (address.u8[0] == 127) || (address.u8[0] == 10) diff --git a/src/network_messages/custom_mining.h b/src/network_messages/custom_mining.h index bd7aed79c..336ff5323 100644 --- a/src/network_messages/custom_mining.h +++ b/src/network_messages/custom_mining.h @@ -18,11 +18,6 @@ struct RequestedCustomMiningData unsigned long long fromTaskIndex; unsigned long long toTaskIndex; - // Determine which task partition - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int padding; - // Type of the request: either task (taskType) or solution (solutionType). long long dataType; }; @@ -50,10 +45,12 @@ struct RequestedCustomMiningSolutionVerification type = 62, }; unsigned long long taskIndex; - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int nonce; + unsigned long long nonce; + unsigned long long encryptionLevel; + unsigned long long computorRandom; + unsigned long long reserve2; unsigned long long isValid; // validity of the solution. 0: invalid, >0: valid + }; struct RespondCustomMiningSolutionVerification { @@ -69,9 +66,10 @@ struct RespondCustomMiningSolutionVerification customMiningStateEnded = 3, // not in custom mining state }; unsigned long long taskIndex; - unsigned short firstComputorIdx; - unsigned short lastComputorIdx; - unsigned int nonce; + unsigned long long nonce; + unsigned long long encryptionLevel; + unsigned long long computorRandom; + unsigned long long reserve2; long long status; // Flag indicate the status of solution }; diff --git a/src/private_settings.h b/src/private_settings.h index dc5e68c4a..2bebf927d 100644 --- a/src/private_settings.h +++ b/src/private_settings.h @@ -4,12 +4,15 @@ // Do NOT share the data of "Private Settings" section with anybody!!! -#define OPERATOR "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +#define OPERATOR "MEFKYFCDXDUILCAJKOIKWQAPENJDUHSSYPBRWFOTLALILAYWQFDSITJELLHG" static unsigned char computorSeeds[][55 + 1] = { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }; +// number of private ips for computor's internal services +// these are the first N ip in knownPublicPeers, these IPs will never be shared or deleted +#define NUMBER_OF_PRIVATE_IP 2 // Enter static IPs of peers (ideally at least 4 including your own IP) to disseminate them to other peers. // You can find current peer IPs at https://app.qubic.li/network/live static const unsigned char knownPublicPeers[][4] = { @@ -24,35 +27,29 @@ static const unsigned char whiteListPeers[][4] = { }; */ -#define ENABLE_STANDARD_LOGGING 0 // logging universe + spectrum -#define ENABLE_SMART_CONTRACT_LOGGING 0// logging smart contract +#define ENABLE_QUBIC_LOGGING_EVENT 0 // turn on logging events -#if !ENABLE_STANDARD_LOGGING && ENABLE_SMART_CONTRACT_LOGGING -#error ENABLE_SMART_CONTRACT_LOGGING 1 also requires ENABLE_STANDARD_LOGGING 1 -#endif - -#if ENABLE_STANDARD_LOGGING -#define LOG_UNIVERSE 1 // all universe activities/events (incl: issue, ownership/possession changes) -#define LOG_SPECTRUM 1 // all spectrum activities/events (incl: transfers, burn, dust cleaning) -#else -#define LOG_UNIVERSE 0 -#define LOG_SPECTRUM 0 -#endif -#if ENABLE_SMART_CONTRACT_LOGGING +#if ENABLE_QUBIC_LOGGING_EVENT +// DO NOT MODIFY THIS AREA UNLESS YOU ARE DEVELOPING LOGGING FEATURES +#define LOG_UNIVERSE 1 +#define LOG_SPECTRUM 1 #define LOG_CONTRACT_ERROR_MESSAGES 1 #define LOG_CONTRACT_WARNING_MESSAGES 1 #define LOG_CONTRACT_INFO_MESSAGES 1 #define LOG_CONTRACT_DEBUG_MESSAGES 1 #define LOG_CUSTOM_MESSAGES 1 #else +#define LOG_UNIVERSE 0 +#define LOG_SPECTRUM 0 #define LOG_CONTRACT_ERROR_MESSAGES 0 #define LOG_CONTRACT_WARNING_MESSAGES 0 #define LOG_CONTRACT_INFO_MESSAGES 0 #define LOG_CONTRACT_DEBUG_MESSAGES 0 #define LOG_CUSTOM_MESSAGES 0 #endif + static unsigned long long logReaderPasscodes[4] = { - 0, 0, 0, 0 // REMOVE THIS ENTRY AND REPLACE IT WITH YOUR OWN RANDOM NUMBERS IN [0..18446744073709551615] RANGE IF LOGGING IS ENABLED + 1,2,3,4// REMOVE THIS ENTRY AND REPLACE IT WITH YOUR OWN RANDOM NUMBERS IN [0..18446744073709551615] RANGE IF LOGGING IS ENABLED }; // Mode for auto save ticks: @@ -65,4 +62,4 @@ static unsigned long long logReaderPasscodes[4] = { // Perform state persisting when your node is misaligned will also make your node misaligned after resuming. // Thus, picking various TICK_STORAGE_AUTOSAVE_TICK_PERIOD numbers across AUX nodes is recommended. // some suggested prime numbers you can try: 971 977 983 991 997 -#define TICK_STORAGE_AUTOSAVE_TICK_PERIOD 1000 \ No newline at end of file +#define TICK_STORAGE_AUTOSAVE_TICK_PERIOD 1337 \ No newline at end of file diff --git a/src/public_settings.h b/src/public_settings.h index 4d058020c..5b68399d8 100644 --- a/src/public_settings.h +++ b/src/public_settings.h @@ -7,9 +7,9 @@ // no need to define AVX512 here anymore, just change the project settings to use the AVX512 version // random seed is now obtained from spectrumDigests - +#define TESTNET #define MAX_NUMBER_OF_PROCESSORS 32 -#define NUMBER_OF_SOLUTION_PROCESSORS 12 +#define NUMBER_OF_SOLUTION_PROCESSORS 2 // Number of buffers available for executing contract functions in parallel; having more means reserving a bit more RAM (+1 = +32 MB) // and less waiting in request processors if there are more parallel contract function requests. The maximum value that may make sense @@ -23,11 +23,19 @@ // Number of ticks from prior epoch that are kept after seamless epoch transition. These can be requested after transition. #define TICKS_TO_KEEP_FROM_PRIOR_EPOCH 100 -#define TARGET_TICK_DURATION 1500 -#define TRANSACTION_SPARSENESS 1 +// The tick duration used for timing and scheduling logic. +#define TARGET_TICK_DURATION 7000 + +// The tick duration used to calculate the size of memory buffers. +// This determines the memory footprint of the application. +#define TICK_DURATION_FOR_ALLOCATION_MS 7000 +#define TRANSACTION_SPARSENESS 4 + +// Number of ticks that are stored in the pending txs pool. This also defines how many ticks in advance a tx can be registered. +#define PENDING_TXS_POOL_NUM_TICKS (1000 * 60 * 10ULL / TICK_DURATION_FOR_ALLOCATION_MS) // 10 minutes // Below are 2 variables that are used for auto-F5 feature: -#define AUTO_FORCE_NEXT_TICK_THRESHOLD 0ULL // Multiplier of TARGET_TICK_DURATION for the system to detect "F5 case" | set to 0 to disable +#define AUTO_FORCE_NEXT_TICK_THRESHOLD 20ULL // Multiplier of TARGET_TICK_DURATION for the system to detect "F5 case" | set to 0 to disable // to prevent bad actor causing misalignment. // depends on actual tick time of the network, operators should set this number randomly in this range [12, 26] // eg: If AUTO_FORCE_NEXT_TICK_THRESHOLD is 8 and TARGET_TICK_DURATION is 2, then the system will start "auto F5 procedure" after 16 seconds after receveing 451+ votes @@ -37,7 +45,7 @@ #define NEXT_TICK_TIMEOUT_THRESHOLD 5ULL // Multiplier of TARGET_TICK_DURATION for the system to discard next tick in tickData. // This will lead to zero `expectedNextTickTransactionDigest` in consensus - + #define PEER_REFRESHING_PERIOD 120000ULL #if AUTO_FORCE_NEXT_TICK_THRESHOLD != 0 static_assert(NEXT_TICK_TIMEOUT_THRESHOLD < AUTO_FORCE_NEXT_TICK_THRESHOLD, "Timeout threshold must be smaller than auto F5 threshold"); @@ -56,15 +64,16 @@ static_assert(AUTO_FORCE_NEXT_TICK_THRESHOLD* TARGET_TICK_DURATION >= PEER_REFRE // Config options that should NOT be changed by operators #define VERSION_A 1 -#define VERSION_B 252 +#define VERSION_B 264 #define VERSION_C 0 // Epoch and initial tick for node startup -#define EPOCH 171 -#define TICK 30220000 +#define EPOCH 183 +#define TICK 34815000 +#define TICK_IS_FIRST_TICK_OF_EPOCH 1 // Set to 0 if the network is restarted during the EPOCH with a new initial TICK -#define ARBITRATOR "AFZPUAIYVPNUYGJRQVLUKOPPVLHAZQTGLYAAUUNBXFTVTAMSBKQBLEIEPCVJ" -#define DISPATCHER "XPXYKFLGSWRHRGAUKWFWVXCDVEYAPCPCNUTMUDWFGDYQCWZNJMWFZEEGCFFO" +#define ARBITRATOR "MEFKYFCDXDUILCAJKOIKWQAPENJDUHSSYPBRWFOTLALILAYWQFDSITJELLHG" +#define DISPATCHER "DISPAPLNOYSWXCJMZEMFUNCCMMJANGQPYJDSEXZTTBFSUEPYPEKCICADBUCJ" static unsigned short SYSTEM_FILE_NAME[] = L"system"; static unsigned short SYSTEM_END_OF_EPOCH_FILE_NAME[] = L"system.eoe"; @@ -73,11 +82,11 @@ static unsigned short UNIVERSE_FILE_NAME[] = L"universe.???"; static unsigned short SCORE_CACHE_FILE_NAME[] = L"score.???"; static unsigned short CONTRACT_FILE_NAME[] = L"contract????.???"; static unsigned short CUSTOM_MINING_REVENUE_END_OF_EPOCH_FILE_NAME[] = L"custom_revenue.eoe"; -static unsigned short CUSTOM_MINING_CACHE_FILE_NAME[] = L"custom_mining_cache???.???"; +static unsigned short CUSTOM_MINING_CACHE_FILE_NAME[] = L"custom_mining_cache.???"; static constexpr unsigned long long NUMBER_OF_INPUT_NEURONS = 512; // K static constexpr unsigned long long NUMBER_OF_OUTPUT_NEURONS = 512; // L -static constexpr unsigned long long NUMBER_OF_TICKS = 600; // N +static constexpr unsigned long long NUMBER_OF_TICKS = 1000; // N static constexpr unsigned long long NUMBER_OF_NEIGHBORS = 728; // 2M. Must be divided by 2 static constexpr unsigned long long NUMBER_OF_MUTATIONS = 150; static constexpr unsigned long long POPULATION_THRESHOLD = NUMBER_OF_INPUT_NEURONS + NUMBER_OF_OUTPUT_NEURONS + NUMBER_OF_MUTATIONS; // P @@ -87,12 +96,15 @@ static constexpr unsigned int SOLUTION_THRESHOLD_DEFAULT = 321; #define SOLUTION_SECURITY_DEPOSIT 1000000 // Signing difficulty -#define TARGET_TICK_VOTE_SIGNATURE 0x00095CBEU // around 7000 signing operations per ID +#define TARGET_TICK_VOTE_SIGNATURE 0x07FFFFFFU // around 7000 signing operations per ID // include commonly needed definitions #include "network_messages/common_def.h" -#define MAX_NUMBER_OF_TICKS_PER_EPOCH (((((60 * 60 * 24 * 7) / (TARGET_TICK_DURATION / 1000)) + NUMBER_OF_COMPUTORS - 1) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS) +#define TESTNET_EPOCH_DURATION 3000 +#define MAX_NUMBER_OF_TICKS_PER_EPOCH (TESTNET_EPOCH_DURATION + 5) + + #define FIRST_TICK_TRANSACTION_OFFSET sizeof(unsigned long long) #define MAX_TRANSACTION_SIZE (MAX_INPUT_SIZE + sizeof(Transaction) + SIGNATURE_SIZE) @@ -100,10 +112,15 @@ static constexpr unsigned int SOLUTION_THRESHOLD_DEFAULT = 321; #define EXTERNAL_COMPUTATIONS_INTERVAL (676 + 1) static_assert(INTERNAL_COMPUTATIONS_INTERVAL >= NUMBER_OF_COMPUTORS, "Internal computation phase needs to be at least equal NUMBER_OF_COMPUTORS"); -// Format is DoW-hh-mm-ss in hex format, total 4bytes, each use 1 bytes +// List of start-end for full external computation times. The event must not be overlap. +// Format is DoW-hh-mm-ss in hex format, total 4 bytes, each use 1 bytes // DoW: Day of the week 0: Sunday, 1 = Monday ... -#define FULL_EXTERNAL_COMPUTATIONS_TIME_START_TIME 0x060C0000 // Sat 12:00:00 -#define FULL_EXTERNAL_COMPUTATIONS_TIME_STOP_TIME 0x000C0000 // Sun 12:00:00 +static unsigned int gFullExternalComputationTimes[][2] = +{ + {0x040C0000U, 0x040C1E00U}, // Thu 12:00:00 - Fri 12:00:00 + {0x060C0000U, 0x060C1E00U}, // Sat 12:00:00 - Sun 12:00:00 + {0x010C0000U, 0x010C1E00U}, // Mon 12:00:00 - Tue 12:00:00 +}; #define STACK_SIZE 4194304 #define TRACK_MAX_STACK_BUFFER_SIZE diff --git a/src/qubic.cpp b/src/qubic.cpp index 9f4babb9a..e5b3c2a05 100644 --- a/src/qubic.cpp +++ b/src/qubic.cpp @@ -1,7 +1,5 @@ #define SINGLE_COMPILE_UNIT -// #define NO_NOST - // contract_def.h needs to be included first to make sure that contracts have minimal access #include "contract_core/contract_def.h" #include "contract_core/contract_exec.h" @@ -70,6 +68,7 @@ #include "mining/mining.h" #include "oracles/oracle_machines.h" +#include "contract_core/qpi_mining_impl.h" #include "revenue.h" ////////// Qubic \\\\\\\\\\ @@ -82,7 +81,7 @@ #define MAX_MESSAGE_PAYLOAD_SIZE MAX_TRANSACTION_SIZE #define MAX_UNIVERSE_SIZE 1073741824 #define MESSAGE_DISSEMINATION_THRESHOLD 1000000000 -#define PORT 21841 +#define PORT 31841 #define SYSTEM_DATA_SAVING_PERIOD 300000ULL #define TICK_TRANSACTIONS_PUBLICATION_OFFSET 2 // Must be only 2 #define MIN_MINING_SOLUTIONS_PUBLICATION_OFFSET 3 // Must be 3+ @@ -132,6 +131,7 @@ static unsigned short ownComputorIndicesMapping[sizeof(computorSeeds) / sizeof(c static TickStorage ts; static VoteCounter voteCounter; static TickData nextTickData; +static PendingTxsPool pendingTxsPool; static m256i uniqueNextTickTransactionDigests[NUMBER_OF_COMPUTORS]; static unsigned int uniqueNextTickTransactionDigestCounters[NUMBER_OF_COMPUTORS]; @@ -139,13 +139,7 @@ static unsigned int uniqueNextTickTransactionDigestCounters[NUMBER_OF_COMPUTORS] static unsigned int resourceTestingDigest = 0; static unsigned int numberOfTransactions = 0; -static volatile char entityPendingTransactionsLock = 0; -static unsigned char* entityPendingTransactions = NULL; -static unsigned char* entityPendingTransactionDigests = NULL; -static unsigned int entityPendingTransactionIndices[SPECTRUM_CAPACITY]; // [SPECTRUM_CAPACITY] must be >= than [NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR] -static volatile char computorPendingTransactionsLock = 0; -static unsigned char* computorPendingTransactions = NULL; -static unsigned char* computorPendingTransactionDigests = NULL; + static unsigned long long spectrumChangeFlags[SPECTRUM_CAPACITY / (sizeof(unsigned long long) * 8)]; static unsigned long long mainLoopNumerator = 0, mainLoopDenominator = 0; @@ -525,115 +519,7 @@ static void processBroadcastMessage(const unsigned long long processorNumber, Re recordCustomMining = gIsInCustomMiningState; RELEASE(gIsInCustomMiningStateLock); - if (messagePayloadSize == sizeof(CustomMiningTask) && request->sourcePublicKey == dispatcherPublicKey) - { - // See CustomMiningTaskMessage structure - // MESSAGE_TYPE_CUSTOM_MINING_TASK - - // Compute the gamming key to get the sub-type of message - unsigned char sharedKeyAndGammingNonce[64]; - setMem(sharedKeyAndGammingNonce, 32, 0); - copyMem(&sharedKeyAndGammingNonce[32], &request->gammingNonce, 32); - unsigned char gammingKey[32]; - KangarooTwelve64To32(sharedKeyAndGammingNonce, gammingKey); - - // Record the task emitted by dispatcher - if (recordCustomMining && gammingKey[0] == MESSAGE_TYPE_CUSTOM_MINING_TASK) - { - const CustomMiningTask* task = ((CustomMiningTask*)((unsigned char*)request + sizeof(BroadcastMessage))); - - // Determine the task part id - int partId = customMiningGetPartitionID(task->firstComputorIndex, task->lastComputorIndex); - if (partId >= 0) - { - // Record the task message - ACQUIRE(gCustomMiningTaskStorageLock); - int taskAddSts = gCustomMiningStorage._taskStorage[partId].addData(task); - RELEASE(gCustomMiningTaskStorageLock); - - if (CustomMiningTaskStorage::OK == taskAddSts) - { - ATOMIC_INC64(gCustomMiningStats.phase[partId].tasks); - } - } - } - } - else if (messagePayloadSize == sizeof(CustomMiningSolution)) - { - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS; i++) - { - if (request->sourcePublicKey == broadcastedComputors.computors.publicKeys[i]) - { - // Compute the gamming key to get the sub-type of message - unsigned char sharedKeyAndGammingNonce[64]; - setMem(sharedKeyAndGammingNonce, 32, 0); - copyMem(&sharedKeyAndGammingNonce[32], &request->gammingNonce, 32); - unsigned char gammingKey[32]; - KangarooTwelve64To32(sharedKeyAndGammingNonce, gammingKey); - - if (recordCustomMining && gammingKey[0] == MESSAGE_TYPE_CUSTOM_MINING_SOLUTION) - { - // Record the solution - bool isSolutionGood = false; - const CustomMiningSolution* solution = ((CustomMiningSolution*)((unsigned char*)request + sizeof(BroadcastMessage))); - - int partId = customMiningGetPartitionID(solution->firstComputorIndex, solution->lastComputorIndex); - - // TODO: taskIndex can use for detect for-sure stale shares - if (partId >= 0 && solution->taskIndex > 0) - { - CustomMiningSolutionCacheEntry cacheEntry; - cacheEntry.set(solution); - - unsigned int cacheIndex = 0; - int sts = gSystemCustomMiningSolutionCache[partId].tryFetching(cacheEntry, cacheIndex); - - // Check for duplicated solution - if (sts == CUSTOM_MINING_CACHE_MISS) - { - gSystemCustomMiningSolutionCache[partId].addEntry(cacheEntry, cacheIndex); - isSolutionGood = true; - } - - if (isSolutionGood) - { - // Check the computor idx of this solution. - unsigned short computorID = customMiningGetComputorID(solution->nonce, partId); - if (computorID <= gTaskPartition[partId].lastComputorIdx) - { - - ACQUIRE(gCustomMiningSharesCountLock); - gCustomMiningSharesCount[computorID]++; - RELEASE(gCustomMiningSharesCountLock); - - CustomMiningSolutionStorageEntry solutionStorageEntry; - solutionStorageEntry.taskIndex = solution->taskIndex; - solutionStorageEntry.nonce = solution->nonce; - solutionStorageEntry.cacheEntryIndex = cacheIndex; - - ACQUIRE(gCustomMiningSolutionStorageLock); - gCustomMiningStorage._solutionStorage[partId].addData(&solutionStorageEntry); - RELEASE(gCustomMiningSolutionStorageLock); - - } - } - - // Record stats - const unsigned int hitCount = gSystemCustomMiningSolutionCache[partId].hitCount(); - const unsigned int missCount = gSystemCustomMiningSolutionCache[partId].missCount(); - const unsigned int collision = gSystemCustomMiningSolutionCache[partId].collisionCount(); - - ATOMIC_STORE64(gCustomMiningStats.phase[partId].shares, missCount); - ATOMIC_STORE64(gCustomMiningStats.phase[partId].duplicated, hitCount); - ATOMIC_MAX64(gCustomMiningStats.maxCollisionShareCount, collision); - - } - } - break; - } - } - } - else if (messagePayloadSize == sizeof(CustomMiningTaskV2) && request->sourcePublicKey == dispatcherPublicKey) + if (messagePayloadSize == sizeof(CustomMiningTaskV2) && request->sourcePublicKey == dispatcherPublicKey) { unsigned char sharedKeyAndGammingNonce[64]; setMem(sharedKeyAndGammingNonce, 32, 0); @@ -649,7 +535,7 @@ static void processBroadcastMessage(const unsigned long long processorNumber, Re // Record the task message ACQUIRE(gCustomMiningTaskStorageLock); int taskAddSts = gCustomMiningStorage._taskV2Storage.addData(task); - if (CustomMiningTaskStorage::OK == taskAddSts) + if (CustomMiningTaskV2Storage::OK == taskAddSts) { ATOMIC_INC64(gCustomMiningStats.phaseV2.tasks); gCustomMiningStorage.updateTaskIndex(task->taskIndex); @@ -696,7 +582,7 @@ static void processBroadcastMessage(const unsigned long long processorNumber, Re if (isSolutionGood) { // Check the computor idx of this solution. - unsigned short computorID = (solution->nonce >> 32ULL) % 676ULL; + unsigned short computorID = customMiningGetComputorID(solution); ACQUIRE(gCustomMiningSharesCountLock); gCustomMiningSharesCount[computorID]++; @@ -1073,42 +959,7 @@ static void processBroadcastTransaction(Peer* peer, RequestResponseHeader* heade enqueueResponse(NULL, header); } - const int computorIndex = ::computorIndex(request->sourcePublicKey); - if (computorIndex >= 0) - { - ACQUIRE(computorPendingTransactionsLock); - - const unsigned int offset = random(MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR); - if (((Transaction*)&computorPendingTransactions[computorIndex * offset * MAX_TRANSACTION_SIZE])->tick < request->tick - && request->tick < system.initialTick + MAX_NUMBER_OF_TICKS_PER_EPOCH) - { - copyMem(&computorPendingTransactions[computorIndex * offset * MAX_TRANSACTION_SIZE], request, transactionSize); - KangarooTwelve(request, transactionSize, &computorPendingTransactionDigests[computorIndex * offset * 32ULL], 32); - } - - RELEASE(computorPendingTransactionsLock); - } - else - { - const int spectrumIndex = ::spectrumIndex(request->sourcePublicKey); - if (spectrumIndex >= 0) - { - ACQUIRE(entityPendingTransactionsLock); - - // Pending transactions pool follows the rule: A transaction with a higher tick overwrites previous transaction from the same address. - // The second filter is to avoid accident made by users/devs (setting scheduled tick too high) and get locked until end of epoch. - // It also makes sense that a node doesn't need to store a transaction that is scheduled on a tick that node will never reach. - // Notice: MAX_NUMBER_OF_TICKS_PER_EPOCH is not set globally since every node may have different TARGET_TICK_DURATION time due to memory limitation. - if (((Transaction*)&entityPendingTransactions[spectrumIndex * MAX_TRANSACTION_SIZE])->tick < request->tick - && request->tick < system.initialTick + MAX_NUMBER_OF_TICKS_PER_EPOCH) - { - copyMem(&entityPendingTransactions[spectrumIndex * MAX_TRANSACTION_SIZE], request, transactionSize); - KangarooTwelve(request, transactionSize, &entityPendingTransactionDigests[spectrumIndex * 32ULL], 32); - } - - RELEASE(entityPendingTransactionsLock); - } - } + pendingTxsPool.add(request); unsigned int tickIndex = ts.tickToIndexCurrentEpoch(request->tick); ts.tickData.acquireLock(); @@ -1240,15 +1091,15 @@ static void processRequestTickTransactions(Peer* peer, RequestResponseHeader* he if (tickEpoch != 0) { - unsigned short tickTransactionIndices[NUMBER_OF_TRANSACTIONS_PER_TICK]; - unsigned short numberOfTickTransactions; + unsigned int tickTransactionIndices[NUMBER_OF_TRANSACTIONS_PER_TICK]; + unsigned int numberOfTickTransactions; for (numberOfTickTransactions = 0; numberOfTickTransactions < NUMBER_OF_TRANSACTIONS_PER_TICK; numberOfTickTransactions++) { tickTransactionIndices[numberOfTickTransactions] = numberOfTickTransactions; } while (numberOfTickTransactions) { - const unsigned short index = random(numberOfTickTransactions); + const unsigned int index = random(numberOfTickTransactions); if (!(request->transactionFlags[tickTransactionIndices[index] >> 3] & (1 << (tickTransactionIndices[index] & 7)))) { @@ -1377,7 +1228,7 @@ static void processRequestContractIPO(Peer* peer, RequestResponseHeader* header) respondContractIPO.contractIndex = request->contractIndex; respondContractIPO.tick = system.tick; if (request->contractIndex >= contractCount - || system.epoch >= contractDescriptions[request->contractIndex].constructionEpoch) + || system.epoch != (contractDescriptions[request->contractIndex].constructionEpoch - 1)) { setMem(respondContractIPO.publicKeys, sizeof(respondContractIPO.publicKeys), 0); setMem(respondContractIPO.prices, sizeof(respondContractIPO.prices), 0); @@ -1472,8 +1323,6 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, KangarooTwelve(request, header->size() - sizeof(RequestResponseHeader) - SIGNATURE_SIZE, digest, sizeof(digest)); if (verify(operatorPublicKey.m256i_u8, digest, ((const unsigned char*)header + (header->size() - SIGNATURE_SIZE)))) { - RespondCustomMiningSolutionVerification respond; - // Update the share counting // Only record shares in idle phase char recordSolutions = 0; @@ -1481,25 +1330,20 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, recordSolutions = gIsInCustomMiningState; RELEASE(gIsInCustomMiningStateLock); + RespondCustomMiningSolutionVerification respond = customMiningVerificationRequestToRespond(request); if (recordSolutions) { - CustomMiningSolutionCacheEntry fullEntry; - fullEntry.set(request->taskIndex, request->nonce, request->firstComputorIdx, request->lastComputorIdx); + CustomMiningSolutionV2 solution = customMiningVerificationRequestToSolution(request); + + CustomMiningSolutionV2CacheEntry fullEntry; + fullEntry.set(&solution); fullEntry.setVerified(true); fullEntry.setValid(request->isValid > 0); - // Make sure the solution still existed - int partId = customMiningGetPartitionID(request->firstComputorIdx, request->lastComputorIdx); // Check the computor idx of this solution - int computorID = NUMBER_OF_COMPUTORS; - if (partId >= 0) - { - computorID = customMiningGetComputorID(request->nonce, partId); - } - - if (partId >=0 - && computorID <= gTaskPartition[partId].lastComputorIdx - && CUSTOM_MINING_CACHE_HIT == gSystemCustomMiningSolutionCache[partId].tryFetchingAndUpdate(fullEntry, CUSTOM_MINING_CACHE_HIT)) + unsigned short computorID = customMiningGetComputorID(&solution); + // Also re-update the cache data with verified = true and validity + if ( gSystemCustomMiningSolutionV2Cache.tryFetchingAndUpdateHitData(fullEntry)) { // Reduce the share of this nonce if it is invalid if (0 == request->isValid) @@ -1509,13 +1353,13 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, RELEASE(gCustomMiningSharesCountLock); // Save the number of invalid share count - ATOMIC_INC64(gCustomMiningStats.phase[partId].invalid); + ATOMIC_INC64(gCustomMiningStats.phaseV2.invalid); respond.status = RespondCustomMiningSolutionVerification::invalid; } else { - ATOMIC_INC64(gCustomMiningStats.phase[partId].valid); + ATOMIC_INC64(gCustomMiningStats.phaseV2.valid); respond.status = RespondCustomMiningSolutionVerification::valid; } } @@ -1528,11 +1372,6 @@ static void processRequestedCustomMiningSolutionVerificationRequest(Peer* peer, { respond.status = RespondCustomMiningSolutionVerification::customMiningStateEnded; } - - respond.taskIndex = request->taskIndex; - respond.firstComputorIdx = request->firstComputorIdx; - respond.lastComputorIdx = request->lastComputorIdx; - respond.nonce = request->nonce; enqueueResponse(peer, sizeof(respond), RespondCustomMiningSolutionVerification::type, header->dejavu(), &respond); } } @@ -1585,18 +1424,12 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long else if (request->dataType == RequestedCustomMiningData::solutionType) { // For solution type, return all solution from the current phase - int partId = customMiningGetPartitionID(request->firstComputorIdx, request->lastComputorIdx); - if (partId >= 0) { ACQUIRE(gCustomMiningSolutionStorageLock); // Look for all solution data - respond = gCustomMiningStorage._solutionStorage[partId].getSerializedData(request->fromTaskIndex, processorNumber); + respond = gCustomMiningStorage._solutionV2Storage.getSerializedData(request->fromTaskIndex, processorNumber); RELEASE(gCustomMiningSolutionStorageLock); } - else - { - respond = NULL; - } // Has the solutions if (NULL != respond) @@ -1610,12 +1443,12 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long unsigned char* respondSolutionPayload = respondSolution + sizeof(CustomMiningRespondDataHeader); long long remainedDataToSend = CUSTOM_MINING_RESPOND_MESSAGE_MAX_SIZE; int sendItem = 0; - for (int k = 0; k < customMiningInternalHeader->itemCount && remainedDataToSend > sizeof(CustomMiningSolution); k++) + for (int k = 0; k < customMiningInternalHeader->itemCount && remainedDataToSend > sizeof(CustomMiningSolutionV2); k++) { CustomMiningSolutionStorageEntry entry = solutionEntries[k]; - CustomMiningSolutionCacheEntry fullEntry; + CustomMiningSolutionV2CacheEntry fullEntry; - gSystemCustomMiningSolutionCache[partId].getEntry(fullEntry, (unsigned int)entry.cacheEntryIndex); + gSystemCustomMiningSolutionV2Cache.getEntry(fullEntry, (unsigned int)entry.cacheEntryIndex); // Check data is matched and not verifed yet if (!fullEntry.isEmpty() @@ -1624,16 +1457,16 @@ static void processCustomMiningDataRequest(Peer* peer, const unsigned long long && fullEntry.getNonce() == entry.nonce) { // Append data to send - CustomMiningSolution solution; + CustomMiningSolutionV2 solution; fullEntry.get(solution); - copyMem(respondSolutionPayload + k * sizeof(CustomMiningSolution), &solution, sizeof(CustomMiningSolution)); - remainedDataToSend -= sizeof(CustomMiningSolution); + copyMem(respondSolutionPayload + k * sizeof(CustomMiningSolutionV2), &solution, sizeof(CustomMiningSolutionV2)); + remainedDataToSend -= sizeof(CustomMiningSolutionV2); sendItem++; } } - customMiningInternalHeader->itemSize = sizeof(CustomMiningSolution); + customMiningInternalHeader->itemSize = sizeof(CustomMiningSolutionV2); customMiningInternalHeader->itemCount = sendItem; customMiningInternalHeader->respondType = RespondCustomMiningData::solutionType; const unsigned long long respondDataSize = sizeof(CustomMiningRespondDataHeader) + customMiningInternalHeader->itemCount * customMiningInternalHeader->itemSize; @@ -1816,13 +1649,27 @@ static void setNewMiningSeed() score->initMiningData(spectrumDigests[(SPECTRUM_CAPACITY * 2 - 1) - 1]); } -WeekDay gFullExternalStartTime; -WeekDay gFullExternalEndTime; +// Total number of external mining event. +// Can set to zero to disable event +static constexpr int gNumberOfFullExternalMiningEvents = sizeof(gFullExternalComputationTimes) > 0 ? sizeof(gFullExternalComputationTimes) / sizeof(gFullExternalComputationTimes[0]) : 0; +struct FullExternallEvent +{ + WeekDay startTime; + WeekDay endTime; +}; +FullExternallEvent* gFullExternalEventTime = NULL; static bool gSpecialEventFullExternalComputationPeriod = false; // a flag indicates a special event (period) that the network running 100% external computation +static WeekDay currentEventEndTime; static bool isFullExternalComputationTime(TimeDate tickDate) { + // No event + if (gNumberOfFullExternalMiningEvents <= 0) + { + return false; + } + // Get current day of the week WeekDay tickWeekDay; tickWeekDay.hour = tickDate.hour; @@ -1831,12 +1678,16 @@ static bool isFullExternalComputationTime(TimeDate tickDate) tickWeekDay.millisecond = tickDate.millisecond; tickWeekDay.dayOfWeek = getDayOfWeek(tickDate.day, tickDate.month, 2000 + tickDate.year); - - // Check if the day is in range. - if (isWeekDayInRange(tickWeekDay, gFullExternalStartTime, gFullExternalEndTime)) + // Check if the day is in range. Expect the time is not overlap. + for (int i = 0; i < gNumberOfFullExternalMiningEvents; ++i) { - gSpecialEventFullExternalComputationPeriod = true; - return true; + if (isWeekDayInRange(tickWeekDay, gFullExternalEventTime[i].startTime, gFullExternalEventTime[i].endTime)) + { + gSpecialEventFullExternalComputationPeriod = true; + + currentEventEndTime = gFullExternalEventTime[i].endTime; + return true; + } } // When not in range, and the time pass the gFullExternalEndTime. We need to make sure the ending happen @@ -1845,9 +1696,9 @@ static bool isFullExternalComputationTime(TimeDate tickDate) { // Check time pass the end time TimeDate endTimeDate = tickDate; - endTimeDate.hour = gFullExternalEndTime.hour; - endTimeDate.minute = gFullExternalEndTime.minute; - endTimeDate.second = gFullExternalEndTime.second; + endTimeDate.hour = currentEventEndTime.hour; + endTimeDate.minute = currentEventEndTime.minute; + endTimeDate.second = currentEventEndTime.second; if (compareTimeDate(tickDate, endTimeDate) == 1) { @@ -1860,117 +1711,101 @@ static bool isFullExternalComputationTime(TimeDate tickDate) } } - // The event only happen once + // Event is marked as end gSpecialEventFullExternalComputationPeriod = false; return false; } -static void checkAndSwitchMiningPhase(short tickEpoch, TimeDate tickDate) -{ - // Check if current time is for full custom mining period - static bool fullExternalTimeBegin = false; - bool restartTheMiningPhase = false; - - // Make sure the tick is valid - if (tickEpoch == system.epoch) - { - if (isFullExternalComputationTime(tickDate)) - { - // Trigger time - if (!fullExternalTimeBegin) - { - fullExternalTimeBegin = true; - - // Turn off the qubic mining phase - score->initMiningData(m256i::zero()); - } - } - else // Not in the full external time, just behavior like normal. - { - // Switch back to qubic mining phase if neccessary - if (fullExternalTimeBegin) - { - if (getTickInMiningPhaseCycle() <= INTERNAL_COMPUTATIONS_INTERVAL) - { - setNewMiningSeed(); - } - } - fullExternalTimeBegin = false; - } - } - - // Incase of the full custom mining is just end. The setNewMiningSeed() will wait for next period of qubic mining phase - if (!fullExternalTimeBegin) - { - const unsigned int r = getTickInMiningPhaseCycle(); - if (!r) - { - setNewMiningSeed(); - } - else - { - if (r == INTERNAL_COMPUTATIONS_INTERVAL + 3) // 3 is added because of 3-tick shift for transaction confirmation - { - score->initMiningData(m256i::zero()); - } - } - } -} - // Clean up before custom mining phase. Thread-safe function static void beginCustomMiningPhase() { - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - gSystemCustomMiningSolutionCache[i].reset(); - } - gSystemCustomMiningSolutionV2Cache.reset(); gCustomMiningStorage.reset(); gCustomMiningStats.phaseResetAndEpochAccumulate(); } -static void checkAndSwitchCustomMiningPhase(short tickEpoch, TimeDate tickDate) +// resetPhase: If true, allows reinitializing mining seed and the custom mining phase flag +// even when already inside the current phase. These values are normally set only once +// at the beginning of a phase. +static void checkAndSwitchMiningPhase(short tickEpoch, TimeDate tickDate, bool resetPhase) { bool isBeginOfCustomMiningPhase = false; char isInCustomMiningPhase = 0; - // Check if current time is for full custom mining period - static bool fullExternalTimeBegin = false; - - // Make sure the tick is valid - if (tickEpoch == system.epoch) + // When resetting the phase: + // - If in the internal mining phase => reset the mining seed for the new epoch + // - If in the external (custom) mining phase => reset mining data (counters, etc.) + if (resetPhase) { - if (isFullExternalComputationTime(tickDate)) + const unsigned int r = getTickInMiningPhaseCycle(); + if (r < INTERNAL_COMPUTATIONS_INTERVAL) { - // Trigger time - if (!fullExternalTimeBegin) - { - fullExternalTimeBegin = true; - isBeginOfCustomMiningPhase = true; - } - isInCustomMiningPhase = 1; + setNewMiningSeed(); } - else // Not in the full external time, just behavior like normal. + else { - fullExternalTimeBegin = false; + score->initMiningData(m256i::zero()); + isBeginOfCustomMiningPhase = true; + isInCustomMiningPhase = 1; } } - - if (!fullExternalTimeBegin) + else { - const unsigned int r = getTickInMiningPhaseCycle(); - if (r >= INTERNAL_COMPUTATIONS_INTERVAL) + // Track whether we’re currently in a full external computation window + static bool isInFullExternalTime = false; + + // Make sure the tick is valid and not in the reset phase state + if (tickEpoch == system.epoch) { - isInCustomMiningPhase = 1; - if (r == INTERNAL_COMPUTATIONS_INTERVAL) + if (isFullExternalComputationTime(tickDate)) { - isBeginOfCustomMiningPhase = true; + // Trigger time + if (!isInFullExternalTime) + { + isInFullExternalTime = true; + + // Turn off the qubic mining phase + score->initMiningData(m256i::zero()); + + // Start the custom mining phase + isBeginOfCustomMiningPhase = true; + } + isInCustomMiningPhase = 1; + } + else + { + // Not in the full external phase anymore + isInFullExternalTime = false; } } - else + + // Incase of the full custom mining is just end. The setNewMiningSeed() will wait for next period of qubic mining phase + if (!isInFullExternalTime) { - isInCustomMiningPhase = 0; + const unsigned int r = getTickInMiningPhaseCycle(); + if (!r) + { + setNewMiningSeed(); + } + else + { + if (r == INTERNAL_COMPUTATIONS_INTERVAL + 3) // 3 is added because of 3-tick shift for transaction confirmation + { + score->initMiningData(m256i::zero()); + } + + // Setting for custom mining phase + isInCustomMiningPhase = 0; + if (r >= INTERNAL_COMPUTATIONS_INTERVAL) + { + isInCustomMiningPhase = 1; + // Begin of custom mining phase. Turn the flag on so we can reset some state variables + if (r == INTERNAL_COMPUTATIONS_INTERVAL) + { + isBeginOfCustomMiningPhase = true; + } + } + } } } @@ -1984,29 +1819,6 @@ static void checkAndSwitchCustomMiningPhase(short tickEpoch, TimeDate tickDate) ACQUIRE(gIsInCustomMiningStateLock); gIsInCustomMiningState = isInCustomMiningPhase; RELEASE(gIsInCustomMiningStateLock); - -} - -// a function to check and switch mining phase especially for begin/end epoch event -// if we are in internal mining phase (no matter beginning or in the middle) => reset mining seed to new spectrum of the new epoch -// same for external mining phase => reset all counters are needed -// this function should be called after beginEpoch procedure -// TODO: merge checkMiningPhaseBeginAndEndEpoch + checkAndSwitchCustomMiningPhase + checkAndSwitchMiningPhase -static void checkMiningPhaseBeginAndEndEpoch() -{ - const unsigned int r = getTickInMiningPhaseCycle(); - if (r < INTERNAL_COMPUTATIONS_INTERVAL) - { - setNewMiningSeed(); - } - else - { - score->initMiningData(m256i::zero()); - beginCustomMiningPhase(); - ACQUIRE(gIsInCustomMiningStateLock); - gIsInCustomMiningState = 1; - RELEASE(gIsInCustomMiningStateLock); - } } // Updates the global numberTickTransactions based on the tick data in the tick storage. @@ -2511,7 +2323,7 @@ static void processTickTransactionContractIPO(const Transaction* transaction, co ASSERT(!transaction->amount && transaction->inputSize == sizeof(ContractIPOBid)); ASSERT(spectrumIndex >= 0); ASSERT(contractIndex < contractCount); - ASSERT(system.epoch < contractDescriptions[contractIndex].constructionEpoch); + ASSERT(system.epoch == (contractDescriptions[contractIndex].constructionEpoch - 1)); ContractIPOBid* contractIPOBid = (ContractIPOBid*)transaction->inputPtr(); bidInContractIPO(contractIPOBid->price, contractIPOBid->quantity, transaction->sourcePublicKey, spectrumIndex, contractIndex); @@ -2918,7 +2730,7 @@ static void processTickTransaction(const Transaction* transaction, const m256i& && contractIndex < contractCount) { // Contract transactions - if (system.epoch < contractDescriptions[contractIndex].constructionEpoch) + if (system.epoch == (contractDescriptions[contractIndex].constructionEpoch - 1)) { // IPO if (!transaction->amount @@ -2927,7 +2739,8 @@ static void processTickTransaction(const Transaction* transaction, const m256i& processTickTransactionContractIPO(transaction, spectrumIndex, contractIndex); } } - else if (system.epoch < contractDescriptions[contractIndex].destructionEpoch) + else if (system.epoch >= contractDescriptions[contractIndex].constructionEpoch + && system.epoch < contractDescriptions[contractIndex].destructionEpoch) { // Regular contract procedure invocation moneyFlew = processTickTransactionContractProcedure(transaction, spectrumIndex, contractIndex); @@ -3064,7 +2877,12 @@ static void processTick(unsigned long long processorNumber) // it should never go here } - if (system.tick == system.initialTick) + // Ensure to only call INITIALIZE and BEGIN_EPOCH once per epoch: + // system.initialTick usually is the first tick of the epoch, except when the network is restarted + // from scratch with a new TICK (which shall be indicated by TICK_IS_FIRST_TICK_OF_EPOCH == 0). + // However, after seamless epoch transition (system.epoch > EPOCH), system.initialTick is the first + // tick of the epoch in any case. + if (system.tick == system.initialTick && (TICK_IS_FIRST_TICK_OF_EPOCH || system.epoch > EPOCH)) { PROFILE_NAMED_SCOPE_BEGIN("processTick(): INITIALIZE"); logger.registerNewTx(system.tick, logger.SC_INITIALIZE_TX); @@ -3307,109 +3125,56 @@ static void processTick(unsigned long long processorNumber) timelockPreimage[2] = etalonTick.saltedComputerDigest; KangarooTwelve(timelockPreimage, sizeof(timelockPreimage), &broadcastedFutureTickData.tickData.timelock, sizeof(broadcastedFutureTickData.tickData.timelock)); - unsigned int j = 0; - - ACQUIRE(computorPendingTransactionsLock); - - // Get indices of pending computor transactions that are scheduled to be included in tickData - unsigned int numberOfEntityPendingTransactionIndices = 0; - for (unsigned int k = 0; k < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; k++) - { - const Transaction* tx = ((Transaction*)&computorPendingTransactions[k * MAX_TRANSACTION_SIZE]); - if (tx->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET) - { - entityPendingTransactionIndices[numberOfEntityPendingTransactionIndices++] = k; - } - } - - // Randomly select computor tx scheduled for the tick until tick is full or all pending tx are included - while (j < NUMBER_OF_TRANSACTIONS_PER_TICK && numberOfEntityPendingTransactionIndices) + unsigned int nextTxIndex = 0; + unsigned int numPendingTickTxs = pendingTxsPool.getNumberOfPendingTickTxs(system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET); + pendingTxsPool.acquireLock(); + for (unsigned int tx = 0; tx < numPendingTickTxs; ++tx) { - const unsigned int index = random(numberOfEntityPendingTransactionIndices); - - const Transaction* pendingTransaction = ((Transaction*)&computorPendingTransactions[entityPendingTransactionIndices[index] * MAX_TRANSACTION_SIZE]); - ASSERT(pendingTransaction->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET); +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"pendingTxsPool.get() call in processTick()"); +#endif + const Transaction* pendingTransaction = pendingTxsPool.getTx(system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET, tx); + if (pendingTransaction) { ASSERT(pendingTransaction->checkValidity()); const unsigned int transactionSize = pendingTransaction->totalSize(); + ts.tickTransactions.acquireLock(); if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) { - ts.tickTransactions.acquireLock(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - ts.tickTransactionOffsets(pendingTransaction->tick, j) = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), (void*)pendingTransaction, transactionSize); - broadcastedFutureTickData.tickData.transactionDigests[j] = &computorPendingTransactionDigests[entityPendingTransactionIndices[index] * 32ULL]; - j++; - ts.nextTickTransactionOffset += transactionSize; - } - ts.tickTransactions.releaseLock(); + ts.tickTransactionOffsets(pendingTransaction->tick, nextTxIndex) = ts.nextTickTransactionOffset; + copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), (void*)pendingTransaction, transactionSize); + const m256i* digest = pendingTxsPool.getDigest(system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET, tx); + // digest should always be != nullptr because pendingTransaction != nullptr + ASSERT(digest); + broadcastedFutureTickData.tickData.transactionDigests[nextTxIndex] = digest ? *digest : m256i::zero(); + ts.nextTickTransactionOffset += transactionSize; + nextTxIndex++; } + ts.tickTransactions.releaseLock(); } - - entityPendingTransactionIndices[index] = entityPendingTransactionIndices[--numberOfEntityPendingTransactionIndices]; - } - - RELEASE(computorPendingTransactionsLock); - - ACQUIRE(entityPendingTransactionsLock); - - // Get indices of pending non-computor transactions that are scheduled to be included in tickData - numberOfEntityPendingTransactionIndices = 0; - for (unsigned int k = 0; k < SPECTRUM_CAPACITY; k++) - { - const Transaction* tx = ((Transaction*)&entityPendingTransactions[k * MAX_TRANSACTION_SIZE]); - if (tx->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET) - { - entityPendingTransactionIndices[numberOfEntityPendingTransactionIndices++] = k; - } - } - - // Randomly select non-computor tx scheduled for the tick until tick is full or all pending tx are included - while (j < NUMBER_OF_TRANSACTIONS_PER_TICK && numberOfEntityPendingTransactionIndices) - { - const unsigned int index = random(numberOfEntityPendingTransactionIndices); - - const Transaction* pendingTransaction = ((Transaction*)&entityPendingTransactions[entityPendingTransactionIndices[index] * MAX_TRANSACTION_SIZE]); - ASSERT(pendingTransaction->tick == system.tick + TICK_TRANSACTIONS_PUBLICATION_OFFSET); + else { - ASSERT(pendingTransaction->checkValidity()); - const unsigned int transactionSize = pendingTransaction->totalSize(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - ts.tickTransactions.acquireLock(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - ts.tickTransactionOffsets(pendingTransaction->tick, j) = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), (void*)pendingTransaction, transactionSize); - broadcastedFutureTickData.tickData.transactionDigests[j] = &entityPendingTransactionDigests[entityPendingTransactionIndices[index] * 32ULL]; - j++; - ts.nextTickTransactionOffset += transactionSize; - } - ts.tickTransactions.releaseLock(); - } + break; } - - entityPendingTransactionIndices[index] = entityPendingTransactionIndices[--numberOfEntityPendingTransactionIndices]; } - - RELEASE(entityPendingTransactionsLock); + pendingTxsPool.releaseLock(); { // insert & broadcast vote counter tx - makeAndBroadcastTickVotesTransaction(i, broadcastedFutureTickData, j++); + makeAndBroadcastTickVotesTransaction(i, broadcastedFutureTickData, nextTxIndex++); } { - // insert & broadcast custom mining share - if (makeAndBroadcastCustomMiningTransaction(i, broadcastedFutureTickData, j)) // this type of tx is only broadcasted in mining phases + // insert & broadcast external mining score packet (containing the score for each computor on the last external mining phase) + // this type of tx is only broadcasted in internal mining phases + if (makeAndBroadcastCustomMiningTransaction(i, broadcastedFutureTickData, nextTxIndex)) { - j++; + nextTxIndex++; } } - for (; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) + for (; nextTxIndex < NUMBER_OF_TRANSACTIONS_PER_TICK; ++nextTxIndex) { - broadcastedFutureTickData.tickData.transactionDigests[j] = m256i::zero(); + broadcastedFutureTickData.tickData.transactionDigests[nextTxIndex] = m256i::zero(); } setMem(broadcastedFutureTickData.tickData.contractFees, sizeof(broadcastedFutureTickData.tickData.contractFees), 0); @@ -3542,11 +3307,6 @@ static void resetCustomMining() gCustomMiningSharesCounter.init(); setMem(gCustomMiningSharesCount, sizeof(gCustomMiningSharesCount), 0); - for (int i = 0; i < NUMBER_OF_TASK_PARTITIONS; i++) - { - gSystemCustomMiningSolutionCache[i].reset(); - } - gSystemCustomMiningSolutionV2Cache.reset(); for (int i = 0; i < NUMBER_OF_COMPUTORS; ++i) { @@ -3574,25 +3334,19 @@ static void beginEpoch() #ifndef NDEBUG ts.checkStateConsistencyWithAssert(); + pendingTxsPool.checkStateConsistencyWithAssert(); #endif ts.beginEpoch(system.initialTick); + pendingTxsPool.beginEpoch(system.initialTick); voteCounter.init(); #ifndef NDEBUG ts.checkStateConsistencyWithAssert(); + pendingTxsPool.checkStateConsistencyWithAssert(); #endif #if ADDON_TX_STATUS_REQUEST beginEpochTxStatusRequestAddOn(system.initialTick); #endif - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; i++) - { - ((Transaction*)&computorPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick = 0; - } - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) - { - ((Transaction*)&entityPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick = 0; - } - setMem(solutionPublicationTicks, sizeof(solutionPublicationTicks), 0); setMem(faultyComputorFlags, sizeof(faultyComputorFlags), 0); @@ -3641,6 +3395,7 @@ static void beginEpoch() #endif logger.reset(system.initialTick); + } @@ -4441,90 +4196,56 @@ static void prepareNextTickTransactions() if (numberOfKnownNextTickTransactions != numberOfNextTickTransactions) { - // Checks if any of the missing transactions is available in the computorPendingTransaction and remove unknownTransaction flag if found - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; i++) - { - Transaction* pendingTransaction = (Transaction*)&computorPendingTransactions[i * MAX_TRANSACTION_SIZE]; - if (pendingTransaction->tick == nextTick) - { - ACQUIRE(computorPendingTransactionsLock); - - ASSERT(pendingTransaction->checkValidity()); - auto* tsPendingTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(pendingTransaction->tick); - for (unsigned int j = 0; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) - { - if (unknownTransactions[j >> 6] & (1ULL << (j & 63))) - { - if (&computorPendingTransactionDigests[i * 32ULL] == nextTickData.transactionDigests[j]) - { - ts.tickTransactions.acquireLock(); - // write tx to tick tx storage, no matter if tsNextTickTransactionOffsets[i] is 0 (new tx) - // or not (tx with digest that doesn't match tickData needs to be overwritten) - { - const unsigned int transactionSize = pendingTransaction->totalSize(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) - { - tsPendingTransactionOffsets[j] = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), pendingTransaction, transactionSize); - ts.nextTickTransactionOffset += transactionSize; + // Checks if any of the missing transactions is available in the pending transaction pool and remove unknownTransaction flag if found - numberOfKnownNextTickTransactions++; - } - } - ts.tickTransactions.releaseLock(); - - unknownTransactions[j >> 6] &= ~(1ULL << (j & 63)); - - break; - } - } - } - - RELEASE(computorPendingTransactionsLock); - } - } - // Checks if any of the missing transactions is available in the entityPendingTransaction and remove unknownTransaction flag if found - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) + unsigned int numPendingTickTxs = pendingTxsPool.getNumberOfPendingTickTxs(nextTick); + pendingTxsPool.acquireLock(); + for (unsigned int i = 0; i < numPendingTickTxs; ++i) { - Transaction* pendingTransaction = (Transaction*)&entityPendingTransactions[i * MAX_TRANSACTION_SIZE]; - if (pendingTransaction->tick == nextTick) +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"pendingTxsPool.get() call in prepareNextTickTransactions()"); +#endif + Transaction* pendingTransaction = pendingTxsPool.getTx(nextTick, i); + if (pendingTransaction) { - ACQUIRE(entityPendingTransactionsLock); - ASSERT(pendingTransaction->checkValidity()); auto* tsPendingTransactionOffsets = ts.tickTransactionOffsets.getByTickInCurrentEpoch(pendingTransaction->tick); - for (unsigned int j = 0; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) + + const m256i* digest = pendingTxsPool.getDigest(nextTick, i); + if (digest) { - if (unknownTransactions[j >> 6] & (1ULL << (j & 63))) + for (unsigned int j = 0; j < NUMBER_OF_TRANSACTIONS_PER_TICK; j++) { - if (&entityPendingTransactionDigests[i * 32ULL] == nextTickData.transactionDigests[j]) + if (unknownTransactions[j >> 6] & (1ULL << (j & 63))) { - ts.tickTransactions.acquireLock(); - // write tx to tick tx storage, no matter if tsNextTickTransactionOffsets[i] is 0 (new tx) - // or not (tx with digest that doesn't match tickData needs to be overwritten) + if (*digest == nextTickData.transactionDigests[j]) { - const unsigned int transactionSize = pendingTransaction->totalSize(); - if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) + ts.tickTransactions.acquireLock(); + // write tx to tick tx storage, no matter if tsNextTickTransactionOffsets[i] is 0 (new tx) + // or not (tx with digest that doesn't match tickData needs to be overwritten) { - tsPendingTransactionOffsets[j] = ts.nextTickTransactionOffset; - copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), pendingTransaction, transactionSize); - ts.nextTickTransactionOffset += transactionSize; + const unsigned int transactionSize = pendingTransaction->totalSize(); + if (ts.nextTickTransactionOffset + transactionSize <= ts.tickTransactions.storageSpaceCurrentEpoch) + { + tsPendingTransactionOffsets[j] = ts.nextTickTransactionOffset; + copyMem(ts.tickTransactions(ts.nextTickTransactionOffset), pendingTransaction, transactionSize); + ts.nextTickTransactionOffset += transactionSize; - numberOfKnownNextTickTransactions++; + numberOfKnownNextTickTransactions++; + } } - } - ts.tickTransactions.releaseLock(); + ts.tickTransactions.releaseLock(); - unknownTransactions[j >> 6] &= ~(1ULL << (j & 63)); + unknownTransactions[j >> 6] &= ~(1ULL << (j & 63)); - break; + break; + } } } } - - RELEASE(entityPendingTransactionsLock); } } + pendingTxsPool.releaseLock(); // At this point unknownTransactions is set to 1 for all transactions that are unknown // Update requestedTickTransactions the list of txs that not exist in memory so the MAIN loop can try to fetch them from peers @@ -4532,7 +4253,8 @@ static void prepareNextTickTransactions() // As processNextTickTransactions returns tx for which the flag ist set to 0 (tx with flag set to 1 are not returned) // We check if the last tickTransactionRequest it already sent - if(requestedTickTransactions.requestedTickTransactions.tick == 0){ + if (requestedTickTransactions.requestedTickTransactions.tick == 0) + { // Initialize transactionFlags to one so that by default we do not request any transaction setMem(requestedTickTransactions.requestedTickTransactions.transactionFlags, sizeof(requestedTickTransactions.requestedTickTransactions.transactionFlags), 0xff); for (unsigned int i = 0; i < NUMBER_OF_TRANSACTIONS_PER_TICK; i++) @@ -5136,8 +4858,7 @@ static void tickProcessor(void*) if (tickDataSuits) { const int dayIndex = ::dayIndex(etalonTick.year, etalonTick.month, etalonTick.day); - if ((dayIndex == 738570 + system.epoch * 7 && etalonTick.hour >= 12) - || dayIndex > 738570 + system.epoch * 7) + if (system.tick - system.initialTick >= TESTNET_EPOCH_DURATION) { // start seamless epoch transition epochTransitionState = 1; @@ -5206,25 +4927,9 @@ static void tickProcessor(void*) system.tick++; updateNumberOfTickTransactions(); + pendingTxsPool.incrementFirstStoredTick(); - short tickEpoch = 0; - TimeDate currentTickDate; - ts.tickData.acquireLock(); - const TickData& td = ts.tickData[currentTickIndex]; - currentTickDate.millisecond = td.millisecond; - currentTickDate.second = td.second; - currentTickDate.minute = td.minute; - currentTickDate.hour = td.hour; - currentTickDate.day = td.day; - currentTickDate.month = td.month; - currentTickDate.year = td.year; - tickEpoch = td.epoch == system.epoch ? system.epoch : 0; - ts.tickData.releaseLock(); - - checkAndSwitchMiningPhase(tickEpoch, currentTickDate); - - checkAndSwitchCustomMiningPhase(tickEpoch, currentTickDate); - + bool isBeginEpoch = false; if (epochTransitionState == 1) { @@ -5243,7 +4948,7 @@ static void tickProcessor(void*) epochTransitionState = 2; beginEpoch(); - checkMiningPhaseBeginAndEndEpoch(); + isBeginEpoch = true; // Some debug checks that we are ready for the next epoch ASSERT(system.numberOfSolutions == 0); @@ -5275,6 +4980,22 @@ static void tickProcessor(void*) } ASSERT(epochTransitionWaitingRequestProcessors >= 0 && epochTransitionWaitingRequestProcessors <= nRequestProcessorIDs); + short tickEpoch = 0; + TimeDate currentTickDate; + ts.tickData.acquireLock(); + const TickData& td = ts.tickData[currentTickIndex]; + currentTickDate.millisecond = td.millisecond; + currentTickDate.second = td.second; + currentTickDate.minute = td.minute; + currentTickDate.hour = td.hour; + currentTickDate.day = td.day; + currentTickDate.month = td.month; + currentTickDate.year = td.year; + tickEpoch = td.epoch == system.epoch ? system.epoch : 0; + ts.tickData.releaseLock(); + + checkAndSwitchMiningPhase(tickEpoch, currentTickDate, isBeginEpoch); + gTickNumberOfComputors = 0; gTickTotalNumberOfComputors = 0; targetNextTickDataDigestIsKnown = false; @@ -5336,18 +5057,22 @@ static bool loadComputer(CHAR16* directory, bool forceLoadFromFile) logToConsole(L"Loading contract files ..."); for (unsigned int contractIndex = 0; contractIndex < contractCount; contractIndex++) { + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 9] = contractIndex / 1000 + L'0'; + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 8] = (contractIndex % 1000) / 100 + L'0'; + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 7] = (contractIndex % 100) / 10 + L'0'; + CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 6] = contractIndex % 10 + L'0'; if (contractDescriptions[contractIndex].constructionEpoch == system.epoch && !forceLoadFromFile) { + setText(message, L" -> "); + appendText(message, CONTRACT_FILE_NAME); setMem(contractStates[contractIndex], contractDescriptions[contractIndex].stateSize, 0); + appendText(message, L" not loaded but initialized with zeros for construction"); + logToConsole(message); } else { - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 9] = contractIndex / 1000 + L'0'; - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 8] = (contractIndex % 1000) / 100 + L'0'; - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 7] = (contractIndex % 100) / 10 + L'0'; - CONTRACT_FILE_NAME[sizeof(CONTRACT_FILE_NAME) / sizeof(CONTRACT_FILE_NAME[0]) - 6] = contractIndex % 10 + L'0'; long long loadedSize = load(CONTRACT_FILE_NAME, contractDescriptions[contractIndex].stateSize, contractStates[contractIndex], directory); - setText(message, L" -> "); + setText(message, L" -> "); // set the message after loading otherwise `message` will contain potential messages from load() appendText(message, CONTRACT_FILE_NAME); if (loadedSize != contractDescriptions[contractIndex].stateSize) { @@ -5486,22 +5211,12 @@ static bool initialize() { if (!ts.init()) return false; - if (!allocPoolWithErrorLog(L"entityPendingTransaction buffer", SPECTRUM_CAPACITY * MAX_TRANSACTION_SIZE,(void**)&entityPendingTransactions, __LINE__) || - !allocPoolWithErrorLog(L"entityPendingTransaction buffer", SPECTRUM_CAPACITY * 32ULL,(void**)&entityPendingTransactionDigests , __LINE__)) - { - return false; - } - if (!allocPoolWithErrorLog(L"computorPendingTransactions buffer", NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR * MAX_TRANSACTION_SIZE, (void**)&computorPendingTransactions, __LINE__) || - !allocPoolWithErrorLog(L"computorPendingTransactions buffer", NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR * 32ULL, (void**)&computorPendingTransactionDigests, __LINE__)) - { - return false; - } - + if (!pendingTxsPool.init()) + return false; setMem(spectrumChangeFlags, sizeof(spectrumChangeFlags), 0); - if (!initSpectrum()) return false; @@ -5527,6 +5242,12 @@ static bool initialize() } setMem(score, sizeof(*score), 0); + if (!allocPoolWithErrorLog(L"score", sizeof(*score_qpi), (void**)&score_qpi, __LINE__)) + { + return false; + } + setMem(score_qpi, sizeof(*score_qpi), 0); + setMem(solutionThreshold, sizeof(int) * MAX_NUMBER_EPOCH, 0); if (!allocPoolWithErrorLog(L"minserSolutionFlag", NUMBER_OF_MINER_SOLUTION_FLAGS / 8, (void**)&minerSolutionFlags, __LINE__)) { @@ -5682,7 +5403,10 @@ static bool initialize() } else { - checkMiningPhaseBeginAndEndEpoch(); + short tickEpoch = -1; + TimeDate tickDate; + setMem((void*)&tickDate, sizeof(TimeDate), 0); + checkAndSwitchMiningPhase(tickEpoch, tickDate, true); } score->loadScoreCache(system.epoch); @@ -5784,8 +5508,18 @@ static bool initialize() emptyTickResolver.lastTryClock = 0; // Convert time parameters for full custom mining time - gFullExternalStartTime = convertWeekTimeFromPackedData(FULL_EXTERNAL_COMPUTATIONS_TIME_START_TIME); - gFullExternalEndTime = convertWeekTimeFromPackedData(FULL_EXTERNAL_COMPUTATIONS_TIME_STOP_TIME); + if (gNumberOfFullExternalMiningEvents > 0) + { + if ((!allocPoolWithErrorLog(L"gFullExternalEventTime", gNumberOfFullExternalMiningEvents * sizeof(FullExternallEvent), (void**)&gFullExternalEventTime, __LINE__))) + { + return false; + } + for (int i = 0; i < gNumberOfFullExternalMiningEvents; i++) + { + gFullExternalEventTime[i].startTime = convertWeekTimeFromPackedData(gFullExternalComputationTimes[i][0]); + gFullExternalEventTime[i].endTime = convertWeekTimeFromPackedData(gFullExternalComputationTimes[i][1]); + } + } return true; } @@ -5822,24 +5556,10 @@ static void deinitialize() } } - if (computorPendingTransactionDigests) - { - freePool(computorPendingTransactionDigests); - } - if (computorPendingTransactions) - { - freePool(computorPendingTransactions); - } - if (entityPendingTransactionDigests) - { - freePool(entityPendingTransactionDigests); - } - if (entityPendingTransactions) - { - freePool(entityPendingTransactions); - } ts.deinit(); + pendingTxsPool.deinit(); + if (score) { freePool(score); @@ -6012,38 +5732,12 @@ static void logInfo() } else { - const CHAR16 alphabet[26][2] = { L"A", L"B", L"C", L"D", L"E", L"F", L"G", L"H", L"I", L"J", L"K", L"L", L"M", L"N", L"O", L"P", L"Q", L"R", L"S", L"T", L"U", L"V", L"W", L"X", L"Y", L"Z" }; - for (unsigned int i = 0; i < numberOfOwnComputorIndices; i++) - { - appendText(message, alphabet[ownComputorIndices[i] / 26]); - appendText(message, alphabet[ownComputorIndices[i] % 26]); - if (i < (unsigned int)(numberOfOwnComputorIndices - 1)) - { - appendText(message, L"+"); - } - else - { - appendText(message, L"."); - } - } + appendText(message, L"[Owning "); + appendNumber(message, numberOfOwnComputorIndices, false); + appendText(message, L" indices]"); } logToConsole(message); - unsigned int numberOfPendingTransactions = 0; - for (unsigned int i = 0; i < NUMBER_OF_COMPUTORS * MAX_NUMBER_OF_PENDING_TRANSACTIONS_PER_COMPUTOR; i++) - { - if (((Transaction*)&computorPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick > system.tick) - { - numberOfPendingTransactions++; - } - } - for (unsigned int i = 0; i < SPECTRUM_CAPACITY; i++) - { - if (((Transaction*)&entityPendingTransactions[i * MAX_TRANSACTION_SIZE])->tick > system.tick) - { - numberOfPendingTransactions++; - } - } if (nextTickTransactionsSemaphore) { setText(message, L"?"); @@ -6089,7 +5783,7 @@ static void logInfo() appendNumber(message, td.millisecond % 10, FALSE); appendText(message, L".) "); } - appendNumber(message, numberOfPendingTransactions, TRUE); + appendNumber(message, pendingTxsPool.getTotalNumberOfPendingTxs(system.tick), TRUE); appendText(message, L" pending transactions."); logToConsole(message); @@ -6925,7 +6619,8 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) // prepare and send ExchangePublicPeers message ExchangePublicPeers* request = (ExchangePublicPeers*)&peers[i].dataToTransmit[sizeof(RequestResponseHeader)]; bool noVerifiedPublicPeers = true; - for (unsigned int k = 0; k < numberOfPublicPeers; k++) + // Only check non-private peers for handshake status + for (unsigned int k = NUMBER_OF_PRIVATE_IP; k < numberOfPublicPeers; k++) { if (publicPeers[k].isHandshaked /*&& publicPeers[k].isFullnode*/) { @@ -6943,15 +6638,24 @@ EFI_STATUS efi_main(EFI_HANDLE imageHandle, EFI_SYSTEM_TABLE* systemTable) } else { - // randomly select verified public peers - const unsigned int publicPeerIndex = random(numberOfPublicPeers); - if (publicPeers[publicPeerIndex].isHandshaked /*&& publicPeers[publicPeerIndex].isFullnode*/) + if (NUMBER_OF_PRIVATE_IP < numberOfPublicPeers) { - request->peers[j] = publicPeers[publicPeerIndex].address; + // randomly select verified public peers and discard private IPs + // first NUMBER_OF_PRIVATE_IP ips are same on both array publicPeers and knownPublicPeers + const unsigned int publicPeerIndex = NUMBER_OF_PRIVATE_IP + random(numberOfPublicPeers - NUMBER_OF_PRIVATE_IP); + // share the peer if it's not our private IPs and is handshaked + if (publicPeers[publicPeerIndex].isHandshaked) + { + request->peers[j] = publicPeers[publicPeerIndex].address; + } + else + { + j--; + } } else { - j--; + request->peers[j].u32 = 0; } } } diff --git a/src/score.h b/src/score.h index 0b027cff9..0921cfbf2 100644 --- a/src/score.h +++ b/src/score.h @@ -24,9 +24,6 @@ static unsigned long long top_of_stack; ////////// Scoring algorithm \\\\\\\\\\ -#define NOT_CALCULATED -127 //not yet calculated -#define NULL_INDEX -2 - constexpr unsigned char INPUT_NEURON_TYPE = 0; constexpr unsigned char OUTPUT_NEURON_TYPE = 1; constexpr unsigned char EVOLUTION_NEURON_TYPE = 2; @@ -36,30 +33,29 @@ static_assert(false, "Either AVX2 or AVX512 is required."); #endif #if defined (__AVX512F__) - static constexpr int BATCH_SIZE = 64; - static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; - static inline int popcnt512(__m512i v) - { - __m512i pc = _mm512_popcnt_epi64(v); - return (int)_mm512_reduce_add_epi64(pc); - } +static constexpr int BATCH_SIZE = 64; +static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; +static inline int popcnt512(__m512i v) +{ + __m512i pc = _mm512_popcnt_epi64(v); + return (int)_mm512_reduce_add_epi64(pc); +} #elif defined(__AVX2__) - static constexpr int BATCH_SIZE = 32; - static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; - static inline unsigned popcnt256(__m256i v) - { - return popcnt64(_mm256_extract_epi64(v, 0)) + - popcnt64(_mm256_extract_epi64(v, 1)) + - popcnt64(_mm256_extract_epi64(v, 2)) + - popcnt64(_mm256_extract_epi64(v, 3)); - } +static constexpr int BATCH_SIZE = 32; +static constexpr int BATCH_SIZE_X8 = BATCH_SIZE * 8; +static inline unsigned popcnt256(__m256i v) +{ + return popcnt64(_mm256_extract_epi64(v, 0)) + + popcnt64(_mm256_extract_epi64(v, 1)) + + popcnt64(_mm256_extract_epi64(v, 2)) + + popcnt64(_mm256_extract_epi64(v, 3)); +} #endif constexpr unsigned long long POOL_VEC_SIZE = (((1ULL << 32) + 64)) >> 3; // 2^32+64 bits ~ 512MB constexpr unsigned long long POOL_VEC_PADDING_SIZE = (POOL_VEC_SIZE + 200 - 1) / 200 * 200; // padding for multiple of 200 constexpr unsigned long long STATE_SIZE = 200; -static const char gLUT3States[] = { 0, 1, -1 }; static void generateRandom2Pool(const unsigned char* miningSeed, unsigned char* state, unsigned char* pool) { @@ -143,50 +139,6 @@ static void extract64Bits(unsigned long long number, char* output) } } -static void packNegPos(const char* data, - unsigned long long dataSize, - unsigned char* negMask, - unsigned char* posMask) -{ - for (unsigned long long i = 0; i < dataSize; ++i) - { - negMask[i] = (data[i] == -1); - posMask[i] = (data[i] == 1); - } -} - -static void unpackNegPos( - const unsigned char* negMask, - const unsigned char* posMask, - const unsigned long long dataSize, - char* data) -{ - for (unsigned long long i = 0; i < dataSize; ++i) - { - data[i] = 0; - if (negMask[i]) - { - data[i] = -1; - continue; - } - if (posMask[i]) - { - data[i] = 1; - continue; - } - } -} - -static char convertMaskValue(unsigned char pos, unsigned char neg) -{ - /* - value = +1 if pos=1 , neg=0 - value = -1 if pos=0 , neg=1 - value = 0 otherwise - */ - return (char)(pos - neg); -} - static void setBitValue(unsigned char* data, unsigned long long bitIdx, unsigned char bitValue) { // (data[bitIdx >> 3] & ~(1u << (bitIdx & 7u))). Set the bit at data[bitIdx >> 3] byte become zeros @@ -203,14 +155,6 @@ static unsigned char getBitValue(const unsigned char* data, unsigned long long return ((data[bitIdx >> 3] >> (bitIdx & 7u)) & 1u); } -static char getValueFromBit(const unsigned char* negMask, const unsigned char* posMask, unsigned long long bitIdx) -{ - unsigned long long idx = bitIdx; /* bit index */ - unsigned char negBit = (negMask[idx >> 3] >> (idx & 7)) & 1u; - unsigned char posBit = (posMask[idx >> 3] >> (idx & 7)) & 1u; - return convertMaskValue(posBit, negBit); -} - template static void paddingDatabits( unsigned char* data, @@ -281,7 +225,7 @@ static void packNegPosWithPadding(const char* data, const __m512i vMinus1 = _mm512_set1_epi8(-1); const __m512i vPlus1 = _mm512_set1_epi8(+1); unsigned long long k = 0; - for (; k + BATCH_SIZE < dataSizeInBits; k += BATCH_SIZE) + for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) { __m512i v = _mm512_loadu_si512(reinterpret_cast(data + k)); __mmask64 mNeg = _mm512_cmpeq_epi8_mask(v, vMinus1); @@ -300,7 +244,7 @@ static void packNegPosWithPadding(const char* data, const __m256i vMinus1 = _mm256_set1_epi8(-1); const __m256i vPlus1 = _mm256_set1_epi8(+1); unsigned long long k = 0; - for (; k + BATCH_SIZE < dataSizeInBits; k += BATCH_SIZE) + for (; k + BATCH_SIZE <= dataSizeInBits; k += BATCH_SIZE) { __m256i v = _mm256_loadu_si256(reinterpret_cast(data + k)); @@ -340,22 +284,6 @@ static void packNegPosWithPadding(const char* data, } } -void unpackNegPosBits(const unsigned char* negMask, - const unsigned char* posMask, - unsigned long long dataSize, - unsigned long long paddedSize, - char* out) -{ - const unsigned long long startBit = paddedSize; /* first real */ - for (unsigned long long i = 0; i < dataSize; ++i) - { - unsigned long long idx = startBit + i; /* bit index */ - unsigned char negBit = (negMask[idx >> 3] >> (idx & 7)) & 1u; - unsigned char posBit = (posMask[idx >> 3] >> (idx & 7)) & 1u; - out[i] = convertMaskValue(posBit, negBit); - } -} - // Load 256/512 values start from a bit index into a m512 or m256 register #if defined (__AVX512F__) static inline __m512i load512Bits(const unsigned char* array, unsigned long long bitLocation) @@ -377,7 +305,7 @@ static inline __m512i load512Bits(const unsigned char* array, unsigned long long static inline __m256i load256Bits(const unsigned char* array, unsigned long long bitLocation) { const unsigned long long byteIndex = bitLocation >> 3; - const unsigned long long bitOffset = bitLocation & 7ULL; + const int bitOffset = (int)(bitLocation & 7ULL); // Load a 256-bit (32-byte) vector starting at the byte index. const __m256i v = _mm256_loadu_si256(reinterpret_cast(array + byteIndex)); @@ -465,7 +393,7 @@ struct ScoreFunction typedef char Neuron; typedef unsigned char NeuronType; - + // Data for roll back struct ANN { @@ -875,53 +803,158 @@ struct ScoreFunction { unsigned long long population = currentANN.population; - // Prepare the padding regions - currentANN.prepareData(); - // Test copy padding unsigned char* pPaddingNeuronMinus = currentANN.neuronMinus1s; unsigned char* pPaddingNeuronPlus = currentANN.neuronPlus1s; - unsigned char* pPaddingSynapseMinus = currentANN.synapseMinus1s; unsigned char* pPaddingSynapsePlus = currentANN.synapsePlus1s; paddingDatabits(pPaddingNeuronMinus, population); paddingDatabits(pPaddingNeuronPlus, population); + #if defined (__AVX512F__) - const unsigned long long chunks = incommingSynapsesPitch >> 9; /* bits/512 */ -#else - const unsigned long long chunks = incommingSynapsesPitch >> 8; /* bits/256 */ -#endif - for (unsigned long long n = 0; n < population; ++n, pPaddingSynapsePlus += incommingSynapseBatchSize, pPaddingSynapseMinus += incommingSynapseBatchSize) + constexpr unsigned long long chunks = incommingSynapsesPitch >> 9; + __m512i minusBlock[chunks]; + __m512i minusNext[chunks]; + __m512i plusBlock[chunks]; + __m512i plusNext[chunks]; + + constexpr unsigned long long blockSizeNeurons = 64ULL; + constexpr unsigned long long bytesPerWord = 8ULL; + + unsigned long long n = 0; + const unsigned long long lastBlock = (population / blockSizeNeurons) * blockSizeNeurons; + for (; n < lastBlock; n += blockSizeNeurons) + { + // byteIndex = start byte for word containing neuron n + unsigned long long byteIndex = ((n >> 6) << 3); // (n / 64) * 8 + unsigned long long curIdx = byteIndex; + unsigned long long nextIdx = byteIndex + bytesPerWord; // +8 bytes + + // Load the neuron windows once per block for all chunks + unsigned long long loadCur = curIdx; + unsigned long long loadNext = nextIdx; + for (unsigned blk = 0; blk < chunks; ++blk, loadCur += BATCH_SIZE, loadNext += BATCH_SIZE) + { + plusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadCur)); + plusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + loadNext)); + minusBlock[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadCur)); + minusNext[blk] = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + loadNext)); + } + + __m512i sh = _mm512_setzero_si512(); + __m512i sh64 = _mm512_set1_epi64(64); + const __m512i ones512 = _mm512_set1_epi64(1); + + // For each neuron inside this 64-neuron block + for (unsigned int lane = 0; lane < 64; ++lane) + { + const unsigned long long current_n = n + lane; + // synapse pointers for this neuron + unsigned char* pSynapsePlus = pPaddingSynapsePlus + current_n * incommingSynapseBatchSize; + unsigned char* pSynapseMinus = pPaddingSynapseMinus + current_n * incommingSynapseBatchSize; + + __m512i plusPopulation = _mm512_setzero_si512(); + __m512i minusPopulation = _mm512_setzero_si512(); + + for (unsigned blk = 0; blk < chunks; ++blk) + { + const __m512i synP = _mm512_loadu_si512((const void*)(pSynapsePlus + blk * BATCH_SIZE)); + const __m512i synM = _mm512_loadu_si512((const void*)(pSynapseMinus + blk * BATCH_SIZE)); + + // stitch 64-bit lanes: cur >> s | next << (64 - s) + __m512i neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(plusBlock[blk], sh), _mm512_sllv_epi64(plusNext[blk], sh64)); + __m512i neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(minusBlock[blk], sh), _mm512_sllv_epi64(minusNext[blk], sh64)); + + __m512i tmpP = _mm512_and_si512(neuronMinus, synM); + const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synP, tmpP, 234); + + __m512i tmpM = _mm512_and_si512(neuronMinus, synP); + const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synM, tmpM, 234); + + plusPopulation = _mm512_add_epi64(plusPopulation, _mm512_popcnt_epi64(plus)); + minusPopulation = _mm512_add_epi64(minusPopulation, _mm512_popcnt_epi64(minus)); + } + sh = _mm512_add_epi64(sh, ones512); + sh64 = _mm512_sub_epi64(sh64, ones512); + + // Reduce to scalar and compute neuron value + int score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + char neuronValue = (score > 0) - (score < 0); + neuronValueBuffer[current_n] = neuronValue; + + // Update the neuron positive and negative bitmaps + unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; + unsigned char nNextPos = neuronValue > 0 ? 1 : 0; + setBitValue(currentANN.nextneuronMinus1s, current_n + radius, nNextNeg); + setBitValue(currentANN.nextNeuronPlus1s, current_n + radius, nNextPos); + } + } + + for (; n < population; ++n) { char neuronValue = 0; int score = 0; - int synapseBlkIdx = 0; // blk index of synapse - int neuronBlkIdx = 0; - for (unsigned blk = 0; blk < chunks; ++blk, synapseBlkIdx += BATCH_SIZE, neuronBlkIdx +=BATCH_SIZE_X8) + unsigned char* pSynapsePlus = pPaddingSynapsePlus + n * incommingSynapseBatchSize; + unsigned char* pSynapseMinus = pPaddingSynapseMinus + n * incommingSynapseBatchSize; + + const unsigned long long byteIndex = n >> 3; + const unsigned int bitOffset = (n & 7U); + const unsigned int bitOffset_8 = (8u - bitOffset); + __m512i sh = _mm512_set1_epi64((long long)bitOffset); + __m512i sh8 = _mm512_set1_epi64((long long)bitOffset_8); + + __m512i plusPopulation = _mm512_setzero_si512(); + __m512i minusPopulation = _mm512_setzero_si512(); + + for (unsigned blk = 0; blk < chunks; ++blk, pSynapsePlus += BATCH_SIZE, pSynapseMinus += BATCH_SIZE) { -#if defined (__AVX512F__) - const __m512i synapsePlus = _mm512_loadu_si512((const void*)(pPaddingSynapsePlus + synapseBlkIdx)); - const __m512i synapseMinus = _mm512_loadu_si512((const void*)(pPaddingSynapseMinus + synapseBlkIdx)); + const __m512i synapsePlus = _mm512_loadu_si512((const void*)(pSynapsePlus)); + const __m512i synapseMinus = _mm512_loadu_si512((const void*)(pSynapseMinus)); - __m512i neuronPlus = load512Bits(pPaddingNeuronPlus, n + neuronBlkIdx); - __m512i neuronMinus = load512Bits(pPaddingNeuronMinus, n + neuronBlkIdx); + __m512i neuronPlus = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE)); + __m512i neuronPlusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronPlus + byteIndex + blk * BATCH_SIZE + 1)); + __m512i neuronMinus = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE)); + __m512i neuronMinusNext = _mm512_loadu_si512((const void*)(pPaddingNeuronMinus + byteIndex + blk * BATCH_SIZE + 1)); - //__m512i plus = _mm512_or_si512(_mm512_and_si512(neuronPlus, synapsePlus), - // _mm512_and_si512(neuronMinus, synapseMinus)); + neuronPlus = _mm512_or_si512(_mm512_srlv_epi64(neuronPlus, sh), _mm512_sllv_epi64(neuronPlusNext, sh8)); + neuronMinus = _mm512_or_si512(_mm512_srlv_epi64(neuronMinus, sh), _mm512_sllv_epi64(neuronMinusNext, sh8)); - //__m512i minus = _mm512_or_si512(_mm512_and_si512(neuronPlus, synapseMinus), - // _mm512_and_si512(neuronMinus, synapsePlus)); + __m512i tempP = _mm512_and_si512(neuronMinus, synapseMinus); + const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synapsePlus, tempP, 234); - const __m512i plus = _mm512_ternarylogic_epi64(neuronPlus, synapsePlus, _mm512_and_si512(neuronMinus, synapseMinus), 234); - const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synapseMinus, _mm512_and_si512(neuronMinus, synapsePlus), 234); + __m512i tempM = _mm512_and_si512(neuronMinus, synapsePlus); + const __m512i minus = _mm512_ternarylogic_epi64(neuronPlus, synapseMinus, tempM, 234); - const __m512i plusPopulation = _mm512_popcnt_epi64(plus); - const __m512i minusPopulation = _mm512_popcnt_epi64(minus); - score += (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + tempP = _mm512_popcnt_epi64(plus); + tempM = _mm512_popcnt_epi64(minus); + plusPopulation = _mm512_add_epi64(tempP, plusPopulation); + minusPopulation = _mm512_add_epi64(tempM, minusPopulation); + } + score = (int)_mm512_reduce_add_epi64(_mm512_sub_epi64(plusPopulation, minusPopulation)); + neuronValue = (score > 0) - (score < 0); + neuronValueBuffer[n] = neuronValue; + + unsigned char nNextNeg = neuronValue < 0 ? 1 : 0; + unsigned char nNextPos = neuronValue > 0 ? 1 : 0; + setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); + setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); + } #else + constexpr unsigned long long chunks = incommingSynapsesPitch >> 8; + for (unsigned long long n = 0; n < population; ++n, pPaddingSynapsePlus += incommingSynapseBatchSize, pPaddingSynapseMinus += incommingSynapseBatchSize) + { + char neuronValue = 0; + int score = 0; + unsigned char* pSynapsePlus = pPaddingSynapsePlus; + unsigned char* pSynapseMinus = pPaddingSynapseMinus; + + int synapseBlkIdx = 0; // blk index of synapse + int neuronBlkIdx = 0; + for (unsigned blk = 0; blk < chunks; ++blk, synapseBlkIdx += BATCH_SIZE, neuronBlkIdx += BATCH_SIZE_X8) + { // Process 256bits at once, neigbor shilf 64 bytes = 256 bits const __m256i synapsePlus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapsePlus + synapseBlkIdx)); const __m256i synapseMinus = _mm256_loadu_si256((const __m256i*)(pPaddingSynapseMinus + synapseBlkIdx)); @@ -936,7 +969,6 @@ struct ScoreFunction _mm256_and_si256(neuronMinus, synapsePlus)); score += popcnt256(plus) - popcnt256(minus); -#endif } neuronValue = (score > 0) - (score < 0); @@ -947,8 +979,11 @@ struct ScoreFunction unsigned char nNextPos = neuronValue > 0 ? 1 : 0; setBitValue(currentANN.nextneuronMinus1s, n + radius, nNextNeg); setBitValue(currentANN.nextNeuronPlus1s, n + radius, nNextPos); - } +#endif + + + copyMem(currentANN.neurons, neuronValueBuffer, population * sizeof(Neuron)); copyMem(currentANN.neuronMinus1s, currentANN.nextneuronMinus1s, sizeof(currentANN.neuronMinus1s)); copyMem(currentANN.neuronPlus1s, currentANN.nextNeuronPlus1s, sizeof(currentANN.neuronPlus1s)); @@ -963,16 +998,8 @@ struct ScoreFunction // Save the neuron value for comparison copyMem(previousNeuronValue, neurons, population * sizeof(Neuron)); - - const __m512i vPop = _mm512_set1_epi64(population); - const __m512i vPitch = _mm512_set1_epi64(static_cast(incommingSynapsesPitch)); - const __m512i vStrideL = _mm512_set1_epi64(incommingSynapsesPitch - 1); - const __m512i vStrideR = _mm512_set1_epi64(incommingSynapsesPitch + 1); - const __m512i vNeighbor = _mm512_set1_epi64(static_cast(numberOfNeighbors)); - - const __m512i lane01234567 = _mm512_set_epi64(7, 6, 5, 4, 3, 2, 1, 0); // constant - long long test[8] = { 0 }; { + //PROFILE_NAMED_SCOPE("convertSynapse"); // Compute the incomming synapse of each neurons setMem(paddingIncommingSynapses, sizeof(paddingIncommingSynapses), 0); for (unsigned long long n = 0; n < population; ++n) @@ -1000,6 +1027,7 @@ struct ScoreFunction // Prepare masks { + //PROFILE_NAMED_SCOPE("prepareMask"); packNegPosWithPadding(currentANN.neurons, population, radius, @@ -1014,6 +1042,7 @@ struct ScoreFunction } { + //PROFILE_NAMED_SCOPE("processTickLoop"); for (unsigned long long tick = 0; tick < numberOfTicks; ++tick) { processTick(); @@ -1021,25 +1050,9 @@ struct ScoreFunction // - N ticks have passed (already in for loop) // - All neuron values are unchanged // - All output neurons have non-zero values - bool shouldExit = true; - bool allNeuronsUnchanged = true; - bool allOutputNeuronsIsNonZeros = true; - for (unsigned long long n = 0; n < population; ++n) - { - // Neuron unchanged check - if (previousNeuronValue[n] != neurons[n]) - { - allNeuronsUnchanged = false; - } - - // Ouput neuron value check - if (neuronTypes[n] == OUTPUT_NEURON_TYPE && neurons[n] == 0) - { - allOutputNeuronsIsNonZeros = false; - } - } - if (allOutputNeuronsIsNonZeros || allNeuronsUnchanged) + if (areAllNeuronsUnchanged((const char*)previousNeuronValue, (const char*)neurons, population) + || areAllNeuronsZeros((const char*)neurons, (const char*)neuronTypes, population)) { break; } @@ -1050,6 +1063,111 @@ struct ScoreFunction } } + bool areAllNeuronsZeros( + const char* neurons, + const char* neuronTypes, + unsigned long long population) + { + +#if defined (__AVX512F__) + const __m512i zero = _mm512_setzero_si512(); + const __m512i typeOutput = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); + + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); + __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); + + __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutput); + __mmask64 zero_mask = _mm512_cmpeq_epi8_mask(cur, zero); + + if (type_mask & zero_mask) + return false; + } +#else + const __m256i zero = _mm256_setzero_si256(); + const __m256i typeOutput = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); + + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m256i cur = _mm256_loadu_si256((const __m256i*)(neurons + i)); + __m256i types = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); + + // Compare for type == OUTPUT + __m256i type_cmp = _mm256_cmpeq_epi8(types, typeOutput); + int type_mask = _mm256_movemask_epi8(type_cmp); + + // Compare for neuron == 0 + __m256i zero_cmp = _mm256_cmpeq_epi8(cur, zero); + int zero_mask = _mm256_movemask_epi8(zero_cmp); + + // If both masks overlap → some output neuron is zero + if (type_mask & zero_mask) + { + return false; + } + } + +#endif + for (; i < population; i++) + { + // Neuron unchanged check + if (neuronTypes[i] == OUTPUT_NEURON_TYPE && neurons[i] == 0) + { + return false; + } + } + + return true; + } + + bool areAllNeuronsUnchanged( + const char* previousNeuronValue, + const char* neurons, + unsigned long long population) + { + unsigned long long i = 0; + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + +#if defined (__AVX512F__) + __m512i prev = _mm512_loadu_si512((const void*)(previousNeuronValue + i)); + __m512i cur = _mm512_loadu_si512((const void*)(neurons + i)); + + __mmask64 neq_mask = _mm512_cmpneq_epi8_mask(prev, cur); + if (neq_mask) + { + return false; + } +#else + __m256i v_prev = _mm256_loadu_si256((const __m256i*)(previousNeuronValue + i)); + __m256i v_curr = _mm256_loadu_si256((const __m256i*)(neurons + i)); + __m256i cmp = _mm256_cmpeq_epi8(v_prev, v_curr); + + int mask = _mm256_movemask_epi8(cmp); + + // -1 means all bytes equal + if (mask != -1) + { + return false; + } +#endif + } + + for (; i < population; i++) + { + // Neuron unchanged check + if (previousNeuronValue[i] != neurons[i]) + { + return false; + } + } + + return true; + } + unsigned int computeNonMatchingOutput() { unsigned long long population = currentANN.population; @@ -1060,7 +1178,61 @@ struct ScoreFunction // Because the output neuron order never changes, the order is preserved unsigned int R = 0; unsigned long long outputIdx = 0; - for (unsigned long long i = 0; i < population; i++) + unsigned long long i = 0; +#if defined (__AVX512F__) + const __m512i typeOutputAVX = _mm512_set1_epi8(OUTPUT_NEURON_TYPE); + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + // Load 64 neuron types and compare with OUTPUT_NEURON_TYPE + __m512i types = _mm512_loadu_si512((const void*)(neuronTypes + i)); + __mmask64 type_mask = _mm512_cmpeq_epi8_mask(types, typeOutputAVX); + + if (type_mask == 0) + { + continue; // no output neurons in this 64-wide block, just skip + } + + // Output neuron existed in this block + for (int k = 0; k < BATCH_SIZE; ++k) + { + if (type_mask & (1ULL << k)) + { + char neuronVal = neurons[i + k]; + if (neuronVal != outputNeuronExpectedValue[outputIdx]) + { + R++; + } + outputIdx++; + } + } + } +#else + const __m256i typeOutputAVX = _mm256_set1_epi8(OUTPUT_NEURON_TYPE); + for (; i + BATCH_SIZE <= population; i += BATCH_SIZE) + { + __m256i types_vec = _mm256_loadu_si256((const __m256i*)(neuronTypes + i)); + __m256i cmp_vec = _mm256_cmpeq_epi8(types_vec, typeOutputAVX); + unsigned int type_mask = _mm256_movemask_epi8(cmp_vec); + + if (type_mask == 0) + { + continue; // no output neurons in this 32-wide block, just skip + } + for (int k = 0; k < BATCH_SIZE; ++k) + { + if (type_mask & (1U << k)) + { + char neuronVal = neurons[i + k]; + if (neuronVal != outputNeuronExpectedValue[outputIdx]) + R++; + outputIdx++; + } + } + } +#endif + + // remainder loop + for (; i < population; i++) { if (neuronTypes[i] == OUTPUT_NEURON_TYPE) { @@ -1071,6 +1243,7 @@ struct ScoreFunction outputIdx++; } } + return R; } @@ -1162,7 +1335,7 @@ struct ScoreFunction } void initializeRandom2( - const unsigned char* publicKey, + const unsigned char* publicKey, const unsigned char* nonce, const unsigned char* pRandom2Pool) { @@ -1205,9 +1378,9 @@ struct ScoreFunction unsigned char extractValue = (unsigned char)((initValue->synapseWeight[i] >> shiftVal) & mask); switch (extractValue) { - case 2: synapses[32 * i + j] = -1; break; - case 3: synapses[32 * i + j] = 1; break; - default: synapses[32 * i + j] = 0; + case 2: synapses[32 * i + j] = -1; break; + case 3: synapses[32 * i + j] = 1; break; + default: synapses[32 * i + j] = 0; } } } @@ -1239,7 +1412,7 @@ struct ScoreFunction { // Setup the random starting point initializeRandom2(publicKey, nonce, pRandom2Pool); - + // Initialize unsigned int bestR = initializeANN(); @@ -1275,13 +1448,51 @@ struct ScoreFunction currentANN.copyDataTo(bestANN); } - ASSERT(bestANN.population <= populationThreshold); + //ASSERT(bestANN.population <= populationThreshold); } unsigned int score = numberOfOutputNeurons - bestR; return score; } + // returns last computed output neurons, only returns 256 non-zero neurons, neuron values are compressed to bit + m256i getLastOutput() + { + unsigned long long population = bestANN.population; + Neuron* neurons = bestANN.neurons; + NeuronType* neuronTypes = bestANN.neuronTypes; + int count = 0; + int byteCount = 0; + uint8_t A = 0; + m256i result; + result = m256i::zero(); + + for (unsigned long long i = 0; i < population; i++) + { + if (neuronTypes[i] == OUTPUT_NEURON_TYPE) + { + if (neurons[i]) + { + uint8_t v = (neurons[i] > 0); + v = v << (7 - count); + A |= v; + if (++count == 8) + { + result.m256i_u8[byteCount++] = A; + A = 0; + count = 0; + if (byteCount >= 32) + { + break; + } + } + } + } + } + + return result; + } + } _computeBuffer[solutionBufferCount]; m256i currentRandomSeed; @@ -1378,6 +1589,15 @@ struct ScoreFunction return _computeBuffer[solutionBufIdx].computeScore(publicKey.m256i_u8, nonce.m256i_u8, poolVec); } + m256i getLastOutput(const unsigned long long processor_Number) + { + ACQUIRE(solutionEngineLock[processor_Number]); + + m256i result = _computeBuffer[processor_Number].getLastOutput(); + + RELEASE(solutionEngineLock[processor_Number]); + return result; + } // main score function unsigned int operator()(const unsigned long long processor_Number, const m256i& publicKey, const m256i& miningSeed, const m256i& nonce) { @@ -1523,5 +1743,3 @@ struct ScoreFunction } } }; - - diff --git a/src/system.h b/src/system.h index 4e5ebfc15..e4bbc3a57 100644 --- a/src/system.h +++ b/src/system.h @@ -15,7 +15,8 @@ struct System unsigned short epoch; unsigned int tick; unsigned int initialTick; - unsigned int latestCreatedTick, latestLedTick; + unsigned int latestCreatedTick; + unsigned int latestLedTick; // contains latest tick t in which TickData for tick (t + TICK_TRANSACTIONS_PUBLICATION_OFFSET) was broadcasted as tick leader unsigned short initialMillisecond; unsigned char initialSecond; diff --git a/src/ticking/pending_txs_pool.h b/src/ticking/pending_txs_pool.h new file mode 100644 index 000000000..87ccb1fae --- /dev/null +++ b/src/ticking/pending_txs_pool.h @@ -0,0 +1,534 @@ +#pragma once + +#include "network_messages/transactions.h" + +#include "platform/memory_util.h" +#include "platform/concurrency.h" +#include "platform/console_logging.h" + +#include "spectrum/spectrum.h" + +#include "mining/mining.h" + +#include "contracts/qpi.h" +#include "contracts/math_lib.h" +#include "contract_core/qpi_collection_impl.h" + +#include "public_settings.h" +#include "kangaroo_twelve.h" +#include "vote_counter.h" + +// Mempool that saves pending transactions (txs) of all entities. +// This is a kind of singleton class with only static members (so all instances refer to the same data). +class PendingTxsPool +{ +protected: + // The PendingTxsPool will always leave space for the two protocol-level txs (tick votes and custom mining). + static constexpr unsigned int maxNumTxsPerTick = NUMBER_OF_TRANSACTIONS_PER_TICK - 2; + static constexpr unsigned long long maxNumTxsTotal = PENDING_TXS_POOL_NUM_TICKS * maxNumTxsPerTick; + + // Sizes of different buffers in bytes + static constexpr unsigned long long tickTransactionsSize = maxNumTxsTotal * MAX_TRANSACTION_SIZE; + static constexpr unsigned long long txsDigestsSize = maxNumTxsTotal * sizeof(m256i); + + // `maxNumTxsTotal` priorities have to be saved at a time. Collection capacity has to be 2^N so find the next bigger power of 2. + static constexpr unsigned long long txsPrioritiesCapacity = math_lib::findNextPowerOf2(maxNumTxsTotal); + + // The pool stores the tick range [firstStoredTick, firstStoredTick + PENDING_TXS_POOL_NUM_TICKS[ + inline static unsigned int firstStoredTick = 0; + + // Allocated tickTransactions buffer with tickTransactionsSize bytes + inline static unsigned char* tickTransactionsBuffer = nullptr; + + // Allocated txsDigests buffer with maxNumTxs elements + inline static m256i* txsDigestsBuffer = nullptr; + + // Records the number of saved transactions for each tick + inline static unsigned int numSavedTxsPerTick[PENDING_TXS_POOL_NUM_TICKS]; + + // Begin index for tickTransactionOffsetsBuffer, txsDigestsBuffer, and numSavedTxsPerTick + // buffersBeginIndex corresponds to firstStoredTick + inline static unsigned int buffersBeginIndex = 0; + + // Lock for securing the data in the PendingTxsPool + inline static volatile char lock = 0; + + // Priority queues for transactions in each saved tick + inline static Collection* txsPriorities; + + static void cleanupTxsPriorities(unsigned int tickIndex) + { + sint64 elementIndex = txsPriorities->headIndex(m256i{ tickIndex, 0, 0, 0 }); + // use a `for` instead of a `while` loop to make sure it cannot run forever + // there can be at most `maxNumTxsPerTick` elements in one pov + for (unsigned int t = 0; t < maxNumTxsPerTick; ++t) + { + if (elementIndex != NULL_INDEX) + elementIndex = txsPriorities->remove(elementIndex); + else + break; + } + txsPriorities->cleanupIfNeeded(); + } + + static sint64 calculateTxPriority(const Transaction* tx) + { + sint64 priority = 0; + int sourceIndex = spectrumIndex(tx->sourcePublicKey); + if (sourceIndex >= 0) + { + sint64 balance = energy(sourceIndex); + if (balance > 0) + { + if (isZero(tx->destinationPublicKey) && tx->amount == 0LL + && (tx->inputType == VOTE_COUNTER_INPUT_TYPE || tx->inputType == CustomMiningSolutionTransaction::transactionType())) + { + // protocol-level tx always have max priority + return INT64_MAX; + } + else + { + // calculate tx priority as [balance of src] * [scheduledTick - latestOutgoingTransferTick + 1] + EntityRecord entity = spectrum[sourceIndex]; + priority = smul(balance, static_cast(tx->tick - entity.latestOutgoingTransferTick + 1)); + // decrease by 1 to make sure no normal tx reaches max priority + priority--; + } + } + } + return priority; + } + + // Return pointer to Transaction based on tickIndex and transactionIndex (checking offset with ASSERT) + inline static Transaction* getTxPtr(unsigned int tickIndex, unsigned int transactionIndex) + { + ASSERT(tickIndex < PENDING_TXS_POOL_NUM_TICKS); + ASSERT(transactionIndex < maxNumTxsPerTick); + return (Transaction*)(tickTransactionsBuffer + (tickIndex * maxNumTxsPerTick + transactionIndex) * MAX_TRANSACTION_SIZE); + } + + // Return pointer to transaction digest based on tickIndex and transactionIndex (checking offset with ASSERT) + inline static m256i* getDigestPtr(unsigned int tickIndex, unsigned int transactionIndex) + { + ASSERT(tickIndex < PENDING_TXS_POOL_NUM_TICKS); + ASSERT(transactionIndex < maxNumTxsPerTick); + return &txsDigestsBuffer[tickIndex * maxNumTxsPerTick + transactionIndex]; + } + + // Check whether tick is stored in the pending txs pool + inline static bool tickInStorage(unsigned int tick) + { + return tick >= firstStoredTick && tick < firstStoredTick + PENDING_TXS_POOL_NUM_TICKS; + } + + // Return index of tick data in current storage window (does not check tick). + inline static unsigned int tickToIndex(unsigned int tick) + { + return ((tick - firstStoredTick) + buffersBeginIndex) % PENDING_TXS_POOL_NUM_TICKS; + } + +public: + + // Init at node startup. + static bool init() + { + if (!allocPoolWithErrorLog(L"PendingTxsPool::tickTransactionsPtr ", tickTransactionsSize, (void**)&tickTransactionsBuffer, __LINE__) + || !allocPoolWithErrorLog(L"PendingTxsPool::txsDigestsPtr ", txsDigestsSize, (void**)&txsDigestsBuffer, __LINE__) + || !allocPoolWithErrorLog(L"PendingTxsPool::txsPriorities", sizeof(Collection), (void**)&txsPriorities, __LINE__)) + { + return false; + } + + ASSERT(lock == 0); + + setMem(tickTransactionsBuffer, tickTransactionsSize, 0); + setMem(txsDigestsBuffer, txsDigestsSize, 0); + setMem(numSavedTxsPerTick, sizeof(numSavedTxsPerTick), 0); + + txsPriorities->reset(); + + firstStoredTick = 0; + buffersBeginIndex = 0; + + return true; + } + + // Cleanup at node shutdown. + static void deinit() + { + if (tickTransactionsBuffer) + { + freePool(tickTransactionsBuffer); + } + if (txsDigestsBuffer) + { + freePool(txsDigestsBuffer); + } + if (txsPriorities) + { + freePool(txsPriorities); + } + } + + // Acquire lock for returned pointers to transactions or digests. + inline static void acquireLock() + { + ACQUIRE(lock); + } + + // Release lock for returned pointers to transactions or digests. + inline static void releaseLock() + { + RELEASE(lock); + } + + // Return number of transactions scheduled for the specified tick. + static unsigned int getNumberOfPendingTickTxs(unsigned int tick) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin pendingTxsPool.getNumberOfPendingTickTxs()"); +#endif + unsigned int res = 0; + ACQUIRE(lock); + if (tickInStorage(tick)) + { + res = numSavedTxsPerTick[tickToIndex(tick)]; + } + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + CHAR16 dbgMsgBuf[200]; + setText(dbgMsgBuf, L"End pendingTxsPool.getNumberOfPendingTickTxs() for tick="); + appendNumber(dbgMsgBuf, tick, FALSE); + appendText(dbgMsgBuf, L" -> res="); + appendNumber(dbgMsgBuf, res, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + return res; + } + + // Return number of transactions scheduled later than the specified tick. + static unsigned int getTotalNumberOfPendingTxs(unsigned int tick) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin pendingTxsPool.getTotalNumberOfPendingTxs()"); +#endif + unsigned int res = 0; + ACQUIRE(lock); + if (tickInStorage(tick + 1)) + { + unsigned int startIndex = tickToIndex(tick + 1); + + if (startIndex < buffersBeginIndex) + { + for (unsigned int t = startIndex; t < buffersBeginIndex; ++t) + res += numSavedTxsPerTick[t]; + } + else + { + for (unsigned int t = startIndex; t < PENDING_TXS_POOL_NUM_TICKS; ++t) + res += numSavedTxsPerTick[t]; + for (unsigned int t = 0; t < buffersBeginIndex; ++t) + res += numSavedTxsPerTick[t]; + } + } + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + CHAR16 dbgMsgBuf[200]; + setText(dbgMsgBuf, L"End pendingTxsPool.getTotalNumberOfPendingTxs() for tick="); + appendNumber(dbgMsgBuf, tick, FALSE); + appendText(dbgMsgBuf, L" -> res="); + appendNumber(dbgMsgBuf, res, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + return res; + } + + // Check validity of transaction and add to the pool. Return boolean indicating whether transaction was added. + static bool add(const Transaction* tx) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin pendingTxsPool.add()"); +#endif + bool txAdded = false; + ACQUIRE(lock); + if (tx->checkValidity() && tickInStorage(tx->tick)) + { + unsigned int tickIndex = tickToIndex(tx->tick); + const unsigned int transactionSize = tx->totalSize(); + + sint64 priority = calculateTxPriority(tx); + if (priority > 0) + { + m256i povIndex{ tickIndex, 0, 0, 0 }; + + if (numSavedTxsPerTick[tickIndex] < maxNumTxsPerTick) + { + KangarooTwelve(tx, transactionSize, getDigestPtr(tickIndex, numSavedTxsPerTick[tickIndex]), sizeof(m256i)); + + copyMem(getTxPtr(tickIndex, numSavedTxsPerTick[tickIndex]), tx, transactionSize); + + txsPriorities->add(povIndex, numSavedTxsPerTick[tickIndex], priority); + + numSavedTxsPerTick[tickIndex]++; + txAdded = true; + } + else + { + // check if priority is higher than lowest priority tx in this tick and replace in this case + sint64 lowestElementIndex = txsPriorities->tailIndex(povIndex); + if (lowestElementIndex != NULL_INDEX) + { + if (txsPriorities->priority(lowestElementIndex) < priority) + { + unsigned int replacedTxIndex = txsPriorities->element(lowestElementIndex); + txsPriorities->remove(lowestElementIndex); + txsPriorities->add(povIndex, replacedTxIndex, priority); + + KangarooTwelve(tx, transactionSize, getDigestPtr(tickIndex, replacedTxIndex), sizeof(m256i)); + + copyMem(getTxPtr(tickIndex, replacedTxIndex), tx, transactionSize); + + txAdded = true; + } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + CHAR16 dbgMsgBuf[300]; + setText(dbgMsgBuf, L"tx could not be added, already saved "); + appendNumber(dbgMsgBuf, numSavedTxsPerTick[tickIndex], FALSE); + appendText(dbgMsgBuf, L" txs for tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + appendText(dbgMsgBuf, L" and priority "); + appendNumber(dbgMsgBuf, priority, FALSE); + appendText(dbgMsgBuf, L" is lower than lowest saved priority "); + appendNumber(dbgMsgBuf, txsPriorities->priority(lowestElementIndex), FALSE); + addDebugMessage(dbgMsgBuf); + } +#endif + } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + // debug log, this should never happen + CHAR16 dbgMsgBuf[300]; + setText(dbgMsgBuf, L"maximum number of txs "); + appendNumber(dbgMsgBuf, numSavedTxsPerTick[tickIndex], FALSE); + appendText(dbgMsgBuf, L" saved for tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + appendText(dbgMsgBuf, L" but povIndex is unknown. This should never happen."); + addDebugMessage(dbgMsgBuf); + } +#endif + } + } +#if !defined(NDEBUG) && !defined(NO_UEFI) + else + { + CHAR16 dbgMsgBuf[300]; + setText(dbgMsgBuf, L"tx with priority 0 was rejected for tick "); + appendNumber(dbgMsgBuf, tx->tick, FALSE); + addDebugMessage(dbgMsgBuf); + } +#endif + } + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + if (txAdded) + addDebugMessage(L"End pendingTxsPool.add(), txAdded true"); + else + addDebugMessage(L"End pendingTxsPool.add(), txAdded false"); +#endif + return txAdded; + } + + // Get a transaction for the specified tick. If no more transactions for this tick, return nullptr. + // ATTENTION: when running multiple threads, you need to have acquired the lock via acquireLock() before calling this function. + static Transaction* getTx(unsigned int tick, unsigned int index) + { + unsigned int tickIndex; + + if (tickInStorage(tick)) + tickIndex = tickToIndex(tick); + else + return nullptr; + + bool hasTx = index < numSavedTxsPerTick[tickIndex]; + + if (hasTx) + return getTxPtr(tickIndex, index); + else + return nullptr; + } + + // Get a transaction digest for the specified tick. If no more transactions for this tick, return nullptr. + // ATTENTION: when running multiple threads, you need to have acquired the lock via acquireLock() before calling this function. + static m256i* getDigest(unsigned int tick, unsigned int index) + { + unsigned int tickIndex; + + if (tickInStorage(tick)) + tickIndex = tickToIndex(tick); + else + return nullptr; + + bool hasTx = index < numSavedTxsPerTick[tickIndex]; + + if (hasTx) + return getDigestPtr(tickIndex, index); + else + return nullptr; + } + + static void incrementFirstStoredTick() + { + ACQUIRE(lock); + + // set memory at buffersBeginIndex to 0 + unsigned long long numTxsBeforeBegin = buffersBeginIndex * maxNumTxsPerTick; + setMem(tickTransactionsBuffer + numTxsBeforeBegin * MAX_TRANSACTION_SIZE, maxNumTxsPerTick * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer + numTxsBeforeBegin, maxNumTxsPerTick * sizeof(m256i), 0); + numSavedTxsPerTick[buffersBeginIndex] = 0; + + // remove txs priorities stored for firstStoredTick + cleanupTxsPriorities(tickToIndex(firstStoredTick)); + + // increment buffersBeginIndex and firstStoredTick + firstStoredTick++; + buffersBeginIndex = (buffersBeginIndex + 1) % PENDING_TXS_POOL_NUM_TICKS; + + RELEASE(lock); + } + + static void beginEpoch(unsigned int newInitialTick) + { +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin pendingTxsPool.beginEpoch()"); +#endif + ACQUIRE(lock); + if (tickInStorage(newInitialTick)) + { + unsigned int newInitialIndex = tickToIndex(newInitialTick); + + // reset memory of discarded ticks + if (newInitialIndex < buffersBeginIndex) + { + unsigned long long numTxsBeforeNew = newInitialIndex * maxNumTxsPerTick; + setMem(tickTransactionsBuffer, numTxsBeforeNew * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer, numTxsBeforeNew * sizeof(m256i), 0); + setMem(numSavedTxsPerTick, newInitialIndex * sizeof(unsigned int), 0); + + for (unsigned int tickIndex = 0; tickIndex < newInitialIndex; ++tickIndex) + cleanupTxsPriorities(tickIndex); + + unsigned long long numTxsBeforeBegin = buffersBeginIndex * maxNumTxsPerTick; + unsigned long long numTxsStartingAtBegin = (PENDING_TXS_POOL_NUM_TICKS - buffersBeginIndex) * maxNumTxsPerTick; + setMem(tickTransactionsBuffer + numTxsBeforeBegin * MAX_TRANSACTION_SIZE, numTxsStartingAtBegin * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer + numTxsBeforeBegin, numTxsStartingAtBegin * sizeof(m256i), 0); + setMem(numSavedTxsPerTick + buffersBeginIndex, (PENDING_TXS_POOL_NUM_TICKS - buffersBeginIndex) * sizeof(unsigned int), 0); + + for (unsigned int tickIndex = buffersBeginIndex; tickIndex < PENDING_TXS_POOL_NUM_TICKS; ++tickIndex) + cleanupTxsPriorities(tickIndex); + } + else + { + unsigned long long numTxsBeforeBegin = buffersBeginIndex * maxNumTxsPerTick; + unsigned long long numTxsStartingAtBegin = (newInitialIndex - buffersBeginIndex) * maxNumTxsPerTick; + setMem(tickTransactionsBuffer + numTxsBeforeBegin * MAX_TRANSACTION_SIZE, numTxsStartingAtBegin * MAX_TRANSACTION_SIZE, 0); + setMem(txsDigestsBuffer + numTxsBeforeBegin, numTxsStartingAtBegin * sizeof(m256i), 0); + setMem(numSavedTxsPerTick + buffersBeginIndex, (newInitialIndex - buffersBeginIndex) * sizeof(unsigned int), 0); + + for (unsigned int tickIndex = buffersBeginIndex; tickIndex < newInitialIndex; ++tickIndex) + cleanupTxsPriorities(tickIndex); + } + + buffersBeginIndex = newInitialIndex; + } + else + { + setMem(tickTransactionsBuffer, tickTransactionsSize, 0); + setMem(txsDigestsBuffer, txsDigestsSize, 0); + setMem(numSavedTxsPerTick, sizeof(numSavedTxsPerTick), 0); + + txsPriorities->reset(); + + buffersBeginIndex = 0; + } + + firstStoredTick = newInitialTick; + + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"End pendingTxsPool.beginEpoch()"); +#endif + } + + // Useful for debugging, but expensive: check that everything is as expected. + static void checkStateConsistencyWithAssert() + { + ACQUIRE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"Begin tsxPool.checkStateConsistencyWithAssert()"); + CHAR16 dbgMsgBuf[200]; + setText(dbgMsgBuf, L"firstStoredTick="); + appendNumber(dbgMsgBuf, firstStoredTick, FALSE); + appendText(dbgMsgBuf, L", buffersBeginIndex="); + appendNumber(dbgMsgBuf, buffersBeginIndex, FALSE); + addDebugMessage(dbgMsgBuf); +#endif + + ASSERT(buffersBeginIndex >= 0); + ASSERT(buffersBeginIndex < PENDING_TXS_POOL_NUM_TICKS); + + ASSERT(tickTransactionsBuffer != nullptr); + ASSERT(txsDigestsBuffer != nullptr); + + for (unsigned int tick = firstStoredTick; tick < firstStoredTick + PENDING_TXS_POOL_NUM_TICKS; ++tick) + { + ASSERT(tickInStorage(tick)); + if (tickInStorage(tick)) + { + unsigned int tickIndex = tickToIndex(tick); + unsigned int numSavedForTick = numSavedTxsPerTick[tickIndex]; + ASSERT(numSavedForTick <= maxNumTxsPerTick); + for (unsigned int txIndex = 0; txIndex < numSavedForTick; ++txIndex) + { + Transaction* transaction = (Transaction*)(tickTransactionsBuffer + (tickIndex * maxNumTxsPerTick + txIndex) * MAX_TRANSACTION_SIZE); + ASSERT(transaction->checkValidity()); + ASSERT(transaction->tick == tick); +#if !defined(NDEBUG) && !defined(NO_UEFI) + if (!transaction->checkValidity() || transaction->tick != tick) + { + setText(dbgMsgBuf, L"Error in previous epoch transaction "); + appendNumber(dbgMsgBuf, txIndex, FALSE); + appendText(dbgMsgBuf, L" in tick "); + appendNumber(dbgMsgBuf, tick, FALSE); + addDebugMessage(dbgMsgBuf); + + setText(dbgMsgBuf, L"t->tick "); + appendNumber(dbgMsgBuf, transaction->tick, FALSE); + appendText(dbgMsgBuf, L", t->inputSize "); + appendNumber(dbgMsgBuf, transaction->inputSize, FALSE); + appendText(dbgMsgBuf, L", t->inputType "); + appendNumber(dbgMsgBuf, transaction->inputType, FALSE); + appendText(dbgMsgBuf, L", t->amount "); + appendNumber(dbgMsgBuf, transaction->amount, TRUE); + addDebugMessage(dbgMsgBuf); + } +#endif + } + } + } + + RELEASE(lock); + +#if !defined(NDEBUG) && !defined(NO_UEFI) + addDebugMessage(L"End pendingTxsPool.checkStateConsistencyWithAssert()"); +#endif + } + +}; \ No newline at end of file diff --git a/src/ticking/tick_storage.h b/src/ticking/tick_storage.h index 24f02ad8c..b350237f8 100644 --- a/src/ticking/tick_storage.h +++ b/src/ticking/tick_storage.h @@ -28,7 +28,7 @@ constexpr unsigned short INVALIDATED_TICK_DATA = 0xffff; // - ticks (one Tick struct per tick and Computor) // - tickTransactions (continuous buffer efficiently storing the variable-size transactions) // - tickTransactionOffsets (offsets of transactions in buffer, order in tickTransactions may differ) -// - nextTickTransactionOffset (offset of next transition to be added) +// - nextTickTransactionOffset (offset of next transaction to be added) class TickStorage { private: diff --git a/src/ticking/ticking.h b/src/ticking/ticking.h index 1b5216926..4bbb0d61a 100644 --- a/src/ticking/ticking.h +++ b/src/ticking/ticking.h @@ -6,6 +6,7 @@ #include "network_messages/tick.h" #include "ticking/tick_storage.h" +#include "ticking/pending_txs_pool.h" #include "private_settings.h" diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 6723ec0a1..d429b8716 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable( # contract_qearn.cpp # contract_qvault.cpp # contract_qx.cpp + contract_vottunbridge.cpp # kangaroo_twelve.cpp m256.cpp math_lib.cpp diff --git a/test/contract_msvault.cpp b/test/contract_msvault.cpp index 9c51671f8..e37260230 100644 --- a/test/contract_msvault.cpp +++ b/test/contract_msvault.cpp @@ -8,9 +8,13 @@ static const id OWNER2 = ID(_F, _X, _J, _F, _B, _T, _J, _M, _Y, _F, _J, _H, _P, static const id OWNER3 = ID(_K, _E, _F, _D, _Z, _T, _Y, _L, _F, _E, _R, _A, _H, _D, _V, _L, _N, _Q, _O, _R, _D, _H, _F, _Q, _I, _B, _S, _B, _Z, _C, _W, _S, _Z, _X, _Z, _F, _F, _A, _N, _O, _T, _F, _A, _H, _W, _M, _O, _V, _G, _T, _R, _Q, _J, _P, _X, _D); static const id TEST_VAULT_NAME = ID(_M, _Y, _M, _S, _V, _A, _U, _L, _U, _S, _E, _D, _F, _O, _R, _U, _N, _I, _T, _T, _T, _E, _S, _T, _I, _N, _G, _P, _U, _R, _P, _O, _S, _E, _S, _O, _N, _L, _Y, _U, _N, _I, _T, _T, _E, _S, _C, _O, _R, _E, _S, _M, _A, _R, _T, _T); -static constexpr uint64 TWO_OF_TWO = 2ULL; +static constexpr uint64 TWO_OF_TWO = 2ULL; static constexpr uint64 TWO_OF_THREE = 2ULL; +static const id DESTINATION = id::randomValue(); +static constexpr uint64 QX_ISSUE_ASSET_FEE = 1000000000ull; +static constexpr uint64 QX_MANAGEMENT_TRANSFER_FEE = 100ull; + class ContractTestingMsVault : protected ContractTesting { public: @@ -20,6 +24,8 @@ class ContractTestingMsVault : protected ContractTesting initEmptyUniverse(); INIT_CONTRACT(MSVAULT); callSystemProcedure(MSVAULT_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QX); + callSystemProcedure(QX_CONTRACT_INDEX, INITIALIZE); } void beginEpoch(bool expectSuccess = true) @@ -32,7 +38,7 @@ class ContractTestingMsVault : protected ContractTesting callSystemProcedure(MSVAULT_CONTRACT_INDEX, END_EPOCH, expectSuccess); } - void registerVault(uint64 requiredApprovals, id vaultName, const std::vector& owners, uint64 fee) + MSVAULT::registerVault_output registerVault(uint64 requiredApprovals, id vaultName, const std::vector& owners, uint64 fee) { MSVAULT::registerVault_input input; for (uint64 i = 0; i < MSVAULT_MAX_OWNERS; i++) @@ -43,18 +49,20 @@ class ContractTestingMsVault : protected ContractTesting input.vaultName = vaultName; MSVAULT::registerVault_output regOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 1, input, regOut, owners[0], fee); + return regOut; } - void deposit(uint64 vaultId, uint64 amount, const id& from) + MSVAULT::deposit_output deposit(uint64 vaultId, uint64 amount, const id& from) { MSVAULT::deposit_input input; input.vaultId = vaultId; increaseEnergy(from, amount); MSVAULT::deposit_output depOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 2, input, depOut, from, amount); + return depOut; } - void releaseTo(uint64 vaultId, uint64 amount, const id& destination, const id& owner, uint64 fee = MSVAULT_RELEASE_FEE) + MSVAULT::releaseTo_output releaseTo(uint64 vaultId, uint64 amount, const id& destination, const id& owner, uint64 fee = MSVAULT_RELEASE_FEE) { MSVAULT::releaseTo_input input; input.vaultId = vaultId; @@ -64,9 +72,10 @@ class ContractTestingMsVault : protected ContractTesting increaseEnergy(owner, fee); MSVAULT::releaseTo_output relOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 3, input, relOut, owner, fee); + return relOut; } - void resetRelease(uint64 vaultId, const id& owner, uint64 fee = MSVAULT_RELEASE_RESET_FEE) + MSVAULT::resetRelease_output resetRelease(uint64 vaultId, const id& owner, uint64 fee = MSVAULT_RELEASE_RESET_FEE) { MSVAULT::resetRelease_input input; input.vaultId = vaultId; @@ -74,6 +83,7 @@ class ContractTestingMsVault : protected ContractTesting increaseEnergy(owner, fee); MSVAULT::resetRelease_output rstOut; invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 4, input, rstOut, owner, fee); + return rstOut; } MSVAULT::getVaultName_output getVaultName(uint64 vaultId) const @@ -131,11 +141,115 @@ class ContractTestingMsVault : protected ContractTesting return -1; } + void issueAsset(const id& issuer, const std::string& assetNameStr, sint64 numberOfShares) + { + uint64 assetName = assetNameFromString(assetNameStr.c_str()); + QX::IssueAsset_input input{ assetName, numberOfShares, 0, 0 }; + QX::IssueAsset_output output; + increaseEnergy(issuer, QX_ISSUE_ASSET_FEE); + invokeUserProcedure(QX_CONTRACT_INDEX, 1, input, output, issuer, QX_ISSUE_ASSET_FEE); + } + + QX::TransferShareOwnershipAndPossession_output transferAsset(const id& from, const id& to, const Asset& asset, uint64_t amount) { + QX::TransferShareOwnershipAndPossession_input input; + input.issuer = asset.issuer; + input.newOwnerAndPossessor = to; + input.assetName = asset.assetName; + input.numberOfShares = amount; + QX::TransferShareOwnershipAndPossession_output output; + invokeUserProcedure(QX_CONTRACT_INDEX, 2, input, output, from, 1000000); + return output; + } + + int64_t transferShareManagementRights(const id& from, const Asset& asset, sint64 numberOfShares, uint32 newManagingContractIndex) + { + QX::TransferShareManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + input.newManagingContractIndex = newManagingContractIndex; + QX::TransferShareManagementRights_output output; + output.transferredNumberOfShares = 0; + invokeUserProcedure(QX_CONTRACT_INDEX, 9, input, output, from, 0); + return output.transferredNumberOfShares; + } + + MSVAULT::revokeAssetManagementRights_output revokeAssetManagementRights(const id& from, const Asset& asset, sint64 numberOfShares) + { + MSVAULT::revokeAssetManagementRights_input input; + input.asset = asset; + input.numberOfShares = numberOfShares; + MSVAULT::revokeAssetManagementRights_output output; + output.transferredNumberOfShares = 0; + output.status = 0; + + // The fee required by QX is 100. Do this to ensure enough fee. + const uint64 fee = 100; + increaseEnergy(from, fee); + + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 25, input, output, from, fee); + return output; + } + + MSVAULT::depositAsset_output depositAsset(uint64 vaultId, const Asset& asset, uint64 amount, const id& from) + { + MSVAULT::depositAsset_input input; + input.vaultId = vaultId; + input.asset = asset; + input.amount = amount; + MSVAULT::depositAsset_output output; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 19, input, output, from, 0); + return output; + } + + MSVAULT::releaseAssetTo_output releaseAssetTo(uint64 vaultId, const Asset& asset, uint64 amount, const id& destination, const id& owner, uint64 fee = MSVAULT_RELEASE_FEE) + { + MSVAULT::releaseAssetTo_input input; + input.vaultId = vaultId; + input.asset = asset; + input.amount = amount; + input.destination = destination; + + increaseEnergy(owner, fee); + MSVAULT::releaseAssetTo_output output; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 20, input, output, owner, fee); + return output; + } + + MSVAULT::resetAssetRelease_output resetAssetRelease(uint64 vaultId, const id& owner, uint64 fee = MSVAULT_RELEASE_RESET_FEE) + { + MSVAULT::resetAssetRelease_input input; + input.vaultId = vaultId; + + increaseEnergy(owner, fee); + MSVAULT::resetAssetRelease_output output; + invokeUserProcedure(MSVAULT_CONTRACT_INDEX, 21, input, output, owner, fee); + return output; + } + + MSVAULT::getVaultAssetBalances_output getVaultAssetBalances(uint64 vaultId) const + { + MSVAULT::getVaultAssetBalances_input input; + input.vaultId = vaultId; + MSVAULT::getVaultAssetBalances_output output; + callFunction(MSVAULT_CONTRACT_INDEX, 22, input, output); + return output; + } + + MSVAULT::getAssetReleaseStatus_output getAssetReleaseStatus(uint64 vaultId) const + { + MSVAULT::getAssetReleaseStatus_input input; + input.vaultId = vaultId; + MSVAULT::getAssetReleaseStatus_output output; + callFunction(MSVAULT_CONTRACT_INDEX, 23, input, output); + return output; + } + ~ContractTestingMsVault() { } }; + TEST(ContractMsVault, RegisterVault_InsufficientFee) { ContractTestingMsVault msVault; @@ -144,8 +258,10 @@ TEST(ContractMsVault, RegisterVault_InsufficientFee) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + // Attempt with insufficient fee - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, 5000ULL); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, 5000ULL); + EXPECT_EQ(regOut.status, 2ULL); // FAILURE_INSUFFICIENT_FEE // No new vault should be created auto vaultsO1After = msVault.getVaults(OWNER1); @@ -158,8 +274,10 @@ TEST(ContractMsVault, RegisterVault_OneOwner) ContractTestingMsVault msVault; auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); + // Only one owner => should fail - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 5ULL); // FAILURE_INVALID_PARAMS // Should fail, no new vault auto vaultsO1After = msVault.getVaults(OWNER1); @@ -173,7 +291,8 @@ TEST(ContractMsVault, RegisterVault_Success) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); // SUCCESS auto vaultsO1After = msVault.getVaults(OWNER1); EXPECT_EQ(static_cast(vaultsO1After.numberOfVaults), @@ -202,7 +321,8 @@ TEST(ContractMsVault, GetVaultName) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1After = msVault.getVaults(OWNER1); EXPECT_EQ(static_cast(vaultsO1After.numberOfVaults), @@ -221,7 +341,8 @@ TEST(ContractMsVault, Deposit_InvalidVault) ContractTestingMsVault msVault; // deposit to a non-existent vault auto beforeBalance = msVault.getBalanceOf(999ULL); - msVault.deposit(999ULL, 5000ULL, OWNER1); + auto depOut = msVault.deposit(999ULL, 5000ULL, OWNER1); + EXPECT_EQ(depOut.status, 3ULL); // FAILURE_INVALID_VAULT // no change in balance auto afterBalance = msVault.getBalanceOf(999ULL); EXPECT_EQ(afterBalance.balance, beforeBalance.balance); @@ -234,13 +355,15 @@ TEST(ContractMsVault, Deposit_Success) auto vaultsO1Before = msVault.getVaults(OWNER1); increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1After = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1After.vaultIds.get(vaultsO1Before.numberOfVaults); auto balBefore = msVault.getBalanceOf(vaultId); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto balAfter = msVault.getBalanceOf(vaultId); EXPECT_EQ(balAfter.balance, balBefore.balance + 10000ULL); } @@ -250,16 +373,19 @@ TEST(ContractMsVault, ReleaseTo_NonOwner) ContractTestingMsVault msVault; increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto releaseStatusBefore = msVault.getReleaseStatus(vaultId); // Non-owner attempt release - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER3); + auto relOut = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER3); + EXPECT_EQ(relOut.status, 4ULL); // FAILURE_NOT_AUTHORIZED auto releaseStatusAfter = msVault.getReleaseStatus(vaultId); // No approvals should be set @@ -273,21 +399,25 @@ TEST(ContractMsVault, ReleaseTo_InvalidParams) increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE); // 2 out of 2 owners - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto releaseStatusBefore = msVault.getReleaseStatus(vaultId); // amount=0 - msVault.releaseTo(vaultId, 0ULL, OWNER2, OWNER1); + auto relOut1 = msVault.releaseTo(vaultId, 0ULL, OWNER2, OWNER1); + EXPECT_EQ(relOut1.status, 5ULL); // FAILURE_INVALID_PARAMS auto releaseStatusAfter1 = msVault.getReleaseStatus(vaultId); EXPECT_EQ(releaseStatusAfter1.amounts.get(0), releaseStatusBefore.amounts.get(0)); // destination NULL_ID - msVault.releaseTo(vaultId, 5000ULL, NULL_ID, OWNER1); + auto relOut2 = msVault.releaseTo(vaultId, 5000ULL, NULL_ID, OWNER1); + EXPECT_EQ(relOut2.status, 5ULL); // FAILURE_INVALID_PARAMS auto releaseStatusAfter2 = msVault.getReleaseStatus(vaultId); EXPECT_EQ(releaseStatusAfter2.amounts.get(0), releaseStatusBefore.amounts.get(0)); } @@ -300,13 +430,16 @@ TEST(ContractMsVault, ReleaseTo_PartialApproval) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 3 owners - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 15000ULL, OWNER1); - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + auto depOut = msVault.deposit(vaultId, 15000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + auto relOut = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + EXPECT_EQ(relOut.status, 9ULL); // PENDING_APPROVAL auto status = msVault.getReleaseStatus(vaultId); // Partial approval means just first owner sets the request @@ -323,18 +456,22 @@ TEST(ContractMsVault, ReleaseTo_FullApproval) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 3 - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); // OWNER1 requests 5000 Qubics to OWNER3 - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + auto relOut1 = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER1); + EXPECT_EQ(relOut1.status, 9ULL); // PENDING_APPROVAL // Not approved yet - msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER2); // second approval + auto relOut2 = msVault.releaseTo(vaultId, 5000ULL, OWNER3, OWNER2); // second approval + EXPECT_EQ(relOut2.status, 1ULL); // SUCCESS // After full approval, amount should be released auto bal = msVault.getBalanceOf(vaultId); @@ -350,16 +487,19 @@ TEST(ContractMsVault, ReleaseTo_InsufficientBalance) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 2 - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 10000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 10000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); auto balBefore = msVault.getBalanceOf(vaultId); // Attempt to release more than balance - msVault.releaseTo(vaultId, 20000ULL, OWNER3, OWNER1); + auto relOut = msVault.releaseTo(vaultId, 20000ULL, OWNER3, OWNER1); + EXPECT_EQ(relOut.status, 6ULL); // FAILURE_INSUFFICIENT_BALANCE // Should fail, balance no change auto balAfter = msVault.getBalanceOf(vaultId); @@ -375,17 +515,21 @@ TEST(ContractMsVault, ResetRelease_NonOwner) increaseEnergy(OWNER3, 100000000ULL); // 2 out of 2 - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 5000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 5000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); - msVault.releaseTo(vaultId, 2000ULL, OWNER2, OWNER1); + auto relOut = msVault.releaseTo(vaultId, 2000ULL, OWNER2, OWNER1); + EXPECT_EQ(relOut.status, 9ULL); // PENDING_APPROVAL auto statusBefore = msVault.getReleaseStatus(vaultId); - msVault.resetRelease(vaultId, OWNER3); // Non owner tries to reset + auto rstOut = msVault.resetRelease(vaultId, OWNER3); // Non owner tries to reset + EXPECT_EQ(rstOut.status, 4ULL); // FAILURE_NOT_AUTHORIZED auto statusAfter = msVault.getReleaseStatus(vaultId); // No change in release requests @@ -401,17 +545,22 @@ TEST(ContractMsVault, ResetRelease_Success) increaseEnergy(OWNER2, 100000000ULL); increaseEnergy(OWNER3, 100000000ULL); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); auto vaultsO1 = msVault.getVaults(OWNER1); uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); - msVault.deposit(vaultId, 5000ULL, OWNER1); + auto depOut = msVault.deposit(vaultId, 5000ULL, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); // OWNER2 requests a releaseTo - msVault.releaseTo(vaultId, 2000ULL, OWNER1, OWNER2); + auto relOut = msVault.releaseTo(vaultId, 2000ULL, OWNER1, OWNER2); + EXPECT_EQ(relOut.status, 9ULL); + // Now reset by OWNER2 - msVault.resetRelease(vaultId, OWNER2); + auto rstOut = msVault.resetRelease(vaultId, OWNER2); + EXPECT_EQ(rstOut.status, 1ULL); auto status = msVault.getReleaseStatus(vaultId); // All cleared @@ -432,60 +581,678 @@ TEST(ContractMsVault, GetVaults_Multiple) auto vaultsForOwner2Before = msVault.getVaults(OWNER2); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + auto regOut1 = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut1.status, 1ULL); + auto regOut2 = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut2.status, 1ULL); auto vaultsForOwner2After = msVault.getVaults(OWNER2); EXPECT_GE(static_cast(vaultsForOwner2After.numberOfVaults), - static_cast(vaultsForOwner2Before.numberOfVaults + 2U)); + static_cast(vaultsForOwner2Before.numberOfVaults + 2U)); } TEST(ContractMsVault, GetRevenue) { ContractTestingMsVault msVault; + const Asset assetTest = { OWNER1, assetNameFromString("TESTREV") }; + + increaseEnergy(OWNER1, 1000000000ULL); + increaseEnergy(OWNER2, 1000000000ULL); + + uint64 expectedRevenue = 0; + auto revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 0U); + + // Register a vault, generating the first fee + auto regOut = msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + expectedRevenue += MSVAULT_REGISTERING_FEE; + + auto vaults = msVault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + + // Deposit QUs to ensure the vault can pay holding fees + const uint64 depositAmount = 10000000; // 10M QUs + auto depOut = msVault.deposit(vaultId, depositAmount, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + // expectedRevenue += state.liveDepositFee; // Fee is currently 0 + + // Generate Qubic-based fees + auto relOut = msVault.releaseTo(vaultId, 1000ULL, DESTINATION, OWNER1); + EXPECT_EQ(relOut.status, 9ULL); // Pending approval + expectedRevenue += MSVAULT_RELEASE_FEE; + auto rstOut = msVault.resetRelease(vaultId, OWNER1); + EXPECT_EQ(rstOut.status, 1ULL); + expectedRevenue += MSVAULT_RELEASE_RESET_FEE; + + // Generate Asset-based fees + msVault.issueAsset(OWNER1, "TESTREV", 10000); + msVault.transferShareManagementRights(OWNER1, assetTest, 5000, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msVault.depositAsset(vaultId, assetTest, 1000, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + // expectedRevenue += state.liveDepositFee; // Fee is currently 0 + auto relAssetOut = msVault.releaseAssetTo(vaultId, assetTest, 50, DESTINATION, OWNER2); + EXPECT_EQ(relAssetOut.status, 9ULL); // Pending approval + expectedRevenue += MSVAULT_RELEASE_FEE; + auto rstAssetOut = msVault.resetAssetRelease(vaultId, OWNER2); + EXPECT_EQ(rstAssetOut.status, 1ULL); + expectedRevenue += MSVAULT_RELEASE_RESET_FEE; + + // Verify revenue before the first epoch ends + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + + msVault.endEpoch(); msVault.beginEpoch(); - auto revenueOutput = msVault.getRevenueInfo(); - EXPECT_EQ(revenueOutput.totalRevenue, 0U); - EXPECT_EQ(revenueOutput.totalDistributedToShareholders, 0U); - EXPECT_EQ(revenueOutput.numberOfActiveVaults, 0U); + // Holding fee from the active vault is collected + expectedRevenue += MSVAULT_HOLDING_FEE; - increaseEnergy(OWNER1, 100000000ULL); - increaseEnergy(OWNER2, 100000000000ULL); - increaseEnergy(OWNER3, 100000ULL); + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 1U); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + + // Verify dividends were distributed correctly based on the total revenue so far + uint64 expectedDistribution = (expectedRevenue / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS; + EXPECT_EQ(revenueInfo.totalDistributedToShareholders, expectedDistribution); + + // Make more revenue generation actions in the new epoch + auto relOut2 = msVault.releaseTo(vaultId, 2000ULL, DESTINATION, OWNER2); + EXPECT_EQ(relOut2.status, 9ULL); + expectedRevenue += MSVAULT_RELEASE_FEE; + + auto rstOut2 = msVault.resetRelease(vaultId, OWNER2); + EXPECT_EQ(rstOut2.status, 1ULL); + expectedRevenue += MSVAULT_RELEASE_RESET_FEE; - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + // Revoke some of the previously granted management rights. + // This one has a fee, but it is paid to QX, not kept by MsVault. + // Therefore, expectedRevenue should NOT be incremented. + auto revokeOut = msVault.revokeAssetManagementRights(OWNER1, assetTest, 2000); + EXPECT_EQ(revokeOut.status, 1ULL); + // End the second epoch msVault.endEpoch(); + msVault.beginEpoch(); + + // Another holding fee is collected + expectedRevenue += MSVAULT_HOLDING_FEE; + + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 1U); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); + + // Verify the new cumulative dividend distribution + expectedDistribution = (expectedRevenue / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS; + EXPECT_EQ(revenueInfo.totalDistributedToShareholders, expectedDistribution); + // No new transactions in this epoch + msVault.endEpoch(); msVault.beginEpoch(); - revenueOutput = msVault.getRevenueInfo(); - // first vault is destroyed after paying dividends - EXPECT_EQ(revenueOutput.totalRevenue, MSVAULT_REGISTERING_FEE); - EXPECT_EQ(revenueOutput.totalDistributedToShareholders, ((int)MSVAULT_REGISTERING_FEE / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS); - EXPECT_EQ(revenueOutput.numberOfActiveVaults, 0U); + // A third holding fee is collected + expectedRevenue += MSVAULT_HOLDING_FEE; - increaseEnergy(OWNER1, 100000000ULL); - increaseEnergy(OWNER2, 100000000ULL); - increaseEnergy(OWNER3, 100000000ULL); + revenueInfo = msVault.getRevenueInfo(); + EXPECT_EQ(revenueInfo.numberOfActiveVaults, 1U); + EXPECT_EQ(revenueInfo.totalRevenue, expectedRevenue); - msVault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + // Verify the final cumulative dividend distribution + expectedDistribution = (expectedRevenue / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS; + EXPECT_EQ(revenueInfo.totalDistributedToShareholders, expectedDistribution); +} - auto vaultsO1 = msVault.getVaults(OWNER1); - uint64 vaultId = vaultsO1.vaultIds.get(vaultsO1.numberOfVaults - 1); +TEST(ContractMsVault, ManagementRightsVsDirectDeposit) +{ + ContractTestingMsVault msvault; + + // Create an issuer and two users. + const id ISSUER = id::randomValue(); + const id USER_WITH_RIGHTS = id::randomValue(); // This user will do it correctly + const id USER_WITHOUT_RIGHTS = id::randomValue(); // This user will attempt a direct deposit first + + Asset assetTest = { ISSUER, assetNameFromString("ASSET") }; + const sint64 initialDistribution = 50000; + + // Give everyone energy for fees + increaseEnergy(ISSUER, QX_ISSUE_ASSET_FEE + (1000000 * 2)); + increaseEnergy(USER_WITH_RIGHTS, MSVAULT_REGISTERING_FEE + (1000000 * 3)); + increaseEnergy(USER_WITHOUT_RIGHTS, 1000000 * 3); // More energy for the correct attempt later + + // Issue the asset and distribute it to the two users + msvault.issueAsset(ISSUER, "ASSET", initialDistribution * 2); + msvault.transferAsset(ISSUER, USER_WITH_RIGHTS, assetTest, initialDistribution); + msvault.transferAsset(ISSUER, USER_WITHOUT_RIGHTS, assetTest, initialDistribution); + + // Verify initial on-chain balances (both users' shares are managed by QX currently) + EXPECT_EQ(numberOfShares(assetTest, { USER_WITH_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, QX_CONTRACT_INDEX }), initialDistribution); + EXPECT_EQ(numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }), initialDistribution); + + // Create a simple vault owned by USER_WITH_RIGHTS + auto regOut = msvault.registerVault(2, TEST_VAULT_NAME, { USER_WITH_RIGHTS, OWNER1 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + + auto vaults = msvault.getVaults(USER_WITH_RIGHTS); + uint64 vaultId = vaults.vaultIds.get(0); + + // User with Management Rights + const sint64 sharesToManage1 = 10000; + msvault.transferShareManagementRights(USER_WITH_RIGHTS, assetTest, sharesToManage1, MSVAULT_CONTRACT_INDEX); + + // verify that management rights were transferred successfully + EXPECT_EQ(numberOfShares(assetTest, { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }), sharesToManage1); + EXPECT_EQ(numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }), 0); + + // This user now makes multiple deposits + const sint64 deposit1_U1 = 1000; + const sint64 deposit2_U1 = 2500; + auto depAssetOut1 = msvault.depositAsset(vaultId, assetTest, deposit1_U1, USER_WITH_RIGHTS); + EXPECT_EQ(depAssetOut1.status, 1ULL); + + // Verify balances after first deposit + sint64 sc_onchain_balance = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(sc_onchain_balance, deposit1_U1); + sint64 user_managed_balance = numberOfShares(assetTest, { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user_managed_balance, sharesToManage1 - deposit1_U1); + + auto depAssetOut2 = msvault.depositAsset(vaultId, assetTest, deposit2_U1, USER_WITH_RIGHTS); + EXPECT_EQ(depAssetOut2.status, 1ULL); + + // verify balances after second deposit + sc_onchain_balance = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(sc_onchain_balance, deposit1_U1 + deposit2_U1); + sint64 user1_managed_balance = numberOfShares(assetTest, { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITH_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user1_managed_balance, sharesToManage1 - deposit1_U1 - deposit2_U1); + + // user without management rights + sint64 sc_balance_before_direct_attempt = sc_onchain_balance; + sint64 user3_balance_before = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }); + + // This user attempts to deposit directly + auto depAssetOut3 = msvault.depositAsset(vaultId, assetTest, 500, USER_WITHOUT_RIGHTS); + EXPECT_EQ(depAssetOut3.status, 6ULL); // FAILURE_INSUFFICIENT_BALANCE + + // Verify that no shares were transferred + sint64 sc_balance_after_direct_attempt = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(sc_balance_after_direct_attempt, sc_balance_before_direct_attempt); + + sint64 user3_balance_after = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, QX_CONTRACT_INDEX }); + EXPECT_EQ(user3_balance_after, user3_balance_before); // User's balance should be unchanged + + sint64 user3_balance_after_msvault = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user3_balance_after_msvault, 0); + + // the second user now does it the correct way + const sint64 sharesToManage2 = 8000; + msvault.transferShareManagementRights(USER_WITHOUT_RIGHTS, assetTest, sharesToManage2, MSVAULT_CONTRACT_INDEX); + + // Verify their management rights were transferred successfully + EXPECT_EQ(numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }), sharesToManage2); + + const sint64 deposit1_U2 = 4000; + auto depAssetOut4 = msvault.depositAsset(vaultId, assetTest, deposit1_U2, USER_WITHOUT_RIGHTS); + EXPECT_EQ(depAssetOut4.status, 1ULL); + + // check the total balance in the smart contract + sint64 final_sc_balance = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + sint64 total_deposited = deposit1_U1 + deposit2_U1 + deposit1_U2; + EXPECT_EQ(final_sc_balance, total_deposited); + + // Also verify the second user's remaining managed balance + sint64 user2_managed_balance = numberOfShares(assetTest, { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }, + { USER_WITHOUT_RIGHTS, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(user2_managed_balance, sharesToManage2 - deposit1_U2); +} - msVault.deposit(vaultId, 500000000ULL, OWNER1); +TEST(ContractMsVault, DepositAsset_Success) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; - msVault.endEpoch(); + // Create a vault and issue an asset to OWNER1 + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + QX_ISSUE_ASSET_FEE); + auto regOut = msvault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); - msVault.beginEpoch(); + msvault.issueAsset(OWNER1, "ASSET", 1000000); + + auto OWNER4 = id::randomValue(); + + auto transfered = msvault.transferShareManagementRights(OWNER1, assetTest, 5000, MSVAULT_CONTRACT_INDEX); + + // Deposit the asset into the vault + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 500, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // Check the vault's asset balance + auto assetBalances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(assetBalances.status, 1ULL); + EXPECT_EQ(assetBalances.numberOfAssetTypes, 1ULL); + + auto firstAssetBalance = assetBalances.assetBalances.get(0); + EXPECT_EQ(firstAssetBalance.asset.issuer, assetTest.issuer); + EXPECT_EQ(firstAssetBalance.asset.assetName, assetTest.assetName); + EXPECT_EQ(firstAssetBalance.balance, 500ULL); + + // Check SC's shares + sint64 scShares = numberOfShares(assetTest, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(scShares, 500LL); +} + +TEST(ContractMsVault, DepositAsset_MaxTypes) +{ + ContractTestingMsVault msvault; + + // Create a vault + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + QX_ISSUE_ASSET_FEE * (MSVAULT_MAX_ASSET_TYPES + 1)); // Extra energy for fees + auto regOut = msvault.registerVault(2ULL, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + + // Deposit the maximum number of different asset types + for (uint64 i = 0; i < MSVAULT_MAX_ASSET_TYPES; i++) + { + std::string assetName = "ASSET" + std::to_string(i); + Asset currentAsset = { OWNER1, assetNameFromString(assetName.c_str()) }; + msvault.issueAsset(OWNER1, assetName, 1000000); + msvault.transferShareManagementRights(OWNER1, currentAsset, 100000, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, currentAsset, 1000, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + } + + // Check if max asset types reached + auto balances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(balances.numberOfAssetTypes, MSVAULT_MAX_ASSET_TYPES); + + // Try to deposit one more asset type + Asset extraAsset = { OWNER1, assetNameFromString("ASSETE") }; + msvault.issueAsset(OWNER1, "ASSETE", 100000); + msvault.transferShareManagementRights(OWNER1, extraAsset, 100000, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, extraAsset, 1000, OWNER1); + EXPECT_EQ(depAssetOut.status, 7ULL); // FAILURE_LIMIT_REACHED + + // The number of asset types should not have increased + auto balancesAfter = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(balancesAfter.numberOfAssetTypes, MSVAULT_MAX_ASSET_TYPES); +} + +TEST(ContractMsVault, ReleaseAssetTo_FullApproval) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; + + // Create a 2-of-3 vault, issue and deposit an asset + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + MSVAULT_RELEASE_FEE + QX_ISSUE_ASSET_FEE + QX_MANAGEMENT_TRANSFER_FEE); + increaseEnergy(OWNER2, MSVAULT_RELEASE_FEE); + auto regOut = msvault.registerVault(TWO_OF_THREE, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + + msvault.issueAsset(OWNER1, "ASSET", 1000000); + msvault.transferShareManagementRights(OWNER1, assetTest, 800, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 800, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // Deposit funds into the vault to cover the upcoming management transfer fee. + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + + // Check initial balances for the destination + EXPECT_EQ(numberOfShares(assetTest, { DESTINATION, QX_CONTRACT_INDEX }, { DESTINATION, QX_CONTRACT_INDEX }), 0LL); + EXPECT_EQ(numberOfShares(assetTest, { DESTINATION, MSVAULT_CONTRACT_INDEX }, { DESTINATION, MSVAULT_CONTRACT_INDEX }), 0LL); + auto vaultAssetBalanceBefore = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultAssetBalanceBefore, 800ULL); + + // Owners approve the release + auto relAssetOut1 = msvault.releaseAssetTo(vaultId, assetTest, 800, DESTINATION, OWNER1); + EXPECT_EQ(relAssetOut1.status, 9ULL); + auto relAssetOut2 = msvault.releaseAssetTo(vaultId, assetTest, 800, DESTINATION, OWNER2); + EXPECT_EQ(relAssetOut2.status, 1ULL); + + // Check final balances + sint64 destBalanceManagedByQx = numberOfShares(assetTest, { DESTINATION, QX_CONTRACT_INDEX }, { DESTINATION, QX_CONTRACT_INDEX }); + sint64 destBalanceManagedByMsVault = numberOfShares(assetTest, { DESTINATION, MSVAULT_CONTRACT_INDEX }, { DESTINATION, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(destBalanceManagedByQx, 800LL); + EXPECT_EQ(destBalanceManagedByMsVault, 0LL); + + auto vaultAssetBalanceAfter = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultAssetBalanceAfter, 0ULL); // 800 - 800 + + // The vault's qubic balance should be 0 after paying the management transfer fee + auto vaultQubicBalanceAfter = msvault.getBalanceOf(vaultId); + EXPECT_EQ(vaultQubicBalanceAfter.balance, 0LL); + + // Release status should be reset + auto releaseStatus = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(releaseStatus.amounts.get(0), 0ULL); + EXPECT_EQ(releaseStatus.destinations.get(0), NULL_ID); +} + +TEST(ContractMsVault, ReleaseAssetTo_PartialApproval) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; + + // Setup + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + MSVAULT_RELEASE_FEE + QX_MANAGEMENT_TRANSFER_FEE); + auto regOut = msvault.registerVault(TWO_OF_THREE, TEST_VAULT_NAME, { OWNER1, OWNER2, OWNER3 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + msvault.issueAsset(OWNER1, "ASSET", 1000000); + msvault.transferShareManagementRights(OWNER1, assetTest, 800, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 800, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // Deposit the fee into the vault so it can process release requests. + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + + // Only one owner approves + auto relAssetOut = msvault.releaseAssetTo(vaultId, assetTest, 500, DESTINATION, OWNER1); + EXPECT_EQ(relAssetOut.status, 9ULL); // PENDING_APPROVAL + + // Check release status is pending + auto status = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(status.status, 1ULL); + // Owner 1 is at index 0 + EXPECT_EQ(status.assets.get(0).assetName, assetTest.assetName); + EXPECT_EQ(status.amounts.get(0), 500ULL); + EXPECT_EQ(status.destinations.get(0), DESTINATION); + // Other owner slots are empty + EXPECT_EQ(status.amounts.get(1), 0ULL); + + // Balances should be unchanged + sint64 destinationBalance = numberOfShares(assetTest, { DESTINATION, QX_CONTRACT_INDEX }, + { DESTINATION, QX_CONTRACT_INDEX }); + EXPECT_EQ(destinationBalance, 0LL); + auto vaultBalance = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultBalance, 800ULL); +} + +TEST(ContractMsVault, ResetAssetRelease_Success) +{ + ContractTestingMsVault msvault; + Asset assetTest = { OWNER1, assetNameFromString("ASSET") }; + + // Setup + increaseEnergy(OWNER1, MSVAULT_REGISTERING_FEE + MSVAULT_RELEASE_RESET_FEE + MSVAULT_RELEASE_FEE + QX_MANAGEMENT_TRANSFER_FEE); + auto regOut = msvault.registerVault(TWO_OF_TWO, TEST_VAULT_NAME, { OWNER1, OWNER2 }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + auto vaults = msvault.getVaults(OWNER1); + uint64 vaultId = vaults.vaultIds.get(0); + msvault.issueAsset(OWNER1, "ASSET", 1000000); + msvault.transferShareManagementRights(OWNER1, assetTest, 100, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, 100, OWNER1); + EXPECT_EQ(depAssetOut.status, 1ULL); + + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, OWNER1); + EXPECT_EQ(depOut.status, 1ULL); + + // Propose and then reset a release + auto relAssetOut = msvault.releaseAssetTo(vaultId, assetTest, 50, DESTINATION, OWNER1); + EXPECT_EQ(relAssetOut.status, 9ULL); + + // Check status is pending before reset + auto statusBefore = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(statusBefore.amounts.get(0), 50ULL); + + // Reset the release + auto rstAssetOut = msvault.resetAssetRelease(vaultId, OWNER1); + EXPECT_EQ(rstAssetOut.status, 1ULL); + + // Status should be cleared for that owner + auto statusAfter = msvault.getAssetReleaseStatus(vaultId); + EXPECT_EQ(statusAfter.amounts.get(0), 0ULL); + EXPECT_EQ(statusAfter.destinations.get(0), NULL_ID); + + // Vault balance should be unchanged + auto vaultBalance = msvault.getVaultAssetBalances(vaultId).assetBalances.get(0).balance; + EXPECT_EQ(vaultBalance, 100ULL); +} + +TEST(ContractMsVault, FullLifecycle_BalanceVerification) +{ + ContractTestingMsVault msvault; + const id USER = OWNER1; + const id PARTNER = OWNER2; + const id DESTINATION_ACC = OWNER3; + Asset assetTest = { USER, assetNameFromString("ASSET") }; + const sint64 initialShares = 10000; + const sint64 sharesToManage = 5000; + const sint64 sharesToDeposit = 4000; + const sint64 sharesToRelease = 1500; + + // Issue asset and create a type 2 vault + increaseEnergy(USER, MSVAULT_REGISTERING_FEE + QX_ISSUE_ASSET_FEE + QX_MANAGEMENT_TRANSFER_FEE); + increaseEnergy(PARTNER, MSVAULT_RELEASE_FEE); + + msvault.issueAsset(USER, "ASSET", initialShares); + auto regOut = msvault.registerVault(TWO_OF_TWO, TEST_VAULT_NAME, { USER, PARTNER }, MSVAULT_REGISTERING_FEE); + EXPECT_EQ(regOut.status, 1ULL); + + auto vaults = msvault.getVaults(USER); + uint64 vaultId = vaults.vaultIds.get(0); + + // Verify user has full on-chain balance under QX management + sint64 userShares_QX = numberOfShares(assetTest, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }); + EXPECT_EQ(userShares_QX, initialShares); + + // Fund the vault for the future management transfer fee + auto depOut = msvault.deposit(vaultId, QX_MANAGEMENT_TRANSFER_FEE, USER); + EXPECT_EQ(depOut.status, 1ULL); + + // User gives MsVault management rights over a portion of their shares + msvault.transferShareManagementRights(USER, assetTest, sharesToManage, MSVAULT_CONTRACT_INDEX); + + // Verify on-chain balances after management transfer + sint64 userShares_MSVAULT_Managed = numberOfShares(assetTest, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }); + userShares_QX = numberOfShares(assetTest, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }); + EXPECT_EQ(userShares_MSVAULT_Managed, sharesToManage); + EXPECT_EQ(userShares_QX, initialShares - sharesToManage); + + // User deposits the MsVault-managed shares into the vault + auto depAssetOut = msvault.depositAsset(vaultId, assetTest, sharesToDeposit, USER); + EXPECT_EQ(depAssetOut.status, 1ULL); + + // User's on-chain balance of MsVault-managed shares should decrease + userShares_MSVAULT_Managed = numberOfShares(assetTest, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(userShares_MSVAULT_Managed, sharesToManage - sharesToDeposit); // 5000 - 4000 = 1000 + + // MsVault contract's on-chain balance should increase + sint64 scShares_onchain = numberOfShares(assetTest, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(scShares_onchain, sharesToDeposit); + + // Vault's internal balance should match the on-chain balance + auto vaultBalances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(vaultBalances.status, 1ULL); + EXPECT_EQ(vaultBalances.numberOfAssetTypes, 1ULL); + EXPECT_EQ(vaultBalances.assetBalances.get(0).balance, sharesToDeposit); + + // Both owners approve a release to the destination + auto relAssetOut1 = msvault.releaseAssetTo(vaultId, assetTest, sharesToRelease, DESTINATION_ACC, USER); + EXPECT_EQ(relAssetOut1.status, 9ULL); + auto relAssetOut2 = msvault.releaseAssetTo(vaultId, assetTest, sharesToRelease, DESTINATION_ACC, PARTNER); + EXPECT_EQ(relAssetOut2.status, 1ULL); + + // MsVault contract's on-chain balance should decrease + scShares_onchain = numberOfShares(assetTest, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(scShares_onchain, sharesToDeposit - sharesToRelease); + + // Vault's internal balance should be updated correctly + vaultBalances = msvault.getVaultAssetBalances(vaultId); + EXPECT_EQ(vaultBalances.assetBalances.get(0).balance, sharesToDeposit - sharesToRelease); + + // Vault's internal qubic balance should decrease by the fee + EXPECT_EQ(msvault.getBalanceOf(vaultId).balance, 0); + + // Destination's on-chain balance should increase, and it should be managed by QX + sint64 destinationSharesManagedByQx = numberOfShares(assetTest, { DESTINATION_ACC, QX_CONTRACT_INDEX }, { DESTINATION_ACC, QX_CONTRACT_INDEX }); + sint64 destinationSharesManagedByMsVault = numberOfShares(assetTest, { DESTINATION_ACC, MSVAULT_CONTRACT_INDEX }, { DESTINATION_ACC, MSVAULT_CONTRACT_INDEX }); + + EXPECT_EQ(destinationSharesManagedByQx, sharesToRelease); + EXPECT_EQ(destinationSharesManagedByMsVault, 0); +} + +TEST(ContractMsVault, StressTest_MultiUser_MultiAsset) +{ + ContractTestingMsVault msvault; + + // Define users, assets, and vaults + const int USER_COUNT = 16; + const int ASSET_COUNT = 8; + const int VAULT_COUNT = 3; + std::vector users; + std::vector assets; + + for (int i = 0; i < USER_COUNT; ++i) + { + users.push_back(id::randomValue()); + increaseEnergy(users[i], 1000000000000ULL); + } + + for (int i = 0; i < ASSET_COUNT; ++i) + { + // Issue each asset from a different user for variety + id issuer = users[i]; + std::string assetName = "ASSET" + std::to_string(i); + assets.push_back({ issuer, assetNameFromString(assetName.c_str()) }); + msvault.issueAsset(issuer, assetName, 1000000); // Issue 1M of each token + } + + // Create 3 vaults with different sets of 4 owners each + EXPECT_EQ(msvault.registerVault(3, id::randomValue(), { users[0], users[1], users[2], users[3] }, MSVAULT_REGISTERING_FEE).status, 1ULL); + EXPECT_EQ(msvault.registerVault(2, id::randomValue(), { users[4], users[5], users[6], users[7] }, MSVAULT_REGISTERING_FEE).status, 1ULL); + EXPECT_EQ(msvault.registerVault(4, id::randomValue(), { users[8], users[9], users[10], users[11] }, MSVAULT_REGISTERING_FEE).status, 1ULL); + + // Each of the 8 assets is deposited twice by its owner + uint64 targetVaultId = 0; + const sint64 depositAmount = 100; + + for (int i = 0; i < USER_COUNT; ++i) + { + int assetIndex = i % ASSET_COUNT; + Asset assetToDeposit = assets[assetIndex]; + id owner_of_asset = users[assetIndex]; + + msvault.transferShareManagementRights(owner_of_asset, assetToDeposit, depositAmount, MSVAULT_CONTRACT_INDEX); + auto depAssetOut = msvault.depositAsset(targetVaultId, assetToDeposit, depositAmount, owner_of_asset); + EXPECT_EQ(depAssetOut.status, 1ULL); + } + + auto depOut = msvault.deposit(targetVaultId, QX_MANAGEMENT_TRANSFER_FEE, users[0]); + EXPECT_EQ(depOut.status, 1ULL); + + // Check the state of the target vault + auto vaultBalances = msvault.getVaultAssetBalances(targetVaultId); + EXPECT_EQ(vaultBalances.status, 1ULL); + EXPECT_EQ(vaultBalances.numberOfAssetTypes, (uint64_t)ASSET_COUNT); + for (uint64 i = 0; i < ASSET_COUNT; ++i) + { + // Verify each asset has a balance of 200 (deposited twice) + EXPECT_EQ(vaultBalances.assetBalances.get(i).balance, depositAmount * 2); + } + + // From Vault 0, owners 0, 1, and 2 approve a release + const id releaseDestination = users[15]; + const Asset assetToRelease = assets[0]; // Release ASSET_0 + const sint64 releaseAmount = 75; + + // A 3-of-4 vault, so we need 3 approvals + EXPECT_EQ(msvault.releaseAssetTo(targetVaultId, assetToRelease, releaseAmount, releaseDestination, users[0]).status, 9ULL); + EXPECT_EQ(msvault.releaseAssetTo(targetVaultId, assetToRelease, releaseAmount, releaseDestination, users[1]).status, 9ULL); + EXPECT_EQ(msvault.releaseAssetTo(targetVaultId, assetToRelease, releaseAmount, releaseDestination, users[2]).status, 1ULL); + + // Check destination on-chain balance + sint64 destBalance = numberOfShares(assetToRelease, { releaseDestination, QX_CONTRACT_INDEX }, + { releaseDestination, QX_CONTRACT_INDEX }); + EXPECT_EQ(destBalance, releaseAmount); + + // Check vault's internal accounting for the released asset + vaultBalances = msvault.getVaultAssetBalances(targetVaultId); + bool foundReleasedAsset = false; + for (uint64 i = 0; i < vaultBalances.numberOfAssetTypes; ++i) + { + auto bal = vaultBalances.assetBalances.get(i); + if (bal.asset.assetName == assetToRelease.assetName && bal.asset.issuer == assetToRelease.issuer) + { + // Expected balance is (100 * 2) - 75 = 125 + EXPECT_EQ(bal.balance, (depositAmount * 2) - releaseAmount); + foundReleasedAsset = true; + break; + } + } + EXPECT_TRUE(foundReleasedAsset); + + // Check MsVault's on-chain balance for the released asset + sint64 scOnChainBalance = numberOfShares(assetToRelease, { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }, + { id(MSVAULT_CONTRACT_INDEX, 0, 0, 0), MSVAULT_CONTRACT_INDEX }); + // The total on-chain balance should also be (100 * 2) - 75 = 125 + EXPECT_EQ(scOnChainBalance, (depositAmount * 2) - releaseAmount); +} + +TEST(ContractMsVault, RevokeAssetManagementRights_Success) +{ + ContractTestingMsVault msvault; + + const id USER = OWNER1; + const Asset asset = { USER, assetNameFromString("REVOKE") }; + const sint64 initialShares = 10000; + const sint64 sharesToManage = 4000; + const sint64 sharesToRevoke = 3000; + + // Issue asset and transfer management rights to MsVault + increaseEnergy(USER, QX_ISSUE_ASSET_FEE + 1000000 + 100); // Energy for all fees + msvault.issueAsset(USER, "REVOKE", initialShares); + + // Verify initial state: all shares managed by QX + EXPECT_EQ(numberOfShares(asset, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }), initialShares); + EXPECT_EQ(numberOfShares(asset, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }), 0); + + // User gives MsVault management rights over a portion of their shares + msvault.transferShareManagementRights(USER, asset, sharesToManage, MSVAULT_CONTRACT_INDEX); + + // Verify intermediate state: rights are split between QX and MsVault + EXPECT_EQ(numberOfShares(asset, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }), initialShares - sharesToManage); + EXPECT_EQ(numberOfShares(asset, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }), sharesToManage); + + // User revokes a portion of the managed rights from MsVault. The helper now handles the fee. + auto revokeOut = msvault.revokeAssetManagementRights(USER, asset, sharesToRevoke); - revenueOutput = msVault.getRevenueInfo(); + // Verify the outcome + EXPECT_EQ(revokeOut.status, 1ULL); + EXPECT_EQ(revokeOut.transferredNumberOfShares, sharesToRevoke); - auto total_revenue = MSVAULT_REGISTERING_FEE * 2 + MSVAULT_HOLDING_FEE; - EXPECT_EQ(revenueOutput.totalRevenue, total_revenue); - EXPECT_EQ(revenueOutput.totalDistributedToShareholders, ((int)(total_revenue) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS); - EXPECT_EQ(revenueOutput.numberOfActiveVaults, 1U); + // The amount managed by MsVault should decrease + sint64 finalManagedByMsVault = numberOfShares(asset, { USER, MSVAULT_CONTRACT_INDEX }, { USER, MSVAULT_CONTRACT_INDEX }); + EXPECT_EQ(finalManagedByMsVault, sharesToManage - sharesToRevoke); // 4000 - 3000 = 1000 + // The amount managed by QX should increase accordingly + sint64 finalManagedByQx = numberOfShares(asset, { USER, QX_CONTRACT_INDEX }, { USER, QX_CONTRACT_INDEX }); + EXPECT_EQ(finalManagedByQx, (initialShares - sharesToManage) + sharesToRevoke); // 6000 + 3000 = 9000 } diff --git a/test/contract_nostromo.cpp b/test/contract_nostromo.cpp index a1148e20f..c102cd5e3 100644 --- a/test/contract_nostromo.cpp +++ b/test/contract_nostromo.cpp @@ -227,7 +227,7 @@ class NostromoChecker : public NOST assetInfo.assetName = assetName; assetInfo.issuer = id(NOST_CONTRACT_INDEX, 0, 0, 0); EXPECT_EQ(numberOfShares(assetInfo), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken); - EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, 13, 13), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - fundaraisings.get(indexOfFundraising).soldAmount); + EXPECT_EQ(numberOfPossessedShares(assetName, id(NOST_CONTRACT_INDEX, 0, 0, 0), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).creator, NOST_CONTRACT_INDEX, NOST_CONTRACT_INDEX), projects.get(fundaraisings.get(indexOfFundraising).indexOfProject).supplyOfToken - fundaraisings.get(indexOfFundraising).soldAmount); } } void endEpochSucceedFundraisingChecker(id creator, uint32 indexOfFundraising, uint64 totalInvestedFund, uint64 originalCreatorBalance, uint64 assetName) diff --git a/test/contract_qbond.cpp b/test/contract_qbond.cpp new file mode 100644 index 000000000..762f08258 --- /dev/null +++ b/test/contract_qbond.cpp @@ -0,0 +1,432 @@ +#define NO_UEFI + +#include "contract_testing.h" + +std::string assetNameFromInt64(uint64 assetName); +const id adminAddress = ID(_B, _O, _N, _D, _A, _A, _F, _B, _U, _G, _H, _E, _L, _A, _N, _X, _G, _H, _N, _L, _M, _S, _U, _I, _V, _B, _K, _B, _H, _A, _Y, _E, _Q, _S, _Q, _B, _V, _P, _V, _N, _B, _H, _L, _F, _J, _I, _A, _Z, _F, _Q, _C, _W, _W, _B, _V, _E); +const id testAddress1 = ID(_H, _O, _G, _T, _K, _D, _N, _D, _V, _U, _U, _Z, _U, _F, _L, _A, _M, _L, _V, _B, _L, _Z, _D, _S, _G, _D, _D, _A, _E, _B, _E, _K, _K, _L, _N, _Z, _J, _B, _W, _S, _C, _A, _M, _D, _S, _X, _T, _C, _X, _A, _M, _A, _X, _U, _D, _F); +const id testAddress2 = ID(_E, _Q, _M, _B, _B, _V, _Y, _G, _Z, _O, _F, _U, _I, _H, _E, _X, _F, _O, _X, _K, _T, _F, _T, _A, _N, _E, _K, _B, _X, _L, _B, _X, _H, _A, _Y, _D, _F, _F, _M, _R, _E, _E, _M, _R, _Q, _E, _V, _A, _D, _Y, _M, _M, _E, _W, _A, _C); + +class QBondChecker : public QBOND +{ +public: + int64_t getCFAPopulation() + { + return _commissionFreeAddresses.population(); + } +}; + +class ContractTestingQBond : protected ContractTesting +{ +public: + ContractTestingQBond() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QBOND); + callSystemProcedure(QBOND_CONTRACT_INDEX, INITIALIZE); + INIT_CONTRACT(QEARN); + callSystemProcedure(QEARN_CONTRACT_INDEX, INITIALIZE); + } + + QBondChecker* getState() + { + return (QBondChecker*)contractStates[QBOND_CONTRACT_INDEX]; + } + + void beginEpoch(bool expectSuccess = true) + { + callSystemProcedure(QBOND_CONTRACT_INDEX, BEGIN_EPOCH, expectSuccess); + } + + void endEpoch(bool expectSuccess = true) + { + callSystemProcedure(QBOND_CONTRACT_INDEX, END_EPOCH, expectSuccess); + } + + void stake(const id& staker, const int64_t& quMillions, const int64_t& quAmount) + { + QBOND::Stake_input input{ quMillions }; + QBOND::Stake_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 1, input, output, staker, quAmount); + } + + QBOND::TransferMBondOwnershipAndPossession_output transfer(const id& from, const id& to, const uint16_t& epoch, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::TransferMBondOwnershipAndPossession_input input{ to, epoch, mbondsAmount }; + QBOND::TransferMBondOwnershipAndPossession_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 2, input, output, from, quAmount); + return output; + } + + QBOND::AddAskOrder_output addAskOrder(const id& asker, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::AddAskOrder_input input{ epoch, price, mbondsAmount }; + QBOND::AddAskOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 3, input, output, asker, quAmount); + return output; + } + + QBOND::RemoveAskOrder_output removeAskOrder(const id& asker, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::RemoveAskOrder_input input{ epoch, price, mbondsAmount }; + QBOND::RemoveAskOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 4, input, output, asker, quAmount); + return output; + } + + QBOND::AddBidOrder_output addBidOrder(const id& bider, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::AddBidOrder_input input{ epoch, price, mbondsAmount }; + QBOND::AddBidOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 5, input, output, bider, quAmount); + return output; + } + + QBOND::RemoveBidOrder_output removeBidOrder(const id& bider, const uint16_t& epoch, const int64_t& price, const int64_t& mbondsAmount, const int64_t& quAmount) + { + QBOND::RemoveBidOrder_input input{ epoch, price, mbondsAmount }; + QBOND::RemoveBidOrder_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 6, input, output, bider, quAmount); + return output; + } + + QBOND::BurnQU_output burnQU(const id& invocator, const int64_t& quToBurn, const int64_t& quAmount) + { + QBOND::BurnQU_input input{ quToBurn }; + QBOND::BurnQU_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 7, input, output, invocator, quAmount); + return output; + } + + bool updateCFA(const id& invocator, const id& address, const bool operation) + { + QBOND::UpdateCFA_input input{ address, operation }; + QBOND::UpdateCFA_output output; + invokeUserProcedure(QBOND_CONTRACT_INDEX, 8, input, output, invocator, 0); + return output.result; + } + + QBOND::GetEarnedFees_output getEarnedFees() + { + QBOND::GetEarnedFees_input input; + QBOND::GetEarnedFees_output output; + callFunction(QBOND_CONTRACT_INDEX, 2, input, output); + return output; + } + + QBOND::GetInfoPerEpoch_output getInfoPerEpoch(const uint16_t& epoch) + { + QBOND::GetInfoPerEpoch_input input{ epoch }; + QBOND::GetInfoPerEpoch_output output; + callFunction(QBOND_CONTRACT_INDEX, 3, input, output); + return output; + } + + QBOND::GetOrders_output getOrders(const uint16_t& epoch, const int64_t& asksOffset, const int64_t& bidsOffset) + { + QBOND::GetOrders_input input{ epoch, asksOffset, bidsOffset }; + QBOND::GetOrders_output output; + callFunction(QBOND_CONTRACT_INDEX, 4, input, output); + return output; + } + + QBOND::GetUserOrders_output getUserOrders(const id& user, const int64_t& asksOffset, const int64_t& bidsOffset) + { + QBOND::GetUserOrders_input input{ user, asksOffset, bidsOffset }; + QBOND::GetUserOrders_output output; + callFunction(QBOND_CONTRACT_INDEX, 5, input, output); + return output; + } + + QBOND::GetMBondsTable_output getMBondsTable() + { + QBOND::GetMBondsTable_input input; + QBOND::GetMBondsTable_output output; + callFunction(QBOND_CONTRACT_INDEX, 6, input, output); + return output; + } + + QBOND::GetUserMBonds_output getUserMBonds(const id& user) + { + QBOND::GetUserMBonds_input input{ user }; + QBOND::GetUserMBonds_output output; + callFunction(QBOND_CONTRACT_INDEX, 7, input, output); + return output; + } + + QBOND::GetCFA_output getCFA() + { + QBOND::GetCFA_input input; + QBOND::GetCFA_output output; + callFunction(QBOND_CONTRACT_INDEX, 8, input, output); + return output; + } +}; + +TEST(ContractQBond, Stake) +{ + system.epoch = QBOND_START_EPOCH; + ContractTestingQBond qbond; + qbond.beginEpoch(); + + increaseEnergy(testAddress1, 100000000LL); + increaseEnergy(testAddress2, 100000000LL); + + // scenario 1: testAddress1 want to stake 50 millions, but send to sc 30 millions + qbond.stake(testAddress1, 50, 30000000LL); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 2: testAddress1 want to stake 50 millions, but send to sc 50 millions (without commission) + qbond.stake(testAddress1, 50, 50000000LL); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 3: testAddress1 want to stake 50 millions and send full amount with commission + qbond.stake(testAddress1, 50, 50250000LL); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 50LL); + + // scenario 4.1: testAddress2 want to stake 5 millions, recieve 0 MBonds, because minimum is 10 and 5 were put in queue + qbond.stake(testAddress2, 5, 5025000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 4.2: testAddress1 want to stake 7 millions, testAddress1 recieve 7 MBonds and testAddress2 recieve 5 MBonds, because the total qu millions in the queue became more than 10 + qbond.stake(testAddress1, 7, 7035000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 57); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 5); +} + + +TEST(ContractQBond, TransferMBondOwnershipAndPossession) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + qbond.stake(testAddress1, 50, 50250000); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 50); + + // scenario 1: not enough gas, 100 needed + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 10, 50).transferredMBonds, 0); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 2: enough gas, not enough mbonds + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 70, 100).transferredMBonds, 0); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + + // scenario 3: success + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 40, 100).transferredMBonds, 40); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 40); +} + +TEST(ContractQBond, AddRemoveAskOrder) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + qbond.stake(testAddress1, 50, 50250000); + + // scenario 1: not enough mbonds + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1500000, 100, 0).addedMBondsAmount, 0); + + // scenario 2: success to add ask, asked mbonds are blocked and cannot be transferred to another address + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1500000, 30, 0).addedMBondsAmount, 30); + // not enough free mbonds + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 21, 100).transferredMBonds, 0); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 0); + // successful transfer + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 20, 100).transferredMBonds, 20); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 20); + + // scenario 3: no orders to remove at this price + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1400000, 30, 0).removedMBondsAmount, 0); + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1600000, 30, 0).removedMBondsAmount, 0); + + // scenario 4: no free mbonds, then successful removal ask order and transfer to another address + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 1, 100).transferredMBonds, 0); + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1500000, 5, 0).removedMBondsAmount, 5); + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 5, 100).transferredMBonds, 5); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 25); + + EXPECT_EQ(qbond.removeAskOrder(testAddress1, system.epoch, 1500000, 500, 0).removedMBondsAmount, 25); +} + +TEST(ContractQBond, AddRemoveBidOrder) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + increaseEnergy(testAddress2, 1000000000); + qbond.stake(testAddress1, 50, 50250000); + + // scenario 1: not enough qu + EXPECT_EQ(qbond.addBidOrder(testAddress2, system.epoch, 1500000, 10, 100).addedMBondsAmount, 0); + + // scenario 2: success to add bid + EXPECT_EQ(qbond.addBidOrder(testAddress2, system.epoch, 1500000, 10, 15000000).addedMBondsAmount, 10); + + // scenario 3: testAddress1 add ask order which matches the bid order + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1500000, 3, 0).addedMBondsAmount, 3); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress1, testAddress1, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 47); + EXPECT_EQ(numberOfPossessedShares(assetNameFromString(std::string("MBND").append(std::to_string(system.epoch)).c_str()), id(QBOND_CONTRACT_INDEX, 0, 0, 0), testAddress2, testAddress2, QBOND_CONTRACT_INDEX, QBOND_CONTRACT_INDEX), 3); + + // scenario 3: no orders to remove at this price + EXPECT_EQ(qbond.removeBidOrder(testAddress2, system.epoch, 1400000, 30, 0).removedMBondsAmount, 0); + EXPECT_EQ(qbond.removeBidOrder(testAddress2, system.epoch, 1600000, 30, 0).removedMBondsAmount, 0); + + // scenario 4: successful removal bid order, qu are returned (7 mbonds per 1500000 each) + int64_t prevBalance = getBalance(testAddress2); + EXPECT_EQ(qbond.removeBidOrder(testAddress2, system.epoch, 1500000, 100, 0).removedMBondsAmount, 7); + EXPECT_EQ(getBalance(testAddress2) - prevBalance, 10500000); + + // check earned fees + auto fees = qbond.getEarnedFees(); + EXPECT_EQ(fees.stakeFees, 250000); + EXPECT_EQ(fees.tradeFees, 1350); // 1500000 (MBond price) * 3 (MBonds) * 0.0003 (0.03% fees for trade) + + // getOrders checks + EXPECT_EQ(qbond.addAskOrder(testAddress1, system.epoch, 1600000, 5, 0).addedMBondsAmount, 5); + EXPECT_EQ(qbond.addAskOrder(testAddress2, system.epoch, 1500000, 3, 0).addedMBondsAmount, 3); + EXPECT_EQ(qbond.addBidOrder(testAddress1, system.epoch, 1400000, 10, 14000000).addedMBondsAmount, 10); + EXPECT_EQ(qbond.addBidOrder(testAddress2, system.epoch, 1300000, 5, 6500000).addedMBondsAmount, 5); + + // all orders sorted by price, therefore the element with index 0 contains an order with a price of 1500000 + auto orders = qbond.getOrders(system.epoch, 0, 0); + EXPECT_EQ(orders.askOrders.get(0).epoch, 182); + EXPECT_EQ(orders.askOrders.get(0).numberOfMBonds, 3); + EXPECT_EQ(orders.askOrders.get(0).owner, testAddress2); + EXPECT_EQ(orders.askOrders.get(0).price, 1500000); + + EXPECT_EQ(orders.bidOrders.get(0).epoch, 182); + EXPECT_EQ(orders.bidOrders.get(0).numberOfMBonds, 10); + EXPECT_EQ(orders.bidOrders.get(0).owner, testAddress1); + EXPECT_EQ(orders.bidOrders.get(0).price, 1400000); + + // with offset + orders = qbond.getOrders(system.epoch, 1, 1); + EXPECT_EQ(orders.askOrders.get(0).epoch, 182); + EXPECT_EQ(orders.askOrders.get(0).numberOfMBonds, 5); + EXPECT_EQ(orders.askOrders.get(0).owner, testAddress1); + EXPECT_EQ(orders.askOrders.get(0).price, 1600000); + + EXPECT_EQ(orders.bidOrders.get(0).epoch, 182); + EXPECT_EQ(orders.bidOrders.get(0).numberOfMBonds, 5); + EXPECT_EQ(orders.bidOrders.get(0).owner, testAddress2); + EXPECT_EQ(orders.bidOrders.get(0).price, 1300000); + + // user orders + auto userOrders = qbond.getUserOrders(testAddress1, 0, 0); + EXPECT_EQ(userOrders.askOrders.get(0).epoch, 182); + EXPECT_EQ(userOrders.askOrders.get(0).numberOfMBonds, 5); + EXPECT_EQ(userOrders.askOrders.get(0).owner, testAddress1); + EXPECT_EQ(userOrders.askOrders.get(0).price, 1600000); + + EXPECT_EQ(userOrders.bidOrders.get(0).epoch, 182); + EXPECT_EQ(userOrders.bidOrders.get(0).numberOfMBonds, 10); + EXPECT_EQ(userOrders.bidOrders.get(0).owner, testAddress1); + EXPECT_EQ(userOrders.bidOrders.get(0).price, 1400000); + + // with offset + userOrders = qbond.getUserOrders(testAddress1, 1, 1); + EXPECT_EQ(userOrders.askOrders.get(0).epoch, 0); + EXPECT_EQ(userOrders.askOrders.get(0).numberOfMBonds, 0); + EXPECT_EQ(userOrders.askOrders.get(0).owner, NULL_ID); + EXPECT_EQ(userOrders.askOrders.get(0).price, 0); + + EXPECT_EQ(userOrders.bidOrders.get(0).epoch, 0); + EXPECT_EQ(userOrders.bidOrders.get(0).numberOfMBonds, 0); + EXPECT_EQ(userOrders.bidOrders.get(0).owner, NULL_ID); + EXPECT_EQ(userOrders.bidOrders.get(0).price, 0); +} + +TEST(ContractQBond, BurnQu) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + + // scenario 1: not enough qu + EXPECT_EQ(qbond.burnQU(testAddress1, 1000000, 1000).amount, -1); + + // scenario 2: successful burning + EXPECT_EQ(qbond.burnQU(testAddress1, 1000000, 1000000).amount, 1000000); + + // scenario 3: successful burning, the surplus is returned + int64_t prevBalance = getBalance(testAddress1); + EXPECT_EQ(qbond.burnQU(testAddress1, 1000000, 10000000).amount, 1000000); + EXPECT_EQ(prevBalance - getBalance(testAddress1), 1000000); +} + +TEST(ContractQBond, UpdateCFA) +{ + ContractTestingQBond qbond; + increaseEnergy(testAddress1, 1000); + increaseEnergy(adminAddress, 1000); + + // only adminAddress can update CFA + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 1); + EXPECT_FALSE(qbond.updateCFA(testAddress1, testAddress2, 1)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 1); + EXPECT_TRUE(qbond.updateCFA(adminAddress, testAddress2, 1)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 2); + + auto cfa = qbond.getCFA(); + EXPECT_EQ(cfa.commissionFreeAddresses.get(0), testAddress2); + EXPECT_EQ(cfa.commissionFreeAddresses.get(1), adminAddress); + EXPECT_EQ(cfa.commissionFreeAddresses.get(2), NULL_ID); + + EXPECT_FALSE(qbond.updateCFA(testAddress1, testAddress2, 0)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 2); + EXPECT_TRUE(qbond.updateCFA(adminAddress, testAddress2, 0)); + EXPECT_EQ(qbond.getState()->getCFAPopulation(), 1); +} + +TEST(ContractQBond, GetInfoPerEpoch) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + increaseEnergy(testAddress2, 1000000000); + + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 0); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 0); + + qbond.stake(testAddress1, 50, 50250000); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 1); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 50); + + qbond.stake(testAddress2, 100, 100500000); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 2); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 150); + + EXPECT_EQ(qbond.transfer(testAddress1, testAddress2, system.epoch, 50, 100).transferredMBonds, 50); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).stakersAmount, 1); + EXPECT_EQ(qbond.getInfoPerEpoch(system.epoch).totalStaked, 150); +} + +TEST(ContractQBond, GetMBondsTable) +{ + ContractTestingQBond qbond; + qbond.beginEpoch(); + increaseEnergy(testAddress1, 1000000000); + increaseEnergy(testAddress2, 1000000000); + + qbond.stake(testAddress1, 50, 50250000); + qbond.stake(testAddress2, 100, 100500000); + qbond.endEpoch(); + + system.epoch++; + qbond.beginEpoch(); + qbond.stake(testAddress1, 10, 10050000); + qbond.stake(testAddress2, 20, 20100000); + + auto table = qbond.getMBondsTable(); + EXPECT_EQ(table.info.get(0).epoch, 182); + EXPECT_EQ(table.info.get(1).epoch, 183); + EXPECT_EQ(table.info.get(2).epoch, 0); + + auto userMBonds = qbond.getUserMBonds(testAddress1); + EXPECT_EQ(userMBonds.totalMBondsAmount, 60); + EXPECT_EQ(userMBonds.mbonds.get(0).epoch, 182); + EXPECT_EQ(userMBonds.mbonds.get(0).amount, 50); + EXPECT_EQ(userMBonds.mbonds.get(1).epoch, 183); + EXPECT_EQ(userMBonds.mbonds.get(1).amount, 10); +} diff --git a/test/contract_qswap.cpp b/test/contract_qswap.cpp index 4daffc022..78c1a4296 100644 --- a/test/contract_qswap.cpp +++ b/test/contract_qswap.cpp @@ -12,7 +12,7 @@ static const id QSWAP_CONTRACT_ID(QSWAP_CONTRACT_INDEX, 0, 0, 0); //constexpr uint32 SWAP_FEE_IDX = 1; constexpr uint32 GET_POOL_BASIC_STATE_IDX = 2; -constexpr uint32 GET_LIQUDITY_OF_IDX = 3; +constexpr uint32 GET_LIQUIDITY_OF_IDX = 3; constexpr uint32 QUOTE_EXACT_QU_INPUT_IDX = 4; constexpr uint32 QUOTE_EXACT_QU_OUTPUT_IDX = 5; constexpr uint32 QUOTE_EXACT_ASSET_INPUT_IDX = 6; @@ -22,13 +22,14 @@ constexpr uint32 TEAM_INFO_IDX = 8; constexpr uint32 ISSUE_ASSET_IDX = 1; constexpr uint32 TRANSFER_SHARE_OWNERSHIP_AND_POSSESSION_IDX = 2; constexpr uint32 CREATE_POOL_IDX = 3; -constexpr uint32 ADD_LIQUDITY_IDX = 4; -constexpr uint32 REMOVE_LIQUDITY_IDX = 5; +constexpr uint32 ADD_LIQUIDITY_IDX = 4; +constexpr uint32 REMOVE_LIQUIDITY_IDX = 5; constexpr uint32 SWAP_EXACT_QU_FOR_ASSET_IDX = 6; constexpr uint32 SWAP_QU_FOR_EXACT_ASSET_IDX = 7; constexpr uint32 SWAP_EXACT_ASSET_FOR_QU_IDX = 8; constexpr uint32 SWAP_ASSET_FOR_EXACT_QU_IDX = 9; constexpr uint32 SET_TEAM_INFO_IDX = 10; +constexpr uint32 TRANSFER_SHARE_MANAGEMENT_RIGHTS_IDX = 11; class QswapChecker : public QSWAP @@ -62,39 +63,45 @@ class ContractTestingQswap : protected ContractTesting return load(filename, sizeof(QSWAP), contractStates[QSWAP_CONTRACT_INDEX]) == sizeof(QSWAP); } - QSWAP::TeamInfo_output teamInfo(){ + QSWAP::TeamInfo_output teamInfo() + { QSWAP::TeamInfo_input input{}; QSWAP::TeamInfo_output output; callFunction(QSWAP_CONTRACT_INDEX, TEAM_INFO_IDX, input, output); return output; } - bool setTeamId(const id& issuer, QSWAP::SetTeamInfo_input input){ + bool setTeamId(const id& issuer, QSWAP::SetTeamInfo_input input) + { QSWAP::CreatePool_output output; invokeUserProcedure(QSWAP_CONTRACT_INDEX, SET_TEAM_INFO_IDX, input, output, issuer, 0); return output.success; } - sint64 issueAsset(const id& issuer, QSWAP::IssueAsset_input input){ + sint64 issueAsset(const id& issuer, QSWAP::IssueAsset_input input) + { QSWAP::IssueAsset_output output; invokeUserProcedure(QSWAP_CONTRACT_INDEX, ISSUE_ASSET_IDX, input, output, issuer, QSWAP_ISSUE_ASSET_FEE); return output.issuedNumberOfShares; } - sint64 transferAsset(const id& issuer, QSWAP::TransferShareOwnershipAndPossession_input input){ + sint64 transferAsset(const id& issuer, QSWAP::TransferShareOwnershipAndPossession_input input) + { QSWAP::TransferShareOwnershipAndPossession_output output; invokeUserProcedure(QSWAP_CONTRACT_INDEX, TRANSFER_SHARE_OWNERSHIP_AND_POSSESSION_IDX, input, output, issuer, QSWAP_TRANSFER_ASSET_FEE); return output.transferredAmount; } - bool createPool(const id& issuer, uint64 assetName){ + bool createPool(const id& issuer, uint64 assetName) + { QSWAP::CreatePool_input input{issuer, assetName}; QSWAP::CreatePool_output output; invokeUserProcedure(QSWAP_CONTRACT_INDEX, CREATE_POOL_IDX, input, output, issuer, QSWAP_CREATE_POOL_FEE); return output.success; } - QSWAP::GetPoolBasicState_output getPoolBasicState(const id& issuer, uint64 assetName){ + QSWAP::GetPoolBasicState_output getPoolBasicState(const id& issuer, uint64 assetName) + { QSWAP::GetPoolBasicState_input input{issuer, assetName}; QSWAP::GetPoolBasicState_output output; @@ -102,11 +109,12 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::AddLiqudity_output addLiqudity(const id& issuer, QSWAP::AddLiqudity_input input, uint64 inputValue){ - QSWAP::AddLiqudity_output output; + QSWAP::AddLiquidity_output addLiquidity(const id& issuer, QSWAP::AddLiquidity_input input, uint64 inputValue) + { + QSWAP::AddLiquidity_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, - ADD_LIQUDITY_IDX, + ADD_LIQUIDITY_IDX, input, output, issuer, @@ -115,11 +123,12 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::RemoveLiqudity_output removeLiqudity(const id& issuer, QSWAP::RemoveLiqudity_input input, uint64 inputValue){ - QSWAP::RemoveLiqudity_output output; + QSWAP::RemoveLiquidity_output removeLiquidity(const id& issuer, QSWAP::RemoveLiquidity_input input, uint64 inputValue) + { + QSWAP::RemoveLiquidity_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, - REMOVE_LIQUDITY_IDX, + REMOVE_LIQUIDITY_IDX, input, output, issuer, @@ -128,13 +137,15 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::GetLiqudityOf_output getLiqudityOf(QSWAP::GetLiqudityOf_input input){ - QSWAP::GetLiqudityOf_output output; - callFunction(QSWAP_CONTRACT_INDEX, GET_LIQUDITY_OF_IDX, input, output); + QSWAP::GetLiquidityOf_output getLiquidityOf(QSWAP::GetLiquidityOf_input input) + { + QSWAP::GetLiquidityOf_output output; + callFunction(QSWAP_CONTRACT_INDEX, GET_LIQUIDITY_OF_IDX, input, output); return output; } - QSWAP::SwapExactQuForAsset_output swapExactQuForAsset( const id& issuer, QSWAP::SwapExactQuForAsset_input input, uint64 inputValue) { + QSWAP::SwapExactQuForAsset_output swapExactQuForAsset( const id& issuer, QSWAP::SwapExactQuForAsset_input input, uint64 inputValue) + { QSWAP::SwapExactQuForAsset_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -148,7 +159,8 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::SwapQuForExactAsset_output swapQuForExactAsset( const id& issuer, QSWAP::SwapQuForExactAsset_input input, uint64 inputValue) { + QSWAP::SwapQuForExactAsset_output swapQuForExactAsset( const id& issuer, QSWAP::SwapQuForExactAsset_input input, uint64 inputValue) + { QSWAP::SwapQuForExactAsset_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -162,7 +174,8 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::SwapExactAssetForQu_output swapExactAssetForQu(const id& issuer, QSWAP::SwapExactAssetForQu_input input, uint64 inputValue) { + QSWAP::SwapExactAssetForQu_output swapExactAssetForQu(const id& issuer, QSWAP::SwapExactAssetForQu_input input, uint64 inputValue) + { QSWAP::SwapExactAssetForQu_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -176,7 +189,8 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::SwapAssetForExactQu_output swapAssetForExactQu(const id& issuer, QSWAP::SwapAssetForExactQu_input input, uint64 inputValue) { + QSWAP::SwapAssetForExactQu_output swapAssetForExactQu(const id& issuer, QSWAP::SwapAssetForExactQu_input input, uint64 inputValue) + { QSWAP::SwapAssetForExactQu_output output; invokeUserProcedure( QSWAP_CONTRACT_INDEX, @@ -190,25 +204,36 @@ class ContractTestingQswap : protected ContractTesting return output; } - QSWAP::QuoteExactQuInput_output quoteExactQuInput(QSWAP::QuoteExactQuInput_input input) { + QSWAP::TransferShareManagementRights_output transferShareManagementRights(const id& invocator, QSWAP::TransferShareManagementRights_input input, uint64 inputValue) + { + QSWAP::TransferShareManagementRights_output output; + invokeUserProcedure(QSWAP_CONTRACT_INDEX, TRANSFER_SHARE_MANAGEMENT_RIGHTS_IDX, input, output, invocator, inputValue); + return output; + } + + QSWAP::QuoteExactQuInput_output quoteExactQuInput(QSWAP::QuoteExactQuInput_input input) + { QSWAP::QuoteExactQuInput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_QU_INPUT_IDX, input, output); return output; } - QSWAP::QuoteExactQuOutput_output quoteExactQuOutput(QSWAP::QuoteExactQuOutput_input input) { + QSWAP::QuoteExactQuOutput_output quoteExactQuOutput(QSWAP::QuoteExactQuOutput_input input) + { QSWAP::QuoteExactQuOutput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_QU_OUTPUT_IDX, input, output); return output; } - QSWAP::QuoteExactAssetInput_output quoteExactAssetInput(QSWAP::QuoteExactAssetInput_input input){ + QSWAP::QuoteExactAssetInput_output quoteExactAssetInput(QSWAP::QuoteExactAssetInput_input input) + { QSWAP::QuoteExactAssetInput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_ASSET_INPUT_IDX, input, output); return output; } - QSWAP::QuoteExactAssetOutput_output quoteExactAssetOutput(QSWAP::QuoteExactAssetOutput_input input){ + QSWAP::QuoteExactAssetOutput_output quoteExactAssetOutput(QSWAP::QuoteExactAssetOutput_input input) + { QSWAP::QuoteExactAssetOutput_output output; callFunction(QSWAP_CONTRACT_INDEX, QUOTE_EXACT_ASSET_OUTPUT_IDX, input, output); return output; @@ -263,7 +288,7 @@ TEST(ContractSwap, QuoteTest) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -275,8 +300,8 @@ TEST(ContractSwap, QuoteTest) sint64 inputValue = 30*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 30*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 30*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); QSWAP::QuoteExactQuInput_input qi_input = {issuer, assetName, 1000}; QSWAP::QuoteExactQuInput_output qi_output = qswap.quoteExactQuInput(qi_input); @@ -306,7 +331,7 @@ TEST(ContractSwap, QuoteTest) 2. issue duplicate asset 3. issue asset with invalid input params, such as numberOfShares: 0 */ -TEST(ContractSwap, IssueAsset) +TEST(ContractSwap, IssueAssetAndTransferShareManagementRights) { ContractTestingQswap qswap; @@ -332,6 +357,15 @@ TEST(ContractSwap, IssueAsset) EXPECT_EQ(qswap.transferAsset(issuer, ts_input), transferAmount); EXPECT_EQ(numberOfPossessedShares(assetName, issuer, newId, newId, QSWAP_CONTRACT_INDEX, QSWAP_CONTRACT_INDEX), transferAmount); // printf("%lld\n", getBalance(QSWAP_CONTRACT_ID)); + increaseEnergy(issuer, 100); + uint64 qswapIdBalance = getBalance(QSWAP_CONTRACT_ID); + uint64 issuerBalance = getBalance(issuer); + QSWAP::TransferShareManagementRights_input tsr_input = {Asset{issuer, assetName}, transferAmount, QX_CONTRACT_INDEX}; + EXPECT_EQ(qswap.transferShareManagementRights(issuer, tsr_input, 100).transferredNumberOfShares, transferAmount); + EXPECT_EQ(getBalance(id(QX_CONTRACT_INDEX, 0, 0, 0)), 100); + EXPECT_EQ(getBalance(QSWAP_CONTRACT_ID), qswapIdBalance); + EXPECT_EQ(getBalance(issuer), issuerBalance - 100); + EXPECT_EQ(numberOfPossessedShares(assetName, issuer, issuer, issuer, QX_CONTRACT_INDEX, QX_CONTRACT_INDEX), transferAmount); } // 1. not enough energy for asset issue fee @@ -370,7 +404,7 @@ TEST(ContractSwap, SwapExactQuForAsset) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -382,9 +416,9 @@ TEST(ContractSwap, SwapExactQuForAsset) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); } { @@ -406,11 +440,11 @@ TEST(ContractSwap, SwapExactQuForAsset) EXPECT_TRUE(output.assetAmountOut <= 50000); // 49924 if swapFee 0.3% QSWAP::GetPoolBasicState_output psOutput = qswap.getPoolBasicState(issuer, assetName); - // printf("%lld, %lld, %lld\n", psOutput.reservedAssetAmount, psOutput.reservedQuAmount, psOutput.totalLiqudity); + // printf("%lld, %lld, %lld\n", psOutput.reservedAssetAmount, psOutput.reservedQuAmount, psOutput.totalLiquidity); // swapFee is 200_000 * 0.3% = 600, teamFee: 120, protocolFee: 96 EXPECT_TRUE(psOutput.reservedQuAmount >= 399784); // 399784 = (400_000 - 120 - 96) EXPECT_TRUE(psOutput.reservedAssetAmount >= 50000 ); // 50076 - EXPECT_EQ(psOutput.totalLiqudity, 141421); // liqudity stay the same + EXPECT_EQ(psOutput.totalLiquidity, 141421); // liquidity stay the same } } @@ -422,7 +456,7 @@ TEST(ContractSwap, SwapQuForExactAsset) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -434,9 +468,9 @@ TEST(ContractSwap, SwapQuForExactAsset) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); } { @@ -468,7 +502,7 @@ TEST(ContractSwap, SwapExactAssetForQu) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -480,9 +514,9 @@ TEST(ContractSwap, SwapExactAssetForQu) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); } { @@ -511,7 +545,7 @@ TEST(ContractSwap, SwapAssetForExactQu) uint64 assetName = assetNameFromString("QSWAP0"); sint64 numberOfShares = 10000 * 1000; - // issue an asset and create a pool, and init liqudity + // issue an asset and create a pool, and init liquidity { increaseEnergy(issuer, QSWAP_ISSUE_ASSET_FEE); QSWAP::IssueAsset_input input = { assetName, numberOfShares, 0, 0 }; @@ -523,12 +557,12 @@ TEST(ContractSwap, SwapAssetForExactQu) sint64 inputValue = 200*1000; increaseEnergy(issuer, inputValue); - QSWAP::AddLiqudity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, alInput, inputValue); - // printf("increase liqudity: %lld, %lld, %lld\n", output.userIncreaseLiqudity, output.assetAmount, output.quAmount); + QSWAP::AddLiquidity_input alInput = { issuer, assetName, 100*1000, 0, 0 }; + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, alInput, inputValue); + // printf("increase liquidity: %lld, %lld, %lld\n", output.userIncreaseLiquidity, output.assetAmount, output.quAmount); // QSWAP::GetPoolBasicState_output gp_output = qswap.getPoolBasicState(issuer, assetName); - // printf("%lld, %lld, %lld\n", gp_output.reservedQuAmount, gp_output.reservedAssetAmount, gp_output.totalLiqudity); + // printf("%lld, %lld, %lld\n", gp_output.reservedQuAmount, gp_output.reservedAssetAmount, gp_output.totalLiquidity); } { @@ -600,7 +634,7 @@ TEST(ContractSwap, CreatePool) EXPECT_EQ(output.poolExists, true); EXPECT_EQ(output.reservedQuAmount, 0); EXPECT_EQ(output.reservedAssetAmount, 0); - EXPECT_EQ(output.totalLiqudity, 0); + EXPECT_EQ(output.totalLiquidity, 0); } // 2. create duplicate pool @@ -616,7 +650,7 @@ TEST(ContractSwap, CreatePool) } /* -add liqudity 2 times, and then remove +add liquidity 2 times, and then remove */ TEST(ContractSwap, LiqTest1) { @@ -638,13 +672,13 @@ TEST(ContractSwap, LiqTest1) EXPECT_TRUE(qswap.createPool(issuer, assetName)); } - // 1. add liqudity to a initial pool, first time + // 1. add liquidity to a initial pool, first time { sint64 quStakeAmount = 200*1000; sint64 inputValue = quStakeAmount; sint64 assetStakeAmount = 100*1000; increaseEnergy(issuer, quStakeAmount); - QSWAP::AddLiqudity_input addLiqInput = { + QSWAP::AddLiquidity_input addLiqInput = { issuer, assetName, assetStakeAmount, @@ -652,9 +686,9 @@ TEST(ContractSwap, LiqTest1) 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, addLiqInput, inputValue); - // actually, 141421 liqudity add to the pool, but the first 1000 liqudity is retainedd by the pool rather than the staker - EXPECT_EQ(output.userIncreaseLiqudity, 140421); + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, addLiqInput, inputValue); + // actually, 141421 liquidity add to the pool, but the first 1000 liquidity is retainedd by the pool rather than the staker + EXPECT_EQ(output.userIncreaseLiquidity, 140421); EXPECT_EQ(output.quAmount, 200*1000); EXPECT_EQ(output.assetAmount, 100*1000); @@ -662,18 +696,18 @@ TEST(ContractSwap, LiqTest1) EXPECT_EQ(output2.poolExists, true); EXPECT_EQ(output2.reservedQuAmount, 200*1000); EXPECT_EQ(output2.reservedAssetAmount, 100*1000); - EXPECT_EQ(output2.totalLiqudity, 141421); - // printf("pool state: %lld, %lld, %lld\n", output2.reservedQuAmount, output2.reservedAssetAmount, output2.totalLiqudity); + EXPECT_EQ(output2.totalLiquidity, 141421); + // printf("pool state: %lld, %lld, %lld\n", output2.reservedQuAmount, output2.reservedAssetAmount, output2.totalLiquidity); - QSWAP::GetLiqudityOf_input getLiqInput = { + QSWAP::GetLiquidityOf_input getLiqInput = { issuer, assetName, issuer }; - QSWAP::GetLiqudityOf_output getLiqOutput = qswap.getLiqudityOf(getLiqInput); - EXPECT_EQ(getLiqOutput.liqudity, 140421); + QSWAP::GetLiquidityOf_output getLiqOutput = qswap.getLiquidityOf(getLiqInput); + EXPECT_EQ(getLiqOutput.liquidity, 140421); - // 2. add liqudity second time + // 2. add liquidity second time increaseEnergy(issuer, quStakeAmount); addLiqInput = { issuer, @@ -683,15 +717,15 @@ TEST(ContractSwap, LiqTest1) 0 }; - QSWAP::AddLiqudity_output output3 = qswap.addLiqudity(issuer, addLiqInput, inputValue); - EXPECT_EQ(output3.userIncreaseLiqudity, 141421); + QSWAP::AddLiquidity_output output3 = qswap.addLiquidity(issuer, addLiqInput, inputValue); + EXPECT_EQ(output3.userIncreaseLiquidity, 141421); EXPECT_EQ(output3.quAmount, 200*1000); EXPECT_EQ(output3.assetAmount, 100*1000); - getLiqOutput = qswap.getLiqudityOf(getLiqInput); - EXPECT_EQ(getLiqOutput.liqudity, 281842); // 140421 + 141421 + getLiqOutput = qswap.getLiquidityOf(getLiqInput); + EXPECT_EQ(getLiqOutput.liquidity, 281842); // 140421 + 141421 - QSWAP::RemoveLiqudity_input rmLiqInput = { + QSWAP::RemoveLiquidity_input rmLiqInput = { issuer, assetName, 141421, @@ -699,15 +733,15 @@ TEST(ContractSwap, LiqTest1) 100*1000, // should lte 1000*100 }; - // 3. remove liqudity - QSWAP::RemoveLiqudity_output rmLiqOutput = qswap.removeLiqudity(issuer, rmLiqInput, 0); + // 3. remove liquidity + QSWAP::RemoveLiquidity_output rmLiqOutput = qswap.removeLiquidity(issuer, rmLiqInput, 0); // printf("qu: %lld, asset: %lld\n", rmLiqOutput.quAmount, rmLiqOutput.assetAmount); EXPECT_EQ(rmLiqOutput.quAmount, 1000 * 200); EXPECT_EQ(rmLiqOutput.assetAmount, 1000 * 100); - getLiqOutput = qswap.getLiqudityOf(getLiqInput); - // printf("liq: %lld\n", getLiqOutput.liqudity); - EXPECT_EQ(getLiqOutput.liqudity, 140421); // 281842 - 141421 + getLiqOutput = qswap.getLiquidityOf(getLiqInput); + // printf("liq: %lld\n", getLiqOutput.liquidity); + EXPECT_EQ(getLiqOutput.liquidity, 140421); // 281842 - 141421 } } @@ -732,12 +766,12 @@ TEST(ContractSwap, LiqTest2) EXPECT_TRUE(qswap.createPool(issuer, assetName)); } - // add liqudity to invalid pool, + // add liquidity to invalid pool, { // decreaseEnergy(getBalance(issuer)); uint64 quAmount = 1000; increaseEnergy(issuer, quAmount); - QSWAP::AddLiqudity_input addLiqInput = { + QSWAP::AddLiquidity_input addLiqInput = { issuer, invalidAssetName, 1000, @@ -745,16 +779,16 @@ TEST(ContractSwap, LiqTest2) 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, addLiqInput, 1000); - EXPECT_EQ(output.userIncreaseLiqudity, 0); + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, addLiqInput, 1000); + EXPECT_EQ(output.userIncreaseLiquidity, 0); EXPECT_EQ(output.quAmount, 0); EXPECT_EQ(output.assetAmount, 0); } - // add liqudity with asset more than holdings + // add liquidity with asset more than holdings { increaseEnergy(issuer, 1000); - QSWAP::AddLiqudity_input addLiqInput = { + QSWAP::AddLiquidity_input addLiqInput = { issuer, assetName, 1000*1000 + 100, // excced 1000*1000 @@ -762,8 +796,8 @@ TEST(ContractSwap, LiqTest2) 0 }; - QSWAP::AddLiqudity_output output = qswap.addLiqudity(issuer, addLiqInput, 1000); - EXPECT_EQ(output.userIncreaseLiqudity, 0); + QSWAP::AddLiquidity_output output = qswap.addLiquidity(issuer, addLiqInput, 1000); + EXPECT_EQ(output.userIncreaseLiquidity, 0); EXPECT_EQ(output.quAmount, 0); EXPECT_EQ(output.assetAmount, 0); } diff --git a/test/contract_qutil.cpp b/test/contract_qutil.cpp index 8a6a17296..5b926e14a 100644 --- a/test/contract_qutil.cpp +++ b/test/contract_qutil.cpp @@ -100,6 +100,13 @@ class ContractTestingQUtil : public ContractTesting { callFunction(QUTIL_CONTRACT_INDEX, 6, input, output); return output; } + + QUTIL::DistributeQuToShareholders_output distributeQuToShareholders(const id& invocator, const Asset& asset, sint64 amount) { + QUTIL::DistributeQuToShareholders_input input{ asset }; + QUTIL::DistributeQuToShareholders_output output; + invokeUserProcedure(QUTIL_CONTRACT_INDEX, 7, input, output, invocator, amount); + return output; + } }; // Helper function to generate random ID @@ -1155,8 +1162,8 @@ TEST(QUtilTest, CreatePoll_InvalidGithubLink) { id creator = generateRandomId(); id poll_name = generateRandomId(); uint64_t min_amount = 1000; - // Invalid GitHub link (does not start with "https://github.com/qubic") - Array invalid_github_link = stringToArray("https://github.com/invalidorg/proposal/abc"); + // Invalid GitHub link (does not start with "https://github.com/") + Array invalid_github_link = stringToArray("https://gitlab.com/invalidlink/proposal/abc"); uint64_t poll_type = QUTIL_POLL_TYPE_QUBIC; QUTIL::CreatePoll_input input; @@ -1575,3 +1582,165 @@ TEST(QUtilTest, MultipleVoters_ShareTransfers_EligibilityTest) EXPECT_EQ(result.voter_count.get(1), 2); EXPECT_EQ(result.voter_count.get(2), 3); } + +TEST(QUtilTest, DistributeQuToShareholders) +{ + ContractTestingQUtil qutil; + + id distributor = generateRandomId(); + id issuer = generateRandomId(); + std::vector shareholder(10); + for (auto& v : shareholder) + { + v = generateRandomId(); + } + + increaseEnergy(issuer, 4000000000); // for issuance and transfers + increaseEnergy(distributor, 10000000000); + + // Issue 3 asset + unsigned long long assetNameA = assetNameFromString("ASSETA"); + unsigned long long assetNameB = assetNameFromString("ASSETB"); + unsigned long long assetNameC = assetNameFromString("ASSETC"); + Asset assetA = { issuer, assetNameA }; + Asset assetB = { issuer, assetNameB }; + Asset assetC = { issuer, assetNameC }; + qutil.issueAsset(issuer, assetNameA, 10); + qutil.issueAsset(issuer, assetNameB, 10000); + qutil.issueAsset(issuer, assetNameC, 10000000); + + // Distribute assets + // shareholder 0-2: 1 A, 500 B, 500 C + for (int i = 0; i < 3; i++) + { + qutil.transferAsset(issuer, shareholder[i], assetA, 1); + qutil.transferAsset(issuer, shareholder[i], assetB, 500); + qutil.transferAsset(issuer, shareholder[i], assetC, 600); + } + // shareholder 3-5: 0 A, 2000 B, 500 C + for (int i = 3; i < 6; i++) + { + qutil.transferAsset(issuer, shareholder[i], assetB, 2000); + qutil.transferAsset(issuer, shareholder[i], assetC, 500); + } + // shareholder 6-9: 1 A, 500 B, 0 C + for (int i = 6; i < 10; i++) + { + qutil.transferAsset(issuer, shareholder[i], assetA, 1); + qutil.transferAsset(issuer, shareholder[i], assetB, 500); + } + + QUTIL::DistributeQuToShareholders_output output; + sint64 distributorBalanceBefore, shareholderBalanceBefore; + + // Error case 1: asset without shareholders + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, { distributor, assetNameA }, 10000000); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore); + EXPECT_EQ(output.shareholders, 0); + EXPECT_EQ(output.totalShares, 0); + EXPECT_EQ(output.amountPerShare, 0); + EXPECT_EQ(output.fees, 0); + + // Error case 2: amount too low to pay fee + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, 1); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_LE(output.amountPerShare, 0); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Error case 3: amount too low to pay 1 QU per share + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER + 9); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_EQ(output.amountPerShare, 0); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetA + exactly calculated amount + sint64 amountPerShare = 50; + sint64 totalAmount = 10 * amountPerShare + 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, totalAmount); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + amountPerShare); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetA + amount with some QUs that cannot be evenly distributed and are refundet + amountPerShare = 100; + totalAmount = 10 * amountPerShare + 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetA, totalAmount + 7); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + amountPerShare); + EXPECT_EQ(output.shareholders, 8); + EXPECT_EQ(output.totalShares, 10); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 8 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetB + exactly calculated amount + amountPerShare = 1000; + totalAmount = 10000 * amountPerShare + 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetB, totalAmount); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 500 * amountPerShare); + EXPECT_EQ(output.shareholders, 11); + EXPECT_EQ(output.totalShares, 10000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetB + amount with some QUs that cannot be evenly distributed and are refundet + amountPerShare = 42; + totalAmount = 10000 * amountPerShare + 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetB, totalAmount + 9999); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 500 * amountPerShare); + EXPECT_EQ(output.shareholders, 11); + EXPECT_EQ(output.totalShares, 10000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 11 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetC + exactly calculated amount (fee is minimal) + amountPerShare = 123; + totalAmount = 10000000 * amountPerShare + 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetC, totalAmount); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 600 * amountPerShare); + EXPECT_EQ(output.shareholders, 7); + EXPECT_EQ(output.totalShares, 10000000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); + + // Success case with assetC + non-minimal fee (fee payed too much is donation for running QUTIL -> burned with fee) + amountPerShare = 654; + totalAmount = 10000000 * amountPerShare + 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER; + distributorBalanceBefore = getBalance(distributor); + shareholderBalanceBefore = getBalance(shareholder[0]); + output = qutil.distributeQuToShareholders(distributor, assetC, totalAmount + 123456); + EXPECT_EQ(getBalance(distributor), distributorBalanceBefore - totalAmount); + EXPECT_EQ(getBalance(shareholder[0]), shareholderBalanceBefore + 600 * amountPerShare); + EXPECT_EQ(output.shareholders, 7); + EXPECT_EQ(output.totalShares, 10000000); + EXPECT_EQ(output.amountPerShare, amountPerShare); + EXPECT_EQ(output.fees, 7 * QUTIL_DISTRIBUTE_QU_TO_SHAREHOLDER_FEE_PER_SHAREHOLDER); +} diff --git a/test/contract_rl.cpp b/test/contract_rl.cpp new file mode 100644 index 000000000..5c0bd008b --- /dev/null +++ b/test/contract_rl.cpp @@ -0,0 +1,370 @@ +// File: test/contract_rl.cpp +#define NO_UEFI + +#include "contract_testing.h" + +constexpr uint16 PROCEDURE_INDEX_BUY_TICKET = 1; +constexpr uint16 FUNCTION_INDEX_GET_FEES = 1; +constexpr uint16 FUNCTION_INDEX_GET_PLAYERS = 2; +constexpr uint16 FUNCTION_INDEX_GET_WINNERS = 3; + +static const id RL_DEV_ADDRESS = ID(_Z, _T, _Z, _E, _A, _Q, _G, _U, _P, _I, _K, _T, _X, _F, _Y, _X, _Y, _E, _I, _T, _L, _A, _K, _F, _T, _D, _X, _C, + _R, _L, _W, _E, _T, _H, _N, _G, _H, _D, _Y, _U, _W, _E, _Y, _Q, _N, _Q, _S, _R, _H, _O, _W, _M, _U, _J, _L, _E); + +// Equality operator for comparing WinnerInfo objects +bool operator==(const RL::WinnerInfo& left, const RL::WinnerInfo& right) +{ + return left.winnerAddress == right.winnerAddress && left.revenue == right.revenue && left.epoch == right.epoch && left.tick == right.tick; +} + +// Test helper that exposes internal state assertions +class RLChecker : public RL +{ +public: + void checkTicketPrice(const uint64& price) { EXPECT_EQ(ticketPrice, price); } + + void checkFees(const GetFees_output& fees) + { + EXPECT_EQ(fees.returnCode, static_cast(EReturnCode::SUCCESS)); + + EXPECT_EQ(fees.distributionFeePercent, distributionFeePercent); + EXPECT_EQ(fees.teamFeePercent, teamFeePercent); + EXPECT_EQ(fees.winnerFeePercent, winnerFeePercent); + EXPECT_EQ(fees.burnPercent, burnPercent); + } + + void checkPlayers(const GetPlayers_output& output) const + { + EXPECT_EQ(output.returnCode, static_cast(EReturnCode::SUCCESS)); + EXPECT_EQ(output.players.capacity(), players.capacity()); + EXPECT_EQ(static_cast(output.numberOfPlayers), players.population()); + + for (uint64 i = 0, playerArrayIndex = 0; i < players.capacity(); ++i) + { + if (!players.isEmptySlot(i)) + { + EXPECT_EQ(output.players.get(playerArrayIndex++), players.key(i)); + } + } + } + + void checkWinners(const GetWinners_output& output) const + { + EXPECT_EQ(output.returnCode, static_cast(EReturnCode::SUCCESS)); + EXPECT_EQ(output.winners.capacity(), winners.capacity()); + EXPECT_EQ(output.numberOfWinners, winnersInfoNextEmptyIndex); + + for (uint64 i = 0; i < output.numberOfWinners; ++i) + { + EXPECT_EQ(output.winners.get(i), winners.get(i)); + } + } + + void randomlyAddPlayers(uint64 maxNewPlayers) + { + const uint64 newPlayerCount = mod(maxNewPlayers, players.capacity()); + for (uint64 i = 0; i < newPlayerCount; ++i) + { + players.add(id::randomValue()); + } + } + + void randomlyAddWinners(uint64 maxNewWinners) + { + const uint64 newWinnerCount = mod(maxNewWinners, winners.capacity()); + + winnersInfoNextEmptyIndex = 0; + WinnerInfo wi; + + for (uint64 i = 0; i < newWinnerCount; ++i) + { + wi.epoch = 1; + wi.tick = 1; + wi.revenue = 1000000; + wi.winnerAddress = id::randomValue(); + winners.set(winnersInfoNextEmptyIndex++, wi); + } + } + + void setSelling() { currentState = EState::SELLING; } + + void setLocked() { currentState = EState::LOCKED; } + + uint64 playersPopulation() const { return players.population(); } + + uint64 getTicketPrice() const { return ticketPrice; } +}; + +class ContractTestingRL : protected ContractTesting +{ +public: + ContractTestingRL() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(RL); + callSystemProcedure(RL_CONTRACT_INDEX, INITIALIZE); + } + + RLChecker* getState() { return reinterpret_cast(contractStates[RL_CONTRACT_INDEX]); } + + RL::GetFees_output getFees() + { + RL::GetFees_input input; + RL::GetFees_output output; + + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_FEES, input, output); + return output; + } + + RL::GetPlayers_output getPlayers() + { + RL::GetPlayers_input input; + RL::GetPlayers_output output; + + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_PLAYERS, input, output); + return output; + } + + RL::GetWinners_output getWinners() + { + RL::GetWinners_input input; + RL::GetWinners_output output; + + callFunction(RL_CONTRACT_INDEX, FUNCTION_INDEX_GET_WINNERS, input, output); + return output; + } + + RL::BuyTicket_output buyTicket(const id& user, uint64 reward) + { + RL::BuyTicket_input input; + RL::BuyTicket_output output; + invokeUserProcedure(RL_CONTRACT_INDEX, PROCEDURE_INDEX_BUY_TICKET, input, output, user, reward); + return output; + } + + void BeginEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, BEGIN_EPOCH); } + + void EndEpoch() { callSystemProcedure(RL_CONTRACT_INDEX, END_EPOCH); } +}; + +TEST(ContractRandomLottery, GetFees) +{ + ContractTestingRL ctl; + RL::GetFees_output output = ctl.getFees(); + ctl.getState()->checkFees(output); +} + +TEST(ContractRandomLottery, GetPlayers) +{ + ContractTestingRL ctl; + + // Initially empty + RL::GetPlayers_output output = ctl.getPlayers(); + ctl.getState()->checkPlayers(output); + + // Add random players directly to state (test helper) + constexpr uint64 maxPlayersToAdd = 10; + ctl.getState()->randomlyAddPlayers(maxPlayersToAdd); + output = ctl.getPlayers(); + ctl.getState()->checkPlayers(output); +} + +TEST(ContractRandomLottery, GetWinners) +{ + ContractTestingRL ctl; + + // Populate winners history artificially + constexpr uint64 maxNewWinners = 10; + ctl.getState()->randomlyAddWinners(maxNewWinners); + RL::GetWinners_output winnersOutput = ctl.getWinners(); + ctl.getState()->checkWinners(winnersOutput); +} + +TEST(ContractRandomLottery, BuyTicket) +{ + ContractTestingRL ctl; + + const uint64 ticketPrice = ctl.getState()->getTicketPrice(); + + // 1. Attempt when state is LOCKED (should fail and refund invocation reward) + { + const id userLocked = id::randomValue(); + increaseEnergy(userLocked, ticketPrice * 2); + RL::BuyTicket_output out = ctl.buyTicket(userLocked, ticketPrice); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::TICKET_SELLING_CLOSED)); + EXPECT_EQ(ctl.getState()->playersPopulation(), 0); + } + + // Switch to SELLING to allow purchases + ctl.getState()->setSelling(); + + // 2. Loop over several users and test invalid price, success, duplicate + constexpr uint64 userCount = 5; + uint64 expectedPlayers = 0; + + for (uint64 i = 0; i < userCount; ++i) + { + const id user = id::randomValue(); + increaseEnergy(user, ticketPrice * 5); + + // (a) Invalid price (wrong reward sent) — player not added + { + const RL::BuyTicket_output outInvalid = ctl.buyTicket(user, ticketPrice - 1); + EXPECT_EQ(outInvalid.returnCode, static_cast(RL::EReturnCode::TICKET_INVALID_PRICE)); + EXPECT_EQ(ctl.getState()->playersPopulation(), expectedPlayers); + } + + // (b) Valid purchase — player added + { + const RL::BuyTicket_output outOk = ctl.buyTicket(user, ticketPrice); + EXPECT_EQ(outOk.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + ++expectedPlayers; + EXPECT_EQ(ctl.getState()->playersPopulation(), expectedPlayers); + } + + // (c) Duplicate purchase — rejected + { + const RL::BuyTicket_output outDup = ctl.buyTicket(user, ticketPrice); + EXPECT_EQ(outDup.returnCode, static_cast(RL::EReturnCode::TICKET_ALREADY_PURCHASED)); + EXPECT_EQ(ctl.getState()->playersPopulation(), expectedPlayers); + } + } + + // 3. Sanity check: number of unique players matches expectations + EXPECT_EQ(expectedPlayers, userCount); +} + +TEST(ContractRandomLottery, EndEpoch) +{ + ContractTestingRL ctl; + + // Helper: contract balance holder (SELF account) + const id contractAddress = id(RL_CONTRACT_INDEX, 0, 0, 0); + const uint64 ticketPrice = ctl.getState()->getTicketPrice(); + + // Current fee configuration (set in INITIALIZE) + const RL::GetFees_output fees = ctl.getFees(); + const uint8 teamPercent = fees.teamFeePercent; // Team commission percent + const uint8 distributionPercent = fees.distributionFeePercent; // Distribution (dividends) percent + const uint8 burnPercent = fees.burnPercent; // Burn percent + const uint8 winnerPercent = fees.winnerFeePercent; // Winner payout percent + + // --- Scenario 1: No players (should just lock and clear silently) --- + { + ctl.BeginEpoch(); + EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + + RL::GetWinners_output before = ctl.getWinners(); + EXPECT_EQ(before.numberOfWinners, 0u); + + ctl.EndEpoch(); + + RL::GetWinners_output after = ctl.getWinners(); + EXPECT_EQ(after.numberOfWinners, 0u); + EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + } + + // --- Scenario 2: Exactly one player (ticket refunded, no winner recorded) --- + { + ctl.BeginEpoch(); + + const id solo = id::randomValue(); + increaseEnergy(solo, ticketPrice); + const uint64 balanceBefore = getBalance(solo); + + const RL::BuyTicket_output out = ctl.buyTicket(solo, ticketPrice); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(ctl.getState()->playersPopulation(), 1u); + EXPECT_EQ(getBalance(solo), balanceBefore - ticketPrice); + + ctl.EndEpoch(); + + // Refund happened + EXPECT_EQ(getBalance(solo), balanceBefore); + EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + + const RL::GetWinners_output winners = ctl.getWinners(); + EXPECT_EQ(winners.numberOfWinners, 0u); + } + + // --- Scenario 3: Multiple players (winner chosen, fees processed, remainder burned) --- + { + ctl.BeginEpoch(); + + constexpr uint32 N = 5; + struct PlayerInfo + { + id addr; + uint64 balanceBefore; + uint64 balanceAfterBuy; + }; + std::vector infos; + infos.reserve(N); + + // Add N distinct players with valid purchases + for (uint32 i = 0; i < N; ++i) + { + const id randomUser = id::randomValue(); + increaseEnergy(randomUser, ticketPrice * 2); + const uint64 bBefore = getBalance(randomUser); + const RL::BuyTicket_output out = ctl.buyTicket(randomUser, ticketPrice); + EXPECT_EQ(out.returnCode, static_cast(RL::EReturnCode::SUCCESS)); + EXPECT_EQ(getBalance(randomUser), bBefore - ticketPrice); + infos.push_back({randomUser, bBefore, bBefore - ticketPrice}); + } + + EXPECT_EQ(ctl.getState()->playersPopulation(), N); + + const uint64 contractBalanceBefore = getBalance(contractAddress); + EXPECT_EQ(contractBalanceBefore, ticketPrice * N); + + const uint64 teamBalanceBefore = getBalance(RL_DEV_ADDRESS); + + const RL::GetWinners_output winnersBefore = ctl.getWinners(); + const uint64 winnersCountBefore = winnersBefore.numberOfWinners; + + ctl.EndEpoch(); + + // Players reset after epoch end + EXPECT_EQ(ctl.getState()->playersPopulation(), 0u); + + const RL::GetWinners_output winnersAfter = ctl.getWinners(); + EXPECT_EQ(winnersAfter.numberOfWinners, winnersCountBefore + 1); + + // Newly appended winner info + const RL::WinnerInfo wi = winnersAfter.winners.get(winnersCountBefore); + EXPECT_NE(wi.winnerAddress, id::zero()); + EXPECT_EQ(wi.revenue, (ticketPrice * N * winnerPercent) / 100); + + // Winner address must be one of the players + bool found = false; + for (const PlayerInfo& inf : infos) + { + if (inf.addr == wi.winnerAddress) + { + found = true; + break; + } + } + EXPECT_TRUE(found); + + // Check winner balance increment and others unchanged + for (const PlayerInfo& inf : infos) + { + const uint64 bal = getBalance(inf.addr); + const uint64 balanceAfterBuy = inf.addr == wi.winnerAddress ? inf.balanceAfterBuy + wi.revenue : inf.balanceAfterBuy; + EXPECT_EQ(bal, balanceAfterBuy); + } + + // Team fee transferred + const uint64 teamFeeExpected = (ticketPrice * N * teamPercent) / 100; + EXPECT_EQ(getBalance(RL_DEV_ADDRESS), teamBalanceBefore + teamFeeExpected); + + // Burn + const uint64 burnExpected = contractBalanceBefore - ((contractBalanceBefore * burnPercent) / 100) - + ((((contractBalanceBefore * distributionPercent) / 100) / NUMBER_OF_COMPUTORS) * NUMBER_OF_COMPUTORS) - + ((contractBalanceBefore * teamPercent) / 100) - ((contractBalanceBefore * winnerPercent) / 100); + EXPECT_EQ(getBalance(contractAddress), burnExpected); + } +} diff --git a/test/contract_testing.h b/test/contract_testing.h index 2e92a267b..a2664991b 100644 --- a/test/contract_testing.h +++ b/test/contract_testing.h @@ -19,6 +19,7 @@ #include "contract_core/qpi_system_impl.h" #include "contract_core/qpi_ticking_impl.h" #include "contract_core/qpi_ipo_impl.h" +#include "contract_core/qpi_mining_impl.h" #include "test_util.h" @@ -28,6 +29,10 @@ class ContractTesting : public LoggingTest public: ContractTesting() { + +#ifdef __AVX512F__ + initAVX512FourQConstants(); +#endif initCommonBuffers(); initContractExec(); initSpecialEntities(); diff --git a/test/contract_vottunbridge.cpp b/test/contract_vottunbridge.cpp new file mode 100644 index 000000000..8ca52edd4 --- /dev/null +++ b/test/contract_vottunbridge.cpp @@ -0,0 +1,473 @@ +#define NO_UEFI + +#include + +#include "gtest/gtest.h" +#include "contract_testing.h" + +namespace { +constexpr unsigned short PROCEDURE_CREATE_ORDER = 1; +constexpr unsigned short PROCEDURE_TRANSFER_TO_CONTRACT = 6; + +uint64 requiredFee(uint64 amount) +{ + // Total fee is 0.5% (ETH) + 0.5% (Qubic) = 1% of amount + return 2 * ((amount * 5000000ULL) / 1000000000ULL); +} +} + +class ContractTestingVottunBridge : protected ContractTesting +{ +public: + using ContractTesting::invokeUserProcedure; + + ContractTestingVottunBridge() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(VOTTUNBRIDGE); + callSystemProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, INITIALIZE); + } + + VOTTUNBRIDGE* state() + { + return reinterpret_cast(contractStates[VOTTUNBRIDGE_CONTRACT_INDEX]); + } + + bool findOrder(uint64 orderId, VOTTUNBRIDGE::BridgeOrder& out) + { + for (uint64 i = 0; i < state()->orders.capacity(); ++i) + { + VOTTUNBRIDGE::BridgeOrder order = state()->orders.get(i); + if (order.orderId == orderId) + { + out = order; + return true; + } + } + return false; + } + + bool findProposal(uint64 proposalId, VOTTUNBRIDGE::AdminProposal& out) + { + for (uint64 i = 0; i < state()->proposals.capacity(); ++i) + { + VOTTUNBRIDGE::AdminProposal proposal = state()->proposals.get(i); + if (proposal.proposalId == proposalId) + { + out = proposal; + return true; + } + } + return false; + } + + bool setOrderById(uint64 orderId, const VOTTUNBRIDGE::BridgeOrder& updated) + { + for (uint64 i = 0; i < state()->orders.capacity(); ++i) + { + VOTTUNBRIDGE::BridgeOrder order = state()->orders.get(i); + if (order.orderId == orderId) + { + state()->orders.set(i, updated); + return true; + } + } + return false; + } + + VOTTUNBRIDGE::createOrder_output createOrder( + const id& user, uint64 amount, bit fromQubicToEthereum, uint64 fee) + { + VOTTUNBRIDGE::createOrder_input input{}; + VOTTUNBRIDGE::createOrder_output output{}; + input.qubicDestination = id(9, 0, 0, 0); + input.amount = amount; + input.fromQubicToEthereum = fromQubicToEthereum; + for (uint64 i = 0; i < 42; ++i) + { + input.ethAddress.set(i, static_cast('A')); + } + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, PROCEDURE_CREATE_ORDER, + input, output, user, static_cast(fee)); + return output; + } + + void seedBalance(const id& user, uint64 amount) + { + increaseEnergy(user, amount); + } + + VOTTUNBRIDGE::transferToContract_output transferToContract( + const id& user, uint64 amount, uint64 orderId, uint64 invocationReward) + { + VOTTUNBRIDGE::transferToContract_input input{}; + VOTTUNBRIDGE::transferToContract_output output{}; + input.amount = amount; + input.orderId = orderId; + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, PROCEDURE_TRANSFER_TO_CONTRACT, + input, output, user, static_cast(invocationReward)); + return output; + } + + VOTTUNBRIDGE::createProposal_output createProposal( + const id& admin, uint8 proposalType, const id& target, const id& oldAddress, uint64 amount) + { + VOTTUNBRIDGE::createProposal_input input{}; + VOTTUNBRIDGE::createProposal_output output{}; + input.proposalType = proposalType; + input.targetAddress = target; + input.oldAddress = oldAddress; + input.amount = amount; + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, 9, input, output, admin, 0); + return output; + } + + VOTTUNBRIDGE::approveProposal_output approveProposal(const id& admin, uint64 proposalId) + { + VOTTUNBRIDGE::approveProposal_input input{}; + VOTTUNBRIDGE::approveProposal_output output{}; + input.proposalId = proposalId; + + this->invokeUserProcedure(VOTTUNBRIDGE_CONTRACT_INDEX, 10, input, output, admin, 0); + return output; + } +}; + +TEST(VottunBridge, CreateOrder_RequiresFee) +{ + ContractTestingVottunBridge bridge; + const id user = id(1, 0, 0, 0); + const uint64 amount = 1000; + const uint64 fee = requiredFee(amount); + + std::cout << "[VottunBridge] CreateOrder_RequiresFee: amount=" << amount + << " fee=" << fee << " (sending fee-1)" << std::endl; + + increaseEnergy(user, fee - 1); + auto output = bridge.createOrder(user, amount, true, fee - 1); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::insufficientTransactionFee)); +} + +TEST(VottunBridge, TransferToContract_RejectsMissingReward) +{ + ContractTestingVottunBridge bridge; + const id user = id(2, 0, 0, 0); + const uint64 amount = 200; + const uint64 fee = requiredFee(amount); + const id contractId = id(VOTTUNBRIDGE_CONTRACT_INDEX, 0, 0, 0); + + std::cout << "[VottunBridge] TransferToContract_RejectsMissingReward: amount=" << amount + << " fee=" << fee << " reward=0 contractBalanceSeed=1000" << std::endl; + + // Seed balances: user only has fees; contract already has balance > amount + increaseEnergy(user, fee); + increaseEnergy(contractId, 1000); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + const uint64 lockedBefore = bridge.state()->lockedTokens; + const long long contractBalanceBefore = getBalance(contractId); + const long long userBalanceBefore = getBalance(user); + + auto transferOutput = bridge.transferToContract(user, amount, orderOutput.orderId, 0); + + EXPECT_EQ(transferOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidAmount)); + EXPECT_EQ(bridge.state()->lockedTokens, lockedBefore); + EXPECT_EQ(getBalance(contractId), contractBalanceBefore); + EXPECT_EQ(getBalance(user), userBalanceBefore); +} + +TEST(VottunBridge, TransferToContract_AcceptsExactReward) +{ + ContractTestingVottunBridge bridge; + const id user = id(3, 0, 0, 0); + const uint64 amount = 500; + const uint64 fee = requiredFee(amount); + const id contractId = id(VOTTUNBRIDGE_CONTRACT_INDEX, 0, 0, 0); + + std::cout << "[VottunBridge] TransferToContract_AcceptsExactReward: amount=" << amount + << " fee=" << fee << " reward=amount" << std::endl; + + increaseEnergy(user, fee + amount); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + const uint64 lockedBefore = bridge.state()->lockedTokens; + const long long contractBalanceBefore = getBalance(contractId); + + auto transferOutput = bridge.transferToContract(user, amount, orderOutput.orderId, amount); + + EXPECT_EQ(transferOutput.status, 0); + EXPECT_EQ(bridge.state()->lockedTokens, lockedBefore + amount); + EXPECT_EQ(getBalance(contractId), contractBalanceBefore + amount); +} + +TEST(VottunBridge, TransferToContract_OrderNotFound) +{ + ContractTestingVottunBridge bridge; + const id user = id(5, 0, 0, 0); + const uint64 amount = 100; + + bridge.seedBalance(user, amount); + + auto output = bridge.transferToContract(user, amount, 9999, amount); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::orderNotFound)); +} + +TEST(VottunBridge, TransferToContract_InvalidAmountMismatch) +{ + ContractTestingVottunBridge bridge; + const id user = id(6, 0, 0, 0); + const uint64 amount = 100; + const uint64 fee = requiredFee(amount); + + bridge.seedBalance(user, fee + amount + 1); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + auto output = bridge.transferToContract(user, amount + 1, orderOutput.orderId, amount + 1); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidAmount)); +} + +TEST(VottunBridge, TransferToContract_InvalidOrderState) +{ + ContractTestingVottunBridge bridge; + const id user = id(7, 0, 0, 0); + const uint64 amount = 150; + const uint64 fee = requiredFee(amount); + + bridge.seedBalance(user, fee + amount); + + auto orderOutput = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(orderOutput.status, 0); + + VOTTUNBRIDGE::BridgeOrder order{}; + ASSERT_TRUE(bridge.findOrder(orderOutput.orderId, order)); + order.status = 1; // completed + ASSERT_TRUE(bridge.setOrderById(orderOutput.orderId, order)); + + auto output = bridge.transferToContract(user, amount, orderOutput.orderId, amount); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidOrderState)); +} + +TEST(VottunBridge, CreateOrder_CleansCompletedAndRefundedSlots) +{ + ContractTestingVottunBridge bridge; + const id user = id(4, 0, 0, 0); + const uint64 amount = 1000; + const uint64 fee = requiredFee(amount); + + VOTTUNBRIDGE::BridgeOrder filledOrder{}; + filledOrder.orderId = 1; + filledOrder.amount = amount; + filledOrder.status = 1; // completed + filledOrder.fromQubicToEthereum = true; + filledOrder.qubicSender = user; + + for (uint64 i = 0; i < bridge.state()->orders.capacity(); ++i) + { + filledOrder.orderId = i + 1; + filledOrder.status = (i % 2 == 0) ? 1 : 2; // completed/refunded + bridge.state()->orders.set(i, filledOrder); + } + + increaseEnergy(user, fee); + auto output = bridge.createOrder(user, amount, true, fee); + EXPECT_EQ(output.status, 0); + + VOTTUNBRIDGE::BridgeOrder createdOrder{}; + EXPECT_TRUE(bridge.findOrder(output.orderId, createdOrder)); + EXPECT_EQ(createdOrder.status, 0); + + uint64 emptySlots = 0; + for (uint64 i = 0; i < bridge.state()->orders.capacity(); ++i) + { + if (bridge.state()->orders.get(i).status == 255) + { + emptySlots++; + } + } + EXPECT_GT(emptySlots, 0); +} + +TEST(VottunBridge, CreateProposal_CleansExecutedProposalsWhenFull) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + for (uint64 i = 2; i < bridge.state()->admins.capacity(); ++i) + { + bridge.state()->admins.set(i, NULL_ID); + } + + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + VOTTUNBRIDGE::AdminProposal proposal{}; + proposal.approvalsCount = 1; + proposal.active = true; + proposal.executed = false; + for (uint64 i = 0; i < bridge.state()->proposals.capacity(); ++i) + { + proposal.proposalId = i + 1; + proposal.executed = (i % 2 == 0); + bridge.state()->proposals.set(i, proposal); + } + + auto output = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + + EXPECT_EQ(output.status, 0); + + VOTTUNBRIDGE::AdminProposal createdProposal{}; + EXPECT_TRUE(bridge.findProposal(output.proposalId, createdProposal)); + EXPECT_TRUE(createdProposal.active); + EXPECT_FALSE(createdProposal.executed); + + uint64 clearedSlots = 0; + for (uint64 i = 0; i < bridge.state()->proposals.capacity(); ++i) + { + VOTTUNBRIDGE::AdminProposal p = bridge.state()->proposals.get(i); + if (!p.active && p.proposalId == 0) + { + clearedSlots++; + } + } + EXPECT_GT(clearedSlots, 0); +} + +TEST(VottunBridge, CreateProposal_InvalidTypeRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + + bridge.state()->numberOfAdmins = 1; + bridge.state()->requiredApprovals = 1; + bridge.state()->admins.set(0, admin1); + bridge.seedBalance(admin1, 1); + + auto output = bridge.createProposal(admin1, 99, NULL_ID, NULL_ID, 0); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::invalidAmount)); +} + +TEST(VottunBridge, ApproveProposal_NotOwnerRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + const id outsider = id(99, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + bridge.seedBalance(outsider, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(outsider, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::notOwner)); + EXPECT_FALSE(approveOutput.executed); +} + +TEST(VottunBridge, ApproveProposal_DoubleApprovalRejected) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(admin1, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, static_cast(VOTTUNBRIDGE::EthBridgeError::proposalAlreadyApproved)); + EXPECT_FALSE(approveOutput.executed); +} + +TEST(VottunBridge, ApproveProposal_ExecutesChangeThreshold) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(10, 0, 0, 0); + const id admin2 = id(11, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(admin2, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, 0); + EXPECT_TRUE(approveOutput.executed); + EXPECT_EQ(bridge.state()->requiredApprovals, 2); +} + +TEST(VottunBridge, ApproveProposal_ProposalNotFound) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(12, 0, 0, 0); + + bridge.state()->numberOfAdmins = 1; + bridge.state()->requiredApprovals = 1; + bridge.state()->admins.set(0, admin1); + bridge.seedBalance(admin1, 1); + + auto output = bridge.approveProposal(admin1, 12345); + EXPECT_EQ(output.status, static_cast(VOTTUNBRIDGE::EthBridgeError::proposalNotFound)); + EXPECT_FALSE(output.executed); +} + +TEST(VottunBridge, ApproveProposal_AlreadyExecuted) +{ + ContractTestingVottunBridge bridge; + const id admin1 = id(13, 0, 0, 0); + const id admin2 = id(14, 0, 0, 0); + + bridge.state()->numberOfAdmins = 2; + bridge.state()->requiredApprovals = 2; + bridge.state()->admins.set(0, admin1); + bridge.state()->admins.set(1, admin2); + bridge.seedBalance(admin1, 1); + bridge.seedBalance(admin2, 1); + + auto proposalOutput = bridge.createProposal(admin1, VOTTUNBRIDGE::PROPOSAL_CHANGE_THRESHOLD, + NULL_ID, NULL_ID, 2); + EXPECT_EQ(proposalOutput.status, 0); + + auto approveOutput = bridge.approveProposal(admin2, proposalOutput.proposalId); + EXPECT_EQ(approveOutput.status, 0); + EXPECT_TRUE(approveOutput.executed); + + auto secondApprove = bridge.approveProposal(admin1, proposalOutput.proposalId); + EXPECT_EQ(secondApprove.status, static_cast(VOTTUNBRIDGE::EthBridgeError::proposalAlreadyExecuted)); + EXPECT_FALSE(secondApprove.executed); +} diff --git a/test/custom_mining.cpp b/test/custom_mining.cpp index 176812740..9a68f8dcf 100644 --- a/test/custom_mining.cpp +++ b/test/custom_mining.cpp @@ -19,13 +19,13 @@ TEST(CustomMining, TaskStorageGeneral) { constexpr unsigned long long NUMBER_OF_TASKS = 100; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); for (unsigned long long i = 0; i < NUMBER_OF_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = NUMBER_OF_TASKS - i; storage.addData(&task); @@ -34,8 +34,8 @@ TEST(CustomMining, TaskStorageGeneral) // Expect the task are sort in ascending order for (unsigned long long i = 0; i < NUMBER_OF_TASKS - 1; i++) { - CustomMiningTask* task0 = storage.getDataByIndex(i); - CustomMiningTask* task1 = storage.getDataByIndex(i + 1); + CustomMiningTaskV2* task0 = storage.getDataByIndex(i); + CustomMiningTaskV2* task1 = storage.getDataByIndex(i + 1); EXPECT_LT(task0->taskIndex, task1->taskIndex); } EXPECT_EQ(storage.getCount(), NUMBER_OF_TASKS); @@ -47,14 +47,14 @@ TEST(CustomMining, TaskStorageDuplicatedItems) { constexpr unsigned long long NUMBER_OF_TASKS = 100; constexpr unsigned long long DUPCATED_TASKS = 10; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); // For DUPCATED_TASKS will only recorded 1 task for (unsigned long long i = 0; i < DUPCATED_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = 1; storage.addData(&task); @@ -62,7 +62,7 @@ TEST(CustomMining, TaskStorageDuplicatedItems) for (unsigned long long i = DUPCATED_TASKS; i < NUMBER_OF_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = i; storage.addData(&task); @@ -78,19 +78,19 @@ TEST(CustomMining, TaskStorageExistedItem) { constexpr unsigned long long NUMBER_OF_TASKS = 100; constexpr unsigned long long DUPCATED_TASKS = 10; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); for (unsigned long long i = 1; i < NUMBER_OF_TASKS + 1 ; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = i; storage.addData(&task); } // Test an existed task - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = NUMBER_OF_TASKS - 10; storage.addData(&task); @@ -119,19 +119,19 @@ TEST(CustomMining, TaskStorageExistedItem) TEST(CustomMining, TaskStorageOverflow) { constexpr unsigned long long NUMBER_OF_TASKS = CUSTOM_MINING_TASK_STORAGE_COUNT; - CustomMiningTaskStorage storage; + CustomMiningTaskV2Storage storage; storage.init(); for (unsigned long long i = 0; i < NUMBER_OF_TASKS; i++) { - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = i; storage.addData(&task); } // Overflow. Add one more and get error status - CustomMiningTask task; + CustomMiningTaskV2 task; task.taskIndex = NUMBER_OF_TASKS + 1; EXPECT_NE(storage.addData(&task), 0); diff --git a/test/fourq.cpp b/test/fourq.cpp new file mode 100644 index 000000000..1f606ef9d --- /dev/null +++ b/test/fourq.cpp @@ -0,0 +1,264 @@ +#define NO_UEFI + +#include "../src/platform/memory.h" +#include "../src/four_q.h" +#include "utils.h" + +#include +#include "gtest/gtest.h" + +#include +#include + +static constexpr int ID_SIZE = 61; +static inline void getIDChar(const unsigned char* key, char* identity, bool isLowerCase) +{ + CHAR16 computorID[61]; + getIdentity(key, computorID, true); + for (int k = 0; k < 60; ++k) + { + identity[k] = computorID[k] - L'a' + 'a'; + } + identity[60] = 0; +} + +TEST(TestFourQ, TestMultiply) +{ + // 8 test cases for 256-bit multiplication + unsigned long long a[8][4] = { + {9951791076627133056ULL, 8515301911953011018ULL, 10503917255838740547ULL, 9403542041099946340ULL}, + {9634782769625085733ULL, 3923345248364070851ULL, 12874006609097115757ULL, 9445681298461330583ULL}, + {9314926113594160360ULL, 9012577733633554087ULL, 15853326627100346762ULL, 3353532907889994600ULL}, + {11822735244239455150ULL, 14860878323222532373ULL, 839169842161576273ULL, 8384082473945502970ULL}, + {6391904870724534887ULL, 7752608459014781040ULL, 8834893383869603648ULL, 14432583643443481392ULL}, + {9034457083341789982ULL, 15550692794033658766ULL, 18370398459251091929ULL, 161212377777301450ULL}, + {12066041174979511630ULL, 6197228902632247602ULL, 15544684064627230784ULL, 8662358800126738212ULL}, + {2997608593061094407ULL, 10746661492960439270ULL, 13066743968851273858ULL, 901611315508727516ULL} + }; + + unsigned long long b[8][4] = { + {14556080569315562443ULL, 4784279743451576405ULL, 16952050128007612055ULL, 17448141405813274955ULL}, + {16953856751996506377ULL, 5957469746201176117ULL, 413985909494190460ULL, 5019301766552018644ULL}, + {8337584125020700765ULL, 9891896711220896307ULL, 3688562803407556063ULL, 15879907979249125147ULL}, + {5253913930687524613ULL, 14356908424098313115ULL, 7294083945257658276ULL, 11357758627518780620ULL}, + {6604082675214113798ULL, 8102242472442817269ULL, 4231600794557460268ULL, 9254306641367892880ULL}, + {15307070962626904180ULL, 14565308158529607085ULL, 7804612167412830134ULL, 11197002641182899202ULL}, + {5681082236069360781ULL, 11354469612480482261ULL, 10740484893427922886ULL, 4093428096946105430ULL}, + {16936346349005670285ULL, 16111331879026478134ULL, 281576863978497861ULL, 4843225515675739317ULL} + }; + + unsigned long long expectedMultiplicationResults[8][8] = { + {13505937776277228416ULL, 10691581058996783029ULL, 15857294677093499275ULL, 10551077288120234079ULL, + 10488747005868148888ULL, 3163167577502768305ULL, 12011108917152358447ULL, 8894487319443104894ULL}, + + {11258722506082215245ULL, 3752109657065586715ULL, 9754007644313481322ULL, 10650543212606486248ULL, + 14000725040689989368ULL, 6868107242688413590ULL, 12132679480588703742ULL, 2570140542862762927ULL}, + + {7980800202961401928ULL, 18091087109835938886ULL, 11937230724836153237ULL, 18437285308724511498ULL, + 9256451621004954121ULL, 2817287347866660760ULL, 7356871972350029435ULL, 2886893956455686033ULL}, + + {14821519003237893222ULL, 11951435221854993875ULL, 5649570579164725909ULL, 16529503750125471729ULL, + 5712698943065886767ULL, 10417044944053178538ULL, 10215165497768617151ULL, 5162124257364100363ULL}, + + {10299854845140439658ULL, 5620198573463725080ULL, 18403939479767000599ULL, 3997239017815343129ULL, + 17558583433366224073ULL, 16662387952814143598ULL, 16240400534467578973ULL, 7240494806558978920ULL}, + + {15852260790186789272ULL, 6843720495231156925ULL, 18209245341578934878ULL, 3229051715759855960ULL, + 6553675393672969791ULL, 11442787882881602486ULL, 5043402961965006398ULL, 97854418782578342ULL}, + + {16919816915648955382ULL, 15728350531867604818ULL, 262149976282468082ULL, 16822220236393767682ULL, + 17482650320082366559ULL, 4634282717190265856ULL, 3892072508178358212ULL, 1922222304195309433ULL}, + + {3674093358531938523ULL, 797775358977430453ULL, 6686355987721165902ULL, 16831290265741585642ULL, + 11378657779800286238ULL, 14872963278680745844ULL, 15850192255010623436ULL, 236719656924026199ULL} + }; + + long long averageProcessingTime = 0; + for (int i = 0; i < 8; i++) + { + unsigned long long result[8] = { 0 }; + + multiply(a[i], b[i], result); + + for (int k = 0; k < 8; k++) + { + EXPECT_EQ(result[k], expectedMultiplicationResults[i][k]) << " at [" << k << "]"; + } + } +} + +TEST(TestFourQ, TestMontgomeryMultiplyModOrder) +{ + + // 8 test cases for 256-bit MontgomeryMultiplyMod + unsigned long long a[8][4] = { + {9951791076627133056ULL, 8515301911953011018ULL, 10503917255838740547ULL, 9403542041099946340ULL}, + {9634782769625085733ULL, 3923345248364070851ULL, 12874006609097115757ULL, 9445681298461330583ULL}, + {9314926113594160360ULL, 9012577733633554087ULL, 15853326627100346762ULL, 3353532907889994600ULL}, + {11822735244239455150ULL, 14860878323222532373ULL, 839169842161576273ULL, 8384082473945502970ULL}, + {6391904870724534887ULL, 7752608459014781040ULL, 8834893383869603648ULL, 14432583643443481392ULL}, + {9034457083341789982ULL, 15550692794033658766ULL, 18370398459251091929ULL, 161212377777301450ULL}, + {12066041174979511630ULL, 6197228902632247602ULL, 15544684064627230784ULL, 8662358800126738212ULL}, + {2997608593061094407ULL, 10746661492960439270ULL, 13066743968851273858ULL, 901611315508727516ULL} + }; + + unsigned long long b[8][4] = { + {14556080569315562443ULL, 4784279743451576405ULL, 16952050128007612055ULL, 17448141405813274955ULL}, + {16953856751996506377ULL, 5957469746201176117ULL, 413985909494190460ULL, 5019301766552018644ULL}, + {8337584125020700765ULL, 9891896711220896307ULL, 3688562803407556063ULL, 15879907979249125147ULL}, + {5253913930687524613ULL, 14356908424098313115ULL, 7294083945257658276ULL, 11357758627518780620ULL}, + {6604082675214113798ULL, 8102242472442817269ULL, 4231600794557460268ULL, 9254306641367892880ULL}, + {15307070962626904180ULL, 14565308158529607085ULL, 7804612167412830134ULL, 11197002641182899202ULL}, + {5681082236069360781ULL, 11354469612480482261ULL, 10740484893427922886ULL, 4093428096946105430ULL}, + {16936346349005670285ULL, 16111331879026478134ULL, 281576863978497861ULL, 4843225515675739317ULL} + }; + + unsigned long long expectedMontgomeryMultiplyModOrderResults[8][4] = { + {1178600784049730938ULL,13475129099769568773ULL,8171380610981515619ULL,8889798462048389782ULL,}, + {9346893806433251032ULL,3783576366952291632ULL,9006661425833189295ULL,2561156787602149305ULL,}, + {15012255874803770290ULL,11566062810664104635ULL,4497422501535458145ULL,2875434161571900946ULL,}, + {12004931297526373125ULL,10222857380028780508ULL,17154413062382081055ULL,5158706721726943589ULL,}, + {9724623868153589743ULL,17410218506619807138ULL,7496133043478274651ULL,7229774243864893754ULL,}, + {10156145653863087884ULL,16403847498912163678ULL,18326820829694769537ULL,90612319098335675ULL,}, + {2160566501146101694ULL,16888000406840707060ULL,8270191582668357443ULL,1911769260568884212ULL,}, + {6774298701428010308ULL,11825708701777781499ULL,11766967071579472107ULL,229574109642630470ULL,}, + }; + + for (int i = 0; i < 8; i++) + { + unsigned long long result[4] = { 0 }; + // (a * b) mod n + Montgomery_multiply_mod_order(a[i], b[i], result); + + for (int k = 0; k < 4; k++) + { + EXPECT_EQ(result[k], expectedMontgomeryMultiplyModOrderResults[i][k]) << " at [" << k << "]"; + } + } +} + +TEST(TestFourQ, TestGenerateKeys) +{ + // Data generate from clang-compiled qubic-cli + unsigned char computorSeeds[][56] = { + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "nhtighfbfdvgxnxrwxnbfmisknawppoewsycodciozvqpeqegttwofg", + "fcvgljppwwwjhrawxeywxqdgssttiihcmikxbnnunugldvcitkhcrfl", + "eijytgswxqzfkotmqvwulivpbximuhmgydvnaozwszguqflpfvltqge", + }; + unsigned char expectedPrivateIDs[][ID_SIZE] = { + "cctwbaulwuyhybijykxrmxnyrvzbalwryiiahltfwanuafhyfhepcjjgvaec", + "qfldkxspcgsnbgnccmsjftuhxvlfrmarkqrvqjvjaebwoasbytasvcffdfwd", + "mqlgaugwpdaphhqsacmqdcioomybxkaisrwyefyisayrikqjlckwkpdhuqlc", + "qwqmcjhdlzphgabvnjbedsgwrgpbvplcipmxkzuogglbhzfjiytunaeactsn", + }; + unsigned char expectedPublicIDs[][ID_SIZE] = { + "bzbqfllbncxemglobhuvftluplvcpquassilfaboffbcadqssupnwlzbqexk", + "lsgscfhdoahhlbdmlyzrfkvsfrqbbuznganescizcetyxkcdhljhemofxcwb", + "zsvpltnzfdyjzetanimltroldybdzoctvfguybpbvdxbndsrhyreppgccspo", + "xcfqbuwxxtufpfwyteglgchgnqyanubfbkpwtivfobxybgaqcgiqmzlgscwe" + }; + + unsigned char computorSubseeds[32]; + unsigned char computorPrivateKeys[32]; + unsigned char computorPublicKeys[32]; + char privakeyKeyId[ID_SIZE]; + char publicKeyId[ID_SIZE]; + + int numberOfTests = sizeof(computorSeeds) / sizeof(computorSeeds[0]); + + for (int i = 0; i < numberOfTests; ++i) + { + getSubseed(computorSeeds[i], computorSubseeds); + getPrivateKey(computorSubseeds, computorPrivateKeys); + getPublicKey(computorPrivateKeys, computorPublicKeys); + + getIDChar(computorPrivateKeys, privakeyKeyId, true); + getIDChar(computorPublicKeys, publicKeyId, true); + // Verification + for (int k = 0; k < ID_SIZE; ++k) + { + EXPECT_EQ(expectedPrivateIDs[i][k], privakeyKeyId[k]) << " at [" << i << "][" << k << "]"; + EXPECT_EQ(expectedPublicIDs[i][k], publicKeyId[k]) << " at [" << i << "][" << k << "]"; + } + } +} + +// sign(const unsigned char* subseed, const unsigned char* publicKey, const unsigned char* messageDigest, unsigned char* signature) +TEST(TestFourQ, TestSign) +{ + // For sign and verification, some constants need to be set +#ifdef __AVX512F__ + initAVX512FourQConstants(); +#endif + + const std::string subSeedsStr[] = { + "4ac19e2bf0d3776519aabe31924f7dc2589b3d0e7411a65f84c9b16df72c038e", + "e8217c5b40aa91df662803ce4dbf18722e35f1097ac68fb5da10643a825799e3", + "6d02f48bcb53ac397fc71a9028e4165df9b87044c53e116a0192d7fa83254bb0", + "3cfa1097be482f6e5ce132c2aa657d0fb9d84121048de6f05b90a2cc136bf73a", + "5208dd447a21f3c9911cae547637c580e74fa40d2a9c5e1fb26dcbfa408539d1", + "987b163dc6fd492573b4e18af7016c52a027ced5345fb8904e69037ed2ac1b81", + "d462ab0c83f931c4a87f12953e20bd576be849da5a8f1402c3b176ef92486d05", + "713fc86e289f124bdb2a65f6a37c01e0559a148ebf430f6729c184de76b23a90", + "15cd5b0700000000b168de3a00000000743af15000000000efcdab0000000000" + }; + + const std::string messageDigestsStr[] = { + "94e120a4d3f58c217a53eb9046d9f2c5b11288a9fe340d6ce5a771cf04b82e63", + "77f493b58ea40162dc33f9a718e2543b05f629884d7ca0e31598c45f021ae7c0", + "5cc82fa973101da5bfb3e2448196f0a7d7d3324c86fbbe42907613d5c8c2f1a4", + "c01ae5f2879d11439b30ddae5f4c7b22689f023e17b4955c3b2f05e8d9089af6", + "2f893e70ad52c9186eb4b60dfe137288c4a9e0fb6d34a51897e2365a01b0d443", + "ec09a3f415c2dda5f8419026678ab03524f67ed9817ba230cd24513750e01bc6", + "48b59f32a61dff0e13528e4c7937bdf080c3efa7d221364be87f01d5c60f882a", + "b31e704c8a3f1d02692f05a7d8e5f6c911d370f4a68b2ec3fa4c51d7289003de", + "89d5f92a895987457400219e121e8730f6b248a1fd28bfee017611ef079105b5" + }; + + const std::string expectedSignaturesStr[] = + { + "357d47b1366f33eed311a4458ec7326d35728e9292328a9b7ff8d4ec0f7b0df9323f5d1cd01bd5a380a1a8e4f29ad3ae9c5d94e84f4181a61ca73030d6d11600", + "7d2479a15746839c4c5e1fdf0aadb167974c292ceee80593e18b5135763db63163d8eee5bd309c506f47b16cd1242ddfe985887b19d3943c14ec6ab79a9c1900", + "1b8ed83af3dc12deb1554f48df46bf5bc5e4654f62f97ef20656fae4e965ac87762c9fe6189dfe89192a619bca4a6c390f4e97bb1f926041263f2ba4206b0c00", + "470ad247ff6b2e55d44e9f2a79ce402bfc5e8c5322ed297f71939a9c5398b6fcb5058c05e614d10d90d6bdec8ee4ecc6462cdd54e0ea830fde6be465de3f2900", + "0851db3d4021bdc8cf3816b4672aba2f5f7cd0bf19e779e28ee60241bc4246dabe7442a11953703a44ed1cadd0af9fce683c5a312326341ac7a3e55a18c40100", + "54466ae5ecad45c83798e4e3e02ab40e834bf8d3f4f1628b300601ab87894599a43278efd48be7e9615cd569e656356a9e2307ae257b85a3f1f0f333f2302200", + "6d67294ccd03dc51fdb3bca649b7e060d3cf06c417e7053472ca617b93e5926928a7a48b1791c2487c7e83eeb4919046493709508c0541d1c02e9545401b2100", + "20764a88943fb4e796f81a560bde5e652c82ffb203c00b4846102a268ae68f64cdee6c7a3edbf4de48dd25fd423a4b40e79d97a2a47fd11030b6f30a09130b00", + "9f71d3138ff8a72db3b39883e056ce7f5bfe40de6387e64eff0c17e72bd1862ccd848000be1841725f1da87654235329b685e1c81c939cb0154bbc8d30a20c00", + }; + + constexpr size_t numberOfTests = sizeof(subSeedsStr) / sizeof(subSeedsStr[0]); + m256i subseeds[numberOfTests]; + m256i messageDigests[numberOfTests]; + unsigned char expectedSignatures[numberOfTests][64]; + + for (unsigned long long i = 0; i < numberOfTests; ++i) + { + subseeds[i] = test_utils::hexTo32Bytes(subSeedsStr[i], 32); + messageDigests[i] = test_utils::hexTo32Bytes(messageDigestsStr[i], 32); + test_utils::hexToByte(expectedSignaturesStr[i], 64, expectedSignatures[i]); + } + + for (unsigned long long i = 0; i < numberOfTests; ++i) + { + unsigned char publicKey[32]; + unsigned char privateKey[32]; + getPrivateKey(subseeds[i].m256i_u8, privateKey); + getPublicKey(privateKey, publicKey); + + unsigned char signature[64]; + sign(subseeds[i].m256i_u8, publicKey, messageDigests[i].m256i_u8, signature); + + // Verify functions + bool verifyStatus = verify(publicKey, messageDigests[i].m256i_u8, signature); + + EXPECT_TRUE(verifyStatus); + + for (int k = 0; k < 64; ++k) + { + EXPECT_EQ(expectedSignatures[i][k], signature[k]) << " at [" << i << "][" << k << "]"; + } + } +} diff --git a/test/packages.config b/test/packages.config index a450605d2..d6f4da2b1 100644 --- a/test/packages.config +++ b/test/packages.config @@ -1,4 +1,4 @@  - + \ No newline at end of file diff --git a/test/pending_txs_pool.cpp b/test/pending_txs_pool.cpp new file mode 100644 index 000000000..6778b6496 --- /dev/null +++ b/test/pending_txs_pool.cpp @@ -0,0 +1,534 @@ +#define NO_UEFI + +#include "gtest/gtest.h" + +// workaround for name clash with stdlib +#define system qubicSystemStruct + +#include "../src/contract_core/contract_def.h" +#include "../src/contract_core/contract_exec.h" + +#include "../src/public_settings.h" +#undef PENDING_TXS_POOL_NUM_TICKS +#define PENDING_TXS_POOL_NUM_TICKS 50ULL +#undef NUMBER_OF_TRANSACTIONS_PER_TICK +#define NUMBER_OF_TRANSACTIONS_PER_TICK 128ULL +#include "../src/ticking/pending_txs_pool.h" + +#include +#include + +static constexpr unsigned int NUM_INITIALIZED_ENTITIES = 200U; + +class TestPendingTxsPool : public PendingTxsPool +{ + unsigned char transactionBuffer[MAX_TRANSACTION_SIZE]; +public: + TestPendingTxsPool() + { + // we need the spectrum for tx priority calculation + EXPECT_TRUE(initSpectrum()); + memset(spectrum, 0, spectrumSizeInBytes); + for (unsigned int i = 0; i < NUM_INITIALIZED_ENTITIES; i++) + { + // create NUM_INITIALIZED_ENTITIES entities with balance > 0 to get desired txs priority + spectrum[i].incomingAmount = i + 1; + spectrum[i].outgoingAmount = 0; + spectrum[i].publicKey = m256i{0, 0, 0, i + 1 }; + + // create NUM_INITIALIZED_ENTITIES entities with balance = 0 for testing + spectrum[NUM_INITIALIZED_ENTITIES + i].incomingAmount = 0; + spectrum[NUM_INITIALIZED_ENTITIES + i].outgoingAmount = 0; + spectrum[NUM_INITIALIZED_ENTITIES + i].publicKey = m256i{ 0, 0, 0, NUM_INITIALIZED_ENTITIES + i + 1 }; + } + updateSpectrumInfo(); + } + + ~TestPendingTxsPool() + { + deinitSpectrum(); + } + + static constexpr unsigned int getMaxNumTxsPerTick() + { + return maxNumTxsPerTick; + } + + bool addTransaction(unsigned int tick, long long amount, unsigned int inputSize, const m256i* dest = nullptr, const m256i* src = nullptr) + { + Transaction* transaction = (Transaction*)transactionBuffer; + transaction->amount = amount; + if (dest == nullptr) + transaction->destinationPublicKey.setRandomValue(); + else + transaction->destinationPublicKey.assign(*dest); + if (src == nullptr) + transaction->sourcePublicKey.setRandomValue(); + else + transaction->sourcePublicKey.assign(*src); + transaction->inputSize = inputSize; + transaction->inputType = 0; + transaction->tick = tick; + + return add(transaction); + } +}; + +TestPendingTxsPool pendingTxsPool; + +unsigned int addTickTransactions(unsigned int tick, unsigned long long seed, unsigned int maxTransactions) +{ + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + unsigned int numTransactionsAdded = 0; + + // add transactions of tick + unsigned int transactionNum = gen64() % (maxTransactions + 1); + for (unsigned int transaction = 0; transaction < transactionNum; ++transaction) + { + unsigned int inputSize = gen64() % MAX_INPUT_SIZE; + long long amount = gen64() % MAX_AMOUNT; + m256i srcPublicKey = m256i{ 0, 0, 0, (gen64() % NUM_INITIALIZED_ENTITIES) + 1 }; + if (pendingTxsPool.addTransaction(tick, amount, inputSize, /*dest=*/nullptr, &srcPublicKey)) + numTransactionsAdded++; + } + pendingTxsPool.checkStateConsistencyWithAssert(); + + return numTransactionsAdded; +} + +void checkTickTransactions(unsigned int tick, unsigned long long seed, unsigned int maxTransactions) +{ + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // check transactions of tick + unsigned int transactionNum = gen64() % (maxTransactions + 1); + + for (unsigned int transaction = 0; transaction < transactionNum; ++transaction) + { + unsigned int expectedInputSize = gen64() % MAX_INPUT_SIZE; + long long expectedAmount = gen64() % MAX_AMOUNT; + m256i expectedSrcPublicKey = m256i{ 0, 0, 0, (gen64() % NUM_INITIALIZED_ENTITIES) + 1 }; + + Transaction* tp = pendingTxsPool.getTx(tick, transaction); + + ASSERT_NE(tp, nullptr); + + EXPECT_TRUE(tp->checkValidity()); + EXPECT_EQ(tp->tick, tick); + EXPECT_EQ(static_cast(tp->inputSize), expectedInputSize); + EXPECT_EQ(tp->amount, expectedAmount); + EXPECT_TRUE(tp->sourcePublicKey == expectedSrcPublicKey); + + m256i* digest = pendingTxsPool.getDigest(tick, transaction); + + ASSERT_NE(digest, nullptr); + + m256i tpDigest; + KangarooTwelve(tp, tp->totalSize(), &tpDigest, 32); + EXPECT_EQ(*digest, tpDigest); + } +} + + +TEST(TestPendingTxsPool, EpochTransition) +{ + unsigned long long seed = 42; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 3 epoch transitions + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + constexpr unsigned int firstEpochTicks = PENDING_TXS_POOL_NUM_TICKS; + // second epoch start will reset the pool completely because secondEpochTick0 is not contained + constexpr unsigned int secondEpochTicks = PENDING_TXS_POOL_NUM_TICKS / 2; + // thirdEpochTick0 will be contained with newInitialIndex >= buffersBeginIndex + constexpr unsigned int thirdEpochTicks = PENDING_TXS_POOL_NUM_TICKS / 2 + PENDING_TXS_POOL_NUM_TICKS / 4; + // fourthEpochTick0 will be contained with newInitialIndex < buffersBeginIndex + const unsigned int firstEpochTick0 = gen64() % 10000000; + const unsigned int secondEpochTick0 = firstEpochTick0 + firstEpochTicks; + const unsigned int thirdEpochTick0 = secondEpochTick0 + secondEpochTicks; + const unsigned int fourthEpochTick0 = thirdEpochTick0 + thirdEpochTicks; + unsigned long long firstEpochSeeds[firstEpochTicks]; + unsigned long long secondEpochSeeds[secondEpochTicks]; + unsigned long long thirdEpochSeeds[thirdEpochTicks]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + for (int i = 0; i < secondEpochTicks; ++i) + secondEpochSeeds[i] = gen64(); + for (int i = 0; i < thirdEpochTicks; ++i) + thirdEpochSeeds[i] = gen64(); + unsigned int numAdded = 0; + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + // add ticks transactions + for (int i = 0; i < firstEpochTicks; ++i) + numAdded = addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + + // check ticks transactions + for (int i = 0; i < firstEpochTicks; ++i) + checkTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + + pendingTxsPool.checkStateConsistencyWithAssert(); + + // Epoch transistion + pendingTxsPool.beginEpoch(secondEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(secondEpochTick0), 0); + + // add ticks transactions + for (int i = 0; i < secondEpochTicks; ++i) + numAdded = addTickTransactions(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); + + // check ticks transactions + for (int i = 0; i < secondEpochTicks; ++i) + checkTickTransactions(secondEpochTick0 + i, secondEpochSeeds[i], maxTransactions); + + // add a transaction for the next epoch + numAdded = addTickTransactions(thirdEpochTick0 + 1, thirdEpochSeeds[1], maxTransactions); + + pendingTxsPool.checkStateConsistencyWithAssert(); + + // Epoch transistion + pendingTxsPool.beginEpoch(thirdEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(thirdEpochTick0), numAdded); + + // add ticks transactions + for (int i = 2; i < thirdEpochTicks; ++i) + numAdded = addTickTransactions(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); + + // check ticks transactions + for (int i = 1; i < thirdEpochTicks; ++i) + checkTickTransactions(thirdEpochTick0 + i, thirdEpochSeeds[i], maxTransactions); + + // add a transaction for the next epoch + numAdded = addTickTransactions(fourthEpochTick0 + 1, /*seed=*/42, maxTransactions); + + pendingTxsPool.checkStateConsistencyWithAssert(); + + // Epoch transistion + pendingTxsPool.beginEpoch(fourthEpochTick0); + pendingTxsPool.checkStateConsistencyWithAssert(); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(fourthEpochTick0), numAdded); + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, TotalNumberOfPendingTxs) +{ + unsigned long long seed = 1337; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 1 epoch + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const int firstEpochTicks = (gen64() % (4 * PENDING_TXS_POOL_NUM_TICKS)) + 1; + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned long long firstEpochSeeds[4 * PENDING_TXS_POOL_NUM_TICKS]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add ticks transactions + std::vector numTransactionsAdded(firstEpochTicks); + std::vector numPendingTransactions(firstEpochTicks, 0); + for (int i = firstEpochTicks - 1; i >= 0; --i) + { + numTransactionsAdded[i] = addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + if (i > 0) + { + numPendingTransactions[i - 1] = numPendingTransactions[i] + numTransactionsAdded[i]; + } + } + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), (unsigned int)numTransactionsAdded[0] + numPendingTransactions[0]); + for (int i = 0; i < firstEpochTicks; ++i) + { + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 + i), (unsigned int)numPendingTransactions[i]); + } + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, NumberOfPendingTickTxs) +{ + unsigned long long seed = 67534; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 1 epoch + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + constexpr unsigned int firstEpochTicks = PENDING_TXS_POOL_NUM_TICKS; + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned long long firstEpochSeeds[firstEpochTicks]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add ticks transactions + std::vector numTransactionsAdded(firstEpochTicks); + for (int i = firstEpochTicks - 1; i >= 0; --i) + { + numTransactionsAdded[i] = addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + } + + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0 - 1), 0); + for (int i = 0; i < firstEpochTicks; ++i) + { + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0 + i), (unsigned int)numTransactionsAdded[i]); + } + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, IncrementFirstStoredTick) +{ + unsigned long long seed = 84129; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + // 5x test with running 1 epoch + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + // first, test case of having no transactions + unsigned int maxTransactions = (testIdx == 0) ? 0 : pendingTxsPool.getMaxNumTxsPerTick(); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const int firstEpochTicks = (gen64() % (4 * PENDING_TXS_POOL_NUM_TICKS)) + 1; + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned long long firstEpochSeeds[4 * PENDING_TXS_POOL_NUM_TICKS]; + for (int i = 0; i < firstEpochTicks; ++i) + firstEpochSeeds[i] = gen64(); + + // first epoch + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add ticks transactions + std::vector numTransactionsAdded(firstEpochTicks); + std::vector numPendingTransactions(firstEpochTicks, 0); + for (int i = firstEpochTicks - 1; i >= 0; --i) + { + numTransactionsAdded[i] = addTickTransactions(firstEpochTick0 + i, firstEpochSeeds[i], maxTransactions); + if (i > 0) + { + numPendingTransactions[i - 1] = numPendingTransactions[i] + numTransactionsAdded[i]; + } + } + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), (unsigned int)numTransactionsAdded[0] + numPendingTransactions[0]); + for (int i = 0; i < firstEpochTicks; ++i) + { + pendingTxsPool.incrementFirstStoredTick(); + for (int tx = 0; tx < numTransactionsAdded[i]; ++tx) + { + EXPECT_EQ(pendingTxsPool.getTx(firstEpochTick0 + i, 0), nullptr); + EXPECT_EQ(pendingTxsPool.getDigest(firstEpochTick0 + i, 0), nullptr); + } + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 + i), (unsigned int)numPendingTransactions[i]); + } + + pendingTxsPool.deinit(); + } +} + +TEST(TestPendingTxsPool, TxsPrioritizationMoreThanMaxTxs) +{ + unsigned long long seed = 9532; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + unsigned int numAdditionalTxs = 64; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add more than `pendingTxsPool.getMaxNumTxsPerTick()` with increasing priority + // (entities were set up in a way that u64._0 of the public key corresponds to their balance) + m256i srcPublicKey = m256i::zero(); + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick() + numAdditionalTxs; ++t) + { + srcPublicKey.u64._3 = t + 1; + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, /*amount=*/t + 1, /*inputSize=*/0, /*dest=*/nullptr, &srcPublicKey)); + } + + // adding lower priority tx does not work + srcPublicKey.u64._3 = 1; + EXPECT_FALSE(pendingTxsPool.addTransaction(firstEpochTick0, /*amount=*/1, /*inputSize=*/0, /*dest=*/nullptr, &srcPublicKey)); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), pendingTxsPool.getMaxNumTxsPerTick()); + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0), pendingTxsPool.getMaxNumTxsPerTick()); + + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick(); ++t) + { + if (t < numAdditionalTxs) + EXPECT_EQ(pendingTxsPool.getTx(firstEpochTick0, t)->amount, pendingTxsPool.getMaxNumTxsPerTick() + t + 1); + else + EXPECT_EQ(pendingTxsPool.getTx(firstEpochTick0, t)->amount, t + 1); + } + + pendingTxsPool.deinit(); +} + +TEST(TestPendingTxsPool, TxsPrioritizationDuplicateTxs) +{ + unsigned long long seed = 9532; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + constexpr unsigned int numTxs = pendingTxsPool.getMaxNumTxsPerTick() / 2; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // add duplicate transactions: same dest, src, and amount + m256i dest{ 562, 789, 234, 121 }; + m256i src{ 0, 0, 0, NUM_INITIALIZED_ENTITIES / 3 }; + long long amount = 1; + for (unsigned int t = 0; t < numTxs; ++t) + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, amount, /*inputSize=*/0, &dest, & src)); + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), numTxs); + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0), numTxs); + + for (unsigned int t = 0; t < numTxs; ++t) + { + Transaction* tx = pendingTxsPool.getTx(firstEpochTick0, t); + EXPECT_TRUE(tx->checkValidity()); + EXPECT_EQ(tx->amount, amount); + EXPECT_EQ(tx->tick, firstEpochTick0); + EXPECT_EQ(static_cast(tx->inputSize), 0U); + EXPECT_TRUE(tx->destinationPublicKey == dest); + EXPECT_TRUE(tx->sourcePublicKey == src); + } + + pendingTxsPool.deinit(); +} + +TEST(TestPendingTxsPool, ProtocolLevelTxsMaxPriority) +{ + unsigned long long seed = 9532; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // fill the PendingTxsPool completely for tick `firstEpochTick0` + m256i srcPublicKey = m256i::zero(); + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick(); ++t) + { + srcPublicKey.u64._3 = t + 1; + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + } + + EXPECT_EQ(pendingTxsPool.getTotalNumberOfPendingTxs(firstEpochTick0 - 1), pendingTxsPool.getMaxNumTxsPerTick()); + EXPECT_EQ(pendingTxsPool.getNumberOfPendingTickTxs(firstEpochTick0), pendingTxsPool.getMaxNumTxsPerTick()); + + Transaction tx { + .sourcePublicKey = m256i{ 0, 0, 0, (gen64() % NUM_INITIALIZED_ENTITIES) + 1 }, + .destinationPublicKey = m256i::zero(), + .amount = 0, .tick = firstEpochTick0, + .inputType = VOTE_COUNTER_INPUT_TYPE, + .inputSize = 0, + }; + + EXPECT_TRUE(pendingTxsPool.add(&tx)); + + tx.inputType = CustomMiningSolutionTransaction::transactionType(); + + EXPECT_TRUE(pendingTxsPool.add(&tx)); + + pendingTxsPool.deinit(); +} + +TEST(TestPendingTxsPool, TxsWithSrcBalance0AreRejected) +{ + unsigned long long seed = 3452; + + // use pseudo-random sequence + std::mt19937_64 gen64(seed); + + for (int testIdx = 0; testIdx < 6; ++testIdx) + { + pendingTxsPool.init(); + pendingTxsPool.checkStateConsistencyWithAssert(); + + const unsigned int firstEpochTick0 = gen64() % 10000000; + + pendingTxsPool.beginEpoch(firstEpochTick0); + + // partially fill the PendingTxsPool for tick `firstEpochTick0` + m256i srcPublicKey = m256i::zero(); + for (unsigned int t = 0; t < pendingTxsPool.getMaxNumTxsPerTick() / 2; ++t) + { + srcPublicKey.u64._3 = t + 1; + EXPECT_TRUE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + } + + // public key with balance 0 + srcPublicKey.u64._3 = NUM_INITIALIZED_ENTITIES + 1 + (gen64() % NUM_INITIALIZED_ENTITIES); + EXPECT_FALSE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + + // non-existant public key + srcPublicKey = m256i{ 0, gen64() % MAX_AMOUNT, 0, 0}; + EXPECT_FALSE(pendingTxsPool.addTransaction(firstEpochTick0, gen64() % MAX_AMOUNT, gen64() % MAX_INPUT_SIZE, /*dest=*/nullptr, &srcPublicKey)); + + pendingTxsPool.deinit(); + } +} \ No newline at end of file diff --git a/test/qpi.cpp b/test/qpi.cpp index 2f6b15ba6..38d02f2b4 100644 --- a/test/qpi.cpp +++ b/test/qpi.cpp @@ -23,6 +23,126 @@ void initComputors(unsigned short computorIdOffset) } } +TEST(TestCoreQPI, SafeMath) +{ + { + sint64 a = -1000000000000LL; // This is valid - negative signed integer + sint64 b = 2; // Positive signed integer + EXPECT_EQ(smul(a, b), -2000000000000); + } + + { + uint64_t a = 1000000; + uint64_t b = 2000000; + uint64_t expected_ok = 2000000000000ULL; + EXPECT_EQ(smul(a, b), expected_ok); + } + { + sint64 a = INT64_MIN; // -9223372036854775808 + sint64 b = -1; // -1 + EXPECT_EQ(smul(a, b), INT64_MAX); + } + { + uint64_t a = 123456789ULL; + uint64_t b = 987654321ULL; + + // Case: Multiplication by zero. + EXPECT_EQ(smul(a, 0ULL), 0ULL); + EXPECT_EQ(smul(0ULL, b), 0ULL); + + // Case: Multiplication by one. + EXPECT_EQ(smul(a, 1ULL), a); + EXPECT_EQ(smul(1ULL, b), b); + } + { + // Case: A clear overflow case. + // UINT64_MAX is approximately 1.84e19. + uint64_t c = 4000000000ULL; + uint64_t d = 5000000000ULL; // c * d is 2e19, which overflows. + EXPECT_EQ(smul(c, d), UINT64_MAX); + } + { + // Case: Test the exact boundary of overflow. + uint64_t max_val = UINT64_MAX; + uint64_t divisor = 2; + uint64_t limit = max_val / divisor; + + // This should not overflow. + EXPECT_EQ(smul(limit, divisor), limit * 2); + + // This should overflow and clamp. + EXPECT_EQ(smul(limit + 1, divisor), UINT64_MAX); + } + { + // Case: A simple multiplication that does not overflow. + int64_t e = 1000000; + int64_t f = -2000000; + EXPECT_EQ(smul(e, f), -2000000000000LL); + } + { + // Case: Positive * Positive, causing overflow. + int64_t a = INT64_MAX / 2; + int64_t b = 3; + EXPECT_EQ(smul(a, b), INT64_MAX); + } + { + int64_t a = INT64_MAX / 2; + int64_t b = 3; + int64_t c = -3; + int64_t d = INT64_MIN / 2; + + // Case: Positive * Negative, causing underflow. + EXPECT_EQ(smul(a, c), INT64_MIN); + + // Case: Negative * Positive, causing underflow. + EXPECT_EQ(smul(d, b), INT64_MIN); + } + { + // Case: Negative * Negative, causing overflow. + int64_t c = -3; + int64_t d = INT64_MIN / 2; + EXPECT_EQ(smul(d, c), INT64_MAX); + } + { + // --- Unsigned 32-bit Tests --- + // No Overflow + uint32_t a_u32 = 60000; + uint32_t b_u32 = 60000; + EXPECT_EQ(smul(a_u32, b_u32), 3600000000U); + + // Overflow + uint32_t c_u32 = 70000; + uint32_t d_u32 = 70000; // 70000*70000 = 4,900,000,000 which is > UINT32_MAX (~4.29e9) + EXPECT_EQ(smul(c_u32, d_u32), UINT32_MAX); + + // Boundary + uint32_t limit_u32 = UINT32_MAX / 2; + uint32_t divisor_u32 = 2; + EXPECT_EQ(smul(limit_u32, divisor_u32), limit_u32 * 2); + EXPECT_EQ(smul(limit_u32 + 1, divisor_u32), UINT32_MAX); + + // --- Signed 32-bit Tests --- + // No Overflow + int32_t a_s32 = 10000; + int32_t b_s32 = -10000; + EXPECT_EQ(smul(a_s32, b_s32), -100000000); + + // Positive Overflow + int32_t c_s32 = INT32_MAX / 2; + int32_t d_s32 = 3; + EXPECT_EQ(smul(c_s32, d_s32), INT32_MAX); + + // Underflow + int32_t e_s32 = INT32_MIN / 2; + int32_t f_s32 = 3; + EXPECT_EQ(smul(e_s32, f_s32), INT32_MIN); + + // Negative * Negative, causing overflow. + int32_t g_s32 = -3; + int32_t h_s32 = INT32_MIN / 2; + EXPECT_EQ(smul(h_s32, g_s32), INT32_MAX); + } +} TEST(TestCoreQPI, Array) { diff --git a/test/qpi_collection.cpp b/test/qpi_collection.cpp index d88bf1242..0778553c0 100644 --- a/test/qpi_collection.cpp +++ b/test/qpi_collection.cpp @@ -2,11 +2,6 @@ #include "gtest/gtest.h" -static void* __scratchpadBuffer = nullptr; -static void* __scratchpad() -{ - return __scratchpadBuffer; -} namespace QPI { struct QpiContextProcedureCall; @@ -16,6 +11,7 @@ typedef void (*USER_FUNCTION)(const QPI::QpiContextFunctionCall&, void* state, v typedef void (*USER_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); #include "../src/contracts/qpi.h" +#include "../src/common_buffers.h" #include "../src/contract_core/qpi_collection_impl.h" #include "../src/contract_core/qpi_trivial_impl.h" @@ -1522,7 +1518,7 @@ void testCollectionPseudoRandom(int povs, int seed, bool povCollisions, int clea TEST(TestCoreQPI, CollectionInsertRemoveCleanupRandom) { - __scratchpadBuffer = new char[10 * 1024 * 1024]; + reorgBuffer = new char[10 * 1024 * 1024]; constexpr unsigned int numCleanups = 30; for (int i = 0; i < 10; ++i) { @@ -1540,21 +1536,21 @@ TEST(TestCoreQPI, CollectionInsertRemoveCleanupRandom) testCollectionPseudoRandom<16>(10, 12 + i, povCollisions, numCleanups, 55, 45); testCollectionPseudoRandom<4>(4, 42 + i, povCollisions, numCleanups, 52, 48); } - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(TestCoreQPI, CollectionCleanupWithPovCollisions) { // Shows bugs in cleanup() that occur in case of massive pov hash map collisions and in case of capacity < 32 - __scratchpadBuffer = new char[10 * 1024 * 1024]; + reorgBuffer = new char[10 * 1024 * 1024]; bool cleanupAfterEachRemove = true; testCollectionMultiPovOneElement<16>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<32>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<64>(cleanupAfterEachRemove); testCollectionMultiPovOneElement<128>(cleanupAfterEachRemove); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } @@ -1673,7 +1669,7 @@ QPI::uint64 testCollectionPerformance( TEST(TestCoreQPI, CollectionPerformance) { - __scratchpadBuffer = new char[16 * 1024 * 1024]; + reorgBuffer = new char[16 * 1024 * 1024]; std::vector durations; std::vector descriptions; @@ -1702,8 +1698,8 @@ TEST(TestCoreQPI, CollectionPerformance) durations.push_back(testCollectionPerformance<512>(16, 333)); descriptions.push_back("[CollectionPerformance] Collection<512>(16, 333)"); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; bool verbose = true; if (verbose) diff --git a/test/qpi_hash_map.cpp b/test/qpi_hash_map.cpp index 3760c17f1..47ddbfb72 100644 --- a/test/qpi_hash_map.cpp +++ b/test/qpi_hash_map.cpp @@ -2,11 +2,6 @@ #include "gtest/gtest.h" -static void* __scratchpadBuffer = nullptr; -static void* __scratchpad() -{ - return __scratchpadBuffer; -} namespace QPI { struct QpiContextProcedureCall; @@ -16,6 +11,7 @@ typedef void (*USER_FUNCTION)(const QPI::QpiContextFunctionCall&, void* state, v typedef void (*USER_PROCEDURE)(const QPI::QpiContextProcedureCall&, void* state, void* input, void* output, void* locals); #include "../src/contracts/qpi.h" +#include "../src/common_buffers.h" #include "../src/contract_core/qpi_hash_map_impl.h" #include #include @@ -374,7 +370,7 @@ TYPED_TEST_P(QPIHashMapTest, TestCleanup) constexpr QPI::uint64 capacity = 4; QPI::HashMap hashMap; - __scratchpadBuffer = new char[2 * sizeof(hashMap)]; + reorgBuffer = new char[2 * sizeof(hashMap)]; std::array keyValuePairs = HashMapTestData::CreateKeyValueTestPairs(); auto ids = std::views::keys(keyValuePairs); @@ -413,8 +409,8 @@ TYPED_TEST_P(QPIHashMapTest, TestCleanup) EXPECT_NE(returnedIndex, QPI::NULL_INDEX); EXPECT_EQ(hashMap.population(), 4); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TYPED_TEST_P(QPIHashMapTest, TestCleanupPerformanceShortcuts) @@ -450,7 +446,7 @@ TEST(NonTypedQPIHashMapTest, TestCleanupLargeMapSameHashes) constexpr QPI::uint64 capacity = 64; QPI::HashMap hashMap; - __scratchpadBuffer = new char[2 * sizeof(hashMap)]; + reorgBuffer = new char[2 * sizeof(hashMap)]; for (QPI::uint64 i = 0; i < 64; ++i) { @@ -463,8 +459,8 @@ TEST(NonTypedQPIHashMapTest, TestCleanupLargeMapSameHashes) // Cleanup will have to iterate through the whole map to find an empty slot for the last element. hashMap.cleanup(); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TYPED_TEST_P(QPIHashMapTest, TestReplace) @@ -623,7 +619,7 @@ void testHashMapPseudoRandom(int seed, int cleanups, int percentAdd, int percent std::map referenceMap; QPI::HashMap map; - __scratchpadBuffer = new char[2 * sizeof(map)]; + reorgBuffer = new char[2 * sizeof(map)]; map.reset(); @@ -685,8 +681,8 @@ void testHashMapPseudoRandom(int seed, int cleanups, int percentAdd, int percent // std::cout << "capacity: " << set.capacity() << ", pupulation:" << set.population() << std::endl; } - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(QPIHashMapTest, HashMapPseudoRandom) @@ -718,7 +714,7 @@ TEST(QPIHashMapTest, HashSet) { constexpr QPI::uint64 capacity = 128; QPI::HashSet hashSet; - __scratchpadBuffer = new char[2 * sizeof(hashSet)]; + reorgBuffer = new char[2 * sizeof(hashSet)]; EXPECT_EQ(hashSet.capacity(), capacity); // Test add() and contains() @@ -816,8 +812,8 @@ TEST(QPIHashMapTest, HashSet) hashSet.reset(); EXPECT_EQ(hashSet.population(), 0); - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } template @@ -886,7 +882,7 @@ void testHashSetPseudoRandom(int seed, int cleanups, int percentAdd, int percent std::set referenceSet; QPI::HashSet set; - __scratchpadBuffer = new char[2 * sizeof(set)]; + reorgBuffer = new char[2 * sizeof(set)]; set.reset(); @@ -946,8 +942,8 @@ void testHashSetPseudoRandom(int seed, int cleanups, int percentAdd, int percent // std::cout << "capacity: " << set.capacity() << ", pupulation:" << set.population() << std::endl; } - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(QPIHashMapTest, HashSetPseudoRandom) @@ -983,7 +979,7 @@ static void perfTestCleanup(int seed) std::mt19937_64 gen64(seed); auto* set = new QPI::HashSet(); - __scratchpadBuffer = new char[sizeof(*set)]; + reorgBuffer = new char[sizeof(*set)]; for (QPI::uint64 i = 1; i <= 100; ++i) { @@ -1007,8 +1003,8 @@ static void perfTestCleanup(int seed) } delete set; - delete[] __scratchpadBuffer; - __scratchpadBuffer = nullptr; + delete[] reorgBuffer; + reorgBuffer = nullptr; } TEST(QPIHashMapTest, HashSetPerfTest) diff --git a/test/score.cpp b/test/score.cpp index f382fb7fa..cc7c19f53 100644 --- a/test/score.cpp +++ b/test/score.cpp @@ -4,9 +4,6 @@ #define ENABLE_PROFILING 0 -// needed for scoring task queue -#define NUMBER_OF_TRANSACTIONS_PER_TICK 1024 - // current optimized implementation #include "../src/score.h" @@ -41,11 +38,14 @@ static constexpr bool PRINT_DETAILED_INFO = false; // set to 0 for run all available samples // For profiling enable, run all available samples -static constexpr unsigned long long COMMON_TEST_NUMBER_OF_SAMPLES = ENABLE_PROFILING ? 0 : 32; +static constexpr unsigned long long COMMON_TEST_NUMBER_OF_SAMPLES = 32; +static constexpr unsigned long long PROFILING_NUMBER_OF_SAMPLES = 32; + // set 0 for run maximum number of threads of the computer. // For profiling enable, set it equal to deployment setting -static constexpr int MAX_NUMBER_OF_THREADS = ENABLE_PROFILING ? 12 : 0; +static constexpr int MAX_NUMBER_OF_THREADS = 0; +static constexpr int MAX_NUMBER_OF_PROFILING_THREADS = 12; static bool gCompareReference = false; // Only run on specific index of samples and setting @@ -65,7 +65,6 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, { return; } - auto pScore = std::make_unique>(); + pScore->initMemory(); pScore->initMiningData(miningSeed); int x = 0; @@ -116,7 +116,7 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, gtIndex = gScoreIndexMap[i]; } - if (ENABLE_PROFILING || PRINT_DETAILED_INFO || gtIndex < 0 || (score_value != gScoresGroundTruth[sampleIndex][gtIndex])) + if (PRINT_DETAILED_INFO || gtIndex < 0 || (score_value != gScoresGroundTruth[sampleIndex][gtIndex])) { if (gScoreProcessingTime.count(i) == 0) { @@ -126,8 +126,6 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, { gScoreProcessingTime[i] += elapsed; } - - if (!ENABLE_PROFILING) { std::cout << "[sample " << sampleIndex << "; setting " << i << ": " @@ -137,7 +135,7 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, << kSettings[i][score_params::POPULATION_THRESHOLD] << ", " << kSettings[i][score_params::NUMBER_OF_MUTATIONS] << ", " << kSettings[i][score_params::SOLUTION_THRESHOLD] - << "]" + << "]" << std::endl; std::cout << " score " << score_value; if (gtIndex >= 0) @@ -151,8 +149,6 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, std::cout << " time " << elapsed << " ms " << std::endl; } } - - if (!ENABLE_PROFILING) { EXPECT_GT(gScoreIndexMap.count(i), 0); if (gtIndex >= 0) @@ -163,27 +159,68 @@ static void processElement(unsigned char* miningSeed, unsigned char* publicKey, } } +// Recursive template to process each element in scoreSettings +template +static void processElementWithPerformance(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex) +{ + auto pScore = std::make_unique>(); + + pScore->initMemory(); + pScore->initMiningData(miningSeed); + int x = 0; + top_of_stack = (unsigned long long)(&x); + auto t0 = std::chrono::high_resolution_clock::now(); + unsigned int score_value = (*pScore)(0, publicKey, miningSeed, nonce); + auto t1 = std::chrono::high_resolution_clock::now(); + auto d = t1 - t0; + auto elapsed = std::chrono::duration_cast(d).count(); + +#pragma omp critical + { + if (gScoreProcessingTime.count(i) == 0) + { + gScoreProcessingTime[i] = elapsed; + } + else + { + gScoreProcessingTime[i] += elapsed; + } + } +} + // Main processing function -template +template static void processHelper(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex, std::index_sequence) { - (processElement(miningSeed, publicKey, nonce, sampleIndex), ...); + if constexpr (profiling) + { + (processElementWithPerformance(miningSeed, publicKey, nonce, sampleIndex), ...); + } + else + { + (processElement(miningSeed, publicKey, nonce, sampleIndex), ...); + } } // Recursive template to process each element in scoreSettings -template +template static void process(unsigned char* miningSeed, unsigned char* publicKey, unsigned char* nonce, int sampleIndex) { - processHelper(miningSeed, publicKey, nonce, sampleIndex, std::make_index_sequence{}); + processHelper(miningSeed, publicKey, nonce, sampleIndex, std::make_index_sequence{}); } void runCommonTests() { -#ifdef ENABLE_PROFILING - gProfilingDataCollector.init(1024); -#endif - #if defined (__AVX512F__) && !GENERIC_K12 initAVX512KangarooTwelveConstants(); #endif @@ -198,21 +235,23 @@ void runCommonTests() // Convert the raw string and do the data verification unsigned long long numberOfSamplesReadFromFile = sampleString.size(); unsigned long long numberOfSamples = numberOfSamplesReadFromFile; - if (COMMON_TEST_NUMBER_OF_SAMPLES > 0) + unsigned long long requestedNumberOfSamples = COMMON_TEST_NUMBER_OF_SAMPLES; + + if (requestedNumberOfSamples > 0) { - std::cout << "Request testing with " << COMMON_TEST_NUMBER_OF_SAMPLES << " samples." << std::endl; + std::cout << "Request testing with " << requestedNumberOfSamples << " samples." << std::endl; - numberOfSamples = std::min(COMMON_TEST_NUMBER_OF_SAMPLES, numberOfSamples); - if (COMMON_TEST_NUMBER_OF_SAMPLES <= numberOfSamples) + numberOfSamples = std::min(requestedNumberOfSamples, numberOfSamples); + if (requestedNumberOfSamples <= numberOfSamples) { - numberOfSamples = COMMON_TEST_NUMBER_OF_SAMPLES; + numberOfSamples = requestedNumberOfSamples; } else // Request number of samples greater than existed. Only valid for reference score validation only { if (gCompareReference) { - numberOfSamples = COMMON_TEST_NUMBER_OF_SAMPLES; - std::cout << "Refenrece comparison mode: " << numberOfSamples << " samples are read from file for comparision." + numberOfSamples = requestedNumberOfSamples; + std::cout << "Refenrece comparison mode: " << numberOfSamples << " samples are read from file for comparision." << "Remained are generated randomly." << std::endl; } @@ -232,9 +271,9 @@ void runCommonTests() { if (i < numberOfSamplesReadFromFile) { - miningSeeds[i] = hexToByte(sampleString[i][0], 32); - publicKeys[i] = hexToByte(sampleString[i][1], 32); - nonces[i] = hexToByte(sampleString[i][2], 32); + miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); + publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); + nonces[i] = hexTo32Bytes(sampleString[i][2], 32); } else // Samples from files are not enough, randomly generate more { @@ -282,7 +321,7 @@ void runCommonTests() int count = 0; for (unsigned long long j = 0; j < score_params::MAX_PARAM_TYPE; ++j) { - if (scoresSettingHeader[j] == score_params::kSettings[i][j]) + if (scoresSettingHeader[j] == kSettings[i][j]) { count++; } @@ -321,6 +360,7 @@ void runCommonTests() } } + // Run the test unsigned int numberOfThreads = std::thread::hardware_concurrency(); if (MAX_NUMBER_OF_THREADS > 0) @@ -328,23 +368,15 @@ void runCommonTests() numberOfThreads = numberOfThreads > MAX_NUMBER_OF_THREADS ? MAX_NUMBER_OF_THREADS : numberOfThreads; } - if (ENABLE_PROFILING) + if (numberOfThreads > 1) { - std::cout << "Running " << numberOfThreads << " threads for collecting multiple threads performance" << std::endl; + std::cout << "Compare score only. Lauching test with all available " << numberOfThreads << " threads." << std::endl; } else { - if (numberOfThreads > 1) - { - std::cout << "Compare score only. Lauching test with all available " << numberOfThreads << " threads." << std::endl; - } - else - { - std::cout << "Running one sample on one thread for collecting single thread performance." << std::endl; - } + std::cout << "Running one sample on one thread for collecting single thread performance." << std::endl; } - std::vector samples; for (int i = 0; i < numberOfSamples; ++i) { @@ -356,29 +388,26 @@ void runCommonTests() samples.push_back(i); } - std::string compTerm = " and compare with groundtruths from file."; + std::string compTerm = "and compare with groundtruths from file."; if (gCompareReference) { - compTerm = " and compare with reference code."; - } - if (ENABLE_PROFILING) - { - compTerm = "for profiling, without comparing any result (set test case FAILED as default)"; + compTerm = "and compare with reference code."; } std::cout << "Processing " << samples.size() << " samples " << compTerm << "..." << std::endl; + gScoreProcessingTime.clear(); #pragma omp parallel for num_threads(numberOfThreads) for (int i = 0; i < samples.size(); ++i) { int index = samples[i]; - process(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); + process<0, numberOfGeneratedSetting>(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); #pragma omp critical std::cout << i << ", "; } std::cout << std::endl; // Print the average processing time - if (PRINT_DETAILED_INFO || ENABLE_PROFILING) + if (PRINT_DETAILED_INFO) { for (auto scoreTime : gScoreProcessingTime) { @@ -394,18 +423,137 @@ void runCommonTests() << "]: " << processingTime << " ms" << std::endl; } } +} -#ifdef ENABLE_PROFILING - gProfilingDataCollector.writeToFile(); +void runPerformanceTests() +{ + +#if defined (__AVX512F__) && !GENERIC_K12 + initAVX512KangarooTwelveConstants(); #endif -} + constexpr unsigned long long numberOfGeneratedSetting = sizeof(score_params::kProfileSettings) / sizeof(score_params::kProfileSettings[0]); + + // Read the parameters and results + auto sampleString = readCSV(COMMON_TEST_SAMPLES_FILE_NAME); + auto scoresString = readCSV(COMMON_TEST_SCORES_FILE_NAME); + ASSERT_FALSE(sampleString.empty()); + ASSERT_FALSE(scoresString.empty()); + // Convert the raw string and do the data verification + unsigned long long numberOfSamplesReadFromFile = sampleString.size(); + unsigned long long numberOfSamples = numberOfSamplesReadFromFile; + unsigned long long requestedNumberOfSamples = PROFILING_NUMBER_OF_SAMPLES; + + if (requestedNumberOfSamples > 0) + { + std::cout << "Request testing with " << requestedNumberOfSamples << " samples." << std::endl; + + numberOfSamples = std::min(requestedNumberOfSamples, numberOfSamples); + if (requestedNumberOfSamples <= numberOfSamples) + { + numberOfSamples = requestedNumberOfSamples; + } + } + + std::vector miningSeeds(numberOfSamples); + std::vector publicKeys(numberOfSamples); + std::vector nonces(numberOfSamples); + + // Reading the input samples + for (unsigned long long i = 0; i < numberOfSamples; ++i) + { + if (i < numberOfSamplesReadFromFile) + { + miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); + publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); + nonces[i] = hexTo32Bytes(sampleString[i][2], 32); + } + else // Samples from files are not enough, randomly generate more + { + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & miningSeeds[i].m256i_u8[24]); + + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & publicKeys[i].m256i_u8[24]); + + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[0]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[8]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[16]); + _rdrand64_step((unsigned long long*) & nonces[i].m256i_u8[24]); + + } + } + + std::cout << "Profiling " << numberOfGeneratedSetting << " param combinations. " << std::endl; + + // Run the profiling + unsigned int numberOfThreads = std::thread::hardware_concurrency(); + if (MAX_NUMBER_OF_PROFILING_THREADS > 0) + { + numberOfThreads = numberOfThreads > MAX_NUMBER_OF_PROFILING_THREADS ? MAX_NUMBER_OF_PROFILING_THREADS : numberOfThreads; + } + std::cout << "Running " << numberOfThreads << " threads for collecting multiple threads performance" << std::endl; + + std::vector samples; + for (int i = 0; i < numberOfSamples; ++i) + { + if (!filteredSamples.empty() + && std::find(filteredSamples.begin(), filteredSamples.end(), i) == filteredSamples.end()) + { + continue; + } + samples.push_back(i); + } + + std::string compTerm = "for profiling, don't compare any result."; + + std::cout << "Processing " << samples.size() << " samples " << compTerm << "..." << std::endl; + gScoreProcessingTime.clear(); +#pragma omp parallel for num_threads(numberOfThreads) + for (int i = 0; i < samples.size(); ++i) + { + int index = samples[i]; + process<1, numberOfGeneratedSetting>(miningSeeds[index].m256i_u8, publicKeys[index].m256i_u8, nonces[index].m256i_u8, index); +#pragma omp critical + std::cout << i << ", "; + } + std::cout << std::endl; + + // Print the average processing time + for (auto scoreTime : gScoreProcessingTime) + { + unsigned long long processingTime = filteredSamples.empty() ? scoreTime.second / numberOfSamples : scoreTime.second / filteredSamples.size(); + std::cout << "Avg processing time [setting " << scoreTime.first << " " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_INPUT_NEURONS] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_OUTPUT_NEURONS] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_TICKS] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_NEIGHBORS] << ", " + << kProfileSettings[scoreTime.first][score_params::POPULATION_THRESHOLD] << ", " + << kProfileSettings[scoreTime.first][score_params::NUMBER_OF_MUTATIONS] << ", " + << kProfileSettings[scoreTime.first][score_params::SOLUTION_THRESHOLD] + << "]: " << processingTime << " ms" << std::endl; + } + gProfilingDataCollector.writeToFile(); +} TEST(TestQubicScoreFunction, CommonTests) { runCommonTests(); } +#if ENABLE_PROFILING + +TEST(TestQubicScoreFunction, PerformanceTests) +{ + runPerformanceTests(); +} +#endif + +#if not ENABLE_PROFILING TEST(TestQubicScoreFunction, TestDeterministic) { constexpr int NUMBER_OF_THREADS = 4; @@ -430,9 +578,9 @@ TEST(TestQubicScoreFunction, TestDeterministic) // Reading the input samples for (unsigned long long i = 0; i < numberOfSamples; ++i) { - miningSeeds[i] = hexToByte(sampleString[i][0], 32); - publicKeys[i] = hexToByte(sampleString[i][1], 32); - nonces[i] = hexToByte(sampleString[i][2], 32); + miningSeeds[i] = hexTo32Bytes(sampleString[i][0], 32); + publicKeys[i] = hexTo32Bytes(sampleString[i][1], 32); + nonces[i] = hexTo32Bytes(sampleString[i][2], 32); } auto pScore = std::make_uniqueinitMemory(); // Run with 4 mining seeds, each run 4 separate threads and the result need to matched - int scores[NUMBER_OF_PHASES][NUMBER_OF_THREADS * NUMBER_OF_SAMPLES] = {0}; + int scores[NUMBER_OF_PHASES][NUMBER_OF_THREADS * NUMBER_OF_SAMPLES] = { 0 }; for (unsigned long long i = 0; i < NUMBER_OF_PHASES; ++i) { pScore->initMiningData(miningSeeds[i]); @@ -485,3 +633,4 @@ TEST(TestQubicScoreFunction, TestDeterministic) } } } +#endif diff --git a/test/score_params.h b/test/score_params.h index 1748db117..7ef8b8910 100644 --- a/test/score_params.h +++ b/test/score_params.h @@ -17,15 +17,14 @@ enum ParamType // Comment out when we want to reduce the number of running test static constexpr unsigned long long kSettings[][MAX_PARAM_TYPE] = { -#if ENABLE_PROFILING - {::NUMBER_OF_INPUT_NEURONS, ::NUMBER_OF_OUTPUT_NEURONS, ::NUMBER_OF_TICKS, ::NUMBER_OF_NEIGHBORS, ::POPULATION_THRESHOLD, ::NUMBER_OF_MUTATIONS, ::SOLUTION_THRESHOLD_DEFAULT}, -#else - //{ ::NUMBER_OF_INPUT_NEURONS, ::NUMBER_OF_OUTPUT_NEURONS, ::NUMBER_OF_TICKS, ::NUMBER_OF_NEIGHBORS, ::POPULATION_THRESHOLD, ::NUMBER_OF_MUTATIONS, ::SOLUTION_THRESHOLD_DEFAULT }, {64, 64, 50, 64, 178, 50, 36}, {256, 256, 120, 256, 612, 100, 171}, {512, 512, 150, 512, 1174, 150, 300}, {1024, 1024, 200, 1024, 3000, 200, 600} - -#endif }; + +static constexpr unsigned long long kProfileSettings[][MAX_PARAM_TYPE] = { + {::NUMBER_OF_INPUT_NEURONS, ::NUMBER_OF_OUTPUT_NEURONS, ::NUMBER_OF_TICKS, ::NUMBER_OF_NEIGHBORS, ::POPULATION_THRESHOLD, ::NUMBER_OF_MUTATIONS, ::SOLUTION_THRESHOLD_DEFAULT}, +}; + } diff --git a/test/test.vcxproj b/test/test.vcxproj index b5611ed87..1aa9a609b 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -17,7 +17,7 @@ {30e8e249-6b00-4575-bcdf-be2445d5e099} Win32Proj - 10.0.22621.0 + 10.0 Application v143 Unicode @@ -120,7 +120,10 @@ + + + @@ -133,6 +136,8 @@ + + @@ -155,9 +160,6 @@ - - - @@ -169,15 +171,18 @@ {88b4cda8-8248-44d0-848e-0e938a2aad6d} + + + - + - Dieses Projekt verweist auf mindestens ein NuGet-Paket, das auf diesem Computer fehlt. Verwenden Sie die Wiederherstellung von NuGet-Paketen, um die fehlenden Dateien herunterzuladen. Weitere Informationen finden Sie unter "http://go.microsoft.com/fwlink/?LinkID=322105". Die fehlende Datei ist "{0}". + Este proyecto hace referencia a los paquetes NuGet que faltan en este equipo. Use la restauración de paquetes NuGet para descargarlos. Para obtener más información, consulte http://go.microsoft.com/fwlink/?LinkID=322105. El archivo que falta es {0}. - + \ No newline at end of file diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 73566057a..76ddcae28 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -27,6 +27,7 @@ + @@ -36,6 +37,10 @@ + + + + @@ -44,9 +49,6 @@ - - - {b544a744-99a4-4a9d-b445-211aabef63f2} @@ -57,4 +59,7 @@ core + + + \ No newline at end of file diff --git a/test/utils.h b/test/utils.h index a48cc9995..734d88933 100644 --- a/test/utils.h +++ b/test/utils.h @@ -9,7 +9,7 @@ namespace test_utils { -std::string byteToHex(const unsigned char* byteArray, size_t sizeInByte) +static std::string byteToHex(const unsigned char* byteArray, size_t sizeInByte) { std::ostringstream oss; for (size_t i = 0; i < sizeInByte; ++i) @@ -19,7 +19,7 @@ std::string byteToHex(const unsigned char* byteArray, size_t sizeInByte) return oss.str(); } -m256i hexToByte(const std::string& hex, const int sizeInByte) +static m256i hexTo32Bytes(const std::string& hex, const int sizeInByte) { if (hex.length() != sizeInByte * 2) { throw std::invalid_argument("Hex string length does not match the expected size"); @@ -34,8 +34,21 @@ m256i hexToByte(const std::string& hex, const int sizeInByte) return byteArray; } +static void hexToByte(const std::string& hex, const int sizeInByte, unsigned char* out) +{ + if (hex.length() != sizeInByte * 2) + { + throw std::invalid_argument("Hex string length does not match the expected size"); + } + + for (size_t i = 0; i < sizeInByte; ++i) + { + out[i] = std::stoi(hex.substr(i * 2, 2), nullptr, 16); + } +} + // Function to read and parse the CSV file -std::vector> readCSV(const std::string& filename) +static std::vector> readCSV(const std::string& filename) { std::vector> data; std::ifstream file(filename); @@ -61,7 +74,7 @@ std::vector> readCSV(const std::string& filename) return data; } -m256i convertFromString(std::string& rStr) +static m256i convertFromString(std::string& rStr) { m256i value; std::stringstream ss(rStr); @@ -74,7 +87,7 @@ m256i convertFromString(std::string& rStr) return value; } -std::vector convertULLFromString(std::string& rStr) +static std::vector convertULLFromString(std::string& rStr) { std::vector values; std::stringstream ss(rStr); diff --git a/tests.md b/tests.md new file mode 100644 index 000000000..4ec59cbe8 --- /dev/null +++ b/tests.md @@ -0,0 +1,22 @@ +## Compliance +- Result: Contract compliance check PASSED +- Tool: Qubic Contract Verification Tool (`qubic-contract-verify`) + +## Test Execution +- Command: `test.exe --gtest_filter=VottunBridge*` +- Result: 14 tests passed (0 failed) +- Tests (scope/expected behavior): + - `CreateOrder_RequiresFee`: rejects creation if the paid fee is below the required amount. + - `TransferToContract_RejectsMissingReward`: with pre-funded contract balance, a call without attached QUs fails and does not change balances or `lockedTokens`. + - `TransferToContract_AcceptsExactReward`: accepts when `invocationReward` matches `amount` and locks tokens. + - `TransferToContract_OrderNotFound`: rejects when `orderId` does not exist. + - `TransferToContract_InvalidAmountMismatch`: rejects when `input.amount` does not match the order amount. + - `TransferToContract_InvalidOrderState`: rejects when the order is not in the created state. + - `CreateOrder_CleansCompletedAndRefundedSlots`: cleans completed/refunded slots to allow a new order. + - `CreateProposal_CleansExecutedProposalsWhenFull`: cleans executed proposals to free slots and create a new one. + - `CreateProposal_InvalidTypeRejected`: rejects out-of-range proposal types. + - `ApproveProposal_NotOwnerRejected`: blocks approval when invocator is not a multisig admin. + - `ApproveProposal_DoubleApprovalRejected`: prevents double approval by the same admin. + - `ApproveProposal_ExecutesChangeThreshold`: executes the proposal once the threshold is reached. + - `ApproveProposal_ProposalNotFound`: rejects approval for a non-existent `proposalId`. + - `ApproveProposal_AlreadyExecuted`: rejects approval for a proposal already executed. \ No newline at end of file