Skip to content

Automatic state change detection#774

Draft
fnordspace wants to merge 3 commits intoqubic:developfrom
fnordspace:features/2026-01-30-StateChangeDetection
Draft

Automatic state change detection#774
fnordspace wants to merge 3 commits intoqubic:developfrom
fnordspace:features/2026-01-30-StateChangeDetection

Conversation

@fnordspace
Copy link
Contributor

@fnordspace fnordspace commented Feb 20, 2026

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 when state.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, returns T&). Same size and layout as T.
  • Removed unconditional dirty-marking from 5 procedure call sites in contract_exec.h
  • All procedure/function macros now pass ContractState<StateData, INDEX> instead of the raw contract struct
  • Deleted overloads for setMemory/copyMemory/copyToBuffer/copyFromBuffer taking ContractState& to prevent bypassing dirty tracking
  • Added needsCleanup() const method to HashMap, HashSet, Collection to allow checking without mutating state
  • using QpiContextFunctionCall::operator() fix for C++ name hiding of const qpi() overload

All 26 contract headers

  • Persistent state fields moved into nested struct StateData
  • state.field reads → state.get().field, writes → state.mut().field
  • Proposal voting: qpi(state.proposals)qpi(state.get().proposals) / qpi(state.mut().proposals)
  • sizeof(CONTRACT)sizeof(CONTRACT::StateData) in contract_def.h

Documentation & templates

  • Updated contracts.md, contracts_proposals.md, README.md, and EmptyTemplate.h

Only 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.

- 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
@fnordspace fnordspace self-assigned this Feb 20, 2026
@fnordspace fnordspace added the enhancement New feature or request label Feb 20, 2026
@fnordspace fnordspace added this to qubic Feb 20, 2026
@fnordspace fnordspace moved this to 👀 In review in qubic Feb 21, 2026
Copy link
Contributor

@philippwerner philippwerner left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: 👀 In review

Development

Successfully merging this pull request may close these issues.

2 participants