여러 스테이커의 코인을 1개 Proof로 묶고, 운영 노드 1개로 돌린 뒤, 보상을 자동 정산하는 방법
이 글이 다루는 구조
이 글의 목표 구조는 아주 단순하다.
- Electrum ABC에서 최종 Proof 1개를 만든다.
- staker1, staker2 같은 여러 스테이커의 stake를 그 1개 Proof 안에 넣는다.
- 서버에서는 운영 노드 1개(
ecash-op)만 돌린다. - delegation이나 추가 노드(
del1)는 이번 구조에서는 쓰지 않는다. - 보상은 Proof에 지정한 payout address 1개로 받는다.
- 이후 Python 스크립트로 운영 수수료 + 스테이커 자동 정산을 수행한다.
이게 가능한 이유는 공식 튜토리얼이 이미 설명하고 있다. 하나의 Avalanche Proof는 여러 코인 stake를 담을 수 있고, 하나의 노드는 하나의 Proof를 사용하며, 여러 지갑/여러 사람의 코인도 하나의 Proof에 모두 넣을 수 있다. 반대로 delegation은 같은 Proof를 다른 노드에도 붙이고 싶을 때 쓰는 선택 기능이다.

먼저 꼭 알아야 할 핵심 개념
1) eCash staking은 “지갑 예치”가 아니라 “노드 운영”이다
eCash 공식 staking 페이지는 wallet-only staking option은 없다고 분명히 적고 있다. 즉 단순히 코인을 들고만 있다고 보상이 생기는 구조가 아니라, Avalanche가 켜진 fully-validating node가 필요하다. 또한 노드는 지속적으로 켜져 있고, 인바운드 연결을 받을 수 있어야 하며, maxconnections 제한 같은 설정은 Avalanche 동작을 방해할 수 있다.
2) stake 지갑 수와 노드 수는 다르다
이 부분이 제일 많이 헷갈린다.
- staker1, staker2는 코인을 제공하는 지갑
ecash-op는 그 Proof를 사용하는 노드
즉 스테이커가 2명이라고 노드가 2개 필요한 게 아니다. 이 글에서는 스테이커 2명 이상 → Proof 1개 → 노드 1개 구조로 간다. 공식 튜토리얼도 “같은 Node에서 여러 지갑의 stake를 쓰고 싶으면 그 stake들을 모두 하나의 Proof에 넣으면 된다”고 설명한다.
3) 보상은 자동으로 스테이커별 분배되지 않는다
Proof 안에는 Payout Address가 1개 들어간다. 공식 튜토리얼은 이 주소를 “staking rewards를 보내는 유효한 XEC 주소”라고 설명한다. 즉 공동 Proof를 썼다고 해서 네트워크가 staker1, staker2에게 알아서 나눠 보내주는 구조는 아니다. 보상은 Proof에 적힌 1개 주소로 먼저 들어오고, 이후 분배는 운영자가 따로 설계해야 한다. 이 글에서는 그 분배를 Python 스크립트로 자동화한다.
1단계. stake 조건 확인
Proof에 넣으려는 UTXO는 공식 튜토리얼 기준으로 아래 조건을 만족해야 한다.
- UTXO당 최소 100,000,000.00 XEC
- 최소 2016 confirmations
- P2PKH 타입만 지원 (Electrum ABC 지갑을 사용하면 조건 충족)
- Proof당 최대 1000 UTXO
- 최근 2주 이내 움직인 코인은 조건을 못 맞출 수 있음
이 조건을 먼저 확인하고 작업을 시작하는 게 좋다.
2단계. Electrum ABC에서 최종 Proof 만들기
공식 튜토리얼의 권장 방식은 3단계다.
- Proof wallet에서 skeleton proof 생성
- coin-holding wallet에서 stake 추가
- 다시 Proof wallet에서 최종 서명
이렇게 하는 이유는 코인 스펜딩 키와 Proof Master Key를 분리하기 위해서다. 공식 문서도 Electrum ABC Proof Editor를 사용해 이 흐름으로 Proof를 만들라고 설명한다.
여기서 반드시 확보해야 할 최종 결과물
최종적으로 아래 2개를 확보해야 한다.
OP_PROOF_HEX
최종 signed proof 전체 hexOP_MASTER_WIF
그 Proof의 Master Private Key (WIF)
그리고 Proof 생성 시 Payout Address도 정한다. 공식 튜토리얼은 Payout Address를 내가 소유한 다른 지갑 주소로 바꿀 수 있다고 설명한다. 실전에서는 이 주소를 보상 전용 주소로 두는 게 좋다.
3단계. 서버에 운영 노드 1개만 구성하기
이번 글은 delegation 없이 운영 노드 1개만 구성하는 기준이다.
즉 ecash-op 하나만 띄운다.
그 이유는 명확하다. 이미 staker1, staker2의 stake가 최종 Proof 1개 안에 다 들어가 있기 때문이다. delegation은 이 Proof를 다른 노드에 또 붙이고 싶을 때만 쓰는 기능이다. 공식 튜토리얼도 여러 노드를 같은 Proof에 붙이는 목적을 redundancy, geographic diversity, load balancing이라고 설명한다.
4단계. set-ecash-op.sh 작성
아래 스크립트는 ecash-op 단독 운영용이다.
이 스크립트가 하는 일은 다음과 같다.
ecash실행 계정 확인/data/ecash-op디렉터리 생성bitcoin.conf작성ecash-op.service작성- systemd 등록 및 시작
중요한 placeholder는 딱 3개다.
OP_RPC_PASSWORDOP_PROOF_HEXOP_MASTER_WIF
공식 튜토리얼의 노드 예시는 avaproof=<your hex Proof>와 avamasterkey=<your Master Private Key, WIF format> 조합을 사용한다.
#!/usr/bin/env bash
set -euo pipefail# =========================
# 사용자 수정값
# =========================
RUN_USER="ecash"
RUN_GROUP="ecash"BITCOIN_ABC_BIN="/opt/bitcoin-abc/bin"
BITCOIND="${BITCOIN_ABC_BIN}/bitcoind"
BITCOINCLI="${BITCOIN_ABC_BIN}/bitcoin-cli"BASE_DIR="/data"# 운영자 노드
OP_NAME="ecash-op"
OP_DIR="${BASE_DIR}/${OP_NAME}"
OP_CONF="${OP_DIR}/bitcoin.conf"
OP_RPC_USER="ecashrpc_op"
OP_RPC_PASSWORD="CHANGE_TO_LONG_RANDOM_PASSWORD_OP"
OP_P2P_PORT="8333"
OP_RPC_PORT="8332"# 최종 Proof
OP_PROOF_HEX="PASTE_YOUR_FINAL_PROOF_HEX_HERE"# Proof master private key (WIF)
OP_MASTER_WIF="PASTE_OPERATOR_PROOF_MASTER_WIF_HERE"OP_SERVICE="/etc/systemd/system/${OP_NAME}.service"# =========================
# 공통 함수
# =========================
require_binary() {
local path="$1"
if [[ ! -x "$path" ]]; then
echo "실행 파일이 없거나 실행 권한이 없습니다: $path" >&2
exit 1
fi
}ensure_user() {
if ! id -u "${RUN_USER}" >/dev/null 2>&1; then
sudo useradd -r -m -s /usr/sbin/nologin "${RUN_USER}"
fi
}make_dir() {
local dir="$1"
sudo mkdir -p "$dir"
sudo chown -R "${RUN_USER}:${RUN_GROUP}" "$dir"
sudo chmod 700 "$dir"
}write_op_conf() {
sudo tee "${OP_CONF}" >/dev/null <<EOF
server=1
daemon=0
txindex=1pid=${OP_DIR}/bitcoind.pid
debuglogfile=${OP_DIR}/debug.logrpcbind=127.0.0.1
rpcallowip=127.0.0.1
rpcport=${OP_RPC_PORT}
rpcuser=${OP_RPC_USER}
rpcpassword=${OP_RPC_PASSWORD}listen=1
port=${OP_P2P_PORT}avalanche=1
debug=avalanche
persistavapeers=1
maxavalancheoutbound=300avaproof=${OP_PROOF_HEX}
avamasterkey=${OP_MASTER_WIF}
EOF sudo chown "${RUN_USER}:${RUN_GROUP}" "${OP_CONF}"
sudo chmod 600 "${OP_CONF}"
}write_service() {
sudo tee "${OP_SERVICE}" >/dev/null <<EOF
[Unit]
Description=eCash Bitcoin ABC Node - Operator
After=network-online.target
Wants=network-online.target[Service]
Type=forking
User=${RUN_USER}
Group=${RUN_GROUP}ExecStart=${BITCOIND} -conf=${OP_CONF} -datadir=${OP_DIR} -daemonwait
ExecStop=${BITCOINCLI} -conf=${OP_CONF} -datadir=${OP_DIR} stopPIDFile=${OP_DIR}/bitcoind.pid
Restart=on-failure
RestartSec=10
TimeoutStartSec=600
TimeoutStopSec=120[Install]
WantedBy=multi-user.target
EOF
}# =========================
# 실행
# =========================
echo "[1/7] 바이너리 확인"
require_binary "${BITCOIND}"
require_binary "${BITCOINCLI}"echo "[2/7] 실행 계정 확인"
ensure_userecho "[3/7] 데이터 디렉터리 생성"
make_dir "${OP_DIR}"echo "[4/7] bitcoin.conf 작성"
write_op_confecho "[5/7] systemd 서비스 작성"
write_serviceecho "[6/7] systemd 반영 및 자동 시작 등록"
sudo systemctl daemon-reload
sudo systemctl enable "${OP_NAME}"echo "[7/7] 서비스 시작"
sudo systemctl restart "${OP_NAME}"echo
echo "완료"
echo
echo "[상태 확인]"
echo "sudo systemctl status ${OP_NAME} --no-pager -l"
echo
echo "[동기화 확인]"
echo "${BITCOINCLI} -conf=${OP_CONF} -datadir=${OP_DIR} getblockchaininfo"
echo
echo "[Avalanche 확인]"
echo "${BITCOINCLI} -conf=${OP_CONF} -datadir=${OP_DIR} getavalancheinfo"
echo
echo "[로그 확인]"
echo "tail -n 100 ${OP_DIR}/debug.log"
5단계. set-ecash-op.sh 저장 및 실행
저장
nano /root/set-ecash-op.sh
chmod +x /root/set-ecash-op.sh
placeholder 수정
/root/set-ecash-op.sh 안에서 아래 3개를 채운다.
OP_RPC_PASSWORD="충분히_긴_RPC_비밀번호"
OP_PROOF_HEX="최종_PROOF_HEX"
OP_MASTER_WIF="최종_PROOF_MASTER_WIF"
주의: rpcpassword에 #를 넣으면 설정 파싱 에러가 날 수 있다. 실제로 bitcoind는 rpcpassword에 # 사용을 피하라고 에러를 낸다. 이건 우리가 실전에서 겪은 문제이기도 하다.
실행
/root/set-ecash-op.sh
6단계. 노드 상태 확인
서비스 상태
sudo systemctl status ecash-op --no-pager -l
동기화 상태
/opt/bitcoin-abc/bin/bitcoin-cli -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op getblockchaininfo
여기서 중요한 값은:
blocksheadersinitialblockdownload
initialblockdownload: false 가 되어야 초기 동기화가 끝난 것이다.
Avalanche 상태
/opt/bitcoin-abc/bin/bitcoin-cli -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op getavalancheinfo
초기엔 verified: false, verification_status: "utxo-missing-or-spent" 같은 값이 보일 수 있다. 노드가 아직 충분히 동기화되지 않았을 때는 그럴 수 있으므로, 먼저 IBD가 끝났는지 확인하는 것이 우선이다. 공식 튜토리얼도 getavalancheinfo, getavalanchepeerinfo, debug.log 확인을 권장한다.
7단계. 자동 정산을 위한 rewards wallet 만들기
이제부터는 보상 자동 정산을 붙인다.
자동 정산 스크립트는 getreceivedbyaddress, sendmany, walletpassphrase 같은 wallet RPC를 쓰기 때문에, bitcoind에 wallet이 로드되어 있어야 한다. createwallet은 wallet을 만들고, loadwallet은 기존 wallet을 로드한다. importprivkey는 개인키를 wallet에 추가하며, 공식 문서상 legacy wallet 전용이다. rescan=true면 몇 분 걸릴 수 있고, 진행 상태는 getwalletinfo로 확인할 수 있다.
7-1. rewards wallet 생성
/opt/bitcoin-abc/bin/bitcoin-cli -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op createwallet "rewards" false false "" false false true
이미 있으면 로드:
/opt/bitcoin-abc/bin/bitcoin-cli -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op loadwallet "rewards"
7-2. payout address 비밀키 import
현재 Proof에 넣어둔 payout address의 비밀키(WIF) 를 import한다.
/opt/bitcoin-abc/bin/bitcoin-cli -rpcclienttimeout=0 -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op -rpcwallet=rewards importprivkey "PAYOUT_ADDRESS_WIF" "staking-payout" false
여기서 false는 일단 rescan 없이 키만 넣는다는 뜻이다.
이미 예전에 이 payout 주소로 받은 보상까지 다시 긁어와야 하면 나중에 rescanblockchain을 돌리면 된다. 공식 문서도 importprivkey는 rescan이 오래 걸릴 수 있다고 적고 있다.
7-3. wallet 로드 확인
/opt/bitcoin-abc/bin/bitcoin-cli -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op listwallets
7-4. payout 주소 조회 확인
/opt/bitcoin-abc/bin/bitcoin-cli -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op -rpcwallet=rewards getreceivedbyaddress "ecash:YOUR_PAYOUT_ADDRESS" 12
getreceivedbyaddress는 해당 주소가 받은 총액을 XEC 기준으로 돌려준다.
8단계. 자동 정산 스크립트 개념
자동 정산은 아래 순서로 돌아간다.
PAYOUT_ADDRESS가 받은 누적 수령액을 읽는다.- 지난 정산 시점의 누적 수령액(state 파일 저장값)을 뺀다.
- 증가분을 새로운 확정 보상으로 본다.
- 운영 수수료를 먼저 차감한다.
- 남은 금액을 스테이커별
stake_xec비율로 자동 분배한다. sendmany로 여러 주소에 한 번에 송금한다.- 송금 수수료는
subtractfeefrom으로 모든 수신자에게 균등 차감한다.
공식 sendmany 문서도 subtractfeefrom을 지정하면 그 주소들의 금액에서 수수료가 균등 차감되고, 지정하지 않으면 sender가 수수료를 낸다고 설명한다.
9단계. 자동 정산 Python 스크립트 작성
아래를 /root/xec_reward_distributor.py 로 저장한다.
#!/usr/bin/env python3
import json
import subprocess
import sys
import fcntl
from decimal import Decimal, ROUND_DOWN, getcontext
from pathlib import Path
from datetime import datetimegetcontext().prec = 28CLI = "/opt/bitcoin-abc/bin/bitcoin-cli"
CONF = "/data/ecash-op/bitcoin.conf"
DATADIR = "/data/ecash-op"
RPC_WALLET = "rewards"PAYOUT_ADDRESS = "ecash:REPLACE_ME_PAYOUT_ADDRESS"
MINCONF = 12
MIN_DISTRIBUTION_XEC = Decimal("1000.00")STATE_FILE = Path("/data/ecash-op/reward_distribution_state.json")
LOCK_FILE = Path("/data/ecash-op/reward_distribution.lock")OPERATOR_FEE_PCT = Decimal("0.10")
OPERATOR_FEE_ADDRESS = "ecash:REPLACE_ME_OPERATOR_FEE_ADDRESS"STAKERS = [
{
"name": "staker1",
"address": "ecash:REPLACE_ME_STAKER1_ADDRESS",
"stake_xec": Decimal("5057339988.42"),
},
{
"name": "staker2",
"address": "ecash:REPLACE_ME_STAKER2_ADDRESS",
"stake_xec": Decimal("100000020.00"),
},
]WALLET_PASSPHRASE = None
UNLOCK_SECONDS = 30
TX_COMMENT = "xec staking reward auto-distribution"
XEC_Q = Decimal("0.01")def run_cli(*args: str) -> str:
cmd = [
CLI,
f"-conf={CONF}",
f"-datadir={DATADIR}",
f"-rpcwallet={RPC_WALLET}",
*args
]
p = subprocess.run(cmd, capture_output=True, text=True)
if p.returncode != 0:
raise RuntimeError(
f"bitcoin-cli failed:\n"
f"CMD: {' '.join(cmd)}\n"
f"STDERR: {p.stderr.strip()}"
)
return p.stdout.strip()def quantize_xec(x: Decimal) -> Decimal:
return x.quantize(XEC_Q, rounding=ROUND_DOWN)def load_state() -> dict:
if not STATE_FILE.exists():
return {
"settled_total_received": "0.00",
"last_txid": None,
"last_run_at": None,
}
with STATE_FILE.open("r", encoding="utf-8") as f:
return json.load(f)def save_state(state: dict) -> None:
tmp = STATE_FILE.with_suffix(".tmp")
with tmp.open("w", encoding="utf-8") as f:
json.dump(state, f, ensure_ascii=False, indent=2)
tmp.replace(STATE_FILE)def validate_config():
if not PAYOUT_ADDRESS.startswith("ecash:"):
raise ValueError("PAYOUT_ADDRESS가 비어 있거나 ecash: 주소 형식이 아닙니다.")
if not OPERATOR_FEE_ADDRESS.startswith("ecash:"):
raise ValueError("OPERATOR_FEE_ADDRESS가 비어 있거나 ecash: 주소 형식이 아닙니다.")
if OPERATOR_FEE_PCT < Decimal("0") or OPERATOR_FEE_PCT >= Decimal("1"):
raise ValueError("OPERATOR_FEE_PCT 는 0 이상 1 미만이어야 합니다.")
if MINCONF < 0:
raise ValueError("MINCONF 는 0 이상이어야 합니다.")
if MIN_DISTRIBUTION_XEC < Decimal("0.00"):
raise ValueError("MIN_DISTRIBUTION_XEC 는 0 이상이어야 합니다.")
if not STAKERS:
raise ValueError("STAKERS 가 비어 있습니다.") total_stake = Decimal("0.00")
for s in STAKERS:
if "name" not in s or not s["name"]:
raise ValueError("각 스테이커에 name 이 필요합니다.")
if "address" not in s or not str(s["address"]).startswith("ecash:"):
raise ValueError(f'{s.get("name", "<unknown>")} 의 address 가 비어 있거나 ecash: 형식이 아닙니다.')
if "stake_xec" not in s:
raise ValueError(f'{s.get("name", "<unknown>")} 의 stake_xec 가 없습니다.')
if s["stake_xec"] <= Decimal("0.00"):
raise ValueError(f'{s["name"]} 의 stake_xec 는 0보다 커야 합니다.')
total_stake += s["stake_xec"] if total_stake <= Decimal("0.00"):
raise ValueError("전체 stake_xec 합계가 0보다 커야 합니다.")def print_staker_ratios():
total_stake = sum(s["stake_xec"] for s in STAKERS)
print("[INFO] 스테이커별 stake 비율")
for s in STAKERS:
ratio_pct = (s["stake_xec"] / total_stake) * Decimal("100")
print(f' - {s["name"]}: {s["stake_xec"]:.2f} XEC ({ratio_pct.quantize(Decimal("0.0001"))}%)')def unlock_wallet_if_needed():
if WALLET_PASSPHRASE:
run_cli("walletpassphrase", WALLET_PASSPHRASE, str(UNLOCK_SECONDS))def get_total_received() -> Decimal:
out = run_cli("getreceivedbyaddress", PAYOUT_ADDRESS, str(MINCONF))
return Decimal(out)def build_distribution(delta: Decimal) -> dict:
total_stake = sum(s["stake_xec"] for s in STAKERS) fee_amount = quantize_xec(delta * OPERATOR_FEE_PCT)
distributable = quantize_xec(delta - fee_amount) payouts = {} if fee_amount > Decimal("0.00"):
payouts[OPERATOR_FEE_ADDRESS] = fee_amount allocated = Decimal("0.00")
for i, s in enumerate(STAKERS):
ratio = s["stake_xec"] / total_stake
if i < len(STAKERS) - 1:
amt = quantize_xec(distributable * ratio)
allocated += amt
else:
amt = quantize_xec(distributable - allocated) payouts[s["address"]] = payouts.get(s["address"], Decimal("0.00")) + amt payouts = {addr: amt for addr, amt in payouts.items() if amt > Decimal("0.00")}
return payoutsdef send_many(payouts: dict) -> str:
json_amounts = {addr: f"{amt:.2f}" for addr, amt in payouts.items()}
amounts_arg = json.dumps(json_amounts, separators=(",", ":")) subtract_fee_from = list(payouts.keys())
subtract_fee_arg = json.dumps(subtract_fee_from, separators=(",", ":")) txid = run_cli("sendmany", "", amounts_arg, "1", TX_COMMENT, subtract_fee_arg)
return txiddef acquire_lock():
LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
f = LOCK_FILE.open("w")
try:
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise RuntimeError("이미 정산 스크립트가 실행 중입니다.")
return fdef main():
lock_handle = acquire_lock() validate_config()
print_staker_ratios() state = load_state()
settled_total = Decimal(state["settled_total_received"])
current_total = get_total_received()
delta = quantize_xec(current_total - settled_total) print(f"[INFO] settled_total_received = {settled_total:.2f}")
print(f"[INFO] current_total_received = {current_total:.2f}")
print(f"[INFO] new_confirmed_rewards = {delta:.2f}") if delta < Decimal("0.00"):
raise RuntimeError(
"현재 누적 수령액이 이전 정산 기준보다 작습니다. "
"state 파일이 꼬였거나 payout address를 바꿨을 수 있습니다."
) if delta == Decimal("0.00"):
print("[INFO] 새로 정산할 확정 보상이 없습니다.")
return if delta < MIN_DISTRIBUTION_XEC:
print(f"[INFO] 새 보상 {delta:.2f} XEC < 최소 정산 금액 {MIN_DISTRIBUTION_XEC:.2f} XEC 이므로 보류합니다.")
return payouts = build_distribution(delta) print("[INFO] 이번 정산 송금안 (표시 금액에서 실제 수수료가 균등 차감됨)")
for addr, amt in payouts.items():
print(f" - {addr}: {amt:.2f} XEC") unlock_wallet_if_needed()
txid = send_many(payouts) state["settled_total_received"] = f"{current_total:.2f}"
state["last_txid"] = txid
state["last_run_at"] = datetime.utcnow().isoformat() + "Z"
save_state(state) print(f"[OK] 송금 완료 txid = {txid}")
lock_handle.close()if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"[ERROR] {e}", file=sys.stderr)
sys.exit(1)
이 스크립트는 getreceivedbyaddress로 payout address가 받은 누적 확정 금액을 읽고, sendmany의 subtractfeefrom을 써서 모든 수신자에게서 수수료를 균등 차감한다. 지갑이 암호화돼 있으면 walletpassphrase가 필요하다. walletlock 문서도 private key가 필요한 작업 전 walletpassphrase로 잠금을 해제한 뒤 송금하는 흐름을 예시로 보여준다.
10단계. 자동 정산 스크립트 설정값 채우기
최소한 아래는 실제 값으로 바꿔야 한다.
PAYOUT_ADDRESS = "ecash:실제_보상_수령_주소"
OPERATOR_FEE_ADDRESS = "ecash:운영수수료_받을_주소"
OPERATOR_FEE_PCT = Decimal("0.10")
스테이커 정보도 채운다.
STAKERS = [
{
"name": "staker1",
"address": "ecash:staker1_정산주소",
"stake_xec": Decimal("staker1의 예치 수량"),
},
{
"name": "staker2",
"address": "ecash:staker2_정산주소",
"stake_xec": Decimal("staker2의 예치 수량"),
},
]
지갑이 암호화돼 있으면:
WALLET_PASSPHRASE = "지갑암호"
11단계. 자동 정산 스크립트 저장 및 테스트
저장
nano /root/xec_reward_distributor.py
chmod +x /root/xec_reward_distributor.py
문법 검사
python3 -m py_compile /root/xec_reward_distributor.py
첫 실행은 테스트 모드 권장
처음에는 실제 송금을 막고 싶으면 아래 값을 아주 크게 둔다.
MIN_DISTRIBUTION_XEC = Decimal("999999999999.00")
그러면 계산과 조회만 하고 실제 정산은 하지 않는다.
실행
python3 /root/xec_reward_distributor.py
정상이라면:
- stake 비율
- 이전 정산 누적액
- 현재 누적 수령액
- 새 보상
- 정산 대상 여부
가 출력된다.
12단계. 자동 실행(cron)
매시간 1번 돌리려면:
crontab -e
추가:
0 * * * * /usr/bin/python3 /root/xec_reward_distributor.py >> /var/log/xec_reward_distributor.log 2>&1
로그 확인:
tail -f /var/log/xec_reward_distributor.log
13단계. 자주 나는 문제
No wallet is loaded
rewards wallet이 로드되지 않은 상태다.
/opt/bitcoin-abc/bin/bitcoin-cli -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op loadwallet "rewards"
importprivkey가 timeout
rescan 때문에 오래 걸리는 경우가 많다.
일단 false로 import하고, 필요한 경우 나중에 rescanblockchain을 돌리면 된다. 공식 문서도 importprivkey는 rescan 시 몇 분 걸릴 수 있다고 적고 있다.
payout 주소 내역을 과거까지 다시 잡아야 할 때
/opt/bitcoin-abc/bin/bitcoin-cli -rpcclienttimeout=0 -conf=/data/ecash-op/bitcoin.conf -datadir=/data/ecash-op -rpcwallet=rewards rescanblockchain
정산 금액과 실제 수령 금액이 조금 다를 때
정상이다.
이번 스크립트는 sendmany의 subtractfeefrom을 써서 운영자 주소와 모든 스테이커 주소에서 수수료를 균등 차감하는 방식이기 때문이다.
14단계. 이 구조의 장점과 한계
장점
- 스테이커 여러 명의 코인을 하나의 Proof에 묶을 수 있다.
- 노드는 1개만 운영하면 된다.
- payout address를 기준으로 자동 정산이 가능하다.
- 스테이커별 stake 수량만 입력하면 비율 계산을 자동화할 수 있다.
한계
- 네트워크가 staker별로 직접 분배해주지는 않는다.
- 보상은 payout address 1개로만 들어온다.
- payout 주소 비밀키를
rewardswallet에 import하는 방식이므로, 그 wallet은 해당 주소 자금을 실제로 쓸 수 있는 권한을 가진다.importprivkey문서도 개인키를 wallet에 추가한다고 명시한다. 따라서 이 wallet 보안은 매우 중요하다.
마무리
이 글의 핵심은 한 문장으로 정리된다.
“여러 스테이커의 코인을 하나의 Avalanche Proof로 묶고, 운영 노드 1개로 eCash staking에 참여한 뒤, 보상은 payout address로 받은 다음 wallet RPC와 Python 스크립트로 자동 정산한다.”
공식 문서가 제공하는 Proof / payout address / node 구조 위에, 실전 운영용 자동 분배 로직을 붙인 형태라고 이해하면 된다. eCash staking은 풀 노드 운영이 전제이고, 여러 코인을 하나의 Proof에 넣을 수 있으며, 보상은 payout address로 들어온다. 이 가이드는 바로 그 구조를 실전에 맞게 풀어쓴 버전이다.