Cheapest way to access and modify a struct - solidity

I am using solidity 0.8.10.
In my contract I have a state variable struct:
struct Product {
uint id_prod;
address payable producer_addr;
address payable owner_addr;
bool onSale;
}
and a state variable array of products:
Product[] public ProductList;
and a function that allows to modify the attributes of the product. Nothing really complex.
Considering the cost in deploying and using the contract, I think there are two ways of changing the attributes of the product.
Solution 1, by using a storage variable:
Product storage _product = ProductList[_id_product];
_product.owner_addr = payable(msg.sender);
_product.onSale = false;
Solution 2, without a storage variable:
ProductList[_id_product].owner_addr = payable(msg.sender);
ProductList[_id_product].onSale = false;
Which solution is the cheapest, cleanest, most advisable?

First, the numbers:
I deployed a similar contract to yours and called each function. Although implementations can vary,
The transaction costs were:
Solution 1: 28785 gas
Solution 2: 28985 gas
And, deployment costs were:
Solution 1: 275180
Solution 2: 281006
Solution 2 uses 2 sload and 2 sstore operations. It loads from storage each time you want to access the data, and writes to storage each time you assign new value.
Solution 1 uses a storage pointer. So we sload only once and than use the reference.
You can see from the transaction cost differences, that the difference is 200 gas and a single sload operation uses 200 gas as well.
So, both in terms of readability(imho), transaction cost and deployment cost; solution 1 was more efficient. And the more fields you use, the more efficient the solution 1 will become.

Related

Why does this solidity function run into gas errors?

I'm trying to figure out some strange behavior. The function below takes in an array like [1,2,3,4,5], loops through it, and looks at another contract to verify ownership. I wrote it like this (taking in a controlled / limited array) to limit the amount of looping required (to avoid gas issues). The weird part (well, to me) is that I can run this a few times and it works great, mapping the unmapped values. It will always process as expected until I run about 50 items through it. After that, the next time it will gas out even if the array includes only one value. So, I'm wondering what's going on here...
function claimFreeNFTs (uint[] memory _IDlist) external payable noReentrant {
IERC721 OGcontract = IERC721(ERC721_contract);
uint numClaims = 0;
for (uint i = 0; i < _IDlist.length; i++) {
uint thisID = _IDlist[i];
require(OGcontract.ownerOf(thisID)==msg.sender, 'Must own token.' );
if ( !claimedIDList(thisID) ) { // checks mapping here...
claimIDset(thisID); // maps unmapped values here;
numClaims++;
}
}
if ( numClaims > 0 ) {
_safeMint(msg.sender, numClaims);
emit Mint(msg.sender, totalSupply());
}
}
Any thoughts / directions appreciated. :-)
Well, there was a bit more to the function, actually. I'd edited out some of what I thought was extraneous, but it turned out my error was in the extra stuff. The above does actually work. (Sorry.) After doing the mint, I was also reducing the supply of a reserve wallet on the contract -- one that held (suprise!) 50 NFTs. So, after this function processed 50, it was making that wallet hold negative NFTs, which screwed things up. Long story, but on Remix, I'd forgotten to set values in the constructor in the proper order, which is how I screwed it up in the first place. Anyway, solved.

Solidity -- using uninitialized storage pointers safely

I'm trying to gas-optimize the following solidity function by allowing users to sort the array they pass in such a way that I can perform less storage reads. However, to do this I need to use an uninitialized storage pointer -- which the compiler doesnt let me do (^0.8.0). How can I safely use an uninitialized storage pointer and have it be accepted by the compiler?
function safeBatchReleaseCollaterals(
uint256[] memory bondIds,
uint256[] memory collateralIds,
address to
) public {
// 'memoization' variables
uint256 lastAuthorizedBond = 2**256 - 1;
uint256 lastCurrencyRef = 2**256 - 1;
Currency storage currency;
for (uint256 i = 0; i < bondIds.length; i++) {
uint256 bondId = bondIds[i];
// check if we authorized this bond previously?
if (lastAuthorizedBond != bondId) {
require( // expensive check. Reads 2 slots!!
_isAuthorizedToReleaseCollateral(bondId, msg.sender),
"CollateralManager: unauthorized to release collateral"
);
lastAuthorizedBond = bondId;
}
uint256 collateralId = collateralIds[i];
Collateral storage c = collateral[bondId][collateralId];
// check if we read this Currency previously?
if (lastCurrencyRef != c.currencyRef) {
currency = currencies[c.currencyRef]; // expensive 1 slot read
lastCurrencyRef = c.currencyRef;
}
_transferGenericCurrency(currency, address(this), to, c.amountOrId, "");
emit CollateralReleased(bondId, collateralId, to);
}
}
As a quick explanation of the structure: this is similar to a batch erc1155 transfer, except I'm storing a lot of data related to the transaction in some slots under the Currency and Collateral and Bond objects. Since the reads can get intensive, I want to optimize gas by caching reads. Since having an actual cache map is also expensive, I instead optimize by caching only the previous list item, and rely on the user to sort the array in such a way that results in the smallest gas costs.
The lastAuthorizedBond variable caches which bondId was last authorized -- if it repeats, we can cut out an expensive 2-slot read! (which results in maybe 16% gas savings during tests. You can see, significant). I tried doing something similar with the currency read, storing the lastCurrencyRef and hoping to store the actual result of the read into the currency variable. The compiler complains about this however, and maybe justly so.
Is there a way to pass this by the compiler? Do I just have to ditch this optimization? Though nobody is allowed to register the 2**256-1 currency or bond, is this code even safe?
Note that the collateral entry gets deleted after this runs -- cannot double release.

"Out of Gas" when calling function twice quickly, but not when calls are spaced out?

I have a smart contract, and one of the functions (queue) is meant to allow users to find "matches" with other users of the smart contract. The logic is that if you call queue and there is nobody waiting, you are now the queued user / wallet address. If you call queue and there is already a queued user, you clear them from the queue and set up the match.
This works fine if the first queue call is a few seconds before the second one, but if both users call queue at the same time, the second one always reverts with an Out of Gas error. Increasing the amount of gas does not solve the issue.
I would appreciate any ideas!
The code fails in the if block. If I remove most of the logic, it succeeds, but I can't figure out any rhyme or reason as to why.
if (awaitingMatch != address(0)) {
userMap[awaitingMatch].opponent = msg.sender;
userMap[awaitingMatch].matchedBlock = block.number;
userMap[awaitingMatch].matchWins = 0;
userMap[awaitingMatch].playAmount = msg.value;
userMap[awaitingMatch].winsNeeded = winsToWin;
userMap[msg.sender].opponent = awaitingMatch;
userMap[msg.sender].matchedBlock = block.number;
userMap[msg.sender].matchWins = 0;
userMap[msg.sender].winsNeeded = winsToWin;
awaitingMatch = address(0);
emit Match(msg.sender);
emit Match(userMap[msg.sender].opponent);
// add this guy to the list awaiting a match, and set his desposit flag true
} else {
awaitingMatch = msg.sender;
}
I think I have figured this out. The issue is that MetaMask tries to estimate the amount of gas that will be used for each transaction. MetaMask is quite good at this, and analyzes the state of the contract before estimating the gas. The if section (run by the second caller) does a lot more work than the else section (run by the first caller). If I make both calls at the same time, they both estimate that they'll run the lighter else section, but one of them will wind up running the first, more expensive if section.
I think my best bet here is to tweak the amount of gas being supplied on any call to a function like this that could do quite different amounts of work depending on the moment the function is called.

StackExchange.Redis: Does a transaction hit the server multiple times?

