Simple and appropriate data organization can challenge Solidity newcomers. It wants us to organize everything in ways many of us aren’t accustomed to.
Are there well-solved general patterns for routine on-chain data organization?
Simple and appropriate data organization can challenge Solidity newcomers. It wants us to organize everything in ways many of us aren’t accustomed to.
Are there well-solved general patterns for routine on-chain data organization?
Here are some simple and useful patterns in increasing order of utility.
Event logs are omitted for brevity. In practice, it's desirable to emit events for every important state change.
Simple List Using Array
Strengths
Weaknesses
Example:
pragma solidity ^0.4.6;
contract simpleList {
struct EntityStruct {
address entityAddress;
uint entityData;
// more fields
}
EntityStruct[] public entityStructs;
function newEntity(address entityAddress, uint entityData) public returns(uint rowNumber) {
EntityStruct memory newEntity;
newEntity.entityAddress = entityAddress;
newEntity.entityData = entityData;
return entityStructs.push(newEntity)-1;
}
function getEntityCount() public constant returns(uint entityCount) {
return entityStructs.length;
}
}
Mapping with Struct
Strengths
Weaknesses
Example:
contract mappingWithStruct {
struct EntityStruct {
uint entityData;
bool isEntity;
}
mapping (address => EntityStruct) public entityStructs;
function isEntity(address entityAddress) public constant returns(bool isIndeed) {
return entityStructs[entityAddress].isEntity;
}
function newEntity(address entityAddress, uint entityData) public returns(bool success) {
if(isEntity(entityAddress)) revert();
entityStructs[entityAddress].entityData = entityData;
entityStructs[entityAddress].isEntity = true;
return true;
}
function deleteEntity(address entityAddress) public returns(bool success) {
if(!isEntity(entityAddress)) revert();
entityStructs[entityAddress].isEntity = false;
return true;
}
function updateEntity(address entityAddress, uint entityData) public returns(bool success) {
if(!isEntity(entityAddress)) revert();
entityStructs[entityAddress].entityData = entityData;
return true;
}
}
Array of Structs with Unique Ids
Strengths
Weaknesses
Example:
contract arrayWithUniqueIds {
struct EntityStruct {
address entityAddress;
uint entityData;
}
EntityStruct[] public entityStructs;
mapping(address => bool) knownEntity;
function isEntity(address entityAddress) public constant returns(bool isIndeed) {
return knownEntity[entityAddress];
}
function getEntityCount() public constant returns(uint entityCount) {
return entityStructs.length;
}
function newEntity(address entityAddress, uint entityData) public returns(uint rowNumber) {
if(isEntity(entityAddress)) revert();
EntityStruct memory newEntity;
newEntity.entityAddress = entityAddress;
newEntity.entityData = entityData;
knownEntity[entityAddress] = true;
return entityStructs.push(newEntity) - 1;
}
function updateEntity(uint rowNumber, address entityAddress, uint entityData) public returns(bool success) {
if(!isEntity(entityAddress)) revert();
if(entityStructs[rowNumber].entityAddress != entityAddress) revert();
entityStructs[rowNumber].entityData = entityData;
return true;
}
}
Mapped Structs with Index
Strengths
Weaknesses
Example:
contract MappedStructsWithIndex {
struct EntityStruct {
uint entityData;
bool isEntity;
}
mapping(address => EntityStruct) public entityStructs;
address[] public entityList;
function isEntity(address entityAddress) public constant returns(bool isIndeed) {
return entityStructs[entityAddress].isEntity;
}
function getEntityCount() public constant returns(uint entityCount) {
return entityList.length;
}
function newEntity(address entityAddress, uint entityData) public returns(uint rowNumber) {
if(isEntity(entityAddress)) revert();
entityStructs[entityAddress].entityData = entityData;
entityStructs[entityAddress].isEntity = true;
return entityList.push(entityAddress) - 1;
}
function updateEntity(address entityAddress, uint entityData) public returns(bool success) {
if(!isEntity(entityAddress)) revert();
entityStructs[entityAddress].entityData = entityData;
return true;
}
}
Mapped Structs with Delete-enabled Index
Strengths
Weaknesses
UPDATE, 2019
This pattern is available as a library for Solidity 0.5.1: https://medium.com/@robhitchens/solidity-crud-epilogue-e563e794fde, https://github.com/rob-Hitchens/UnorderedKeySet
Example:
contract mappedWithUnorderedIndexAndDelete {
struct EntityStruct {
uint entityData;
uint listPointer;
}
mapping(address => EntityStruct) public entityStructs;
address[] public entityList;
function isEntity(address entityAddress) public constant returns(bool isIndeed) {
if(entityList.length == 0) return false;
return (entityList[entityStructs[entityAddress].listPointer] == entityAddress);
}
function getEntityCount() public constant returns(uint entityCount) {
return entityList.length;
}
function newEntity(address entityAddress, uint entityData) public returns(bool success) {
if(isEntity(entityAddress)) revert();
entityStructs[entityAddress].entityData = entityData;
entityStructs[entityAddress].listPointer = entityList.push(entityAddress) - 1;
return true;
}
function updateEntity(address entityAddress, uint entityData) public returns(bool success) {
if(!isEntity(entityAddress)) revert();
entityStructs[entityAddress].entityData = entityData;
return true;
}
function deleteEntity(address entityAddress) public returns(bool success) {
if(!isEntity(entityAddress)) revert();
uint rowToDelete = entityStructs[entityAddress].listPointer;
address keyToMove = entityList[entityList.length-1];
entityList[rowToDelete] = keyToMove;
entityStructs[keyToMove].listPointer = rowToDelete;
entityList.length--;
return true;
}
}
This last one has an explainer here: https://medium.com/@robhitchens/solidity-crud-part-2-ed8d8b4f74ec#.ekc22r5lf
Folder Tree Example: How can we organize storage of a folder or object tree in Solidity?
Linked List example shows a way to maintain an ordered list using a library. https://github.com/ethereum/dapp-bin/blob/master/library/linkedList.sol 0
Adding to Rob's answer, please use revert() as an alternative of throw.From version 0.4.13 the throw keyword is deprecated and will be phased out in the future. Read here for more information: require, assert and revert in solidity.
So, as an example you should change
if(isEntity(entityAddress)) throw;
to
if(isEntity(entityAddress)) revert();
in above code provided by Rob.
revert()
: ethfiddle.com/PgDM-drAc9
Commented
Nov 14, 2017 at 6:39
require()
but that would mean reversing all the rules ... if(bad) revert()
=> require(!bad)
Commented
Nov 14, 2017 at 14:48
throw
and revert()
? Do they do the same thing, by reverting back all the states? @Abhishek Sinha && @Rob Hitchens
solc 0.4.13
, revert
, require
and assert
deprecate throw
. They differ slightly from the original throw
in the details such as gas destruction. Have a look over here: ethereum.stackexchange.com/questions/15166/…
Commented
Dec 9, 2017 at 16:03
Great answer by Rob Hitchens. Would like to point out to a minor breaking change in the code mentioned.
In the newEntity
function of the simpleList
contract, the author has used return entityStructs.push(newEntity)-1;
Ever since Solidity version 0.6.0, the array.push() function does not return anything. Please refer to the answer here: https://ethereum.stackexchange.com/a/87791/73743.
So, the return entityStructs.push(newEntity)-1;
part of the code breaks the smart contract.
array--
has been replaced by .pop
, for example.
Commented
Jun 15, 2021 at 16:52
Thanks Rob for inspiration!
Here is extended version of Mapped Structs with Delete-enabled Index that also support nested mappings, "unstructured" data and ownership
Hope it will help someone