Hyperledger Fabric网络API服务器的实现

总览

Hyperledger Fabric随附软件开发工具包(SDK),用于从外部访问结构网络和结构网络中部署的链码功能。如今,一种更流行的访问应用程序的方法是通过REST API,它尚未在Hyperledger Fabric中正式提供。然而,用一些库来实现一个并不是很困难。

本文旨在展示如何使用ExpressJS来实现一个。对于跟随我的人,您可能会注意到,这已经在我之前的文章“深入研究Fabcar”(链接)中得到了展示。在本文中,我在单独的实例中运行API服务器。我认为这是更现实的实现。

我再次使用Fabcar作为应用程序(链码),并使用First Network作为结构网络。API服务器代码主要是从原始javascript代码中采用的,并进行了修改以满足部署。

设定

image.png

在此设置中,我们有两个物理节点

  • Fabric节点:运行带有实例化Fabcar链码的First Network
  • API服务器节点:运行API服务器代码并用于外部访问

这是在AWS中部署为EC2实例的两个节点。

image.png

设置结构节点

步骤1:通过安装必备工具和与Hyperledger Fabric相关的所有软件,制作Fabric节点。在本演示中,我正在使用1.4.3版。您可以参考我之前关于构建结构节点的文章。

[##

设置Hyperledger Fabric主机并创建机器映像

使用机器映像(例如AMI)来加快Hyperledger Fabric主机的准备,以进行测试和练习

第2步:运行脚本fabcar/startFabric.sh

cd fabric-samples/fabcar
./startFabric.sh

完全执行此脚本之后,我们现在具有正在运行的First Network(2-org 4-peer设置),mychannel已启动,Fabcar chaincode已安装在所有四个对等节点上,并在mychannel上实例化。分类帐有10条汽车记录,这是initLedger()被调用的结果。光纤网络端的所有设备均已启动并正在运行。

要了解有关First Network和Fabcar应用程序的更多信息,请看一下我以前的文章,其中有更详细的描述。

[##

深入Fabcar(修订)

Hyperledger Fabric的完整应用示例

现在,让我们为API服务器准备一个身份。

步骤3:使用提供的客户端代码fabcar/javascript创建用户(user1)身份。这将在API服务器中使用。

cd javascript
npm install
node enrollAdmin.js
node registerUser.js
ls wallet/user1
image.png

现在,我们拥有API服务器所需的一切。他们是

  • org1的连接配置文件: first-network/connection-org1.json
  • 节点包文件: fabcar/package.json
  • User1身份或钱包: fabcar/javascript/wallet/user1/

这些文件将在以后复制到API Server。

API服务器:设计

我们正在使用ExpressJS来构建API服务器,并利用客户端代码query.jsinvoke.js构建实际的逻辑。在这里,我们讨论一些代码和配置设置。

API设计

我们将定义四个API。他们是

  • GET /api/queryallcars 还车
  • GET /api/query/CarID返回指定的CarID的汽车记录
  • POST /api/addcar/ 添加新的汽车记录,并在正文中指定详细信息
  • PUT /api/changeowner/CarID 更改车身中指定的新车主的汽车记录

apiserver.js

var express = require('express');
var bodyParser = require('body-parser');

var app = express();
app.use(bodyParser.json());

// Setting for Hyperledger Fabric
const { FileSystemWallet, Gateway } = require('fabric-network');
const path = require('path');
const ccpPath = path.resolve(__dirname, '.',  'connection-org1.json');


app.get('/api/queryallcars', async function (req, res) {
    try {

        // Create a new file system based wallet for managing identities.
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = new FileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user.
        const userExists = await wallet.exists('user1');
        if (!userExists) {
            console.log('An identity for the user "user1" does not exist in the wallet');
            console.log('Run the registerUser.js application before retrying');
            return;
        }

        // Create a new gateway for connecting to our peer node.
        const gateway = new Gateway();
        await gateway.connect(ccpPath, { wallet, identity: 'user1', discovery: { enabled: true, asLocalhost: false } });

        // Get the network (channel) our contract is deployed to.
        const network = await gateway.getNetwork('mychannel');

        // Get the contract from the network.
        const contract = network.getContract('fabcar');

        // Evaluate the specified transaction.
        // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4')
        // queryAllCars transaction - requires no arguments, ex: ('queryAllCars')
        const result = await contract.evaluateTransaction('queryAllCars');
        console.log(`Transaction has been evaluated, result is: ${result.toString()}`);
        res.status(200).json({response: result.toString()});

    } catch (error) {
        console.error(`Failed to evaluate transaction: ${error}`);
        res.status(500).json({error: error});
        process.exit(1);
    }
});


app.get('/api/query/:car_index', async function (req, res) {
    try {

        // Create a new file system based wallet for managing identities.
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = new FileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user.
        const userExists = await wallet.exists('user1');
        if (!userExists) {
            console.log('An identity for the user "user1" does not exist in the wallet');
            console.log('Run the registerUser.js application before retrying');
            return;
        }

        // Create a new gateway for connecting to our peer node.
        const gateway = new Gateway();
        await gateway.connect(ccpPath, { wallet, identity: 'user1', discovery: { enabled: true, asLocalhost: false } });

        // Get the network (channel) our contract is deployed to.
        const network = await gateway.getNetwork('mychannel');

        // Get the contract from the network.
        const contract = network.getContract('fabcar');

        // Evaluate the specified transaction.
        // queryCar transaction - requires 1 argument, ex: ('queryCar', 'CAR4')
        // queryAllCars transaction - requires no arguments, ex: ('queryAllCars')
        const result = await contract.evaluateTransaction('queryCar', req.params.car_index);
        console.log(`Transaction has been evaluated, result is: ${result.toString()}`);
        res.status(200).json({response: result.toString()});

    } catch (error) {
        console.error(`Failed to evaluate transaction: ${error}`);
        res.status(500).json({error: error});
        process.exit(1);
    }
});

app.post('/api/addcar/', async function (req, res) {
    try {

        // Create a new file system based wallet for managing identities.
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = new FileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user.
        const userExists = await wallet.exists('user1');
        if (!userExists) {
            console.log('An identity for the user "user1" does not exist in the wallet');
            console.log('Run the registerUser.js application before retrying');
            return;
        }

        // Create a new gateway for connecting to our peer node.
        const gateway = new Gateway();
        await gateway.connect(ccpPath, { wallet, identity: 'user1', discovery: { enabled: true, asLocalhost: false } });

        // Get the network (channel) our contract is deployed to.
        const network = await gateway.getNetwork('mychannel');

        // Get the contract from the network.
        const contract = network.getContract('fabcar');

        // Submit the specified transaction.
        // createCar transaction - requires 5 argument, ex: ('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom')
        // changeCarOwner transaction - requires 2 args , ex: ('changeCarOwner', 'CAR10', 'Dave')
        await contract.submitTransaction('createCar', req.body.carid, req.body.make, req.body.model, req.body.colour, req.body.owner);
        console.log('Transaction has been submitted');
        res.send('Transaction has been submitted');

        // Disconnect from the gateway.
        await gateway.disconnect();

    } catch (error) {
        console.error(`Failed to submit transaction: ${error}`);
        process.exit(1);
    }
})

app.put('/api/changeowner/:car_index', async function (req, res) {
    try {

        // Create a new file system based wallet for managing identities.
        const walletPath = path.join(process.cwd(), 'wallet');
        const wallet = new FileSystemWallet(walletPath);
        console.log(`Wallet path: ${walletPath}`);

        // Check to see if we've already enrolled the user.
        const userExists = await wallet.exists('user1');
        if (!userExists) {
            console.log('An identity for the user "user1" does not exist in the wallet');
            console.log('Run the registerUser.js application before retrying');
            return;
        }

        // Create a new gateway for connecting to our peer node.
        const gateway = new Gateway();
        await gateway.connect(ccpPath, { wallet, identity: 'user1', discovery: { enabled: true, asLocalhost: false } });

        // Get the network (channel) our contract is deployed to.
        const network = await gateway.getNetwork('mychannel');

        // Get the contract from the network.
        const contract = network.getContract('fabcar');

        // Submit the specified transaction.
        // createCar transaction - requires 5 argument, ex: ('createCar', 'CAR12', 'Honda', 'Accord', 'Black', 'Tom')
        // changeCarOwner transaction - requires 2 args , ex: ('changeCarOwner', 'CAR10', 'Dave')
        await contract.submitTransaction('changeCarOwner', req.params.car_index, req.body.owner);
        console.log('Transaction has been submitted');
        res.send('Transaction has been submitted');

        // Disconnect from the gateway.
        await gateway.disconnect();

    } catch (error) {
        console.error(`Failed to submit transaction: ${error}`);
        process.exit(1);
    }   
})

app.listen(8080);

这是API服务器的标准ExpressJS代码。从提供的内容query.jsinvoke.js内部进行了一些修改fabcar/javascript

  • 将ccpPath更改为当前目录,因为我们将connection-org1.json使用相同的路径。
  • 在所有API 的gateway.connect中,我们将Discovery.asLocalhost的选项更改为false

连接配置文件:connection-org1.json

API Server依赖于给定的连接配置文件以正确连接到光纤网络。该文件connection-org1.json是直接从光纤网络(即第一网络)获得的。

{
    "name": "first-network-org1",
    "version": "1.0.0",
    "client": {
        "organization": "Org1",
        "connection": {
            "timeout": {
                "peer": {
                    "endorser": "300"
                }
            }
        }
    },
    "organizations": {
        "Org1": {
            "mspid": "Org1MSP",
            "peers": [
                "peer0.org1.example.com",
                "peer1.org1.example.com"
            ],
            "certificateAuthorities": [
                "ca.org1.example.com"
            ]
        }
    },
    "peers": {
        "peer0.org1.example.com": {
            "url": "grpcs://localhost:7051",
            "tlsCACerts": {
                "pem": "-----BEGIN CERTIFICATE-----\nMIICVjCCAf2gAwIBAgIQEB1sDT11gzTv0/N4cIGoEjAKBggqhkjOPQQDA
jB2MQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGF
tcGxlLmNvbTEfMB0GA1UEAxMWdGxz\nY2Eub3JnMS5leGFtcGxlLmNvbTAeFw0xOTA5MDQwMjQzMDBaFw0yOTA5MDEwMjQz\nMDBaMHYxCzAJB
gNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\nEw1TYW4gRnJhbmNpc2NvMRkwFwYDVQQKExBvcmcxLmV4YW1wbGUuY29tM
R8wHQYD\nVQQDExZ0bHNjYS5vcmcxLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAEoN0qd5hM2SDfvGzNjTCXuQqyk+X
K4VISa16/y9iXBPpa0onyAXJuv7T0\noPf+mh3T7/g8uYtV2bwTpT2XFO3Q6KNtMGswDgYDVR0PAQH/BAQDAgGmMB0GA1Ud\nJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1Ud\nDgQiBCCalpyChmrLtpgOll6TVmlMOO/2iiyI2PadNPsIYx51mTAKBggqh
kjOPQQD\nAgNHADBEAiBLNoAYWe9LvoxxBxl3sUM64kl7rx6dI3JU+dJG6FRxWgIgCu1ONEyp\nfux9lZWr6gcrIdsn/8fQuWiOIbAgq0HSr60
=\n-----END CERTIFICATE-----\n"
            },
            "grpcOptions": {
                "ssl-target-name-override": "peer0.org1.example.com",
                "hostnameOverride": "peer0.org1.example.com"
            }
        },
        "peer1.org1.example.com": {
            "url": "grpcs://localhost:8051",
            "tlsCACerts": {
                "pem": "-----BEGIN CERTIFICATE-----\nMIICVjCCAf2gAwIBAgIQEB1sDT11gzTv0/N4cIGoEjAKBggqhkjOPQQDA
jB2MQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGF
tcGxlLmNvbTEfMB0GA1UEAxMWdGxz\nY2Eub3JnMS5leGFtcGxlLmNvbTAeFw0xOTA5MDQwMjQzMDBaFw0yOTA5MDEwMjQz\nMDBaMHYxCzAJB
gNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQH\nEw1TYW4gRnJhbmNpc2NvMRkwFwYDVQQKExBvcmcxLmV4YW1wbGUuY29tM
R8wHQYD\nVQQDExZ0bHNjYS5vcmcxLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0D\nAQcDQgAEoN0qd5hM2SDfvGzNjTCXuQqyk+X
K4VISa16/y9iXBPpa0onyAXJuv7T0\noPf+mh3T7/g8uYtV2bwTpT2XFO3Q6KNtMGswDgYDVR0PAQH/BAQDAgGmMB0GA1Ud\nJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1Ud\nDgQiBCCalpyChmrLtpgOll6TVmlMOO/2iiyI2PadNPsIYx51mTAKBggqh
kjOPQQD\nAgNHADBEAiBLNoAYWe9LvoxxBxl3sUM64kl7rx6dI3JU+dJG6FRxWgIgCu1ONEyp\nfux9lZWr6gcrIdsn/8fQuWiOIbAgq0HSr60
=\n-----END CERTIFICATE-----\n"
            },
            "grpcOptions": {
                "ssl-target-name-override": "peer1.org1.example.com",
                "hostnameOverride": "peer1.org1.example.com"
            }
        }
    },
    "certificateAuthorities": {
        "ca.org1.example.com": {
            "url": "https://localhost:7054",
            "caName": "ca-org1",
            "tlsCACerts": {
                "pem": "-----BEGIN CERTIFICATE-----\nMIICUTCCAfegAwIBAgIQSiMHm4n9QvhD6wltAHkZPTAKBggqhkjOPQQDA
jBzMQsw\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZy\nYW5jaXNjbzEZMBcGA1UEChMQb3JnMS5leGF
tcGxlLmNvbTEcMBoGA1UEAxMTY2Eu\nb3JnMS5leGFtcGxlLmNvbTAeFw0xOTA5MDQwMjQzMDBaFw0yOTA5MDEwMjQzMDBa\nMHMxCzAJBgNVB
AYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1T\nYW4gRnJhbmNpc2NvMRkwFwYDVQQKExBvcmcxLmV4YW1wbGUuY29tMRwwG
gYDVQQD\nExNjYS5vcmcxLmV4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE\nz93lOhLJG93uJQgnh93QcPPal5NQXQnAutF
KYkun/eMHMe23wNPd0aJhnXdCjWF8\nMRHVAjtPn4NVCJYiTzSAnaNtMGswDgYDVR0PAQH/BAQDAgGmMB0GA1UdJQQWMBQG\nCCsGAQUFBwMCB
ggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MCkGA1UdDgQiBCDK\naDhLwl3RBO6eKgHh4lHJovIyDJO3jTNb1ix1W86bFjAKBggqhkjOPQQDA
gNIADBF\nAiEA8KTKkjQwb1TduTWWkmsLmKdxrlE6/H7CfsdeGE+onewCIHJ1S0nLhbWYv+G9\nTbAFlNCpqr0AQefaRT3ghdURrlbo\n-----
END CERTIFICATE-----\n"
            },
            "httpOptions": {
                "verify": false
            }
        }
    }
}

我们可以交叉检查证书在fabcar/startFabric.sh执行时是否正确传播。那些peer0.org1peer1.org1从ORG1的TLS根CA证书取出,而一个用于CA是ORG1的CA证书。

请注意,所有节点均引用localhost。稍后我们需要将其更改为结构节点公共IP。

用户身份

我们已经在Fabric节点上生成了一个用户(user1)。它们存储在wallet目录中。在其中我们看到三个文件。它们是私钥(签名密钥),公钥和保存所需信息(例如证书)的对象。

image.png

稍后我们将把此身份复制到API服务器。

设置API服务器节点

步骤1:在API服务器节点上安装npm,node和make。

sudo apt-get update
sudo apt install curl
curl -sL https://deb.nodesource.com/setup_8.x | sudo bash -
sudo apt install -y nodejs
sudo apt-get install build-essential
node -v
npm -v
image.png

步骤2:在API服务器中创建目录。

mkdir apiserver
cd apiserver

步骤3:将以下文件从Fabric Node复制到API服务器。我们使用本地主机在两个EC2实例之间进行复制。

# localhost (update your own IP of the two servers)
# temp is an empty directory
cd temp
scp -i ~/Downloads/aws.pem ubuntu@[Fabric-Node-IP]:/home/ubuntu/fabric-samples/first-network/connection-org1.json .
scp -i ~/Downloads/aws.pem ubuntu@[Fabric-Node-IP]:/home/ubuntu/fabric-samples/fabcar/javascript/package.json .
scp -r -i ~/Downloads/aws.pem ubuntu@[Fabric-Node-IP]:/home/ubuntu/fabric-samples/fabcar/javascript/wallet/user1/ .
scp -r -i ~/Downloads/aws.pem * ubuntu@[API-Server-Node-IP]:/home/ubuntu/apiserver/
image.png

第4步:我们现在在API服务器中看到所有这些文件。对于一致性的缘故,我们移动user1/wallet/user1/

cd apiserver
mkdir wallet
mv user1 wallet/user1
image.png

步骤5:现在apiserver.js为API服务器创建文件。

步骤6:connection-org1.json使用Fabric Node IP地址修改连接配置文件。

sed -i 's/localhost/[Fabric-Node-IP]/g' connection-org1.json
image.png

步骤7:在中添加条目,/etc/hosts使它们将指向Fabric Node。

127.0.0.1 localhost
[Fabric-Node-IP] orderer.example.com
[Fabric-Node-IP] peer0.org1.example.com
[Fabric-Node-IP] peer1.org1.example.com
[Fabric-Node-IP] peer0.org2.example.com
[Fabric-Node-IP] peer1.org2.example.com
image.png

步骤8:安装所需的模块。

npm install
npm install express body-parser --save

步骤9:现在,我们具有所有必需的模块。我们可以运行API服务器。

node apiserver.js

访问API

我们的API服务器侦听端口8080。我们设置了API服务器节点,以便可以在任何地方访问这些API。在此演示中,我使用本地主机通过curl访问API。

测试1:查询所有汽车

curl http://[API-Server-Node-IP]:8080/api/queryallcars
在这里,我们可以看到分类帐中的10个预载汽车记录

测试2:添加新车并查询该车

curl -d '{"carid":"CAR12","make":"Honda","model":"Accord","colour":"black","owner":"Tom"}' -H "Content-Type: application/json" -X POST http://[API-Server-Node-IP]:8080/api/addcar
curl http://[API-Server-Node-IP]:8080/api/query/CAR12
我们会在分类帐中看到新添加的汽车记录。

测试3:更改车主并再次查询该车

curl http://[API-Server-Node-IP]:8080/api/query/CAR4
curl -d '{"owner":"KC"}' -H "Content-Type: application/json" -X PUT http://[API-Server-Node-IP]:8080/api/changeowner/CAR4
curl http://[API-Server-Node-IP]:8080/api/query/CAR4
我们会在分类账中看到指定汽车的所有者。

使用Postman可以得到相同的结果。

image.png

讨论区

  1. 尽管我们使用一个Fabric节点来模拟整个Fabric网络,但此设置应适用于更复杂的部署(例如,多节点,多通道)。我们需要提供正确的连接配置文件供API服务器访问。
  2. 请注意,数字身份(证书)保存在API服务器中。这意味着,无论来自世界任何地方的任何交易,所有交易都将由API服务器“代理”。如果我们需要在API服务器上进行访问控制,则它超出了Hyperledger Fabric。我们可以使用传统方法来控制对API Server的访问。
  3. 如果我们希望在结构网络级别上识别传入用户,则需要将用户身份传递给最终用户,并开发一种机制来让用户签署交易。这超出了本文的范围。
  4. 只要创建映像时已完成所有必需的组件和配置,此方法就应轻松适用于docker容器。可以通过适当的变量设置进一步优化它,以避免手动修改。

摘要

我们使用了一个单独的EC2实例来保存API服务器,并使用已部署的链码访问结构网络。这是一种标准方法,允许其他应用程序或前端根据API服务器中定义的逻辑访问那些链码功能。

你可能感兴趣的:(Hyperledger Fabric网络API服务器的实现)