POW NFT — Technical Details
This is a short technical write-up of POW NFT mining, for people making their own mining software or are interested in adding mining to their own NFT projects.
Hashing
The hash function used to mine POW NFTs is keccak256, because it’s built right into Solidity. A successful hash will be below the block target and takes the following form:
bytes32 hash = keccak256(abi.encodePacked(address miner_address,bytes32 prev_hash, uint nonce));
A POW NFT can be minted if a hash (cast to uint) is found below the difficulty
, which follows the following rule:
difficulty = BASE_DIFFICULTY / (DIFFICULTY_RAMP**generation);
if(generation > 13){
difficulty /= (tokenId — 2**14 + 1);
}
Where tokenId increments by 1 with each new token,
BASE_DIFFICULTY = uint(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)/uint(300);
DIFFICULTY_RAMP = 3;
And (pseudocode):
generation = floor(log2(tokenId))
Note: base difficulty is 1/300 the max value of a uint
.
Generational demand curve
Once a valid hash is found, the mine function must be executed on the POW NFT smart contract. It takes only one argument, and also requires msg.value equal to the current minting price according to the demand curve.
function mine(uint nonce) payable{
This function uses msg.sender and the stored previous hash to validate the nonce, and if the hash is valid a token is minted.
The demand curve is described as:
cost = (2**generation — 1)* BASE_COST;
Where
uint BASE_COST = 0.000045 ether;
Essentially, the minting cost jumps discretely every time the number of tokens doubles.
Previous hash
When a token is minted, the hash is re-hashed with block.timestamp
to provide the prev_hash
for the next token.
keccak256(abi.encodePacked(hash,block.timestamp))
This has a dual purpose of preventing miners from pre-mining several tokens at once and submitting them all at once, but also ensure that hash data used to render POW NFT Atoms isn’t affected by the changing difficulty level.
Sample code for writing your own miner
There has been a lot of enthusiasm around this project since launch last week, and everyone is eager to maximise their hash rate. Although I’ve made some marginal improvements to the in-site miner (and hopefully more are on their way), there are plenty more speed gains to be had for someone writing their own.
The in-site miner is written in Javascript, so I’m providing some functions here that should help people interested in writing their own, either using the code for a JS miner, or for writing in their preferred language.
If you want to see a finished product, you can see a bare-bones web miner on GitHub.
Dependencies
Although I’m a big fan of web3.js, the site uses ethers.js for it’s web3 stuff. This is because the JS framework (svelte.js) wouldn’t play nice with web3.js, but don’t get me started on JS frameworks and the headaches they cause. Vanilla 4 lyf.
Anyway, the only dependency you need for the following code is ethers.js. It’s mostly for sending transactions, but I also make use of its BN (big number) class.
const BN = ethers.BigNumber;
If you haven’t used it before, its pretty simple. It can handle 256 bit integers (unlike pure JS), but syntax is a little different. To multiply two BN variables we do something like this:
result = var1.mul(var2);
ethers.js also lets you use a human-readable ABI for contract instances. I’ll paste this at the end of this article.
Important contract variables
There’s a few key variables which you’ll need for your code, the BASE_DIFFICULTY
, DIFFICULTY_RAMP
and BASE_COST
(for calculating minting cost). These all mirror values in the smart contract.
const BASE_DIFFICULTY = BN.from('0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff').div(BN.from(300));const DIFFICULTY_RAMP = 3;const BASE_COST = BN.from(ethers.utils.parseEther('0.000045'));
Token classification functions
Your miner needs to know the current generation of your token in order to work out the target difficulty as well as the current minting cost, so you’ll want the following functions too
//Get the generation of a given tokenId
function generationOf(_tokenId){
return Math.floor(Math.log2(_tokenId));
}
and
//Get the difficulty target to mine a given tokenId
function getDifficulty(tokenId){
const generation = generationOf(tokenId);
let difficulty = BASE_DIFFICULTY.div(BN.from(Math.pow(DIFFICULTY_RAMP,generation)));
if(generation > 13){
difficulty.div(
BN.from(parseInt(tokenId) - Math.pow(2,14) + 1)
);
}
return difficulty;
}
And our cost calculation function is:
const calculate_cost = (_tokenId) => {
const generation = generationOf(_tokenId);
return (BN.from(Math.pow(2,generation) - 1)).mul(BASE_COST);
}
The hashing function
Now that we have all of these parts ready, we need an actual hashing function. Luckily ethers.js has a keccak256 function built in, so this is pretty easy:
function Hash(address,prev,nonce){
return ethers.utils.solidityKeccak256(["address","bytes32","uint256"],[address,prev,nonce]);
}
Where address
is your address, prev
is the hash of the previously minted token, and nonce
is the current nonce.
However, this returns a value in bytes, and we need a BN to be able to compare it to our current difficulty, so you can wrap it all in a function with the current difficulty and let it tell you whether it’s a successful nonce.
function Attempt(address,prev_hash,nonce,difficulty){
const _hash = Hash(address,prev_hash,nonce);
const hash = BN.from(_hash);
return hash.lt(difficulty);
}
Note: lt
is a “less than” comparison function. This will return true if hash is less than difficulty. So basically you just need to iterate nonce and test if the hash was successful. If not, iterate again and repeat.
Monitoring the contract
Obviously you need to know the hash of the latest token as soon as it’s minted, because otherwise you’re hashing garbage. The site monitors events emitted by the contract to do this. The relevant event in the smart contract is:
event Mined(uint indexed _tokenId, bytes32 hash);
This will tell you the tokenId and hash of the token that has been mined. So your miner just needs to watch for these events, and adjust itself accordingly, the prev hash is hash
and the difficulty and cost can be calculated based on _tokenId + 1
.
Subscribing to events with ethers.js is pretty easy, it basically takes this form:
contract.on( "Mined" , (author,oldValue,newValue,event,e,f,g,h)=>{
if(typeof event === "undefined"){
//This catches weird stuff with ethers.js that their
// docs don't explain
event = newValue;
}
//Do stuff with the event
});
And the components of event
that you’ll want are:
//TokenId of token minted
tokenId = Number(event.args._tokenId._hex);
//Hash of token minted
hash = event.args.hash;
Just let that event update your stored current tokenId and hash (and probably reset your nonce) and you should be good to go. The rest of it is design choices.
Go forth and build
That should give you a head-start on writing your own miner. I’m currently investigating using a web-assembly based keccak256 instead of the js one in ethers.js. Hopefully that will add to performance.
If you want to recompile the smart contract for whatever reason, you can see the Solidity code at:
https://etherscan.io/address/0x9abb7bddc43fa67c76a62d8c016513827f59be1b#code
The address of the contract is0x9Abb7BdDc43FA67c76a62d8C016513827f59bE1b
But if you’re playing around and want to test, there’s an instance on Ropsten at 0x88066567a7F90409Ced36D4030C47909Eb910926
If you are deploying a version on your local RPC you’ll probably want the pre-migration contract code too, which can be found here:
https://etherscan.io/address/0x7d4e35A2090b3ba805ddB39B2c4b83612890Df87#code
The new contract takes the previous contract address as its only argument (because it took a snapshot when it was launched).
If there is any more information you want, please don’t hesitate to ask. The only reason I haven’t provided the full miner code is because its tangled up with frontend code and is a little incomprehensible at a glance.
Human readable ABI for ethers.js
Rather than making you recompile the contract, I’ve provided the human-readable ABI that you can use to create your contract instance, ie, like this:
contract = new ethers.Contract(contractAddress, contractAbi, provider);
contractWithSigner = contract.connect(signer);
Here it is for you to copy/paste:
const contractAbi = ["event Approval(address indexed _owner,address indexed _approved,uint256 indexed _tokenId)","event ApprovalForAll(address indexed _owner,address indexed _operator,bool _approved)","event Migrate(uint256 indexed _tokenId)","event Mined(uint256 indexed _tokenId,bytes32 hash)","event Transfer(address indexed _from,address indexed _to,uint256 indexed _tokenId)","event Withdraw(uint256 indexed _tokenId,uint256 value)","function PREV_CHAIN_LAST_HASH() view returns(bytes32 )","function UNMIGRATED() view returns(uint256 )","function V2_TOTAL() view returns(uint256 )","function approve(address _approved,uint256 _tokenId) nonpayable","function balanceOf(address _owner) view returns(uint256 )","function getApproved(uint256 _tokenId) view returns(address )","function hashOf(uint256 _tokenId) view returns(bytes32 )","function isApprovedForAll(address _owner,address _operator) view returns(bool )","function migrate(uint256 _tokenId,uint256 _withdrawEthUntil) nonpayable","function migrateMultiple(uint256[] _tokenIds,uint256[] _withdrawUntil) nonpayable","function mine(uint256 nonce) payable","function name() view returns(string _name)","function ownerOf(uint256 _tokenId) view returns(address )","function safeTransferFrom(address _from,address _to,uint256 _tokenId) nonpayable","function safeTransferFrom(address _from,address _to,uint256 _tokenId,bytes data) nonpayable","function setApprovalForAll(address _operator,bool _approved) nonpayable","function supportsInterface(bytes4 interfaceID) view returns(bool )","function symbol() view returns(string _symbol)","function tokenByIndex(uint256 _index) view returns(uint256 )","function tokenOfOwnerByIndex(address _owner,uint256 _index) view returns(uint256 )","function tokenURI(uint256 _tokenId) view returns(string )","function totalSupply() view returns(uint256 )","function transferFrom(address _from,address _to,uint256 _tokenId) nonpayable","function withdraw(uint256 _tokenId,uint256 _withdrawUntil) nonpayable","function withdrawMultiple(uint256[] _tokenIds,uint256[] _withdrawUntil) nonpayable"];