TL;DR;
Events are way cheaper than contract state. Bytes32 is probably the best choice, with or without structs. Event-only is possible for a narrow band of use-cases.
Store it as a string
This is wasteful because strings are variable length and therefore require two words, one for the length and one for the payload. Since IPFS hashes are always 32 bytes, this overhead both in the interface and the storage is unnecessary.
Store it as a struct
A struct
is a container, within which you can store other defined types, in effect creating a compound type. As worded, this option is invalid.
Store it in event log
Event storage is much cheaper than state storage, with the caveat that values stored in an event are inaccessible to contracts. That may be acceptable in this case.
Not mentioned
bytes32
is a fixed sized type that maps nicely to IPFS (without the leading Qm
) as well as to Ethereum's natural word size and smallest addressable storage slot. This makes it seem rather ideal for IPFS hashes, whether stored in contract states or in event logs.
Realistically, you will probably end up creating a struct
with metadata about the docs.
struct Doc {
bytes32 IPFShash;
// other stuff
}
Even the most rudimentary logic in the contract (is this a known document?) will require some state storage. Organization usually calls for a little more. So generally, while every effort should be made to keep it to a minimum, some contract storage is likely required.
And, if following best practice, also emit an event for important state changes. An event log and emitters would look something like:
event LogNewDoc(address sender, bytes hash, ...);
and later, when something happens:
emit LogNewDoc(msg.sender, ipfsHash, ...);
There is a stateless pattern that might work in the case that the contract will never inspect hashes and you want to client-side to fetch relevant events quickly.
You maintain a uint
in the contract that points to the most recent block that contains an interesting event. Within the event, another pointer the event before that. That means a client doesn't need to explore the whole chain to retreive events and events arrive most-recent first. The contract state requires overwriting a uint
which is currently 5,000 because it wouldn't be 0
for very long.
Hope it helps.
Elaboration
Super-simple example. A client inspects latestEvent()
and then listens to that block only to fetch it then closes the listener. The breadcrumb in the log suggests where to look for an earlier event.
pragma solidity 0.5.1;
contract EventChain {
// one of these for each chain you want conveniently accessible from client software
uint public latestEvent;
event LogChainedEvent(address sender, uint previousEvent);
function logData() public {
emit LogChainedEvent(msg.sender, latestEvent);
latestEvent++;
}
}
With that approach, it's conceivable to organize event chains in a way that makes data access convenient. For example, you could track a version history (IPFS hash changes every time) based on some sort of identifier for the subject matter.
Consider:
mapping(bytes32 => uint) public latestVersion; // topic => events (ipfsHash)