Zero to Blockchain: 실제 Peer 만들기
Hyperledger Fabric에서 Peer 역할 정의하고 수행할 액션 로직 짜기
chapter 5에서 어드민 사용자를 추가하여 네트워크 뼈대를 만들고, 핵심 관리 기능을 구현하는 실제 로직(스크립트 부분)을 자세히 보았습니다. 그 뒤의 6~10 챕터는 Peer들을 참여시켜 네트워크를 확장합니다. 한 피어가 등장할 때, 그에 따라 개발해야 하는 부분이 어떤 것이 있는지 소개하기 위해 저는 Buyer의 예만 이 포스팅에 소개합니다. 아래 흐름을 정확히 파악하시면 나머지 챕터의 내용은 일부 로직을 다듬는 정도이니 쫓아가기 어렵지 않으실 겁니다.
본 포스팅의 예시 코드는 Zero To Blockchain github을 발췌한 것임을 미리 밝힙니다.
1 ACL 설정
// permission.aclrule BuyerACL { description: "Allow buyer full access to order"
participant(m): "org.acme.Z2BTestNetwork.Buyer"
operation: READ, CREATE, UPDATE, DELETE
resource(v): "org.acme.Z2BTestNetwork.**"
condition: (v.buyer.buyerID == m.getIdentifier())
action: ALLOW}rule netAccessBuyer { description: "Allow participants access to the network"
participant: "org.acme.Z2BTestNetwork.Buyer"
operation: READ, CREATE, UPDATE, DELETE
resource: "org.hyperledger.composer.system.**"
action: ALLOW}
acl 파일은 위와 같은 구조로 정의합니다. participant는 cto 모델 파일에 정의되어 있는 조직 번호를 가져와야 합니다. Operation은 기본 CRUD 이며, 해당 참여자가 조작을 가할 수 있는 자원에 대해서도 정의합니다. 그리고 condition을 넣어서 이 역할과 관련된 제약 조건을 넣습니다. 이 경우에는, 대상 자원의 buyer ID가 participant의 ID와 동일할 경우에만 buyer로 인식한다는 간단한 조건 체크가 있네요. 즉, 그 바이어가 가지고 있는 자산만 확인할 수 있도록 하는 겁니다.
그런데 여기서 유의해야 할 점은, buyerACL과 netAccessBuyer 룰을 분리해서 정의해줘야 한다는 것입니다. 얼핏 보기에는 중복된 내용인 것 같으나, ACL은 시스템 레벨에서 하나, 비즈니스 네트워크 레벨에서 하나 정의가 되어야 합니다. 시스템 레벨의 ACL은 어떤 사람이 뭔가를 할 수 있냐는 관점에서 검증을 한다면, 비즈니스 네트워크 레벨에서의 ACL은 ‘특정 자산'에 대해서 그런 행위를 할 수 있냐를 다시 한 번 검증합니다. 위 예시 코드의 resource 부분이 달라진 것에서 알 수 있죠.
2 Query
query selectOrders {
description: "Select all Orders"
statement:
SELECT org.acme.Z2BTestNetwork.Order
}query selectOrdersByBuyer {
description: "Select all orders for a specific buyer"
statement:
SELECT org.acme.Z2BTestNetwork.Order
WHERE (seller.sellerID == _$id)
}
이런 ACL 제약을 설정하고 나면 Query를 정의합니다. 위의 selectOrder의 경우는 사용자 계정에서 조회 가능한 — 위에 v.buyer.buyerId == m.getIdentifier()에서 정의했죠 — 레코드를 가져옵니다.
3 Business Logic: add Order
이제는 추가된 Peer가 수행해야 하는 로직들을 정의합니다. 이전 포스팅의 로직 살펴보기와 약간 비슷합니다만, Peer가 등장할 때의 흐름을 유지한다는 생각으로 다시 정리해봅니다.
// funciton 도입부
exports.addOrder = function (req, res, next) { let method = 'addOrder';
console.log(method+' req.body.buyer is: '+req.body.buyer );
let businessNetworkConnection;
let factory;
let ts = Date.now();
let orderNo = req.body.buyer.replace(/@/, '').replace(/\./, '')+ts; if (svc.m_connection === null) {svc.createMessageSocket();} businessNetworkConnection = new BusinessNetworkConnection(); return businessNetworkConnection.connect(req.body.buyer)
- Information 남기기: 일단 수행하는 로직의 정보를 콘솔에 기록합니다. method 변수를 따로 정의한 것을 보면 다른 로직에서도 재사용하고자 했던 것 같은데, 왜 따로 메서드로 모듈화하지 않았는지 개인적으로 궁금하기는 하네요.
- businessNetworkConnection, Factory, orderNo 초기화: composer에서 주는 객체 businessNetworkConnection, factory를 초기화합니다. BNC를 통해 네트워크 연결을 형성할 것이고, factory를 통해 본 로직에서 찍어내고자 하는 asset의 템플릿을 로드할 것입니다. 그리고 이 일련의 과정을 식별해줄 오더 번호를 생성합니다.
- Message Socket 생성하기: svc는 비즈니스 네트워크 전반에서 공통적으로 사용되는 기능들을 모아 놓은 모듈입니다. 이 튜토리얼에서는 별도로 개발하지는 않고 기본 설정 파일 중 하나로 제공합니다 — 실제 프로젝트에서는 이 부분을 네트워크 생성하는 단계에서 같이 개발을 시작해야겠죠? — 그 중 m_connection을 체크하는 부분은 클라이언트 브라우저와 소통하기 위한 소켓이 담기는 부분입니다. 만약 이 소켓이 생성되지 않았으면 createMessageSocket로 새로 생성해서 넣어줍니다.
- BusinessNetworkConnection 인스턴스 생성 및 네트워크 연결: 위에서 초기화했던 businessNetworkConnection에 담아줍니다. 그리고 요청으로 전달 받은 buyer 정보로 네트워크 연결을 생성합니다.
이제 연결을 생성하고 나면 then 절에서 어떤 로직을 수행하는지 3부로 나누어 살펴보겠습니다.
// Order factory 정보 넣기
factory = businessNetworkConnection.getBusinessNetwork().getFactory();let order = factory.newResource(NS, 'Order', orderNo);order = svc.createOrderTemplate(order);
order.amount = 0;
order.orderNumber = orderNo;
order.buyer = factory.newRelationship(NS, 'Buyer', req.body.buyer);
order.seller = factory.newRelationship(NS, 'Seller', req.body.seller);
order.provider = factory.newRelationship(NS, 'Provider', 'noop@dummy');
order.shipper = factory.newRelationship(NS, 'Shipper', 'noop@dummy');
order.financeCo = factory.newRelationship(NS, 'FinanceCo', financeCoID);for (let each in req.body.items) {(function(_idx, _arr)
{ _arr[_idx].description = _arr[_idx].itemDescription;
order.items.push(JSON.stringify(_arr[_idx]));
order.amount += parseInt(_arr[_idx].extendedPrice); })(each, req.body.items);}
- Factory 호출해서 자원 템플릿 생성하기: 모델에서 정의된 템플릿 호출해서, 그 안에서 정의된 asset에 대한 템플릿 가져옴.
- 요청으로 넘어온 정보를 템플릿에 넣기: order 객체를 생성하는 데에 필요한 정보를 채워넣습니다. 한 order 내에 여러 아이템들이 거래될 수 있기 때문에, 각 아이템에 대한 설명과 주문 총액은 for loop를 즉시 실행해서 처리하는 것이 인상적인 부분이네요.
// 새로운 Transaction 생성하기const createNew = factory.newTransaction(NS, 'CreateOrder');createNew.order = factory.newRelationship(NS, 'Order', order.$identifier);
createNew.buyer = factory.newRelationship(NS, 'Buyer', req.body.buyer);
createNew.seller = factory.newRelationship(NS, 'Seller', req.body.seller);
createNew.financeCo = factory.newRelationship(NS, 'FinanceCo', financeCoID);
createNew.amount = order.amount;
- 그 다음 절에서는 위에서 생성한 order 정보를 바탕으로 Transaction 정보를 채웁니다. 직전 포스팅에서 cto 파일에 트랜잭션 정보 역시 정의한다고 이야기 했었죠. 그때 인터페이스 구현했던 내용이 여기서 정의가 되는 겁니다.
return businessNetworkConnection.getAssetRegistry(NS+'.Order').then((assetRegistry) => {
return assetRegistry.add(order)
.then(() => {
return businessNetworkConnection.submitTransaction(createNew)
.then(() => {
console.log(' order '+orderNo+' successfully added');
res.send({'result': ' order '+orderNo+' successfully added'});
})
.catch((error) => {
if (error.message.search('MVCC_READ_CONFLICT') !== -1) {console.log(orderNo+' retrying assetRegistry.add for: '+orderNo);
svc.loadTransaction(createNew, orderNo, businessNetworkConnection); } else {
console.log(orderNo+' submitTransaction failed with text: ',error.message);} });})
.catch((error) => {
if (error.message.search('MVCC_READ_CONFLICT') !== -1) {console.log(orderNo+' retrying assetRegistry.add for: '+orderNo);
svc.loadTransaction(createNew, orderNo, businessNetworkConnection);} else {console.log(orderNo+' assetRegistry.add failed: ',error.message);res.send({'result': 'failed', 'error':' order '+orderNo+' getAssetRegistry failed '+error.message});} }); })
- 트랜잭션 실제 적용을 위해 AssetRegistry 호출: 위에서 새로운 order와 transaction 생성이 완료됐다면, 이제 그 내용을 바탕으로 AssetRegistry 를 추가해줍니다.
- SubmitTransaction: businessNetworkConnection에서 submitTransaction 메서드를 호출하고, 위에서 생성한 createNew 객체(트랜잭션 객체)를 인자로 넘겨줍니다.
- 이외에 에러 처리는 한 번 읽어보세요.
4 Front-end Development
프론트는 사실 Hyperledger Fabric이라고 다를 것 없습니다. 여기서는, 주문을 조회하는 페이지와 Order를 제출하는 템플릿 페이지를 두 장 만듭니다. html 페이지 두 장과 이를 컨트롤하는 javascript가 jQuery로 정의되어 있습니다. 이 부분은 화면 그리는 부분이라 포스팅에서 굳이 짚고 넘어가지는 않겠습니다.
사족: 저는 사실 모던 자바스크립트 부터 개발 공부를 시작한 사람이라, 뭐랄까 본투비 jQuery hater 입니다. ZtoB 는 jQuery로 프론트 개발을 하는데요, 나중에 시간을 따로 내서 프론트 부분을 react로 리팩토링 해야겠다는 생각을 했습니다.
