Skip to content

Operator Wiki

pk910 edited this page Jun 8, 2024 · 31 revisions

This operator wiki describes the v2 version of the faucet.

Installing the faucet

Use Release with local NodeJS

You can download packaged releases of the faucet from the Releases Page.

You need a machine with NodeJS >= 14 installed.

Download powfaucet-server-all.tar.gz from the latest release and run the following commands to start the faucet:

tar -xzf powfaucet-server-all.tar.gz
./run-faucet.sh

For windows, download & extract powfaucet-server-all.zip from the latest release and run run-faucet.bat.

After doing the above commands, you should be able to access the faucet via http://localhost:8080

The faucet server process needs to run all the time, so I suggest running it in a screen session or systemd daemon.

When the faucet is started the first time, it creates a copy of the example configuration at faucet-config.yaml. Some settings are individualized by random values for your security (faucetSecret, ethWalletKey). Read through the configuration file to customize the faucet for your needs.

The configuration is also described more detailed in this wiki.

Use all-in-one executable from Release

There are also distribution specific all-in-one executables available for windows and linux. These executables are basically standalone nodejs binaries with the faucet scripts and the static folder baked in. There is no local NodeJS installation required to run these executables.
This works fine for testing environments, but I recommend using a locally installed NodeJs for better performance.

Build from source

To build the faucet from source, you need a machine with git & NodeJS >= 20 installed. The OS doesn't really matter. I'm testing on debian & windows, but I'm pretty sure others work too.

Clone the repository, install dependencies & build the faucet:

git clone https://github.com/pk910/PoWFaucet.git
cd PoWFaucet
npm install
cd faucet-client
npm install
node ./build-client.js
cd ..
npm run start

After doing the above commands, you should be able to access the faucet via http://localhost:8080

The faucet server process needs to run all the time, so I suggest running it in a screen session or systemd daemon.

When the faucet is started the first time, it creates a copy of the example configuration at faucet-config.yaml. Some settings are individualized by random values for your security (faucetSecret, ethWalletKey). Read through the configuration file to customize the faucet for your needs.

The configuration is also described more detailed in this wiki.

Use docker image

I'm maintaining a docker image for this faucet: pk910/powfaucet

There are various images available:

  • v2-stable: The latest v2.x.x release
  • v2-latest: That latest master branch version (automatically built)
  • v2.x.x: Version specific images for all v2.x.x releases.

