acehash

package
v0.0.0-...-3bf5e7c Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: ISC Imports: 15 Imported by: 0

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

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

View Source
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.

View Source
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.

View Source
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

func BelowTarget(d *[32]byte, target *big.Int) bool

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

func CommitmentRoot(txRoot, fruitRoot *chainhash.Hash) chainhash.Hash

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

func Indices(cfg Config, msg *chainhash.Hash, sol *Solution, out *[K]uint32)

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

func InnerCommit(version int32, merkleRoot *chainhash.Hash,
	timestamp uint32) chainhash.Hash

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

func PoWMessage(prevBlock *chainhash.Hash, bits uint32,
	inner *chainhash.Hash) chainhash.Hash

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

func Sum(cfg Config, t *Table, epoch uint32, idx *[K]uint32, out *secp.ModNScalar)

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

func TargetBytes(target *big.Int) [32]byte

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

func (c *Config) DatasetBytes() uint64

DatasetBytes reports how much memory a full table for this configuration occupies.

func (*Config) Element

func (c *Config) Element(epoch, i uint32, dst []byte)

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.

func (*Config) Epoch

func (c *Config) Epoch(height int32) uint32

Epoch maps a block height to the dataset epoch that height mines against. Negative heights clamp to epoch zero so that callers do not have to special case the genesis block.

func (*Config) Validate

func (c *Config) Validate() error

Validate returns an error if the configuration is not usable.

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

func (s *Searcher) OneTime() [33]byte

OneTime returns the compressed one-time key this searcher solves for.

func (*Searcher) PubKey

func (s *Searcher) PubKey() [33]byte

PubKey returns the compressed payout key this searcher solves for.

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.

func (*Searcher) Solve

func (s *Searcher) Solve(target *[32]byte, start, count uint32,
	quit <-chan struct{}) *Solution

Solve is a convenience wrapper that returns the first solution below target, or nil if the nonce range is exhausted.

type Solution

type Solution struct {
	Nonce   uint32
	PubKey  [33]byte
	OneTime [33]byte
	D       [32]byte
}

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

func (s *Solution) Hash() chainhash.Hash

Hash returns a commitment to the solution, used to bind it into the block header without enlarging the header.

func (*Solution) PayoutKeyHash

func (s *Solution) PayoutKeyHash() [20]byte

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.

func (*Solution) SetBytes

func (s *Solution) SetBytes(b []byte) error

SetBytes parses the canonical serialisation produced by Bytes.

func (*Solution) String

func (s *Solution) String() string

String renders the solution as hex for logs and test failures.

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

func (*Table) Bytes

func (t *Table) Bytes() int

Bytes reports the size of the table in memory.

func (*Table) Config

func (t *Table) Config() Config

Config returns the configuration this table was built with.

func (*Table) Epoch

func (t *Table) Epoch() uint32

Epoch returns the epoch this table was built for.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL