./ahmedhashim

Logarithms in Solidity

July 24, 2022

Patterns that occur in life and nature are often non-linear. From comparing frequencies of notes on a piano to Moore’s Law, logarithms provide a way to measure values extremely far apart from one another. When building tools for society, this can also be a useful property to map nuanced human behavior at scale.

Normalizing magnitude

Simply put, a logarithm is the inverse function to exponentiation. In other words, it’s the power y to which a base number x must be raised to in order to get the argument z:

$$ x^y = z \ \iff \ \log_{x} z = y $$

This makes comparing large ranges of numbers easier by normalizing the distance between extremely small and extremely large values in the range according to a regular base interval.

For example, Reddit uses a logarithm in its hot ranking function to decide which posts appear on the homepage. Specifically, it weighs the first 10 votes on a post the same as the next 100. Those 100 in turn weigh the same as the next 1000, and so on for every magnitude of ten thereafter (10 being the base).

As the votes increase by orders of magnitude, the weight between each order remains the same. This helps to mitigate a “pile-on” effect on post submissions which may occur through brigading.

In other words: Logarithms can be used to clamp linear growth.

Floating in Solidity

While working with Solidity, the smart-contract programming language for Ethereum, I found myself in a similar situation. I needed a function to rank a post in a microblogging dApp based on the number of “likes” it had.

My initial plan was to take the logarithm of likes in order to normalize its value in the ranking function. However, I quickly learned that solidity doesn’t natively support logarithms due to the EVM’s lack of floating point numbers.

To put it bluntly, it’s because implementing floating point types consistently across programming languages is difficult.

Different implementation of the same function could possibly return slightly different values. This is largely due to how the language handles binary arithmetic under the hood to represent floats as decimals. If you’ve worked with one such language, you might have come across this familiar example:

0.1 + 0.2 = 0.30000000000000004

Ethereum clients (aka “nodes”) are written in a variety of languages. If floating point types were used in the consensus mechanism across polyglot nodes, they might arrive at different values. This would cause forks to occur, and over time, will weaken the cumulative strength of the network — leaving it vulnerable to 51% attacks.

Now that we understand the “why”, it still poses the question: how does one perform logarithms in a language that doesn’t support floating points?

The key lies in understanding how computers represent numbers — namely, in binary.

Fixed imagination

Take the number 3 represented by four bits:

0011

Using bitwise shifting, we can double the value by shifting to the left, or half it by shifting to the right. So in order to multiply by 2, we shift each value to the left by 1 bit, thereby doubling it to arrive at a binary value of 6:

0011 << 1 == 0110 // 6

But suppose we wanted to go the other way and divide by 2 instead? Shifting the value right by 1 drops the least significant bit from the memory register, and we’re left with a binary value of 1.

0011 >> 1 == 0001 // lost the least significant bit

How can we represent the decimal value 1.5 in binary?

Let’s rewind & break down exactly what’s going on in the original four bit representation:

0011 == (0 * 2^3) + (0 * 2^2) + (1 * 2^1) + (1 * 2^0)

Which equals 3.

Notice that each bit is represented by a power of 2 decreasing, until it gets raised to the power 0 in the least significant bit. If we continued the series past the last bit, it would look like the following:

(n * 2^0) + (n * 2^-1) + (n * 2^-2) + ...

We know 2^-1 is equal to 0.5, and 2^-2 is equal to 0.25, so we’re heading in the right direction! But how can you represent a number lower than 2^0 in binary?

Picture an imaginary decimal point in the middle of the four bit value representing 3:

00.11

Applying the same formula, this would be equivalent to:

0.75 == (0 * 2^1) + (0 * 2^0) + (1 * 2^-1) + (1 * 2^-2)

Not quite the answer we’re looking for, but getting closer.

What if, before placing the imaginary decimal point, we left shifted the original value by half the bits available? (2 bits in this example):

0011 << 2 == 1100

The computer sees this value as 12. However, we’ll continue to use our imagination to place a decimal point in the middle of the four bits to split our whole numbers to the left of it, and fractional values to the right.

11.00

This lets us pretend we’re back at the bit representation of 3, along with space for our fractional values.

Now if we right shift by 1 in order to divide in half, we’re left with:

11.00 >> 1 == 01.10

Giving us a value of 1.5 in our minds. We did it!

However, regardless of our shift, the computer still isn’t aware of the decimal point:

0110 // 6

As it turns out, this doesn’t matter!

Deja Q

In order to perform floating point calcuations in binary, we need to reserve an arbitrary amount of bits representing the fractional component for all numbers in the equation.

This amount is known as the Q-value. The higher the Q, the more floating point precision there is to work with.

