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));
difficulty = BASE_DIFFICULTY / (DIFFICULTY_RAMP**generation);
if(
generation > 13){
difficulty /= (tokenId — 2**14 + 1);
}
BASE_DIFFICULTY = uint(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff)/uint(300);
DIFFICULTY_RAMP = 3;
generation = floor(log2(tokenId))

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{
uint BASE_COST = 0.000045 ether;

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))

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.

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.

const BN = ethers.BigNumber;
result = var1.mul(var2);

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));
}
//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;
}
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]);
}
function Attempt(address,prev_hash,nonce,difficulty){
const _hash = Hash(address,prev_hash,nonce);
const hash = BN.from(_hash);

return hash.lt(difficulty);
}

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);
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
});
//TokenId of token minted
tokenId = Number(event.args._tokenId._hex);
//Hash of token minted
hash = event.args.hash;

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.

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);
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"];

Codeslinger. Melbourne based Solidity developer for hire.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store