diff --git a/src/Qubic.vcxproj b/src/Qubic.vcxproj index 961ee7dc..1165b1ab 100644 --- a/src/Qubic.vcxproj +++ b/src/Qubic.vcxproj @@ -36,7 +36,8 @@ - + + diff --git a/src/Qubic.vcxproj.filters b/src/Qubic.vcxproj.filters index cfcb739a..33711b95 100644 --- a/src/Qubic.vcxproj.filters +++ b/src/Qubic.vcxproj.filters @@ -98,6 +98,9 @@ contracts + + + contracts contracts diff --git a/src/contract_core/contract_def.h b/src/contract_core/contract_def.h index f6f11909..591dd9d3 100644 --- a/src/contract_core/contract_def.h +++ b/src/contract_core/contract_def.h @@ -302,6 +302,16 @@ #endif +#undef CONTRACT_INDEX +#undef CONTRACT_STATE_TYPE +#undef CONTRACT_STATE2_TYPE + +#define QASSAND_CONTRACT_INDEX 29 +#define CONTRACT_INDEX QASSAND_CONTRACT_INDEX +#define CONTRACT_STATE_TYPE QASSAND +#define CONTRACT_STATE2_TYPE QASSAND2 +#include "contracts/Qassand.h" + // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES @@ -417,8 +427,9 @@ constexpr struct ContractDescription {"QUSINO", 208, 10000, sizeof(QUSINO::StateData)}, // proposal in epoch 206, IPO in 207, construction and first use in 208 {"ESCROW", 210, 10000, sizeof(ESCROW::StateData)}, // proposal in epoch 208, IPO in 209, construction and first use in 210 #ifndef NO_GGWP - {"GGWP", 217, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 215, IPO in 216, construction and first use in 217 + {"GGWP", 218, 10000, sizeof(WOLFPACK::StateData)}, // proposal in epoch 216, IPO in 217, construction and first use in 218 #endif + {"QASSAND", QASSAND_CONSTRUCTION_EPOCH_PLACEHOLDER, 10000, sizeof(QASSAND::StateData)}, // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES {"TESTEXA", 138, 10000, sizeof(TESTEXA::StateData)}, @@ -545,6 +556,7 @@ static void initializeContracts() #ifndef NO_GGWP REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(WOLFPACK); #endif + REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(QASSAND); // new contracts should be added above this line #ifdef INCLUDE_CONTRACT_TEST_EXAMPLES REGISTER_CONTRACT_FUNCTIONS_AND_PROCEDURES(TESTEXA); @@ -557,7 +569,9 @@ static void initializeContracts() // Automatic Contract State Changes enum ContractStateChangeType { + // Keeps the saved state's old bytes, only zero-fills the new bytes at the end (used when struct grew; old fields preserved) PADDING, + // Discards the saved state entirely, zeros the whole buffer RESET, }; struct ContractStateChangeInfo @@ -566,8 +580,9 @@ struct ContractStateChangeInfo ContractStateChangeType changeType; }; // Contracts whose state struct changed this epoch. Update this list each epoch as needed. +// Each entry is { CONTRACT_INDEX, PADDING or RESET } // When enabling, replace both lines below, e.g.: -constexpr ContractStateChangeInfo contractStateChangeInfos[] = { { QIP_CONTRACT_INDEX, RESET } }; +constexpr ContractStateChangeInfo contractStateChangeInfos[] = { {RANDOM_CONTRACT_INDEX, PADDING} }; constexpr unsigned int contractStateChangeCount = sizeof(contractStateChangeInfos) / sizeof(contractStateChangeInfos[0]); // constexpr const ContractStateChangeInfo* contractStateChangeInfos = nullptr; // constexpr unsigned int contractStateChangeCount = 0; diff --git a/src/contracts/Qassand.h b/src/contracts/Qassand.h new file mode 100644 index 00000000..9053351d --- /dev/null +++ b/src/contracts/Qassand.h @@ -0,0 +1,256 @@ +using namespace QPI; + +// QASSAND is the Qubic contract implementation name for the Qassandra protocol v0. +constexpr uint16 QASSAND_VERSION = 0; +constexpr uint16 QASSAND_CONSTRUCTION_EPOCH_PLACEHOLDER = 10000; +constexpr uint64 QASSAND_PROTOCOL_FEE = 75000; +constexpr uint64 QASSAND_BURN_FEE = 25000; +constexpr uint64 QASSAND_PING_FEE = QASSAND_PROTOCOL_FEE + QASSAND_BURN_FEE; +constexpr uint8 QASSAND_SUCCESS = 0; +constexpr uint8 QASSAND_UNDERPAID = 1; +constexpr uint8 QASSAND_UNKNOWN_LANE = 2; +constexpr uint16 QASSAND_LANE_UNKNOWN = 0; +constexpr uint16 QASSAND_LANE_FORECASTING = 1; +constexpr uint16 QASSAND_LANE_STABLE_OPERATIONS = 2; +constexpr uint16 QASSAND_LANE_DATA_ATTESTATION = 3; + +struct QASSAND2 +{ +}; + +struct QASSAND : public ContractBase +{ + struct StateData + { + uint16 version; + uint16 constructionEpoch; + uint64 protocolFee; + uint64 burnFee; + uint64 totalPingCount; + uint64 protocolEarnedFee; + uint64 burnEarnedFee; + uint64 pendingBurnAmount; + uint64 totalBurnedAmount; + }; + + struct Ping_input + { + }; + + struct Ping_output + { + uint8 returnCode; + uint64 acceptedFee; + uint64 refundedAmount; + uint64 protocolEarnedFee; + uint64 burnEarnedFee; + uint64 totalPingCount; + }; + + struct Ping_locals + { + uint64 paidAmount; + uint64 refundAmount; + }; + + struct GetInfo_input + { + }; + + struct GetInfo_output + { + Array protocolName; + uint16 version; + uint16 constructionEpoch; + uint64 totalPingCount; + }; + + struct GetFeeInfo_input + { + }; + + struct GetFeeInfo_output + { + uint64 pingFee; + uint64 protocolFee; + uint64 burnFee; + uint64 protocolEarnedFee; + uint64 burnEarnedFee; + }; + + struct GetBurnInfo_input + { + }; + + struct GetBurnInfo_output + { + uint64 pendingBurnAmount; + uint64 totalBurnedAmount; + }; + + struct GetLaneInfo_input + { + uint16 laneId; + }; + + struct GetLaneInfo_output + { + uint8 returnCode; + uint16 laneId; + Array laneName; + uint64 requiredFee; + }; + + struct END_TICK_locals + { + uint64 burnAmount; + }; + + INITIALIZE() + { + state.mut().version = QASSAND_VERSION; + state.mut().constructionEpoch = QASSAND_CONSTRUCTION_EPOCH_PLACEHOLDER; + state.mut().protocolFee = QASSAND_PROTOCOL_FEE; + state.mut().burnFee = QASSAND_BURN_FEE; + } + + REGISTER_USER_FUNCTIONS_AND_PROCEDURES() + { + REGISTER_USER_PROCEDURE(Ping, 1); + REGISTER_USER_FUNCTION(GetInfo, 1); + REGISTER_USER_FUNCTION(GetFeeInfo, 2); + REGISTER_USER_FUNCTION(GetBurnInfo, 3); + REGISTER_USER_FUNCTION(GetLaneInfo, 4); + } + + END_TICK_WITH_LOCALS() + { + locals.burnAmount = state.get().pendingBurnAmount; + if (locals.burnAmount == 0) + { + return; + } + + qpi.burn(locals.burnAmount); + state.mut().pendingBurnAmount = 0; + state.mut().totalBurnedAmount += locals.burnAmount; + } + + PUBLIC_PROCEDURE_WITH_LOCALS(Ping) + { + locals.paidAmount = qpi.invocationReward(); + + if (locals.paidAmount < (state.get().protocolFee + state.get().burnFee)) + { + if (locals.paidAmount != 0) + { + qpi.transfer(qpi.invocator(), locals.paidAmount); + } + output.returnCode = QASSAND_UNDERPAID; + output.refundedAmount = locals.paidAmount; + return; + } + + locals.refundAmount = locals.paidAmount - (state.get().protocolFee + state.get().burnFee); + if (locals.refundAmount != 0) + { + qpi.transfer(qpi.invocator(), locals.refundAmount); + } + + state.mut().totalPingCount += 1; + state.mut().protocolEarnedFee += state.get().protocolFee; + state.mut().burnEarnedFee += state.get().burnFee; + state.mut().pendingBurnAmount += state.get().burnFee; + + output.returnCode = QASSAND_SUCCESS; + output.acceptedFee = (state.get().protocolFee + state.get().burnFee); + output.refundedAmount = locals.refundAmount; + output.protocolEarnedFee = state.get().protocolFee; + output.burnEarnedFee = state.get().burnFee; + output.totalPingCount = state.get().totalPingCount; + } + + PUBLIC_FUNCTION(GetInfo) + { + output.protocolName.set(0, 81); + output.protocolName.set(1, 97); + output.protocolName.set(2, 115); + output.protocolName.set(3, 115); + output.protocolName.set(4, 97); + output.protocolName.set(5, 110); + output.protocolName.set(6, 100); + output.protocolName.set(7, 114); + output.protocolName.set(8, 97); + output.version = state.get().version; + output.constructionEpoch = state.get().constructionEpoch; + output.totalPingCount = state.get().totalPingCount; + } + + PUBLIC_FUNCTION(GetFeeInfo) + { + output.pingFee = (state.get().protocolFee + state.get().burnFee); + output.protocolFee = state.get().protocolFee; + output.burnFee = state.get().burnFee; + output.protocolEarnedFee = state.get().protocolEarnedFee; + output.burnEarnedFee = state.get().burnEarnedFee; + } + + PUBLIC_FUNCTION(GetBurnInfo) + { + output.pendingBurnAmount = state.get().pendingBurnAmount; + output.totalBurnedAmount = state.get().totalBurnedAmount; + } + + PUBLIC_FUNCTION(GetLaneInfo) + { + if (input.laneId == QASSAND_LANE_FORECASTING) + { + output.returnCode = QASSAND_SUCCESS; + output.laneId = QASSAND_LANE_FORECASTING; + output.laneName.set(0, 70); + output.laneName.set(1, 111); + output.laneName.set(2, 114); + output.laneName.set(3, 101); + output.laneName.set(4, 99); + output.laneName.set(5, 97); + output.laneName.set(6, 115); + output.laneName.set(7, 116); + output.laneName.set(8, 105); + output.laneName.set(9, 110); + output.laneName.set(10, 103); + output.requiredFee = 0; + return; + } + + if (input.laneId == QASSAND_LANE_STABLE_OPERATIONS) + { + output.returnCode = QASSAND_SUCCESS; + output.laneId = QASSAND_LANE_STABLE_OPERATIONS; + output.laneName.set(0, 83); + output.laneName.set(1, 116); + output.laneName.set(2, 97); + output.laneName.set(3, 98); + output.laneName.set(4, 108); + output.laneName.set(5, 101); + output.laneName.set(6, 79); + output.laneName.set(7, 112); + output.laneName.set(8, 115); + output.requiredFee = 0; + return; + } + + if (input.laneId == QASSAND_LANE_DATA_ATTESTATION) + { + output.returnCode = QASSAND_SUCCESS; + output.laneId = QASSAND_LANE_DATA_ATTESTATION; + output.laneName.set(0, 68); + output.laneName.set(1, 97); + output.laneName.set(2, 116); + output.laneName.set(3, 97); + output.requiredFee = 0; + return; + } + + output.returnCode = QASSAND_UNKNOWN_LANE; + } +}; diff --git a/test/contract_qassand.cpp b/test/contract_qassand.cpp new file mode 100644 index 00000000..6c46a4c3 --- /dev/null +++ b/test/contract_qassand.cpp @@ -0,0 +1,230 @@ +#define NO_UEFI + +#include "contract_testing.h" + +constexpr uint16 QASSAND_PROC_PING = 1; +constexpr uint16 QASSAND_FUNC_GET_INFO = 1; +constexpr uint16 QASSAND_FUNC_GET_FEE_INFO = 2; +constexpr uint16 QASSAND_FUNC_GET_BURN_INFO = 3; +constexpr uint16 QASSAND_FUNC_GET_LANE_INFO = 4; + +static const id QASSAND_CONTRACT_ID(QASSAND_CONTRACT_INDEX, 0, 0, 0); + +class QassandChecker : public QASSAND, public QASSAND::StateData +{ +public: + uint16 versionValue() const { return version; } + uint16 constructionEpochValue() const { return constructionEpoch; } + uint64 totalPingCountValue() const { return totalPingCount; } + uint64 protocolEarnedFeeValue() const { return protocolEarnedFee; } + uint64 burnEarnedFeeValue() const { return burnEarnedFee; } + uint64 pendingBurnAmountValue() const { return pendingBurnAmount; } + uint64 totalBurnedAmountValue() const { return totalBurnedAmount; } +}; + +class ContractTestingQassand : protected ContractTesting +{ +public: + ContractTestingQassand() + { + initEmptySpectrum(); + initEmptyUniverse(); + INIT_CONTRACT(QASSAND); + callSystemProcedure(QASSAND_CONTRACT_INDEX, INITIALIZE); + } + + QassandChecker* state() { return reinterpret_cast(contractStates[QASSAND_CONTRACT_INDEX]); } + + uint64 balanceOf(const id& account) const { return static_cast(getBalance(account)); } + uint64 balanceQassand() const { return balanceOf(QASSAND_CONTRACT_ID); } + void fund(const id& account, uint64 amount) { increaseEnergy(account, amount); } + + QASSAND::Ping_output ping(const id& invocator, uint64 amount) + { + QASSAND::Ping_input input{}; + QASSAND::Ping_output output{}; + invokeUserProcedure(QASSAND_CONTRACT_INDEX, QASSAND_PROC_PING, input, output, invocator, amount); + return output; + } + + QASSAND::GetInfo_output getInfo() const + { + QASSAND::GetInfo_input input{}; + QASSAND::GetInfo_output output{}; + callFunction(QASSAND_CONTRACT_INDEX, QASSAND_FUNC_GET_INFO, input, output); + return output; + } + + QASSAND::GetFeeInfo_output getFeeInfo() const + { + QASSAND::GetFeeInfo_input input{}; + QASSAND::GetFeeInfo_output output{}; + callFunction(QASSAND_CONTRACT_INDEX, QASSAND_FUNC_GET_FEE_INFO, input, output); + return output; + } + + QASSAND::GetBurnInfo_output getBurnInfo() const + { + QASSAND::GetBurnInfo_input input{}; + QASSAND::GetBurnInfo_output output{}; + callFunction(QASSAND_CONTRACT_INDEX, QASSAND_FUNC_GET_BURN_INFO, input, output); + return output; + } + + QASSAND::GetLaneInfo_output getLaneInfo(uint16 laneId) const + { + QASSAND::GetLaneInfo_input input{laneId}; + QASSAND::GetLaneInfo_output output{}; + callFunction(QASSAND_CONTRACT_INDEX, QASSAND_FUNC_GET_LANE_INFO, input, output); + return output; + } + + void endTick() { callSystemProcedure(QASSAND_CONTRACT_INDEX, END_TICK); } +}; + +static bool arrayStartsWith(const Array& value, const char* expected) +{ + for (uint16 i = 0; expected[i] != 0; ++i) + { + if (value.get(i) != static_cast(expected[i])) + { + return false; + } + } + return true; +} + +TEST(ContractQassand, InitializeSetsPingV0Metadata) +{ + ContractTestingQassand qassand; + QassandChecker* state = qassand.state(); + + EXPECT_EQ(state->versionValue(), QASSAND_VERSION); + EXPECT_EQ(state->constructionEpochValue(), QASSAND_CONSTRUCTION_EPOCH_PLACEHOLDER); + EXPECT_EQ(state->totalPingCountValue(), 0ull); + EXPECT_EQ(state->protocolEarnedFeeValue(), 0ull); + EXPECT_EQ(state->burnEarnedFeeValue(), 0ull); + EXPECT_EQ(state->pendingBurnAmountValue(), 0ull); + EXPECT_EQ(state->totalBurnedAmountValue(), 0ull); +} + +TEST(ContractQassand, ReadsMetadataAndFeeState) +{ + ContractTestingQassand qassand; + + const QASSAND::GetInfo_output info = qassand.getInfo(); + EXPECT_TRUE(arrayStartsWith(info.protocolName, "Qassandra")); + EXPECT_EQ(info.version, QASSAND_VERSION); + EXPECT_EQ(info.constructionEpoch, QASSAND_CONSTRUCTION_EPOCH_PLACEHOLDER); + EXPECT_EQ(info.totalPingCount, 0ull); + + const QASSAND::GetFeeInfo_output fees = qassand.getFeeInfo(); + EXPECT_EQ(fees.pingFee, QASSAND_PING_FEE); + EXPECT_EQ(fees.protocolFee, QASSAND_PROTOCOL_FEE); + EXPECT_EQ(fees.burnFee, QASSAND_BURN_FEE); + EXPECT_EQ(fees.protocolEarnedFee, 0ull); + EXPECT_EQ(fees.burnEarnedFee, 0ull); + + const QASSAND::GetBurnInfo_output burn = qassand.getBurnInfo(); + EXPECT_EQ(burn.pendingBurnAmount, 0ull); + EXPECT_EQ(burn.totalBurnedAmount, 0ull); +} + +TEST(ContractQassand, ReadsLaneTaxonomy) +{ + ContractTestingQassand qassand; + + const QASSAND::GetLaneInfo_output forecastingLane = qassand.getLaneInfo(QASSAND_LANE_FORECASTING); + EXPECT_EQ(forecastingLane.returnCode, QASSAND_SUCCESS); + EXPECT_EQ(forecastingLane.laneId, QASSAND_LANE_FORECASTING); + EXPECT_TRUE(arrayStartsWith(forecastingLane.laneName, "Forecasting")); + EXPECT_EQ(forecastingLane.requiredFee, 0ull); + + const QASSAND::GetLaneInfo_output stableLane = qassand.getLaneInfo(QASSAND_LANE_STABLE_OPERATIONS); + EXPECT_EQ(stableLane.returnCode, QASSAND_SUCCESS); + EXPECT_EQ(stableLane.laneId, QASSAND_LANE_STABLE_OPERATIONS); + EXPECT_TRUE(arrayStartsWith(stableLane.laneName, "StableOps")); + EXPECT_EQ(stableLane.requiredFee, 0ull); + + const QASSAND::GetLaneInfo_output attestationLane = qassand.getLaneInfo(QASSAND_LANE_DATA_ATTESTATION); + EXPECT_EQ(attestationLane.returnCode, QASSAND_SUCCESS); + EXPECT_EQ(attestationLane.laneId, QASSAND_LANE_DATA_ATTESTATION); + EXPECT_TRUE(arrayStartsWith(attestationLane.laneName, "Data")); + EXPECT_EQ(attestationLane.requiredFee, 0ull); + + const QASSAND::GetLaneInfo_output unknownLane = qassand.getLaneInfo(QASSAND_LANE_UNKNOWN); + EXPECT_EQ(unknownLane.returnCode, QASSAND_UNKNOWN_LANE); +} + +TEST(ContractQassand, UnderpaymentRefundsAndDoesNotAccountFee) +{ + ContractTestingQassand qassand; + const id user = id::randomValue(); + qassand.fund(user, QASSAND_PING_FEE); + + const uint64 underpaidAmount = QASSAND_PING_FEE - 1; + const QASSAND::Ping_output underpaid = qassand.ping(user, underpaidAmount); + EXPECT_EQ(underpaid.returnCode, QASSAND_UNDERPAID); + EXPECT_EQ(underpaid.refundedAmount, underpaidAmount); + EXPECT_EQ(qassand.balanceOf(user), QASSAND_PING_FEE); + EXPECT_EQ(qassand.balanceQassand(), 0ull); + EXPECT_EQ(qassand.state()->totalPingCountValue(), 0ull); + EXPECT_EQ(qassand.state()->protocolEarnedFeeValue(), 0ull); + EXPECT_EQ(qassand.state()->burnEarnedFeeValue(), 0ull); + EXPECT_EQ(qassand.state()->pendingBurnAmountValue(), 0ull); +} + +TEST(ContractQassand, ExactFeeAccountsProtocolAndDeferredBurn) +{ + ContractTestingQassand qassand; + const id user = id::randomValue(); + qassand.fund(user, QASSAND_PING_FEE); + + const QASSAND::Ping_output ping = qassand.ping(user, QASSAND_PING_FEE); + EXPECT_EQ(ping.returnCode, QASSAND_SUCCESS); + EXPECT_EQ(ping.acceptedFee, QASSAND_PING_FEE); + EXPECT_EQ(ping.refundedAmount, 0ull); + EXPECT_EQ(ping.protocolEarnedFee, QASSAND_PROTOCOL_FEE); + EXPECT_EQ(ping.burnEarnedFee, QASSAND_BURN_FEE); + EXPECT_EQ(ping.totalPingCount, 1ull); + + EXPECT_EQ(qassand.balanceOf(user), 0ull); + EXPECT_EQ(qassand.balanceQassand(), QASSAND_PING_FEE); + EXPECT_EQ(qassand.state()->totalPingCountValue(), 1ull); + EXPECT_EQ(qassand.state()->protocolEarnedFeeValue(), QASSAND_PROTOCOL_FEE); + EXPECT_EQ(qassand.state()->burnEarnedFeeValue(), QASSAND_BURN_FEE); + EXPECT_EQ(qassand.state()->pendingBurnAmountValue(), QASSAND_BURN_FEE); +} + +TEST(ContractQassand, ExcessFeeRefundsOnlyOverage) +{ + ContractTestingQassand qassand; + const id user = id::randomValue(); + const uint64 paidAmount = QASSAND_PING_FEE + 12345; + qassand.fund(user, paidAmount); + + const QASSAND::Ping_output ping = qassand.ping(user, paidAmount); + EXPECT_EQ(ping.returnCode, QASSAND_SUCCESS); + EXPECT_EQ(ping.acceptedFee, QASSAND_PING_FEE); + EXPECT_EQ(ping.refundedAmount, 12345ull); + EXPECT_EQ(qassand.balanceOf(user), 12345ull); + EXPECT_EQ(qassand.balanceQassand(), QASSAND_PING_FEE); + EXPECT_EQ(qassand.state()->totalPingCountValue(), 1ull); + EXPECT_EQ(qassand.state()->pendingBurnAmountValue(), QASSAND_BURN_FEE); +} + +TEST(ContractQassand, EndTickBurnsDeferredAmount) +{ + ContractTestingQassand qassand; + const id user = id::randomValue(); + qassand.fund(user, QASSAND_PING_FEE); + + qassand.ping(user, QASSAND_PING_FEE); + EXPECT_EQ(qassand.state()->pendingBurnAmountValue(), QASSAND_BURN_FEE); + EXPECT_EQ(qassand.state()->totalBurnedAmountValue(), 0ull); + + qassand.endTick(); + EXPECT_EQ(qassand.state()->pendingBurnAmountValue(), 0ull); + EXPECT_EQ(qassand.state()->totalBurnedAmountValue(), QASSAND_BURN_FEE); + EXPECT_EQ(qassand.balanceQassand(), QASSAND_PROTOCOL_FEE); +} diff --git a/test/test.vcxproj b/test/test.vcxproj index 2372e73d..c9d15ba7 100644 --- a/test/test.vcxproj +++ b/test/test.vcxproj @@ -148,6 +148,7 @@ + diff --git a/test/test.vcxproj.filters b/test/test.vcxproj.filters index 8eebbc6f..683c43bb 100644 --- a/test/test.vcxproj.filters +++ b/test/test.vcxproj.filters @@ -22,6 +22,7 @@ +