To summarize: we fix an imaginary decimal point after a specific amount of bits in the space available to represent the fractional number, hence the term “fixed point”.

If every number in the calculation has the same Q, then the final result will be the desired value with the same imaginary fixed point.

It can be shifted back by the Q amount to get its (accurate enough) integer representation for display purposes. However, if displaying the value isn’t required, then keeping all the calculations in fixed point will be more precise.

The accuracy of the result will depend on the precision involved. Namely, how high the Q is (usually determined by the amount of memory & compute you have to work with). Luckily the IEEE-754 standard exists to serve as a guide on this matter.

Equipped with this knowledge, we can now take logarithms in Solidity by converting our integers to fixed point values.

Shifting perspective

Since we’re working with fixed point binary numbers, a binary logarithm is a good place to begin (that of base 2). They can be represented such that 2 to the power y is equal to x:

$$ 2^y = x \ \iff \ \log_{2} x = y $$

Which means, in order for y to exist, x must be a positive number. To calculate the integer component of the log (the bits to the left of the fixed point), we can use the following formula for a positive integer x:

for (n = 0; x > 1; x >>= 1) n += 1;

On every loop, we count an additional order of magnitude represented by n, and right shift x until it’s past the least significant bit. Since the orders in a binary log are powers of of 2, bit-shifting is an efficient way to update the value.

If x itself is a fractional number, we can factor it into a mantissa multiplied by an exponent:

$$ x = m \times 2^{e} = \log_{2} m + e $$

and use the above loop to calculate the binary log of the mantissa, then add the exponent to it.

Too close for missiles, switching to guns

On the right side of the fixed point, we have the fractional part of the number x. Since we know it must lie between the orders of magnitude represented by the integer n that we calculated in the previous step:

$$ 2^n \ \leq \ x \ < \ 2^{n+1} $$

then using the properties of logarithms, we know that x is at least 1, and less than 2:

$$ 1 \ \leq \ \frac{x}{2^n} \ < \ 2 $$

Recall that the fractional values on the right side of the fixed point were calculated by 2 * x^-n where n starts at 0 decreasing for every bit m. Each bit’s value can be represented as:

$$ 2 \times \frac{x}{2^{n-m}} $$

So we’re looking for the binary logarithm of that fraction for every bit of precision available:

$$ \log_{2} \frac{x}{2^n} \ + \ \log_{2} \frac{x}{2^{n-1}} \ + \ … $$

In order to find such a number, we’ll use another two properties of logarithms:

$$ \log_{2}x = \frac{\log_{2}x^2}{2} \ = \ 1 + \log_{2}x \frac{x}{2} $$

Based on a specific level of precision, we can use the following code to calculate the fractional value:

for (delta = 1; delta >= precision; delta /= 2) {
  if (x >= 2) { result += delta; x /= 2; }
  x *= x;
}

Every loop applies the first property of squaring x, and halfing the delta until x is equal to or greater than 2. At that point, you save the current delta as part of the result, divide x in half & keep repeating until the delta value has fallen below the level of precision desired.

What results is a function that converges towards the fractional value of x.

Reinventing the wheel

Unfortunately, the last bit of code won’t accurately work in Solidity due to the lack of floating points. Instead, one would have to write a library to perform integer math for a specific Q value of the number that you’re taking a logarithm of in order to reach the desired result.

Fortunately, this exists in the form of ABDK Libraries for Solidity.

Not only does it perform binary logarithms, it can also take the natural log, exponent, and other useful floating point calculations) all with 64x64 fixed point or 128x128 IEEE 754 quadruple precision.

If a common logarithm (base 10) is required, you can use the following property to compute it from a binary log:

$$ \log_{10}x = \log_{2}x \times \log_{10}2 $$

the common logarithm of 2 can be precomputed ahead of time as:

0.3010299956639811952137388947244930267681898814621085

or in 128x128 bit quadruple precision hexadecimal format:

0x4d104d427de7fbcc47c4acd605be48bc

By multiplying the fixed point binary log of a number with the above hex value, common logarithms can be calculated in Solidity.

Going further

Now that we’ve unlocked the secrets of fixed point calculations in a language without floating point types, a world of opportunity is available to us.

I was able to use natural logarithms in the microblogging dApp to clamp my “likes” after all. But that’s only scratching the surface of what could be done with the power of floating point precision on the EVM.

Some interesting ideas I’d like to see implemented in Solidity using logarithms would include:

Relief-fund contracts for crises like earthquakes or contaminated water zones? A peer-to-peer digital audio workstation that incentivizes collaboration? John Carmack releases Quake 4 on the EVM?

What can you think of next?