pow

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: May 2, 2025 License: MIT Imports: 9 Imported by: 8

README

go-pow

GoDoc Go Report Card lint test codecov

A library that implements a proof-of-work system with customizable challenges.

Features

  • use of patterns:
  • definition of challenges with the following fields:
    • leading zero bit count — the required number of leading zero bits in the resulting hash;
    • target bit index — a bit index used in determining the solution’s difficulty:
      • it refers to the bit position such that the resulting hash will be less than the number where that bit is set (for example, with a 256-bit hash and a requirement of 6 leading zeros, we would set the 250th bit);
      • only one of leading zero bit count or target bit index should be set explicitly — the other is derived from it;
    • created at (optional) — the timestamp when the challenge was created;
    • TTL (optional) — the duration after which the challenge expires:
      • created at and TTL must either be both specified or both omitted;
    • resource (optional) — the resource associated with the challenge, typically for scoping:
    • serialized payload — the raw data to be included in the hash:
      • must be pre-serialized to a string; the library does not handle serialization itself;
    • hash — the hash function used to verify the solution:
    • hash data layout — the structure of the data used during hashing:
      • based on the text/template.Template type;
      • defines which fields of the challenge will be hashed and in what order, giving full control over the hash input structure;
  • generation of solutions that meet specified challenge criteria:
    • starting nonce value:
      • it can be zero;
      • it can be randomly selected within a given range;
    • the generation process can be interrupted via:
      • context cancellation;
      • an attempt limit;
  • validation of solutions against their corresponding challenges;
  • sentinel errors provided through a dedicated errors subpackage.

Installation

$ go get github.com/thewizardplusplus/go-pow

Examples

