Skip to content

Commit ff681a1

Browse files
authored
Automatic state change detection (#774)
* Add automatic state change detection with ContractState wrapper - Wrap contract state access in ContractState<T, idx> template with .get() (const) and .mut() (marks dirty) accessors - Move all contract state fields into nested struct StateData for each contract - Add needsCleanup() const method to containers - Add using declaration to unhide const operator() overload in QpiContextProcedureCall - Use .get() instead of .mut() for const proposal method calls - Update contracts.md: describe StateData, state.get()/state.mut() accessors, and dirty state digest recomputation at end of tick - Update contracts_proposals.md: update all code examples to use state.get().proposals / state.mut().proposals - Update README.md: mention StateData and accessor pattern - Update EmptyTemplate.h: add empty StateData with usage comments
1 parent 2fd3ecc commit ff681a1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+5184
-4955
lines changed

doc/contracts.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,13 @@ A contract also never gets access to uninitialized memory (all memory is initial
2626

2727
Each contract is implemented in one C++ header file in the directory `src/contracts`.
2828

29-
A contract has a state struct, containing all its data as member variables.
29+
A contract is defined as a struct inheriting from `ContractBase`. All persistent data of the contract must be declared inside a nested `struct StateData`.
3030
The memory available to the contract is allocated statically, but extending the state will be possible between epochs through special `EXPAND` events.
3131

32-
The state struct also includes the procedures and functions of the contract, which have to be defined using special macros such as `PUBLIC_PROCEDURE()`, `PRIVATE_FUNCTION()`, or `BEGIN_EPOCH()`.
33-
Functions cannot modify the state, but they are useful to query information with the network message `RequestContractFunction`.
34-
Procedures can modify the state and are either invoked by special transactions (user procedures) or internal core events (system procedures).
32+
The contract struct also includes the procedures and functions of the contract, which have to be defined using special macros such as `PUBLIC_PROCEDURE()`, `PRIVATE_FUNCTION()`, or `BEGIN_EPOCH()`.
33+
The state is accessed through a `ContractState` wrapper named `state`: use `state.get()` for read-only access and `state.mut()` for write access. Calling `state.mut()` marks the state as dirty for automatic change detection. The digest of dirty contract states is recomputed at the end of each tick, so that only contracts whose state actually changed incur the cost of rehashing.
34+
Functions can only read the state (via `state.get()`), making them useful to query information with the network message `RequestContractFunction`.
35+
Procedures can modify the state (via `state.mut()`) and are either invoked by special transactions (user procedures) or internal core events (system procedures).
3536

3637
Contract developers should be aware of the following parts of the Qubic protocol that are not implemented yet in the core:
3738
- Execution of contract procedures will cost fees that will be paid from its contract fee reserve.
@@ -163,7 +164,7 @@ In order to make the function available for external requests through the `Reque
163164
`[INPUT_TYPE]` is an integer greater or equal to 1 and less or equal to 65535, which identifies the function to call in the `RequestContractFunction` message (`inputType` member).
164165
If the `inputSize` member in `RequestContractFunction` does not match `sizeof([NAME]_input)`, the input data is either cut off or padded with zeros.
165166

166-
The contract state is passed to the function as a const reference named `state`.
167+
The contract state is accessed through a `ContractState` wrapper named `state`. In functions, only `state.get()` is available, returning a const reference to the `StateData`. For example, `state.get().myField` reads a field from the state.
167168

168169
Use the macro with the postfix `_WITH_LOCALS` if the function needs local variables, because (1) the contract state cannot be modified within contract functions and (2) creating local variables / objects on the regular function call stack is forbidden.
169170
With these macros, you have to define the struct `[NAME]_locals`.
@@ -201,8 +202,7 @@ In order to make the function available for invocation by transactions, you need
201202
`REGISTER_USER_PROCEDURE` has its own set of input types, so the same input type number may be used for both `REGISTER_USER_PROCEDURE` and `REGISTER_USER_FUNCTION` (for example there may be one function with input type 1 and one procedure with input type 1).
202203
If the `inputSize` member in `Transaction` does not match `sizeof([NAME]_input)`, the input data is either cut off or padded with zeros.
203204

204-
The contract state is passed to the procedure as a reference named `state`.
205-
And it can be modified (in contrast to contract functions).
205+
The contract state is accessed through a `ContractState` wrapper named `state`. In procedures, both `state.get()` (read-only) and `state.mut()` (read-write) are available. Use `state.mut()` when modifying state fields, e.g. `state.mut().myField = newValue`. Calling `state.mut()` marks the state as dirty so that its digest is recomputed at the end of the tick.
206206

207207
Use the macro with the postfix `_WITH_LOCALS` if the procedure needs local variables, because creating local variables / objects on the regular function call stack is forbidden.
208208
With these macros, you have to define the struct `[NAME]_locals`.
@@ -252,8 +252,7 @@ System procedures 1 to 5 have no input and output.
252252
The input and output of system procedures 6 to 9 are discussed in the section about [management rights transfer](#management-rights-transfer).
253253
The system procedure 11 and 12 are discussed in the section about [contracts as shareholder of other contracts](contracts_proposals.md#contracts-as-shareholders-of-other-contracts)
254254

255-
The contract state is passed to each of the procedures as a reference named `state`.
256-
And it can be modified (in contrast to contract functions).
255+
The contract state is accessed through a `ContractState` wrapper named `state`, the same as in user procedures. Use `state.get()` for reading and `state.mut()` for modifying state fields.
257256

258257
For each of the macros above, there is a variant with the postfix `_WITH_LOCALS`.
259258
These can be used, if the procedure needs local variables, because creating local variables / objects on the regular function call stack is forbidden.
@@ -572,7 +571,7 @@ https://github.com/qubic/core/issues/574
572571

573572
It is prohibited to locally instantiate objects or variables on the function call stack. This includes loop index variables `for (int i = 0; ...)`.
574573
Instead, use the function and procedure definition macros with the postfix `_WITH_LOCALS` (see above).
575-
In procedures you alternatively may store temporary variables permanently as members of the state.
574+
In procedures you alternatively may store temporary variables permanently as members of `StateData` (accessed via `state.mut()`).
576575

577576
Defining, casting, and dereferencing pointers is forbidden.
578577
The character `*` is only allowed in the context of multiplication.

doc/contracts_proposals.md

Lines changed: 46 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ There are some general characteristics of the proposal voting:
1010
- Each proposal has a type and some types of proposals commonly trigger action (such as setting a contract state variable or transferring QUs to another entity) after the end of the epoch if the proposal is accepted by getting enough votes.
1111
- The proposer entity can have at most one proposal at a time. Setting a new proposal with the same seed will overwrite the previous one.
1212
- Number of simultaneous proposals per epoch is limited as configured by the contract. The data structures storing the proposal and voting state are stored as a part of the contract state.
13-
- In this data storage, commonly named `state.proposals`, and the function/procedure interface, each proposal is identified by a proposal index.
13+
- In this data storage, commonly named `proposals` (a member of `StateData`, accessed via `state.get().proposals` or `state.mut().proposals`), and the function/procedure interface, each proposal is identified by a proposal index.
1414
- The types of proposals that are allowed are restricted as configured by the contract.
1515
- The common types of proposals have a predefined set of options that the voters can vote for. Option 0 is always "no change".
1616
- Each vote, which is connected to each voter entity, can have a value (most commonly an option index) or `NO_VOTE_VALUE` (which means abstaining).
@@ -63,10 +63,11 @@ Features:
6363

6464
If you need more than these features, go through the following steps anyway and continue reading the section about understanding the shareholder voting implementation.
6565

66-
#### 1. Setup proposal storage
66+
#### 1. Setup proposal types and storage
6767

68-
First, you need to add the proposal storage to your contract state.
69-
You can easily do this using the QPI macro `DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(numProposalSlots, assetName)`.
68+
First, you need to add the proposal types and storage to your contract.
69+
Use the QPI macro `DEFINE_SHAREHOLDER_PROPOSAL_TYPES(numProposalSlots, assetName)` to define the required types,
70+
and declare the `proposals` field manually inside your `StateData`.
7071
With the yes/no shareholder proposals supported by this macro, each proposal slot occupies 22144 Bytes of state memory.
7172
The number of proposal slots limits how many proposals can be open for voting simultaneously.
7273
The `assetName` that you have to pass as the second argument is the `uint64` representation of your contract's 7-character asset name.
@@ -80,20 +81,26 @@ std::cout << assetNameFromString("QUTIL") << std::endl;
8081
Replace "QUTIL" by your contract's asset name as given in `contractDescriptions` in `src/contact_core/contract_def.h`.
8182
You will get an integer that we recommend to assign to a `constexpr uint64` with a name following the scheme `QUTIL_CONTRACT_ASSET_NAME`.
8283

83-
When you have decided about the number of proposal slots and found out the the asset name, you can define the proposal storage similarly to this example taken from the contract QUTIL:
84+
When you have decided about the number of proposal slots and found out the the asset name, you can define the proposal types and storage similarly to this example taken from the contract QUTIL:
8485

8586
```C++
8687
struct QUTIL
8788
{
88-
// other state variables ...
89+
DEFINE_SHAREHOLDER_PROPOSAL_TYPES(8, QUTIL_CONTRACT_ASSET_NAME);
8990

90-
DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(8, QUTIL_CONTRACT_ASSET_NAME);
91+
struct StateData
92+
{
93+
// other state variables ...
9194

92-
// ...
95+
ProposalVotingT proposals;
96+
97+
// ...
98+
};
9399
};
94100
```
95101
96-
`DEFINE_SHAREHOLDER_PROPOSAL_STORAGE` defines a state object `state.proposals` and the types `ProposalDataT`, `ProposersAndVotersT`, and `ProposalVotingT`.
102+
`DEFINE_SHAREHOLDER_PROPOSAL_TYPES` defines the types `ProposalDataT`, `ProposersAndVotersT`, and `ProposalVotingT`.
103+
The `proposals` field of type `ProposalVotingT` must be declared manually inside `StateData`.
97104
Make sure to have no name clashes with these.
98105
Using other names isn't possible if you want to benefit from the QPI macros for simplifying the implementation.
99106
@@ -123,19 +130,19 @@ struct QUTIL
123130
switch (input.proposal.data.variableOptions.variable)
124131
{
125132
case 0:
126-
state.smt1InvocationFee = input.acceptedValue;
133+
state.mut().smt1InvocationFee = input.acceptedValue;
127134
break;
128135
case 1:
129-
state.pollCreationFee = input.acceptedValue;
136+
state.mut().pollCreationFee = input.acceptedValue;
130137
break;
131138
case 2:
132-
state.pollVoteFee = input.acceptedValue;
139+
state.mut().pollVoteFee = input.acceptedValue;
133140
break;
134141
case 3:
135-
state.distributeQuToShareholderFeePerShareholder = input.acceptedValue;
142+
state.mut().distributeQuToShareholderFeePerShareholder = input.acceptedValue;
136143
break;
137144
case 4:
138-
state.shareholderProposalFee = input.acceptedValue;
145+
state.mut().shareholderProposalFee = input.acceptedValue;
139146
break;
140147
}
141148
}
@@ -175,7 +182,7 @@ struct QUTIL
175182
{
176183
// ...
177184
178-
IMPLEMENT_DEFAULT_SHAREHOLDER_PROPOSAL_VOTING(5, state.shareholderProposalFee)
185+
IMPLEMENT_DEFAULT_SHAREHOLDER_PROPOSAL_VOTING(5, state.get().shareholderProposalFee)
179186
180187
// ...
181188
}
@@ -231,7 +238,7 @@ The following elements are required to support shareholder proposals and voting:
231238
- `GetShareholderVotes`: Function for getting the votes of a shareholder. Usually shouldn't require a custom implementation.
232239
- `GetShareholderVotingResults`: Function for getting the vote results summary. Usually doesn't require a custom implementation.
233240
- `SET_SHAREHOLDER_PROPOSAL` and `SET_SHAREHOLDER_VOTES`: These are notification procedures required to handle voting of other contracts that are shareholder of your contract. They usually just invoke `SetShareholderProposal` or `SetShareholderVote`, respectively.
234-
- Proposal data storage and types: The default implementations expect the object `state.proposals` and the types `ProposalDataT`, `ProposersAndVotersT`, and `ProposalVotingT`, which can be defined via `DEFINE_SHAREHOLDER_PROPOSAL_STORAGE` in some cases.
241+
- Proposal types and storage: The default implementations expect the types `ProposalDataT`, `ProposersAndVotersT`, and `ProposalVotingT` (defined via `DEFINE_SHAREHOLDER_PROPOSAL_TYPES`) and the object `proposals` of type `ProposalVotingT` in `StateData` (accessed via `state.get().proposals` or `state.mut().proposals`).
235242
236243
QPI provides default implementations through several macros, as used in the [Introduction to Shareholder Proposals](#introduction-to-shareholder-proposals).
237244
The following tables gives an overview about when the macros can be used.
@@ -244,7 +251,7 @@ Finally, multi-variable proposals change more than one variable if accepted. The
244251
245252
Default implementation can be used? | 1-var yes/no | 1-var N option | 1-var scalar | multi-var
246253
------------------------------------------------|--------------|----------------|--------------|-----------
247-
`DEFINE_SHAREHOLDER_PROPOSAL_STORAGE` | X | | | X
254+
`DEFINE_SHAREHOLDER_PROPOSAL_TYPES` | X | | | X
248255
`IMPLEMENT_FinalizeShareholderStateVarProposals`| X | | |
249256
`IMPLEMENT_SetShareholderProposal` | X | | |
250257
`IMPLEMENT_GetShareholderProposal` | X | X | X |
@@ -265,15 +272,23 @@ You may also have a look into the example contracts given in the table.
265272
266273
#### Proposal types and storage
267274
268-
The default implementation of `DEFINE_SHAREHOLDER_PROPOSAL_STORAGE(assetNameInt64, numProposalSlots)` is defined as follows:
275+
The default implementation of `DEFINE_SHAREHOLDER_PROPOSAL_TYPES(numProposalSlots, assetNameInt64)` is defined as follows:
269276
270277
```C++
271278
public:
272279
typedef ProposalDataYesNo ProposalDataT;
273280
typedef ProposalAndVotingByShareholders<numProposalSlots, assetNameInt64> ProposersAndVotersT;
274281
typedef ProposalVoting<ProposersAndVotersT, ProposalDataT> ProposalVotingT;
275-
protected:
282+
```
283+
284+
The `proposals` field must be declared manually inside `StateData`:
285+
286+
```C++
287+
struct StateData
288+
{
276289
ProposalVotingT proposals;
290+
// ...
291+
};
277292
```
278293
279294
With `ProposalDataT` your have the following options:
@@ -285,7 +300,7 @@ With `ProposalDataT` your have the following options:
285300
The number of proposal slots linearly scales the storage and digest compute requirements. So we recommend to use a quite low number here, similar to the number of variables that can be set in your state.
286301
287302
`ProposalVotingT` combines `ProposersAndVotersT` and `ProposalDataT` into the class used for storing all proposal and voting data.
288-
It is instantiated as `state.proposals`.
303+
It is instantiated as `proposals` inside `StateData` (accessed via `state.get().proposals` for reads and `state.mut().proposals` for writes).
289304
290305
In order to support MultiVariables proposals that allow to change multiple variables in a single proposal, the variable values need to be stored separately, for example in an array of `numProposalSlots` structs, one for each potential proposal.
291306
See the contract TestExampleA to see how to support multi-variable proposals.
@@ -330,9 +345,9 @@ PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeShareholderStateVarProposals)
330345
// Analyze proposal results and set variables:
331346
// Iterate all proposals that were open for voting in this epoch ...
332347
locals.p.proposalIndex = -1;
333-
while ((locals.p.proposalIndex = qpi(state.proposals).nextProposalIndex(locals.p.proposalIndex, qpi.epoch())) >= 0)
348+
while ((locals.p.proposalIndex = qpi(state.get().proposals).nextProposalIndex(locals.p.proposalIndex, qpi.epoch())) >= 0)
334349
{
335-
if (!qpi(state.proposals).getProposal(locals.p.proposalIndex, locals.p.proposal))
350+
if (!qpi(state.get().proposals).getProposal(locals.p.proposalIndex, locals.p.proposal))
336351
continue;
337352

338353
locals.proposalClass = ProposalTypes::cls(locals.p.proposal.type);
@@ -341,7 +356,7 @@ PRIVATE_PROCEDURE_WITH_LOCALS(FinalizeShareholderStateVarProposals)
341356
if (locals.proposalClass == ProposalTypes::Class::Variable || locals.proposalClass == ProposalTypes::Class::MultiVariables)
342357
{
343358
// Get voting results and check if conditions for proposal acceptance are met
344-
if (!qpi(state.proposals).getVotingSummary(locals.p.proposalIndex, locals.p.results))
359+
if (!qpi(state.get().proposals).getVotingSummary(locals.p.proposalIndex, locals.p.results))
345360
continue;
346361

347362
locals.p.acceptedOption = locals.p.results.getAcceptedOption();
@@ -386,7 +401,7 @@ PUBLIC_PROCEDURE(SetShareholderProposal)
386401
}
387402
388403
// try to set proposal (checks invocator's rights and general validity of input proposal), returns proposal index
389-
output = qpi(state.proposals).setProposal(qpi.invocator(), input);
404+
output = qpi(state.mut().proposals).setProposal(qpi.invocator(), input);
390405
if (output == INVALID_PROPOSAL_INDEX)
391406
{
392407
// error -> reimburse invocation reward
@@ -423,8 +438,8 @@ struct GetShareholderProposal_output
423438
PUBLIC_FUNCTION(GetShareholderProposal)
424439
{
425440
// On error, output.proposal.type is set to 0
426-
output.proposerPubicKey = qpi(state.proposals).proposerId(input.proposalIndex);
427-
qpi(state.proposals).getProposal(input.proposalIndex, output.proposal);
441+
output.proposerPubicKey = qpi(state.get().proposals).proposerId(input.proposalIndex);
442+
qpi(state.get().proposals).getProposal(input.proposalIndex, output.proposal);
428443
}
429444
```
430445
@@ -452,7 +467,7 @@ PUBLIC_FUNCTION(GetShareholderProposalIndices)
452467
{
453468
// Return proposals that are open for voting in current epoch
454469
// (output is initialized with zeros by contract system)
455-
while ((input.prevProposalIndex = qpi(state.proposals).nextProposalIndex(input.prevProposalIndex, qpi.epoch())) >= 0)
470+
while ((input.prevProposalIndex = qpi(state.get().proposals).nextProposalIndex(input.prevProposalIndex, qpi.epoch())) >= 0)
456471
{
457472
output.indices.set(output.numOfIndices, input.prevProposalIndex);
458473
++output.numOfIndices;
@@ -465,7 +480,7 @@ PUBLIC_FUNCTION(GetShareholderProposalIndices)
465480
{
466481
// Return proposals of previous epochs not overwritten yet
467482
// (output is initialized with zeros by contract system)
468-
while ((input.prevProposalIndex = qpi(state.proposals).nextFinishedProposalIndex(input.prevProposalIndex)) >= 0)
483+
while ((input.prevProposalIndex = qpi(state.get().proposals).nextFinishedProposalIndex(input.prevProposalIndex)) >= 0)
469484
{
470485
output.indices.set(output.numOfIndices, input.prevProposalIndex);
471486
++output.numOfIndices;
@@ -511,7 +526,7 @@ typedef bit SetShareholderVotes_output;
511526
512527
PUBLIC_PROCEDURE(SetShareholderVotes)
513528
{
514-
output = qpi(state.proposals).vote(qpi.invocator(), input);
529+
output = qpi(state.mut().proposals).vote(qpi.invocator(), input);
515530
}
516531
```
517532

@@ -530,7 +545,7 @@ typedef ProposalMultiVoteDataV1 GetShareholderVotes_output;
530545
PUBLIC_FUNCTION(GetShareholderVotes)
531546
{
532547
// On error, output.votes.proposalType is set to 0
533-
qpi(state.proposals).getVotes(input.proposalIndex, input.voter, output);
548+
qpi(state.get().proposals).getVotes(input.proposalIndex, input.voter, output);
534549
}
535550
```
536551
@@ -548,7 +563,7 @@ typedef ProposalSummarizedVotingDataV1 GetShareholderVotingResults_output;
548563
PUBLIC_FUNCTION(GetShareholderVotingResults)
549564
{
550565
// On error, output.totalVotesAuthorized is set to 0
551-
qpi(state.proposals).getVotingSummary(
566+
qpi(state.get().proposals).getVotingSummary(
552567
input.proposalIndex, output);
553568
}
554569
```

0 commit comments

Comments
 (0)