Follow these steps to run the docker image:

  1. Create a new directory that will be used for persistent faucet data

    mkdir faucet-data
    cd faucet-data
    
  2. Create default config

    docker run -u $UID:$GID -v $(pwd):/config pk910/powfaucet:v2-stable --datadir=/config --create-config
    

    this shoould create a copy of the example config called faucet-config.yaml

  3. Edit faucet-config.yaml and prepend /config/ to database.file, faucetLogFile & faucetPidFile (ensure they're persisted outside of the container and not lost on updates)

    nano faucet-config.yaml
    

    database:
      driver: "sqlite"
      file: "/config/faucet-store.db"
    faucetLogFile: "/config/faucet-events.log"
    faucetPidFile: "/config/faucet-pid.txt"

    Also change ethRpcHost to use a more reliably RPC host (Infura, Alchemy, ...) and configure the faucet modules for your needs.

  4. Start the container

    docker run -d --restart unless-stopped --name=powfaucet -v $(pwd):/config -p 8080:8080 -it pk910/powfaucet:v2-stable --datadir=/config
    

You should now be able to access the faucet via http://localhost:8080

read logs:

docker logs powfaucet --follow

stop container:

docker rm -f powfaucet

Run it behind a webserver

For productive setups I suggest using a more complex webserver than the built in low-level static server, because it does not support ssl, caching and stuff.

To setup the faucet with a proper webserver, you just need to point the document root to the /static folder of the faucet and forward websocket (Endpoint: /pow) and api (Endpoint: /api) calls to the faucet process.

See a more detailed description and example configs for apache & nginx here: docs/webserver-setup.md

Embed faucet into a existing Website

Since v2.2.3 the powfaucet UI can be embedded as a independent component to an existing website. This allows a wide range of customization as the faucet can be styled for custom needs.

You can find a example implementation that loads the demo1-instance as a component to a jsfiddle site here: https://jsfiddle.net/pk910/nf1k9vxq

The integration is easily done with the following steps:

  1. Install the faucet and make the faucet site accessible via a public reachable URL
  2. Add faucet style to the page you want to show the faucet on
<link rel="Stylesheet" type="text/css" href="https://faucet.example.com/css/powfaucet.css">
  1. Add faucet script to the page you want to show the faucet on
<script src="https://faucet.example.com/js/powfaucet.js"></script>
  1. Add container element where the faucet gets rendered to
<div class="faucet-bootstrap" id="powfaucet"></div>
  1. Initialize & render the faucet
<script type="text/javascript">
(function() {
// get container element
var container = document.getElementById("powfaucet");

// faucet UI config
var baseUrl = "https://faucet.example.com";
var config = {
  apiUrl: baseUrl + "/api",
  wsBaseUrl: baseUrl.replace(/^http/, "ws") + "/ws",
  imagesUrl: baseUrl + "/images",
  minerSrc: {
    scrypt: baseUrl + "/js/powfaucet-worker-sc.js",
    cryptonight: baseUrl + "/js/powfaucet-worker-cn.js",
    argon2: baseUrl + "/js/powfaucet-worker-a2.js",
  }
};

// render UI
PoWFaucet.initializeFaucet(container, config);
})();
</script>

I recommend to always load the faucet script & style from the faucet process instead of including these js/css files in your site to avoid incompatible versions during faucet updates.

For security, the faucet needs to explicitly allow embedding from certain origins via the configurable cors policies:

# allow external sites embedding this faucet
corsAllowOrigin:
- "https://fiddle.jshell.net"

Reloading & Restarting the faucet

Most configuration settings can be changed without needing to restart the faucet process.

This is helpful because restarting the process leads to a a high number of session re-connects and might even result in a loss of rewards when loosing connectivity in time-critical situations (eg. a few secs before session timeout).

To reload the configuration without restarting, you need to send a SIGUSR1 signal to the faucet process.

You can use the following command in combination with the faucetPidFile configuration to reload the faucet config of a running instance:

kill -SIGUSR1 $(cat ./faucet-pid.txt)

The faucet should reload the config file and print something like this when receiving the signal: # Received SIGURS1 signal - reloading faucet config

However, all session progress is restored from the database when the server restarts. Nothing is lost, so it's not super critical to restart the faucet process when you need to do it.

Updating the faucet

To update the faucet, you just need to load the latest release and overwrite the existing files. The faucet automatically upgrades a existing database when needed.

Demo Instances

To demonstrate some scenarios with different protection methods, I've created a bunch of demo instances of the faucet.

You can find them here: https://github.com/pk910/PoWFaucet/blob/master/docs/demo/README.md

Distribute ERC20 tokens

The faucet allows distributing standard ERC20 tokens instead of native network tokens.

To enable this functionality, you need to configure the following settings:

  • faucetCoinType: Set this to 'erc20'
  • faucetCoinContract: Set this to the address of the ERC20 token contract

The faucet uses a standard ERC20 abi to transfer funds via the transfer call. The number of decimals and balances are requested via decimals / balanceOf.

Be aware that most amount-related configuration options of the faucet are based on uint values ("wei") and are set to work with the native token and 18 decimal places.
For custom tokens with a different number of decimals, you probably need to change these settings to reflect the intended values for your token.

Faucet drop amount

The default drop amount can be configured via the maxDropAmount & minDropAmount settings:

# min/max drop amount (max is the default if no module lowers it)
maxDropAmount: 1000000000000000000 # 1 ETH
minDropAmount: 10000000000000000 # 0.01 ETH

By default, the faucet drops the amount specified by maxDropAmount, but faucet modules may change the drop size according to their protection logic. The final drop size is limited by the min/max amounts specified.

Protection Mechanisms / Modules

The V2 version of this faucet has a modular protection system.

Even if the name of this project is still "PoW"-Faucet, the PoW protection is a optional part of the faucet just like other modules.

There are various modules available, that can be enabled independendly to fit your protection needs.

Module: captcha

Add captcha protection to fornt page (session start) and/or claim page (session claim)

You need to set a proper siteKey / secret from HCaptcha or Google / reCaptcha

modules:
  captcha:
    # enable / disable captcha protection
    enabled: true

    # captcha provider
    # hcaptcha:  HCaptcha (https://hcaptcha.com)
    # recaptcha: ReCAPTCHA (https://developers.google.com/recaptcha)
    provider: "hcaptcha"

    # captcha site key
    siteKey: "00000000-0000-0000-0000-000000000000"

    # captcha secret key
    secret: "0xCensoredHCaptchaSecretKey"

    # require captcha to start a new session (default: false)
    checkSessionStart: false

    # require captcha to start claim payout (default: false)
    checkBalanceClaim: false

Module: concurrency-limit

Limits the number of concurrent sessions on a per IP / per ETH Address base.

Concurrent sessions are sessions in running state. However, sessions leave the running state almost immediatly if no module introduces a long running task (eg. mining). So this module is only effective in combination with such a module.

modules:
  concurrency-limit:
    # enable / disable concurrency limit
    enabled: true
    
    concurrencyLimit: 1 # only allow 1 concurrent session (sessions in 'running' state at the same time for the same ip / target addr)
    byAddrOnly: false # only check concurrency by target address
    byIPOnly: false # only check concurrency by IP address
    messageByAddr: "" # custom error message when limit is exceeded by same target address
    messageByIP: "" # custom error message when limit is exceeded by same IP address

Module: ensname

This module allows entering a mainnet ENS name instead of a wallet address.

By default this is a optional feature. But ENS names can also be made a strict requirement and therefore be used as a protection method via the required setting.

modules:
  ensname:
    # enable / disable ENS module
    enabled: true
    
    # RPC Host for ENS name resolver (mainnet RPC)
    rpcHost: "https://rpc.flashbots.net/"
    # Custom ENS Resolver contract address
    #ensAddr: "0x"
    
    # require ENS name for sessions
    required: false

Module: ethinfo

This module checks the target wallet on the testnet the faucet is running on whenever a session is started.
There are several restrictions that can be enforced:

  • maxBalance: Max amount of funds the user is allowed to have in his wallet to be allowed to use the faucet.
  • denyContract: Deny sessions for contract addresses to avoid farming with forwarder contracts.
modules:
  ethinfo:
    # enable / disable max balance protection
    enabled: true

    # check balance and deny session if balance exceeds the limit (in wei)
    maxBalance: 50000000000000000000 # 50 ETH

    # deny sessions for contract addresses
    denyContract: true

Module: faucet-balance

This module introduces a global reward factor based on the faucet wallet balance.

It is used to lower the drop amounts when the faucet wallet gets low on funds.

modules:
  faucet-balance:
    # enable / disable faucet balance protection
    enabled: true

    # reward restriction based on faucet wallet balance. lowest value applies
    fixedRestriction:
      1000000000000: 90 # 90% if lower than 1000 ETH
      500000000000: 50  # 50% if lower than 500 ETH
      200000000000: 10  # 10% if lower than 200 ETH
      100000000000: 5   # 5% if lower than 100 ETH

    # dynamic reward restriction based on faucet balance
    # alternative to fixedRestriction, if both are set, the lower factor gets applied
    # limits the rewards linearly according to the faucet balance (100% >= targetBalance, 0% <= spareFundsAmount)
    dynamicRestriction:
      targetBalance: 1100000000000000000000 # no restriction with faucet balance > 1100 ETH

Module: faucet-outflow

This module introduces a global reward factor based on the faucet outflow.

To keep track of the outflow, the module uses an internal "outflow balance".
The outflow balance initially starts at 0 and gets increased by the configured amount (amount/duration) every second.
Whenever a session gets a reward assigned, that amount is subtracted from the outflow balance.

When the outflow balance gets negative, there are more funds requested than the outflow limit allows and the reward is reduced. The reduction is applied linearly down to 0% the closer the outflow balance gets to lowerLimit.

When the outflow balance gets positive, there are less funds requested than allowed. The positive balance is a 'buffer' for future activity spikes. However, the balance won't grow higher than upperLimit.

This logic will ensure that the average mining reward will not exceed the specified limit over long term. In short term the reward for one day might exceed the limit by the specifies boundaries, but the algorithm will then be more restrictive on the next day (negative outflow balance), so in average it will meet the limit again.

The status of the faucet-outflow module is displayed on the faucet status page, so the faucet operator can keep track of the internal outflow balance and the applied reward reduction.

modules:
  faucet-outflow:
    # enable / disable faucet outflow protection
    enabled: true

    # limit outflow to 1000ETH per day
    amount: 100000000000 # 1000 ETH
    duration: 86400 # 1 day

    # outflow balance limits
    lowerLimit: -50000000000 # -500 ETH (when outflow balance gets smaller or equal to this, the reward will get closer/equal to 0%)
    upperLimit: 50000000000 # 500 ETH (used as buffer from less-active periods for activity spikes)

Module: github

This module adds the ability to authenticate with a github account before session start.
Besides of strict barriers, the authentication can also be made optional to apply reward factors based on github profile stats.

The github module needs a github api key & secret to handle the oauth authentication flow.
To register a github application, head over to the Github Developer Settings and register a "OAuth App".
The Authorization callback URL for the faucet is {faucet-host}/api/githubCallback (replace {faucet-host} with your hostname)

Note, that the callback URL specified within the github app settings matches to subdomains as well.
So for all my faucets running under xy-faucet.pk910.de, I can use the same github app with the callback url set to https://pk910.de/api/githubCallback. That URL doesn't need to work, its just used as a pattern. When the authentication workflow is triggered from a valid subdomain, the oauth flow will automatically redirect to the correct subdomain on completion.
Also see github documentation on this.

modules:
  ## Github login protection
  github:
    # enable / disable github login protection
    enabled: true

    # github api credentials
    appClientId: "" # client id from github app
    appSecret: "" # app secret from github app

    # authentication timeout
    authTimeout: 86400

    # github account checks
    checks:
      - required: true # easiest cheack - just requires being logged in
     	
      - minAccountAge: 604800 # min account age (7 days)
        minRepoCount: 10 # min number of repositories (includes forked ones)
        minFollowers: 10 # min number of followers
        minOwnRepoCount: 5 # min number of own repos (non-forked)
        minOwnRepoStars: 5 # min number of stars on own repos (non-forked)
        required: true # require passing this check or throw error
        message: "Your github account does not meet the minimum requirements" # custom error message

      - minFollowers: 50
        minOwnRepoCount: 10
        minOwnRepoStars: 1000
        rewardFactor: 2 # double reward if account looks trustful

    # recurring restrictions based on github account
    restrictions:
      # max 10 sessions / 10 ETH per day for each github account
      - limitCount: 10
        limitAmount: 10000000000000000000 # 10 ETH
        duration: 86400 # 1 day

Module: ipinfo

This module fetches IP related information form an external API and allows defining restrictions based on that.

The API endpoint is specified via the apiUrl setting. It defaults to the free JSON api provided by ip-api.com. When using a custom API, you need to ensure the request/response formats match the format provided by ip-api.com.

The information gathered and used for the following optional restrictions look as follows:

ETH: <Wallet-Address>
IP: <IP-Address>
Ident: <Browser-Fingerprint>
Country: <Country-Code>
Region: <Region-Code>
City: <City-Name>
ISP: <ISP-Name>
Org: <Organization-Name>
AS: <AS-Number-and-Name>
Proxy: <true/false>
Hosting: <true/false>

Based on that information, several optional restrictions can be applied:

  • Basic property restrictions
    This was the first per-session restriction mechanism and easily restricts the rewards based on country or Proxy/Hosting setting.\
  • Regex-based restrictions This allows restricting the reward via regular expressions for any of the gathered information.
  • Dynamically loaded regex-based restrictions
    This is quite similar to the Regex-based mechanism, but loads the restrictions from another file. The contents from that file gets refreshed periodically, so it can be updated dynamically via an external process.
    I'm heavily using this to add additional "proprietary" protections like a abusive use from IP-Range detection and stuff on my goerli / sepolia instances. These parts are closed source because sharing their code and detection rules would prevent them from being effective.
    The format of that yaml file is as follows:
    restrictions:
    - pattern: "^IP: 1\\.2\\."
      reward: 0 # set reward to 0%
      message: "Sorry, this faucet is not intended for farmers!"
      blocked: true # kill session, but allow claiming the already collected reward
    - pattern: "^ETH: 0x0000000000000000000000000000000000001337"
      reward: 50 # set reward to 50%
      message: "I just don't like you! Reward reduced to 50%"
      notify: true # display message as notification
    - pattern: "^Org: Hostingrsdotcom"
      reward: 0
      message: "Sorry, this faucet is not intended for farmers!"
      blocked: "kill" # "kill" terminates the session without allowing to claim the collected rewards
    # ...
    

The 3 restriction classes can be configured individually and can be left out entirely if not needed. All configured restriction classes are processed in order of the list above.

modules:
  ipinfo:
    # enable / disable IP-Info protection
    enabled: true

    # ip info lookup api url (default: http://ip-api.com/json/{ip}?fields=21155839)
    apiUrl: "http://ip-api.com/json/{ip}?fields=21155839"

    # ip info caching time (stored in database)
    ipInfoCacheTime: 86400 # 1 day

    # require valid ip info (throw error and fail session if lookup failed)
    ipInfoRequired: false
    
    # Restriction Classes
    # Basic property restrictions
    ipRestrictedRewardShare:
      # percentage of drop amount if IP is in a hosting range (default: 100), 0 to block entirely
      hosting: 1

      # percentage of drop amount if IP is in a proxy range (default: 100), 0 to block entirely
      proxy: 1

      # percentage of drop amount if IP is from given country code (DE/US/...), 0 to block entirely
      #US: 100

    # Regex-based restrictions
    ipInfoMatchRestrictedReward:
      "^.*Tencent Cloud.*$": 1 # 1% if info matches pattern
      "^.*UCLOUD.*$": 1
      "^.*Server Hosting.*$": 1
      "^.*SCloud.*$": 1
    
    # Dynamically loaded regex-based restrictions
    restrictionsFile:
      yaml: "./restrictions.yaml"
      refresh: 30 # reload file every 30 sec
    

Module: mainnet-wallet

This module checks the target wallet on mainnet whenever a session is started.
There are several restrictions that can be enforced:

  • minBalance: Minimum amount of funds the user needs to hold in his mainnet wallet to be allowed to use the faucet.
  • minTxCount: Minimum number of transactions the user needs to have sent from his mainnet wallet to be allowed to use the faucet.
modules:
  mainnet-wallet:
    # enable / disable mainnet wallet protection
    enabled: true

    # RPC host for ETH mainnet
    rpcHost: "https://rpc.flashbots.net/"

    # require minimum balance on mainnet wallet
    minBalance: 10000000000000000  # 0.01 ETH

    # require minimum number of transactions from mainnet wallet (nonce count)
    minTxCount: 5

Module: passport

This module fetches the gitcoin passport for a target address when a session is started.

The gitcoin passport gets scored by the configured score matrix. Each stamp can be given a individual score value. Afterwards, the score value is used to select a reward factor that then gets applied to all session rewards.

The module can be used to increase the rewards for more legitimate users in mining scenarios, or lower the rewards for less legitimate users in other scenarios.

You need to gather an API Key from the Gitcoin Passport Scorer Dashboard to load passports automatically.

To allow optional manual submission & verification of the passport JSON, you need to specify the public key of the stamp signer for verification.
For gitcoin passport, that's currently did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC

modules:
  passport:
    enabled: true
    scorerApiKey: "" # Gitcoin Passport Scorer API Key
    trustedIssuers:
      - "did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC" # Gitcoin Passport
    passportCachePath: "passport-cache"
    
    refreshCooldown: 300 # allow refreshing every 5mins
    cacheTime: 86400 # cache passports for 1 day (does not affect manual refreshing)

    stampDeduplicationTime: 604800 # allow reusing a stamp in another passport after 7 days

	# applied reward factors based on passport score
    boostFactor:
      1: 1.1 # x1.1 if score >= 1
      5: 1.5 # x1.5 if score >= 5
      10: 2 # x2 if score >= 10
    
    # stamp scoring matrix
    stampScoring:
      "Google": 1
      "Twitter": 1
      "Ens": 2
      "GnosisSafe": 2
      "Poh": 4
      "POAP": 1
      "GitPOAP": 1
      "Facebook": 1
      "FacebookFriends": 1
      "FacebookProfilePicture": 1
      "Brightid": 10
      "Github": 1
      "Coinbase": 1
      "FiveOrMoreGithubRepos": 1
      "TenOrMoreGithubFollowers": 2
      "FiftyOrMoreGithubFollowers": 4
      "ForkedGithubRepoProvider": 1
      "StarredGithubRepoProvider": 1
      "Linkedin": 1
      "Discord": 1
      "TwitterTweetGT10": 1
      "TwitterFollowerGT100": 1
      "TwitterFollowerGT500": 2
      "TwitterFollowerGTE1000": 3
      "TwitterFollowerGT5000": 5
      "SelfStakingBronze": 1
      "SelfStakingSilver": 1
      "SelfStakingGold": 1
      "CommunityStakingBronze": 1
      "CommunityStakingSilver": 1
      "CommunityStakingGold": 1
      "ClearTextSimple": 1
      "ClearTextTwitter": 1
      "ClearTextGithubOrg": 1
      "SnapshotProposalsProvider": 1
      "SnapshotVotesProvider": 1
      "EthGasProvider": 1
      "FirstEthTxnProvider": 1
      "EthGTEOneTxnProvider": 1
      "GitcoinContributorStatistics#numGrantsContributeToGte#1": 1
      "GitcoinContributorStatistics#numGrantsContributeToGte#10": 1
      "GitcoinContributorStatistics#numGrantsContributeToGte#25": 2
      "GitcoinContributorStatistics#numGrantsContributeToGte#100": 2
      "GitcoinContributorStatistics#totalContributionAmountGte#10": 1
      "GitcoinContributorStatistics#totalContributionAmountGte#100": 2
      "GitcoinContributorStatistics#totalContributionAmountGte#1000": 2
      "GitcoinContributorStatistics#numRoundsContributedToGte#1": 1
      "GitcoinContributorStatistics#numGr14ContributionsGte#1": 1
      "GitcoinGranteeStatistics#numOwnedGrants#1": 1
      "GitcoinGranteeStatistics#numGrantContributors#10": 1
      "GitcoinGranteeStatistics#numGrantContributors#25": 1
      "GitcoinGranteeStatistics#numGrantContributors#100": 2
      "GitcoinGranteeStatistics#totalContributionAmount#100": 1
      "GitcoinGranteeStatistics#totalContributionAmount#1000": 2
      "GitcoinGranteeStatistics#totalContributionAmount#10000": 4
      "GitcoinGranteeStatistics#numGrantsInEcoAndCauseRound#1": 1
      "gtcPossessionsGte#100": 2
      "gtcPossessionsGte#10": 1
      "ethPossessionsGte#32": 1
      "ethPossessionsGte#10": 1
      "ethPossessionsGte#1": 1
      "NFT": 1
      "Lens": 1
      "ZkSync": 1

Module: pow

This module introduces the mining requirement, this faucet is originally known for.

When the module is enabled, the session rewards are not dropped immediatly. Instead, they need to be collected by providing some processing power for mining in the browser.

The module is highly configurable to control all aspects of the mining session.
The most important settings are the following:

  • powShareReward: The reward a session gets for sending in a eligible hash
  • powSessionTimeout: The maximum time a user may mine on the faucet.
    Keep in mind that abusive users might use unowned devices for mining. So don't make this too long and eg. use captchas to enforce some user interaction.
  • powDifficulty: This is the difficulty of the mining algorithm. See Eligable Hashes for more details.
  • powHashrateSoftLimit / powHashrateHardLimit This controls the max allowed hashrate that can be used to mine on the faucet. The soft limit is enforced client side only, and might be slightly inaccurate with clock drifts. To prevent abusive users from working around that client side limitation, the hard limit is checked on server side. It should be slightly higher than the soft limit to avoid unwanted nonce rejections due to client side inaccuracies.

Take a look into Eligable Hashes & Hash Verification Process for more details about the other configuration settings.

All other settings default to the values specified in the configuration summary below and can be left out entirely to make the config more readable.

modules:
  pow:
    # enable / disable PoW protection
    enabled: true

    # reward amount per eligible hash (in wei)
    powShareReward: 12500000000000000  # 0.0125

    # maximum mining session time (in seconds)
    powSessionTimeout: 18000   # 5h

    # maximum number of seconds a session can idle (no connection to server) until it gets closed
    powIdleTimeout: 1800 # 30min

    # maximum allowed mining hashrate (will be throttled to this rate when faster)
    powHashrateSoftLimit: 1000 # soft limit (enforced client side)
    powHashrateHardLimit: 1100 # hard limit (reject shares with too high nonces)

    # number of 0-bits a hash needs to start with to be eligible for a reward
    powDifficulty: 11
    
    ## all settings below can be left out to keep the config more readable.
    ## these settings default to the values specified below.

    # penalty for not responding to a verification request (percent of powShareReward)
    # shouldn't be too high as this can happen regularily in case of connection loss or so
    verifyMinerMissPenaltyPerc: 10  # 10% of powShareReward

    # reward for responding to a verification request in time (percent of powShareReward)
    # some extra reward for slow miners
    # comment out to disable rewards for verification requests
    verifyMinerRewardPerc:   15  # 15% of powShareReward

    # websocket ping interval
    #powPingInterval: 60

    # kill websocket if no ping/pong for that number of seconds
    #powPingTimeout: 120

    
    # mining algorithm to use
    powHashAlgo: "argon2"  # scrypt / cryptonight / argon2

    # scrypt mining parameters
    powScryptParams:
      # N - iterations count: affects memory and CPU usage, must be a power of 2
      cpuAndMemory: 4096
      # r - block size: affects memory and CPU usage
      blockSize: 8
      # p - parallelism factor: threads to run in parallel, affects the memory & CPU usage, should be 1 as webworker is single threaded
      parallelization: 1
      # klen - how many bytes to generate as output, e.g. 16 bytes (128 bits)
      keyLength: 16

    # cryptonight mining parameters
    powCryptoNightParams:
      # crypto night hash algo & variant
      algo: 0
      variant: 0
      height: 0

    # argon2 mining parameters
    powArgon2Params:
      type: 0
      version: 13
      timeCost: 4
      memoryCost: 4096
      parallelization: 1
      keyLength: 16

    # number of scrypt hashs to pack into a share (should be 1, higher value just increases verification load on server side)
    powNonceCount: 1

    # Proof of Work shares need to be verified to prevent malicious users from just sending in random numbers.
    # As that can lead to a huge verification workload on the server, this faucet can redistribute shares back to other miners for verification.
    # These randomly selected miners need to check the share and return its validity to the server within 10 seconds or they're penalized.
    # If there's a mismatch in validity-result the share is checked again locally and miners returning a bad verification result are slashed.
    # Bad shares always result in a slashing (termination of session and loss of all collected mining balance)

    # percentage of shares validated locally (0 - 100)
    verifyLocalPercent: 10

    # max number of shares in local validation queue
    verifyLocalMaxQueue: 100

    # min number of mining sessions for verification redistribution
    # only local verification if not enough active sessions (should be higher than verifyMinerIndividuals)
    verifyMinerPeerCount: 4

    # percentage of shares validated locally if there are not enough sessions for verification redistribution (0 - 100)
    verifyLocalLowPeerPercent: 80

    # percentage of shares to redistribute to miners for verification (0 - 100)
    verifyMinerPercent: 75

    # number of other mining sessions to redistribute a share to for verification
    verifyMinerIndividuals: 2

    # max number of pending verifications per miner before not sending any more verification requests
    verifyMinerMaxPending: 5

    # max number of missed verifications before not sending any more verification requests
    verifyMinerMaxMissed: 10

    # timeout for verification requests 
    # client gets penalized if not responding within this timespan
    verifyMinerTimeout: 30

Eligible Hashes

Hashes that are eligible for a reward need to start with a specific number of 0-bits. This requirement is called difficulty and can be configured via the powDifficulty setting within the pow module.

It is set to 11 by default, which seems reasonable for small testnets and test instances. For higher activity (> 500 sessions) I strongly suggest increasing the difficulty to reduce traffic and server side processing time.

A lower difficulty is generally more user-friendly, because more hashes are found and the mining balance increases more frequently. But the lower the difficulty is, the higher the traffic & processing and validation work-load on server side will be.

A difficulty of 11 means, that on average 1 of 2^11 hashes are eligible for a reward. With a average hashrate of ~300H/s that would lead to about 1 eligible hash every 7 secs for every mining session.

Difficulty 100 H/s 300 H/s 500 H/s 800 H/s 1000 H/s
11 20,5 s 6,8 s 4,1 s 2,6 s 2 s
12 41 s 13,6 s 8,2 s 5,1 s 4,1 s
13 82 s 27,3 s 16,4 s 10,2 s 8,2 s

Hash Verification Process

To avoid malicious users from sending in random hashes that do not meet the specified criteria, all hashes need to be verified somehow. This can lead to a extremely high verification-load on the faucet server, which is not really helpful. To avoid that, the faucet is able to redistribute the verification-work for these hashes back to other randomly selected miners. These randomly selected miners ("verifiers") receive the hash and do the computation work to verify the hash is valid.

The redistribution feature is optional, but enabled by default. For security, it will only be active in when a specified minimum number of sessions is actively mining.

The security of the validation redistribution relies on the random selection of multiple verifiers. So the miner does not know if his result gets redistributed to a honest verifier or a malicious one. There should always be 2 verifiers for each hash. If the validity-results returned from the selected verifiers differ, the hash is checked again locally.

Invalid hashes or incorrect verifications always lead to a immediate session termination and loss of all collected funds. Due to that, it's not super critical if a group of malicious users manages to get a few invalid hashes validated. If they repeatedly send in invalid hashes, a invalid hash will be redistributed to a honest validator at some time. The attacker does not know where his hashes get redistributed to, but when an invalid hash reaches a honest verifier, the invalidity is detected and the attacker looses all his rewards.

The redistribution process is highly configurable via the verify* settings. I suggest looking through these options as they're well described in the example config. However, the configured defaults should just work fine :)

Reward portions

The rewards that accumulates during mining is a combination of two reward portions:

  • Reward for eligible hashes
  • Reward for verifying hashes from other miners

The reward for eligible hashes can be configured via the powShareReward option. It is also the base value (100%) for all other reward / penalty options.

The reward for verifications can be configured via the verifyMinerRewardPerc option and penalties for not doing those verifications via the verifyMinerMissPenaltyPerc option. Both settings are percental values based on powShareReward. So setting verifyMinerRewardPerc to eg. 15 means 15% of powShareReward are rewarded for doing a verification.

The verification reward mostly benefits slow miners that don't find many eligible hashes themselves. But it shouldn't be too high to avoid farming these rewards without actually mining. I'm using a reward of 10% and a penalty of 15% on goerli & sepolia, which works well for a long time now :)

As described above, there should always be at least 2 verifiers for each hash to avoid malicious verifier behavior. The number of verifiers can be configured via the verifyMinerIndividuals setting. Keep in mind that the reward specified by verifyMinerRewardPerc will be paid to each verifier, so the total reward for every eligible hash from a operator perspective is powShareReward + verifyMinerIndividuals * (powShareReward / 100 * verifyMinerRewardPerc)

Module: recurring-limits

This module allows defining restrictions for recurring users.

These restrictions are based on IP and/or ETH wallet address. There can be multiple restrictions defined for different amounts / durations.

Keep in mind that session data gets removed from the database after the time defined by the global sessionCleanup setting. The restrictions specified in this module can only aggregate the numbers from that time frame.

modules:
  recurring-limits:
    # enable / disable recurring limits protection
    enabled: true

    limits: # array of individual limits, which all need to be passed
      - limitCount: 10 # limit number of sessions to 10
        duration: 3600 # aggregate data from last 1 hour
      - limitCount: 100 # limit number of sessions to 100
        limitAmount: 10000000000000000000 # limit total drop amount to 10 ETH
        duration: 86400 # aggregate data from last 1 day 
      - limitAmount: 20000000000000000000 # 20 ETH
        duration: 172800 # 5 days
        byAddrOnly: true

Wallet Fund Management

There are two supported ways to manage funds in the faucet wallet.

You can keep it all in the faucet wallet, which should theoretically work fine. Or you can use the more complex, but more secure automatic wallet refill function.

Automatic wallet refill / Vault Contract

The automatic wallet refill feature allows the faucet to operate with a low-balance wallet that can be refilled automatically when it gets empty. A smart contract is used to protect the majority of funds and limit the funds available to the hot wallet to a specific amount.

The general idea is, that even when the faucet server gets hacked or there is a critical bug somewhere in the faucet code, the attacker can only steal the funds on the hot wallet. The funds in the vault contract are still safe in that situation because the hot wallet is limited to a specific amount.

For my sepolia / goerli instances, I'm using a custom Vault Contract to secure the funds. The contract basically just limits the withdrawable balance to a specific amount per interval, which can be configured by the owner via the setAllowance(address addr, uint256 amount, uint256 interval) function.

The faucet can be configured to request funds from that contract automatically via these settings:

ethRefillContract:
  contract: "0xA5058fbcD09425e922E3E9e78D569aB84EdB88Eb" # contract address
  abi: '[{"inputs":[{"internalType":"address","name":"addr","type":"address"}],"name":"getAllowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"withdraw","outputs":[],"stateMutability":"nonpayable","type":"function"}]'
  allowanceFn: "getAllowance" # function to get the withdrawable amount
  allowanceFnArgs: [] # function args for getAllowance callwithdrawFnArgs
  withdrawFn: "withdraw" # function to call for withdrawals
  allowanceFnArgs: ["{amount}"] # function args for withdraw call
  
  withdrawGasLimit: 300000 # gas limit for calls
  checkContractBalance: true # check contract balance to avoid withdrawing more than the balance
  contractDustBalance: 1000000000000000000  # keep 1 ETH in the contract
  triggerBalance: 1100000000000000000000  # trigger withdrawal when hot wallet balance falls below 1100 ETH
  cooldownTime: 5430 # minimum time between withdrawal calls: 1.5h + 30sec 
  requestAmount: 125000000000000000000 # amount to withdraw with each withdraw call: 125 ETH