Blockchain

#24 다른 네트워크 참여자의 상태 업데이트

Sila 2022. 7. 14. 18:01

 

지난 글에서 우리가 리액트의 컴포넌트에서 함수를 실행해 값을 바꿀 경우,

 

다른 사람들은 그 값을 바로 전달받지 못한다는 문제가 있었다.

 

이 문제를 해결하기 위해 컨트랙트와 리액트 컴포넌트 내부의 함수를 수정해

 

실시간으로 블록체인 네트워크에서 동작하는 스마트 컨트랙트의 상태를 확인할 수 있도록 해보자.

 

1. ganache 네트워크 재기동

ganache는 한 번 껏다가 다시 키면 네트워크가 전부 초기화 되기 때문에 내가 만든 계정이나 tx한

 

컨트랙트가 전부 사라진다. (윈도우의 경우)

 

지갑 같은 경우는 출력되는 주소들을 그냥 사용하면 되지만 컨트랙트의 경우 그 때마다 CA나

 

네트워크 ID, tx hash등이 달라진다.

 

(네트워크 아이디는 한 블록체인 네트워크에 참여하는 노드들의 등록 번호라고 보면 된다.)

 

이렇게 되면 truffle/build/contracts 폴더 내에 생기는 컨트랙트를 컴파일한 JSON 파일도 변하게 되는데,

 

이를 다시 front 서버 (리액트 앱) 폴더의 Contracts 폴더로 가져가 원래 있던 파일을 대체하고,

 

그 JSON 파일 안에 있는 정보에 맞춰 ca 를 바꿔줘야 한다.

 

이제부터 컨트랙트를 작성한 솔리디티 파일을 수정하고 이를 재배포한다.

 

다만 이 때 새로 생성된 ca 값으로 리액트의 Counter 컴포넌트를 수정하기보단 여러 번의 컨트랙트 수정과

 

재배포를 거치더라도 알아서 ca 값을 찾아 사용할 수 있도록 코드를 약간 일반화 해볼 것이다.

 

/*  truffle/contracts/Counter.sol  */

// SPDX-License-Identifier: MIT

contract Counter {
    uint256 private _count;
    event Count(uint256 count);
    // Count 이벤트를 선언한다.
    // 이 Counter 이벤트는 count 값 (uint256)을 출력해준다.
    
    function current() public view retuns (uint256) {
       return _count;
    }
    
    function increment() public {
        _count +=1;
        emit Count(_count);
        // increment 함수가 실행 될 경우, 이 안에서 Count 이벤트가 호출된다.
    }
    
    function decrement() public {
        _count -=1;
        emit Count(_count);
    }
}

 

이제 ganache를 실행한다.

 

npx ganache-cli

 

새로 생성된 개인키와 지갑 주소도 잘 기록해두고, truffle을 이용해 수정된 컨트랙트를 배포한다.

 

truffle migration

 

 

컨트랙트의 CA 를 기록해두고 json 파일을 확인해보자.

 

/*  truffle/buikd/contracts/Counter.json  */

{

// ...중략

  "networks": {
    "1657757492372": { // 노드 아이디
      "events": {},
      "links": {},
      "address": "0x339C5C410D005487f5ca74E8049Af02DD1552c57", // CA
      "transactionHash": "0x65db86f17d3e0bdf14c457b197bd2f336f60edbd1f0aedb3919f118bfc5fd6c6"
    }
  },
  
// ... 중략

}

 

다음과 같이 JSON 파일의 아래쪽에 networks 항목에 CA와 tx hash, 노드 아이디가 생성됨을 알 수 있다.

 

이를 다시 front 폴더에 있는 JSON으로 덮어쓰기 해주자.

 

/*  front/src/Contracts/Counter.json  */
// JSON 파일 경로

 

이제 바뀐 JSON에 맞춰 Counter 컴포넌트를 수정해준다.

 

주로 수정해줄 부분은 useEffect 내부의 즉시실행함수인데, 이 함수의 컨트랙트 지정, 호출 부분을 다음과 같이 수정한다.

 

/*  front/src/components/Counter.jsx  */

// ...중략

useEffect(() => {
    (async () => {
        if(deployed) return
        
        const networkId = await web3.eth.net.getId()
        // networkId 를 가져온다. 여기서 networkId는 1657757492372였다.
        const ca = CounterContract.networks[networkId].address
        // networkId 객체의 address 키의 값을 가져오면 그게 CA이다.
        const abi = CounterContract.abi
        
        const Deployed = new web3.eth.Contract(abi,ca)
        const count = await Deployed.methods.current().call()
        
        setCount(count)
        setDeployed(Deployed)
    })()
}, [])

 

2. 컨트랙트 내부 데이터의 실시간 반영

다시 원래의 이슈로 돌아와서 디앱에서 누군가 컨트랙트의 기능을 사용해 그 컨트랙트의 상태를 바꿀 경우,

 

디앱을 사용하는 다른 사람은 그걸 바로 반영받지 못하는 문제를 해결하기 위한 해결책으로는

 

