120 lines
4.2 KiB

package applications
import (
ristretto "github.com/gtank/ristretto255"
// ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability
type ProofOfWorkApplication struct {
// transcript constants
const (
PoWApp = "pow-app"
PoWSeed = "pow-seed"
PoWChallenge = "pow-challenge"
PoWPRNG = "pow-prng"
PoWSolution = "pow-solution"
// SuccessfulProofOfWorkCapability is given when a successfully PoW Challenge has been Completed
const SuccessfulProofOfWorkCapability = tapir.Capability("SuccessfulProofOfWorkCapability")
// NewInstance should always return a new instantiation of the application.
func (powapp *ProofOfWorkApplication) NewInstance() tapir.Application {
return new(ProofOfWorkApplication)
// Init is run when the connection is first started.
func (powapp *ProofOfWorkApplication) Init(connection tapir.Connection) {
if connection.IsOutbound() {
powapp.Transcript().AddToTranscript(PoWSeed, connection.Expect())
solution := powapp.solveChallenge(powapp.Transcript().CommitToTranscript(PoWChallenge), powapp.transcript.CommitToPRNG(PoWPRNG))
powapp.transcript.AddToTranscript(PoWSolution, solution)
connection.SetCapability(SuccessfulProofOfWorkCapability) // We can self grant.because the server will close the connection on failure
// We may be the first application, in which case we need to randomize the transcript challenge
// We use the random hostname of the inbound server (if we've authenticated them then the challenge will
// already be sufficiently randomized, so this doesn't hurt)
// It does sadly mean an additional round trip.
powapp.Transcript().AddToTranscript(PoWSeed, []byte(connection.Hostname()))
solution := connection.Expect()
challenge := powapp.Transcript().CommitToTranscript(PoWChallenge)
// soft-commitment to the prng, doesn't force the client to use it (but we could technically check that it did, not necessary for the security of this App)
powapp.transcript.AddToTranscript(PoWSolution, solution)
if powapp.validateChallenge(challenge, solution) {
// SolveChallenge takes in a challenge and a message and returns a solution
// The solution is a 24 byte nonce which when hashed with the challenge and the message
// produces a sha256 hash with Difficulty leading 0s
func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core.PRNG) []byte {
solved := false
var sum [32]byte
solution := []byte{}
solve := make([]byte, len(challenge)+32)
// reuse our allocation
buf := make([]byte, 64)
next := new(ristretto.Scalar)
encodedSolution := make([]byte, 0, 32)
for !solved {
err := prng.Next(buf, next)
if err != nil {
// this will cause the challenge to fail...
log.Errorf("error completing challenge: %v", err)
return nil
//lint:ignore SA1019 API this is "deprecated", but without it it will cause an allocation on every single check
solution = next.Encode(encodedSolution)
copy(solve[0:], solution[:])
copy(solve[len(solution):], challenge[:])
sum = sha256.Sum256(solve)
solved = true
for i := 0; i < 2; i++ {
if sum[i] != 0x00 {
solved = false
// reuse this allocated memory next time...
encodedSolution = encodedSolution[:0]
log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
return solution[:]
// ValidateChallenge returns true if the message and spamguard pass the challenge
func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool {
if len(solution) != 32 {
return false
solve := make([]byte, len(challenge)+32)
copy(solve[0:], solution[0:32])
copy(solve[32:], challenge[:])
sum := sha256.Sum256(solve)
for i := 0; i < 2; i++ {
if sum[i] != 0x00 {
return false
log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
return true