Documentation
¶
Overview ¶
Package acehash implements ACEHash, the proof-of-work function of the ACE network.
ACEHash deliberately borrows from two generations of Ergo's Autolykos family rather than inventing new cryptography:
The *dataset* is Autolykos v2 shaped: a large table of scalars derived from nothing but an epoch number, where regenerating a single element costs a hash over an 8 KiB pad. Storing the table is roughly seventy times cheaper than recomputing it, so honest miners hold it in DRAM and the achievable hash rate is pinned to memory bandwidth instead of to transistor count. A verifier that does not want to hold two gibibytes recomputes only the K elements a given solution touches.
The *equation* is Autolykos v1 shaped: the predicate that decides whether an attempt is a solution cannot be evaluated without the miner's secret key. This is what makes the puzzle non-outsourceable. Ergo abandoned v1 because pools formed anyway once the demand for variance reduction outweighed the friction; ACE reinstates it because the fruit layer removes that demand at the source. See doc/WHITEPAPER.md §4.
The inner mining loop contains no elliptic curve operations at all: it is a BLAKE2b compression, K dependent random reads, K scalar additions, one scalar multiply and one scalar subtract. Every one of those maps cleanly onto a GPU. The curve arithmetic appears only in verification.
Index ¶
- Constants
- Variables
- func BelowTarget(d *[32]byte, target *big.Int) bool
- func CommitmentRoot(txRoot, fruitRoot *chainhash.Hash) chainhash.Hash
- func Indices(cfg Config, msg *chainhash.Hash, sol *Solution, out *[K]uint32)
- func InnerCommit(version int32, merkleRoot *chainhash.Hash, timestamp uint32) chainhash.Hash
- func PoWMessage(prevBlock *chainhash.Hash, bits uint32, inner *chainhash.Hash) chainhash.Hash
- func Sum(cfg Config, t *Table, epoch uint32, idx *[K]uint32, out *secp.ModNScalar)
- func TargetBytes(target *big.Int) [32]byte
- func Verify(cfg Config, t *Table, epoch uint32, msg *chainhash.Hash, sol *Solution, ...) error
- type Config
- type Searcher
- func (s *Searcher) OneTime() [33]byte
- func (s *Searcher) PubKey() [33]byte
- func (s *Searcher) Search(target *[32]byte, start, count uint32, found func(*Solution) bool, ...) (attempts uint32, stopped bool)
- func (s *Searcher) Solve(target *[32]byte, start, count uint32, quit <-chan struct{}) *Solution
- type Solution
- type Table
- type TableCache
Constants ¶
const ( // K is the number of dataset elements summed by a single attempt. Each // read depends on the message and the miner's keys, so the reads cannot // be prefetched and the loop is latency bound. K = 32 // ElementSize is the number of bytes each dataset element occupies. A // element is a 31 byte big-endian integer stored right-aligned in 32 // bytes; 31 bytes guarantees the value is below the secp256k1 group // order without a rejection loop. ElementSize = 32 )
const SolutionSize = 4 + 33 + 33 + 32
SolutionSize is the wire size of a serialised solution: a 32 bit nonce, two compressed points and one scalar.
Variables ¶
var ( ErrBadPubKey = errors.New("acehash: solution public key is not a valid curve point") ErrBadOneTime = errors.New("acehash: solution one-time key is not a valid curve point") ErrScalarRange = errors.New("acehash: d is not a canonical scalar below the group order") ErrHighHash = errors.New("acehash: d is not below the target") ErrDegenerateSum = errors.New("acehash: element sum is zero") ErrEquation = errors.New("acehash: solution does not satisfy d*G + P = f*W") ErrEpochMismatch = errors.New("acehash: table epoch does not match the requested epoch") // ErrConfigMismatch is returned when a dataset was built for a different // configuration than the one verification is using. Their element counts // differ, so an index masked by one would read out of range in the other - // a panic inside block validation rather than a rejection. ErrConfigMismatch = errors.New("acehash: dataset was built for a different configuration") )
Errors returned by Verify. They are distinct values so that consensus code can decide which ones justify banning a peer.
var ( // MainNetConfig uses a 2 GiB dataset rotating every 2048 blocks, which // is about 34 hours at the one minute target spacing. MainNetConfig = Config{N: 1 << 26, EpochLength: 2048} // TestNetConfig uses a 128 MiB dataset so that a laptop can participate // in testnet without a discrete GPU. TestNetConfig = Config{N: 1 << 22, EpochLength: 2048} // SimNetConfig uses a 2 MiB dataset for fast deterministic tests. SimNetConfig = Config{N: 1 << 16, EpochLength: 512} // RegressionNetConfig matches SimNet. RegressionNetConfig = Config{N: 1 << 16, EpochLength: 512} )
Predefined configurations. MainNetConfig is sized so that the dataset does not fit in any plausible on-die SRAM budget but does fit in the memory of every discrete GPU sold since roughly 2016.
var Pad = func() [padSize]byte { var m [padSize]byte for i := 0; i < padSize/8; i++ { binary.BigEndian.PutUint64(m[i*8:], uint64(i)) } return m }()
Pad is the static 8 KiB array M mixed into every element derivation. It is simply the first 1024 non-negative integers as big-endian uint64s, matching Autolykos v2 so the construction can be cross-checked against an existing production implementation.
Functions ¶
func BelowTarget ¶
BelowTarget reports whether a solution's d is strictly below the given target. Miners use it to test one solution against a second, harder target without redoing the search.
func CommitmentRoot ¶
CommitmentRoot returns the value stored in the block header's MerkleRoot field. It binds both the transaction set and the harvested fruit set into a single header field.
func Indices ¶
Indices derives the K dataset indices touched by an attempt.
The derivation follows Autolykos v2: one BLAKE2b-256 over the seed, extended by its own first three bytes, then a sliding four byte window. Producing K indices from a single compression keeps the non-memory work per attempt negligible, which is the point - we want the loop bound by DRAM, not by the hash.
func InnerCommit ¶
InnerCommit binds the parts of a block that the proof-of-work must fix but that a fruit does not need to reveal.
A fruit publishes only (prevBlock, bits, innerCommit). That is enough to prove the work was done against a recent tip at the correct difficulty, and it keeps the miner's unpublished template private, which is the whole point: a fruit is a block attempt that missed, and the attempt's contents are the miner's business.
func PoWMessage ¶
PoWMessage returns the message m that ACEHash is evaluated over.
bits is exposed rather than folded into innerCommit so that a verifier holding only a fruit can check the fruit was mined at the difficulty in force at its parent's height. Without that, a miner could quietly solve fruits against a trivial target.
func Sum ¶
Sum accumulates the K selected dataset elements into a scalar modulo the group order.
A nil table selects the light path, which recomputes each element from the pad. That is roughly seventy times slower per element than a DRAM read, which is exactly the ratio that makes holding the table worthwhile for a miner and irrelevant for a verifier touching only K of them.
func TargetBytes ¶
TargetBytes renders a target as the fixed width big-endian form used by the mining loop. Targets at or above 2^256 saturate, which happens only on regression networks where the fruit target is the block target scaled up.
func Verify ¶
func Verify(cfg Config, t *Table, epoch uint32, msg *chainhash.Hash, sol *Solution, target *big.Int) error
Verify checks a solution against a message and target.
t may be nil, in which case the K touched elements are recomputed. Passing a table for the wrong epoch is a programming error and is reported rather than silently producing a wrong answer.
Types ¶
type Config ¶
type Config struct {
// N is the number of elements in the dataset and must be a power of
// two. The dataset occupies N*ElementSize bytes.
N uint32
// EpochLength is the number of blocks a dataset is valid for. Shorter
// epochs rotate the table more often, which costs miners a rebuild;
// longer epochs amortise the rebuild but give a hypothetical attacker
// more time to bake a table into silicon.
EpochLength int32
}
Config captures the tunable dimensions of ACEHash. Every network picks one; they are consensus critical.
func (*Config) DatasetBytes ¶
DatasetBytes reports how much memory a full table for this configuration occupies.
func (*Config) Element ¶
Element derives dataset element i for the given epoch without consulting a table. This is the light verification path and the inner loop of table construction.
The returned slice is ElementSize bytes with the top byte always zero.
type Searcher ¶
type Searcher struct {
// contains filtered or unexported fields
}
Searcher grinds nonces for a single (message, payout key, one-time key) triple.
The expensive state - the dataset, the derived public points, and the negated secret - is computed once here so the inner loop does nothing but hash, read memory, and do two scalar operations. There are no elliptic curve operations in the loop. That is deliberate: it is what makes the puzzle implementable as a GPU kernel with the memory subsystem as the only bottleneck.
func NewSearcher ¶
func NewSearcher(cfg Config, t *Table, epoch uint32, msg *chainhash.Hash, sk *secp.ModNScalar, x *secp.ModNScalar) (*Searcher, error)
NewSearcher prepares a grinding context.
sk is the miner's payout secret; the reward for anything found here is spendable only by whoever knows it. x is the one-time secret and must be fresh for each search: reusing an x across two different messages leaks sk, exactly as reusing a nonce does in ECDSA. Pass nil to have one generated.
func (*Searcher) Search ¶
func (s *Searcher) Search(target *[32]byte, start, count uint32, found func(*Solution) bool, quit <-chan struct{}) (attempts uint32, stopped bool)
Search grinds count nonces starting at start, invoking found for every attempt whose d falls below target.
Callers pass the *easier* of their two targets. A block-difficulty solution is by construction also below the fruit target, so a single pass over the nonce space produces both kinds of proof with no wasted work - the two-for-one property that lets a miner be paid for near misses without ever splitting hash rate between two searches.
found returns false to stop the search. attempts is the number of nonces actually tried.
type Solution ¶
Solution is an ACEHash proof.
The tuple is (n, pk, w, d). It attests that whoever produced it knew the discrete logs of both pk and w, because d is only computable as
d = f*x - sk (mod q)
where x = dlog(w), sk = dlog(pk), and f is the sum of K dataset elements selected by hashing the message together with n, pk and w. A verifier checks the rearranged form d*G + pk = f*w, which needs no secrets.
The consequence that matters: the *predicate* d < target cannot be evaluated without sk. A miner cannot filter candidates on behalf of somebody else's payout key, and an operator who hands out sk has handed out the ability to spend the reward.
func (*Solution) Bytes ¶
func (s *Solution) Bytes() [SolutionSize]byte
Bytes serialises the solution into its canonical 102 byte form.
func (*Solution) Hash ¶
Hash returns a commitment to the solution, used to bind it into the block header without enlarging the header.
func (*Solution) PayoutKeyHash ¶
PayoutKeyHash returns hash160(pk), the identity that a fruit or block pays. Binding the payout to the very key the puzzle requires is what gives the scheme its non-outsourceability; they cannot be separated.
type Table ¶
type Table struct {
// contains filtered or unexported fields
}
Table is a fully materialised ACEHash dataset for a single epoch.
Miners hold one. Verifiers generally do not: Sum falls back to recomputing the touched elements when handed a nil table, which costs K passes over the 8 KiB pad instead of K random reads. A full node therefore needs no special hardware, while a miner without the table pays a roughly seventyfold penalty. That asymmetry is the entire memory-hardness argument.
func BuildTable ¶
func BuildTable(ctx context.Context, cfg Config, epoch uint32, workers int, progress func(done, total uint32)) (*Table, error)
BuildTable materialises the dataset for the given epoch.
workers <= 0 selects GOMAXPROCS. progress, if non-nil, is called periodically with the number of elements completed; it must be cheap and safe to call from multiple goroutines. The build aborts promptly if ctx is cancelled, in which case the returned error is ctx.Err().
type TableCache ¶
type TableCache struct {
// contains filtered or unexported fields
}
TableCache keeps the most recent tables alive so that an epoch rollover does not stall mining, and so a node that reorgs across an epoch boundary does not have to rebuild.
It holds at most two tables: the current epoch and the one before it. Two is enough because a reorg deep enough to cross two epoch boundaries would be a far larger problem than a table rebuild.
func NewTableCache ¶
func NewTableCache(cfg Config, workers int) *TableCache
NewTableCache returns a cache that builds tables for cfg using the given number of worker goroutines (<= 0 selects GOMAXPROCS).
func (*TableCache) Get ¶
func (c *TableCache) Get(ctx context.Context, epoch uint32, progress func(done, total uint32)) (*Table, error)
Get returns the table for epoch, building it if necessary. Concurrent callers requesting the same epoch share a single build.
func (*TableCache) Peek ¶
func (c *TableCache) Peek(epoch uint32) *Table
Peek returns the cached table for epoch without building one, or nil.