-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Operator Wiki (Legacy V1)
This operator wiki describes the v1 version of the faucet.
To install & configure v2, check out the Operator Wiki V2
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.
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:
-
Create a new directory that will be used for persistent faucet data
mkdir faucet-data cd faucet-data
-
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
-
Edit
faucet-config.yaml
and prepend /config/ tofaucetStore
,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
ðWalletKey
-
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
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.
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
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.
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.
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 |
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 :)
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)
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 :)
There are different mechanisms that allow further limitation of the mining rewards based on different criteria.
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 tolowerLimit
.
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 thanupperLimit
.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.
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:The format of that yaml file is as follows:ipInfoMatchRestrictedRewardFile: yaml: "./ipinforewards.yaml" refresh: 30 # refresh every 30 secs
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 # ...
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.
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