Skip to content

Operator Wiki (Legacy V1)

pk910 edited this page Jun 13, 2023 · 3 revisions

This operator wiki describes the v1 version of the faucet.

To install & configure v2, check out the Operator Wiki V2

Installing the faucet

Build from source

You need a machine with git & NodeJS >= 14 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 & start the faucet:

git clone -b v1 https://github.com/pk910/PoWFaucet.git
cd PoWFaucet
npm install
npm run start

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

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

When the faucet is started for the first time, a configuration file called faucet-config.yaml is created. Read through the configuration file (there are a lot of comments that describe each setting) to customize the faucet for your needs.

Use docker image

I'm maintaining a docker image for this faucet: pk910/powfaucet:v1-latest
The latest tag is automatically built and published via a github action from the latest master commit.

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. Download a copy of faucet-config.example.yaml and save as faucet-config.yaml

    wget https://raw.githubusercontent.com/pk910/PoWFaucet/v1/faucet-config.example.yaml
    cp faucet-config.example.yaml faucet-config.yaml
    
  3. Edit faucet-config.yaml and prepend /config/ to faucetStore, faucetDBFile & faucetLogFile (ensure they're not lost on updates)

    nano faucet-config.yaml
    

    faucetStore: "/config/faucet-store.json"
    faucetDBFile: "/config/faucet-store.db"
    faucetLogFile: "/config/faucet-events.log"

    Also change faucetSecret, ethRpcHost & ethWalletKey

  4. Start the container (change /home/powfaucet/faucet-data to your datadir)

    docker run -d --restart unless-stopped --name=powfaucet -v /home/powfaucet/faucet-data:/config -p 8080:8080 -it pk910/powfaucet:v1-latest --config=/config/faucet-config.yaml
    

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

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, besides of some rare situations, all session progress is restorable after the server restarts. So it's not super critical to restart the faucet process when you need to do it.

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

ERC20 Tokens

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 you probably need to change these settings to reflect the intended values for your token: powShareReward, claimMinAmount, claimMaxAmount, claimAddrMaxBalance, spareFundsAmount, noFundsBalance, lowFundsBalance and outflow restriction settings.

Reward Management

To describe the reward calculation, it's important to understand which hashes are eligible for a reward and how the faucet processes these hashes when sent in by users.

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 powScryptParams.difficulty setting.

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

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)

Min / Max Reward

The minimum claimable amount can be configured via claimMinAmount, the maximum amount via claimMaxAmount.

The minimum amount should require mining for at least 10 mins with ~300 H/s. Going lower than that will not work reliable against botted requests.

The upper limit technically doesn't matter much. That limit fully depends on the financial situation of the faucet operator :)

Reward Restrictions

There are different mechanisms that allow further limitation of the mining rewards based on different criteria.

Global Reward Restrictions

Global restriction mechanisms apply to all sessions equally. There are currently the following mechanisms available:

  • Restrict Reward by fixed Wallet Balances
    This was the first mechanism and easily restricts the rewards based on the faucet wallet balance in fixed steps.
    It is configured via:

    faucetBalanceRestrictedReward:
      1000: 90 # 90% if lower than 1000 ETH
      500: 40  # 40% if lower than 500 ETH
      200: 10  # 10% if lower than 200 ETH
    
  • Restrict Reward dynamically based on Wallet Balance
    This is an improvement to the first mechanism, but basically works the same way. Instead of specifying fixed restriction steps, the restriction gets calculated automatically and linearly based on the faucet wallet balance.
    It is configured via:

    faucetBalanceRestriction:
      enabled: true
      targetBalance: 1100
    

    With targetBalance set to 1100, there is no restriction with a faucet balance higher than 1100 ETH.
    When lower than 1100 ETH, the restriction is: 100 / targetBalance * currentWalletBalance.
    So basically the reward gets lower, the lower the wallet balance gets.

  • Restrict Reward dynamically based on Faucet Outflow
    This mechanism directly limits the amount of funds that are allowed to be mined in a specific time.
    It is the major restriction mechanism that controls the outflow on my goerli / sepolia instances.
    It is configured via:

    faucetOutflowRestriction:
      enabled: true
      # limit outflow to 1000ETH per day
      amount: 1000000000000000000000 # 1000 ETH
      duration: 86400 # 1 day
      lowerLimit: -500000000000000000000 # -500 ETH
      upperLimit: 500000000000000000000 # 500 ETH
    

    The logic behind that mechanism keeps track of the outflow via a internal outflow balance.
    The outflow balance initially starts 0 and gets increased by amount/duration every second.
    Whenever a session gets rewarded for anything, the reward amount gets subtracted from the outflow balance.
    When the outflow balance gets negative, there is more mining activity than the allowed outflow and the reward is reduced linearly down to 0% the closer it gets to lowerLimit.
    When the outflow balance gets positive, there is less mining activity than allowed. The positive balance is a 'buffer' for activity spikes. The balance won't grow higher than upperLimit.

    The 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, but the algorithm will be more restrictive on the next day then (negative outflow balance), so in average it will meet the limit again.

Session Reward Restrictions

In addition to the global reward restrictions, there are some restriction mechanisms that apply for individual sessions only.

These per-session restrictions are mostly based on IP Address information, which are fetched from a remote API (https://ip-api.com, configurable via ipInfoApi).
The information gathered and used for the following restrictions looks 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>

Per-session restriction mechanisms:

  • Basic property restrictions
    This was the first per-session restriction mechanism and easily restricts the rewards based on country or Proxy/Hosting setting.
    It is configured via:
    ipRestrictedRewardShare:
      hosting: 10 # restrict rewards to 10% when IP is from hosting range
      proxy: 20 # restrict rewards to 20% when IP is from proxy range
    
      US: 50 # restrict rewards to 50% when IP is from a US located IP range
      # all other 2-char country codes are allowed here (UPPERCASE!)
    
  • Regex-based restrictions
    This allows restricting the reward via regular expressions for any of the gathered information.
    It is configured via:
    ipInfoMatchRestrictedReward:
      "^ETH: 0x0000000.*$": 50  # restrict rewards to 50% when eth address starts with 0x0000000
      "^Org: Abusive Org$": 1  # restrict rewards to 1% for Abusive Org >:)
      "^.*Server Hosting.*$": 1 # restrict rewards to 1% when "Server Hosting" anywhere in the properties
    
  • 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.
    It is configured via:
    ipInfoMatchRestrictedRewardFile:
      yaml: "./ipinforewards.yaml"
      refresh: 30 # refresh every 30 secs
    
    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
    # ...
    

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