Stellar恒星私链搭建

Stellar恒星私链搭建。

运行环境及组件

  1. Ubuntu 18.04 LTS
  2. postgresql
  3. stellar-core
  4. horizon
  5. friendbot
  6. 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}"

两个节点运行在一台服务器,但配置文件有几个项不同

  1. HTTP_PORT
  2. PEER_PORT
  3. NODE_SEED
  4. KNOWN_PEERS
  5. 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. 注意点

  1. 新版本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的时候才开始,这是正常的,所以要耐心等待下。

  1. firendbot-url要用公网地址。

  2. 每次重新初始化时,要把horizon的数据库删了再新建并授权。

  3. 测试环境只用部署一个节点即可,正式环境可部署多个。

  4. friendbot要等horizon完全可用后再执行,也就是至少产生64个块之后,不然会报错!

  5. friendbot每次运行时都会创建一批账号

  6. stellar-core force-scp ,每次运行都要添加,不然重启时不产生新块!

Leave a Comment

豫ICP备19001387号-1