1. Adding block
// 요 아이가 다음 블럭의 header와 body를 만들어주는 함수
function nextBlock(){
}
// 얘는 단순히 Push만 할 것 !
function addBlock() {
// new header 만들어서 => new block(header, body)
// 마지막 block 가져오기
}
새로운 블록을 생성할 때 필요한 이전 블록의 정보들 : index, previousHash 및 현재 블록의 정보들
function createGenesisBlock() {
// header 만들기 - 5개의 인자값이 필요해! version, index, hash, time, merkle..
const version = getVersion();
const time = getCurrentTime();
const index = 0 // 제네시스 블록은 index=0으로 설정 (최초의 블록이라 우리가 직접 하드코딩)
const previousHash = '0'.repeat(64) // 제네시스 블록은 이전 hash가 없으므로 자리수만 0으로 맞춰서 제공
// body는 배열 형태로
const body = ['hello block']
// body를 가지고 merkeltree 값을 구성하기
const tree = merkle('sha256').sync(body)
const root = tree.root() || '0'.repeat(64) // ||사용해서 예외처리
// header 완성시키기
const header = new BlockHeader(version, index, previousHash, time, root)
console.log(header)
console.log('asdf', new Block(header, body))
// header 만들어진 결과 가지고 block class에 넣어주기
return new Block(header, body)
}
// 요 아이가 다음 블럭의 header와 body를 만들어주는 함수
function nextBlock(data){
// Header
const prevBlock = getLastBlock();
const version = getVersion();
const index = prevBlock.header.index + 1;
// const previousHash = prevBlock.header.previousHash
/*
이전 해쉬값 구하기
이전 해쉬값 모두 하나의 String으로 묶어서 sha256 암호화
previousHash = SHA256(version + index + previousHash + timestamp + merkleRoot)
** 이전 블록의 정보가 다 들어감
*/
const previousHash = createHash(prevBlock);
const timestamp = getCurrentTime();
// 배열타입의 변수 생성
const merkleTree = merkle('sha256').sync(data)
// merkleTree에서 merkle root 가져오기 + 예외처리
const merkleRoot = merkleTree.root() || '0'.repeat(64);
}
// 얘는 단순히 Push만 할 것 !
function addBlock() {
// new header 만들어서 => new block(header, body)
// 마지막 block 가져오기
}
createHash 함수 완성하기
function createHash(block){
const {
version,
index,
previousHash,
time,
merkleRoot
} = block.header
const blockString = version+index+previousHash+time+merkleRoot;
//암호화 진행
const Hash = CryptoJs.SHA256(blockString).toString()
return Hash
}
이제 nextBlock function 의 header는 완성 , body 완성시키기
// 요 아이가 다음 블럭의 header와 body를 만들어주는 함수
function nextBlock(data) {
// Header
const prevBlock = getLastBlock();
const version = getVersion();
const index = prevBlock.header.index + 1;
// const previousHash = prevBlock.header.previousHash
/*
이전 해쉬값 구하기
이전 해쉬값 모두 하나의 String으로 묶어서 sha256 암호화
previousHash = SHA256(version + index + previousHash + timestamp + merkleRoot)
** 이전 블록의 정보가 다 들어감
*/
const previousHash = createHash(prevBlock);
const time = getCurrentTime();
// 배열타입의 변수 생성
const merkleTree = merkle('sha256').sync(data)
// merkleTree에서 merkle root 가져오기 + 예외처리
const merkleRoot = merkleTree.root() || '0'.repeat(64);
const newBlockHeader = new BlockHeader(version, index, previousHash, time, merkleRoot)
// 인자값으로 받은 data 는 body
console.log(new Block(newBlockHeader,data))
return new Block(newBlockHeader, data)
}
전체 함수
// file system package
const fs = require('fs') //질문 : fs npm 설치 안해도 되는지 ?
const merkle = require('merkle')
const CryptoJs = require('crypto-js')
// 암호화할 방법 넣어주기
// sync 배열만 가능
//const tree = merkle("sha256").sync([]) //tree 구조
//tree.root()
function getVersion() {
const { version } = JSON.parse(fs.readFileSync("../package.json"))
// console.log('package=',package.toString('utf8'))
// console.log('JSON.parse(package)=',JSON.parse(package))
// console.log('JSON.parse(package) version=',JSON.parse(package).version)
return version
}
function getCurrentTime() {
return Math.ceil(new Date().getTime() / 1000)
}
class BlockHeader {
// Header를 만들 인자값 5개 받기
constructor(version, index, previousHash, time, merkleRoot) {
// 현재 클래스의 version, index, hash, time, merkle은 내가 받은 값으로 하겠다.
this.version = version
this.index = index // 이전 블록 내용이 필요한 경우
this.previousHash = previousHash // 이전 블록 내용이 필요한 경우
this.time = time
this.merkleRoot = merkleRoot
}
}
class Block {
constructor(header, body) {
this.header = header
this.body = body
}
}
function createGenesisBlock() {
// header 만들기 - 5개의 인자값이 필요해! version, index, hash, time, merkle..
const version = getVersion();
const time = getCurrentTime();
const index = 0 // 제네시스 블록은 index=0으로 설정 (최초의 블록이라 우리가 직접 하드코딩)
const previousHash = '0'.repeat(64) // 제네시스 블록은 이전 hash가 없으므로 자리수만 0으로 맞춰서 제공
// body는 배열 형태로
const body = ['hello block']
// body를 가지고 merkletree 값을 구성하기
const tree = merkle('sha256').sync(body)
const root = tree.root() || '0'.repeat(64) // ||사용해서 예외처리
// header 완성시키기
const header = new BlockHeader(version, index, previousHash, time, root)
// console.log(header)
// console.log('asdf', new Block(header, body))
// header 만들어진 결과 가지고 block class에 넣어주기
return new Block(header, body)
}
// ----------------------------- add block --------------------------------------
// 여기에 block 쌓기
let Blocks = [createGenesisBlock()]
addBlock(['hello2'])
// 얘는 단순히 Push만 할 것 ! 인자값에 배열을 넣어주기 (data)
function addBlock(data) {
// new header 만들어서 => new block(header, body)
const newBlock = nextBlock(data) // return 다음 블록에 대한 객체
Blocks.push(newBlock);
// 마지막 block 가져오기
}
function getLastBlock() {
return Blocks[Blocks.length - 1]
}
// 요 아이가 다음 블럭의 header와 body를 만들어주는 함수
function nextBlock(data) {
// Header
const prevBlock = getLastBlock();
const version = getVersion();
const index = prevBlock.header.index + 1;
// const previousHash = prevBlock.header.previousHash
/*
이전 해쉬값 구하기
이전 해쉬값 모두 하나의 String으로 묶어서 sha256 암호화
previousHash = SHA256(version + index + previousHash + timestamp + merkleRoot)
** 이전 블록의 정보가 다 들어감
*/
const previousHash = createHash(prevBlock);
const time = getCurrentTime();
// 배열타입의 변수 생성
const merkleTree = merkle('sha256').sync(data)
// merkleTree에서 merkle root 가져오기 + 예외처리
const merkleRoot = merkleTree.root() || '0'.repeat(64);
const newBlockHeader = new BlockHeader(version, index, previousHash, time, merkleRoot)
// 인자값으로 받은 data 는 body
console.log(new Block(newBlockHeader,data))
return new Block(newBlockHeader, data)
}
function createHash(block) {
const {
version,
index,
previousHash,
time,
merkleRoot
} = block.header
const blockString = version + index + previousHash + time + merkleRoot;
//암호화 진행
const Hash = CryptoJs.SHA256(blockString).toString()
return Hash
}
검증하기
type , data(body 부분)가 맞는지에 대한 검증
addBlock 함수에 Blocks배열에 새로운 블록을 push 추가하기 직전 ! 에 해주기
// 얘는 단순히 Push만 할 것 ! 인자값에 배열을 넣어주기 (data)
function addBlock(data) {
// new header 만들어서 => new block(header, body)
const newBlock = nextBlock(data) // return 다음 블록에 대한 객체
// newBlock과 마지막
if(isValidNewBlock(newBlock, getLastBlock())){
Blocks.push(newBlock);
return true; //함수가 여기서 끝나버리게
}
return false;
}
function isValidNewBlock(currentBlock, previousBlock){
// type검사 : 변수 안의 값이 String or Object or Number etc.등등인지
// 숫자가 들어가면 문제가 생김 // index가 number맞는지
isValidType(currentBlock)
return true;
}
// 검사할 block을 넣어주면 됨 (앞으로 만들 new block) =>
// 만들 때마다 검사하면 이전의 block은 재검사할 필요가 읍다.
function isValidType(block){
console.log('asdff',block)
}
그리고 오류있는지 중간확인
잘 나옴 !
1. data type 검사
위의 block 안에 모든 항목 검사
// 검사할 block을 넣어주면 됨 (앞으로 만들 new block) =>
// 만들 때마다 검사하면 이전의 block은 재검사할 필요가 읍다.
function isValidType(block){
console.log('isValieTyp 함수=',block)
console.log(typeof(block.header.version)) // string
console.log(typeof(block.header.index)) // number
console.log(typeof(block.header.previousHash)) // string
console.log(typeof(block.header.time)) // number
console.log(typeof(block.header.merkleRoot)) // string
console.log(typeof(block.body)) // object
}
true 나오는지 췤
function isValidType(block){
console.log('isValieTyp 함수=',block)
console.log(typeof(block.header.version)=="string") // string
console.log(typeof(block.header.index)=="number") // number
console.log(typeof(block.header.previousHash)=="string") // string
console.log(typeof(block.header.time)=="number") // number
console.log(typeof(block.header.merkleRoot)=="string") // string
console.log(typeof(block.body)=="object") // object
}
마지막 true 췤
function isValidType(block){
console.log(
typeof(block.header.version)=="string" && // string
typeof(block.header.index)=="number" && // number
typeof(block.header.previousHash)=="string" && // string
typeof(block.header.time)=="number" && // number
typeof(block.header.merkleRoot)=="string" && // string
typeof(block.body)=="object" // object
)
}
console.log를 return으로 바꾸기
function isValidType(block){
return (
typeof(block.header.version)=="string" && // string
typeof(block.header.index)=="number" && // number
typeof(block.header.previousHash)=="string" && // string
typeof(block.header.time)=="number" && // number
typeof(block.header.merkleRoot)=="string" && // string
typeof(block.body)=="object" // object
)
}
질문 : 해결
function isValidNewBlock(currentBlock, previousBlock){
// type검사 : 변수 안의 값이 String or Object or Number etc.등등인지
// 숫자가 들어가면 문제가 생김 // index가 number맞는지
if(!isValidType(currentBlock)){
console.log(`inValidType(currentBlock)=false ${currentBlock}`)
console.log(`inValidType(currentBlock)=false ${JSON.stringify(currentBlock)}`)
return false
}
return true;
}
'' `` string으로 감싸면 [object object] 로 나옴 ! 그래서 JSON.stringify를 사용해서 나타내주기
2. index 검사
function isValidNewBlock(currentBlock, previousBlock){
// 1. type검사 : 변수 안의 값이 String or Object or Number etc.등등인지
// 숫자가 들어가면 문제가 생김 // index가 number맞는지
if(!isValidType(currentBlock)){
console.log(`inValidType(currentBlock)=false ${JSON.stringify(currentBlock)}`)
return false; // 함수 종료
}
// 2. index값이 유효한지 췤췤
// 조건 : 이전block index + 1이 현재 block의 index와 다른 경우 - 오류 잡기
if(previousBlock.header.index +1 !== currentBlock.header.index){
console.log(`invalid index입니다. `)
return false;
}
return true;
}
3. PreviousHash 쳌
function isValidNewBlock(currentBlock, previousBlock){
// 1. type검사 : 변수 안의 값이 String or Object or Number etc.등등인지
// 숫자가 들어가면 문제가 생김 // index가 number맞는지
if(!isValidType(currentBlock)){
console.log(`inValidType(currentBlock)=false ${JSON.stringify(currentBlock)}`);
return false; // 함수 종료
}
// 2. index값이 유효한지 췤췤
// 조건 : 이전block index + 1이 현재 block의 index와 다른 경우 - 오류 잡기
if(previousBlock.header.index +1 !== currentBlock.header.index){
console.log(`invalid index입니다. `);
return false;
}
// 3. previousHash 검증 - 2번째 블록이 첫번쨰 블록을 바라보고 있는지, 3번째 블록이 2번째 블록을 바라보고 있는지 ...반복
// previousHash 만든 방법 : header의 내용 string으로 합쳐서 sha256 활용하여 암호화함
// 제네시스 블럭 기준 - 두 번째 블록을 쳌 해보자
// 제네시스가 자기 자신의 header를 넣어서 암호화를 진행하기
if(createHash(previousBlock) !== currentBlock.header.previousHash){
console.log(`invalid previousHash입니다.`);
return false;
}
return true;
}
4. MerkleRoot & body 쳌
merkle 목적 : data 변한적이 있는지 (+ 특정 노드 빠르고 쉽게 찾기)
1) 네트워크를 통해 결국 여러~~~~~~~ 사람들이 똑같은 코드가 실행되다 데이터가 바뀔 수 있음. (네트워크 수업은 내일!)
2) body(배열) 의 내용이 없으면 안됨
function isValidNewBlock(currentBlock, previousBlock){
// 1. type검사 : 변수 안의 값이 String or Object or Number etc.등등인지
// 숫자가 들어가면 문제가 생김 // index가 number맞는지
if(!isValidType(currentBlock)){
console.log(`inValidType(currentBlock)=false ${JSON.stringify(currentBlock)}`);
return false; // 함수 종료
}
// 2. index값이 유효한지 췤췤
// 조건 : 이전block index + 1이 현재 block의 index와 다른 경우 - 오류 잡기
if(previousBlock.header.index +1 !== currentBlock.header.index){
console.log(`invalid index입니다. `);
return false;
}
// 3. previousHash 검증 - 2번째 블록이 첫번쨰 블록을 바라보고 있는지, 3번째 블록이 2번째 블록을 바라보고 있는지 ...반복
// previousHash 만든 방법 : header의 내용 string으로 합쳐서 sha256 활용하여 암호화함
// 제네시스 블럭 기준 - 두 번째 블록을 쳌 해보자
// 제네시스가 자기 자신의 header를 넣어서 암호화를 진행하기
if(createHash(previousBlock) !== currentBlock.header.previousHash){
console.log(`invalid previousHash입니다.`);
return false;
}
// 4. Body(정보 저장소) 체크
// root가 정확한지 체크
if(currentBlock.header.merkleRoot !== merkle('sha256').sync(currentBlock.body).root() || currentBlock.body.length === 0 ){
console.log(`invalid body 입니다. (body 내용이 없거나 )`)
return false;
}
return true;
}
5. 전체 Blocks 에 있는 배열을 검증
Check Points 🍖
1) 제네시스 블록이 유효한지 - 데이터가 바뀐적이 없는지
2) Blocks 모든 배열을 검사 (배열이 3개 요소로 이루어져있으면 검사는 2번, 10개면 9번 .... )
1) 제네시스 블록 유효성 검사
function isValidBlocks(Blocks){
// 제네시스 블록 유효한지, 데이터가 바뀐 적이 없는지
// 제네시스 블록은 하드코딩으로 만들어짐
// 아래 비교 대상 모두 return 값이 객체임
// JS는 {} === {} => always FALSE !!!
// string으로 바꿔서 비교 ㄱㄱ
if(JSON.stringify(Blocks[0]) !== JSON.stringify(createGenesisBlock())){
console.log(`Invalid Genesis block입니다. `)
return false;
}
}
2) Blocks에 있는 모든 배열의 요소들을 모 - 두 검사
필요한 것 : 현재 블록, 이전 블록
function isValidBlocks(Blocks){
// 1. 제네시스 블록 검사 - 유효한지, 데이터가 바뀐 적이 없는지
// 제네시스 블록은 하드코딩으로 만들어짐
// 아래 비교 대상 모두 return 값이 객체임
// JS는 {} === {} => always FALSE !!!
// string으로 바꿔서 비교 ㄱㄱ
if(JSON.stringify(Blocks[0]) !== JSON.stringify(createGenesisBlock())){
console.log(`Invalid Genesis block입니다. `)
return false;
}
// 2. 배열 요소 하나하나 검사
// Blocks[0] = 이미 검증이 위에서 끝난 제네시스 블록
let tempBlocks = [Blocks[0]]
// 첫 번째 (제네시스) 블록을 빼고 for문 돌리기
for(let i=1; i<Blocks.length; i++){
if(isValidNewBlock(Blocks[i], tempBlocks[i-1])){
tempBlocks.push(Blocks[i]);
}else{
return false;
}
}
return true;
}
전체 코드
// file system package
const fs = require('fs') //질문 : fs npm 설치 안해도 되는지 ?
const merkle = require('merkle')
const CryptoJs = require('crypto-js')
const { type } = require('os')
// 암호화할 방법 넣어주기
// sync 배열만 가능
//const tree = merkle("sha256").sync([]) //tree 구조
//tree.root()
function getVersion() {
const { version } = JSON.parse(fs.readFileSync("../package.json"))
// console.log('package=',package.toString('utf8'))
// console.log('JSON.parse(package)=',JSON.parse(package))
// console.log('JSON.parse(package) version=',JSON.parse(package).version)
return version
}
function getCurrentTime() {
return Math.ceil(new Date().getTime() / 1000)
}
class BlockHeader {
// Header를 만들 인자값 5개 받기
constructor(version, index, previousHash, time, merkleRoot) {
// 현재 클래스의 version, index, hash, time, merkle은 내가 받은 값으로 하겠다.
this.version = version
this.index = index // 이전 블록 내용이 필요한 경우
this.previousHash = previousHash // 이전 블록 내용이 필요한 경우
this.time = time
this.merkleRoot = merkleRoot
}
}
class Block {
constructor(header, body) {
this.header = header
this.body = body
}
}
function createGenesisBlock() {
// header 만들기 - 5개의 인자값이 필요해! version, index, hash, time, merkle..
const version = getVersion();
const time = getCurrentTime();
const index = 0 // 제네시스 블록은 index=0으로 설정 (최초의 블록이라 우리가 직접 하드코딩)
const previousHash = '0'.repeat(64) // 제네시스 블록은 이전 hash가 없으므로 자리수만 0으로 맞춰서 제공
// body는 배열 형태로
const body = ['hello block']
// body를 가지고 merkletree 값을 구성하기
const tree = merkle('sha256').sync(body)
const root = tree.root() || '0'.repeat(64) // ||사용해서 예외처리
// header 완성시키기
const header = new BlockHeader(version, index, previousHash, time, root)
// console.log(header)
// console.log('asdf', new Block(header, body))
// header 만들어진 결과 가지고 block class에 넣어주기
return new Block(header, body)
}
// ----------------------------- add block --------------------------------------
// 여기에 block 쌓기
let Blocks = [createGenesisBlock()]
addBlock(['hello2'])
// 얘는 단순히 Push만 할 것 ! 인자값에 배열을 넣어주기 (data)
function addBlock(data) {
// new header 만들어서 => new block(header, body)
const newBlock = nextBlock(data) // return 다음 블록에 대한 객체
// newBlock과 마지막
if(isValidNewBlock(newBlock, getLastBlock())){
Blocks.push(newBlock);
return true; //함수가 여기서 끝나버리게
}
return false;
}
// 검증
function isValidNewBlock(currentBlock, previousBlock){
// 1. type검사 : 변수 안의 값이 String or Object or Number etc.등등인지
// 숫자가 들어가면 문제가 생김 // index가 number맞는지
if(!isValidType(currentBlock)){
console.log(`inValidType(currentBlock)=false ${JSON.stringify(currentBlock)}`);
return false; // 함수 종료
}
// 2. index값이 유효한지 췤췤
// 조건 : 이전block index + 1이 현재 block의 index와 다른 경우 - 오류 잡기
if(previousBlock.header.index +1 !== currentBlock.header.index){
console.log(`invalid index입니다. `);
return false;
}
// 3. previousHash 검증 - 2번째 블록이 첫번쨰 블록을 바라보고 있는지, 3번째 블록이 2번째 블록을 바라보고 있는지 ...반복
// previousHash 만든 방법 : header의 내용 string으로 합쳐서 sha256 활용하여 암호화함
// 제네시스 블럭 기준 - 두 번째 블록을 쳌 해보자
// 제네시스가 자기 자신의 header를 넣어서 암호화를 진행하기
if(createHash(previousBlock) !== currentBlock.header.previousHash){
console.log(`invalid previousHash입니다.`);
return false;
}
// 4. Body(정보 저장소) 체크
// root가 정확한지 체크
if(currentBlock.header.merkleRoot !== merkle('sha256').sync(currentBlock.body).root() || currentBlock.body.length === 0 ){
console.log(`invalid body 입니다. (body 내용이 없거나 )`)
return false;
}
// 5. Blocks 전체 체크
return true;
}
function isValidBlocks(Blocks){
// 1. 제네시스 블록 검사 - 유효한지, 데이터가 바뀐 적이 없는지
// 제네시스 블록은 하드코딩으로 만들어짐
// 아래 비교 대상 모두 return 값이 객체임
// JS는 {} === {} => always FALSE !!!
// string으로 바꿔서 비교 ㄱㄱ
if(JSON.stringify(Blocks[0]) !== JSON.stringify(createGenesisBlock())){
console.log(`Invalid Genesis block입니다. `)
return false;
}
// 2. 배열 요소 하나하나 검사
// Blocks[0] = 이미 검증이 위에서 끝난 제네시스 블록
let tempBlocks = [Blocks[0]]
// 첫 번째 (제네시스) 블록을 빼고 for문 돌리기
for(let i=1; i<Blocks.length; i++){
if(isValidNewBlock(Blocks[i], tempBlocks[i-1])){
tempBlocks.push(Blocks[i]);
}else{
return false;
}
}
return true;
}
// 검사할 block을 넣어주면 됨 (앞으로 만들 new block) =>
// 만들 때마다 검사하면 이전의 block은 재검사할 필요가 읍다.
function isValidType(block){
return (
typeof(block.header.version)=="string" && // string
typeof(block.header.index)=="number" && // number
typeof(block.header.previousHash)=="string" && // string
typeof(block.header.time)=="number" && // number
typeof(block.header.merkleRoot)=="string" && // string
typeof(block.body)=="object" // object
)
}
function getLastBlock() {
return Blocks[Blocks.length - 1]
}
// 요 아이가 다음 블럭의 header와 body를 만들어주는 함수
function nextBlock(data) {
// Header
const prevBlock = getLastBlock();
const version = getVersion();
const index = prevBlock.header.index + 1;
// const previousHash = prevBlock.header.previousHash
/*
이전 해쉬값 구하기
이전 해쉬값 모두 하나의 String으로 묶어서 sha256 암호화
previousHash = SHA256(version + index + previousHash + timestamp + merkleRoot)
** 이전 블록의 정보가 다 들어감
*/
const previousHash = createHash(prevBlock);
const time = getCurrentTime();
// 배열타입의 변수 생성
const merkleTree = merkle('sha256').sync(data)
// merkleTree에서 merkle root 가져오기 + 예외처리
const merkleRoot = merkleTree.root() || '0'.repeat(64);
const newBlockHeader = new BlockHeader(version, index, previousHash, time, merkleRoot)
// 인자값으로 받은 data 는 body
console.log(new Block(newBlockHeader,data))
return new Block(newBlockHeader, data)
}
function createHash(block) {
const {
version,
index,
previousHash,
time,
merkleRoot
} = block.header
const blockString = version + index + previousHash + time + merkleRoot;
//암호화 진행
const Hash = CryptoJs.SHA256(blockString).toString()
return Hash
}
module.exports = {
// getBlock,
getLastBlock,
addBlock,
getVersion,
}
블록체인 네트워크 필요..
위에서 만든 코드는 모듈화가 되어야함!
내일 배울 것 : 블록체인 네트웤, P2P
예전 p2p 프로그램들
ex) 프루나, 당나귀, 소리바다 - 탈 중앙화st 시스템이었다!
+ websocket
클라이언트 - 서버 -> http tcp
다음주에 배울 것 : 트랜잭션 거래 / 지갑 생성 / 지분증명 / 합의 알고리즘
다다음주 : 실제 코인을 가지고 빌드 1-2개
그리고 Dapp 배우기
질문 : merkle root 가 모든 블럭이 같은지 ? - 500개씩 따로 만들어도? -> 한 블록에 들어가는 내용이 500여개 !
'블록체인 기반 핀테크 및 응용 SW개발자 양성과정 일기' 카테고리의 다른 글
[119일차] 블록체인 네트워크 웹소켓 http ws 웹서버 구축 (0) | 2021.09.03 |
---|---|
[118일차 복습] 블록체인 새 블록 추가, 연결하고 검증하기 (0) | 2021.09.03 |
[117일차 복습] 블록체인 제네시스 블록 만들기 with JavaScript (0) | 2021.09.01 |
[116일차 복습] 리눅스/셸Shell script if문, for문, 반복문, break, continue (0) | 2021.09.01 |
[117일차] 블록체인 제네시스 블록 만들기 with JavaScript (0) | 2021.09.01 |