subscribe 함수와 on 함수가 있다.

 

기본적인 작동방식은 컨트랙트의 호출과 큰 차이가 없다.

 

ca를 이용해 블록체인 네트워크의 컨트랙트를 특정하고,

 

그 컨트랙트의 변화를 감지해 (subscribe)

 

그에 맞는 콜백함수를 조건에 따라 실행하면 된다. (on)

 

/*  front/src/components/Counter.jsx  */

// ...중략

useEffect(() => {
    (async () => {
        if(deployed) return
        
        const networkId = await web3.eth.net.getId()
        // ... 중략
        const count = await Deployed.methods.current().call()
        
        web3.eth
            .subscribe('logs', { address : ca })
            .on('data', (logs) => {
                const params = [{type : 'uint256', name:'thing'}]
                const value = web3.eth.abi.decodeLog(params, logs.data)
                
                setCount(value.thing)
            })
        
        setCount(count)
        setDeployed(Deployed)
    }
},[])

 

코드가 좀 복잡해졌는데, 우선 subscribe 메소드부터 하나씩 해석해보자.

 

이 subscribe 메소드는 address가 ca (여기선 우리가 선언후 값을 대입한 그 ca이다.)인 컨트랙트의

 

logs의 발생을 인식한다. 그럼 logs는 무엇인가.

 

logs는 우리가 tx를 발생시키고 이 tx가 블록에 담길 경우 만들어지는 txReceipt로부터 나온다.

 

 

메타마스크를 열어 tx를 발생시킨 ( = counter 값을 바꾼) 계정의 활동 내역을 보면

 

쉽게 txHash값을 알 수 있다. 가장 최근 사용한 컨트랙트의 함수 Increment를 클릭하면

 

그 tx의 상태를 확인할 수 있다.

 

거래 ID 복사를 클릭하면 해당 거래의 txHash 값을 복사할 수 있고,

 

이를 이용해 truffle에서 해당 tx의 receipt를 불러와보자.

 

truffle(development)> web3.eth.getTransactionReceipt('복사한 거래 id')

{
  transactionHash: '0x4431fb587c5f15f2733670e6db6f302c25935c72888dc03238ae232fc5b5253a',
  transactionIndex: 0,
  blockNumber: 29,
  blockHash: '0xc0f671dc160eb1774f95ad14e83b73ef3c1f06b538eb921ec66b10415fc83c57',
  from: '0x017252c8461176543e7d103d21745c5c37da2244',
  to: '0x339c5c410d005487f5ca74e8049af02dd1552c57',
  cumulativeGasUsed: 27742,
  gasUsed: '0x6c5e',
  contractAddress: null,
  logs: [
    {
      address: '0x339C5C410D005487f5ca74E8049Af02DD1552c57',
      blockHash: '0xc0f671dc160eb1774f95ad14e83b73ef3c1f06b538eb921ec66b10415fc83c57',
      blockNumber: 29,
      data: '0x000000000000000000000000000000000000000000000000000000000000000a',
      logIndex: 0,
      removed: false,
      topics: [Array],
      transactionHash: '0x4431fb587c5f15f2733670e6db6f302c25935c72888dc03238ae232fc5b5253a',
      transactionIndex: 0,
      id: 'log_2ffd2335'
    }
  ],
  logsBloom: '0x00000000000000000000000000000000000000200000000000000000000000000000000000000000000000008000000000000000000000010000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', 
  status: true,
  effectiveGasPrice: 2521201929,
  type: '0x2'
}

 

Receipt를 보면 객체 내부에 여러가지 데이터 중 logs가 존재하는 것을 확인할 수 있다.

 

이 logs의 발생을 useEffect의 즉시실행함수가 인식했다면 그 데이터를 가져오고

 

그 데이터를 가지고 다음으로 .on method가 실행된다.

 

 

새로운 data를 인식해 .on 메소드가 실행되면 그 데이터를 어떤 식으로 읽을 것인지에 대한 설정이 필요한데,

 

그게 params 변수이다. params 변수는 받는 데이터들의 데이터 타입과 우리가 앞으로 부를 명칭 2가지 정보를

 

객체 형태로 담는다. ( 받는 데이터가 여러 개일 경우, 객체 여러 개가 배열에 담긴다.)

 

 

우리가 읽고 싶은건 새로 바뀐 컨트랙트 내부의 count 변수의 값인데,

 

이는 방금 subscribe method가 받아온 logs 객체의 data에 담겨 있으며

 

이는 우리가 uint256 형식으로 지정했기 때문에 0x00000000.... 과 같이

 

64자리 (0x를 포함하면 66자리)의 16진수로 나타난다.

 

 

이를 보기 좋게 10진수로 바꿔주기 위해 디코딩을 한 번 거쳐 그 값을 변수 value에 대입한다.

 

이 value는 컨트랙트 내부의 count 변수의 값을 가져온다.

 

이 value를 치면 다시 객체가 나오는데, 우리가 설정한 name을 key로, 현재 count 변수의 값의 value로 가진다.

 

따라서 value.thing의 값을 setCount 함수를 통해 count 변수에 대입해주면 다른 사람이 실행한 컨트랙트로 인해

 

변한 데이터를 바로 반영해 렌더링해줄 수 있다.

 

 

그 아래의 setCount(count)와 setDeployed(Deployed)는

 

처음 블록체인 네트워크에서 데이터를 가져올 때 실행되는 함수이다.

 

변화를 감지해 실행되는 것이 아닌 페이지를 렌더링할 때 실행되어 그 시점의 데이터를 가져온다는 점에서

 

subscribe, on과 차이가 있다.

 

3. 프론트, 백엔드의 분리

지금까지 작성한 코드들을 잘 보면 프론트 앤드 서버의 리액트 컴포넌트에 클릭시 실행되는 함수가 있고,

 

우리가 버튼을 클릭할 경우 바로 블록체인 네트워크에 요청을 보내 결과값을 받아오는 식으로 동작하고 있다.

 

이렇게 프론트 서버에서 메타마스크를 이용해 블록체인 네트워크와 상호작용하는 것도 불가능한 것은 아니다.

 

하지만 이 경우 프론트 앤드 쪽의 코드가 너무 복잡해지고 다양한 컨트랙트와 그 기능들을 사용할 경우

 

데이터를 확인, 관리하는 것이 너무 어려워진다.

 

 

이제부터는 axios를 이용해 프론트 앤드 서버와 블록체인 네트워크 사이에 백엔드 서버를 끼워넣고,

 

프론트 서버는 백엔드 서버에 요청을 보내고, 백엔드 서버가 메타마스크를 거쳐 블록체인 네트워크와 상호작용 해

 

그 결과값을 프론트 서버에 주면 프론트앤드 서버는 그 데이터를 기반으로 블록체인 네트워크에 tx를 전송해주도록 하자.

 

tx를 전송하면 이를 기반으로 컨트랙트의 데이터를 바꾸고 이를 감지해 subscribe, on method가 발동해 결과적으로

 

렌더링을 바꿔줄 것이다.

 

npm init -y
npm i express cors web3

 

백엔드 서버에 관련된 코드를 작성할 back 폴더를 만들고 라이브러리를 설치한 후,

 

프론트앤드 서버와 마찬가지로 컨트랙트의 json 파일을 똑같이 복사해 contracts 폴더에 넣어준다.

 

서버에 관련한 코드는 다음과 같이 작성해주면 된다.

 

/*  back/server.js  */

const express = require('express')
const app = express()
const cors = require('cors')
const Web3 = require('web3')
const web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:8545'))
const CounterContract = require('./contracts/Counter.json')

app.use(
    cors({
        origin : true,
        credentials : true
    })
)

app.use(express.json())

app.post('/api/increment', async (req, res) => {
    const {from} = req.body
    const nonce = await web3.eth.getTransactionCount(from)
    // getTransacitonCount 함수를 이용해 nonce 값을 확인할 수 있다.
    const networkId = await web3.eth.net.getId()
    // 블록체인 네트워크에서 networkId 를 찾는 메소드
    const ca = CounterContract.networks[networkId].address
    // networkId를 이용해 JSON에서 해당하는 객체의 ca를 찾는다. 
    const deployed = new web3.eth.Contract(abi,ca)
    const data = await deployed.methods.increment().encodeABI()
    // increment 함수의 실행을 컨트랙트에 요청하려면 이를 우선 abi로
    // 바꿔서 기계가 이를 이해할 수 있도록 형식을 바꿔줘야한다.
    // 그렇게 인코딩한 값을 data 변수에 담는다.
    
    let txObject = {
        nonce,
        from,
        to:ca,
        data
    }
    res.json(txObejct)
})

app.post('/api/decrement', async(req,res) => {
    const { from } = req.body
    const nonce = await web3.eth.getTransactionCount(from)
    
    const networkId = await web3.eth.net.getId()
    const ca = CounterContract.networks[networkId].address
    const abi = CounterContract.abi
    const deployed = new web3.eth.Contract(abi,ca)
    const data = await deployed.methods.decrement().encodeABI()

    let txObject = {
        nonce,
        from,
        to : ca,
        data
    }
    res.json(txObject)
})

app.listen(3005, () => {
    console.log('back 3005')
})

 

백엔드 서버에서 txObject내의 data를 출력해보면 0xd09de08a 처럼 0x로 시작하는 문자열을 줄텐데,

 

이는 컨트랙트 내부의 함수의 일련번호라고 보면 된다.

 

여기서는 컨트랙트 내부의 increment 함수 일련번호가 0xd09de08a 이고,

 

이 문자열을 주면서 이 일련번호에 해당하는 함수를 호출해달라고 하면

 

컨트랙트에서 그 일련번호에 해당하는 함수인 increment를 실행해주는 식이다.