Conversation
- 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
philippwerner
left a comment
There was a problem hiding this comment.
I think this is a good solution. But please check in the disassembly what the optimizer does with many consecutive mut() calls... if it is smart enough to run markContractStateDirty() only once. If it isn't, we may need to revise markContractStateDirty().
An idea for discussion: I think we may get rid of the .get() for all read-only access if we pass the const reference to the StateData to all functions directly and a const reference to the following ProcedureStateAccess to all procedures:
template <typename T, unsigned int contractIndex>
struct ProcedureStateAccess : public T
{
static constexpr unsigned int __contract_index = contractIndex;
T& mut() { ::markContractStateDirty(contractIndex); return const_cast<T&>(*this); }
};This would mean that read access shouldn't require .get() in both, the functions and procedures but that the write access requires .mut() to cast away the const. This may be a bit more convenient as an interface to the state. However, I am not sure this breaks the IntelliSense code completion. If it does, we shouldn't do it.
| }; | ||
| } | ||
|
|
||
| // Letters for defining identity with ID function |
There was a problem hiding this comment.
This comment shouldn't be removed. Please add it again below the definition of ContractState (with an empty line between ContractState and the new "Section" about the letters for the ID function.
|
|
||
|
|
||
| // Forward declaration — implementation in contract_exec.h | ||
| static void markContractStateDirty(unsigned int contractIndex); |
There was a problem hiding this comment.
Currently, I think a contract may call markContractStateDirty() for another contract as a kind of attack. I suggest to prefix the function name with __ as the other functions that are not allowed to be used directly by the contract devs. For these, the contract verification tool already prevents that contracts call them directly in their code, correct @Franziska-Mueller ?
| NO_IO_SYSTEM_PROC_WITH_LOCALS(CapLetterName, FuncName, InputType, OutputType) | ||
|
|
||
| // Internal macro for defining the system procedure macros | ||
| #define NO_IO_SYSTEM_PROC_WITH_LOCALS(CapLetterName, FuncName, InputType, OutputType) \ |
There was a problem hiding this comment.
Side node for the refactoring ToDo list: NO_IO_SYSTEM_PROC and NO_IO_SYSTEM_PROC_WITH_LOCALS are legacy names. It would be good to rename these to __SYSTEM_PROC and __SYSTEM_PROC_WITH_LOCALS.
| public: \ | ||
| enum { __expandEmpty = 0 }; \ | ||
| static void __expand(const QPI::QpiContextProcedureCall& qpi, CONTRACT_STATE_TYPE& state, CONTRACT_STATE2_TYPE& state2) { ::__FunctionOrProcedureBeginEndGuard<(CONTRACT_INDEX << 22) | __LINE__> __prologueEpilogueCaller; | ||
| static void __expand(const QPI::QpiContextProcedureCall& qpi, QPI::ContractState<CONTRACT_STATE_TYPE::StateData, CONTRACT_INDEX>& state, CONTRACT_STATE2_TYPE& state2) { ::__FunctionOrProcedureBeginEndGuard<(CONTRACT_INDEX << 22) | __LINE__> __prologueEpilogueCaller; |
There was a problem hiding this comment.
This isn't used and supported yet, but please also add the QPI::ContractState wrapper to state2 here.
| typedef ProposalVoting<ProposersAndVotersT, ProposalDataT> ProposalVotingT; \ | ||
| protected: \ | ||
| ProposalVotingT proposals | ||
| typedef ProposalVoting<ProposersAndVotersT, ProposalDataT> ProposalVotingT |
There was a problem hiding this comment.
Here you have removed the definition of ProposalVotingT proposals from the macro. This is an option, but the documentation in contracts_proposals.md still says that this macro defined proposals. I suggest to rename this to DEFINE_SHAREHOLDER_PROPOSAL_TYPES and update the documentation, clarifying that ProposalVotingT proposals; needs to be added in the state struct.
Introduces a
ContractState<T, contractIndex>wrapper that tracks contract state modifications at the point of write. Previously, the core unconditionally marked every contract's state as dirty after any procedure call, forcing digest recomputation for all contracts every tick. Now, state is only marked dirty whenstate.mut()is called, allowing unchanged contracts to skip the expensive rehashing.No contract logic is changed — this is a purely mechanical refactoring of state access patterns.
Changes
Core infrastructure
ContractState<T, contractIndex>template:state.get()for reads (const T&),state.mut()for writes (marks dirty, returnsT&). Same size and layout asT.contract_exec.hContractState<StateData, INDEX>instead of the raw contract structsetMemory/copyMemory/copyToBuffer/copyFromBuffertakingContractState&to prevent bypassing dirty trackingneedsCleanup()const method to HashMap, HashSet, Collection to allow checking without mutating stateusing QpiContextFunctionCall::operator()fix for C++ name hiding of constqpi()overloadAll 26 contract headers
struct StateDatastate.fieldreads →state.get().field, writes →state.mut().fieldqpi(state.proposals)→qpi(state.get().proposals)/qpi(state.mut().proposals)sizeof(CONTRACT)→sizeof(CONTRACT::StateData)incontract_def.hDocumentation & templates
contracts.md,contracts_proposals.md,README.md, andEmptyTemplate.hOnly a draft for the moment to get an early review due to many touched files. The contract verification might workflow might need adjustment due to new syntax in contracts.