When I execute a transaction (MULTI/EXEC) via SE.Redis, does it hit the server multiple times? For example,
ITransaction tran = Database.CreateTransaction();
tran.AddCondition(Condition.HashExists(cacheKey, oldKey));
HashEntry hashEntry = GetHashEntry(newKeyValuePair);
Task fieldDeleteTask = tran.HashDeleteAsync(cacheKey, oldKey);
Task hashSetTask = tran.HashSetAsync(cacheKey, new[] { hashEntry });
if (await tran.ExecuteAsync())
{
await fieldDeleteTask;
await hashSetTask;
}
Here I am executing two tasks in the transaction. Does this mean I hit the server 4 times? 1 for MULTI, 1 for delete, 1 for set, 1 for exec? Or is SE.Redis smart enough to buffer the tasks in local memory and send everything in one shot when we call ExecuteAsync?
It has to send multiple commands, but it doesn't pay latency costs per command; specifically, when you call Execute[Async] (and not before) it issues a pipeline (all together, not waiting for replies) of:
WATCH cacheKey // observes any competing changes to cacheKey
HEXIST cacheKey oldKey // see if the existing field exists
MULTI // starts the transacted commands
HDEL cacheKey oldKey // delete the existing field
HSET cachKey newField newValue // assign the new field
then it pays latency costs to get the result from the HEXIST, because only when that is known can it decide whether to proceed with the transaction (issuing EXEC and checking the result - which can be negative if the WATCH detects a conflict), or whether to throw everything away (DISCARD).
So; either way 6 commands are going to be issued, but in terms of latency: you're paying for 2 round trips due to the need for a decision point before the final EXEC/DISCARD. In many cases, though, this can itself be further masked by the reality that the result of HEXIST could already be on the way back to you before we've even got as far as checking, especially if you have any non-trivial bandwidth, for example a large newValue.
However! As a general rule: anything you can do with redis MULTI/EXEC: can be done faster, more reliably, and with fewer bugs, by using a Lua script instead. It looks like what we're actually trying to do here is:
for the hash cacheKey, if (and only if) the field oldField exists: remove oldField and set newField to newValue
We can do this very simply in Lua, because Lua scripts are executed at the server from start to finish without interruption from competing connections. This means that we don't need to worry about things like atomicity i.e. other connections changing data that we're making decisions with. So:
var success = (bool)await db.ScriptEvaluateAsync(#"
if redis.call('hdel', KEYS[1], ARGV[1]) == 1 then
redis.call('hset', KEYS[1], ARGV[2], ARGV[3])
return true
else
return false
end
", new RedisKey[] { cacheKey }, new RedisValue[] { oldField, newField, newValue });
The verbatim string literal here is our Lua script, noting that we don't need to do a separate HEXISTS/HDEL any more - we can make our decision based on the result of the HDEL. Behind the scenes, the library performs SCRIPT LOAD operations as needed, so: if you are doing this lots of times, it doesn't need to send the script itself over the network more than once.
From the perspective of the client: you are now only paying a single latency fee, and we're not sending the same things repeatedly (the original code sent cacheKey four times, and oldKey twice).
(a note on the choice of KEYS vs ARGV: the distinction between keys and values is important for routing purposes, in particular on sharded environments such as redis-cluster; sharding is done based on the key, and the only key here is cacheKey; the field identifiers in hashes do not impact sharding, so for the purpose of routing they are values, not keys - and as such, you should convey them via ARGV, not KEYS; this won't impact you on redis-server, but on redis-cluster this difference is very important, as if you get it wrong: the server will most-likely reject your script, thinking that you are attempting a cross-slot operation; multi-key commands on redis-cluster are only supported when all the keys are on the same slot, usually achieved via "hash tags")

how common is it to use both a guid and an int as unique identifiers for a table?

I think a Guid is generally the preferred unique table row identifier from a dba perspective. But I'm working on a project where the developers and managers appear to want a way to reference things by an int value. I can understand their perspective b/c they want a simple and easy way to reference different entities.
I was thinking about using a pattern for my tables where each table would have an int Id column representing the PK column but then it would also include a Guid column as a globally unique identifier. How common is it to use this type of pattern?
In the vast majority of cases you'll want to either use an INT or BIGINT for you primekey/foreign key. For the most part you are looking to make sure that table can be joined to and have a way to easily select a single unique row. In theory using GUIDs all over the place gets you there too, if you were a robot and could quickly ask a colleague, "Hey can you check out ROW_ID FD229C39-2074-4B04-8A50-456402705C02" vs "Hey can you check out ROW_ID 523". But we are human. I don't think there is a really good reason to include another column that is simply a GUID in addition to your PK (which should be an INT or BIGINT)
It can also be nice to have your PK in an order, that seems to come in handy. GUIDs won't be in a order. However, a case for using a GUID would be if you have to expose this value to a customer. You may not want them to know they are customer #6. But being customer #B8D44820-DF75-44C9-8527-F6AC7D1D259B isn't too great if they have to call in and identify themselves, but might be fine for writing code against (say a webservice or some kind of API). SQL is a lot of art with the science!
In addition do you really need a global unique id for a row? Probably not. If you are designing a system that could use up more than what INT can handle (say total number of tweets in all time) then use BIGINT. If you can use up all the BIGINTs, wow. I'd be interested in hearing how and would like to subscribe to your newsletter.
A question I ask myself when writing stuff, "If I'm wrong how hard will it be to do the other way?". If you really need a GUID later, add it. If you put it in now and just 1 person uses it you can never take it out and it will have to be maintained... job security? nah, don't think that way :) Don't over engineer it.
I would not say GUID is generally preferred from a DBA perspective. It is larger (16 bytes rather than 4 for int or 8 for bigint) and the random variety introduces fragmentation and causes much more IO with large tables due to lower page life expectancy. This is especially a problem with spinning media and limited RAM.
When a GUID is actually needed, some of these issues can be avoided using a sequential version for the GUID value rather than introducing another surrogate key. The value can be assigned in by SQL Server with a NEWSEQUENTIALID() default constraint on a column or generated in application code with the bytes ordered properly for SQL Server. Below is a Windows C# example of the latter technique.
using System;
using System.Runtime.InteropServices;
public class Example
{
[DllImport("rpcrt4.dll", CharSet = CharSet.Auto)]
public static extern int UuidCreateSequential(ref Guid guid);
/// sequential guid for SQL Server
public static Guid NewSequentialGuid()
{
const int S_OK = 0;
const int RPC_S_UUID_LOCAL_ONLY = 1824;
Guid oldGuid = Guid.Empty;
int result = UuidCreateSequential(ref oldGuid);
if (result != S_OK && result != RPC_S_UUID_LOCAL_ONLY)
{
throw new ExternalException("UuidCreateSequential call failed", result);
}
byte[] oldGuidBytes = oldGuid.ToByteArray();
byte[] newGuidBytes = new byte[16];
oldGuidBytes.CopyTo(newGuidBytes, 0);
// swap low timestamp bytes (0-3)
newGuidBytes[0] = oldGuidBytes[3];
newGuidBytes[1] = oldGuidBytes[2];
newGuidBytes[2] = oldGuidBytes[1];
newGuidBytes[3] = oldGuidBytes[0];
// swap middle timestamp bytes (4-5)
newGuidBytes[4] = oldGuidBytes[5];
newGuidBytes[5] = oldGuidBytes[4];
// swap high timestamp bytes (6-7)
newGuidBytes[6] = oldGuidBytes[7];
newGuidBytes[7] = oldGuidBytes[6];
//remaining 8 bytes are unchanged (8-15)
return new Guid(newGuidBytes);
}
}