Stellar恒星私链搭建。
运行环境及组件
- Ubuntu 18.04 LTS
- postgresql
- stellar-core
- horizon
- friendbot
- node (nvm)
1. 编译安装stellar-core
# 先安装依赖环境
sudo apt install git gcc make autoconf automake pkg-config zlib1g-dev libpq-dev bison flex parallel build-essential libtool
git clone --single-branch --branch prod https://github.com/stellar/stellar-core.git
cd stellar-core
git submodule init
git submodule update
./autogen.sh
./configure
make
sudo make install
stellar-core --version # 验证安装
编译大概需要1小时左右。
2. 数据库
sudo apt install postgresql postgresql-contrib
# 数据据是以postgres的身份创建的,先登录它创建用户并初始化数据库
su postgres
psql
# 登录成功后执行
CREATE USER someuser WITH PASSWORD 'somepass';
ALTER USER someuser WITH SUPERUSER;
CREATE DATABASE stellar_node01_db;
CREATE DATABASE stellar_node02_db;
CREATE DATABASE stellar_horizon01_db;
GRANT ALL PRIVILEGES ON DATABASE stellar_node01_db TO someuser;
GRANT ALL PRIVILEGES ON DATABASE stellar_node02_db TO someuser;
GRANT ALL PRIVILEGES ON DATABASE stellar_horizon01_db TO someuser;
# 退出数据库管理
\q
# exit 退出postgres用户
# 启动数据库
systemctl enable postgresql
systemctl start postgresql
3. 初始化并运行 stellar-core
生成私钥种子,两次,两个节点各一个
stellar-core gen-seed
# node1
Secret seed: SA5D3BWPXUSKMIFXI4BTJTVND6UZXAKCTM5N3OYZTAGRMRIL5XHZEDKK
Public: GCH4MAQCJD2WJLBX2FFBZWIM4O3Z6UJ4LCMP64CF2HS5ZDLY5LII5NQB
# node2
Secret seed: SCE2T56IAZLERUDVOM367E6LXH5HDNIENQADI5EZ6VHQ2L3LMGCKAXA6
Public: GAUGV6VNXIIIQCRUNYWE7UH5NO2CE3OBGC7FAMEG5ZRNAHJCUVIMZ2ID
创建两个文件夹 node1和node2,在其下新建stellar-core.cfg
内容如下
# What port stellar-core listens for commands on. This is for Horizon server.
HTTP_PORT=11626
PUBLIC_HTTP_PORT=false
# If it is true, It prevents you from trying to connect to other peers
RUN_STANDALONE=false
# A phrase for your network. All nodes should have the same network phrase.
NETWORK_PASSPHRASE="stellar_standlone_20200728"
# The seed used for generating the public key this node will be identified within SCP.
NODE_SEED="SA5D3BWPXUSKMIFXI4BTJTVND6UZXAKCTM5N3OYZTAGRMRIL5XHZEDKK self"
# Only nodes that want to participate in SCP should set NODE_IS_VALIDATOR=true.
# Most instances should operate in observer mode with NODE_IS_VALIDATOR=false.
NODE_IS_VALIDATOR=true
# Comma separated peers list
KNOWN_PEERS=["127.0.0.1:11635"]
# Postgres DB URL
DATABASE="postgresql://dbname=stellar_node01_db host=localhost user=someuser password=somepass"
# The port other instances of stellar-core can connect to you on.
PEER_PORT=11625
# Log level setup
COMMANDS=["ll?level=trace"]
FAILURE_SAFETY=0
UNSAFE_QUORUM=true
#The public keys of the Stellar servers
[QUORUM_SET]
THRESHOLD_PERCENT=100
# comma sepearted validator list
VALIDATORS=["$self"]
[HISTORY.vs]
get="cp /tmp/stellar-core/history/vs/{0} {1}"
put="cp {0} /tmp/stellar-core/history/vs/{1}"
mkdir="mkdir -p /tmp/stellar-core/history/vs/{0}"
两个节点运行在一台服务器,但配置文件有几个项不同
- HTTP_PORT
- PEER_PORT
- NODE_SEED
- KNOWN_PEERS
- DATABASE
其中KNOWN_PEERS
互为对方。
分别在两个节点目录下执行以下操作
stellar-core new-db
stellar-core new-hist vs
stellar-core report-last-history-checkpoint
stellar-core force-scp
# 后台运行
stellar-core run > /dev/null 2>&1 &
两个节点的Root Account seed,一样
SCVRNHZ75VUC24Z3YBZLAEBV47UNILE62UB2YFJKO4LYPHIHTPP4KM3K
4. 配置horizon
下载horizon,解压并cp到/usr/bin
初始化horizon
horizon db migrate up --db-url "postgresql://someuser:somepass@localhost:5432/stellar_horizon01_db"
运行在后台
horizon --port 8000 --ingest=true --db-url "postgresql://someuser:somepass@localhost:5432/stellar_horizon01_db" --stellar-core-db-url "postgresql://someuser:somepass@localhost:5432/stellar_node01_db" --stellar-core-url "http://127.0.0.1:11626" --network-passphrase "network passphrase" > /dev/null 2>&1 &
5. friendbot
下载地址同 horizon,配置文件 friendbot.cfg
port = 8004
friendbot_secret = "SCVRNHZ75VUC24Z3YBZLAEBV47UNILE62UB2YFJKO4LYPHIHTPP4KM3K"
network_passphrase = "stellar_standlone_20200728"
horizon_url = "http://localhost:8000"
starting_balance = "10000.00"
num_minions = 1000
base_fee = 300
6. wallet-app
npm init
npm install --save express body-parser request-promise request stellar-sdk
index.js
const express = require('express')
const bodyParser = require('body-parser')
const rp = require('request-promise')
const port = process.env.PORT || 4000
const app = express()
const Stellar = require('stellar-sdk')
const fetch = require("node-fetch");
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
const HORIZON_ENDPOINT = 'http://xxx.xx.xx.xxx:8000'
const NETWORK_PASSPHRASE = "stellar_standlone_20200728"
// Getting instance of Stellar blockchain
var opts = new Stellar.Config.setAllowHttp(true);
const server = new Stellar.Server(HORIZON_ENDPOINT, opts);
let accounts = []
// Get 100 coins from root account
const getFromFaucet = async (req,res) =>{
try{
const pk = req.body.pk
if(pk){
// faucet is our root account. Make sure you replace this value with your key
let sourceKeys = Stellar.Keypair.fromSecret("SCVRNHZ75VUC24Z3YBZLAEBV47UNILE62UB2YFJKO4LYPHIHTPP4KM3K");
// loading root account
const fee = await server.fetchBaseFee();
server.loadAccount(sourceKeys.publicKey())
.then(function(sourceAccount) {
let txn = new Stellar.TransactionBuilder(sourceAccount, {fee, networkPassphrase:NETWORK_PASSPHRASE})
.addOperation(
Stellar.Operation.createAccount({
destination: pk,
startingBalance: "100"}))
.addMemo(Stellar.Memo.text('Test Transaction'))
.setTimeout(60)
.build();
txn.sign(sourceKeys);
return server.submitTransaction(txn);
})
.then(function(result) {
res.send({"Msg" : `SUCCESS : ${JSON.stringify(result)}`})
})
.catch(function(error) {
console.error('Something went wrong!', error);
res.send({"Msg" : `ERROR : ${error}`})
});
}else{
res.send({"Msg" : "ERROR : please provide public key!"})
}
}catch(err){
res.send({"Msg" : `ERROR : ${error}`})
}
}
// Fetch all created accounts
const getAccounts = async (req,res) =>{
res.send(accounts);
}
// Get balance of an account
const getBalance = async (req, res) =>{
try{
const pk = req.body.pk;
let balance = 0;
// Load newly created accounts
account = await server.loadAccount(pk)
// check the balances
account.balances.forEach((bal) => {
balance = balance + bal.balance;
})
res.send({"Msg" : balance})
}catch(err){
res.send({"Msg" : "ERROR : " + err})
}
}
const newAccount = async (req,res) => {
let pair = Stellar.Keypair.random();
console.log(pair.secret());
console.log(pair.publicKey());
try {
const response = await fetch(
HORIZON_ENDPOINT + `/friendbot?addr=${encodeURIComponent(pair.publicKey())}`
);
const responseJSON = await response.json();
console.log("SUCCESS! You have a new account :)\n", responseJSON);
res.send({"Msg" : "SUCCESS! You have a new account : " + pair.publicKey()});
} catch (e) {
console.error("ERROR!", e);
}
}
// Do transactions
const makePayment = async (req,res) => {
const {from, to, value} = req.body;
//Let get the secret of the spender
const spender = accounts.find((acc) => {
if(acc.pk === from) return acc;
})
if(spender && spender != null){
// First, check to make sure that the destination account exists.
// You could skip this, but if the account does not exist, you will be charged
// the transaction fee when the transaction fails.
server.loadAccount(to)
.catch((err)=>{
res.send({"Msg" : `Error : receiver ${to} not found!`})
})
.then(() =>{
// lets load spender account
return server.loadAccount(from);
})
.then((spenderAccount) => {
// Start building the transaction.
const transaction = new Stellar.TransactionBuilder(spenderAccount)
.addOperation(Stellar.Operation.payment({
destination: to,
// Because Stellar allows transaction in many currencies, you must
// specify the asset type. The special "native" asset represents Lumens.
asset: Stellar.Asset.native(),
amount: value
}))
// A memo allows you to add your own metadata to a transaction. It's
// optional and does not affect how Stellar treats the transaction.
.addMemo(Stellar.Memo.text('Test Transaction'))
.build()
// get the key pair for signing the transaction
const pairA = Stellar.Keypair.fromSecret(spender.sk);
// Sign the transaction to prove you are actually the person sending it
transaction.sign(pairA)
return server.submitTransaction(transaction);
})
.then((result)=>{
res.send({"Msg" : JSON.stringify(result, null, 2)})
})
.catch((err)=>{
res.send({"Msg" : `Error : Something went wrong : ${JSON.stringify(err.response.data.extras)}`})
})
}else{
res.send({"Msg" : `Error : spender ${to} not found!`})
}
}
/* CORS */
app.use((req, res, next) => {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', '*')
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE')
// Request headers you wish to allow
res.setHeader('Access-Control-Allow-Headers', 'Origin,X-Requested-With,content-type')
// Pass to next layer of middleware
next()
})
/* API Routes */
app.get('/accounts', getAccounts)
app.post('/faucet', getFromFaucet)
app.post('/newAccount', newAccount)
app.post('/balance', getBalance)
app.post('/payment', makePayment)
/* Serve API */
app.listen(port, () => {
console.log(`Stellar test app listening on port ${port}!`)
})
运行 node index.js
上面的JavaScript是本人改写后的,创建新账户时调用friendbot
。
7. 自动化脚本
stop
#!/bin/bash
kill -s 9 `ps -aux | grep stellar-core | awk '{print $2}'`
kill -s 9 `ps -aux | grep horizon | awk '{print $2}'`
kill -s 9 `ps -aux | grep friendbot | awk '{print $2}'`
init
cd /root/node1
rm -rf buckets
rm -rf *.log
rm -rf /tmp/stellar-core
stellar-core new-db
stellar-core new-hist vs
stellar-core report-last-history-checkpoint
stellar-core force-scp
horizon db migrate up --db-url "postgresql://someuser:somepass@localhost:5432/stellar_horizon01_db"
up1
cd /root/node1
stellar-core force-scp # 每次运行都要添加,不然重启时不产生新块!
stellar-core run > /dev/null 2>&1 &
horizon --port 8000 --ingest=true --db-url "postgresql://someuser:somepass@localhost:5432/stellar_horizon01_db" --stellar-core-db-url "postgresql://someuser:somepass@localhost:5432/stellar_node01_db" --stellar-core-url "http://127.0.0.1:11626" --network-passphrase "stellar_standlone_20200728" --history-archive-urls "file:///tmp/stellar-core/history/vs" --friendbot-url "http://xxx.xx.xx.xxx:8004" > /dev/null 2>&1 &
friendbot
friendbot --conf /root/friendbot/friendbot.cfg > /dev/null 2>&1 &
up2
cd /root/node1
stellar-core run > /dev/null 2>&1 &
horizon --port 8000 --ingest=true --db-url "postgresql://someuser:somepass@localhost:5432/stellar_horizon01_db" --stellar-core-db-url "postgresql://someuser:somepass@localhost:5432/stellar_node01_db" --stellar-core-url "http://127.0.0.1:11626" --network-passphrase "stellar_standlone_20200728" --history-archive-urls "file:///tmp/stellar-core/history/vs" --friendbot-url "http://xxx.xx.xx.xxx:8004" > /dev/null 2>&1 &
friendbot --conf /root/friendbot/friendbot.cfg > /dev/null 2>&1 &
8. 注意点
- 新版本horizon要求如果
--ingest=true
,将必须指定history-archive-urls
。而如果--ingest=false
,则horizon不会接收stellar-core的数据,所以需要将ingest设置为true。在stellar-core初始化时,通过执行以下两行来初始化history-archive。stellar-core new-hist vs stellar-core report-last-history-checkpoint
开始运行时,horizon并不会马上接收stellar-core的数据,而是要等到ledgers产生到差不多64的时候才开始,这是正常的,所以要耐心等待下。
-
firendbot-url
要用公网地址。 -
每次重新初始化时,要把horizon的数据库删了再新建并授权。
-
测试环境只用部署一个节点即可,正式环境可部署多个。
-
friendbot要等horizon完全可用后再执行,也就是至少产生64个块之后,不然会报错!
-
friendbot每次运行时都会创建一批账号
-
stellar-core force-scp
,每次运行都要添加,不然重启时不产生新块!