Documentation
¶
Overview ¶
Package fruit implements ACE's reward-smoothing layer.
A fruit is a block attempt that missed. A miner assembles a template, grinds nonces against it, and publishes whatever the search turns up: a solution under the block target becomes a block, and a solution merely under the fruit target - by default 512 times easier - becomes a fruit. Fruits are gossiped, harvested by whichever block comes next, and paid directly from the coinbase.
This is deliberately the same object a mining pool already calls a "share". The difference is that ACE pays for it on-chain, so the miner never has to hand their hash rate to an operator to convert near misses into income. A solo GPU is paid on the cadence of its shares rather than the cadence of its blocks, which for a small miner is the difference between hours and decades.
The construction follows FruitChains (Pass and Shi, PODC 2017): decoupling what earns reward from what extends the chain leaves the security argument intact while making the reward stream phi-fair over a short horizon rather than only in the limit.
Index ¶
- Constants
- Variables
- func CheckSetShape(fruits []*Fruit, maxFruits uint32) error
- func CheckSolution(cfg acehash.Config, tbl *acehash.Table, epoch uint32, f *Fruit, ...) error
- func FruitCount(rp *RewardParams, anchorTarget, blockTarget *big.Int) uint32
- func FruitTarget(blockTarget *big.Int, divisor uint32) *big.Int
- func MaxFruits(fruits uint32) uint32
- func MaxMinted(rp *RewardParams, fruitCount uint32, subsidy int64) int64
- func MerkleRoot(fruits []*Fruit) chainhash.Hash
- func OptimalLotteryShare(fruitsPerBlock uint32) uint32
- func Slot(keyHash [20]byte, interval int32) int32
- func SlotForHeight(height int32, interval int32) int32
- func TotalPaid(payouts []Payout) int64
- func VarianceReduction(rp *RewardParams, fruitCount uint32, feeFraction float64) float64
- type Credit
- type Fruit
- type Payout
- type RewardParams
Constants ¶
const SerializedSize = chainhash.HashSize + 4 + chainhash.HashSize + acehash.SolutionSize
SerializedSize is the fixed wire size of a fruit: the parent it hangs from, the difficulty it was mined at, an opaque commitment to the miner's template, and the ACEHash solution.
Variables ¶
var ( ErrBadSerialization = errors.New("fruit: malformed serialization") ErrDuplicate = errors.New("fruit: duplicate fruit in set") ErrTooManyFruits = errors.New("fruit: set exceeds the per-block maximum") )
Errors returned by sanity checking.
var ( ErrBadRewardParams = errors.New("fruit: invalid reward parameters") ErrNegativeValue = errors.New("fruit: negative subsidy or fee total") )
Errors returned by reward accounting.
var MainNetRewardParams = RewardParams{
FruitShareBP: 9900,
HarvestShareBP: 60,
BlockShareBP: 40,
FeeFinderBP: 2000,
FruitsMin: 512,
FruitsMax: 1024,
FreshnessWindow: 60,
SweepInterval: 240,
MaxPayoutsPerBlock: 4096,
}
MainNetRewardParams are the production values.
One percent of the subsidy is left block-contingent. The strict variance optimum for F = 512 is 1/513, or about 0.195 percent, but the variance surface is extremely flat just above its minimum: one percent still achieves 496x of the 513x theoretical maximum, while paying the block finder five times the bare minimum for the work of assembling and propagating a block. Buying 3 percent of the variance reduction back as block-production incentive is a good trade.
Of that one percent, 60 basis points is the harvest bonus - the reason a miner wants somebody else's fruits in their block - and 40 pays for the block itself.
Fees are the harder half. The finder keeps 20 percent, which is ample incentive to include a transaction, and the other 80 percent is spread across the block's harvested fruits. Without that split, fees are undiluted lottery and they dominate the result the moment they are any real share of revenue: at 10 percent of revenue an unsmoothed fee stream drags 496x down to 74x, and at half of revenue to 3.9x. Smoothing four fifths of them holds the same two points at 372x and 80x. This is not a distant post-halving concern; it is one busy mempool away.
var RegressionNetRewardParams = SimNetRewardParams
RegressionNetRewardParams match SimNet.
var SimNetRewardParams = RewardParams{
FruitShareBP: 8900,
HarvestShareBP: 600,
BlockShareBP: 500,
FeeFinderBP: 2000,
FruitsMin: 8,
FruitsMax: 32,
FreshnessWindow: 10,
SweepInterval: 4,
MaxPayoutsPerBlock: 256,
}
SimNetRewardParams shrink the fruit count and the rota so a single machine can drive a full harvest and payout cycle in a test.
var TestNetRewardParams = MainNetRewardParams
TestNetRewardParams mirror mainnet so testnet exercises the same economics.
Functions ¶
func CheckSetShape ¶
CheckSetShape validates the structural rules on a harvested fruit set: it must respect the per-block cap and contain no repeats.
Duplicate rejection matters for more than tidiness. Without it a block could pad itself with the same fruit many times over and mint the per-fruit subsidy repeatedly for a single proof of work.
func CheckSolution ¶
func CheckSolution(cfg acehash.Config, tbl *acehash.Table, epoch uint32, f *Fruit, fruitTarget *big.Int) error
CheckSolution performs the context-free half of fruit validation: that the ACEHash proof is well formed and below the fruit target.
The context-dependent half - that PrevBlock is a recent ancestor, that Bits is the value consensus mandates at that height, and that the fruit has not already been harvested - needs a chain view and lives in the blockchain package.
func FruitCount ¶
func FruitCount(rp *RewardParams, anchorTarget, blockTarget *big.Int) uint32
FruitCount returns F for a block whose target is blockTarget.
F is the factor by which the fruit target is easier than the block target, and therefore also the number of fruits the network produces per block interval. It is *not* a constant, and that is a deliberate correction to the obvious design.
Holding F fixed silently caps how large a network ACE can serve. A miner's income smoothness depends on how many fruits *they* find per day, which is F/G for a network of G machines - so a fixed F means the payout cadence decays in proportion to adoption, and the fairness property quietly evaporates exactly when the chain succeeds. Pinning the fruit target to an absolute amount of work instead makes F rise with difficulty, holding one machine's payout cadence roughly constant however large the network gets.
The band is bounded at both ends. Below FruitsMin the fruit set is too small for the variance argument to mean anything; above FruitsMax the per-block fruit payload becomes the dominant cost of running a node. FruitsMax is therefore an honest ceiling on the network size ACE serves well, and it is stated as one rather than hidden in a constant.
func FruitTarget ¶
FruitTarget scales a block target into the corresponding fruit target.
There is only one difficulty control loop in ACE. The fruit target is derived from the block target rather than retargeted separately, so the two can never drift apart and a miner cannot game one against the other.
On regression networks the block target is already near the maximum and the scaled value can exceed 256 bits; callers treat such a target as saturated, which makes every attempt a fruit. That is the intended behaviour for a network with no real difficulty.
func MaxFruits ¶
MaxFruits returns the hard cap on how many fruits one block may harvest when F fruits are nominal.
It is twice the nominal count. Fruit arrivals are Poisson and block intervals are exponential, so a cap equal to the mean would leave the harvest queue critically loaded and it would grow without bound. Twice the mean drains any realistic backlog well inside the freshness window.
func MaxMinted ¶
func MaxMinted(rp *RewardParams, fruitCount uint32, subsidy int64) int64
MaxMinted returns the largest subsidy a single block may create: the amount a completely full harvest would mint.
It exists so that consensus can keep a cheap upper bound on coinbase value ahead of the exact check, the way Bitcoin does with subsidy plus fees. ACE's bound is looser than Bitcoin's by construction - roughly twice the nominal subsidy - because the per-block cap was what destroyed a third of issuance.
func MerkleRoot ¶
MerkleRoot computes the commitment to an ordered fruit set.
The element count is folded into the root alongside the tree, which closes the duplicate-tail malleability that lets an attacker forge a second preimage for an odd-sized Bitcoin merkle tree (CVE-2012-2459). Consensus also rejects duplicate fruits outright, so this is belt and braces.
func OptimalLotteryShare ¶
OptimalLotteryShare returns the fraction of the subsidy that should remain block-contingent, in basis points, to minimise a miner's income variance for a given fruit count.
Write b for the block-contingent share, so 1-b rides on fruits. Over one block interval a miner with hash-rate share phi finds Poisson(phi) blocks and Poisson(phi*F) fruits, and the two are independent, so
Var[income] / phi = b^2 + F * ((1-b)/F)^2 = b^2 + (1-b)^2/F
Differentiating and setting to zero gives
b = 1/(F+1)
which has a pleasing reading: the block's flat reward should be worth exactly one fruit, because a block *is* one proof among F. Substituting back, the variance at the optimum is 1/(F+1) of the all-on-blocks case, so the best achievable reduction is F+1 - the full fruit count, not some fraction of it.
This matters because the intuitive choice - "put most of it on fruits and leave a comfortable slice for the block" - is wrong in an expensive direction. A ten percent block share caps the reduction at 100x no matter how large F is. ACE's first cut made exactly that mistake and cmd/acesim measured it: 86x instead of the 512x the fruit count promised.
func Slot ¶
Slot returns the payout slot a key hash belongs to.
The key hash is already a uniformly distributed digest, so its leading bytes are as good a slot selector as anything and need no further mixing. Because the slot is a function of the key alone, a miner cannot choose when they are paid by choosing what to mine - only by choosing a different key, which costs them the accrual on the old one.
func SlotForHeight ¶
SlotForHeight returns the slot a block at the given height pays.
func VarianceReduction ¶
func VarianceReduction(rp *RewardParams, fruitCount uint32, feeFraction float64) float64
VarianceReduction reports how much smaller a miner's income variance is under these parameters than it would be with the whole subsidy riding on blocks.
feeFraction is the share of gross block revenue that arrives as transaction fees rather than subsidy. It belongs in this calculation because it is the single largest threat to the result: fees are collected by whoever found the block, so the part of them the finder keeps is pure lottery. At zero fees the answer is bounded above by F+1. As fees grow it decays, and how fast it decays is entirely a function of FeeFinderBP.
Types ¶
type Credit ¶
type Credit struct {
// Fruits is F, the nominal fruit count in force for this block.
Fruits uint32
// Credited is n: the number of proofs paid, which is the harvested
// fruits plus the block's own solution.
Credited int
// PerFruit is the subsidy each credited proof earns.
PerFruit int64
// PerFruitFee is the fee share each credited proof earns.
PerFruitFee int64
// PerHarvest is the amount the block finder earns per credited proof.
PerHarvest int64
// BlockBase is the flat block reward.
BlockBase int64
// FeeFinder is the share of fees the finder keeps.
FeeFinder int64
// FeeBurned is the fee revenue destroyed because the block harvested
// fewer than F fruits. Burning rather than reassigning it is what stops
// fees from paying a finder to censor: a thin harvest costs the finder
// their harvest bonus and earns them nothing in exchange.
FeeBurned int64
// Minted is the subsidy actually created. Unlike Bitcoin's it is not
// bounded by the nominal subsidy for a single block: a block harvesting a
// backlog mints proportionally more, and one harvesting a thin set mints
// proportionally less. The schedule is the long-run mean rather than a
// per-block ceiling, which is what stops the concavity of a per-block cap
// from quietly destroying a third of all issuance. See ComputeCredit.
Minted int64
// FinderKey is the payout key the block's own solution proves.
FinderKey [20]byte
// PayoutsBurned is accrued credit the per-block payout cap discarded. It
// is zero in honest operation; see MaxPayoutsPerBlock.
PayoutsBurned int64
// Immediate is what FinderKey is paid by this block: the flat block
// share, one harvest bonus per credited proof, and the finder's fee
// share. Their own fruit credit is not here; it defers like everyone
// else's.
Immediate int64
}
Credit is the complete reward accounting for one block.
It separates what the block pays *now* from what it merely *owes*. The finder is paid immediately, because they need the fees and because they are already receiving an output. Fruit credits are owed, and land in the coinbase of a later block under the payout rota - see Payouts.
func ComputeCredit ¶
func ComputeCredit(rp *RewardParams, fruitCount uint32, subsidy, fees int64, blockKey [20]byte, fruits []*Fruit) (*Credit, error)
ComputeCredit performs the reward accounting for a block.
fruits are the explicitly harvested fruits. blockKey is the payout key hash of the block's own ACEHash solution: that solution is below the block target and therefore also below the fruit target, so it is credited as an implicit fruit. The work that found the block is paid exactly once, on the same terms as everybody else's, with nothing wasted and nothing double counted. It is implicit rather than carried in the fruit set because it is already derivable from the header, and 170 bytes of redundant data is 170 bytes of consensus surface.
Two divisors appear, and the difference between them is load bearing.
The subsidy is divided by F unconditionally. An earlier version divided by max(F, n) so that no single block could ever mint above the schedule, which reads like prudence and is not: min(n, F) is concave, block intervals are exponential, and a concave function of a dispersed variable loses to its own average. Simulation put the loss at 31 % of all issuance - a supply cap of 145 million against a nominal 210 million, and a security budget a third smaller than the schedule advertises - converging on 1 - 1/e as the harvest cap is relaxed. Dividing by F always makes the schedule the long-run mean: a block harvesting a backlog mints up to twice the nominal subsidy, one harvesting a thin set mints less, and the two cancel.
Fees are divided by max(F, n), because fees are collected rather than minted and cannot be paid out twice. The shortfall when n < F is burned.
func (*Credit) Deferred ¶
Deferred returns the full map of credits this block owes, aggregated by key.
func (*Credit) EachDeferred ¶
EachDeferred calls fn once for every deferred credit this block creates: one per harvested fruit, plus one for the block's own implicit fruit.
A key that appears more than once is reported more than once; callers aggregate. This is the single definition of who is owed what, so that the validation path and the mining path cannot drift apart.
type Fruit ¶
type Fruit struct {
// PrevBlock is the tip the miner was extending. Consensus requires it
// to be an ancestor of the harvesting block within the freshness window.
PrevBlock chainhash.Hash
// Bits is the compact difficulty that was in force for a block at
// PrevBlock's height plus one.
Bits uint32
// InnerCommit is acehash.InnerCommit over the miner's template.
InnerCommit chainhash.Hash
// Solution is the ACEHash proof.
Solution acehash.Solution
}
Fruit is a proof of work below the fruit target but not necessarily below the block target.
It carries exactly the fields a verifier needs and nothing more. PrevBlock establishes freshness and places the fruit on a specific chain; Bits pins the difficulty so a miner cannot quietly solve against a trivial target; InnerCommit binds the attempt to one template without revealing it. The miner's unpublished transaction selection stays private, which matters because a fruit is by definition a block they did not get to publish.
func (*Fruit) Deserialize ¶
Deserialize reads a fruit written by Serialize.
func (*Fruit) Hash ¶
Hash returns the fruit's identity, used for gossip, deduplication and the fruit merkle tree.
func (*Fruit) PayoutKeyHash ¶
PayoutKeyHash returns the key hash this fruit pays. It is derived from the very public key the puzzle forced the solver to hold the secret for, so the payout cannot be redirected without redoing the work.
func (*Fruit) PoWMessage ¶
PoWMessage returns the message this fruit's solution was mined against.
type Payout ¶
Payout is a single coinbase destination.
func Payouts ¶
func Payouts(rp *RewardParams, tieSalt *chainhash.Hash, accrued map[[20]byte]int64, finderKey [20]byte, immediate int64) ( payouts []Payout, burned int64)
Payouts assembles the exact coinbase payout list for a block, and reports how much accrued credit the per-block cap discarded.
accrued holds every deferred credit due in this block's slot, summed over the blocks in (height-K, height] - which is precisely the credits created since the last block that paid this slot, so nothing is paid twice and nothing is skipped. immediate is what the finder is owed by this block directly.
At most MaxPayoutsPerBlock outputs are produced. The finder is never dropped: they are owed this block's fees and are receiving an output regardless. Among everyone else, the largest accruals survive and the smallest are burned, for the reason set out on MaxPayoutsPerBlock - the cap exists to defeat slot grinding, and grinding produces small accruals.
The returned list is aggregated by key hash and sorted by it, so a miner owed many credits receives one output and every node derives byte-identical ordering. Note that the *selection* order is by amount and the *output* order is by key hash; conflating the two would make the coinbase depend on map iteration.
type RewardParams ¶
type RewardParams struct {
// basis points.
FruitShareBP uint32
// per harvested fruit, in basis points.
HarvestShareBP uint32
// finder, in basis points.
BlockShareBP uint32
// FeeFinderBP is the share of a block's transaction fees the finder
// keeps immediately. The remainder is spread across the block's
// harvested fruits, which is what keeps fee income from re-introducing
// the very variance the subsidy split removes.
//
// It must be strictly positive. A finder who earned nothing from a fee
// would have no reason to include the transaction that paid it.
FeeFinderBP uint32
// FruitsMin and FruitsMax bound F, the number of fruits a block
// nominally harvests. F is not a constant: it tracks difficulty, so
// that the rate at which one machine is paid stays roughly fixed as the
// network grows. See FruitCount.
//
// FruitsMax is set by a block-size budget, not by taste. A block must hold
// the fruit payload at 170 bytes each, up to MaxPayoutsPerBlock coinbase
// outputs at 31 bytes each, and enough room left over to be a useful
// transaction ledger. It also has to hold a payout cap comfortably above
// MaxFruits(FruitsMax)+1, because that many credits can fall due in one slot
// with no adversary present. At FruitsMax = 2048 those constraints have no
// solution: the payload alone is 696 KB of the 1 MB base limit and the cap it
// implies is another 508 KB. 1024 leaves about half the block for
// transactions, and the price is that the fruit band spans 2x rather than 4x
// - a tighter ceiling on the network size ACE serves well, stated in the
// whitepaper's limitations rather than discovered later.
FruitsMin uint32
FruitsMax uint32
// FreshnessWindow is R, the number of blocks a fruit remains harvestable
// after the block it hangs from. It bounds how far back validation must
// look for duplicates, and it bounds how long a miner can hoard.
FreshnessWindow int32
// MaxPayoutsPerBlock caps how many coinbase outputs one block may be
// required to produce.
//
// Without a cap the required payout set is unbounded, and that is
// exploitable rather than merely untidy. A payout key's rota slot is
// derived from the key hash, so a miner chooses their slot by grinding
// keys - about 240 tries, a few milliseconds - and a fresh key per fruit
// costs them nothing, because credits are a flat amount per proof and are
// aggregated per key with no rounding and no minimum. Enough ground keys
// aimed at one slot and the mandatory payout list no longer fits inside a
// block, at which point no valid block exists at that height and the chain
// stops there permanently.
//
// The cap alone is not the fix; what matters is which payouts survive it.
// Selection is by descending amount, so the credits dropped are the
// smallest - which is exactly what grinding many keys produces.
//
// Ordering by amount is not sufficient on its own, and an earlier version of
// this comment claimed otherwise. It asserted that displacing an honest
// miner required the attacker to hold as much as their victim, "paying full
// price for the mined work". That was wrong: the attacker's keys *survive*
// the cap, so they are paid in full and forfeit nothing. With every credit
// worth the same flat amount, ties in amount are the common case, and an
// attacker who broke ties by ascending key hash simply ground keys with
// leading zero bytes and won every one - measured at 100 % of the attacker's
// credits paid and 0 % of the honest miners' in the same slot.
//
// Ties are therefore broken on H(keyHash || the settling block's parent
// hash), which every validator can compute and no miner can grind: the
// parent is not known when the fruits are mined, and changing a key after
// the fact means re-mining every proof under it. With ties settled
// unpredictably, an attacker crowding a slot loses their own credits at the
// same rate they destroy anyone else's, so the attack costs what it inflicts.
//
// Overflow is burned rather than carried forward, because carrying it needs
// an unbounded lookback to know what is still unsettled - the stored state
// the rota exists to avoid.
//
// It must be large enough never to bind in honest operation: with K slots
// and an active key count well under K*MaxPayoutsPerBlock, a slot holds
// roughly activeKeys/K keys, which is orders of magnitude below the cap.
MaxPayoutsPerBlock uint32
// SweepInterval is K, the period of the payout rota. A fruit credit is
// not paid by the block that harvests it; it is paid by the next block
// whose height selects the miner's slot, at most K blocks later.
//
// This exists purely to stop the chain drowning in unspent outputs.
// Paying F fruits per block directly would create F outputs per block -
// at mainnet's parameters, more unspent outputs in one year than Bitcoin
// has created in its entire history. Deferring by a rota divides that
// by K while changing nobody's revenue, only its granularity.
SweepInterval int32
}
RewardParams describes how a block's revenue is divided.
The subsidy shares must sum to one hundred percent. Their sizes are the entire economic argument of ACE, so they are worth spelling out:
FruitShareBP goes to the miners of harvested fruits, at a *fixed* amount per fruit rather than as a split of a pot. Fixing it is what removes any incentive to censor a competitor's fruit: excluding one does not enrich anybody, it simply fails to create the coins.
HarvestShareBP goes to the block finder, again per fruit harvested. This is the countervailing incentive that makes inclusion strictly profitable. It is minted separately rather than skimmed off the fruit's payout, which matters: if it were a skim, a miner would gain by hoarding their own fruits until one of their own blocks could harvest them. Minting it separately makes harvesting your own fruit worth exactly as much as harvesting a stranger's, so hoarding is pure downside.
BlockShareBP goes to the block finder flat, paying for template construction and propagation.
FeeFinderBP is separate because fees are not minted and their total is not known in advance. See ComputeCredit for what happens to the rest of them.
Expected revenue is unaffected by the split. A miner with hash rate share phi earns phi of the fruit pot plus phi of the block pot, which is phi of the subsidy however the two are weighted. What the split changes is *variance*: the fruit share arrives F times more often than the block share. Pushing FruitShareBP up moves income from the lottery into the salary.
func (*RewardParams) Validate ¶
func (rp *RewardParams) Validate() error
Validate reports whether the parameters are self-consistent.