Building a Bitcoin Emulator on Algorand
The Algorand Foundation recently released a Python-to-TEAL compiler called PuyaPy.
It's still in developer preview, but I wanted to try it out.
Inspired by ORA, I thought it would be fun to build a mineable POW asset based on the Bitcoin protocol.
This is purely for educational purposes... don't take it too seriously!
I pulled this together by digging through the Bitcoin core repo and reading some of the Mastering Bitcoin book.
Block Header & Smart Contract State
In order to implement the emulator as a smart contract, we'll need some of that dreaded global state.
Field | Description | PuyaPy Type |
block_height | The number of blocks preceding the current block in the blockchain. | UInt64 |
block_hash | The hash of the most recent block. | arc4.UInt256 |
coinbase | Arbitrary bytes. | Bytes |
prev_retarget_time | The UNIX timestamp of the block when the POW difficulty was last retargeted/adjusted. | UInt64 |
In addition, we need to store some of the fields from the Bitcoin block header:
Field | Description | Implementation Notes | PuyaPy Type |
time | The approximate creation time of this block (UNIX epoch time). | The timestamp will be provided by the Algorand protocol, not the miner. For this reason, the timestamp will not be included in the block hash. | UInt64 |
target | The target threshold this block’s header hash must be less than or equal to. | This will be stored as a uint256, not the encoding format Bitcoin uses. | BigUInt |
nonce | An arbitrary number miners change to modify the header hash in order to produce a hash less than or equal to the target threshold. | This will be a uint64 (Bitcoin uses uint32). | UInt64 |
Genesis Block
The hash of the genesis block will be set to match the Bitcoin genesis hash:
consensus.hashGenesisBlock == uint256S("0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f");
Chain Params
POW
The POW limit (max target) in the Bitcoin protocol is:
consensus.powLimit = uint256S("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
Which roughly means each block hash needs to have at least 32 leading zero bits.
The POW target timespans in Bitcoin (used for adjusting the POW difficulty every 2016 blocks) are:
consensus.nPowTargetTimespan = 14 * 24 * 60 * 60; // two weeks
consensus.nPowTargetSpacing = 10 * 60;
int64_t DifficultyAdjustmentInterval() const { return nPowTargetTimespan / nPowTargetSpacing; }
This proof of work is necessary for securing the Bitcoin network.
But we're building an emulator on Algorand. The proof of work doesn't contribute anything to the security of the smart contract.
So let's increase the POW_LIMIT
:
POW_LIMIT = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
And since Algorand is much faster, let's aim for 10 seconds per block instead of 10 minutes.
The contract will adjust the target difficulty once every 360 blocks, anchoring the time it takes to produce 360 blocks at ~1 hour.
Money Supply
Following the Bitcoin protocol, the genesis block reward is set to 50.
Every 210,000 blocks the reward gets halved.
In Bitcoin, this takes about 4 years.
Our emulator produces blocks about about 60 time faster than Bitcoin, so the halving should occur about every 24.3 days.
Constants in PuyaPy
You can define constants in PuyaPy the same way you would in regular Python.
Not all of these need to be constants, but I've added them for code clarity:
GENESIS_BLOCK_HEIGHT = 0
# https://github.com/bitcoin/bitcoin/blob/bdddf364c9a6f80e3bfcf45ab1ae54f9eab5811b/src/kernel/chainparams.cpp#L29
# consensus.hashGenesisBlock == uint256S("0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")
GENESIS_BLOCK_HASH = 0x000000000019D6689C085AE165831E934FF763AE46A2A6C172B3F1B60A8CE26F
POW_LIMIT = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
# Spanning 1 hour
POW_TARGET_TIMESPAN = 60 * 60
# Aim for 1 block every 10 seconds
POW_TARGET_SPACING = 10
SMALLEST_TIMESPAN = POW_TARGET_TIMESPAN // 4
LARGEST_TIMESPAN = POW_TARGET_TIMESPAN * 4
# Adjust the difficulty every 360 blocks
DIFFICULTY_ADJUSTMENT_INTERVAL = POW_TARGET_TIMESPAN // POW_TARGET_SPACING
Difficulty Retargeting/Adjustment
The POW difficulty should be retargeted every 360 blocks.
Let's create a subroutine (a function) that checks for this:
@subroutine
def should_adjust_difficulty(self) -> arc4.Bool:
return arc4.Bool(self.block_height % DIFFICULTY_ADJUSTMENT_INTERVAL == 0)
In the Bitcoin protocol, the target can only be adjusted by a factor of 4 at each retargeting.
It looks like the builtin min/max functions can't be used with PuyaPy yet, so let's define 'least' and 'greatest' functions to make the syntax a bit cleaner:
@subroutine
def least(a: UInt64, b: UInt64) -> UInt64:
return a if a < b else b
@subroutine
def greatest(a: UInt64, b: UInt64) -> UInt64:
return a if a > b else b
And a function to clamp a value between a lower and upper bound:
@subroutine
def clamp(self, value: UInt64, lower_bound: UInt64, upper_bound: UInt64) -> UInt64:
return self.least(self.greatest(value, lower_bound), upper_bound)
Ideally these functions would accept any numeric type in PuyaPy, but I'm not sure if type unions are supported yet (with or without overloading).
Next, we need to calculate what the next target should be.
In Bitcoin, the code is:
unsigned int CalculateNextWorkRequired(const CBlockIndex* pindexLast, int64_t nFirstBlockTime, const Consensus::Params& params)
{
if (params.fPowNoRetargeting)
return pindexLast->nBits;
// Limit adjustment step
int64_t nActualTimespan = pindexLast->GetBlockTime() - nFirstBlockTime;
if (nActualTimespan < params.nPowTargetTimespan/4)
nActualTimespan = params.nPowTargetTimespan/4;
if (nActualTimespan > params.nPowTargetTimespan*4)
nActualTimespan = params.nPowTargetTimespan*4;
// Retarget
const arith_uint256 bnPowLimit = UintToArith256(params.powLimit);
arith_uint256 bnNew;
bnNew.SetCompact(pindexLast->nBits);
bnNew *= nActualTimespan;
bnNew /= params.nPowTargetTimespan;
if (bnNew > bnPowLimit)
bnNew = bnPowLimit;
return bnNew.GetCompact();
}
The retargeting logic is explained well here:
New Difficulty = Old Difficulty * (Actual Time of Last 2016 Blocks / 20160 minutes)
Using the variable names in our emulator:
new_target = self.target * (actual_timespan / POW_TARGET_TIMESPAN)
Note that actual_timespan
is clamped to fall within the correct range (this covers the 'limit adjustment step' in the code above.
PuyaPy doesn't support floats, so we'll calculate the new target using integers.
Let's run through a quick example using the Bitcoin POW parameters:
>>> current_target = POW_LIMIT
>>> print(f"{current_target=}")
>>> actual_timespan = 1_000_000
>>> print((actual_timespan / POW_TARGET_TIMESPAN))
current_target=26959946667150639794667015087019630673637144422540572481103610249215
0.8267195767195767
POW_TARGET_TIMESPAN
= 1209600 (1.2m seconds)
If the actual_timespan
of the last 2016 blocks is 1_000_000
seconds, it means they were mined in less than two weeks, so the target difficulty should be increased.
actual_timespan
is about 80% of the POW_TARGET_TIMESPAN
, so the target should be lowered to about 80% of its previous value.
Using floats in vanilla Python:
>>> new_target = target * (actual_timespan / POW_TARGET_TIMESPAN)
>>> print(f"{new_target=:.0f}")
new_target=22288315697049140675359445701028488634929446095978145081409698529280
To check that the ratio is correct:
>>> print(f"{new_target / POW_LIMIT:.16f}")
0.8267195767195767
Perfect. So how do we achieve this without floats?
One method would be to multiply the actual_timespan
by some factor, and floor divide the result by the same factor.
>>> new_target = current_target * (actual_timespan * 1000 // POW_TARGET_TIMESPAN) // 1000
>>> print(f"{new_target=:.0f}")
>>> print(f"{new_target / POW_LIMIT:.3f}")
new_target=22268915947066427297078055986432057755814838631063044161294441644032
0.826
We've could increase the precision by multiplying using a number larger than 1000, but this is good enough for the emulator.
Putting this into a subroutine:
@subroutine
def calculate_next_work_required(self) -> BigUInt:
timespan = self.clamp(
value=self.time - self.prev_retarget_time,
lower_bound=UInt64(SMALLEST_TIMESPAN),
upper_bound=UInt64(LARGEST_TIMESPAN),
)
new_target = self.target * (timespan * 1000 // POW_TARGET_TIMESPAN) // 1000
return new_target if new_target < POW_LIMIT else BigUInt(POW_LIMIT)
The final part ensures that the new target is never higher the POW_LIMIT
.
The idea is that this logic will run at the end of a mining transaction:
@arc4.abimethod()
def mine(self) -> None:
# Mining logic will go here
# Target will be updated afterwards
if self.should_adjust_difficulty():
self.target = self.calculate_next_work_required()
Mining
Onto the fun part.
The mining in the emulator will be simple.
A miner will have to provide a nonce
and coinbase
bytes (can be empty), such that:
hash(prev_block_hash, nonce, coinbase) <= target
coinbase
can be used to vary the hash or to add some message to the emulated blockchain.
The coinbase data in the genesis block of Bitcoin contained the message:
The Times 03/Jan/2009 Chancellor on brink of second bailout for banks
We'll use double SHA-256 for hashing, just like Bitcoin does.
Let's update the mine
method:
@arc4.abimethod()
def mine(self, nonce: arc4.UInt64, coinbase: Bytes) -> UInt64:
assert self.is_bootstrapped(), "Application not bootstrapped"
proposed_block_hash = BigUInt.from_bytes(
op.sha256(op.sha256(self.block_hash.bytes + nonce.bytes + coinbase))
)
assert proposed_block_hash < self.target, "Invalid block"
current_time = Global.latest_timestamp
self.block_height += 1
self.block_hash = arc4.UInt256(proposed_block_hash)
self.coinbase = coinbase # Store this in state just for fun
self.time = current_time
if self.should_adjust_difficulty():
next_work = self.calculate_next_work_required()
self.prev_retarget_time = current_time
self.target = next_work
reward = self.reward_miner()
return reward
And add a subroutine for rewarding the miner:
@subroutine
def reward_miner(self) -> UInt64:
return (
itxn.AssetTransfer(
xfer_asset=self.asa,
asset_amount=self.get_block_subsidy(),
asset_receiver=Txn.sender,
fee=0,
)
.submit()
.asset_amount
)
I will explain the get_block_subsidy()
method soon.
Money Supply
The halving rewards in the Bitcoin protocol are calculated as follows:
CAmount GetBlockSubsidy(int nHeight, const Consensus::Params& consensusParams)
{
int halvings = nHeight / consensusParams.nSubsidyHalvingInterval;
// Force block reward to zero when right shift is undefined.
if (halvings >= 64)
return 0;
CAmount nSubsidy = 50 * COIN;
// Subsidy is cut in half every 210,000 blocks which will occur approximately every 4 years.
nSubsidy >>= halvings;
return nSubsidy;
}
Let's add a few more constants to the smart contract:
DECIMALS = 8
COIN = 100_000_000 # 10**DECIMALS
MAX_MONEY = 2_100_000_000_000_000 # 21_000_000 * COIN
SUBSIDY_HALVING_INTERVAL = 210_000
SUBSIDY = 50 * COIN
Note: If I try to define COIN as 10**DECIMALS, I get the error Expression has type "Any"
.
Translating the Bitcoin subsidy code into a subroutine:
@subroutine
def get_block_subsidy(self) -> UInt64:
halvings = self.block_height // SUBSIDY_HALVING_INTERVAL
# Force block reward to zero when right shift is undefined.
return UInt64(0) if halvings >= 64 else SUBSIDY >> halvings
Asset Creation
I'm going to pinch most of this logic from the PuyaPy AMM example code.
The asset supply is fixed at 21m. I guess there are two ways to approach the minting:
Create all 21m coins at the start and hold them in the contract account.
Each block mined will transfer some portion of the supply to the miner.Mint new coins on the fly as each block is mined.
I think the first idea is a bit simpler and potentially reduces transaction fees for miners.
To create the asset with this initial supply:
@subroutine
def create_asset(self) -> Asset:
return (
itxn.AssetConfig(
asset_name=b"EMU",
unit_name=b"EMUSHIS",
total=MAX_MONEY,
decimals=DECIMALS,
manager=Global.current_application_address,
reserve=Global.current_application_address,
fee=0,
)
.submit()
.created_asset
)
'EMU' is short for 'emulated bitcoin'.
And 'EMUSHIS'... 'emulated Satoshis'...? 🤔
Viewing the asset in Dappflow:
Just like Bitcoin, there are 21m EMU, and 2.1 quadrillion EMUSHIS.
Bootstrapping the Application
Again, this is mostly pinched from the PuyaPy example code.
@arc4.abimethod()
def bootstrap(self, seed: gtxn.PaymentTransaction) -> UInt64:
assert not self.is_bootstrapped(), "Application has already been bootstrapped"
assert Txn.sender == self.creator, "Only the creator may call this method"
assert (
seed.receiver == Global.current_application_address
), "Receiver must be app address"
assert Global.group_size == 2, "Group size must be 2"
assert seed.amount >= 300_000, "Amount must be >= 3 Algos" # 0.3 Algos
self.asa = self.create_asset()
self.prev_retarget_time = self.time = Global.latest_timestamp
return self.asa.asset_id
The first step prevents the application boostrap logic running more than once.
This subroutine is mainly to improve readability:
@subroutine
def is_bootstrapped(self) -> bool:
return bool(self.asa)
The bool() method of puyapy.Asset returns True if the asset_id is not 0.
0 is the asset ID of ALGO, so any new asset we create will have an ID > 0.
The next step checks that the account sending the bootstrap transaction is the creator (deployer) of the application. There's nothing stopping someone else deploying another version of the application and calling the bootstrap method themselves.
After creating the asset, we set the initial values of prev_retarget_time
and time
.
Initially I had tried to set the values here:
class Emulator(ARC4Contract):
def __init__(self) -> None:
self.prev_retarget_time = Global.latest_timestamp
self.time = Global.latest_timestamp
But each one ends up as 1 January 1970 (UNIX timestamp 0).
I'm not sure if that's because it's running on LocalNet, or because Global.latest_timestamp
is inaccessible/defaults to zero outside of an application call.
Either way, setting it in the bootstrap method works.
Logging
PuyaPy provides a log()
function, which adds some bytes to the application logs.
You can query these using the indexer client.
I couldn't find an easy way to query the global state of the application at a given round, so logging seems like the easiest way to track state changes like difficulty adjustments, and other events like mining rewards.
In the difficulty adjustment section, we can add the following:
if self.should_adjust_difficulty():
next_work = self.calculate_next_work_required()
log(
"retargeted_at",
current_time,
"prev_retarget_time",
self.prev_retarget_time,
"prev_target",
self.target,
"new_target",
next_work,
sep=b"\x1F",
)
The indexer logs need to be decoded from base64.
With the log format above, we can split the decoded value on the separator/delimiter, and then parse it into a key/value structure.
Anything passed to the log function as a string gets encoded as UTF-8, so the keys are fine as they are. The values need some additional transformation.
I won't go into it in detail here, but the end result of the parsing will look like this:
{
│ 'txid': 'WPSLUVHMPUOOHQYUAQXKRYF4OGREN56OU7SYNVVS46HCO3PPFBUA',
│ 'miner': '2KFJ62AMSMI4KKTG5U3SEL3NKMLETACET67JAUSKV2NJUAXVPZCOROAWWQ',
│ 'reward': 5000000000
}
And this:
{
│ 'txid': 'ATNPFYQSCBMO7FEFFSQAFL3AWPD5E4SHMAKWWA3UFDCFLG2262DA',
│ 'retargeted_at': '2024-02-26T09:15:33',
│ 'prev_retarget_time': '2024-02-26T08:20:58',
│ 'prev_target': '3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
│ 'new_target': '3a2d0e5604189374bc6a7ef9db22d0e5604189374bc6a7ef9db22d0e560417',
│ 'adjustment_factor': 1.1001100110011002
}
There might be a better way to log structured data... maybe using ABI encoding/msgpack/something else.
If anyone's implemented this previously, please let me know!
Simulation & Testing
The ABI methods can be tested quite easily.
For the difficulty adjustment and subsidy calculations, I'm just going to simulate it by mining many blocks and checking the state in Dappflow/application logs.
I can see that the difficulty adjustment works:
{
│ 'txid': 'KTBFQUAO5XXWKOS2CYOBIP554O3XOSQH3Z5LUHE5IGP56KROMMWA',
│ 'retargeted_at': '2024-02-26T08:20:11',
│ 'prev_retarget_time': '2024-02-26T08:11:09',
│ 'prev_target': '3fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
│ 'new_target': '0fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
│ 'adjustment_factor': 4.0
}
Here, the first 360 blocks were mined in 9 minutes, so the target should be lowered by the maximum adjustment factor (4).
That's correct.
Simulating the money supply/subsidy decrease over time is a little harder, since it only occurs every 210,000 blocks.
Given the hashing difficulty will keep increasing until it takes about 10 seconds to mine each block, I would have to mine this for about 24 days to see the subsidy change.
For the sake of testing, I will change the SUBSIDY_HALVING_INTERVAL
to occur every 210 blocks, and deploy a new version of the application.
We can see that the reward for the first block mined is 5000000000 EMUSHIS:
Block 209 has the same reward, and then block 210 has half the reward:
And checking one more halving:
So that seems to be working.
Full Source Code & Mining Instructions
The full source code is available on GitHub.
To mine EMU on the Algorand TestNet:
Clone this repository
Change directory into the repo, or open it in VSCode and create a new terminal
Run
poetry install
Create a TestNet account and fund it using the dispenser
Set the account mnemonic in an environment variable called ACCOUNT_MNEMONIC (see AlgoKit utils)
Run
python -m emu chain
in the terminal. You should see the latest block printed.Run
python -m emu mine
to start mining!
The complete smart contract code is below:
from puyapy import (
Account,
ARC4Contract,
Asset,
BigUInt,
Bytes,
Global,
Txn,
UInt64,
arc4,
gtxn,
itxn,
log,
op,
subroutine,
)
DECIMALS = 8
# https://github.com/bitcoin/bitcoin/blob/master/src/consensus/amount.h
# /** The amount of satoshis in one BTC. */
# static constexpr CAmount COIN = 100000000;
COIN = 100_000_000 # 10**DECIMALS
# static constexpr CAmount MAX_MONEY = 21000000 * COIN;
# inline bool MoneyRange(const CAmount& nValue) { return (nValue >= 0 && nValue <= MAX_MONEY); }
MAX_MONEY = 2_100_000_000_000_000 # 21_000_000 * COIN
GENESIS_BLOCK_HEIGHT = 0
# https://github.com/bitcoin/bitcoin/blob/bdddf364c9a6f80e3bfcf45ab1ae54f9eab5811b/src/kernel/chainparams.cpp#L29
# consensus.hashGenesisBlock == uint256S("0x000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")
GENESIS_BLOCK_HASH = 0x000000000019D6689C085AE165831E934FF763AE46A2A6C172B3F1B60A8CE26F
# https://github.com/bitcoin/bitcoin/blob/45b2a91897ca8f4a3e0c1adcfb30cf570971da4e/src/kernel/chainparams.cpp#L77
# consensus.nSubsidyHalvingInterval = 210000
SUBSIDY_HALVING_INTERVAL = 210_000
# CAmount nSubsidy = 50 * COIN;
SUBSIDY = 50 * COIN
# consensus.powLimit = uint256S("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
# https://github.com/bitcoin/bitcoin/blob/c265aad5b52bf7b1b1e3cc38d04812caa001ba76/src/kernel/chainparams.cpp#L89
# Note: We are raising the target to make it easier to mine blocks.
POW_LIMIT = 0x3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
# Spanning 1 hour
POW_TARGET_TIMESPAN = 60 * 60
# Aim for 1 block every 10 seconds
POW_TARGET_SPACING = 10
SMALLEST_TIMESPAN = POW_TARGET_TIMESPAN // 4
LARGEST_TIMESPAN = POW_TARGET_TIMESPAN * 4
# Adjust the difficulty every 360 blocks
DIFFICULTY_ADJUSTMENT_INTERVAL = POW_TARGET_TIMESPAN // POW_TARGET_SPACING
@subroutine
def least(a: UInt64, b: UInt64) -> UInt64:
"""Return the lesser of two values.
Args:
a (UInt64): The first number.
b (UInt64): The second number.
Returns:
UInt64: The lesser of `a` and `b`.
"""
return a if a < b else b
@subroutine
def greatest(a: UInt64, b: UInt64) -> UInt64:
"""Return the greater of two values.
Args:
a (UInt64): The first number.
b (UInt64): The second number.
Returns:
UInt64: The greater of `a` and `b`.
"""
return a if a > b else b
@subroutine
def clamp(value: UInt64, lower_bound: UInt64, upper_bound: UInt64) -> UInt64:
"""Clamp a value to the range [lower_bound, upper_bound].
Args:
value (UInt64): The value to clamp.
lower_bound (UInt64): The lower bound.
upper_bound (UInt64): The upper bound.
Returns:
UInt64: The clamped value.
"""
return least(greatest(value, lower_bound), upper_bound)
@subroutine
def transfer_asset(receiver: Account, asset: Asset, amount: UInt64) -> Bytes:
"""Submits an inner transaction to transfer an asset.
Args:
receiver (Account): The receiver of the asset.
asset (Asset): The asset to transfer.
amount (UInt64): The amount to transfer.
Returns:
Bytes: The transaction ID.
"""
return (
itxn.AssetTransfer(
xfer_asset=asset,
asset_amount=amount,
asset_receiver=receiver,
fee=0,
)
.submit()
.txn_id
)
class Emulator(ARC4Contract):
def __init__(self) -> None:
self.creator = Txn.sender
self.asa = Asset(0)
self.block_height = UInt64(GENESIS_BLOCK_HEIGHT)
self.block_hash = arc4.UInt256(GENESIS_BLOCK_HASH)
self.coinbase = Bytes()
self.prev_retarget_time = UInt64(0)
self.time = UInt64(0)
self.target = BigUInt(POW_LIMIT)
@subroutine
def is_bootstrapped(self) -> bool:
"""Check if the application has been bootstrapped.
The __bool__() method of puyapy.Asset returns True if the asset_id is not 0.
Reference: https://algorandfoundation.github.io/puya/api-puyapy.html#puyapy.Asset
Returns:
bool: True if the application has been bootstrapped, else False.
"""
return bool(self.asa)
@arc4.abimethod()
def bootstrap(self, seed: gtxn.PaymentTransaction) -> UInt64:
"""Bootstrap the application.
This method is idempotent.
It creates the asset and opts the contract account into the asset.
Args:
seed (gtxn.PaymentTransaction): Initial payment transaction to the app account.
Returns:
UInt64: The ID of the asset created.
"""
assert not self.is_bootstrapped(), "Application has already been bootstrapped"
assert Txn.sender == self.creator, "Only the creator may call this method"
assert seed.receiver == Global.current_application_address, "Receiver must be app address"
assert Global.group_size == 2, "Group size must be 2"
assert seed.amount >= 300_000, "Amount must be >= 3 Algos" # 0.3 Algos
self.asa = self.create_asset()
self.prev_retarget_time = self.time = Global.latest_timestamp
return self.asa.asset_id
@subroutine
def should_adjust_difficulty(self) -> arc4.Bool:
"""Checks whether the difficulty target should be adjusted.
Returns:
arc4.Bool: True if the difficulty target should be adjusted, False otherwise.
"""
return arc4.Bool(self.block_height % DIFFICULTY_ADJUSTMENT_INTERVAL == 0)
@subroutine
def calculate_next_work_required(self) -> BigUInt:
"""Calculate the next POW target.
Returns:
BigUInt: The new target.
"""
timespan = clamp(
value=self.time - self.prev_retarget_time,
lower_bound=UInt64(SMALLEST_TIMESPAN),
upper_bound=UInt64(LARGEST_TIMESPAN),
)
new_target = self.target * (timespan * 1000 // POW_TARGET_TIMESPAN) // 1000
return new_target if new_target < POW_LIMIT else BigUInt(POW_LIMIT)
@subroutine
def get_block_subsidy(self) -> UInt64:
"""Get block subsidy (mining reward).
Returns:
UInt64: The subsidy amount.
"""
halvings = self.block_height // SUBSIDY_HALVING_INTERVAL
# Force block reward to zero when right shift is undefined.
return UInt64(0) if halvings >= 64 else SUBSIDY >> halvings
@subroutine
def create_asset(self) -> Asset:
"""Submits an inner transaction to create the asset.
Returns:
Asset: The asset created.
"""
return (
itxn.AssetConfig(
asset_name=b"EMU",
unit_name=b"EMUSHIS",
total=MAX_MONEY,
decimals=DECIMALS,
manager=Global.current_application_address,
reserve=Global.current_application_address,
fee=0,
)
.submit()
.created_asset
)
@subroutine
def reward_miner(self) -> UInt64:
"""Reward the miner.
Returns:
UInt64: The reward amount.
"""
return (
itxn.AssetTransfer(
xfer_asset=self.asa,
asset_amount=self.get_block_subsidy(),
asset_receiver=Txn.sender,
fee=0,
)
.submit()
.asset_amount
)
@arc4.abimethod()
def mine(self, nonce: arc4.UInt64, coinbase: Bytes) -> UInt64:
"""Mine a block.
Args:
nonce (arc4.UInt64): The nonce.
coinbase (Bytes): The coinbase.
Returns:
UInt64: The amount rewarded to the miner.
"""
assert self.is_bootstrapped(), "Application not bootstrapped"
proposed_block_hash = BigUInt.from_bytes(op.sha256(op.sha256(self.block_hash.bytes + nonce.bytes + coinbase)))
assert proposed_block_hash < self.target, "Invalid block"
current_time = Global.latest_timestamp
self.block_height += 1
self.block_hash = arc4.UInt256(proposed_block_hash)
self.coinbase = coinbase # Store this in state just for fun
self.time = current_time
if self.should_adjust_difficulty():
next_work = self.calculate_next_work_required()
log(
"retargeted_at",
current_time,
"prev_retarget_time",
self.prev_retarget_time,
"prev_target",
self.target,
"new_target",
next_work,
sep=b"\x1F",
)
self.prev_retarget_time = current_time
self.target = next_work
reward = self.reward_miner()
log(
"miner",
Txn.sender.bytes,
"reward",
reward,
sep=b"\x1F",
)
return reward