Minimal (also see in the playground: https://go.dev/play/p/wsUGURDKFvb):

package main

import (
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"strconv"

	pow "github.com/thewizardplusplus/go-pow"
	powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)

func main() {
	leadingZeroBitCount, err := powValueTypes.NewLeadingZeroBitCount(5)
	if err != nil {
		log.Fatalf("unable to construct the leading zero bit count: %s", err)
	}

	namedHash, err := powValueTypes.NewHashWithName(sha256.New(), "SHA-256")
	if err != nil {
		log.Fatalf("unable to construct the hash: %s", err)
	}

	challenge, err := pow.NewChallengeBuilder().
		SetLeadingZeroBitCount(leadingZeroBitCount).
		SetSerializedPayload(powValueTypes.NewSerializedPayload("dummy")).
		SetHash(namedHash).
		SetHashDataLayout(powValueTypes.MustParseHashDataLayout(
			"{{ .Challenge.LeadingZeroBitCount.ToInt }}" +
				":{{ .Challenge.SerializedPayload.ToString }}" +
				":{{ .Nonce.ToString }}",
		)).
		Build()
	if err != nil {
		log.Fatalf("unable to build the challenge: %s", err)
	}

	fmt.Printf("challenge: %v\n", []string{
		strconv.Itoa(challenge.LeadingZeroBitCount().ToInt()),
		challenge.SerializedPayload().ToString(),
		challenge.Hash().Name(),
		challenge.HashDataLayout().ToString(),
	})

	solution, err := challenge.Solve(context.Background(), pow.SolveParams{})
	if err != nil {
		log.Fatalf("unable to solve the challenge: %s", err)
	}

	fmt.Printf("nonce: %s\n", solution.Nonce().ToString())
	fmt.Printf("hash sum: %x\n", solution.HashSum().OrEmpty().ToBytes())

	if err := solution.Verify(); err != nil {
		log.Fatalf("unable to verify the solution: %s", err)
	}

	fmt.Print("verification: OK\n")

	// Output:
	// challenge: [5 dummy SHA-256 {{.Challenge.LeadingZeroBitCount.ToInt}}:{{.Challenge.SerializedPayload.ToString}}:{{.Nonce.ToString}}]
	// nonce: 37
	// hash sum: 005d372c56e6c6b52ad4a8325654692ec9aa3af5f73021748bc3fdb124ae9b20
	// verification: OK
}

Full (also see in the playground: https://go.dev/play/p/_3kPX0VtHFA):

package main

import (
	"bytes"
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"math/big"
	"net/url"
	"strconv"
	"time"

	"github.com/samber/mo"
	pow "github.com/thewizardplusplus/go-pow"
	powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)

func main() {
	leadingZeroBitCount, err := powValueTypes.NewLeadingZeroBitCount(5)
	if err != nil {
		log.Fatalf("unable to construct the leading zero bit count: %s", err)
	}

	rawCreatedAt := time.Date(2000, time.January, 2, 3, 4, 5, 6, time.UTC)
	createdAt, err := powValueTypes.NewCreatedAt(rawCreatedAt)
	if err != nil {
		log.Fatalf("unable to parse the `CreatedAt` timestamp: %s", err)
	}

	ttl, err := powValueTypes.NewTTL(100 * 365 * 24 * time.Hour)
	if err != nil {
		log.Fatalf("unable to parse the TTL: %s", err)
	}

	namedHash, err := powValueTypes.NewHashWithName(sha256.New(), "SHA-256")
	if err != nil {
		log.Fatalf("unable to construct the hash: %s", err)
	}

	challenge, err := pow.NewChallengeBuilder().
		SetLeadingZeroBitCount(leadingZeroBitCount).
		SetCreatedAt(createdAt).
		SetTTL(ttl).
		SetResource(powValueTypes.NewResource(&url.URL{
			Scheme: "https",
			Host:   "example.com",
			Path:   "/",
		})).
		SetSerializedPayload(powValueTypes.NewSerializedPayload("dummy")).
		SetHash(namedHash).
		SetHashDataLayout(powValueTypes.MustParseHashDataLayout(
			"{{ .Challenge.LeadingZeroBitCount.ToInt }}" +
				":{{ .Challenge.CreatedAt.MustGet.ToString }}" +
				":{{ .Challenge.TTL.MustGet.ToString }}" +
				":{{ .Challenge.Resource.MustGet.ToString }}" +
				":{{ .Challenge.SerializedPayload.ToString }}" +
				":{{ .Challenge.Hash.Name }}" +
				":{{ .Challenge.HashDataLayout.ToString }}" +
				":{{ .Nonce.ToString }}",
		)).
		Build()
	if err != nil {
		log.Fatalf("unable to build the challenge: %s", err)
	}
	if !challenge.IsAlive() {
		log.Fatal("challenge is outdated")
	}

	fmt.Printf("challenge: %v\n", []string{
		strconv.Itoa(challenge.LeadingZeroBitCount().ToInt()),
		challenge.CreatedAt().MustGet().ToString(),
		challenge.TTL().MustGet().ToString(),
		challenge.Resource().MustGet().ToString(),
		challenge.SerializedPayload().ToString(),
		challenge.Hash().Name(),
		challenge.HashDataLayout().ToString(),
	})

	solution, err := challenge.Solve(context.Background(), pow.SolveParams{
		RandomInitialNonceParams: mo.Some(powValueTypes.RandomNonceParams{
			// use `crypto/rand.Reader` in a production
			RandomReader: bytes.NewReader([]byte("dummy")),
			MinRawValue:  big.NewInt(123),
			MaxRawValue:  big.NewInt(142),
		}),
	})
	if err != nil {
		log.Fatalf("unable to solve the challenge: %s", err)
	}

	fmt.Printf("nonce: %s\n", solution.Nonce().ToString())
	fmt.Printf("hash sum: %x\n", solution.HashSum().OrEmpty().ToBytes())

	if err := solution.Verify(); err != nil {
		log.Fatalf("unable to verify the solution: %s", err)
	}

	fmt.Print("verification: OK\n")

	// Output:
	// challenge: [5 2000-01-02T03:04:05.000000006Z 876000h0m0s https://example.com/ dummy SHA-256 {{.Challenge.LeadingZeroBitCount.ToInt}}:{{.Challenge.CreatedAt.MustGet.ToString}}:{{.Challenge.TTL.MustGet.ToString}}:{{.Challenge.Resource.MustGet.ToString}}:{{.Challenge.SerializedPayload.ToString}}:{{.Challenge.Hash.Name}}:{{.Challenge.HashDataLayout.ToString}}:{{.Nonce.ToString}}]
	// nonce: 136
	// hash sum: 060056e78e0b90e48c765d4f64c0f63d5926e28a56f3cd229bdc78225f91cd51
	// verification: OK
}

With interruption (also see in the playground: https://go.dev/play/p/xT2G05H8VqN):

package main

import (
	"context"
	"crypto/sha256"
	"errors"
	"fmt"
	"log"
	"strconv"

	"github.com/samber/mo"
	pow "github.com/thewizardplusplus/go-pow"
	powErrors "github.com/thewizardplusplus/go-pow/errors"
	powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)

func main() {
	tooBigLeadingZeroBitCount, err := powValueTypes.NewLeadingZeroBitCount(100)
	if err != nil {
		log.Fatalf("unable to construct the leading zero bit count: %s", err)
	}

	namedHash, err := powValueTypes.NewHashWithName(sha256.New(), "SHA-256")
	if err != nil {
		log.Fatalf("unable to construct the hash: %s", err)
	}

	challenge, err := pow.NewChallengeBuilder().
		SetLeadingZeroBitCount(tooBigLeadingZeroBitCount).
		SetSerializedPayload(powValueTypes.NewSerializedPayload("dummy")).
		SetHash(namedHash).
		SetHashDataLayout(powValueTypes.MustParseHashDataLayout(
			"{{ .Challenge.LeadingZeroBitCount.ToInt }}" +
				":{{ .Challenge.SerializedPayload.ToString }}" +
				":{{ .Nonce.ToString }}",
		)).
		Build()
	if err != nil {
		log.Fatalf("unable to build the challenge: %s", err)
	}

	fmt.Printf("challenge: %v\n", []string{
		strconv.Itoa(challenge.LeadingZeroBitCount().ToInt()),
		challenge.SerializedPayload().ToString(),
		challenge.Hash().Name(),
		challenge.HashDataLayout().ToString(),
	})

	_, solvingErr := challenge.Solve(context.Background(), pow.SolveParams{
		MaxAttemptCount: mo.Some(1000),
	})
	if solvingErr == nil {
		log.Fatal("solving must fail")
	}
	if !errors.Is(solvingErr, powErrors.ErrTaskInterruption) {
		log.Fatalf("unexpected solving error: %s", solvingErr)
	}

	fmt.Print("solving: interrupted\n")

	// Output:
	// challenge: [100 dummy SHA-256 {{.Challenge.LeadingZeroBitCount.ToInt}}:{{.Challenge.SerializedPayload.ToString}}:{{.Nonce.ToString}}]
	// solving: interrupted
}

License

The MIT License (MIT)

Copyright © 2025 thewizardplusplus

Documentation

Overview

Example (Full)
package main

import (
	"bytes"
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"math/big"
	"net/url"
	"strconv"
	"time"

	"github.com/samber/mo"
	pow "github.com/thewizardplusplus/go-pow"

	powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)

func main() {
	leadingZeroBitCount, err := powValueTypes.NewLeadingZeroBitCount(5)
	if err != nil {
		log.Fatalf("unable to construct the leading zero bit count: %s", err)
	}

	rawCreatedAt := time.Date(2000, time.January, 2, 3, 4, 5, 6, time.UTC)
	createdAt, err := powValueTypes.NewCreatedAt(rawCreatedAt)
	if err != nil {
		log.Fatalf("unable to parse the `CreatedAt` timestamp: %s", err)
	}

	ttl, err := powValueTypes.NewTTL(100 * 365 * 24 * time.Hour)
	if err != nil {
		log.Fatalf("unable to parse the TTL: %s", err)
	}

	namedHash, err := powValueTypes.NewHashWithName(sha256.New(), "SHA-256")
	if err != nil {
		log.Fatalf("unable to construct the hash: %s", err)
	}

	challenge, err := pow.NewChallengeBuilder().
		SetLeadingZeroBitCount(leadingZeroBitCount).
		SetCreatedAt(createdAt).
		SetTTL(ttl).
		SetResource(powValueTypes.NewResource(&url.URL{
			Scheme: "https",
			Host:   "example.com",
			Path:   "/",
		})).
		SetSerializedPayload(powValueTypes.NewSerializedPayload("dummy")).
		SetHash(namedHash).
		SetHashDataLayout(powValueTypes.MustParseHashDataLayout(
			"{{ .Challenge.LeadingZeroBitCount.ToInt }}" +
				":{{ .Challenge.CreatedAt.MustGet.ToString }}" +
				":{{ .Challenge.TTL.MustGet.ToString }}" +
				":{{ .Challenge.Resource.MustGet.ToString }}" +
				":{{ .Challenge.SerializedPayload.ToString }}" +
				":{{ .Challenge.Hash.Name }}" +
				":{{ .Challenge.HashDataLayout.ToString }}" +
				":{{ .Nonce.ToString }}",
		)).
		Build()
	if err != nil {
		log.Fatalf("unable to build the challenge: %s", err)
	}
	if !challenge.IsAlive() {
		log.Fatal("challenge is outdated")
	}

	fmt.Printf("challenge: %v\n", []string{
		strconv.Itoa(challenge.LeadingZeroBitCount().ToInt()),
		challenge.CreatedAt().MustGet().ToString(),
		challenge.TTL().MustGet().ToString(),
		challenge.Resource().MustGet().ToString(),
		challenge.SerializedPayload().ToString(),
		challenge.Hash().Name(),
		challenge.HashDataLayout().ToString(),
	})

	solution, err := challenge.Solve(context.Background(), pow.SolveParams{
		RandomInitialNonceParams: mo.Some(powValueTypes.RandomNonceParams{
			// use `crypto/rand.Reader` in a production
			RandomReader: bytes.NewReader([]byte("dummy")),
			MinRawValue:  big.NewInt(123),
			MaxRawValue:  big.NewInt(142),
		}),
	})
	if err != nil {
		log.Fatalf("unable to solve the challenge: %s", err)
	}

	fmt.Printf("nonce: %s\n", solution.Nonce().ToString())
	fmt.Printf("hash sum: %x\n", solution.HashSum().OrEmpty().ToBytes())

	if err := solution.Verify(); err != nil {
		log.Fatalf("unable to verify the solution: %s", err)
	}

	fmt.Print("verification: OK\n")

}
Output:

challenge: [5 2000-01-02T03:04:05.000000006Z 876000h0m0s https://example.com/ dummy SHA-256 {{.Challenge.LeadingZeroBitCount.ToInt}}:{{.Challenge.CreatedAt.MustGet.ToString}}:{{.Challenge.TTL.MustGet.ToString}}:{{.Challenge.Resource.MustGet.ToString}}:{{.Challenge.SerializedPayload.ToString}}:{{.Challenge.Hash.Name}}:{{.Challenge.HashDataLayout.ToString}}:{{.Nonce.ToString}}]
nonce: 136
hash sum: 060056e78e0b90e48c765d4f64c0f63d5926e28a56f3cd229bdc78225f91cd51
verification: OK
Example (Minimal)
package main

import (
	"context"
	"crypto/sha256"
	"fmt"
	"log"
	"strconv"

	pow "github.com/thewizardplusplus/go-pow"

	powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)

func main() {
	leadingZeroBitCount, err := powValueTypes.NewLeadingZeroBitCount(5)
	if err != nil {
		log.Fatalf("unable to construct the leading zero bit count: %s", err)
	}

	namedHash, err := powValueTypes.NewHashWithName(sha256.New(), "SHA-256")
	if err != nil {
		log.Fatalf("unable to construct the hash: %s", err)
	}

	challenge, err := pow.NewChallengeBuilder().
		SetLeadingZeroBitCount(leadingZeroBitCount).
		SetSerializedPayload(powValueTypes.NewSerializedPayload("dummy")).
		SetHash(namedHash).
		SetHashDataLayout(powValueTypes.MustParseHashDataLayout(
			"{{ .Challenge.LeadingZeroBitCount.ToInt }}" +
				":{{ .Challenge.SerializedPayload.ToString }}" +
				":{{ .Nonce.ToString }}",
		)).
		Build()
	if err != nil {
		log.Fatalf("unable to build the challenge: %s", err)
	}

	fmt.Printf("challenge: %v\n", []string{
		strconv.Itoa(challenge.LeadingZeroBitCount().ToInt()),
		challenge.SerializedPayload().ToString(),
		challenge.Hash().Name(),
		challenge.HashDataLayout().ToString(),
	})

	solution, err := challenge.Solve(context.Background(), pow.SolveParams{})
	if err != nil {
		log.Fatalf("unable to solve the challenge: %s", err)
	}

	fmt.Printf("nonce: %s\n", solution.Nonce().ToString())
	fmt.Printf("hash sum: %x\n", solution.HashSum().OrEmpty().ToBytes())

	if err := solution.Verify(); err != nil {
		log.Fatalf("unable to verify the solution: %s", err)
	}

	fmt.Print("verification: OK\n")

}
Output:

challenge: [5 dummy SHA-256 {{.Challenge.LeadingZeroBitCount.ToInt}}:{{.Challenge.SerializedPayload.ToString}}:{{.Nonce.ToString}}]
nonce: 37
hash sum: 005d372c56e6c6b52ad4a8325654692ec9aa3af5f73021748bc3fdb124ae9b20
verification: OK
Example (WithInterruption)
package main

import (
	"context"
	"crypto/sha256"
	"errors"
	"fmt"
	"log"
	"strconv"

	"github.com/samber/mo"
	pow "github.com/thewizardplusplus/go-pow"

	powErrors "github.com/thewizardplusplus/go-pow/errors"

	powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)

func main() {
	tooBigLeadingZeroBitCount, err := powValueTypes.NewLeadingZeroBitCount(100)
	if err != nil {
		log.Fatalf("unable to construct the leading zero bit count: %s", err)
	}

	namedHash, err := powValueTypes.NewHashWithName(sha256.New(), "SHA-256")
	if err != nil {
		log.Fatalf("unable to construct the hash: %s", err)
	}

	challenge, err := pow.NewChallengeBuilder().
		SetLeadingZeroBitCount(tooBigLeadingZeroBitCount).
		SetSerializedPayload(powValueTypes.NewSerializedPayload("dummy")).
		SetHash(namedHash).
		SetHashDataLayout(powValueTypes.MustParseHashDataLayout(
			"{{ .Challenge.LeadingZeroBitCount.ToInt }}" +
				":{{ .Challenge.SerializedPayload.ToString }}" +
				":{{ .Nonce.ToString }}",
		)).
		Build()
	if err != nil {
		log.Fatalf("unable to build the challenge: %s", err)
	}

	fmt.Printf("challenge: %v\n", []string{
		strconv.Itoa(challenge.LeadingZeroBitCount().ToInt()),
		challenge.SerializedPayload().ToString(),
		challenge.Hash().Name(),
		challenge.HashDataLayout().ToString(),
	})

	_, solvingErr := challenge.Solve(context.Background(), pow.SolveParams{
		MaxAttemptCount: mo.Some(1000),
	})
	if solvingErr == nil {
		log.Fatal("solving must fail")
	}
	if !errors.Is(solvingErr, powErrors.ErrTaskInterruption) {
		log.Fatalf("unexpected solving error: %s", solvingErr)
	}

	fmt.Print("solving: interrupted\n")

}
Output:

challenge: [100 dummy SHA-256 {{.Challenge.LeadingZeroBitCount.ToInt}}:{{.Challenge.SerializedPayload.ToString}}:{{.Nonce.ToString}}]
solving: interrupted

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Challenge

type Challenge struct {
	// contains filtered or unexported fields
}

func (Challenge) CreatedAt

func (entity Challenge) CreatedAt() mo.Option[powValueTypes.CreatedAt]

func (Challenge) Hash

func (entity Challenge) Hash() powValueTypes.Hash

func (Challenge) HashDataLayout

func (entity Challenge) HashDataLayout() powValueTypes.HashDataLayout

func (Challenge) IsAlive

func (entity Challenge) IsAlive() bool

func (Challenge) LeadingZeroBitCount

func (entity Challenge) LeadingZeroBitCount() powValueTypes.LeadingZeroBitCount

func (Challenge) Resource

func (entity Challenge) Resource() mo.Option[powValueTypes.Resource]

func (Challenge) SerializedPayload

func (entity Challenge) SerializedPayload() powValueTypes.SerializedPayload

func (Challenge) Solve

func (entity Challenge) Solve(
	ctx context.Context,
	params SolveParams,
) (Solution, error)

func (Challenge) TTL

func (entity Challenge) TTL() mo.Option[powValueTypes.TTL]

func (Challenge) TargetBitIndex

func (entity Challenge) TargetBitIndex() (powValueTypes.TargetBitIndex, error)

type ChallengeBuilder

type ChallengeBuilder struct {
	// contains filtered or unexported fields
}

func NewChallengeBuilder

func NewChallengeBuilder() *ChallengeBuilder

func (ChallengeBuilder) Build

func (builder ChallengeBuilder) Build() (Challenge, error)

func (*ChallengeBuilder) SetCreatedAt

func (builder *ChallengeBuilder) SetCreatedAt(
	value powValueTypes.CreatedAt,
) *ChallengeBuilder

func (*ChallengeBuilder) SetHash

func (builder *ChallengeBuilder) SetHash(
	value powValueTypes.Hash,
) *ChallengeBuilder

func (*ChallengeBuilder) SetHashDataLayout

func (builder *ChallengeBuilder) SetHashDataLayout(
	value powValueTypes.HashDataLayout,
) *ChallengeBuilder

func (*ChallengeBuilder) SetLeadingZeroBitCount

func (builder *ChallengeBuilder) SetLeadingZeroBitCount(
	value powValueTypes.LeadingZeroBitCount,
) *ChallengeBuilder

func (*ChallengeBuilder) SetResource

func (builder *ChallengeBuilder) SetResource(
	value powValueTypes.Resource,
) *ChallengeBuilder

func (*ChallengeBuilder) SetSerializedPayload

func (builder *ChallengeBuilder) SetSerializedPayload(
	value powValueTypes.SerializedPayload,
) *ChallengeBuilder

func (*ChallengeBuilder) SetTTL

func (builder *ChallengeBuilder) SetTTL(
	value powValueTypes.TTL,
) *ChallengeBuilder

func (*ChallengeBuilder) SetTargetBitIndex

func (builder *ChallengeBuilder) SetTargetBitIndex(
	value powValueTypes.TargetBitIndex,
) *ChallengeBuilder

type ChallengeHashData

type ChallengeHashData struct {
	Challenge Challenge
	Nonce     powValueTypes.Nonce
}

type Solution

type Solution struct {
	// contains filtered or unexported fields
}

func (Solution) Challenge

func (entity Solution) Challenge() Challenge

func (Solution) HashSum

func (entity Solution) HashSum() mo.Option[powValueTypes.HashSum]

func (Solution) Nonce

func (entity Solution) Nonce() powValueTypes.Nonce

func (Solution) Verify

func (entity Solution) Verify() error

type SolutionBuilder

type SolutionBuilder struct {
	// contains filtered or unexported fields
}

func NewSolutionBuilder

func NewSolutionBuilder() *SolutionBuilder

func (SolutionBuilder) Build

func (builder SolutionBuilder) Build() (Solution, error)

func (*SolutionBuilder) SetChallenge

func (builder *SolutionBuilder) SetChallenge(value Challenge) *SolutionBuilder

func (*SolutionBuilder) SetHashSum

func (builder *SolutionBuilder) SetHashSum(
	value powValueTypes.HashSum,
) *SolutionBuilder

func (*SolutionBuilder) SetNonce

func (builder *SolutionBuilder) SetNonce(
	value powValueTypes.Nonce,
) *SolutionBuilder

type SolveParams

type SolveParams struct {
	MaxAttemptCount          mo.Option[int]
	RandomInitialNonceParams mo.Option[powValueTypes.RandomNonceParams]
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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