diff --git a/public/content/translations/ko/developers/docs/storage/index.md b/public/content/translations/ko/developers/docs/storage/index.md new file mode 100644 index 00000000000..a1c1c8048d7 --- /dev/null +++ b/public/content/translations/ko/developers/docs/storage/index.md @@ -0,0 +1,216 @@ +--- +title: "분산형 스토리지" +description: "탈중앙화 저장소의 정의와 이를 하나의 디앱으로 통합하는 데 사용할 수 있는 도구들에 대한 개요" +lang: ko +--- + +하나의 회사나 조직이 운영하는 중앙집중형 서버와 달리, 탈중앙화 저장소 시스템은 전체 데이터의 일부를 보유하는 사용자-운영자들의 P2P 네트워크로 구성되어 안정성 있는 파일 저장소 공유 시스템을 구축합니다. 이런 시스템들은 블록체인 기반 애플리케이션이나 모든 P2P 기반 네트워크에 있을 수 있습니다. + +이더리움은 그 자체로 탈중앙화 저장소 시스템으로 활용될 수 있으며, 모든 스마트 컨트랙트에서의 코드 저장공간이 바로 그것입니다. 하지만 많은 양의 데이터를 저장하는 경우, 이더리움이 설계된 목적에 부합하지 않습니다. 체인은 꾸준히 성장하고 있지만, 이 글을 쓰는 시점에서 이더리움 체인은 약 500GB~1TB([클라이언트에 따라 다름](https://etherscan.io/chartsync/chaindefault))이며, 네트워크의 모든 노드는 모든 데이터를 저장할 수 있어야 합니다. 체인을 대량의 데이터(예: 5TB)로 확장해야 하는 경우 모든 노드가 계속 실행되는 것은 실현 가능하지 않을 수 있습니다. 또한, 이 많은 양의 데이터를 메인넷에 배포하는 비용은 [가스](/developers/docs/gas) 수수료 때문에 엄청나게 비쌀 것입니다. + +이러한 제약으로 인해, 많은 양의 데이터를 탈중앙화된 방식으로 저장할 수 있는 다른 체인이나 방법론이 필요합니다. + +탈중앙화 저장소(dStorage) 옵션을 살펴볼 때, 사용자가 유의해야 할 몇가지 사항이 있습니다. + +- 지속성 메카니즘 / 인센티브 구조 +- 데이터 보존 시행 +- 분산성 +- 합의 + +## 지속성 메커니즘 / 인센티브 구조 {#persistence-mechanism} + +### 블록체인 기반 {#blockchain-based} + +데이터 조각이 영원히 지속되려면 지속성 메커니즘을 사용해야 합니다. 예를 들어, 이더리움에서 지속성 메커니즘이란 노드를 실행할 때 전체 체인을 고려해야 한다는 것입니다. 새로운 데이터 조각이 체인의 끝에 추가되고 계속해서 성장하여 모든 노드가 임베디드 데이터를 복제해야 합니다. + +이를 **블록체인 기반** 지속성이라고 합니다. + +블록체인 기반 지속성의 문제점은 체인이 너무 커져서 모든 데이터를 실현 가능한 방식으로 유지하고 저장하기 어렵다는 것입니다(예: [여러 출처](https://healthit.com.au/how-big-is-the-internet-and-how-do-we-measure-it/)에 따르면 인터넷에는 40제타바이트 이상의 저장 공간 용량이 필요하다고 추정됩니다). + +블록체인은 또한 일종의 인센티브 구조가 있어야 합니다. 블록체인 기반 지속성을 위해서는 검증자에게 지급되는 대가가 있습니다. 데이터가 체인에 추가될 때 검증자들이 데이터를 추가하기 위해 대가를 받습니다. + +블록체인 기반 지속성을 갖춘 플랫폼 + +- 이더리움 +- [Arweave](https://www.arweave.org/) + +### 계약 기반 {#contract-based} + +**계약 기반** 지속성은 모든 노드에서 데이터를 복제하고 영구적으로 저장할 수 없으며, 대신 계약 합의에 따라 유지되어야 한다는 개념을 기반으로 합니다. 이것은 일정 기간 동안 데이터 조각을 보유하기로 약속한 여러 노드와 맺은 합의입니다. 데이터를 계속 유지하려면 소진될 때마다 재지불하거나 갱신해야 합니다. + +대부분의 경우, 모든 데이터를 온체인으로 저장하는 대신, 체인 상에 데이터가 위치하는 곳의 해시가 저장됩니다. 이렇게 하면 모든 데이터를 유지하기 위해 전체 체인을 확장할 필요가 없습니다. + +컨트랙트 기반 지속성을 갖춘 플랫폼: + +- [Filecoin](https://docs.filecoin.io/basics/what-is-filecoin) +- [Skynet](https://sia.tech/) +- [Storj](https://storj.io/) +- [Züs](https://zus.network/) +- [Crust Network](https://crust.network) +- [Swarm](https://www.ethswarm.org/) +- [4EVERLAND](https://www.4everland.org/) + +### 추가 고려사항 {#additional-consideration} + +IPFS는 파일, 웹사이트, 애플리케이션 및 데이터를 저장하고 액세스하기 위한 분산 시스템입니다. 이 시스템은 내장된 인센티브 체계가 없지만, 장기적인 지속성을 위해 위에 언급된 계약 기반 인센티브 솔루션 중 하나와 함께 사용할 수 있습니다. 또 다른 방법으로 IPFS에서 데이터를 지속시키는 방법은 '고정 서비스'와 협력하는 것입니다. 이 서비스는 사용자의 데이터를 "고정"해 줍니다. 사용자는 자신의 IPFS 노드를 실행하여 자신 또는 다른 사람의 데이터를 무료로 지속시킬 수 있습니다! + +- [IPFS](https://docs.ipfs.io/concepts/what-is-ipfs/) +- [Pinata](https://www.pinata.cloud/) _(IPFS 피닝 서비스)_ +- [web3.storage](https://web3.storage/) _(IPFS/Filecoin 피닝 서비스)_ +- [Infura](https://infura.io/product/ipfs) _(IPFS 피닝 서비스)_ +- [IPFS Scan](https://ipfs-scan.io) _(IPFS 피닝 탐색기)_ +- [4EVERLAND](https://www.4everland.org/)_(IPFS 피닝 서비스)_ +- [Filebase](https://filebase.com) _(IPFS 피닝 서비스)_ +- [Spheron Network](https://spheron.network/) _(IPFS/Filecoin 피닝 서비스)_ + +SWARM은 저장 인센티브 시스템과 저장 임대 가격 예측 시스템을 갖춘 분산 데이터 저장 및 배포 기술입니다. + +## 데이터 보존 {#data-retention} + +데이터를 보존하기 위해, 시스템은 데이터가 확실히 보존되도록 하는 일종의 메커니즘을 가지고 있어야 합니다. + +### 챌린지 메커니즘 {#challenge-mechanism} + +확실히 데이터가 보존되도록 하는 가장 일반적인 방법 중 하나는, 노드에 전송되는 일종의 암호학적 문제를 사용하여 노드가 데이터를 가지고 있는지 확인하는 것입니다. 간단한 방법은 아르위브의 PoA를 살펴보는 것입니다. 이것은 노드에게 가장 최근 블록과 과거의 랜덤 블록 하나에 있는 데이터가 모두 있는지 확인하는 문제를 냅니다. 만약 노드가 답을 내놓지 못하면, 해당 노드는 불이익을 받습니다. + +챌린지 메커니즘을 갖춘 dStorage 유형: + +- Züs +- 스카이넷 +- 아르위브 +- 파일코인 +- Crust Network +- 4EVERLAND + +### 탈중앙성 {#decentrality} + +플랫폼의 분산화 수준을 측정할 수 있는 좋은 도구는 없지만, 일반적으로 KYC의 형태가 없는 도구를 사용하여 중앙집중화되지 않았다는 증거를 제시하고자 할 것입니다. + +KYC가 없는 탈중앙화 도구: + +- 스카이넷 +- 아르위브 +- 파일코인 +- IPFS +- 이더리움 +- Crust Network +- 4EVERLAND + +### 합의 {#consensus} + +이 도구 대부분은 자체 버전의 [합의 메커니즘](/developers/docs/consensus-mechanisms/)을 가지고 있지만, 일반적으로 [**작업 증명(PoW)**](/developers/docs/consensus-mechanisms/pow/) 또는 [**지분 증명(PoS)**](/developers/docs/consensus-mechanisms/pos/)에 기반합니다. + +작업 증명 기반: + +- 스카이넷 +- 아르위브 + +지분 증명 기반: + +- 이더리움 +- 파일코인 +- Züs +- Crust Network + +## 관련 도구 {#related-tools} + +**IPFS - _InterPlanetary File System은 이더리움을 위한 탈중앙화 저장 공간 및 파일 참조 시스템입니다._** + +- [Ipfs.io](https://ipfs.io/) +- [개발문서](https://docs.ipfs.io/) +- [GitHub](https://github.com/ipfs/ipfs) + +**Storj DCS - _개발자를 위한 안전하고, 프라이빗하며, S3와 호환되는 탈중앙화 클라우드 객체 저장 공간입니다._** + +- [Storj.io](https://storj.io/) +- [개발문서](https://docs.storj.io/) +- [GitHub](https://github.com/storj/storj) + +**Sia - _암호학을 활용하여 구매자와 판매자가 직접 거래할 수 있도록 하는 무신뢰 클라우드 저장 공간 마켓플레이스를 만듭니다._** + +- [Skynet.net](https://sia.tech/) +- [개발문서](https://docs.sia.tech/) +- [GitHub](https://github.com/SiaFoundation/) + +**Filecoin - _Filecoin은 IPFS를 개발한 바로 그 팀에서 만들었습니다. IPFS 이상 위에 있는 인센티브 계층입니다._** + +- [Filecoin.io](https://filecoin.io/) +- [개발문서](https://docs.filecoin.io/) +- [GitHub](https://github.com/filecoin-project/) + +**Arweave - _Arweave는 데이터 저장을 위한 dStorage 플랫폼입니다._** + +- [Arweave.org](https://www.arweave.org/) +- [개발문서](https://docs.arweave.org/info/) +- [Arweave](https://github.com/ArweaveTeam/arweave/) + +**Züs - _Züs는 샤딩과 블로버(blobber)를 갖춘 지분 증명 dStorage 플랫폼입니다._** + +- [zus.network](https://zus.network/) +- [개발문서](https://docs.zus.network/zus-docs/) +- [GitHub](https://github.com/0chain/) + +**Crust Network - _Crust는 IPFS 위에 구축된 dStorage 플랫폼입니다._** + +- [Crust.network](https://crust.network) +- [개발문서](https://wiki.crust.network) +- [GitHub](https://github.com/crustio) + +**Swarm - _이더리움 웹3 스택을 위한 분산형 저장 공간 플랫폼 및 콘텐츠 배포 서비스입니다._** + +- [EthSwarm.org](https://www.ethswarm.org/) +- [개발문서](https://docs.ethswarm.org/) +- [GitHub](https://github.com/ethersphere/) + +**OrbitDB - _IPFS 기반의 탈중앙화 P2P 데이터베이스입니다._** + +- [OrbitDB.org](https://orbitdb.org/) +- [개발문서](https://github.com/orbitdb/field-manual/) +- [GitHub](https://github.com/orbitdb/orbit-db/) + +**Aleph.im - _탈중앙화 클라우드 프로젝트(데이터베이스, 파일 저장 공간, 컴퓨팅 및 DID)입니다._** 오프체인과 온체인 P2P 기술의 독특한 조합. IPFS와 멀티체인 양립 가능._\*\* + +- [Aleph.im](https://aleph.cloud/) +- [개발문서](https://docs.aleph.cloud/) +- [GitHub](https://github.com/aleph-im/) + +**Ceramic - _데이터가 풍부하고 흥미로운 애플리케이션을 위한 사용자 제어 IPFS 데이터베이스 저장 공간입니다._** + +- [Ceramic.network](https://ceramic.network/) +- [개발문서](https://developers.ceramic.network/) +- [GitHub](https://github.com/ceramicnetwork/js-ceramic/) + +**Filebase - _S3와 호환되는 탈중앙화 저장 공간 및 지역 중복 IPFS 피닝 서비스. Filebase를 통해 IPFS에 업로드된 모든 파일은 전 세계에 걸쳐 3중으로 복제되어 Filebase 인프라에 자동으로 피닝됩니다._** + +- [Filebase.com](https://filebase.com/) +- [개발문서](https://docs.filebase.com/) +- [GitHub](https://github.com/filebase) + +**4EVERLAND - _저장 공간, 컴퓨팅, 네트워킹 핵심 기능을 통합한 웹3.0 클라우드 컴퓨팅 플랫폼으로, S3와 호환되며 IPFS, Arweave와 같은 탈중앙화 저장 공간 네트워크에서 동기식 데이터 저장을 제공합니다._** + +- [4everland.org](https://www.4everland.org/) +- [개발문서](https://docs.4everland.org/) +- [GitHub](https://github.com/4everland) + +**Kaleido - _클릭 버튼으로 IPFS 노드를 제공하는 서비스형 블록체인(BaaS) 플랫폼입니다._** + +- [Kaleido](https://kaleido.io/) +- [개발문서](https://docs.kaleido.io/kaleido-services/ipfs/) +- [GitHub](https://github.com/kaleido-io) + +**Spheron Network - _Spheron은 최고의 성능을 갖춘 탈중앙화 인프라에서 애플리케이션을 출시하려는 탈중앙화앱을 위해 설계된 서비스형 플랫폼(PaaS)입니다. 컴퓨팅, 탈중앙화 저장 공간, CDN 및 웹 호스팅을 기본으로 제공합니다._** + +- [spheron.network](https://spheron.network/) +- [개발문서](https://docs.spheron.network/) +- [GitHub](https://github.com/spheronFdn) + +## 더 읽어보기 {#further-reading} + +- [탈중앙화 저장 공간이란?](https://coinmarketcap.com/academy/article/what-is-decentralized-storage-a-deep-dive-by-filecoin) - _CoinMarketCap_ +- [탈중앙화 저장 공간에 대한 5가지 일반적인 오해 바로잡기](https://www.storj.io/blog/busting-five-common-myths-about-decentralized-storage) - _Storj_ + +_도움이 되었던 커뮤니티 참고 자료를 알고 계신가요? 이 페이지를 편집해서 추가하세요!_ + +## 관련 주제 {#related-topics} + +- [개발 프레임워크](/developers/docs/frameworks/) diff --git a/public/content/translations/ko/developers/docs/transactions/index.md b/public/content/translations/ko/developers/docs/transactions/index.md new file mode 100644 index 00000000000..350178e6850 --- /dev/null +++ b/public/content/translations/ko/developers/docs/transactions/index.md @@ -0,0 +1,231 @@ +--- +title: "트랜잭션" +description: "이더리움 트랜잭션의 개요 - 어떻게 동작하는지, 데이터 구조, 그리고 애플리케이션을 통해 어떻게 전송되는지" +lang: ko +--- + +트랜잭션은 계정으로부터 암호학적으로 서명된 명령들이다. 계정은 이더리움 네트워크의 상태를 업데이트하기 위해 트랜잭션을 초기화 할 것이다. 가장 간단한 트랜잭션은 한 계정에서 다른 계정으로 ETH를 보내는 것이다. + +## 필수 구성 요소 {#prerequisites} + +이 페이지를 더 잘 이해할 수 있도록 [계정](/developers/docs/accounts/) 및 [이더리움 소개](/developers/docs/intro-to-ethereum/)를 먼저 읽어보시는 것을 권장합니다. + +## 트랜잭션이란 무엇인가? 트랜잭션이란 무엇인가요? {#whats-a-transaction} + +이더리움 트랜잭션은 외부 계정, 즉 컨트랙트가 아니라 사람이 관리하는 계정에 의해 초기화된 하나의 동작을 말한다. 예를 들어, 밥이 앨리스에게 1 ETH를 보낸다면, 밥의 계정은 인출되어야 하고, 앨리스의 것은 입금되어야 한다. 이 상태-변경 동작은 트랜잭션 안에서 이루어진다. + +![트랜잭션으로 인한 상태 변화를 보여주는 다이어그램](./tx.png) +_[Ethereum EVM illustrated](https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf)에서 발췌한 다이어그램_ + +EVM의 상태를 변경하는 트랜잭션은 전체 네트워크로 브로드캐스트되어야 한다. 어떤 노드든 EVM 위에서 실행되는 트랜잭션을 위한 요청을 브로드캐스트 할 수 있다. 이 과정을 거치고나서, 검증자는 트랜잭션을 실행하고 상태변경 결과를 나머지 네트워크로 전파한다. + +트랜잭션은 수수료가 필요하고 검증된 블록에 포함되어 있어야한다. 이 개요를 더 간단히 하기 위해서 우리는 다른곳에서 가스 수수료와 검증에 대해서 다룰 것이다. + +제출된 트랜잭션은 다음 정보를 갖는다. + +- `from` – 트랜잭션에 서명할 보내는 사람의 주소입니다. 컨트랙트 계정은 트랜잭션을 보낼 수 없기 때문에 외부계정에만 존재한다 +- `to` – 받는 주소(외부 소유 계정인 경우, 트랜잭션이 값을 전송합니다). 컨트랙트 계정이라면 트랜잭션은 컨트랙트 코드를 실행할 것이다) +- `signature` – 보내는 사람의 식별자입니다. 이것은 송신자의 개인 키로 트랜잭션을 서명하고 송신자가 트랜잭션을 인증함을 확정하면 생성된다. +- `nonce` - 계정의 트랜잭션 수를 나타내는 순차적으로 증가하는 카운터입니다. +- `value` – 보내는 사람에서 받는 사람으로 전송할 ETH 금액(WEI 단위이며, 1ETH는 1e+18wei와 같습니다). +- `input data` – 임의의 데이터를 포함하기 위한 선택적 필드입니다. +- `gasLimit` – 트랜잭션이 소비할 수 있는 가스 단위의 최대량입니다. [EVM](/developers/docs/evm/opcodes)은 각 계산 단계에 필요한 가스 단위를 명시합니다. +- `maxPriorityFeePerGas` - 검증인에게 팁으로 포함될 소비된 가스의 최대 가격입니다. +- `maxFeePerGas` - 트랜잭션에 대해 지불하고자 하는 가스 단위당 최대 수수료(`baseFeePerGas` 및 `maxPriorityFeePerGas` 포함)입니다. + +가스는 채굴자에 의해 트랜잭션을 처리하는데 필요한 컴퓨팅 계산에 대한 참조이다. 사용자들은 이 컴퓨팅에 수수료를 지불해야 한다. `gasLimit`과 `maxPriorityFeePerGas`는 검증인에게 지불되는 최대 트랜잭션 수수료를 결정합니다. [가스에 대한 자세한 정보](/developers/docs/gas/). + +트랜잭션 객체는 아래처럼 보일 것이다. + +```js +{ + from: "0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8", + to: "0xac03bb73b6a9e108530aff4df5077c2b3d481e5a", + gasLimit: "21000", + maxFeePerGas: "300", + maxPriorityFeePerGas: "10", + nonce: "0", + value: "10000000000" +} +``` + +그러나 트랜잭션 객체는 송신자의 개인 키를 사용해서 서명되어야 한다. 이는 그 트랜잭션이 그 송신자로부터만 왔고, 부정하게 보내지지 않았음을 증명한다. + +Geth 같은 이더리움 클라이언트가 이 서명 과정을 처리할 것이다. + +[JSON-RPC](/developers/docs/apis/json-rpc) 호출 예시: + +```json +{ + "id": 2, + "jsonrpc": "2.0", + "method": "account_signTransaction", + "params": [ + { + "from": "0x1923f626bb8dc025849e00f99c25fe2b2f7fb0db", + "gas": "0x55555", + "maxFeePerGas": "0x1234", + "maxPriorityFeePerGas": "0x1234", + "input": "0xabcd", + "nonce": "0x0", + "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", + "value": "0x1234" + } + ] +} +``` + +예시 응답: + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "raw": "0xf88380018203339407a565b7ed7d7a678680a4c162885bedbb695fe080a44401a6e4000000000000000000000000000000000000000000000000000000000000001226a0223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20ea02aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", + "tx": { + "nonce": "0x0", + "maxFeePerGas": "0x1234", + "maxPriorityFeePerGas": "0x1234", + "gas": "0x55555", + "to": "0x07a565b7ed7d7a678680a4c162885bedbb695fe0", + "value": "0x1234", + "input": "0xabcd", + "v": "0x26", + "r": "0x223a7c9bcf5531c99be5ea7082183816eb20cfe0bbc322e97cc5c7f71ab8b20e", + "s": "0x2aadee6b34b45bb15bc42d9c09de4a6754e7000908da72d48cc7704971491663", + "hash": "0xeba2df809e7a612a0a0d444ccfa5c839624bdc00dd29e3340d46df3870f8a30e" + } + } +} +``` + +- `raw`는 [RLP(Recursive Length Prefix)](/developers/docs/data-structures-and-encoding/rlp)로 인코딩된 서명된 트랜잭션입니다. +- `tx`는 JSON 형식의 서명된 트랜잭션입니다. + +해시를 서명함으로써, 트랜잭션이 네트워크로부터 제출되었음과 송신자로부터 왔음을 암호학적으로 입증할 수 있다. + +### 데이터 필드 {#the-data-field} + +대부분의 트랜잭션은 외부 소유 계정의 컨트랙트를 액세스한다. +대부분의 컨트랙트는 솔리디티(Solidity)로 작성되며, [ABI(애플리케이션 바이너리 인터페이스)](/glossary/#abi)에 따라 데이터 필드를 해석합니다. + +처음 4 바이트는 함수 이름과 arguments의 해시를 사용하여 호출할 함수를 명시합니다. +[이 데이터베이스](https://www.4byte.directory/signatures/)를 사용하여 셀렉터로부터 함수를 식별할 수 있습니다. + +나머지 calldata는 인자이며, [ABI 사양에 명시된 대로 인코딩됩니다](https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding). + +예를 들어, [이 트랜잭션](https://etherscan.io/tx/0xd0dcbe007569fcfa1902dae0ab8b4e078efe42e231786312289b1eee5590f6a1)을 살펴보겠습니다. +calldata를 보려면 **Click to see More**를 사용하세요. + +함수 셀렉터는 `0xa9059cbb`입니다. [이 서명을 가진 여러 알려진 함수](https://www.4byte.directory/signatures/?bytes4_signature=0xa9059cbb)가 있습니다. +이 경우 [컨트랙트 소스 코드](https://etherscan.io/address/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48#code)가 Etherscan에 업로드되었으므로 함수가 `transfer(address,uint256)`임을 알 수 있습니다. + +나머지 데이터는: + +``` +0000000000000000000000004f6742badb049791cd9a37ea913f2bac38d01279 +000000000000000000000000000000000000000000000000000000003b0559f4 +``` + +ABI 명세서에 따르면, 정수값 (예를 들어 20바이트 정수인 주소) 은 ABI에서 앞에 0이 추가된 32 바이트 단어로 표시된다. +따라서 `to` 주소는 [`4f6742badb049791cd9a37ea913f2bac38d01279`](https://etherscan.io/address/0x4f6742badb049791cd9a37ea913f2bac38d01279)임을 알 수 있습니다. +`value`는 0x3b0559f4 = 990206452입니다. + +## 트랜잭션 유형 {#types-of-transactions} + +이더리움에서는 다른 몇가지 다른 유형의 트랜잭션이 있다. + +- 보통의 트랜잭션: 한 계정에서 다른 계정으로의 트랜잭션 +- Contract deployment transactions: 'to' address 가 없는 트랜잭션으로, 데이터 필드가 컨트랙트 코드로 사용된다. +- 컨트랙트의 실행: 배포된 스마트 컨트랙트와 상호작용하는 트랜잭션이다. 이 경우, 'to' 주소는 스마트 컨트랙트의 주소이다. + +### 가스에 대하여 {#on-gas} + +앞서 언급했듯이 트랜잭션을 실행하려면 [가스](/developers/docs/gas/)가 필요합니다. 간단한 전송 트랜잭션은 21000 단위의 가스가 필요하다. + +따라서 밥이 앨리스에게 `baseFeePerGas` 190gwei와 `maxPriorityFeePerGas` 10gwei로 1ETH를 보내려면, 밥은 다음 수수료를 지불해야 합니다: + +``` +(190 + 10) * 21000 = 4,200,000 gwei +--또는-- +0.0042 ETH +``` + +밥의 계정에서 **-1.0042 ETH**가 인출됩니다(앨리스에게 1 ETH + 가스 수수료 0.0042 ETH). + +앨리스의 계정에 **+1.0 ETH**가 입금됩니다. + +**-0.00399 ETH**의 기본 수수료는 소각됩니다. + +검증인은 팁으로 **+0.000210 ETH**를 받습니다. + +![사용되지 않은 가스가 환불되는 방식을 보여주는 다이어그램](./gas-tx.png) +_[Ethereum EVM illustrated](https://takenobu-hs.github.io/downloads/ethereum_evm_illustrated.pdf)에서 발췌한 다이어그램_ + +트랜잭션 내에서 사용되지 않은 가스는 사용자 계정으로 환불된다. + +### 스마트 계약 상호작용 {#smart-contract-interactions} + +스마트 계약과 관련된 모든 트랜잭션에는 가스가 필요합니다. + +스마트 계약에는 컨트랙트의 상태를 변경하지 않는 [`view`](https://docs.soliditylang.org/en/latest/contracts.html#view-functions) 또는 [`pure`](https://docs.soliditylang.org/en/latest/contracts.html#pure-functions) 함수로 알려진 함수가 포함될 수도 있습니다. 따라서 EOA에서 이러한 함수를 호출할 때 가스가 필요하지 않습니다. 이 시나리오의 기본 RPC 호출은 [`eth_call`](/developers/docs/apis/json-rpc#eth_call)입니다. + +`eth_call`을 사용하여 액세스할 때와 달리, 이러한 `view` 또는 `pure` 함수는 내부적으로(즉, 컨트랙트 자체 또는 다른 컨트랙트에서) 호출될 때도 가스를 소모합니다. + +## 트랜잭션 생명주기 {#transaction-lifecycle} + +트랜잭션이 제출되면 다음이 일어난다. + +1. 트랜잭션 해시는 암호화 방식으로 생성됩니다: + `0x97d99bc7729211111a21b12c933c949d4f31684f1d6954ff477d0477538ff017` +2. 트랜잭션은 네트워크로 브로드캐스트 되고 다른 모든 보류 중인 네트워크 트랜잭션으로 구성된 트랜잭션 풀에 추가됩니다. +3. 검증인은 트랜잭션을를 입증하고 성공적이라고 처리하기 위해 트랜잭션을 선택하고 이를 블록에 포함해야 한다. +4. 시간이 지날 수록 블록에 포함된 당신의 트랜잭션은 "justified" 에서 "finalized"로 업그레이드 될 것이다. 이러한 업그레이드를 통해 트랜잭션이 성공적으로 완료되고 절대 변경되지 않을 것이라고 훨씬 더 확신할 수 있습니다. 블록이 "완료(finalized)"되면 수십억 달러의 비용이 드는 네트워크 수준의 공격에 의해서만 변경될 수 있습니다. + +## 시각적 데모 {#a-visual-demo} + +오스틴의 트랜잭션, 가스, 그리고 채굴에 대한 안내를 보라. + + + +## 유형화된 트랜잭션 봉투 {#typed-transaction-envelope} + +이더리움은 원래 트랜잭션 형식이 하나였다. 각 트랜잭션은 nonce, gas price, gas limit, to address, value, data, v, r, 와 s 를 포함 되어있었다. 이 필드들은 [RLP 인코딩](/developers/docs/data-structures-and-encoding/rlp/)되며, 다음과 같은 형식을 가집니다: + +`RLP([nonce, gasPrice, gasLimit, to, value, data, v, r, s])` + +이더리움은 기존 트랜잭션 형식에 영향을 주지 않고 액세스 목록 및 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)와 같은 새로운 기능을 구현할 수 있도록 여러 유형의 트랜잭션을 지원하도록 발전했습니다. + +[EIP-2718](https://eips.ethereum.org/EIPS/eip-2718)은 이러한 동작을 가능하게 합니다. 트랜잭션들은 다음과 같이 해석된다: + +`TransactionType || TransactionPayload` + +필드는 다음과 같이 정의된다. + +- `TransactionType` - 0과 0x7f 사이의 숫자로, 총 128개의 가능한 트랜잭션 유형을 나타냅니다. +- `TransactionPayload` - 트랜잭션 유형에 따라 정의되는 임의의 바이트 배열입니다. + +`TransactionType` 값에 따라 트랜잭션을 다음과 같이 분류할 수 있습니다. + +1. **유형 0(레거시) 트랜잭션:** 이더리움 출시 이후 사용된 원래 트랜잭션 형식입니다. 이 트랜잭션은 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)의 동적 가스 수수료 계산이나 스마트 계약용 액세스 목록과 같은 기능을 포함하지 않습니다. 레거시 트랜잭션은 직렬화된 형태에서 유형을 나타내는 특정 접두사가 없으며, [RLP(Recursive Length Prefix)](/developers/docs/data-structures-and-encoding/rlp) 인코딩을 사용할 때 바이트 `0xf8`로 시작합니다. 이러한 트랜잭션의 TransactionType 값은 `0x0`입니다. + +2. **유형 1 트랜잭션:** 이더리움의 [베를린 업그레이드](/ethereum-forks/#berlin)의 일부로 [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930)에서 도입되었으며, 이 트랜잭션에는 `accessList` 매개변수가 포함됩니다. 이 목록은 트랜잭션이 액세스할 것으로 예상되는 주소와 스토리지 키를 지정하여 스마트 계약과 관련된 복잡한 트랜잭션의 [가스](/developers/docs/gas/) 비용을 줄이는 데 도움이 될 수 있습니다. 유형 1 트랜잭션에는 EIP-1559 수수료 시장 변경 사항이 포함되지 않습니다. 유형 1 트랜잭션에는 `yParity` 매개변수도 포함되며, `0x0` 또는 `0x1`일 수 있으며 secp256k1 서명의 y-값의 패리티를 나타냅니다. 이 트랜잭션은 바이트 `0x01`로 시작하여 식별되며, TransactionType 값은 `0x1`입니다. + +3. **유형 2 트랜잭션**은 일반적으로 EIP-1559 트랜잭션이라고 하며, 이더리움의 [런던 업그레이드](/ethereum-forks/#london)에서 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)의 일부로 도입되었습니다. 이 트랜잭션들은 이더리움 네트워크의 표준 트랜잭션 유형이 되었습니다. 이 트랜잭션들은 새로운 수수료 시장 메커니즘을 도입하여 기본 수수료와 우선 수수료로 분리하여 수수료 예측성을 향상시킵니다. 이 트랜잭션은 바이트 `0x02`로 시작하며 `maxPriorityFeePerGas` 및 `maxFeePerGas`와 같은 필드를 포함합니다. 유형 2 트랜잭션은 유연성과 효율성 덕분에 현재 기본 설정이며, 특히 높은 네트워크 혼잡 시 사용자들이 트랜잭션 수수료를 보다 예측 가능하게 관리할 수 있게 해줍니다. 이러한 트랜잭션의 TransactionType 값은 `0x2`입니다. + +4. **유형 3(블롭) 트랜잭션**은 이더리움의 [덴쿤 업그레이드](/ethereum-forks/#dencun)의 일부로 [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844)에서 도입되었습니다. 이 트랜잭션은 "블롭" 데이터(바이너리 대규모 개체)를 보다 효율적으로 처리하도록 설계되었으며, 특히 이더리움 네트워크에 더 저렴한 비용으로 데이터를 게시하는 방법을 제공하여 레이어 2 롤업에 이점을 줍니다. 블롭 트랜잭션에는 `blobVersionedHashes`, `maxFeePerBlobGas`, `blobGasPrice`와 같은 추가 필드가 포함됩니다. 이 트랜잭션은 바이트 `0x03`으로 시작하며, TransactionType 값은 `0x3`입니다. 블롭 트랜잭션은 이더리움의 데이터 가용성 및 확장성 기능에 있어 상당한 개선을 의미합니다. + +5. **유형 4 트랜잭션**은 이더리움의 [펙트라 업그레이드](/roadmap/pectra/)의 일부로 [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702)에서 도입되었습니다. 이러한 트랜잭션은 계정 추상화와 순방향 호환이 가능하도록 설계되었습니다. 이러한 트랜잭션을 사용하면 EOA가 원래 기능을 손상시키지 않으면서 일시적으로 스마트 계약 계정처럼 작동할 수 있습니다. `authorization_list` 매개변수를 포함하며, EOA가 권한을 위임하는 스마트 계약을 지정합니다. 트랜잭션 후 EOA의 코드 필드에는 위임된 스마트 계약의 주소가 포함됩니다. + +## 더 읽어보기 {#further-reading} + +- [EIP-2718: 유형화된 트랜잭션 봉투](https://eips.ethereum.org/EIPS/eip-2718) + +_도움이 되었던 커뮤니티 참고 자료를 알고 계신가요? 이 페이지를 편집해서 추가하세요!_ + +## 관련 주제 {#related-topics} + +- [계정](/developers/docs/accounts/) +- [이더리움 가상 머신(EVM)](/developers/docs/evm/) +- [가스](/developers/docs/gas/) diff --git a/public/content/translations/ko/developers/docs/web2-vs-web3/index.md b/public/content/translations/ko/developers/docs/web2-vs-web3/index.md new file mode 100644 index 00000000000..24885502b23 --- /dev/null +++ b/public/content/translations/ko/developers/docs/web2-vs-web3/index.md @@ -0,0 +1,62 @@ +--- +title: "Web2와 Web3의 비교" +description: "중앙화된 웹2 서비스와 이더리움 블록체인 기술을 기반으로 구축된 탈중앙화 웹3 애플리케이션을 비교해 보세요." +lang: ko +--- + +웹2는 오늘날 대부분의 사람들이 알고 있는 인터넷 버전을 의미합니다. 인터넷은 여러분의 개인 정보를 이용하여 서비스를 제공하는 회사들이 지배합니다. 이더리움의 맥락에서 볼 때 웹3은 블록체인 상에서 구동되는 분산형 앱을 뜻합니다. 웹3은 사용자의 개인 정보를 이용하여 수익 창출을 하지 않고도 누구나 참여할 수 있도록 하는 앱입니다. + +입문자에게 더 친화적인 자료를 찾고 있나요? [웹3 소개](/web3/)를 확인해 보세요. + +## 웹3의 이점 {#web3-benefits} + +많은 웹3 개발자들은 이더리움에 기본적으로 내장된 분산형 방식 때문에 디앱을 구축하기로 했습니다. + +- 네트워크에 있는 누구나 서비스를 사용할 권한이 있으며, 다시 말해 서비스 사용에는 권힌이 필요 없다는 뜻입니다. +- 아무도 여러분을 막을 수도 그 서비스의 접근을 거부할 수도 없다. +- 결제는 기본 토큰인 이더(ETH)를 통해 이루어집니다. +- 이더리움은 튜링-완전으로 대부분의 프로그래밍을 할 수 있다. + +## 실용적인 비교 {#practical-comparisons} + +| 웹2 | 웹3 | +| --------------------------------------- | ------------------------------------------------------------------- | +| 트위터는 모든 계정이나 트윗을 검열할 수 있습니다 | 웹3 트윗은 분산형이기 때문에 검열할 수 없을 것입니다 | +| 지불 서비스는 특정 종류의 일에는 지불을 허용하지 않도록 정할 수 있다 | 웹3 지불 앱들은 개인 데이터가 필요 없고 지불을 막을 수 없다 | +| 긱 이코노미 앱들의 서버들이 다운되어 작업자 수입에 영향을 줄 수 있다 | 웹3 서버들은 다운될 수 없다 - 그들은 그들의 백엔드로 1000여대의 컴퓨터들의 탈중앙화 네트워크인 이더리움을 사용한다 | + +이는 모든 서비스를 디앱으로 전환해야 함을 의미하지는 않습니다. 위의 예시들은 웹2와 웹3 서비스의 주요 차이점을 설명할 뿐입니다. + +## 웹3의 한계 {#web3-limitations} + +웹3에는 현재 다음과 같은 한계가 있습니다. + +- 확장성 - 웹3 상에서의 거래는 분산 방식으로 이루어지기 때문에 좀 더 느립니다. 지불과 같은 상태로의 변경은 채굴자를 통해 처리 후, 네트워크를 통해 전파된다. +- UX – 웹3 애플리케이션과 상호작용하려면 추가적인 단계, 소프트웨어, 교육이 필요할 수 있습니다. 이는 받아들이는데 허들이 될 수 있다. +- 접근성 - 웹브라우저들에 통합된 기능들이 부족하여, web3 접근성이 대부분의 사용자들에게 부족합니다. +- 비용 - 대부분의 성공적인 디앱들은 비용이 비싸서 매우 작은 양의 코드만 블록체인 상에 둔다. + +## 중앙화 vs. 탈중앙화 {#centralization-vs-decentralization} + +아래의 테이블에서 우리는 중앙화와 탈중앙화 디지털 네트워크의 대략적인 장점과 단점을 나열했다. + +| 중앙화 시스템 | 분산형 시스템 | +| -------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| 적은 네트워크 거리 (모든 참여자들이 중앙 기관에 연결된다); 전파는 중앙 기관에 의해 많은 컴퓨터 자원으로 처리 되므로 정보는 빠르게 전파된다. | 네트워크 상의 멀리 떨어진 참여자들은 잠재적으로 서로로부터 멀리 떨어진 많은 모서리들이 될 수 있다. 네트워크 상의 한 쪽으로부터의 정보 브로드캐스트는 다른 쪽까지 닿는데 긴 시간이 걸릴 수 있다. | +| 보통 더 높은 성능(더 높은 처리량, 더 적은 확장된 전체 컴퓨팅 자원)과 더 쉬운 구현. | 보통 더 낮은 성능(더 낮은 처리량, 더 많은 확장된 전체 컴퓨팅 자원)과 구현의 복잡함. | +| 데이터의 충돌 시, 해결이 깔끔하고 쉬움: 최종적인 참이 되는 것은 중앙 기관이다. | 참여자들이 동기화된 것으로 믿는 데이터의 상태에 대해 피어들이 충돌하도록 요청한다면 (대개 복잡한) 프로토콜이 해결을 중재하는데 필요하다. | +| 하나의 실패 지점: 악의적인 사람들이 중앙 기관을 표적 삼아 네트워크를 가동 중지시킬 수 있습니다. | 실패 지점이 하나가 아님: 많은 사람들이 참여하여 공격하거나 가동 중지 시켜도 네트워크는 계속 작동합니다. | +| 네트워크 참어자들간의 조정이 훨씬 쉽고, 중앙 기관에 의해 다뤄진다. 중앙 기관은 네트워크 참여자들이 업그레이드, 프로토콜 업데이트 등을 받아들이도록 매우 적은 노력으로 할 수 있다. | 네트워크 레벨 결정과 프로토콜 업그레이드 등에 최종 의견을 가지는 하나의 중개자가 없어서 조정이 대개 어렵다. 최악의 경우에는 프토토콜 변경에 대해 비동의가 있을 때 네트워크가 균열되기 쉽다. | +| 중앙 기관은 데이터를 검열할 수 있고, 잠재적으로 네트워크의 나머지와 협의해서 네트워크의 일부를 잘라버릴 수 있다. | 정보가 네트워크에 걸쳐 많은 방식으로 전파되어서 검열이 매우 더 어렵다. | +| 네트워크 내의 참여는 중앙 기관에 의해 제어된다. | 누구나 네트워크 안에 참여할 수 있다; “문지기”가 없다. 이상적으로는, 참여 비용이 매우 낮다. | + +모든 네트워크에 참이 될 수 없는 일반적인 패턴이 있음에 주의하라. 더 나아가서 실제로 어떤 네트워크가 중앙화/탈중앙화되어 있는지의 정도는 스펙트럼에 달려 있다; 전부 중앙화된 네트워크는 없다 또는 전부 탈중앙화된 네트워크는 없다. + +## 더 읽어보기 {#further-reading} + +- [웹3란 무엇인가요?](/web3/) - _ethereum.org_ +- [웹 3.0 애플리케이션의 아키텍처](https://www.preethikasireddy.com/post/the-architecture-of-a-web-3-0-application) - _Preethi Kasireddy_ +- [탈중앙화의 의미](https://medium.com/@VitalikButerin/the-meaning-of-decentralization-a0c92b76a274) _2017년 2월 6일 - 비탈릭 부테린_ +- [탈중앙화가 중요한 이유](https://onezero.medium.com/why-decentralization-matters-5e3f79f7638e) _2018년 2월 18일 - 크리스 딕슨_ +- [웹 3.0이란 무엇이며 왜 중요한가](https://medium.com/fabric-ventures/what-is-web-3-0-why-it-matters-934eb07f3d2b) _2019년 12월 31일 - 막스 머쉬, 리차드 뮤어헤드_ +- [왜 우리에게 웹 3.0이 필요한가](https://gavofyork.medium.com/why-we-need-web-3-0-5da4f2bf95ab) _2018년 9월 12일 - 개빈 우드_ diff --git a/public/content/translations/ko/developers/tutorials/a-developers-guide-to-ethereum-part-one/index.md b/public/content/translations/ko/developers/tutorials/a-developers-guide-to-ethereum-part-one/index.md new file mode 100644 index 00000000000..0a17eb6e9b9 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/a-developers-guide-to-ethereum-part-one/index.md @@ -0,0 +1,300 @@ +--- +title: "Python 개발자를 위한 이더리움 소개, 1부" +description: "Python 프로그래밍 언어에 대한 지식이 있는 분들에게 특히 유용한 이더리움 개발 입문서입니다." +author: Marc Garreau +lang: ko +tags: [ "python", "web3.py" ] +skill: beginner +published: 2020-09-08 +source: Snake charmers +sourceUrl: https://snakecharmers.ethereum.org/a-developers-guide-to-ethereum-pt-1/ +--- + +이더리움에 대해 들어보셨나요? 그렇다면 이제 토끼굴로 모험을 떠날 준비가 되셨나요? 이 게시물에서는 블록체인의 몇 가지 기본 사항을 간략히 다룬 다음, 시뮬레이션된 이더리움 노드와 상호 작용하여 블록 데이터 읽기, 계정 잔액 확인, 트랜잭션 전송 등을 수행하도록 안내합니다. 그 과정에서 기존의 앱 구축 방식과 이 새로운 탈중앙화 패러다임의 차이점을 중점적으로 살펴보겠습니다. + +## 권장 선수 지식 {#soft-prerequisites} + +이 게시물은 광범위한 개발자가 접근할 수 있도록 하는 것을 목표로 합니다. [Python 도구](/developers/docs/programming-languages/python/)가 사용되지만, 아이디어를 전달하기 위한 수단일 뿐이므로 Python 개발자가 아니어도 문제없습니다. 하지만, 독자 여러분이 이미 알고 있는 몇 가지 사항을 가정하고 이더리움 관련 내용으로 빠르게 넘어가겠습니다. + +가정 사항: + +- 터미널을 다룰 수 있어야 합니다. +- Python 코드를 몇 줄 작성해 본 경험이 있어야 합니다. +- 컴퓨터에 Python 3.6 이상 버전이 설치되어 있어야 합니다([가상 환경](https://realpython.com/effective-python-environment/#virtual-environments) 사용을 적극 권장합니다). +- Python의 패키지 설치 프로그램인 `pip`를 사용해 본 경험이 있어야 합니다. + 다시 한번 말씀드리지만, 이 중 어느 하나라도 해당하지 않거나 이 글의 코드를 재현할 계획이 없더라도 내용을 따라가는 데는 큰 무리가 없을 것입니다. + +## 간단히 알아보는 블록체인 {#blockchains-briefly} + +이더리움을 설명하는 방법은 여러 가지가 있지만, 그 중심에는 블록체인이 있습니다. 블록체인은 일련의 블록으로 구성되어 있으니, 여기서부터 시작하겠습니다. 가장 간단한 용어로, 이더리움 블록체인의 각 블록은 약간의 메타데이터와 트랜잭션 목록에 불과합니다. JSON 형식으로는 다음과 같이 보입니다. + +```json +{ + "number": 1234567, + "hash": "0xabc123...", + "parentHash": "0xdef456...", + ..., + "transactions": [...] +} +``` + +각 [블록](/developers/docs/blocks/)은 그 이전 블록에 대한 참조를 가지며, `parentHash`는 단순히 이전 블록의 해시입니다. + +참고: 이더리움은 고정 크기 값('해시')을 생성하기 위해 해시 함수를 정기적으로 사용합니다. 해시는 이더리움에서 중요한 역할을 하지만, 지금은 고유 ID라고 생각해도 무방합니다. + +![각 블록 내부의 데이터를 포함한 블록체인 다이어그램](./blockchain-diagram.png) + +_블록체인은 본질적으로 각 블록이 이전 블록을 참조하는 연결 리스트입니다._ + +이 데이터 구조는 새로운 것이 아니지만, 네트워크를 관장하는 규칙(즉, P2P 프로토콜)은 새롭습니다. 중앙 기관이 없습니다. 피어들의 네트워크는 네트워크를 유지하기 위해 협력해야 하며, 다음 블록에 어떤 트랜잭션을 포함할지 결정하기 위해 경쟁해야 합니다. 따라서 친구에게 돈을 보내려면 해당 트랜잭션을 네트워크에 브로드캐스트한 다음, 다가오는 블록에 포함될 때까지 기다려야 합니다. + +블록체인이 한 사용자에게서 다른 사용자에게로 돈이 실제로 전송되었는지 확인하는 유일한 방법은 해당 블록체인 고유의(즉, 해당 블록체인에 의해 생성 및 관리되는) 통화를 사용하는 것입니다. 이더리움에서 이 통화는 이더(ether)라고 하며, 이더리움 블록체인에는 계정 잔액에 대한 유일한 공식 기록이 포함되어 있습니다. + +## 새로운 패러다임 {#a-new-paradigm} + +이 새로운 탈중앙화 기술 스택은 새로운 개발자 도구를 탄생시켰습니다. 이러한 도구는 많은 프로그래밍 언어에 존재하지만, 우리는 Python의 관점에서 살펴보겠습니다. 다시 한번 강조하지만, Python이 선호하는 언어가 아니더라도 따라가는 데 큰 어려움은 없을 것입니다. + +이더리움과 상호 작용하려는 Python 개발자는 [Web3.py](https://web3py.readthedocs.io/)를 사용할 가능성이 높습니다. Web3.py는 이더리움 노드에 연결하고 데이터를 주고받는 방식을 크게 단순화하는 라이브러리입니다. + +참고: '이더리움 노드'와 '이더리움 클라이언트'는 같은 의미로 사용됩니다. 두 경우 모두 이더리움 네트워크 참여자가 실행하는 소프트웨어를 의미합니다. 이 소프트웨어는 블록 데이터를 읽고, 새로운 블록이 체인에 추가될 때 업데이트를 수신하고, 새로운 트랜잭션을 브로드캐스트하는 등 다양한 작업을 수행할 수 있습니다. 기술적으로, 클라이언트는 소프트웨어이고, 노드는 소프트웨어를 실행하는 컴퓨터입니다. + +[이더리움 클라이언트](/developers/docs/nodes-and-clients/)는 [IPC](https://wikipedia.org/wiki/Inter-process_communication), HTTP 또는 웹소켓으로 연결할 수 있도록 구성할 수 있으므로, Web3.py도 이 구성을 미러링해야 합니다. Web3.py는 이러한 연결 옵션을 \*\*공급자(providers)\*\*라고 부릅니다. Web3.py 인스턴스를 노드에 연결하려면 세 가지 공급자 중 하나를 선택해야 합니다. + +![web3.py가 IPC를 사용하여 애플리케이션을 이더리움 노드에 연결하는 방법을 보여주는 다이어그램](./web3py-and-nodes.png) + +_이더리움 노드와 Web3.py가 동일한 프로토콜(예: 이 다이어그램의 IPC)을 통해 통신하도록 구성하세요._ + +Web3.py가 제대로 구성되면 블록체인과 상호 작용을 시작할 수 있습니다. 앞으로 나올 내용에 대한 미리보기로 Web3.py 사용 예시 몇 가지를 소개합니다. + +```python +# 블록 데이터 읽기: +w3.eth.get_block('latest') + +# 트랜잭션 전송: +w3.eth.send_transaction({'from': ..., 'to': ..., 'value': ...}) +``` + +## 설치 {#installation} + +이번 연습에서는 Python 인터프리터 내에서만 작업할 것입니다. 디렉터리, 파일, 클래스, 함수 등은 생성하지 않습니다. + +참고: 아래 예제에서 `$`로 시작하는 명령어는 터미널에서 실행되도록 의도된 것입니다. (`$`는 입력하지 마세요. 줄의 시작을 의미할 뿐입니다.) + +먼저, 탐색하기 쉬운 사용자 친화적 환경을 위해 [IPython](https://ipython.org/)을 설치하세요. IPython은 다른 기능들 중에서도 탭 완성 기능을 제공하여 Web3.py 내에서 무엇이 가능한지 훨씬 쉽게 확인할 수 있도록 해줍니다. + +```bash +pip install ipython +``` + +Web3.py는 `web3`라는 이름으로 배포됩니다. 다음과 같이 설치하세요. + +```bash +pip install web3 +``` + +한 가지 더 말씀드리자면, 나중에 블록체인을 시뮬레이션할 건데, 몇 가지 의존성이 더 필요합니다. 다음을 통해 설치할 수 있습니다. + +```bash +pip install 'web3[tester]' +``` + +이제 모든 준비가 끝났습니다! + +참고: `web3[tester]` 패키지는 Python 3.10.xx 버전까지 작동합니다. + +## 샌드박스 실행하기 {#spin-up-a-sandbox} + +터미널에서 `ipython`을 실행하여 새로운 Python 환경을 엽니다. 이는 `python`을 실행하는 것과 비슷하지만, 더 많은 부가 기능을 제공합니다. + +```bash +ipython +``` + +실행 중인 Python 및 IPython 버전에 대한 정보가 출력된 후, 입력을 기다리는 프롬프트가 표시됩니다. + +```python +In [1]: +``` + +지금 대화형 Python 셸을 보고 계십니다. 기본적으로, 마음껏 테스트해볼 수 있는 샌드박스입니다. 여기까지 오셨다면 이제 Web3.py를 가져올 차례입니다. + +```python +In [1]: from web3 import Web3 +``` + +## Web3 모듈 소개 {#introducing-the-web3-module} + +[Web3](https://web3py.readthedocs.io/en/stable/overview.html#base-api) 모듈은 이더리움으로 가는 관문일 뿐만 아니라 몇 가지 편의 함수를 제공합니다. 몇 가지를 살펴보겠습니다. + +이더리움 애플리케이션에서는 보통 통화 단위를 변환해야 합니다. Web3 모듈은 바로 이 작업을 위한 몇 가지 헬퍼 메서드를 제공합니다. 바로 [from_wei](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.from_wei)와 [to_wei](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.to_wei)입니다. + + +참고: 컴퓨터는 소수점 연산을 잘 처리하지 못하는 것으로 악명 높습니다. 이 문제를 해결하기 위해 개발자들은 종종 달러 금액을 센트 단위로 저장합니다. 예를 들어, 가격이 $5.99인 품목은 데이터베이스에 599로 저장될 수 있습니다. + +이더로 트랜잭션을 처리할 때도 비슷한 패턴이 사용됩니다. 하지만 이더는 소수점 두 자리 대신 18자리를 가집니다! 이더의 가장 작은 단위는 wei라고 하며, 트랜잭션을 보낼 때 지정하는 값이 바로 이것입니다. + +1 이더 = 1,000,000,000,000,000,000 wei + +1 wei = 0.000000000000000001 이더 + + + +몇 가지 값을 wei와 주고받으며 변환해 보세요. 이더와 wei 사이에는 [많은 단위에 대한 이름이 있다](https://web3py.readthedocs.io/en/stable/troubleshooting.html#how-do-i-convert-currency-denominations)는 점에 유의하세요. 그중 더 잘 알려진 단위 중 하나는 **gwei**인데, 거래 수수료가 종종 이 단위로 표시되기 때문입니다. + +```python +In [2]: Web3.to_wei(1, 'ether') +Out[2]: 1000000000000000000 + +In [3]: Web3.from_wei(500000000, 'gwei') +Out[3]: Decimal('0.5') +``` + +Web3 모듈의 다른 유틸리티 메서드에는 데이터 형식 변환기(예: [`toHex`](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.toHex)), 주소 헬퍼(예: [`isAddress`](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.isAddress)), 해시 함수(예: [`keccak`](https://web3py.readthedocs.io/en/stable/web3.main.html#web3.Web3.keccak))가 포함됩니다. 이들 중 상당수는 시리즈의 뒷부분에서 다룰 것입니다. 사용 가능한 모든 메서드와 속성을 보려면, `Web3.`을 입력하고 IPython의 자동 완성 기능을 활용하세요. 마침표 뒤에 탭 키를 두 번 누르세요. + +## 체인과 소통하기 {#talk-to-the-chain} + +편리한 메서드들도 좋지만, 이제 블록체인으로 넘어가 봅시다. 다음 단계는 이더리움 노드와 통신하도록 Web3.py를 구성하는 것입니다. 여기서는 IPC, HTTP 또는 웹소켓 공급자를 사용할 수 있습니다. + +우리는 이 경로를 따르지는 않겠지만, HTTP 공급자를 사용하는 전체 워크플로의 예는 다음과 같을 수 있습니다. + +- 이더리움 노드, 예: [Geth](https://geth.ethereum.org/)를 다운로드합니다. +- 한 터미널 창에서 Geth를 시작하고 네트워크와 동기화될 때까지 기다립니다. 기본 HTTP 포트는 `8545`이지만, 구성할 수 있습니다. +- Web3.py에 `localhost:8545`에서 HTTP를 통해 노드에 연결하도록 지시합니다. + `w3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))` +- `w3` 인스턴스를 사용하여 노드와 상호 작용합니다. + +이것이 '실제' 방법 중 하나이긴 하지만, 동기화 과정은 몇 시간이 걸리며 단순히 개발 환경을 원하는 경우에는 불필요합니다. Web3.py는 이러한 목적을 위해 네 번째 공급자인 **EthereumTesterProvider**를 제공합니다. 이 테스터 공급자는 완화된 권한과 테스트용 가짜 통화를 가진 시뮬레이션된 이더리움 노드에 연결됩니다. + +![EthereumTesterProvider가 web3.py 애플리케이션을 시뮬레이션된 이더리움 노드에 연결하는 것을 보여주는 다이어그램](./ethereumtesterprovider.png) + +_EthereumTesterProvider는 시뮬레이션된 노드에 연결되며 빠른 개발 환경에 유용합니다._ + +그 시뮬레이션된 노드는 [eth-tester](https://github.com/ethereum/eth-tester)라고 불리며, `pip install web3[tester]` 명령의 일부로 설치했습니다. 이 테스터 공급자를 사용하도록 Web3.py를 구성하는 것은 다음과 같이 간단합니다. + +```python +In [4]: w3 = Web3(Web3.EthereumTesterProvider()) +``` + +이제 체인을 서핑할 준비가 되었습니다! 사람들이 그렇게 말하지는 않아요. 방금 제가 지어낸 말입니다. 간단히 둘러봅시다. + +## 간단한 둘러보기 {#the-quick-tour} + +가장 먼저, 간단한 확인부터 해보겠습니다. + +```python +In [5]: w3.is_connected() +Out[5]: True +``` + +테스터 공급자를 사용하고 있기 때문에 이것이 그다지 가치 있는 테스트는 아니지만, 만약 실패한다면 `w3` 변수를 인스턴스화할 때 무언가를 잘못 입력했을 가능성이 있습니다. 내부 괄호, 즉 `Web3.EthereumTesterProvider()`를 포함했는지 다시 확인하세요. + +## 둘러보기 #1: [계정](/developers/docs/accounts/) {#tour-stop-1-accounts} + +편의를 위해, 테스터 공급자는 몇 개의 계정을 생성하고 테스트 이더를 미리 채워 넣었습니다. + +먼저, 해당 계정들의 목록을 살펴보겠습니다. + +```python +In [6]: w3.eth.accounts +Out[6]: ['0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', + '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', + '0x6813Eb9362372EEF6200f3b1dbC3f819671cBA69', ...] +``` + +이 명령을 실행하면 `0x`로 시작하는 10개의 문자열 목록이 표시됩니다. 각각은 **공개 주소**이며, 어떤 면에서는 은행 계좌의 계좌 번호와 유사합니다. 이더를 보내려는 사람에게 이 주소를 제공하면 됩니다. + +언급했듯이, 테스터 공급자는 이러한 각 계정에 일부 테스트 이더를 미리 로드해 두었습니다. 첫 번째 계정에 얼마가 있는지 알아봅시다. + +```python +In [7]: w3.eth.get_balance(w3.eth.accounts[0]) +Out[7]: 1000000000000000000000000 +``` + +0이 정말 많네요! 가짜 은행으로 달려가기 전에, 앞에서 배운 통화 단위에 대한 교훈을 떠올려 보세요. 이더 값은 가장 작은 단위인 wei로 표시됩니다. 이 값을 이더로 변환해 보세요. + +```python +In [8]: w3.from_wei(1000000000000000000000000, 'ether') +Out[8]: Decimal('1000000') +``` + +백만 테스트 이더, 여전히 나쁘지 않네요. + +## 둘러보기 #2: 블록 데이터 {#tour-stop-2-block-data} + +이 시뮬레이션된 블록체인의 상태를 살짝 들여다봅시다. + +```python +In [9]: w3.eth.get_block('latest') +Out[9]: AttributeDict({ + 'number': 0, + 'hash': HexBytes('0x9469878...'), + 'parentHash': HexBytes('0x0000000...'), + ... + 'transactions': [] +}) +``` + +블록에 대해 많은 정보가 반환되지만, 여기서 몇 가지만 짚고 넘어가겠습니다. + +- 블록 번호는 0입니다. 테스터 제공자를 얼마나 오래 전에 구성했는지는 중요하지 않습니다. 약 12초마다 새 블록을 추가하는 실제 이더리움 네트워크와 달리, 이 시뮬레이션은 어떤 작업을 지시할 때까지 기다립니다. +- `transactions`는 빈 목록입니다. 같은 이유로, 우리는 아직 아무것도 하지 않았습니다. 이 첫 번째 블록은 체인을 시작하기 위한 **빈 블록**입니다. +- `parentHash`가 단지 빈 바이트들의 묶음이라는 점을 주목하세요. 이는 이 블록이 체인의 첫 번째 블록, 즉 **제네시스 블록**임을 의미합니다. + +## 둘러보기 #3: [트랜잭션](/developers/docs/transactions/) {#tour-stop-3-transactions} + +보류 중인 트랜잭션이 있을 때까지 블록 0에 갇혀 있으니, 하나 만들어 봅시다. 한 계정에서 다른 계정으로 몇 개의 테스트 이더를 보내세요. + +```python +In [10]: tx_hash = w3.eth.send_transaction({ + 'from': w3.eth.accounts[0], + 'to': w3.eth.accounts[1], + 'value': w3.to_wei(3, 'ether'), + 'gas': 21000 +}) +``` + +일반적으로 이 시점에서 트랜잭션이 새 블록에 포함될 때까지 몇 초간 기다려야 합니다. 전체 과정은 다음과 같습니다. + +1. 트랜잭션을 제출하고 트랜잭션 해시를 보관합니다. 트랜잭션을 포함하는 블록이 생성되고 브로드캐스트될 때까지 트랜잭션은 '보류 중(pending)' 상태입니다. + `tx_hash = w3.eth.send_transaction({ … })` +2. 트랜잭션이 블록에 포함될 때까지 기다립니다: + `w3.eth.wait_for_transaction_receipt(tx_hash)` +3. 애플리케이션 로직을 계속 진행합니다. 성공적인 트랜잭션을 보려면: + `w3.eth.get_transaction(tx_hash)` + +우리의 시뮬레이션 환경은 트랜잭션을 즉시 새 블록에 추가하므로, 트랜잭션을 바로 볼 수 있습니다. + +```python +In [11]: w3.eth.get_transaction(tx_hash) +Out[11]: AttributeDict({ + 'hash': HexBytes('0x15e9fb95dc39...'), + 'blockNumber': 1, + 'transactionIndex': 0, + 'from': '0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf', + 'to': '0x2B5AD5c4795c026514f8317c7a215E218DcCD6cF', + 'value': 3000000000000000000, + ... +}) +``` + +여기서 익숙한 세부 정보들을 볼 수 있습니다. `from`, `to`, `value` 필드는 `send_transaction` 호출의 입력값과 일치해야 합니다. 또 다른 안심할 수 있는 점은 이 트랜잭션이 블록 번호 1 내의 첫 번째 트랜잭션(`'transactionIndex': 0`)으로 포함되었다는 것입니다. + +관련된 두 계정의 잔액을 확인하여 이 거래의 성공 여부를 쉽게 확인할 수도 있습니다. 3 이더가 한 계정에서 다른 계정으로 이동했어야 합니다. + +```python +In [12]: w3.eth.get_balance(w3.eth.accounts[0]) +Out[12]: 999996999979000000000000 + +In [13]: w3.eth.get_balance(w3.eth.accounts[1]) +Out[13]: 1000003000000000000000000 +``` + +두 번째 것은 좋아 보입니다! 잔액이 1,000,000 이더에서 1,000,003 이더로 증가했습니다. 그런데 첫 번째 계정은 어떻게 된 걸까요? 3 이더보다 약간 더 많이 잃은 것처럼 보입니다. 아쉽게도, 인생에 공짜는 없으며 이더리움 퍼블릭 네트워크를 사용하려면 동료들의 지원 역할에 대해 보상해야 합니다. 트랜잭션을 제출한 계정에서 소액의 거래 수수료가 차감되었습니다. 이 수수료는 소모된 가스량(ETH 전송의 경우 21,000 가스 단위)에 네트워크 활동량에 따라 달라지는 기본 수수료를 곱한 값과, 트랜잭션을 블록에 포함하는 검증자에게 가는 팁을 더한 금액입니다. + +[가스](/developers/docs/gas/#post-london)에 대해 더 알아보기 + +참고: 퍼블릭 네트워크에서 거래 수수료는 네트워크 수요와 트랜잭션 처리 속도에 따라 변동됩니다. 수수료 계산 방식에 대한 자세한 내용이 궁금하다면, 이전에 작성한 '트랜잭션이 블록에 포함되는 방법'에 대한 게시물을 참조하세요. + +## 그리고 숨 고르기 {#and-breathe} + +꽤 오랫동안 달려왔으니, 이쯤에서 잠시 쉬어가는 것이 좋겠습니다. 토끼굴은 계속 이어지며, 이 시리즈의 2부에서 계속 탐험할 것입니다. 앞으로 다룰 개념: 실제 노드에 연결하기, 스마트 계약, 그리고 토큰. 궁금한 점이 있으신가요? 알려주세요! 여러분의 피드백은 앞으로의 방향에 영향을 미칠 것입니다. 요청은 [트위터](https://twitter.com/wolovim)를 통해 환영합니다. diff --git a/public/content/translations/ko/developers/tutorials/all-you-can-cache/index.md b/public/content/translations/ko/developers/tutorials/all-you-can-cache/index.md new file mode 100644 index 00000000000..1afca386baa --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/all-you-can-cache/index.md @@ -0,0 +1,867 @@ +--- +title: "캐시할 수 있는 모든 것" +description: "더 저렴한 롤업 트랜잭션을 위해 캐싱 계약을 생성하고 사용하는 방법을 알아보세요." +author: Ori Pomerantz +tags: [ "레이어 2", "캐싱", "저장 공간" ] +skill: intermediate +published: 2022-09-15 +lang: ko +--- + +롤업을 사용할 때 트랜잭션에서 1바이트의 비용은 저장 공간 슬롯의 비용보다 훨씬 더 비쌉니다. 따라서 가능한 한 많은 정보를 온체인에 캐시하는 것이 합리적입니다. + +이 글에서는 여러 번 사용될 가능성이 있는 모든 매개변수 값이 캐시되어 (처음 사용 이후) 훨씬 적은 수의 바이트로 사용할 수 있도록 캐싱 계약을 생성하고 사용하는 방법과 이 캐시를 사용하는 오프체인 코드를 작성하는 방법을 배웁니다. + +기사를 건너뛰고 소스 코드만 보려면 [여기를](https://github.com/qbzzt/20220915-all-you-can-cache) 클릭하세요. 개발 스택은 [Foundry](https://getfoundry.sh/introduction/installation/)입니다. + +## 전체적인 설계 {#overall-design} + +단순화를 위해 모든 트랜잭션 매개변수가 32바이트 길이의 `uint256`이라고 가정하겠습니다. 트랜잭션을 수신하면 각 매개변수를 다음과 같이 파싱합니다. + +1. 첫 번째 바이트가 `0xFF`이면 다음 32바이트를 매개변수 값으로 사용하여 캐시에 씁니다. + +2. 첫 번째 바이트가 `0xFE`이면 다음 32바이트를 매개변수 값으로 사용하지만 캐시에 쓰지는 _마세요_. + +3. 다른 값의 경우 상위 4비트는 추가 바이트 수로, 하위 4비트는 캐시 키의 최상위 비트로 사용합니다. 몇 가지 예는 다음과 같습니다. + + | calldata의 바이트 | 캐시 키 | + | :-------------- | -------: | + | 0x0F | 0x0F | + | 0x10,0x10 | 0x10 | + | 0x12,0xAC | 0x02AC | + | 0x2D,0xEA, 0xD6 | 0x0DEAD6 | + +## 캐시 조작 {#cache-manipulation} + +캐시는 [`Cache.sol`](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/src/Cache.sol)에 구현되어 있습니다. 한 줄씩 살펴보겠습니다. + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + + +contract Cache { + + bytes1 public constant INTO_CACHE = 0xFF; + bytes1 public constant DONT_CACHE = 0xFE; +``` + +이러한 상수는 모든 정보를 제공하고 이를 캐시에 쓸지 여부를 결정하는 특수한 경우를 해석하는 데 사용됩니다. 캐시에 쓰는 작업은 이전에 사용되지 않은 저장 공간 슬롯에 각각 22100 가스 비용으로 두 개의 [`SSTORE`](https://www.evm.codes/#55) 작업이 필요하므로 선택 사항으로 만듭니다. + +```solidity + + mapping(uint => uint) public val2key; +``` + +값과 해당 키 간의 [매핑](https://www.geeksforgeeks.org/solidity/solidity-mappings/)입니다. 이 정보는 트랜잭션을 보내기 전에 값을 인코딩하는 데 필요합니다. + +```solidity + // 위치 n에는 키 n+1의 값이 들어갑니다. + // 0은 "캐시에 없음"을 나타내기 위해 남겨둬야 합니다. + uint[] public key2val; +``` + +키를 할당하고 단순화를 위해 순차적으로 수행하기 때문에 키에서 값으로의 매핑에 배열을 사용할 수 있습니다. + +```solidity + function cacheRead(uint _key) public view returns (uint) { + require(_key <= key2val.length, "초기화되지 않은 캐시 항목 읽기"); + return key2val[_key-1]; + } // cacheRead +``` + +캐시에서 값을 읽습니다. + +```solidity + // 캐시에 값이 아직 없는 경우 값을 씁니다 + // 테스트가 작동하도록 공개(public)로 설정 + function cacheWrite(uint _value) public returns (uint) { + // 값이 이미 캐시에 있는 경우 현재 키를 반환합니다 + if (val2key[_value] != 0) { + return val2key[_value]; + } +``` + +동일한 값을 캐시에 두 번 이상 넣을 필요가 없습니다. 값이 이미 있는 경우 기존 키를 반환하면 됩니다. + +```solidity + // 0xFE는 특수한 경우이므로 캐시가 가질 수 있는 + // 가장 큰 키는 0x0D 뒤에 15개의 0xFF가 오는 것입니다. 캐시 길이가 이미 + // 그만큼 크면 실패합니다. + // 1 2 3 4 5 6 7 8 9 A B C D E F + require(key2val.length+1 < 0x0DFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF, + "캐시 오버플로우"); +``` + +캐시가 그렇게 커질 것이라고는 생각하지 않습니다(약 1.8\*1037개의 항목, 저장하는 데 약 1027TB가 필요). 하지만 저는 ["640kB면 항상 충분할 것이다"](https://quoteinvestigator.com/2011/09/08/640k-enough/)를 기억할 만큼 나이가 많습니다. 이 테스트는 비용이 매우 저렴합니다. + +```solidity + // 다음 키를 사용하여 값을 씁니다 + val2key[_value] = key2val.length+1; +``` + +역방향 조회(값에서 키로)를 추가합니다. + +```solidity + key2val.push(_value); +``` + +정방향 조회(키에서 값으로)를 추가합니다. 값을 순차적으로 할당하기 때문에 마지막 배열 값 뒤에 추가하면 됩니다. + +```solidity + return key2val.length; + } // cacheWrite +``` + +새 값이 저장되는 셀인 `key2val`의 새 길이를 반환합니다. + +```solidity + function _calldataVal(uint startByte, uint length) + private pure returns (uint) +``` + +이 함수는 임의의 길이(최대 32바이트, 워드 크기)의 calldata에서 값을 읽습니다. + +```solidity + { + uint _retVal; + + require(length < 0x21, + "_calldataVal 길이 제한은 32바이트입니다"); + require(length + startByte <= msg.data.length, + "_calldataVal이 calldatasize를 초과하여 읽으려 합니다"); +``` + +이 함수는 내부 함수이므로 나머지 코드가 올바르게 작성되었다면 이러한 테스트는 필요하지 않습니다. 하지만 비용이 많이 들지 않으므로 추가하는 것이 좋습니다. + +```solidity + assembly { + _retVal := calldataload(startByte) + } +``` + +이 코드는 [Yul](https://docs.soliditylang.org/en/v0.8.16/yul.html)로 작성되었습니다. calldata에서 32바이트 값을 읽습니다. EVM의 초기화되지 않은 공간은 0으로 간주되기 때문에 `startByte+32` 이전에 calldata가 중지되더라도 이 작업은 작동합니다. + +```solidity + _retVal = _retVal >> (256-length*8); +``` + +반드시 32바이트 값이 필요한 것은 아닙니다. 이렇게 하면 초과 바이트를 제거할 수 있습니다. + +```solidity + return _retVal; + } // _calldataVal + + + // _fromByte에서 시작하여 calldata에서 단일 매개변수를 읽습니다. + function _readParam(uint _fromByte) internal + returns (uint _nextByte, uint _parameterValue) + { +``` + +calldata에서 단일 매개변수를 읽습니다. 매개변수의 길이는 1바이트에서 33바이트까지 다양할 수 있으므로 읽은 값뿐만 아니라 다음 바이트의 위치도 반환해야 합니다. + +```solidity + // 첫 번째 바이트는 나머지를 해석하는 방법을 알려줍니다. + uint8 _firstByte; + + _firstByte = uint8(_calldataVal(_fromByte, 1)); +``` + +Solidity는 잠재적으로 위험한 [암시적 유형 변환](https://docs.soliditylang.org/en/v0.8.16/types.html#implicit-conversions)을 금지하여 버그 수를 줄이려고 합니다. 예를 들어 256비트에서 8비트로의 다운그레이드는 명시적이어야 합니다. + +```solidity + + // 값을 읽지만 캐시에 쓰지 않습니다. + if (_firstByte == uint8(DONT_CACHE)) + return(_fromByte+33, _calldataVal(_fromByte+1, 32)); + + // 값을 읽고 캐시에 씁니다. + if (_firstByte == uint8(INTO_CACHE)) { + uint _param = _calldataVal(_fromByte+1, 32); + cacheWrite(_param); + return(_fromByte+33, _param); + } + + // 여기까지 오면 캐시에서 읽어야 한다는 의미입니다. + + // 읽을 추가 바이트 수 + uint8 _extraBytes = _firstByte / 16; +``` + +하위 [니블](https://en.wikipedia.org/wiki/Nibble)을 가져와 다른 바이트와 결합하여 캐시에서 값을 읽습니다. + +```solidity + uint _key = (uint256(_firstByte & 0x0F) << (8*_extraBytes)) + + _calldataVal(_fromByte+1, _extraBytes); + + return (_fromByte+_extraBytes+1, cacheRead(_key)); + + } // _readParam + + + // n개의 매개변수 읽기(함수는 예상하는 매개변수 수를 알고 있습니다) + function _readParams(uint _paramNum) internal returns (uint[] memory) { +``` + +calldata 자체에서 매개변수 수를 가져올 수 있지만, 우리를 호출하는 함수는 예상하는 매개변수 수를 알고 있습니다. 그들이 알려주는 것이 더 쉽습니다. + +```solidity + // 읽은 매개변수들 + uint[] memory params = new uint[](_paramNum); + + // 매개변수는 4번째 바이트에서 시작하며, 그 이전은 함수 서명입니다. + uint _atByte = 4; + + for(uint i=0; i<_paramNum; i++) { + (_atByte, params[i]) = _readParam(_atByte); + } +``` + +필요한 수의 매개변수를 가질 때까지 매개변수를 읽습니다. calldata의 끝을 지나면 `_readParams`가 호출을 되돌립니다. + +```solidity + + return(params); + } // readParams + + // _readParams 테스트를 위해 4개의 매개변수 읽기 테스트 + function fourParam() public + returns (uint256,uint256,uint256,uint256) + { + uint[] memory params; + params = _readParams(4); + return (params[0], params[1], params[2], params[3]); + } // fourParam +``` + +Foundry의 큰 장점 중 하나는 Solidity로 테스트를 작성할 수 있다는 것입니다([아래 캐시 테스트](#testing-the-cache) 참조). 이렇게 하면 단위 테스트가 훨씬 쉬워집니다. 이 함수는 네 개의 매개변수를 읽고 반환하여 테스트에서 올바른지 확인할 수 있도록 하는 함수입니다. + +```solidity + // 값을 가져와 인코딩할 바이트를 반환합니다(가능한 경우 캐시 사용). + function encodeVal(uint _val) public view returns(bytes memory) { +``` + +`encodeVal`은 오프체인 코드가 캐시를 사용하는 calldata를 만드는 데 도움을 주기 위해 호출하는 함수입니다. 단일 값을 받아 인코딩된 바이트를 반환합니다. 이 함수는 `view`이므로 트랜잭션이 필요하지 않으며 외부에서 호출할 때 가스가 들지 않습니다. + +```solidity + uint _key = val2key[_val]; + + // 값이 아직 캐시에 없으므로 추가합니다. + if (_key == 0) + return bytes.concat(INTO_CACHE, bytes32(_val)); +``` + +[EVM](/developers/docs/evm/)에서 모든 초기화되지 않은 저장 공간은 0으로 간주됩니다. 따라서 존재하지 않는 값의 키를 찾으면 0을 얻게 됩니다. 이 경우 인코딩된 바이트는 `INTO_CACHE`(다음에 캐시됨)이고 그 뒤에 실제 값이 옵니다. + +```solidity + // 키가 0x10보다 작으면 단일 바이트로 반환합니다. + if (_key < 0x10) + return bytes.concat(bytes1(uint8(_key))); +``` + +단일 바이트가 가장 쉽습니다. `bytes.concat`([https://docs.soliditylang.org/en/v0.8.16/types.html#the-functions-bytes-concat-and-string-concat](https://docs.soliditylang.org/en/v0.8.16/types.html#the-functions-bytes-concat-and-string-concat))을 사용하여 `bytes` 유형을 임의의 길이의 바이트 배열로 변환합니다. 이름에도 불구하고 단 하나의 인수를 제공해도 잘 작동합니다. + +```solidity + // 0x1vvv로 인코딩된 2바이트 값 + if (_key < 0x1000) + return bytes.concat(bytes2(uint16(_key) | 0x1000)); +``` + +163보다 작은 키가 있을 때 두 바이트로 표현할 수 있습니다. 먼저 256비트 값인 `_key`를 16비트 값으로 변환하고 논리적 or를 사용하여 첫 번째 바이트에 추가 바이트 수를 더합니다. 그런 다음 `bytes`로 변환할 수 있는 `bytes2` 값으로 변환합니다. + +```solidity + // 다음 줄을 루프로 수행하는 영리한 방법이 있을 수 있지만, + // view 함수이므로 프로그래머 시간과 + // 단순성을 위해 최적화하고 있습니다. + + if (_key < 16*256**2) + return bytes.concat(bytes3(uint24(_key) | (0x2 * 16 * 256**2))); + if (_key < 16*256**3) + return bytes.concat(bytes4(uint32(_key) | (0x3 * 16 * 256**3))); + . + . + . + if (_key < 16*256**14) + return bytes.concat(bytes15(uint120(_key) | (0xE * 16 * 256**14))); + if (_key < 16*256**15) + return bytes.concat(bytes16(uint128(_key) | (0xF * 16 * 256**15))); +``` + +다른 값(3바이트, 4바이트 등) 필드 크기만 다를 뿐 동일한 방식으로 처리됩니다. + +```solidity + // 여기에 도달하면 뭔가 잘못된 것입니다. + revert("encodeVal 오류, 발생해서는 안 됨"); +``` + +여기에 도달하면 16\*25615보다 작지 않은 키를 얻었다는 의미입니다. 그러나 `cacheWrite`는 키를 제한하므로 14\*25616(첫 번째 바이트가 0xFE이므로 `DONT_CACHE`처럼 보임)까지도 도달할 수 없습니다. 하지만 향후 프로그래머가 버그를 도입할 경우를 대비하여 테스트를 추가하는 데 비용이 많이 들지 않습니다. + +```solidity + } // encodeVal + +} // Cache +``` + +### 캐시 테스트 {#testing-the-cache} + +Foundry의 장점 중 하나는 [Solidity로 테스트를 작성할 수 있다는 것](https://getfoundry.sh/forge/tests/overview/)이며, 이는 단위 테스트 작성을 더 쉽게 만듭니다. `Cache` 클래스에 대한 테스트는 [여기](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/test/Cache.t.sol)에 있습니다. 테스트 코드는 테스트 경향에 따라 반복적이므로 이 기사에서는 흥미로운 부분만 설명합니다. + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "forge-std/Test.sol"; + + +// console을 사용하려면 `forge test -vv`를 실행해야 합니다. +import "forge-std/console.sol"; +``` + +이는 테스트 패키지와 `console.log`를 사용하는 데 필요한 상용구일 뿐입니다. + +```solidity +import "src/Cache.sol"; +``` + +테스트 중인 계약을 알아야 합니다. + +```solidity +contract CacheTest is Test { + Cache cache; + + function setUp() public { + cache = new Cache(); + } +``` + +`setUp` 함수는 각 테스트 전에 호출됩니다. 이 경우 새 캐시를 만들어 테스트가 서로 영향을 미치지 않도록 합니다. + +```solidity + function testCaching() public { +``` + +테스트는 이름이 `test`로 시작하는 함수입니다. 이 함수는 값을 쓰고 다시 읽는 기본 캐시 기능을 확인합니다. + +```solidity + for(uint i=1; i<5000; i++) { + cache.cacheWrite(i*i); + } + + for(uint i=1; i<5000; i++) { + assertEq(cache.cacheRead(i), i*i); +``` + +이것이 [`assert...` 함수](https://getfoundry.sh/reference/forge-std/std-assertions/)를 사용하여 실제 테스트를 수행하는 방법입니다. 이 경우 우리가 쓴 값이 우리가 읽은 값과 동일한지 확인합니다. 캐시 키가 선형적으로 할당된다는 것을 알기 때문에 `cache.cacheWrite`의 결과를 버릴 수 있습니다. + +```solidity + } + } // testCaching + + + // 동일한 값을 여러 번 캐시하고 키가 동일하게 + // 유지되는지 확인합니다. + function testRepeatCaching() public { + for(uint i=1; i<100; i++) { + uint _key1 = cache.cacheWrite(i); + uint _key2 = cache.cacheWrite(i); + assertEq(_key1, _key2); + } +``` + +먼저 각 값을 캐시에 두 번 쓰고 키가 동일한지 확인합니다(두 번째 쓰기는 실제로 발생하지 않았음을 의미). + +```solidity + for(uint i=1; i<100; i+=3) { + uint _key = cache.cacheWrite(i); + assertEq(_key, i); + } + } // testRepeatCaching +``` + +이론적으로 연속적인 캐시 쓰기에 영향을 미치지 않는 버그가 있을 수 있습니다. 따라서 여기서는 연속적이지 않은 일부 쓰기를 수행하고 값이 여전히 다시 쓰여지지 않는지 확인합니다. + +```solidity + // 메모리 버퍼에서 uint를 읽습니다(보낸 매개변수를 + // 다시 받는지 확인하기 위해). + function toUint256(bytes memory _bytes, uint256 _start) internal pure + returns (uint256) +``` + +`bytes memory` 버퍼에서 256비트 워드를 읽습니다. 이 유틸리티 함수를 사용하면 캐시를 사용하는 함수 호출을 실행할 때 올바른 결과를 받는지 확인할 수 있습니다. + +```solidity + { + require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); + uint256 tempUint; + + assembly { + tempUint := mload(add(add(_bytes, 0x20), _start)) + } +``` + +Yul은 `uint256` 이상의 자료 구조를 지원하지 않으므로 메모리 버퍼 `_bytes`와 같은 더 복잡한 자료 구조를 참조할 때 해당 구조의 주소를 얻게 됩니다. Solidity는 `bytes memory` 값을 길이, 그 다음에 실제 바이트가 포함된 32바이트 워드로 저장하므로 `_start` 번 바이트를 얻으려면 `_bytes+32+_start`를 계산해야 합니다. + +```solidity + + return tempUint; + } // toUint256 + + // fourParams()의 함수 서명, 제공처: + // https://www.4byte.directory/signatures/?bytes4_signature=0x3edc1e6d + bytes4 constant FOUR_PARAMS = 0x3edc1e6d; + + // 올바른 값을 반환하는지 확인하기 위한 일부 상수 값 + uint256 constant VAL_A = 0xDEAD60A7; + uint256 constant VAL_B = 0xBEEF; + uint256 constant VAL_C = 0x600D; + uint256 constant VAL_D = 0x600D60A7; +``` + +테스트에 필요한 일부 상수입니다. + +```solidity + function testReadParam() public { +``` + +`readParams`를 사용하는 함수인 `fourParams()`를 호출하여 매개변수를 올바르게 읽을 수 있는지 테스트합니다. + +```solidity + address _cacheAddr = address(cache); + bool _success; + bytes memory _callInput; + bytes memory _callOutput; +``` + +일반 ABI 메커니즘을 사용하여 캐시를 사용하는 함수를 호출할 수 없으므로 하위 수준 [`
.call()`](https://docs.soliditylang.org/en/v0.8.16/types.html#members-of-addresses) 메커니즘을 사용해야 합니다. 이 메커니즘은 `bytes memory`를 입력으로 받고 출력으로 반환합니다(부울 값과 함께). + +```solidity + // 첫 번째 호출, 캐시는 비어 있습니다. + _callInput = bytes.concat( + FOUR_PARAMS, +``` + +동일한 계약이 캐시된 함수(트랜잭션에서 직접 호출)와 캐시되지 않은 함수(다른 스마트 계약에서 호출)를 모두 지원하는 것이 유용합니다. 이를 위해서는 모든 것을 [a `fallback` 함수](https://docs.soliditylang.org/en/v0.8.16/contracts.html#fallback-function)에 넣는 대신 올바른 함수를 호출하기 위해 Solidity 메커니즘에 계속 의존해야 합니다. 이렇게 하면 구성 가능성이 훨씬 쉬워집니다. 대부분의 경우 단일 바이트로 함수를 식별하기에 충분하므로 3바이트(16\*3=48 가스)를 낭비하고 있습니다. 하지만 이 글을 쓰는 시점에서 48 가스 비용은 0.07센트로, 더 간단하고 버그가 적은 코드에 대한 합리적인 비용입니다. + +```solidity + // 첫 번째 값, 캐시에 추가합니다. + cache.INTO_CACHE(), + bytes32(VAL_A), +``` + +첫 번째 값: 캐시에 써야 하는 전체 값임을 나타내는 플래그와 그 뒤에 오는 값의 32바이트입니다. 다른 세 값은 비슷하지만 `VAL_B`는 캐시에 기록되지 않고 `VAL_C`는 세 번째 매개변수와 네 번째 매개변수라는 점이 다릅니다. + +```solidity + . + . + . + ); + (_success, _callOutput) = _cacheAddr.call(_callInput); +``` + +여기서 실제로 `Cache` 계약을 호출합니다. + +```solidity + assertEq(_success, true); +``` + +호출이 성공할 것으로 예상합니다. + +```solidity + assertEq(cache.cacheRead(1), VAL_A); + assertEq(cache.cacheRead(2), VAL_C); +``` + +빈 캐시로 시작한 다음 `VAL_A`와 `VAL_C`를 차례로 추가합니다. 첫 번째는 키 1을, 두 번째는 2를 가질 것으로 예상합니다. + +``` + assertEq(toUint256(_callOutput,0), VAL_A); + assertEq(toUint256(_callOutput,32), VAL_B); + assertEq(toUint256(_callOutput,64), VAL_C); + assertEq(toUint256(_callOutput,96), VAL_C); +``` + +출력은 네 개의 매개변수입니다. 여기서 올바른지 확인합니다. + +```solidity + // 두 번째 호출, 캐시를 사용할 수 있습니다. + _callInput = bytes.concat( + FOUR_PARAMS, + + // 캐시의 첫 번째 값 + bytes1(0x01), +``` + +16 미만의 캐시 키는 1바이트입니다. + +```solidity + // 두 번째 값, 캐시에 추가하지 마십시오. + cache.DONT_CACHE(), + bytes32(VAL_B), + + // 세 번째 및 네 번째 값, 동일한 값 + bytes1(0x02), + bytes1(0x02) + ); + . + . + . + } // testReadParam +``` + +호출 후 테스트는 첫 번째 호출 후 테스트와 동일합니다. + +```solidity + function testEncodeVal() public { +``` + +이 함수는 매개변수를 명시적으로 작성하는 대신 `encodeVal()`을 사용한다는 점을 제외하면 `testReadParam`과 유사합니다. + +```solidity + . + . + . + _callInput = bytes.concat( + FOUR_PARAMS, + cache.encodeVal(VAL_A), + cache.encodeVal(VAL_B), + cache.encodeVal(VAL_C), + cache.encodeVal(VAL_D) + ); + . + . + . + assertEq(_callInput.length, 4+1*4); + } // testEncodeVal +``` + +`testEncodeVal()`의 유일한 추가 테스트는 `_callInput`의 길이가 올바른지 확인하는 것입니다. 첫 번째 호출의 경우 4+33\*4입니다. 모든 값이 이미 캐시에 있는 두 번째 호출의 경우 4+1\*4입니다. + +```solidity + // 키가 단일 바이트 이상일 때 encodeVal 테스트 + // 캐시를 4바이트로 채우는 데 시간이 + // 너무 오래 걸리므로 최대 3바이트. + function testEncodeValBig() public { + // 캐시에 여러 값을 넣습니다. + // 간단하게 하기 위해 키 n을 값 n에 사용합니다. + for(uint i=1; i<0x1FFF; i++) { + cache.cacheWrite(i); + } +``` + +위의 `testEncodeVal` 함수는 캐시에 네 개의 값만 쓰므로 [다중 바이트 값을 처리하는 함수 부분](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/src/Cache.sol#L144-L171)은 확인되지 않습니다. 하지만 그 코드는 복잡하고 오류가 발생하기 쉽습니다. + +이 함수의 첫 번째 부분은 1에서 0x1FFF까지의 모든 값을 순서대로 캐시에 쓰는 루프이므로 해당 값을 인코딩하고 어디로 가는지 알 수 있습니다. + +```solidity + . + . + . + + _callInput = bytes.concat( + FOUR_PARAMS, + cache.encodeVal(0x000F), // 1바이트 0x0F + cache.encodeVal(0x0010), // 2바이트 0x1010 + cache.encodeVal(0x0100), // 2바이트 0x1100 + cache.encodeVal(0x1000) // 3바이트 0x201000 + ); +``` + +1바이트, 2바이트 및 3바이트 값을 테스트합니다. 충분한 스택 항목(최소 0x10000000, 약 2억 5천만)을 쓰는 데 너무 오래 걸리기 때문에 그 이상은 테스트하지 않습니다. + +```solidity + . + . + . + . + } // testEncodeValBig + + + // 너무 작은 버퍼로 되돌림을 얻는 것을 테스트합니다. + function testShortCalldata() public { +``` + +매개변수가 충분하지 않은 비정상적인 경우에 어떤 일이 발생하는지 테스트합니다. + +```solidity + . + . + . + (_success, _callOutput) = _cacheAddr.call(_callInput); + assertEq(_success, false); + } // testShortCalldata +``` + +되돌리기 때문에 얻어야 할 결과는 `false`입니다. + +``` + // 없는 캐시 키로 호출 + function testNoCacheKey() public { + . + . + . + _callInput = bytes.concat( + FOUR_PARAMS, + + // 첫 번째 값, 캐시에 추가합니다. + cache.INTO_CACHE(), + bytes32(VAL_A), + + // 두 번째 값 + bytes1(0x0F), + bytes2(0x1234), + bytes11(0xA10102030405060708090A) + ); +``` + +이 함수는 캐시가 비어 있어서 읽을 값이 없다는 점을 제외하면 완벽하게 합법적인 4개의 매개변수를 얻습니다. + +```solidity + . + . + . + // 너무 긴 버퍼로 모든 것이 잘 작동하는지 테스트합니다. + function testLongCalldata() public { + address _cacheAddr = address(cache); + bool _success; + bytes memory _callInput; + bytes memory _callOutput; + + // 첫 번째 호출, 캐시는 비어 있습니다. + _callInput = bytes.concat( + FOUR_PARAMS, + + // 첫 번째 값, 캐시에 추가합니다. + cache.INTO_CACHE(), bytes32(VAL_A), + + // 두 번째 값, 캐시에 추가합니다. + cache.INTO_CACHE(), bytes32(VAL_B), + + // 세 번째 값, 캐시에 추가합니다. + cache.INTO_CACHE(), bytes32(VAL_C), + + // 네 번째 값, 캐시에 추가합니다. + cache.INTO_CACHE(), bytes32(VAL_D), + + // 그리고 "행운을 빌며" 또 다른 값을 추가합니다. + bytes4(0x31112233) + ); +``` + +이 함수는 다섯 개의 값을 보냅니다. 다섯 번째 값은 유효한 캐시 항목이 아니기 때문에 무시된다는 것을 알고 있습니다. 포함되지 않았다면 되돌림이 발생했을 것입니다. + +```solidity + (_success, _callOutput) = _cacheAddr.call(_callInput); + assertEq(_success, true); + . + . + . + } // testLongCalldata + +} // CacheTest + +``` + +## 샘플 애플리케이션 {#a-sample-app} + +Solidity로 테스트를 작성하는 것은 모두 매우 좋지만, 결국 탈중앙화앱은 유용하려면 체인 외부의 요청을 처리할 수 있어야 합니다. 이 기사에서는 "한 번 쓰고, 여러 번 읽기"를 의미하는 `WORM`을 사용하여 탈중앙화앱에서 캐싱을 사용하는 방법을 보여줍니다. 키가 아직 쓰여지지 않았다면 값을 쓸 수 있습니다. 키가 이미 쓰여졌다면 되돌림을 받게 됩니다. + +### 계약 {#the-contract} + +[이것이 계약입니다](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/src/WORM.sol). 주로 `Cache`와 `CacheTest`로 이미 수행한 작업을 반복하므로 흥미로운 부분만 다룹니다. + +```solidity +import "./Cache.sol"; + +contract WORM is Cache { +``` + +`Cache`를 사용하는 가장 쉬운 방법은 우리 자신의 계약에서 상속하는 것입니다. + +```solidity + function writeEntryCached() external { + uint[] memory params = _readParams(2); + writeEntry(params[0], params[1]); + } // writeEntryCached +``` + +이 함수는 위의 `CacheTest`에 있는 `fourParam`과 유사합니다. ABI 사양을 따르지 않기 때문에 함수에 매개변수를 선언하지 않는 것이 가장 좋습니다. + +```solidity + // 더 쉽게 호출할 수 있도록 합니다. + // writeEntryCached()의 함수 서명, 제공처: + // https://www.4byte.directory/signatures/?bytes4_signature=0xe4e4f2d3 + bytes4 constant public WRITE_ENTRY_CACHED = 0xe4e4f2d3; +``` + +ABI 사양을 따르지 않기 때문에 `writeEntryCached`를 호출하는 외부 코드는 `worm.writeEntryCached`를 사용하는 대신 수동으로 calldata를 빌드해야 합니다. 이 상수 값을 사용하면 작성하기가 더 쉬워집니다. + +`WRITE_ENTRY_CACHED`를 상태 변수로 정의하더라도 외부에서 읽으려면 `worm.WRITE_ENTRY_CACHED()` 게터 함수를 사용해야 합니다. + +```solidity + function readEntry(uint key) public view + returns (uint _value, address _writtenBy, uint _writtenAtBlock) +``` + +읽기 함수는 `view`이므로 트랜잭션이 필요하지 않으며 가스가 들지 않습니다. 결과적으로 매개변수에 캐시를 사용하는 이점이 없습니다. view 함수를 사용하면 더 간단한 표준 메커니즘을 사용하는 것이 가장 좋습니다. + +### 테스트 코드 {#the-testing-code} + +[이것은 계약에 대한 테스트 코드입니다](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/test/WORM.t.sol). 다시 한 번, 흥미로운 것만 살펴봅시다. + +```solidity + function testWReadWrite() public { + worm.writeEntry(0xDEAD, 0x60A7); + + vm.expectRevert(bytes("항목이 이미 작성됨")); + worm.writeEntry(0xDEAD, 0xBEEF); +``` + +[이것(`vm.expectRevert`)](https://book.getfoundry.sh/cheatcodes/expect-revert#expectrevert)은 Foundry 테스트에서 다음 호출이 실패해야 한다는 것과 실패의 보고된 이유를 지정하는 방법입니다. 이는 `.` 구문을 사용할 때 적용됩니다.()` 구문을 사용하는 것보다 calldata를 빌드하고 하위 수준 인터페이스(`.call()` 등)를 사용하여 계약을 호출하는 것보다. + +```solidity + function testReadWriteCached() public { + uint cacheGoat = worm.cacheWrite(0x60A7); +``` + +여기서는 `cacheWrite`가 캐시 키를 반환한다는 사실을 사용합니다. `cacheWrite`는 상태를 변경하므로 트랜잭션 중에만 호출할 수 있기 때문에 프로덕션 환경에서 사용할 것으로 예상되는 것은 아닙니다. 트랜잭션에는 반환 값이 없으며, 결과가 있는 경우 해당 결과는 이벤트로 내보내져야 합니다. 따라서 `cacheWrite` 반환 값은 온체인 코드에서만 액세스할 수 있으며 온체인 코드는 매개변수 캐싱이 필요하지 않습니다. + +```solidity + (_success,) = address(worm).call(_callInput); +``` + +이것이 `.call()`에 두 개의 반환 값이 있지만 첫 번째 값에만 관심이 있다는 것을 Solidity에 알리는 방법입니다. + +```solidity + (_success,) = address(worm).call(_callInput); + assertEq(_success, false); +``` + +하위 수준 `
.call()` 함수를 사용하기 때문에 `vm.expectRevert()`를 사용할 수 없으며 호출에서 얻는 부울 성공 값을 확인해야 합니다. + +```solidity + event EntryWritten(uint indexed key, uint indexed value); + + . + . + . + + _callInput = bytes.concat( + worm.WRITE_ENTRY_CACHED(), worm.encodeVal(a), worm.encodeVal(b)); + vm.expectEmit(true, true, false, false); + emit EntryWritten(a, b); + (_success,) = address(worm).call(_callInput); +``` + +이것이 Foundry에서 코드가 [이벤트를 올바르게 내보내는지](https://getfoundry.sh/reference/cheatcodes/expect-emit/) 확인하는 방법입니다. + +### 클라이언트 {#the-client} + +Solidity 테스트에서 얻을 수 없는 한 가지는 자신의 애플리케이션에 잘라내어 붙여넣을 수 있는 JavaScript 코드입니다. 그 코드를 작성하기 위해 WORM을 [Optimism Goerli](https://community.optimism.io/docs/useful-tools/networks/#optimism-goerli), [Optimism의](https://www.optimism.io/) 새로운 테스트넷에 배포했습니다. 주소는 [`0xd34335b1d818cee54e3323d3246bd31d94e6a78a`](https://goerli-optimism.etherscan.io/address/0xd34335b1d818cee54e3323d3246bd31d94e6a78a)입니다. + +[클라이언트에 대한 JavaScript 코드는 여기에서 볼 수 있습니다](https://github.com/qbzzt/20220915-all-you-can-cache/blob/main/javascript/index.js). 사용 방법: + +1. git 리포지토리 복제: + + ```sh + git clone https://github.com/qbzzt/20220915-all-you-can-cache.git + ``` + +2. 필요한 패키지 설치: + + ```sh + cd javascript + yarn + ``` + +3. 구성 파일 복사: + + ```sh + cp .env.example .env + ``` + +4. 구성을 위해 `.env` 편집: + + | 매개 변수 | 값 | + | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | + | MNEMONIC | 트랜잭션 비용을 지불할 수 있는 충분한 ETH가 있는 계정의 니모닉입니다. [여기에서 Optimism Goerli 네트워크용 무료 ETH를 얻을 수 있습니다](https://optimismfaucet.xyz/). | + | OPTIMISM_GOERLI_URL | Optimism Goerli URL. 공용 엔드포인트 `https://goerli.optimism.io`는 속도가 제한되지만 여기서 필요한 만큼 충분합니다. | + +5. `index.js`를 실행합니다. + + ```sh + node index.js + ``` + + 이 샘플 애플리케이션은 먼저 WORM에 항목을 쓰고 calldata와 Etherscan의 트랜잭션 링크를 표시합니다. 그런 다음 해당 항목을 다시 읽고 사용하는 키와 항목의 값(값, 블록 번호 및 작성자)을 표시합니다. + +대부분의 클라이언트는 일반적인 Dapp JavaScript입니다. 따라서 다시 흥미로운 부분만 살펴보겠습니다. + +```javascript +. +. +. +const main = async () => { + const func = await worm.WRITE_ENTRY_CACHED() + + // 매번 새로운 키가 필요합니다. + const key = await worm.encodeVal(Number(new Date())) +``` + +주어진 슬롯은 한 번만 쓸 수 있으므로 타임스탬프를 사용하여 슬롯을 재사용하지 않도록 합니다. + +```javascript +const val = await worm.encodeVal("0x600D") + +// 항목 작성 +const calldata = func + key.slice(2) + val.slice(2) +``` + +Ethers는 호출 데이터가 16진수 문자열, 즉 `0x` 다음에 짝수 개의 16진수 숫자가 오는 것으로 예상합니다. `key`와 `val` 모두 `0x`로 시작하므로 해당 헤더를 제거해야 합니다. + +```javascript +const tx = await worm.populateTransaction.writeEntryCached() +tx.data = calldata + +sentTx = await wallet.sendTransaction(tx) +``` + +Solidity 테스트 코드와 마찬가지로 캐시된 함수를 정상적으로 호출할 수 없습니다. 대신 하위 수준 메커니즘을 사용해야 합니다. + +```javascript + . + . + . + // 방금 작성한 항목 읽기 + const realKey = '0x' + key.slice(4) // FF 플래그 제거 + const entryRead = await worm.readEntry(realKey) + . + . + . +``` + +항목을 읽을 때는 일반적인 메커니즘을 사용할 수 있습니다. `view` 함수와 함께 매개변수 캐싱을 사용할 필요가 없습니다. + +## 결론 {#conclusion} + +이 기사의 코드는 개념 증명이며, 아이디어를 쉽게 이해하도록 하는 것이 목적입니다. 프로덕션 준비 시스템의 경우 몇 가지 추가 기능을 구현할 수 있습니다. + +- `uint256`이 아닌 값을 처리합니다. 예를 들어, 문자열입니다. +- 전역 캐시 대신 사용자-캐시 간 매핑을 사용할 수 있습니다. 사용자마다 다른 값을 사용합니다. +- 주소에 사용되는 값은 다른 용도로 사용되는 값과 구별됩니다. 주소 전용 캐시를 별도로 두는 것이 합리적일 수 있습니다. +- 현재 캐시 키는 "선착순, 가장 작은 키" 알고리즘을 따릅니다. 처음 16개 값은 단일 바이트로 보낼 수 있습니다. 다음 4080개 값은 2바이트로 보낼 수 있습니다. 다음 약 백만 개의 값은 3바이트 등입니다. 프로덕션 시스템은 캐시 항목에 대한 사용 카운터를 유지하고 이를 재구성하여 가장 흔한 16개 값은 1바이트, 다음 4080개 가장 흔한 값은 2바이트 등이 되도록 해야 합니다. + + 하지만 이는 잠재적으로 위험한 작업입니다. 다음과 같은 일련의 이벤트를 상상해 보십시오. + + 1. Noam Naive는 `encodeVal`을 호출하여 토큰을 보내려는 주소를 인코딩합니다. 해당 주소는 애플리케이션에서 처음 사용된 주소 중 하나이므로 인코딩된 값은 0x06입니다. 이것은 트랜잭션이 아닌 `view` 함수이므로 Noam과 그가 사용하는 노드 사이의 일이며 다른 누구도 알지 못합니다. + + 2. Owen Owner는 캐시 재정렬 작업을 실행합니다. 실제로 해당 주소를 사용하는 사람은 거의 없으므로 이제 0x201122로 인코딩됩니다. 다른 값인 1018이 0x06에 할당됩니다. + + 3. Noam Naive는 자신의 토큰을 0x06으로 보냅니다. 토큰은 `0x0000000000000000000000000de0b6b3a7640000` 주소로 이동하며, 해당 주소의 개인 키를 아는 사람이 없으므로 그냥 갇혀 있습니다. Noam은 _행복하지 않습니다_. + + 이 문제와 캐시 재정렬 중 멤풀에 있는 트랜잭션 관련 문제를 해결할 방법이 있지만, 이를 인지하고 있어야 합니다. + +저는 Optimism 직원이고 이것이 제가 가장 잘 아는 롤업이기 때문에 여기서 Optimism으로 캐싱을 시연했습니다. 하지만 내부 처리에 최소한의 비용을 부과하여 트랜잭션 데이터를 L1에 쓰는 것이 주요 비용이 되는 모든 롤업에서 작동해야 합니다. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). + diff --git a/public/content/translations/ko/developers/tutorials/app-plasma/index.md b/public/content/translations/ko/developers/tutorials/app-plasma/index.md new file mode 100644 index 00000000000..64507c93361 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/app-plasma/index.md @@ -0,0 +1,1255 @@ +--- +title: "개인 정보를 보호하는 앱별 플라즈마 작성하기" +description: "이 튜토리얼에서는 예금을 위한 반비밀 은행을 구축합니다. 은행은 중앙화된 구성 요소이며, 각 사용자의 잔액을 알고 있습니다. 하지만 이 정보는 온체인에 저장되지 않습니다. 대신 은행은 상태의 해시를 게시합니다. 트랜잭션이 발생할 때마다 은행은 새로운 해시를 게시하며, 해시 상태를 새로운 상태로 변경하는 서명된 트랜잭션이 있음을 증명하는 영지식 증명도 함께 게시합니다. 이 튜토리얼을 읽고 나면 영지식 증명을 사용하는 방법뿐만 아니라 영지식 증명을 사용하는 이유와 안전하게 사용하는 방법까지 이해하게 될 것입니다." +author: Ori Pomerantz +tags: [ "영지식", "서버", "오프체인", "개인정보 보호" ] +skill: advanced +lang: ko +published: 2025-10-15 +--- + +## 소개 {#introduction} + +[롤업](/developers/docs/scaling/zk-rollups/)과 달리 [플라즈마](/developers/docs/scaling/plasma)는 무결성을 위해 이더리움 메인넷을 사용하지만 가용성을 위해서는 사용하지 않습니다. 이 글에서는 이더리움이 무결성(무단 변경 없음)을 보장하지만 가용성(중앙화된 구성 요소가 다운되어 전체 시스템을 비활성화할 수 있음)은 보장하지 않는 플라즈마처럼 작동하는 애플리케이션을 작성합니다. + +여기서 작성하는 애플리케이션은 개인 정보를 보호하는 은행입니다. 다른 주소에는 잔액이 있는 계정이 있으며, 다른 계정으로 돈(ETH)을 보낼 수 있습니다. 은행은 상태(계정 및 잔액) 및 트랜잭션의 해시를 게시하지만 실제 잔액은 비공개로 유지될 수 있는 오프체인에 보관합니다. + +## 설계 {#design} + +이는 프로덕션용 시스템이 아니라 교육용 도구입니다. 따라서 몇 가지 단순화된 가정을 바탕으로 작성되었습니다. + +- 고정된 계정 풀. 특정 수의 계정이 있으며, 각 계정은 미리 정해진 주소에 속합니다. 영지식 증명에서는 가변 크기 데이터 구조를 처리하기 어렵기 때문에 시스템이 훨씬 더 간단해집니다. 프로덕션용 시스템의 경우, [머클 루트](/developers/tutorials/merkle-proofs-for-offline-data-integrity/)를 상태 해시로 사용하고 필요한 잔액에 대한 머클 증명을 제공할 수 있습니다. + +- 메모리 저장 공간. 프로덕션 시스템에서는 재시작 시 모든 계정 잔액을 보존하기 위해 디스크에 기록해야 합니다. 여기서는 정보가 단순히 손실되어도 괜찮습니다. + +- 전송만 가능. 프로덕션 시스템에서는 은행에 자산을 입금하고 출금하는 방법이 필요합니다. 하지만 여기서는 개념을 설명하기 위한 것이므로 이 은행은 전송으로 제한됩니다. + +### 영지식 증명 {#zero-knowledge-proofs} + +근본적인 수준에서 영지식 증명은 일부 공개 데이터인 _Datapublic_과 _Dataprivate_ 사이에 관계 _Relationship_이 존재하도록 증명자가 일부 데이터인 _Dataprivate_를 알고 있음을 보여줍니다. 검증자는 _Relationship_과 _Datapublic_을 알고 있습니다. + +개인정보를 보호하려면 상태와 트랜잭션을 비공개로 유지해야 합니다. 하지만 무결성을 보장하려면 상태의 [암호화 해시](https://en.wikipedia.org/wiki/Cryptographic_hash_function)를 공개해야 합니다. 트랜잭션을 제출하는 사람들에게 해당 트랜잭션이 실제로 발생했음을 증명하기 위해 트랜잭션 해시도 게시해야 합니다. + +대부분의 경우 _Dataprivate_은 영지식 증명 프로그램의 입력이고 _Datapublic_은 출력입니다. + +다음은 _Dataprivate_의 필드입니다. + +- _Staten_, 이전 상태 +- _Staten+1_, 새로운 상태 +- _Transaction_, 이전 상태에서 새로운 상태로 변경하는 트랜잭션. 이 트랜잭션에는 다음 필드가 포함되어야 합니다. + - _수신 주소_, 전송을 받는 주소 + - 전송되는 _금액_ + - 각 트랜잭션이 한 번만 처리되도록 보장하는 _Nonce_. + 발신 주소는 서명에서 복구할 수 있으므로 트랜잭션에 포함될 필요가 없습니다. +- _서명_, 트랜잭션 수행을 승인하는 서명. 이 경우 트랜잭션을 수행할 수 있는 유일한 권한이 있는 주소는 발신 주소입니다. 우리의 영지식 시스템이 작동하는 방식 때문에 이더리움 서명 외에도 계정의 공개 키가 필요합니다. + +다음은 _Datapublic_의 필드입니다. + +- _Hash(Staten)_ 이전 상태의 해시 +- _Hash(Staten+1)_ 새로운 상태의 해시 +- _Hash(Transaction)_, 상태를 _Staten_에서 _Staten+1_로 변경하는 트랜잭션의 해시. + +관계는 여러 조건을 확인합니다. + +- 공개 해시는 실제로 비공개 필드에 대한 올바른 해시입니다. +- 이전 상태에 적용된 트랜잭션은 새로운 상태를 생성합니다. +- 서명은 트랜잭션의 발신 주소에서 비롯됩니다. + +암호화 해시 함수의 속성으로 인해 이러한 조건을 증명하는 것만으로도 무결성을 보장할 수 있습니다. + +### 데이터 구조 {#data-structures} + +기본 데이터 구조는 서버가 보유한 상태입니다. 모든 계정에 대해 서버는 계정 잔액과 [재전송 공격](https://en.wikipedia.org/wiki/Replay_attack)을 방지하는 데 사용되는 [논스](https://en.wikipedia.org/wiki/Cryptographic_nonce)를 추적합니다. + +### 구성 요소 {#components} + +이 시스템에는 두 가지 구성 요소가 필요합니다. + +- 트랜잭션을 수신하고 처리하며, 영지식 증명과 함께 해시를 체인에 게시하는 _서버_. +- 해시를 저장하고 영지식 증명을 검증하여 상태 전환이 합법적인지 확인하는 _스마트 계약_. + +### 데이터 및 제어 흐름 {#flows} + +다음은 다양한 구성 요소가 한 계정에서 다른 계정으로 전송하기 위해 통신하는 방법입니다. + +1. 웹 브라우저는 서명자의 계정에서 다른 계정으로 전송을 요청하는 서명된 트랜잭션을 제출합니다. + +2. 서버는 트랜잭션이 유효한지 확인합니다. + + - 서명자는 은행에 충분한 잔액이 있는 계정을 가지고 있습니다. + - 수신자는 은행에 계정을 가지고 있습니다. + +3. 서버는 서명자의 잔액에서 전송된 금액을 빼고 수신자의 잔액에 더하여 새로운 상태를 계산합니다. + +4. 서버는 상태 변경이 유효하다는 영지식 증명을 계산합니다. + +5. 서버는 다음을 포함하는 트랜잭션을 이더리움에 제출합니다. + + - 새로운 상태 해시 + - 트랜잭션 해시(트랜잭션 발신자가 처리되었는지 알 수 있도록) + - 새로운 상태로의 전환이 유효함을 증명하는 영지식 증명 + +6. 스마트 계약은 영지식 증명을 검증합니다. + +7. 영지식 증명이 확인되면 스마트 계약은 다음 작업을 수행합니다. + - 현재 상태 해시를 새로운 상태 해시로 업데이트합니다. + - 새로운 상태 해시와 트랜잭션 해시로 로그 항목을 내보냅니다. + +### 도구 {#tools} + +클라이언트 측 코드의 경우 [Vite](https://vite.dev/), [React](https://react.dev/), [Viem](https://viem.sh/), [Wagmi](https://wagmi.sh/)를 사용할 것입니다. 이것들은 업계 표준 도구입니다. 익숙하지 않다면 [이 튜토리얼](/developers/tutorials/creating-a-wagmi-ui-for-your-contract/)을 사용할 수 있습니다. + +서버의 대부분은 [Node](https://nodejs.org/en)를 사용하는 JavaScript로 작성되었습니다. 영지식 부분은 [Noir](https://noir-lang.org/)로 작성되었습니다. `1.0.0-beta.10` 버전이 필요하므로 [지침에 따라 Noir를 설치](https://noir-lang.org/docs/getting_started/quick_start)한 후 다음을 실행합니다. + +``` +noirup -v 1.0.0-beta.10 +``` + +우리가 사용하는 블록체인은 [Foundry](https://getfoundry.sh/introduction/installation)의 일부인 로컬 테스트 블록체인 `anvil`입니다. + +## 구현 {#implementation} + +복잡한 시스템이므로 단계적으로 구현하겠습니다. + +### 1단계 - 수동 영지식 {#stage-1} + +첫 번째 단계에서는 브라우저에서 트랜잭션에 서명한 다음 영지식 증명에 정보를 수동으로 제공합니다. 영지식 코드는 `server/noir/Prover.toml`([여기](https://noir-lang.org/docs/getting_started/project_breakdown#provertoml-1)에 문서화됨)에서 해당 정보를 얻을 것으로 예상합니다. + +작동 모습을 보려면: + +1. [Node](https://nodejs.org/en/download)와 [Noir](https://noir-lang.org/install)가 설치되어 있는지 확인합니다. macOS, Linux 또는 [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)과 같은 UNIX 시스템에 설치하는 것이 좋습니다. + +2. 1단계 코드를 다운로드하고 웹 서버를 시작하여 클라이언트 코드를 제공합니다. + + ```sh + git clone https://github.com/qbzzt/250911-zk-bank.git -b 01-manual-zk + cd 250911-zk-bank + cd client + npm install + npm run dev + ``` + + 여기에 웹 서버가 필요한 이유는 특정 유형의 사기를 방지하기 위해 많은 지갑(예: MetaMask)이 디스크에서 직접 제공되는 파일을 허용하지 않기 때문입니다. + +3. 지갑이 있는 브라우저를 엽니다. + +4. 지갑에 새 암호 구문을 입력합니다. 이렇게 하면 기존 암호 구문이 삭제되므로 _반드시 백업해 두어야 합니다_. + + 암호 구문은 `test test test test test test test test test test test junk`이며, anvil의 기본 테스트 암호 구문입니다. + +5. [클라이언트 측 코드](http://localhost:5173/)로 이동합니다. + +6. 지갑에 연결하고 수신 계정과 금액을 선택합니다. + +7. **서명**을 클릭하고 트랜잭션에 서명합니다. + +8. **Prover.toml** 제목 아래에 텍스트가 있습니다. `server/noir/Prover.toml`을 해당 텍스트로 바꿉니다. + +9. 영지식 증명을 실행합니다. + + ```sh + cd ../server/noir + nargo execute + ``` + + 출력은 다음과 유사해야 합니다. + + ``` + ori@CryptoDocGuy:~/noir/250911-zk-bank/server/noir$ nargo execute + + [zkBank] Circuit witness successfully solved + [zkBank] Witness saved to target/zkBank.gz + [zkBank] Circuit output: (0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b, 0x0cfc0a67cb7308e4e9b254026b54204e34f6c8b041be207e64c5db77d95dd82d, 0x450cf9da6e180d6159290554ae3d8787, 0x6d8bc5a15b9037e52fb59b6b98722a85) + ``` + +10. 마지막 두 값을 웹 브라우저에 표시되는 해시와 비교하여 메시지가 올바르게 해시되었는지 확인합니다. + +#### `server/noir/Prover.toml` {#server-noir-prover-toml} + +[이 파일](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Prover.toml)은 Noir에서 예상하는 정보 형식을 보여줍니다. + +```toml +message="send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 500 finney (milliEth) 0 " +``` + +메시지는 텍스트 형식으로 되어 있어 사용자가 쉽게 이해할 수 있고(서명 시 필요함) Noir 코드가 구문 분석하기 쉽습니다. 금액은 피니 단위로 표시되어 한편으로는 소수점 단위의 전송을 가능하게 하고 다른 한편으로는 쉽게 읽을 수 있도록 합니다. 마지막 숫자는 [논스](https://en.wikipedia.org/wiki/Cryptographic_nonce)입니다. + +문자열의 길이는 100자입니다. 영지식 증명은 가변 크기 데이터를 잘 처리하지 못하므로 데이터를 패딩해야 하는 경우가 많습니다. + +```toml +pubKeyX=["0x83",...,"0x75"] +pubKeyY=["0x35",...,"0xa5"] +signature=["0xb1",...,"0x0d"] +``` + +이 세 가지 매개변수는 고정 크기 바이트 배열입니다. + +```toml +[[accounts]] +address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" +balance=100_000 +nonce=0 + +[[accounts]] +address="0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +balance=100_000 +nonce=0 +``` + +이는 구조체 배열을 지정하는 방법입니다. 각 항목에 대해 주소, 잔액(밀리ETH, 즉 [피니](https://cryptovalleyjournal.com/glossary/finney/)) 및 다음 논스 값을 지정합니다. + +#### `client/src/Transfer.tsx` {#client-src-transfer-tsx} + +[이 파일](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/client/src/Transfer.tsx)은 클라이언트 측 처리를 구현하고 `server/noir/Prover.toml` 파일(영지식 매개변수를 포함하는 파일)을 생성합니다. + +다음은 더 흥미로운 부분에 대한 설명입니다. + +```tsx +export default attrs => { +``` + +이 함수는 다른 파일에서 가져올 수 있는 `Transfer` React 구성 요소를 만듭니다. + +```tsx + const accounts = [ + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "0x90F79bf6EB2c4f870365E785982E1f101E93b906", + "0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65", + ] +``` + +이것은 계정 주소, 즉 `test ...`에 의해 생성된 주소입니다. test junk` 암호 구문. 자신만의 주소를 사용하려면 이 정의를 수정하기만 하면 됩니다. + +```tsx + const account = useAccount() + const wallet = createWalletClient({ + transport: custom(window.ethereum!) + }) +``` + +이러한 [Wagmi 후크](https://wagmi.sh/react/api/hooks)를 통해 [viem](https://viem.sh/) 라이브러리와 지갑에 접근할 수 있습니다. + +```tsx + const message = `send ${toAccount} ${ethAmount*1000} finney (milliEth) ${nonce}`.padEnd(100, " ") +``` + +이것은 공백으로 채워진 메시지입니다. [`useState`](https://react.dev/reference/react/useState) 변수 중 하나가 변경될 때마다 구성 요소가 다시 그려지고 `message`가 업데이트됩니다. + +```tsx + const sign = async () => { +``` + +이 함수는 사용자가 **서명** 버튼을 클릭할 때 호출됩니다. 메시지는 자동으로 업데이트되지만, 서명에는 지갑에서 사용자 승인이 필요하며, 필요하지 않은 경우에는 요청하고 싶지 않습니다. + +```tsx + const signature = await wallet.signMessage({ + account: fromAccount, + message, + }) +``` + +지갑에 [메시지 서명](https://viem.sh/docs/accounts/local/signMessage)을 요청합니다. + +```tsx + const hash = hashMessage(message) +``` + +메시지 해시를 가져옵니다. 디버깅(Noir 코드의)을 위해 사용자에게 제공하는 것이 도움이 됩니다. + +```tsx + const pubKey = await recoverPublicKey({ + hash, + signature + }) +``` + +[공개 키 가져오기](https://viem.sh/docs/utilities/recoverPublicKey). 이는 [Noir `ecrecover`](https://github.com/colinnielsen/ecrecover-noir) 함수에 필요합니다. + +```tsx + setSignature(signature) + setHash(hash) + setPubKey(pubKey) +``` + +상태 변수를 설정합니다. 이렇게 하면 구성 요소가 다시 그려지고 사용자에게 업데이트된 값이 표시됩니다. + +```tsx + let proverToml = ` +``` + +`Prover.toml`의 텍스트입니다. + +```tsx +message="${message}" + +pubKeyX=${hexToArray(pubKey.slice(4,4+2*32))} +pubKeyY=${hexToArray(pubKey.slice(4+2*32))} +``` + +Viem은 65바이트 16진수 문자열로 공개 키를 제공합니다. 첫 번째 바이트는 버전 마커인 `0x04`입니다. 그 뒤에는 공개 키의 `x`에 대한 32바이트와 공개 키의 `y`에 대한 32바이트가 이어집니다. + +그러나 Noir는 이 정보를 두 개의 바이트 배열, 즉 `x`용과 `y`용으로 받기를 기대합니다. 영지식 증명의 일부가 아닌 클라이언트에서 구문 분석하는 것이 더 쉽습니다. + +이는 일반적으로 영지식에서 좋은 관행입니다. 영지식 증명 내부의 코드는 비용이 많이 들기 때문에 영지식 증명 외부에서 수행할 수 있는 모든 처리는 영지식 증명 외부에서 수행_해야 합니다_. + +```tsx +signature=${hexToArray(signature.slice(2,-2))} +``` + +서명은 65바이트 16진수 문자열로도 제공됩니다. 그러나 마지막 바이트는 공개 키를 복구하는 데만 필요합니다. 공개 키는 이미 Noir 코드에 제공될 것이므로 서명을 확인하는 데 필요하지 않으며 Noir 코드에서도 요구하지 않습니다. + +```tsx +${accounts.map(accountInProverToml).reduce((a,b) => a+b, "")} +` +``` + +계정을 제공합니다. + +```tsx + setProverToml(proverToml) + } + + return ( + <> +

전송

+``` + +이것은 구성 요소의 HTML(더 정확하게는 [JSX](https://react.dev/learn/writing-markup-with-jsx)) 형식입니다. + +#### `server/noir/src/main.nr` {#server-noir-src-main-nr} + +[이 파일](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/src/main.nr)은 실제 영지식 코드입니다. + +``` +use std::hash::pedersen_hash; +``` + +[페더슨 해시](https://rya-sge.github.io/access-denied/2024/05/07/pedersen-hash-function/)는 [Noir 표준 라이브러리](https://noir-lang.org/docs/noir/standard_library/cryptographic_primitives/hashes#pedersen_hash)와 함께 제공됩니다. 영지식 증명은 일반적으로 이 해시 함수를 사용합니다. 표준 해시 함수에 비해 [산술 회로](https://rareskills.io/post/arithmetic-circuit) 내부에서 계산하기가 훨씬 쉽습니다. + +``` +use keccak256::keccak256; +use dep::ecrecover; +``` + +이 두 함수는 [`Nargo.toml`](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Nargo.toml)에 정의된 외부 라이브러리입니다. 이름 그대로 [keccak256 해시](https://emn178.github.io/online-tools/keccak_256.html)를 계산하는 함수와 이더리움 서명을 확인하고 서명자의 이더리움 주소를 복구하는 함수입니다. + +``` +global ACCOUNT_NUMBER : u32 = 5; +``` + +Noir는 [Rust](https://www.rust-lang.org/)에서 영감을 받았습니다. 변수는 기본적으로 상수입니다. 이것이 전역 구성 상수를 정의하는 방법입니다. 특히 `ACCOUNT_NUMBER`는 우리가 저장하는 계정의 수입니다. + +`u`라는 이름의 데이터 유형은 해당 비트 수의 부호 없는 숫자입니다. 지원되는 유형은 `u8`, `u16`, `u32`, `u64` 및 `u128`뿐입니다. + +``` +global FLAT_ACCOUNT_FIELDS : u32 = 2; +``` + +이 변수는 아래 설명된 대로 계정의 페더슨 해시에 사용됩니다. + +``` +global MESSAGE_LENGTH : u32 = 100; +``` + +위에서 설명한 것처럼 메시지 길이는 고정되어 있습니다. 여기에 지정되어 있습니다. + +``` +global ASCII_MESSAGE_LENGTH : [u8; 3] = [0x31, 0x30, 0x30]; +global HASH_BUFFER_SIZE : u32 = 26+3+MESSAGE_LENGTH; +``` + +[EIP-191 서명](https://eips.ethereum.org/EIPS/eip-191)에는 26바이트 접두사, ASCII의 메시지 길이, 마지막으로 메시지 자체가 포함된 버퍼가 필요합니다. + +``` +struct Account { + balance: u128, + address: Field, + nonce: u32, +} +``` + +계정에 대해 저장하는 정보입니다. [`Field`](https://noir-lang.org/docs/noir/concepts/data_types/fields)는 일반적으로 최대 253비트의 숫자로, 영지식 증명을 구현하는 [산술 회로](https://rareskills.io/post/arithmetic-circuit)에서 직접 사용할 수 있습니다. 여기서는 `Field`를 사용하여 160비트 이더리움 주소를 저장합니다. + +``` +struct TransferTxn { + from: Field, + to: Field, + amount: u128, + nonce: u32 +} +``` + +전송 트랜잭션에 대해 저장하는 정보입니다. + +``` +fn flatten_account(account: Account) -> [Field; FLAT_ACCOUNT_FIELDS] { +``` + +함수 정의. 매개변수는 `Account` 정보입니다. 결과는 길이가 `FLAT_ACCOUNT_FIELDS`인 `Field` 변수의 배열입니다. + +``` + let flat = [ + account.address, + ((account.balance << 32) + account.nonce.into()).into(), + ]; +``` + +배열의 첫 번째 값은 계정 주소입니다. 두 번째 값에는 잔액과 논스가 모두 포함됩니다. `.into()` 호출은 숫자를 필요한 데이터 유형으로 변경합니다. `account.nonce`는 `u32` 값이지만, `u128` 값인 `account.balance « 32`에 더하려면 `u128`이어야 합니다. 이것이 첫 번째 `.into()`입니다. 두 번째는 `u128` 결과를 `Field`로 변환하여 배열에 맞도록 합니다. + +``` + flat +} +``` + +Noir에서 함수는 끝에서만 값을 반환할 수 있습니다(조기 반환은 없음). 반환 값을 지정하려면 함수의 닫는 괄호 바로 앞에서 평가합니다. + +``` +fn flatten_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] { +``` + +이 함수는 계정 배열을 Petersen 해시의 입력으로 사용할 수 있는 `Field` 배열로 바꿉니다. + +``` + let mut flat: [Field; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER] = [0; FLAT_ACCOUNT_FIELDS*ACCOUNT_NUMBER]; +``` + +이것은 가변 변수, 즉 상수가 _아닌_ 변수를 지정하는 방법입니다. Noir의 변수는 항상 값을 가져야 하므로 이 변수를 모두 0으로 초기화합니다. + +``` + for i in 0..ACCOUNT_NUMBER { +``` + +이것은 `for` 루프입니다. 경계는 상수입니다. Noir 루프는 컴파일 시간에 경계를 알아야 합니다. 그 이유는 산술 회로가 흐름 제어를 지원하지 않기 때문입니다. `for` 루프를 처리할 때 컴파일러는 각 반복에 대해 한 번씩 코드를 여러 번 넣습니다. + +``` + let fields = flatten_account(accounts[i]); + for j in 0..FLAT_ACCOUNT_FIELDS { + flat[i*FLAT_ACCOUNT_FIELDS + j] = fields[j]; + } + } + + flat +} + +fn hash_accounts(accounts: [Account; ACCOUNT_NUMBER]) -> Field { + pedersen_hash(flatten_accounts(accounts)) +} +``` + +마지막으로 계정 배열을 해시하는 함수에 도달했습니다. + +``` +fn find_account(accounts: [Account; ACCOUNT_NUMBER], address: Field) -> u32 { + let mut account : u32 = ACCOUNT_NUMBER; + + for i in 0..ACCOUNT_NUMBER { + if accounts[i].address == address { + account = i; + } + } +``` + +이 함수는 특정 주소를 가진 계정을 찾습니다. 이 함수는 주소를 찾은 후에도 모든 계정을 반복하기 때문에 표준 코드에서는 매우 비효율적입니다. + +그러나 영지식 증명에는 흐름 제어가 없습니다. 조건을 확인해야 하는 경우 매번 확인해야 합니다. + +`if` 문에서도 비슷한 일이 발생합니다. 위의 루프에 있는 `if` 문은 다음 수학적 문으로 변환됩니다. + +_conditionresult = accounts[i].address == address_ // 같으면 1, 그렇지 않으면 0 + +_accountnew = conditionresult\*i + (1-conditionresult)\*accountold_ + +```rust + assert (account < ACCOUNT_NUMBER, f"{address} does not have an account"); + + account +} +``` + +[`assert`](https://noir-lang.org/docs/dev/noir/concepts/assert) 함수는 어설션이 거짓이면 영지식 증명을 충돌시킵니다. 이 경우 관련 주소를 가진 계정을 찾을 수 없습니다. 주소를 보고하려면 [서식 문자열](https://noir-lang.org/docs/noir/concepts/data_types/strings#format-strings)을 사용합니다. + +```rust +fn apply_transfer_txn(accounts: [Account; ACCOUNT_NUMBER], txn: TransferTxn) -> [Account; ACCOUNT_NUMBER] { +``` + +이 함수는 전송 트랜잭션을 적용하고 새 계정 배열을 반환합니다. + +```rust + let from = find_account(accounts, txn.from); + let to = find_account(accounts, txn.to); + + let (txnFrom, txnAmount, txnNonce, accountNonce) = + (txn.from, txn.amount, txn.nonce, accounts[from].nonce); +``` + +Noir의 서식 문자열 내부에서는 구조 요소에 액세스할 수 없으므로 사용 가능한 복사본을 만듭니다. + +```rust + assert (accounts[from].balance >= txn.amount, + f"{txnFrom} does not have {txnAmount} finney"); + + assert (accounts[from].nonce == txn.nonce, + f"Transaction has nonce {txnNonce}, but the account is expected to use {accountNonce}"); +``` + +이들은 트랜잭션을 무효화할 수 있는 두 가지 조건입니다. + +```rust + let mut newAccounts = accounts; + + newAccounts[from].balance -= txn.amount; + newAccounts[from].nonce += 1; + newAccounts[to].balance += txn.amount; + + newAccounts +} +``` + +새 계정 배열을 만들고 반환합니다. + +```rust +fn readAddress(messageBytes: [u8; MESSAGE_LENGTH]) -> Field +``` + +이 함수는 메시지에서 주소를 읽습니다. + +```rust +{ + let mut result : Field = 0; + + for i in 7..47 { +``` + +주소는 항상 20바이트(즉 16진수 40자리) 길이이며 7번째 문자에서 시작합니다. + +```rust + result *= 0x10; + if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9 + result += (messageBytes[i]-48).into(); + } + if messageBytes[i] >= 65 & messageBytes[i] <= 70 { // A-F + result += (messageBytes[i]-65+10).into() + } + if messageBytes[i] >= 97 & messageBytes[i] <= 102 { // a-f + result += (messageBytes[i]-97+10).into() + } + } + + result +} + +fn readAmountAndNonce(messageBytes: [u8; MESSAGE_LENGTH]) -> (u128, u32) +``` + +메시지에서 금액과 논스를 읽습니다. + +```rust +{ + let mut amount : u128 = 0; + let mut nonce: u32 = 0; + let mut stillReadingAmount: bool = true; + let mut lookingForNonce: bool = false; + let mut stillReadingNonce: bool = false; +``` + +메시지에서 주소 뒤의 첫 번째 숫자는 전송할 피니(즉 ETH의 천분의 일)의 양입니다. 두 번째 숫자는 논스입니다. 그 사이의 텍스트는 무시됩니다. + +```rust + for i in 48..MESSAGE_LENGTH { + if messageBytes[i] >= 48 & messageBytes[i] <= 57 { // 0-9 + let digit = (messageBytes[i]-48); + + if stillReadingAmount { + amount = amount*10 + digit.into(); + } + + if lookingForNonce { // We just found it + stillReadingNonce = true; + lookingForNonce = false; + } + + if stillReadingNonce { + nonce = nonce*10 + digit.into(); + } + } else { + if stillReadingAmount { + stillReadingAmount = false; + lookingForNonce = true; + } + if stillReadingNonce { + stillReadingNonce = false; + } + } + } + + (amount, nonce) +} +``` + +[튜플](https://noir-lang.org/docs/noir/concepts/data_types/tuples)을 반환하는 것은 Noir가 함수에서 여러 값을 반환하는 방법입니다. + +```rust +fn readTransferTxn(message: str) -> TransferTxn +{ + let mut txn: TransferTxn = TransferTxn { from: 0, to: 0, amount:0, nonce:0 }; + let messageBytes = message.as_bytes(); + + txn.to = readAddress(messageBytes); + let (amount, nonce) = readAmountAndNonce(messageBytes); + txn.amount = amount; + txn.nonce = nonce; + + txn +} +``` + +이 함수는 메시지를 바이트로 변환한 다음 금액을 `TransferTxn`으로 변환합니다. + +```rust +// Viem의 hashMessage에 해당함 +// https://viem.sh/docs/utilities/hashMessage#hashmessage +fn hashMessage(message: str) -> [u8;32] { +``` + +계정은 영지식 증명 내에서만 해시되므로 페더슨 해시를 사용할 수 있었습니다. 그러나 이 코드에서는 브라우저에서 생성된 메시지의 서명을 확인해야 합니다. 이를 위해 [EIP 191](https://eips.ethereum.org/EIPS/eip-191)의 이더리움 서명 형식을 따라야 합니다. 이는 표준 접두사, ASCII의 메시지 길이, 메시지 자체를 사용하여 결합된 버퍼를 만들고 이더리움 표준 keccak256을 사용하여 해시해야 함을 의미합니다. + +```rust + // ASCII 접두사 + let prefix_bytes = [ + 0x19, // \x19 + 0x45, // 'E' + 0x74, // 't' + 0x68, // 'h' + 0x65, // 'e' + 0x72, // 'r' + 0x65, // 'e' + 0x75, // 'u' + 0x6D, // 'm' + 0x20, // ' ' + 0x53, // 'S' + 0x69, // 'i' + 0x67, // 'g' + 0x6E, // 'n' + 0x65, // 'e' + 0x64, // 'd' + 0x20, // ' ' + 0x4D, // 'M' + 0x65, // 'e' + 0x73, // 's' + 0x73, // 's' + 0x61, // 'a' + 0x67, // 'g' + 0x65, // 'e' + 0x3A, // ':' + 0x0A // '\n' + ]; +``` + +애플리케이션이 사용자에게 트랜잭션이나 다른 목적으로 사용될 수 있는 메시지에 서명하도록 요청하는 경우를 방지하기 위해 EIP 191에서는 모든 서명된 메시지가 0x19(유효한 ASCII 문자가 아님) 문자로 시작하고 그 뒤에 `Ethereum Signed Message:`와 줄 바꿈이 오도록 지정합니다. + +```rust + let mut buffer: [u8; HASH_BUFFER_SIZE] = [0u8; HASH_BUFFER_SIZE]; + for i in 0..26 { + buffer[i] = prefix_bytes[i]; + } + + let messageBytes : [u8; MESSAGE_LENGTH] = message.as_bytes(); + + if MESSAGE_LENGTH <= 9 { + for i in 0..1 { + buffer[i+26] = ASCII_MESSAGE_LENGTH[i]; + } + + for i in 0..MESSAGE_LENGTH { + buffer[i+26+1] = messageBytes[i]; + } + } + + if MESSAGE_LENGTH >= 10 & MESSAGE_LENGTH <= 99 { + for i in 0..2 { + buffer[i+26] = ASCII_MESSAGE_LENGTH[i]; + } + + for i in 0..MESSAGE_LENGTH { + buffer[i+26+2] = messageBytes[i]; + } + } + + if MESSAGE_LENGTH >= 100 { + for i in 0..3 { + buffer[i+26] = ASCII_MESSAGE_LENGTH[i]; + } + + for i in 0..MESSAGE_LENGTH { + buffer[i+26+3] = messageBytes[i]; + } + } + + assert(MESSAGE_LENGTH < 1000, "Messages whose length is over three digits are not supported"); +``` + +최대 999개의 메시지 길이를 처리하고 그보다 크면 실패합니다. 메시지 길이가 상수임에도 불구하고 이 코드를 추가한 이유는 변경하기가 더 쉽기 때문입니다. 프로덕션 시스템에서는 더 나은 성능을 위해 `MESSAGE_LENGTH`가 변경되지 않는다고 가정할 것입니다. + +```rust + keccak256::keccak256(buffer, HASH_BUFFER_SIZE) +} +``` + +이더리움 표준 `keccak256` 함수를 사용합니다. + +```rust +fn signatureToAddressAndHash( + message: str, + pubKeyX: [u8; 32], + pubKeyY: [u8; 32], + signature: [u8; 64] + ) -> (Field, Field, Field) // address, first 16 bytes of hash, last 16 bytes of hash +{ +``` + +이 함수는 서명을 확인하며, 여기에는 메시지 해시가 필요합니다. 그런 다음 서명한 주소와 메시지 해시를 제공합니다. 메시지 해시는 바이트 배열보다 프로그램의 나머지 부분에서 사용하기 쉬운 두 개의 `Field` 값으로 제공됩니다. + +필드 계산은 큰 숫자의 [나머지](https://en.wikipedia.org/wiki/Modulo)로 수행되지만 해당 숫자는 일반적으로 256비트보다 작기 때문에(그렇지 않으면 EVM에서 해당 계산을 수행하기 어려움) 두 개의 `Field` 값을 사용해야 합니다. + +```rust + let hash = hashMessage(message); + + let mut (hash1, hash2) = (0,0); + + for i in 0..16 { + hash1 = hash1*256 + hash[31-i].into(); + hash2 = hash2*256 + hash[15-i].into(); + } +``` + +`hash1`과 `hash2`를 가변 변수로 지정하고 해시를 바이트 단위로 씁니다. + +```rust + ( + ecrecover::ecrecover(pubKeyX, pubKeyY, signature, hash), +``` + +이는 [솔리디티의 `ecrecover`](https://docs.soliditylang.org/en/v0.8.30/cheatsheet.html#mathematical-and-cryptographic-functions)와 유사하지만 두 가지 중요한 차이점이 있습니다. + +- 서명이 유효하지 않으면 호출이 `assert`에 실패하고 프로그램이 중단됩니다. +- 공개 키는 서명과 해시에서 복구할 수 있지만, 이는 외부에서 수행할 수 있는 처리이므로 영지식 증명 내에서 수행할 가치가 없습니다. 여기서 누군가 우리를 속이려고 하면 서명 확인이 실패합니다. + +```rust + hash1, + hash2 + ) +} + +fn main( + accounts: [Account; ACCOUNT_NUMBER], + message: str, + pubKeyX: [u8; 32], + pubKeyY: [u8; 32], + signature: [u8; 64], + ) -> pub ( + Field, // Hash of old accounts array + Field, // Hash of new accounts array + Field, // First 16 bytes of message hash + Field, // Last 16 bytes of message hash + ) +``` + +마지막으로 `main` 함수에 도달합니다. 계정 해시를 이전 값에서 새 값으로 유효하게 변경하는 트랜잭션이 있음을 증명해야 합니다. 또한 보낸 사람이 자신의 트랜잭션이 처리되었음을 알 수 있도록 특정 트랜잭션 해시가 있음을 증명해야 합니다. + +```rust +{ + let mut txn = readTransferTxn(message); +``` + +메시지에서 발신 주소를 읽는 것이 아니라 서명에서 읽기 때문에 `txn`이 변경 가능해야 합니다. + +```rust + let (fromAddress, txnHash1, txnHash2) = signatureToAddressAndHash( + message, + pubKeyX, + pubKeyY, + signature); + + txn.from = fromAddress; + + let newAccounts = apply_transfer_txn(accounts, txn); + + ( + hash_accounts(accounts), + hash_accounts(newAccounts), + txnHash1, + txnHash2 + ) +} +``` + +### 2단계 - 서버 추가 {#stage-2} + +두 번째 단계에서는 브라우저에서 전송 트랜잭션을 수신하고 구현하는 서버를 추가합니다. + +작동 모습을 보려면: + +1. Vite가 실행 중이면 중지합니다. + +2. 서버가 포함된 브랜치를 다운로드하고 필요한 모든 모듈이 있는지 확인합니다. + + ```sh + git checkout 02-add-server + cd client + npm install + cd ../server + npm install + ``` + + Noir 코드를 컴파일할 필요는 없으며, 1단계에서 사용한 코드와 동일합니다. + +3. 서버를 시작하세요. + + ```sh + npm run start + ``` + +4. 별도의 명령줄 창에서 Vite를 실행하여 브라우저 코드를 제공합니다. + + ```sh + cd client + npm run dev + ``` + +5. [http://localhost:5173](http://localhost:5173)에서 클라이언트 코드로 이동합니다. + +6. 트랜잭션을 실행하기 전에 보낼 수 있는 금액과 논스를 알아야 합니다. 이 정보를 얻으려면 **계정 데이터 업데이트**를 클릭하고 메시지에 서명합니다. + + 여기에 딜레마가 있습니다. 한편으로 우리는 재사용될 수 있는 메시지에 서명하는 것을 원하지 않으며([재전송 공격](https://en.wikipedia.org/wiki/Replay_attack)), 이것이 우리가 애초에 논스를 원하는 이유입니다. 하지만 아직 논스가 없습니다. 해결책은 한 번만 사용할 수 있고 양쪽 모두에 이미 있는 논스(예: 현재 시간)를 선택하는 것입니다. + + 이 해결책의 문제는 시간이 완벽하게 동기화되지 않을 수 있다는 것입니다. 따라서 대신 1분마다 변경되는 값에 서명합니다. 이는 재전송 공격에 대한 취약성 기간이 최대 1분임을 의미합니다. 프로덕션에서 서명된 요청은 TLS로 보호되고 터널의 다른 쪽(서버)이 이미 잔액과 논스를 공개할 수 있다는 점(작동하려면 알아야 함)을 고려할 때 이는 수용 가능한 위험입니다. + +7. 브라우저가 잔액과 논스를 다시 받으면 전송 양식이 표시됩니다. 수신 주소와 금액을 선택하고 **전송**을 클릭합니다. 이 요청에 서명합니다. + +8. 전송을 보려면 **계정 데이터 업데이트**를 하거나 서버를 실행하는 창을 확인하세요. 서버는 상태가 변경될 때마다 상태를 기록합니다. + + ``` + ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start + + > server@1.0.0 start + > node --experimental-json-modules index.mjs + + Listening on port 3000 + Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 36000 finney (milliEth) 0 processed + New state: + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 64000 (1) + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 100000 (0) + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) + 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) + 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) + Txn send 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 7200 finney (milliEth) 1 processed + New state: + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 56800 (2) + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) + 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 136000 (0) + 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) + Txn send 0x90F79bf6EB2c4f870365E785982E1f101E93b906 3000 finney (milliEth) 2 processed + New state: + 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 has 53800 (3) + 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 has 107200 (0) + 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC has 100000 (0) + 0x90F79bf6EB2c4f870365E785982E1f101E93b906 has 139000 (0) + 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 has 100000 (0) + ``` + +#### `server/index.mjs` {#server-index-mjs-1} + +[이 파일](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/index.mjs)은 서버 프로세스를 포함하고 [`main.nr`](https://github.com/qbzzt/250911-zk-bank/blob/02-add-server/server/noir/src/main.nr)에서 Noir 코드와 상호 작용합니다. 다음은 흥미로운 부분에 대한 설명입니다. + +```js +import { Noir } from '@noir-lang/noir_js' +``` + +[noir.js](https://www.npmjs.com/package/@noir-lang/noir_js) 라이브러리는 JavaScript 코드와 Noir 코드 간의 인터페이스입니다. + +```js +const circuit = JSON.parse(await fs.readFile("./noir/target/zkBank.json")) +const noir = new Noir(circuit) +``` + +산술 회로(이전 단계에서 만든 컴파일된 Noir 프로그램)를 로드하고 실행할 준비를 합니다. + +```js +// 서명된 요청에 대한 응답으로만 계정 정보를 제공합니다. +const accountInformation = async signature => { + const fromAddress = await recoverAddress({ + hash: hashMessage("Get account data " + Math.floor((new Date().getTime())/60000)), + signature + }) +``` + +계정 정보를 제공하려면 서명만 필요합니다. 메시지가 어떻게 될지 이미 알고 있으므로 메시지 해시도 알고 있기 때문입니다. + +```js +const processMessage = async (message, signature) => { +``` + +메시지를 처리하고 인코딩하는 트랜잭션을 실행합니다. + +```js + // 공개 키 가져오기 + const pubKey = await recoverPublicKey({ + hash, + signature + }) +``` + +이제 서버에서 JavaScript를 실행하므로 클라이언트가 아닌 서버에서 공개 키를 검색할 수 있습니다. + +```js + let noirResult + try { + noirResult = await noir.execute({ + message, + signature: signature.slice(2,-2).match(/.{2}/g).map(x => `0x${x}`), + pubKeyX, + pubKeyY, + accounts: Accounts + }) +``` + +`noir.execute`는 Noir 프로그램을 실행합니다. 매개변수는 [`Prover.toml`](https://github.com/qbzzt/250911-zk-bank/blob/01-manual-zk/server/noir/Prover.toml)에 제공된 매개변수와 동일합니다. 긴 값은 Viem에서와 같이 단일 16진수 값(`0x60A7`)이 아닌 16진수 문자열 배열(`["0x60", "0xA7"]`)로 제공됩니다. + +```js + } catch (err) { + console.log(`Noir error: ${err}`) + throw Error("Invalid transaction, not processed") + } +``` + +오류가 발생하면 이를 포착한 다음 단순화된 버전을 클라이언트에 전달합니다. + +```js + Accounts[fromAccountNumber].nonce++ + Accounts[fromAccountNumber].balance -= amount + Accounts[toAccountNumber].balance += amount +``` + +트랜잭션을 적용합니다. 이미 Noir 코드에서 수행했지만 거기에서 결과를 추출하는 것보다 여기에서 다시 수행하는 것이 더 쉽습니다. + +```js +let Accounts = [ + { + address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + balance: 5000, + nonce: 0, + }, +``` + +초기 `Accounts` 구조입니다. + +### 3단계 - 이더리움 스마트 계약 {#stage-3} + +1. 서버 및 클라이언트 프로세스를 중지합니다. + +2. 스마트 계약이 포함된 브랜치를 다운로드하고 필요한 모든 모듈이 있는지 확인합니다. + + ```sh + git checkout 03-smart-contracts + cd client + npm install + cd ../server + npm install + ``` + +3. 별도의 명령줄 창에서 `anvil`을 실행합니다. + +4. 검증 키와 솔리디티 검증기를 생성한 다음 검증기 코드를 솔리디티 프로젝트에 복사합니다. + + ```sh + cd noir + bb write_vk -b ./target/zkBank.json -o ./target --oracle_hash keccak + bb write_solidity_verifier -k ./target/vk -o ./target/Verifier.sol + cp target/Verifier.sol ../../smart-contracts/src + ``` + +5. 스마트 계약으로 이동하여 `anvil` 블록체인을 사용하도록 환경 변수를 설정합니다. + + ```sh + cd ../../smart-contracts + export ETH_RPC_URL=http://localhost:8545 + ETH_PRIVATE_KEY=ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + ``` + +6. `Verifier.sol`을 배포하고 주소를 환경 변수에 저장합니다. + + ```sh + VERIFIER_ADDRESS=`forge create src/Verifier.sol:HonkVerifier --private-key $ETH_PRIVATE_KEY --optimize --broadcast | awk '/Deployed to:/ {print $3}'` + echo $VERIFIER_ADDRESS + ``` + +7. `ZkBank` 계약을 배포합니다. + + ```sh + ZKBANK_ADDRESS=`forge create ZkBank --private-key $ETH_PRIVATE_KEY --broadcast --constructor-args $VERIFIER_ADDRESS 0x199aa62af8c1d562a6ec96e66347bf3240ab2afb5d022c895e6bf6a5e617167b | awk '/Deployed to:/ {print $3}'` + echo $ZKBANK_ADDRESS + ``` + + `0x199..67b` 값은 `Accounts` 초기 상태의 페더슨 해시입니다. `server/index.mjs`에서 이 초기 상태를 수정하면 트랜잭션을 실행하여 영지식 증명에서 보고된 초기 해시를 볼 수 있습니다. + +8. 서버를 실행합니다. + + ```sh + cd ../server + npm run start + ``` + +9. 다른 명령줄 창에서 클라이언트를 실행합니다. + + ```sh + cd client + npm run dev + ``` + +10. 일부 트랜잭션을 실행합니다. + +11. 상태가 온체인에서 변경되었는지 확인하려면 서버 프로세스를 다시 시작합니다. 트랜잭션의 원래 해시 값이 온체인에 저장된 해시 값과 다르기 때문에 `ZkBank`가 더 이상 트랜잭션을 수락하지 않는 것을 확인합니다. + + 이것은 예상되는 오류 유형입니다. + + ``` + ori@CryptoDocGuy:~/x/250911-zk-bank/server$ npm run start + + > server@1.0.0 start + > node --experimental-json-modules index.mjs + + Listening on port 3000 + Verification error: ContractFunctionExecutionError: The contract function "processTransaction" reverted with the following reason: + Wrong old state hash + + Contract Call: + address: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 + function: processTransaction(bytes _proof, bytes32[] _publicInputs) + args: (0x0000000000000000000000000000000000000000000000042ab5d6d1986846cf00000000000000000000000000000000000000000000000b75c020998797da7800000000000000000000000000000000000000000000000 + ``` + +#### `server/index.mjs` {#server-index-mjs-2} + +이 파일의 변경 사항은 주로 실제 증명을 만들고 온체인에 제출하는 것과 관련이 있습니다. + +```js +import { exec } from 'child_process' +import util from 'util' + +const execPromise = util.promisify(exec) +``` + +온체인으로 보낼 실제 증명을 만들려면 [Barretenberg 패키지](https://github.com/AztecProtocol/aztec-packages/tree/next/barretenberg)를 사용해야 합니다. 이 패키지는 명령줄 인터페이스(`bb`)를 실행하거나 [JavaScript 라이브러리인 `bb.js`](https://www.npmjs.com/package/@aztec/bb.js)를 사용하여 사용할 수 있습니다. JavaScript 라이브러리는 네이티브 코드를 실행하는 것보다 훨씬 느리므로 여기서는 [`exec`](https://nodejs.org/api/child_process.html#child_processexeccommand-options-callback)를 사용하여 명령줄을 사용합니다. + +`bb.js`를 사용하기로 결정했다면 사용 중인 Noir 버전과 호환되는 버전을 사용해야 합니다. 작성 당시 현재 Noir 버전(1.0.0-beta.11)은 `bb.js` 버전 0.87을 사용합니다. + +```js +const zkBankAddress = process.env.ZKBANK_ADDRESS || "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" +``` + +여기 주소는 깨끗한 `anvil`로 시작하여 위 지침을 따를 때 얻는 주소입니다. + +```js +const walletClient = createWalletClient({ + chain: anvil, + transport: http(), + account: privateKeyToAccount("0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6") +}) +``` + +이 개인 키는 `anvil`의 기본 사전 자금 지원 계정 중 하나입니다. + +```js +const generateProof = async (witness, fileID) => { +``` + +`bb` 실행 파일을 사용하여 증명을 생성합니다. + +```js + const fname = `witness-${fileID}.gz` + await fs.writeFile(fname, witness) +``` + +증인을 파일에 씁니다. + +```js + await execPromise(`bb prove -b ./noir/target/zkBank.json -w ${fname} -o ${fileID} --oracle_hash keccak --output_format fields`) +``` + +실제로 증명을 만듭니다. 이 단계에서는 공개 변수가 있는 파일도 만들지만 필요하지 않습니다. 이미 `noir.execute`에서 해당 변수를 가져왔습니다. + +```js + const proof = "0x" + JSON.parse(await fs.readFile(`./${fileID}/proof_fields.json`)).reduce((a,b) => a+b, "").replace(/0x/g, "") +``` + +증명은 각각 16진수 값으로 표시되는 `Field` 값의 JSON 배열입니다. 그러나 Viem이 큰 16진수 문자열로 나타내는 단일 `bytes` 값으로 트랜잭션에서 보내야 합니다. 여기서는 모든 값을 연결하고 모든 `0x`를 제거한 다음 끝에 하나를 추가하여 형식을 변경합니다. + +```js + await execPromise(`rm -r ${fname} ${fileID}`) + + return proof +} +``` + +정리하고 증명을 반환합니다. + +```js +const processMessage = async (message, signature) => { + . + . + . + + const publicFields = noirResult.returnValue.map(x=>'0x' + x.slice(2).padStart(64, "0")) +``` + +공개 필드는 32바이트 값의 배열이어야 합니다. 그러나 두 개의 `Field` 값 사이에 트랜잭션 해시를 분할해야 했기 때문에 16바이트 값으로 나타납니다. 여기서는 Viem이 실제로는 32바이트라는 것을 이해하도록 0을 추가합니다. + +```js + const proof = await generateProof(noirResult.witness, `${fromAddress}-${nonce}`) +``` + +각 주소는 각 논스를 한 번만 사용하므로 증인 파일과 출력 디렉터리의 고유 식별자로 `fromAddress`와 `nonce`의 조합을 사용할 수 있습니다. + +```js + try { + await zkBank.write.processTransaction([ + proof, publicFields]) + } catch (err) { + console.log(`Verification error: ${err}`) + throw Error("Can't verify the transaction onchain") + } + . + . + . +} +``` + +체인에 트랜잭션을 보냅니다. + +#### `smart-contracts/src/ZkBank.sol` {#smart-contracts-src-zkbank-sol} + +이것은 트랜잭션을 받는 온체인 코드입니다. + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity >=0.8.21; + +import {HonkVerifier} from "./Verifier.sol"; + +contract ZkBank { + HonkVerifier immutable myVerifier; + bytes32 currentStateHash; + + constructor(address _verifierAddress, bytes32 _initialStateHash) { + currentStateHash = _initialStateHash; + myVerifier = HonkVerifier(_verifierAddress); + } +``` + +온체인 코드는 검증기(`nargo`에서 만든 별도의 계약)와 현재 상태 해시라는 두 변수를 추적해야 합니다. + +```solidity + event TransactionProcessed( + bytes32 indexed transactionHash, + bytes32 oldStateHash, + bytes32 newStateHash + ); +``` + +상태가 변경될 때마다 `TransactionProcessed` 이벤트를 내보냅니다. + +```solidity + function processTransaction( + bytes calldata _proof, + bytes32[] calldata _publicFields + ) public { +``` + +이 함수는 트랜잭션을 처리합니다. 검증기가 요구하는 형식(온체인 처리 최소화 및 가스 비용 최소화)으로 증명(`bytes`)과 공개 입력(`bytes32` 배열)을 가져옵니다. + +```solidity + require(_publicInputs[0] == currentStateHash, + "Wrong old state hash"); +``` + +영지식 증명은 트랜잭션이 현재 해시에서 새 해시로 변경된다는 것이어야 합니다. + +```solidity + myVerifier.verify(_proof, _publicFields); +``` + +검증자 계약을 호출하여 영지식 증명을 확인합니다. 이 단계는 영지식 증명이 잘못된 경우 트랜잭션을 되돌립니다. + +```solidity + currentStateHash = _publicFields[1]; + + emit TransactionProcessed( + _publicFields[2]<<128 | _publicFields[3], + _publicFields[0], + _publicFields[1] + ); + } +} +``` + +모든 것이 확인되면 상태 해시를 새 값으로 업데이트하고 `TransactionProcessed` 이벤트를 내보냅니다. + +## 중앙화된 구성 요소에 의한 남용 {#abuses} + +정보 보안은 세 가지 속성으로 구성됩니다. + +- _기밀성_, 사용자는 읽을 권한이 없는 정보를 읽을 수 없습니다. +- _무결성_, 정보는 승인된 사용자에 의해 승인된 방식으로만 변경할 수 있습니다. +- _가용성_, 승인된 사용자는 시스템을 사용할 수 있습니다. + +이 시스템에서는 영지식 증명을 통해 무결성이 제공됩니다. 은행이 각 계정의 잔액과 모든 트랜잭션을 알아야 하므로 가용성을 보장하기는 훨씬 더 어렵고 기밀성은 불가능합니다. 정보를 가진 주체가 해당 정보를 공유하는 것을 막을 방법은 없습니다. + +[스텔스 주소](https://vitalik.eth.limo/general/2023/01/20/stealth.html)를 사용하여 진정으로 기밀인 은행을 만들 수 있을지 모르지만, 이는 이 글의 범위를 벗어납니다. + +### 허위 정보 {#false-info} + +서버가 무결성을 위반하는 한 가지 방법은 [데이터가 요청될 때](https://github.com/qbzzt/250911-zk-bank/blob/03-smart-contracts/server/index.mjs#L278-L291) 허위 정보를 제공하는 것입니다. + +이 문제를 해결하기 위해 계정을 비공개 입력으로 받고 정보가 요청된 주소를 공개 입력으로 받는 두 번째 Noir 프로그램을 작성할 수 있습니다. 출력은 해당 주소의 잔액과 논스, 그리고 계정의 해시입니다. + +물론, 논스와 잔액을 온체인에 게시하고 싶지 않기 때문에 이 증명은 온체인에서 확인할 수 없습니다. 그러나 브라우저에서 실행되는 클라이언트 코드로 확인할 수 있습니다. + +### 강제 트랜잭션 {#forced-txns} + +L2에서 가용성을 보장하고 검열을 방지하는 일반적인 메커니즘은 [강제 트랜잭션](https://docs.optimism.io/stack/transactions/forced-transaction)입니다. 그러나 강제 트랜잭션은 영지식 증명과 결합되지 않습니다. 서버는 트랜잭션을 확인할 수 있는 유일한 주체입니다. + +`smart-contracts/src/ZkBank.sol`을 수정하여 강제 트랜잭션을 수락하고 처리될 때까지 서버가 상태를 변경하지 못하도록 할 수 있습니다. 그러나 이것은 우리를 단순한 서비스 거부 공격에 노출시킵니다. 강제 트랜잭션이 유효하지 않아 처리할 수 없다면 어떻게 될까요? + +해결책은 강제 트랜잭션이 유효하지 않다는 영지식 증명을 갖는 것입니다. 이렇게 하면 서버에 세 가지 옵션이 제공됩니다. + +- 강제 트랜잭션을 처리하여 처리되었고 새 상태 해시임을 증명하는 영지식 증명을 제공합니다. +- 강제 트랜잭션을 거부하고 트랜잭션이 유효하지 않음(알 수 없는 주소, 잘못된 논스 또는 잔액 부족)을 계약에 대한 영지식 증명을 제공합니다. +- 강제 트랜잭션을 무시합니다. 서버가 실제로 트랜잭션을 처리하도록 강제할 방법은 없지만, 이는 전체 시스템을 사용할 수 없음을 의미합니다. + +#### 가용성 채권 {#avail-bonds} + +실제 구현에서는 서버를 계속 실행하기 위한某种 이윤 동기가 있을 것입니다. 강제 트랜잭션이 일정 기간 내에 처리되지 않으면 누구나 소각할 수 있는 가용성 채권을 서버가 게시하도록 하여 이 인센티브를 강화할 수 있습니다. + +### 잘못된 Noir 코드 {#bad-noir-code} + +일반적으로 사람들이 스마트 계약을 신뢰하도록 하려면 소스 코드를 [블록 탐색기](https://eth.blockscout.com/address/0x7D16d2c4e96BCFC8f815E15b771aC847EcbDB48b?tab=contract)에 업로드합니다. 그러나 영지식 증명의 경우에는 이것으로 충분하지 않습니다. + +`Verifier.sol`에는 Noir 프로그램의 함수인 검증 키가 포함되어 있습니다. 그러나 그 키는 우리에게 Noir 프로그램이 무엇이었는지 알려주지 않습니다. 실제로 신뢰할 수 있는 솔루션을 가지려면 Noir 프로그램(및 이를 만든 버전)을 업로드해야 합니다. 그렇지 않으면 영지식 증명이 백도어가 있는 다른 프로그램을 반영할 수 있습니다. + +블록 탐색기에서 Noir 프로그램을 업로드하고 확인할 수 있게 될 때까지는 직접 수행해야 합니다(가급적이면 [IPFS](/developers/tutorials/ipfs-decentralized-ui/)에). 그런 다음 정교한 사용자는 소스 코드를 다운로드하고 직접 컴파일하고 `Verifier.sol`을 만들고 온체인에 있는 것과 동일한지 확인할 수 있습니다. + +## 결론 {#conclusion} + +플라즈마 유형 애플리케이션에는 정보 저장 공간으로 중앙화된 구성 요소가 필요합니다. 이는 잠재적인 취약점을 열지만, 그 대가로 블록체인 자체에서는 사용할 수 없는 방식으로 개인정보를 보호할 수 있습니다. 영지식 증명을 사용하면 무결성을 보장하고 중앙화된 구성 요소를 실행하는 사람이 가용성을 유지하는 것이 경제적으로 유리하도록 할 수 있습니다. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). + +## 감사의 말 {#acknowledgements} + +- Josh Crites는 이 글의 초안을 읽고 까다로운 Noir 문제를 해결하는 데 도움을 주었습니다. + +남아있는 오류는 제 책임입니다. diff --git a/public/content/translations/ko/developers/tutorials/calling-a-smart-contract-from-javascript/index.md b/public/content/translations/ko/developers/tutorials/calling-a-smart-contract-from-javascript/index.md new file mode 100644 index 00000000000..f2a4c49126c --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/calling-a-smart-contract-from-javascript/index.md @@ -0,0 +1,131 @@ +--- +title: "자바스크립트에서 스마트 계약 호출하기" +description: "Dai 토큰 예제를 사용하여 자바스크립트에서 스마트 계약 함수를 호출하는 방법" +author: jdourlens +tags: [ "트랜잭션", "프론트엔드", "자바스크립트", "web3.js" ] +skill: beginner +lang: ko +published: 2020-04-19 +source: EthereumDev +sourceUrl: https://ethereumdev.io/calling-a-smart-contract-from-javascript/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +이번 튜토리얼에서는 자바스크립트에서 [스마트 계약](/developers/docs/smart-contracts/) 함수를 호출하는 방법을 알아봅니다. 먼저 스마트 계약의 상태(예: ERC20 보유자의 잔액)를 읽은 다음, 토큰 전송을 통해 블록체인의 상태를 수정합니다. [블록체인과 상호작용하기 위한 JS 환경 설정](/developers/tutorials/set-up-web3js-to-use-ethereum-in-javascript/)에 이미 익숙해야 합니다. + +이 예제에서는 DAI 토큰을 사용합니다. 테스트 목적으로 ganache-cli를 사용하여 블록체인을 포크하고 이미 많은 DAI를 보유한 주소를 잠금 해제(unlock)할 것입니다: + +```bash +ganache-cli -f https://mainnet.infura.io/v3/[YOUR INFURA KEY] -d -i 66 1 --unlock 0x4d10ae710Bd8D1C31bd7465c8CBC3add6F279E81 +``` + +스마트 계약과 상호작용하려면 주소와 ABI가 필요합니다. + +```js +const ERC20TransferABI = [ + { + constant: false, + inputs: [ + { + name: "_to", + type: "address", + }, + { + name: "_value", + type: "uint256", + }, + ], + name: "transfer", + outputs: [ + { + name: "", + type: "bool", + }, + ], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [ + { + name: "_owner", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + name: "balance", + type: "uint256", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, +] + +const DAI_ADDRESS = "0x6b175474e89094c44da98b954eedeac495271d0f" +``` + +이 프로젝트에서는 `balanceOf`와 `transfer` 함수만 유지하기 위해 전체 ERC20 ABI를 간소화했지만, [전체 ERC20 ABI는 여기](https://ethereumdev.io/abi-for-erc20-contract-on-ethereum/)에서 확인할 수 있습니다. + +그런 다음 스마트 계약을 인스턴스화해야 합니다. + +```js +const web3 = new Web3("http://localhost:8545") + +const daiToken = new web3.eth.Contract(ERC20TransferABI, DAI_ADDRESS) +``` + +또한 두 개의 주소를 설정합니다. + +- 전송을 받을 주소와 +- 이미 잠금을 해제했으며 토큰을 보낼 주소: + +```js +const senderAddress = "0x4d10ae710Bd8D1C31bd7465c8CBC3add6F279E81" +const receiverAddress = "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +``` + +다음 파트에서는 `balanceOf` 함수를 호출하여 두 주소가 보유한 현재 토큰 양을 가져옵니다. + +## 호출(Call): 스마트 계약에서 값 읽기 {#call-reading-value-from-a-smart-contract} + +첫 번째 예제에서는 'constant' 메서드를 호출하고 트랜잭션을 전송하지 않고 EVM에서 스마트 계약 메서드를 실행합니다. 이를 위해 주소의 ERC20 잔액을 읽습니다. [ERC20 토큰에 대한 글을 읽어보세요](/developers/tutorials/understand-the-erc-20-token-smart-contract/). + +다음과 같이 ABI를 제공한 인스턴스화된 스마트 계약의 메서드에 접근할 수 있습니다: `yourContract.methods.methodname`. `call` 함수를 사용하면 함수 실행 결과를 받게 됩니다. + +```js +daiToken.methods.balanceOf(senderAddress).call(function (err, res) { + if (err) { + console.log("An error occurred", err) + return + } + console.log("The balance is: ", res) +}) +``` + +DAI ERC20은 소수점 18자리이므로 정확한 금액을 얻으려면 0을 18개 제거해야 합니다. JavaScript는 큰 숫자 값을 처리하지 못하므로 uint256은 문자열로 반환됩니다. JS에서 큰 숫자를 다루는 방법이 확실하지 않다면, [bignumber.js에 대한 저희 튜토리얼을 확인해 보세요](https://ethereumdev.io/how-to-deal-with-big-numbers-in-javascript/). + +## 전송(Send): 스마트 계약 함수에 트랜잭션 보내기 {#send-sending-a-transaction-to-a-smart-contract-function} + +두 번째 예제에서는 DAI 스마트 계약의 전송(transfer) 함수를 호출하여 두 번째 주소로 10 DAI를 보냅니다. 전송(transfer) 함수는 받는 사람 주소와 전송할 토큰 양이라는 두 가지 매개변수를 받습니다. + +```js +daiToken.methods + .transfer(receiverAddress, "100000000000000000000") + .send({ from: senderAddress }, function (err, res) { + if (err) { + console.log("An error occurred", err) + return + } + console.log("Hash of the transaction: " + res) + }) +``` + +call 함수는 블록체인에 채굴될 트랜잭션의 해시를 반환합니다. 이더리움에서 트랜잭션 해시는 예측 가능합니다. 이를 통해 트랜잭션이 실행되기 전에 해시를 얻을 수 있습니다([여기에서 해시가 계산되는 방법 알아보기](https://ethereum.stackexchange.com/questions/45648/how-to-calculate-the-assigned-txhash-of-a-transaction)). + +이 함수는 트랜잭션을 블록체인에 제출만 하기 때문에, 트랜잭션이 채굴되어 블록체인에 포함될 때까지 그 결과를 볼 수 없습니다. 다음 튜토리얼에서는 [해시를 이용해 블록체인에서 트랜잭션이 실행되기를 기다리는 방법](https://ethereumdev.io/waiting-for-a-transaction-to-be-mined-on-ethereum-with-js/)에 대해 알아봅니다. diff --git a/public/content/translations/ko/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md b/public/content/translations/ko/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md new file mode 100644 index 00000000000..2934cecbbc0 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/creating-a-wagmi-ui-for-your-contract/index.md @@ -0,0 +1,585 @@ +--- +title: "계약용 사용자 인터페이스 구축" +description: "TypeScript, React, Vite, Wagmi와 같은 최신 구성 요소를 사용하여 현대적이면서도 최소한의 사용자 인터페이스를 살펴보고, 사용자 인터페이스에 지갑을 연결하고, 스마트 계약을 호출하여 정보를 읽고, 스마트 계약에 트랜잭션을 보내고, 스마트 계약의 이벤트를 모니터링하여 변경 사항을 식별하는 방법을 알아봅니다." +author: Ori Pomerantz +tags: [ "typescript", "react", "vite", "wagmi", "프론트엔드" ] +skill: beginner +published: 2023-11-01 +lang: ko +sidebarDepth: 3 +--- + +이더리움 생태계에 필요한 기능을 발견했습니다. 이를 구현하기 위해 스마트 계약을 작성했고, 어쩌면 오프체인에서 실행되는 관련 코드를 작성했을 수도 있습니다. 훌륭합니다! 안타깝게도 사용자 인터페이스가 없으면 사용자가 없을 것이고, 마지막으로 웹 사이트를 작성했을 때는 사람들이 전화 접속 모뎀을 사용했으며 JavaScript는 새로운 것이었습니다. + +이 글은 바로 당신을 위한 것입니다. 프로그래밍과 JavaScript 및 HTML에 대해 약간 알고 있지만, 사용자 인터페이스 기술은 녹슬고 구식이라고 가정하겠습니다. 요즘에는 어떻게 하는지 볼 수 있도록 간단한 최신 애플리케이션을 함께 살펴보겠습니다. + +## 이것이 왜 중요한가요? {#why-important} + +이론적으로는 사람들이 [Etherscan](https://holesky.etherscan.io/address/0x432d810484add7454ddb3b5311f0ac2e95cecea8#writeContract) 또는 [Blockscout](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=write_contract)을 사용하여 계약과 상호 작용하도록 할 수 있습니다. 경험 많은 이더리안에게는 좋을 것입니다. 하지만 저희는 [또 다른 10억 명의 사람들](https://blog.ethereum.org/2021/05/07/ethereum-for-the-next-billion)에게 서비스를 제공하려고 합니다. 훌륭한 사용자 경험 없이는 이런 일이 일어나지 않을 것이며, 친숙한 사용자 인터페이스가 그 큰 부분을 차지합니다. + +## Greeter 애플리케이션 {#greeter-app} + +최신 UI 작동 방식에 대한 많은 이론이 있으며 [이를 설명하는](https://wagmi.sh/core/getting-started) [많은 좋은 사이트](https://react.dev/learn/thinking-in-react)가 있습니다. 이러한 사이트에서 수행한 훌륭한 작업을 반복하는 대신, 직접 해보면서 배우는 것을 선호하고 직접 다뤄볼 수 있는 애플리케이션으로 시작한다고 가정하겠습니다. 일을 처리하려면 여전히 이론이 필요하며, 저희는 그 이론을 다룰 것입니다. 소스 파일별로 진행하며, 해당 부분에 도달할 때마다 논의할 것입니다. + +### 설치 {#installation} + +1. 필요한 경우, 지갑에 [Holesky 블록체인](https://chainlist.org/?search=holesky&testnets=true)을 추가하고 [테스트용 ETH를 받으세요](https://www.holeskyfaucet.io/). + +2. github 리포지토리를 복제합니다. + + ```sh + git clone https://github.com/qbzzt/20230801-modern-ui.git + ``` + +3. 필요한 패키지를 설치합니다. + + ```sh + cd 20230801-modern-ui + pnpm install + ``` + +4. 애플리케이션을 시작합니다. + + ```sh + pnpm dev + ``` + +5. 애플리케이션에 표시된 URL로 이동합니다. 대부분의 경우 [http://localhost:5173/](http://localhost:5173/)입니다. + +6. Hardhat의 Greeter를 약간 수정한 버전인 계약 소스 코드를 [블록체인 탐색기](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=contract)에서 볼 수 있습니다. + +### 파일 살펴보기 {#file-walk-through} + +#### `index.html` {#index-html} + +이 파일은 스크립트 파일을 가져오는 이 줄을 제외하고는 표준 HTML 상용구입니다. + +```html + +``` + +#### `src/main.tsx` {#main-tsx} + +파일 확장자는 이 파일이 [타입 검사](https://en.wikipedia.org/wiki/Type_system#Type_checking)를 지원하는 JavaScript의 확장인 [TypeScript](https://www.typescriptlang.org/)로 작성된 [React 컴포넌트](https://www.w3schools.com/react/react_components.asp)임을 알려줍니다. TypeScript는 JavaScript로 컴파일되므로 클라이언트 측 실행에 사용할 수 있습니다. + +```tsx +import '@rainbow-me/rainbowkit/styles.css' +import { RainbowKitProvider } from '@rainbow-me/rainbowkit' +import * as React from 'react' +import * as ReactDOM from 'react-dom/client' +import { WagmiConfig } from 'wagmi' +import { chains, config } from './wagmi' +``` + +필요한 라이브러리 코드를 가져옵니다. + +```tsx +import { App } from './App' +``` + +애플리케이션을 구현하는 React 컴포넌트를 가져옵니다(아래 참조). + +```tsx +ReactDOM.createRoot(document.getElementById('root')!).render( +``` + +루트 React 컴포넌트를 생성합니다. `render`의 매개변수는 HTML과 JavaScript/TypeScript를 모두 사용하는 확장 언어인 [JSX](https://www.w3schools.com/react/react_jsx.asp)입니다. 여기서 느낌표는 TypeScript 컴포넌트에 "`document.getElementById('root')`가 `ReactDOM.createRoot`의 유효한 매개변수가 될지 모르지만, 걱정하지 마세요. 제가 개발자이고 그렇게 될 것이라고 말해주고 있습니다"라고 알려줍니다. + +```tsx + +``` + +애플리케이션은 [`React.StrictMode` 컴포넌트](https://react.dev/reference/react/StrictMode) 안에 들어갑니다. 이 컴포넌트는 React 라이브러리에 추가 디버깅 검사를 삽입하도록 지시하며, 이는 개발 중에 유용합니다. + +```tsx + +``` + +애플리케이션은 또한 [`WagmiConfig` 컴포넌트](https://wagmi.sh/react/api/WagmiProvider) 안에 있습니다. [wagmi(we are going to make it) 라이브러리](https://wagmi.sh/)는 React UI 정의를 이더리움 탈중앙화 애플리케이션 작성을 위한 [viem 라이브러리](https://viem.sh/)와 연결합니다. + +```tsx + +``` + +그리고 마지막으로, [`RainbowKitProvider` 컴포넌트](https://www.rainbowkit.com/)입니다. 이 컴포넌트는 로그인과 지갑과 애플리케이션 간의 통신을 처리합니다. + +```tsx + +``` + +이제 실제로 UI를 구현하는 애플리케이션용 컴포넌트를 가질 수 있습니다. 컴포넌트 끝에 있는 `/>`는 XML 표준에 따라 이 컴포넌트 내부에 정의가 없다는 것을 React에 알려줍니다. + +```tsx + + + , +) +``` + +물론, 다른 컴포넌트들도 닫아야 합니다. + +#### `src/App.tsx` {#app-tsx} + +```tsx +import { ConnectButton } from '@rainbow-me/rainbowkit' +import { useAccount } from 'wagmi' +import { Greeter } from './components/Greeter' + +export function App() { +``` + +이것은 React 컴포넌트를 만드는 표준적인 방법입니다. 렌더링이 필요할 때마다 호출되는 함수를 정의하는 것입니다. 이 함수는 일반적으로 상단에 일부 TypeScript 또는 JavaScript 코드가 있고, 그 뒤에 JSX 코드를 반환하는 `return` 문이 따릅니다. + +```tsx + const { isConnected } = useAccount() +``` + +여기서는 [`useAccount`](https://wagmi.sh/react/api/hooks/useAccount)를 사용하여 지갑을 통해 블록체인에 연결되어 있는지 여부를 확인합니다. + +관례적으로, React에서 `use...`라고 불리는 함수는 일종의 데이터를 반환하는 [훅](https://www.w3schools.com/react/react_hooks.asp)입니다. 이러한 훅을 사용하면 컴포넌트가 데이터를 얻을 뿐만 아니라, 해당 데이터가 변경될 때 컴포넌트가 업데이트된 정보로 다시 렌더링됩니다. + +```tsx + return ( + <> +``` + +React 컴포넌트의 JSX는 _반드시_ 하나의 컴포넌트를 반환해야 합니다. 여러 컴포넌트가 있고 "자연스럽게" 묶는 것이 없을 때 빈 컴포넌트(`<> ...` `)를 사용하여 단일 컴포넌트로 만듭니다. + +```tsx +

Greeter

+ +``` + +RainbowKit에서 [`ConnectButton` 컴포넌트](https://www.rainbowkit.com/docs/connect-button)를 가져옵니다. 연결되지 않았을 때, 지갑을 설명하고 사용할 지갑을 선택할 수 있는 모달을 여는 `Connect Wallet` 버튼을 제공합니다. 연결되면, 우리가 사용하는 블록체인, 계정 주소, 그리고 ETH 잔액을 표시합니다. 이 디스플레이를 사용하여 네트워크를 전환하거나 연결을 끊을 수 있습니다. + +```tsx + {isConnected && ( +``` + +실제 JavaScript(또는 JavaScript로 컴파일될 TypeScript)를 JSX에 삽입해야 할 때, 우리는 중괄호(`{}`)를 사용합니다. + +`a && b` 구문은 [`a ?` b : a`](https://www.w3schools.com/react/react_es6_ternary.asp)의 줄임말입니다. 즉, `a`가 참이면 `b`로 평가되고, 그렇지 않으면 `a`(`false`, `0` 등일 수 있음)로 평가됩니다. 이는 특정 조건이 충족될 경우에만 컴포넌트를 표시하도록 React에 알리는 쉬운 방법입니다. + +이 경우, 사용자가 블록체인에 연결된 경우에만 사용자에게 `Greeter`를 표시하고자 합니다. + +```tsx + + )} + + ) +} +``` + +#### `src/components/Greeter.tsx` {#greeter-tsx} + +이 파일에는 대부분의 UI 기능이 포함되어 있습니다. 여기에는 일반적으로 여러 파일에 있을 정의가 포함되어 있지만, 이 프로그램은 튜토리얼이므로 성능이나 유지보수의 용이성보다는 처음 이해하기 쉽도록 최적화되어 있습니다. + +```tsx +import { useState, ChangeEventHandler } from 'react' +import { useNetwork, + useReadContract, + usePrepareContractWrite, + useContractWrite, + useContractEvent + } from 'wagmi' +``` + +이러한 라이브러리 함수를 사용합니다. 다시 말하지만, 이 함수들은 아래에서 사용되는 곳에서 설명됩니다. + +```tsx +import { AddressType } from 'abitype' +``` + +[`abitype` 라이브러리](https://abitype.dev/)는 [`AddressType`](https://abitype.dev/config#addresstype)과 같은 다양한 이더리움 데이터 유형에 대한 TypeScript 정의를 제공합니다. + +```tsx +let greeterABI = [ + . + . + . +] as const // greeterABI +``` + +`Greeter` 계약의 ABI입니다. +계약과 UI를 동시에 개발하는 경우 일반적으로 동일한 리포지토리에 넣고 솔리디티 컴파일러에서 생성된 ABI를 애플리케이션의 파일로 사용합니다. 하지만 계약이 이미 개발되었고 변경되지 않을 것이기 때문에 여기서는 이 작업이 필요하지 않습니다. + +```tsx +type AddressPerBlockchainType = { + [key: number]: AddressType +} +``` + +TypeScript는 강력한 형식의 언어입니다. 이 정의를 사용하여 다른 체인에 배포된 `Greeter` 계약의 주소를 지정합니다. 키는 숫자(chainId)이고 값은 `AddressType`(주소)입니다. + +```tsx +const contractAddrs: AddressPerBlockchainType = { + // Holesky + 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8', + + // Sepolia + 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0' +} +``` + +지원되는 두 네트워크인 [Holesky](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=contact_code)와 [Sepolia](https://eth-sepolia.blockscout.com/address/0x7143d5c190F048C8d19fe325b748b081903E3BF0?tab=contact_code)에서의 계약 주소입니다. + +참고: 실제로는 Redstone Holesky에 대한 세 번째 정의가 있으며, 아래에서 설명할 것입니다. + +```tsx +type ShowObjectAttrsType = { + name: string, + object: any +} +``` + +이 유형은 `ShowObject` 컴포넌트(나중에 설명)의 매개변수로 사용됩니다. 여기에는 객체의 이름과 값이 포함되며, 디버깅 목적으로 표시됩니다. + +```tsx +type ShowGreetingAttrsType = { + greeting: string | undefined +} +``` + +어느 시점에서든 인사말이 무엇인지 알 수도 있고(블록체인에서 읽었기 때문에) 모를 수도 있습니다(아직 받지 못했기 때문에). 따라서 문자열이거나 아무것도 아닐 수 있는 유형을 갖는 것이 유용합니다. + +##### `Greeter` 컴포넌트 {#greeter-component} + +```tsx +const Greeter = () => { +``` + +마지막으로 컴포넌트를 정의합니다. + +```tsx + const { chain } = useNetwork() +``` + +사용 중인 체인에 대한 정보는 [wagmi](https://wagmi.sh/react/hooks/useNetwork)에서 제공합니다. +이것은 훅(`use...`)이므로 이 정보가 변경될 때마다 컴포넌트가 다시 그려집니다. + +```tsx + const greeterAddr = chain && contractAddrs[chain.id] +``` + +Greeter 계약의 주소는 체인에 따라 다르며(체인 정보가 없거나 해당 계약이 없는 체인에 있는 경우 `undefined`임). + +```tsx + const readResults = useReadContract({ + address: greeterAddr, + abi: greeterABI, + functionName: "greet" , // No arguments + watch: true + }) +``` + +[`useReadContract` 훅](https://wagmi.sh/react/api/hooks/useReadContract)은 계약에서 정보를 읽습니다. UI에서 `readResults`를 확장하면 어떤 정보를 반환하는지 정확히 볼 수 있습니다. 이 경우 인사말이 변경될 때 알림을 받을 수 있도록 계속 확인하기를 원합니다. + +**참고:** 인사말이 변경될 때 이를 알기 위해 [`setGreeting` 이벤트](https://eth-holesky.blockscout.com/address/0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8?tab=logs)를 수신하고 그런 식으로 업데이트할 수 있습니다. 하지만 더 효율적일 수는 있지만 모든 경우에 적용되지는 않습니다. 사용자가 다른 체인으로 전환하면 인사말도 변경되지만 해당 변경에는 이벤트가 수반되지 않습니다. 코드의 한 부분은 이벤트를 수신하고 다른 부분은 체인 변경을 식별하도록 할 수 있지만, 이는 [`watch` 매개변수](https://wagmi.sh/react/api/hooks/useReadContract#watch-optional)를 설정하는 것보다 더 복잡합니다. + +```tsx + const [ newGreeting, setNewGreeting ] = useState("") +``` + +React의 [`useState` 훅](https://www.w3schools.com/react/react_usestate.asp)을 사용하면 컴포넌트의 한 렌더링에서 다른 렌더링으로 값이 유지되는 상태 변수를 지정할 수 있습니다. 초기값은 매개변수이며, 이 경우에는 빈 문자열입니다. + +`useState` 훅은 두 가지 값을 가진 목록을 반환합니다. + +1. 상태 변수의 현재 값입니다. +2. 필요할 때 상태 변수를 수정하는 함수입니다. 이것은 훅이므로 호출될 때마다 컴포넌트가 다시 렌더링됩니다. + +이 경우, 사용자가 설정하려는 새 인사말에 대한 상태 변수를 사용하고 있습니다. + +```tsx + const greetingChange : ChangeEventHandler = (evt) => + setNewGreeting(evt.target.value) +``` + +이것은 새 인사말 입력 필드가 변경될 때의 이벤트 핸들러입니다. 유형인 [`ChangeEventHandler`](https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/forms_and_events/)는 이것이 HTML 입력 요소의 값 변경에 대한 핸들러임을 지정합니다. `` 부분은 이것이 [제네릭 유형](https://www.w3schools.com/typescript/typescript_basic_generics.php)이기 때문에 사용됩니다. + +```tsx + const preparedTx = usePrepareContractWrite({ + address: greeterAddr, + abi: greeterABI, + functionName: 'setGreeting', + args: [ newGreeting ] + }) + const workingTx = useContractWrite(preparedTx.config) +``` + +이것은 클라이언트 관점에서 블록체인 트랜잭션을 제출하는 프로세스입니다. + +1. [`eth_estimateGas`](https://docs.alchemy.com/reference/eth-estimategas)를 사용하여 블록체인의 노드에 트랜잭션을 보냅니다. +2. 노드로부터 응답을 기다립니다. +3. 응답을 받으면 사용자에게 지갑을 통해 트랜잭션에 서명하도록 요청합니다. 이 단계는 사용자가 서명하기 전에 트랜잭션의 가스 비용을 보기 때문에 노드 응답을 받은 후에 _반드시_ 발생해야 합니다. +4. 사용자의 승인을 기다립니다. +5. 이번에는 [`eth_sendRawTransaction`](https://docs.alchemy.com/reference/eth-sendrawtransaction)을 사용하여 트랜잭션을 다시 보냅니다. + +2단계는 인지할 수 있는 시간이 걸릴 가능성이 있으며, 이 동안 사용자들은 자신의 명령이 사용자 인터페이스에 의해 실제로 수신되었는지, 왜 아직 트랜잭션에 서명하라는 요청을 받지 못하는지 궁금해할 것입니다. 이는 나쁜 사용자 경험(UX)을 만듭니다. + +해결책은 [준비 훅](https://wagmi.sh/react/prepare-hooks)을 사용하는 것입니다. 매개변수가 변경될 때마다 즉시 노드에 `eth_estimateGas` 요청을 보냅니다. 그러면 사용자가 실제로 트랜잭션을 보내고 싶을 때(이 경우 **인사말 업데이트**를 눌러서), 가스 비용이 알려지고 사용자는 즉시 지갑 페이지를 볼 수 있습니다. + +```tsx + return ( +``` + +이제 드디어 반환할 실제 HTML을 만들 수 있습니다. + +```tsx + <> +

Greeter

+ { + !readResults.isError && !readResults.isLoading && + + } +
+``` + +`ShowGreeting` 컴포넌트(아래 설명)를 만들지만, 인사말이 블록체인에서 성공적으로 읽혔을 경우에만 만듭니다. + +```tsx + +``` + +이것은 사용자가 새 인사말을 설정할 수 있는 입력 텍스트 필드입니다. 사용자가 키를 누를 때마다 `greetingChange`를 호출하고, 이는 `setNewGreeting`을 호출합니다. `setNewGreeting`은 `useState` 훅에서 나오므로 `Greeter` 컴포넌트가 다시 렌더링되게 합니다. 이것은 다음을 의미합니다. + +- 새 인사말의 값을 유지하려면 `value`를 지정해야 합니다. 그렇지 않으면 기본값인 빈 문자열로 돌아가기 때문입니다. +- `usePrepareContractWrite`는 `newGreeting`이 변경될 때마다 호출되므로 준비된 트랜잭션에 항상 최신 `newGreeting`이 포함됩니다. + +```tsx + +``` + +`workingTx.write`가 없으면 인사말 업데이트를 보내는 데 필요한 정보를 아직 기다리고 있는 것이므로 버튼이 비활성화됩니다. `workingTx.write` 값이 있으면 그것이 트랜잭션을 보내기 위해 호출할 함수입니다. + +```tsx +
+ + + + + ) +} +``` + +마지막으로, 우리가 무엇을 하고 있는지 쉽게 볼 수 있도록, 우리가 사용하는 세 가지 객체를 보여줍니다. + +- `readResults` +- `preparedTx` +- `workingTx` + +##### `ShowGreeting` 컴포넌트 {#showgreeting-component} + +이 컴포넌트는 다음을 보여줍니다. + +```tsx +const ShowGreeting = (attrs : ShowGreetingAttrsType) => { +``` + +컴포넌트 함수는 컴포넌트의 모든 속성을 가진 매개변수를 받습니다. + +```tsx + return {attrs.greeting} +} +``` + +##### `ShowObject` 컴포넌트 {#showobject-component} + +정보 제공을 위해, `ShowObject` 컴포넌트를 사용하여 중요한 객체(인사말을 읽기 위한 `readResults`와 우리가 만드는 트랜잭션을 위한 `preparedTx` 및 `workingTx`)를 보여줍니다. + +```tsx +const ShowObject = (attrs: ShowObjectAttrsType ) => { + const keys = Object.keys(attrs.object) + const funs = keys.filter(k => typeof attrs.object[k] == "function") + return <> +
+``` + +UI를 모든 정보로 어지럽히고 싶지 않으므로, 정보를 보거나 닫을 수 있도록 [`details`](https://www.w3schools.com/tags/tag_details.asp) 태그를 사용합니다. + +```tsx + {attrs.name} +
+        {JSON.stringify(attrs.object, null, 2)}
+```
+
+대부분의 필드는 [`JSON.stringify`](https://www.w3schools.com/js/js_json_stringify.asp)를 사용하여 표시됩니다.
+
+```tsx
+      
+ { funs.length > 0 && + <> + Functions: +
    +``` + +예외는 함수인데, 이들은 [JSON 표준](https://www.json.org/json-en.html)의 일부가 아니므로 별도로 표시해야 합니다. + +```tsx + {funs.map((f, i) => +``` + +JSX 내에서 `{` 중괄호 `}` 안의 코드는 JavaScript로 해석됩니다. 그런 다음, `(` 일반 괄호 `)` 안의 코드는 다시 JSX로 해석됩니다. + +```tsx + (
  • {f}
  • ) + )} +``` + +React는 [DOM 트리](https://www.w3schools.com/js/js_htmldom.asp)의 태그에 고유한 식별자가 있어야 합니다. 이는 동일한 태그의 자식(이 경우, [순서 없는 목록](https://www.w3schools.com/tags/tag_ul.asp))은 다른 `key` 속성이 필요하다는 것을 의미합니다. + +```tsx +
+ + } +
+ +} +``` + +다양한 HTML 태그를 끝냅니다. + +##### 최종 `export` {#the-final-export} + +```tsx +export { Greeter } +``` + +`Greeter` 컴포넌트는 애플리케이션에 내보내야 하는 컴포넌트입니다. + +#### `src/wagmi.ts` {#wagmi-ts} + +마지막으로 WAGMI와 관련된 다양한 정의는 `src/wagmi.ts`에 있습니다. 대부분 변경할 필요가 없는 상용구이므로 여기서는 모든 것을 설명하지 않겠습니다. + +여기의 코드는 [github](https://github.com/qbzzt/20230801-modern-ui/blob/main/src/wagmi.ts)에 있는 것과 정확히 같지 않습니다. 왜냐하면 기사 뒷부분에서 다른 체인([Redstone Holesky](https://redstone.xyz/docs/network-info))을 추가하기 때문입니다. + +```ts +import { getDefaultWallets } from '@rainbow-me/rainbowkit' +import { configureChains, createConfig } from 'wagmi' +import { holesky, sepolia } from 'wagmi/chains' +``` + +애플리케이션이 지원하는 블록체인을 가져옵니다. 지원되는 체인 목록은 [viem github](https://github.com/wagmi-dev/viem/tree/main/src/chains/definitions)에서 볼 수 있습니다. + +```ts +import { publicProvider } from 'wagmi/providers/public' + +const walletConnectProjectId = 'c96e690bb92b6311e8e9b2a6a22df575' +``` + +[WalletConnect](https://walletconnect.com/)를 사용하려면 애플리케이션에 대한 프로젝트 ID가 필요합니다. [cloud.walletconnect.com](https://cloud.walletconnect.com/sign-in)에서 얻을 수 있습니다. + +```ts +const { chains, publicClient, webSocketPublicClient } = configureChains( + [ holesky, sepolia ], + [ + publicProvider(), + ], +) + +const { connectors } = getDefaultWallets({ + appName: 'My wagmi + RainbowKit App', + chains, + projectId: walletConnectProjectId, +}) + +export const config = createConfig({ + autoConnect: true, + connectors, + publicClient, + webSocketPublicClient, +}) + +export { chains } +``` + +### 다른 블록체인 추가 {#add-blockchain} + +요즘에는 많은 [L2 확장 솔루션](/layer-2/)이 있으며, viem이 아직 지원하지 않는 일부를 지원하고 싶을 수 있습니다. 이를 위해 `src/wagmi.ts`를 수정합니다. 이 지침은 [Redstone Holesky](https://redstone.xyz/docs/network-info)를 추가하는 방법을 설명합니다. + +1. viem에서 `defineChain` 유형을 가져옵니다. + + ```ts + import { defineChain } from 'viem' + ``` + +2. 네트워크 정의를 추가합니다. + + ```ts + const redstoneHolesky = defineChain({ + id: 17_001, + name: 'Redstone Holesky', + network: 'redstone-holesky', + nativeCurrency: { + decimals: 18, + name: 'Ether', + symbol: 'ETH', + }, + rpcUrls: { + default: { + http: ['https://rpc.holesky.redstone.xyz'], + webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], + }, + public: { + http: ['https://rpc.holesky.redstone.xyz'], + webSocket: ['wss://rpc.holesky.redstone.xyz/ws'], + }, + }, + blockExplorers: { + default: { name: 'Explorer', url: 'https://explorer.holesky.redstone.xyz' }, + }, + }) + ``` + +3. `configureChains` 호출에 새 체인을 추가합니다. + + ```ts + const { chains, publicClient, webSocketPublicClient } = configureChains( + [ holesky, sepolia, redstoneHolesky ], + [ publicProvider(), ], + ) + ``` + +4. 애플리케이션이 새 네트워크에서 계약 주소를 알고 있는지 확인합니다. 이 경우 `src/components/Greeter.tsx`를 수정합니다. + + ```ts + const contractAddrs : AddressPerBlockchainType = { + // Holesky + 17000: '0x432d810484AdD7454ddb3b5311f0Ac2E95CeceA8', + + // Redstone Holesky + 17001: '0x4919517f82a1B89a32392E1BF72ec827ba9986D3', + + // Sepolia + 11155111: '0x7143d5c190F048C8d19fe325b748b081903E3BF0' + } + ``` + +## 결론 {#conclusion} + +물론, `Greeter`를 위한 사용자 인터페이스를 제공하는 데는 그다지 관심이 없을 것입니다. 자신만의 계약을 위한 사용자 인터페이스를 만들고 싶을 것입니다. 자신만의 애플리케이션을 만들려면 다음 단계를 실행하세요. + +1. wagmi 애플리케이션을 생성하도록 지정합니다. + + ```sh copy + pnpm create wagmi + ``` + +2. 애플리케이션의 이름을 지정합니다. + +3. **React** 프레임워크를 선택합니다. + +4. **Vite** 변형을 선택합니다. + +5. [Rainbow kit를 추가](https://www.rainbowkit.com/docs/installation#manual-setup)할 수 있습니다. + +이제 여러분의 계약을 전 세계가 사용할 수 있도록 만들어 보세요. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). + diff --git a/public/content/translations/ko/developers/tutorials/deploying-your-first-smart-contract/index.md b/public/content/translations/ko/developers/tutorials/deploying-your-first-smart-contract/index.md new file mode 100644 index 00000000000..e2e57150593 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/deploying-your-first-smart-contract/index.md @@ -0,0 +1,95 @@ +--- +title: "첫 번째 스마트 계약 배포하기" +description: "이더리움 테스트넷에 첫 스마트 계약 배포하기 소개" +author: "jdourlens" +tags: [ "스마트 계약", "리믹스", "솔리디티", "배포하기" ] +skill: beginner +lang: ko +published: 2020-04-03 +source: EthereumDev +sourceUrl: https://ethereumdev.io/deploying-your-first-smart-contract/ +address: "0x19dE91Af973F404EDF5B4c093983a7c6E3EC8ccE" +--- + +여러분도 저희처럼 이더리움 블록체인에서 첫 [스마트 계약](/developers/docs/smart-contracts/)을 [배포](/developers/docs/smart-contracts/deploying/)하고 상호작용하는 것에 대해 기대가 크실 것으로 생각합니다. + +걱정 마세요. 첫 스마트 계약이니만큼 [로컬 테스트넷](/developers/docs/networks/)에 배포할 것이므로, 배포하고 원하는 만큼 자유롭게 테스트해보는 데 비용이 전혀 들지 않습니다. + +## 계약 작성하기 {#writing-our-contract} + +[Remix에 방문](https://remix.ethereum.org/)하여 새 파일을 만드는 것이 첫 번째 단계입니다. Remix 인터페이스 왼쪽 상단에서 새 파일을 추가하고 원하는 파일 이름을 입력하세요. + +![Remix 인터페이스에서 새 파일 추가하기](./remix.png) + +새 파일에 다음 코드를 붙여넣습니다. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >=0.5.17; + +contract Counter { + + // 카운트 수를 저장하는 부호 없는 정수 유형의 공개 변수 + uint256 public count = 0; + + // 카운터를 증가시키는 함수 + function increment() public { + count += 1; + } + + // 카운트 값을 가져오기 위한 필수적이지 않은 getter + function getCount() public view returns (uint256) { + return count; + } + +} +``` + +프로그래밍에 익숙하다면 이 프로그램이 무엇을 하는지 쉽게 추측할 수 있습니다. 한 줄씩 설명해 보겠습니다. + +- 4번 줄: `Counter`라는 이름의 계약을 정의합니다. +- 7번 줄: 계약은 0에서 시작하는 `count`라는 이름의 부호 없는 정수 하나를 저장합니다. +- 10번 줄: 첫 번째 함수는 계약의 상태를 수정하고 `count` 변수를 `increment()`합니다. +- 15번 줄: 두 번째 함수는 스마트 계약 외부에서 `count` 변수의 값을 읽을 수 있게 해주는 getter일 뿐입니다. 참고로, `count` 변수를 public으로 정의했기 때문에 이 함수는 필수는 아니지만 예시로 보여드립니다. + +첫 번째 간단한 스마트 계약에 대한 설명은 여기까지입니다. 아시다시피, 이것은 Java나 C++와 같은 OOP(객체 지향 프로그래밍) 언어의 클래스와 비슷해 보입니다. 이제 우리 계약을 다뤄볼 시간입니다. + +## 계약 배포하기 {#deploying-our-contract} + +첫 번째 스마트 계약을 작성했으니, 이제 블록체인에 배포하여 테스트해 보겠습니다. + +[블록체인에 스마트 계약을 배포하는 것](/developers/docs/smart-contracts/deploying/)은 사실 수신자를 지정하지 않고 컴파일된 스마트 계약의 코드가 포함된 트랜잭션을 보내는 것일 뿐입니다. + +먼저 왼쪽의 컴파일 아이콘을 클릭하여 [계약을 컴파일합니다](/developers/docs/smart-contracts/compiling/): + +![Remix 툴바의 컴파일 아이콘](./remix-compile-button.png) + +그런 다음 컴파일 버튼을 클릭하세요: + +![Remix Solidity 컴파일러의 컴파일 버튼](./remix-compile.png) + +텍스트 편집기에서 내용을 저장할 때 계약이 항상 컴파일되도록 '자동 컴파일' 옵션을 선택할 수 있습니다. + +그런 다음 'deploy and run transactions' 화면으로 이동합니다: + +![Remix 툴바의 배포 아이콘](./remix-deploy.png) + +'deploy and run transactions' 화면에서 계약 이름이 나타나는지 다시 확인하고 배포(Deploy)를 클릭하세요. 페이지 상단에서 볼 수 있듯이 현재 환경은 'JavaScript VM'입니다. 이는 더 빠르고 수수료 없이 테스트하기 위해 로컬 테스트 블록체인에 스마트 계약을 배포하고 상호작용한다는 의미입니다. + +![Remix Solidity 컴파일러의 배포 버튼](./remix-deploy-button.png) + +'배포(Deploy)' 버튼을 클릭하면 하단에 계약이 표시됩니다. 왼쪽의 화살표를 클릭하여 확장하면 계약 내용을 볼 수 있습니다. 이것은 우리의 `counter` 변수와, `increment()` 함수, 그리고 getter인 `getCounter()`입니다. + +`count` 또는 `getCount` 버튼을 클릭하면 실제로 계약의 `count` 변수 내용을 가져와 표시합니다. 아직 `increment` 함수를 호출하지 않았으므로 0이 표시되어야 합니다. + +![Remix Solidity 컴파일러의 함수 버튼](./remix-function-button.png) + +이제 버튼을 클릭하여 increment 함수를 호출해 보겠습니다. 창 하단에 생성된 트랜잭션의 로그가 나타나는 것을 볼 수 있습니다. `increment` 버튼 대신 데이터를 가져오는 버튼을 누를 때 로그가 다른 것을 볼 수 있습니다. 이는 블록체인에서 데이터를 읽는 데는 트랜잭션(쓰기)이나 수수료가 필요하지 않기 때문입니다. 블록체인의 상태를 수정하는 경우에만 트랜잭션을 생성해야 하기 때문입니다. + +![트랜잭션 로그](./transaction-log.png) + +`increment()` 함수를 호출하는 트랜잭션을 생성하는 increment 버튼을 누른 후, count 또는 getCount 버튼을 다시 클릭하면 count 변수가 0보다 커져 새로 업데이트된 스마트 계약의 상태를 읽게 됩니다. + +![새롭게 업데이트된 스마트 계약 상태](./updated-state.png) + +다음 튜토리얼에서는 [스마트 계약에 이벤트를 추가하는 방법](/developers/tutorials/logging-events-smart-contracts/)에 대해 다룹니다. 이벤트 로깅은 스마트 계약을 디버깅하고 함수를 호출하는 동안 어떤 일이 발생하는지 이해하는 편리한 방법입니다. diff --git a/public/content/translations/ko/developers/tutorials/develop-and-test-dapps-with-a-multi-client-local-eth-testnet/index.md b/public/content/translations/ko/developers/tutorials/develop-and-test-dapps-with-a-multi-client-local-eth-testnet/index.md new file mode 100644 index 00000000000..e155a8e81fc --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/develop-and-test-dapps-with-a-multi-client-local-eth-testnet/index.md @@ -0,0 +1,371 @@ +--- +title: "로컬, 멀티클라이언트 테스트넷에서 dApp을 개발하고 테스트하는 방법" +description: "이 가이드에서는 먼저 다중 클라이언트 로컬 이더리움 테스트넷을 인스턴스화하고 구성하는 방법을 안내한 다음, 테스트넷을 사용하여 dApp을 배포하고 테스트합니다." +author: "Tedi Mitiku" +tags: + [ + "클라이언트", + "노드", + "스마트 계약", + "결합성", + "합의 레이어", + "실행 레이어", + "테스트" + ] +skill: intermediate +lang: ko +published: 2023-04-11 +--- + +## 소개 {#introduction} + +이 가이드는 구성 가능한 로컬 이더리움 테스트넷을 인스턴스화하고, 스마트 계약을 배포하며, 테스트넷을 사용하여 dApp에 대한 테스트를 실행하는 과정을 안내합니다. 이 가이드는 라이브 테스트넷이나 메인넷에 배포하기 전에 다양한 네트워크 구성에 대해 로컬에서 dApp을 개발하고 테스트하려는 dApp 개발자를 위해 설계되었습니다. + +이 가이드에서는 다음을 수행합니다. + +- [Kurtosis](https://www.kurtosis.com/)를 사용하여 [`eth-network-package`](https://github.com/kurtosis-tech/eth-network-package)로 로컬 이더리움 테스트넷을 인스턴스화합니다. +- Hardhat dApp 개발 환경을 로컬 테스트넷에 연결하여 dApp을 컴파일, 배포 및 테스트합니다. +- 노드 수 및 특정 EL/CL 클라이언트 페어링과 같은 파라미터를 포함하여 로컬 테스트넷을 구성하여 다양한 네트워크 구성에 대한 개발 및 테스트 워크플로를 활성화합니다. + +### Kurtosis란 무엇인가요? {#what-is-kurtosis} + +[Kurtosis](https://www.kurtosis.com/)는 다중 컨테이너 테스트 환경 구성을 위해 설계된 구성 가능한 빌드 시스템입니다. 이를 통해 개발자는 블록체인 테스트넷과 같이 동적 설정 로직이 필요한 재현 가능한 환경을 만들 수 있습니다. + +이 가이드에서 Kurtosis eth-network-package는 [`geth`](https://geth.ethereum.org/) 실행 레이어(EL) 클라이언트와 [`teku`](https://consensys.io/teku), [`lighthouse`](https://lighthouse.sigmaprime.io/), [`lodestar`](https://lodestar.chainsafe.io/) 합의 레이어(CL) 클라이언트를 지원하는 로컬 이더리움 테스트넷을 가동합니다. 이 패키지는 Hardhat Network, Ganache, Anvil과 같은 프레임워크의 네트워크에 대한 구성 가능하고 조합 가능한 대안 역할을 합니다. Kurtosis는 개발자가 사용하는 테스트넷에 대한 더 큰 제어와 유연성을 제공하며, 이것이 [이더리움 재단이 머지를 테스트하기 위해 Kurtosis를 사용한](https://www.kurtosis.com/blog/testing-the-ethereum-merge) 주된 이유이며 네트워크 업그레이드를 테스트하기 위해 계속 사용하고 있습니다. + +## Kurtosis 설정 {#setting-up-kurtosis} + +진행하기 전에 다음을 확인하십시오. + +- 로컬 머신에 [Docker 엔진을 설치하고 시작](https://docs.kurtosis.com/install/#i-install--start-docker)했습니다. +- [Kurtosis CLI를 설치](https://docs.kurtosis.com/install#ii-install-the-cli)했습니다(CLI가 이미 설치되어 있는 경우 최신 릴리스로 업그레이드). +- [Node.js](https://nodejs.org/en), [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable), [npx](https://www.npmjs.com/package/npx)를 설치했습니다(dApp 환경용). + +## 로컬 이더리움 테스트넷 인스턴스화 {#instantiate-testnet} + +로컬 이더리움 테스트넷을 가동하려면 다음을 실행하세요. + +```python +kurtosis --enclave local-eth-testnet run github.com/kurtosis-tech/eth-network-package +``` + +참고: 이 명령어는 '--enclave' 플래그를 사용하여 네트워크 이름을 "local-eth-testnet"으로 지정합니다. + +Kurtosis는 지침을 해석, 검증 및 실행하는 동안 내부적으로 수행하는 단계를 출력합니다. 마지막으로 다음과 유사한 출력이 표시되어야 합니다. + +```python +INFO[2023-04-04T18:09:44-04:00] ====================================================== +INFO[2023-04-04T18:09:44-04:00] || Created enclave: local-eth-testnet || +INFO[2023-04-04T18:09:44-04:00] ====================================================== +Name: local-eth-testnet +UUID: 39372d756ae8 +Status: RUNNING +Creation Time: Tue, 04 Apr 2023 18:09:03 EDT + +========================================= Files Artifacts ========================================= +UUID Name +d4085a064230 cl-genesis-data +1c62cb792e4c el-genesis-data +bd60489b73a7 genesis-generation-config-cl +b2e593fe5228 genesis-generation-config-el +d552a54acf78 geth-prefunded-keys +5f7e661eb838 prysm-password +054e7338bb59 validator-keystore-0 + +========================================== User Services ========================================== +UUID Name Ports Status +e20f129ee0c5 cl-client-0-beacon http: 4000/tcp -> RUNNING + metrics: 5054/tcp -> + tcp-discovery: 9000/tcp -> 127.0.0.1:54263 + udp-discovery: 9000/udp -> 127.0.0.1:60470 +a8b6c926cdb4 cl-client-0-validator http: 5042/tcp -> 127.0.0.1:54267 RUNNING + metrics: 5064/tcp -> +d7b802f623e8 el-client-0 engine-rpc: 8551/tcp -> 127.0.0.1:54253 RUNNING + rpc: 8545/tcp -> 127.0.0.1:54251 + tcp-discovery: 30303/tcp -> 127.0.0.1:54254 + udp-discovery: 30303/udp -> 127.0.0.1:53834 + ws: 8546/tcp -> 127.0.0.1:54252 +514a829c0a84 prelaunch-data-generator-1680646157905431468 STOPPED +62bd62d0aa7a prelaunch-data-generator-1680646157915424301 STOPPED +05e9619e0e90 prelaunch-data-generator-1680646157922872635 STOPPED +``` + +축하합니다! Docker를 통해 Kurtosis를 사용하여 CL(`lighthouse`) 및 EL 클라이언트(`geth`)가 있는 로컬 이더리움 테스트넷을 인스턴스화했습니다. + +### 검토 {#review-instantiate-testnet} + +이 섹션에서는 Kurtosis [Enclave](https://docs.kurtosis.com/advanced-concepts/enclaves/) 내에서 로컬 이더리움 테스트넷을 시작하기 위해 Kurtosis가 [GitHub에 원격으로 호스팅된 `eth-network-package`](https://github.com/kurtosis-tech/eth-network-package)를 사용하도록 지시하는 명령을 실행했습니다. 엔클레이브 내부에는 "파일 아티팩트"와 "사용자 서비스"가 모두 있습니다. + +엔클레이브의 [파일 아티팩트](https://docs.kurtosis.com/advanced-concepts/files-artifacts/)에는 EL 및 CL 클라이언트를 부트스트랩하기 위해 생성 및 활용된 모든 데이터가 포함됩니다. 데이터는 이 [Docker 이미지](https://github.com/ethpandaops/ethereum-genesis-generator)에서 빌드된 `prelaunch-data-generator` 서비스를 사용하여 생성되었습니다. + +사용자 서비스는 엔클레이브에서 작동하는 모든 컨테이너화된 서비스를 표시합니다. EL 클라이언트와 CL 클라이언트를 모두 갖춘 단일 노드가 생성된 것을 알 수 있습니다. + +## dApp 개발 환경을 로컬 이더리움 테스트넷에 연결 {#connect-your-dapp} + +### dApp 개발 환경 설정 {#set-up-dapp-env} + +이제 실행 중인 로컬 테스트넷이 있으므로 dApp 개발 환경을 연결하여 로컬 테스트넷을 사용할 수 있습니다. 이 가이드에서는 Hardhat 프레임워크를 사용하여 블랙잭 dApp을 로컬 테스트넷에 배포합니다. + +dApp 개발 환경을 설정하려면 샘플 dApp이 포함된 저장소를 클론하고 종속성을 설치한 후 다음을 실행하세요. + +```python +git clone https://github.com/kurtosis-tech/awesome-kurtosis.git && cd awesome-kurtosis/smart-contract-example && yarn +``` + +여기서 사용되는 [smart-contract-example](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example) 폴더에는 [Hardhat](https://hardhat.org/) 프레임워크를 사용하는 dApp 개발자를 위한 일반적인 설정이 포함되어 있습니다. + +- [`contracts/`](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example/contracts)에는 Blackjack dApp을 위한 몇 가지 간단한 스마트 계약이 포함되어 있습니다. +- [`scripts/`](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example/scripts)에는 로컬 이더리움 네트워크에 토큰 계약을 배포하는 스크립트가 포함되어 있습니다. +- [`test/`](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/smart-contract-example/test)에는 토큰 계약에 대한 간단한 .js 테스트가 포함되어 있으며, Blackjack dApp의 각 플레이어에게 1000개가 민팅되었는지 확인합니다. +- [`hardhat.config.ts`](https://github.com/kurtosis-tech/awesome-kurtosis/blob/main/smart-contract-example/hardhat.config.ts)는 Hardhat 설정을 구성합니다. + +### 로컬 테스트넷을 사용하도록 Hardhat 구성 {#configure-hardhat} + +dApp 개발 환경이 설정되었으므로 이제 Kurtosis를 사용하여 생성된 로컬 이더리움 테스트넷을 사용하도록 Hardhat을 연결합니다. 이를 위해 `hardhat.config.ts` 구성 파일의 `localnet` 구조체에서 `<$YOUR_PORT>`를 `el-client-` 서비스의 rpc uri 출력 포트로 바꿉니다. 이 샘플의 경우 포트는 `64248`입니다. 사용자의 포트는 다를 것입니다. + +`hardhat.config.ts`의 예시: + +```js +localnet: { +url: 'http://127.0.0.1:<$YOUR_PORT>',// TODO: $YOUR_PORT를 ETH 네트워크 KURTOSIS 패키지에서 생성한 노드 URI의 포트로 교체하세요 + +// 이들은 eth-network-package에 의해 생성된, 사전에 자금이 충전된 테스트 계정과 연관된 개인 키입니다 +// +accounts: [ + "ef5177cd0b6b21c87db5a0bf35d4084a8a57a9d6a064f86d51ac85f2b873a4e2", + "48fcc39ae27a0e8bf0274021ae6ebd8fe4a0e12623d61464c498900b28feb567", + "7988b3a148716ff800414935b305436493e1f25237a2a03e5eebc343735e2f31", + "b3c409b6b0b3aa5e65ab2dc1930534608239a478106acf6f3d9178e9f9b00b35", + "df9bb6de5d3dc59595bcaa676397d837ff49441d211878c024eabda2cd067c9f", + "7da08f856b5956d40a72968f93396f6acff17193f013e8053f6fbb6c08c194d6", + ], +}, +``` + +파일을 저장하면 Hardhat dApp 개발 환경이 로컬 이더리움 테스트넷에 연결됩니다! 다음을 실행하여 테스트넷이 작동하는지 확인할 수 있습니다. + +```python +npx hardhat balances --network localnet +``` + +출력은 다음과 같아야 합니다. + +```python +0x878705ba3f8Bc32FCf7F4CAa1A35E72AF65CF766 has balance 10000000000000000000000000 +0x4E9A3d9D1cd2A2b2371b8b3F489aE72259886f1A has balance 10000000000000000000000000 +0xdF8466f277964Bb7a0FFD819403302C34DCD530A has balance 10000000000000000000000000 +0x5c613e39Fc0Ad91AfDA24587e6f52192d75FBA50 has balance 10000000000000000000000000 +0x375ae6107f8cC4cF34842B71C6F746a362Ad8EAc has balance 10000000000000000000000000 +0x1F6298457C5d76270325B724Da5d1953923a6B88 has balance 10000000000000000000000000 +``` + +이는 Hardhat이 로컬 테스트넷을 사용하고 있으며 `eth-network-package`에 의해 생성된 사전에 자금이 충전된 계정을 감지했음을 확인합니다. + +### 로컬에서 dApp 배포 및 테스트 {#deploy-and-test-dapp} + +dApp 개발 환경이 로컬 이더리움 테스트넷에 완전히 연결되었으므로 이제 로컬 테스트넷을 사용하여 dApp에 대한 개발 및 테스트 워크플로를 실행할 수 있습니다. + +`ChipToken.sol` 스마트 계약을 로컬 프로토타이핑 및 개발을 위해 컴파일하고 배포하려면 다음을 실행하세요. + +```python +npx hardhat compile +npx hardhat run scripts/deploy.ts --network localnet +``` + +출력은 다음과 같아야 합니다. + +```python +ChipToken이 0xAb2A01BC351770D09611Ac80f1DE076D56E0487d에 배포되었습니다 +``` + +이제 로컬 dApp에 대해 `simple.js` 테스트를 실행하여 Blackjack dApp의 각 플레이어에게 1000개가 민팅되었는지 확인하세요. + +출력은 다음과 같아야 합니다. + +```python +npx hardhat test --network localnet +``` + +출력은 다음과 같아야 합니다. + +```python +ChipToken + mint + ✔ should mint 1000 chips for PLAYER ONE + + 1 passing (654ms) +``` + +### 검토 {#review-dapp-workflows} + +이 시점에서 dApp 개발 환경을 설정하고, Kurtosis가 생성한 로컬 이더리움 네트워크에 연결했으며, dApp에 대해 컴파일, 배포 및 간단한 테스트를 실행했습니다. + +이제 다양한 네트워크 구성에서 dApp을 테스트하기 위해 기본 네트워크를 구성하는 방법을 살펴보겠습니다. + +## 로컬 이더리움 테스트넷 구성 {#configure-testnet} + +### 클라이언트 구성 및 노드 수 변경 {#configure-client-config-and-num-nodes} + +로컬 이더리움 테스트넷은 개발 또는 테스트하려는 시나리오 및 특정 네트워크 구성에 따라 다양한 EL 및 CL 클라이언트 쌍과 다양한 수의 노드를 사용하도록 구성할 수 있습니다. 즉, 일단 설정되면 맞춤형 로컬 테스트넷을 가동하고 이를 사용하여 동일한 워크플로(배포, 테스트 등)를 실행할 수 있습니다. 다양한 네트워크 구성 하에서 모든 것이 예상대로 작동하는지 확인합니다. 수정할 수 있는 다른 파라미터에 대해 자세히 알아보려면 이 링크를 방문하세요. + +시도해 보세요! JSON 파일을 통해 `eth-network-package`에 다양한 구성 옵션을 전달할 수 있습니다. 이 네트워크 파라미터 JSON 파일은 Kurtosis가 로컬 이더리움 네트워크를 설정하는 데 사용할 특정 구성을 제공합니다. + +기본 구성 파일을 사용하여 다른 EL/CL 쌍을 가진 두 개의 노드를 시작하도록 편집하세요. + +- 노드 1: `geth`/`lighthouse` +- 노드 2: `geth`/`lodestar` +- 노드 3: `geth`/`teku` + +이 구성은 dApp 테스트를 위해 이더리움 노드 구현의 이기종 네트워크를 생성합니다. 이제 구성 파일은 다음과 같이 표시됩니다. + +```yaml +{ + "participants": + [ + { + "el_client_type": "geth", + "el_client_image": "", + "el_client_log_level": "", + "cl_client_type": "lighthouse", + "cl_client_image": "", + "cl_client_log_level": "", + "beacon_extra_params": [], + "el_extra_params": [], + "validator_extra_params": [], + "builder_network_params": null, + }, + { + "el_client_type": "geth", + "el_client_image": "", + "el_client_log_level": "", + "cl_client_type": "lodestar", + "cl_client_image": "", + "cl_client_log_level": "", + "beacon_extra_params": [], + "el_extra_params": [], + "validator_extra_params": [], + "builder_network_params": null, + }, + { + "el_client_type": "geth", + "el_client_image": "", + "el_client_log_level": "", + "cl_client_type": "teku", + "cl_client_image": "", + "cl_client_log_level": "", + "beacon_extra_params": [], + "el_extra_params": [], + "validator_extra_params": [], + "builder_network_params": null, + }, + ], + "network_params": + { + "preregistered_validator_keys_mnemonic": "giant issue aisle success illegal bike spike question tent bar rely arctic volcano long crawl hungry vocal artwork sniff fantasy very lucky have athlete", + "num_validator_keys_per_node": 64, + "network_id": "3151908", + "deposit_contract_address": "0x4242424242424242424242424242424242424242", + "seconds_per_slot": 12, + "genesis_delay": 120, + "capella_fork_epoch": 5, + }, +} +``` + +각 `participants` 구조체는 네트워크의 노드에 매핑되므로 3개의 `participants` 구조체는 Kurtosis에게 네트워크에 3개의 노드를 가동하도록 지시합니다. 각 `participants` 구조체를 통해 특정 노드에 사용되는 EL 및 CL 쌍을 지정할 수 있습니다. + +`network_params` 구조체는 각 노드의 제네시스 파일을 생성하는 데 사용되는 네트워크 설정과 네트워크의 슬롯당 시간(초)과 같은 기타 설정을 구성합니다. + +편집한 파라미터 파일을 원하는 디렉터리에 저장하고(아래 예에서는 바탕화면에 저장됨) 다음을 실행하여 Kurtosis 패키지를 실행하는 데 사용합니다. + +```python +kurtosis clean -a && kurtosis run --enclave local-eth-testnet github.com/kurtosis-tech/eth-network-package "$(cat ~/eth-network-params.json)" +``` + +참고: `kurtosis clean -a` 명령어는 Kurtosis에게 새 테스트넷을 시작하기 전에 이전 테스트넷과 그 내용을 파괴하도록 지시하는 데 사용됩니다. + +다시 한 번, Kurtosis가 잠시 작동하고 진행 중인 개별 단계를 출력합니다. 결과적으로 출력은 다음과 같아야 합니다. + +```python +Starlark code successfully run. No output was returned. +INFO[2023-04-07T11:43:16-04:00] ========================================================== +INFO[2023-04-07T11:43:16-04:00] || Created enclave: local-eth-testnet || +INFO[2023-04-07T11:43:16-04:00] ========================================================== +Name: local-eth-testnet +UUID: bef8c192008e +Status: RUNNING +Creation Time: Fri, 07 Apr 2023 11:41:58 EDT + +========================================= Files Artifacts ========================================= +UUID Name +cc495a8e364a cl-genesis-data +7033fcdb5471 el-genesis-data +a3aef43fc738 genesis-generation-config-cl +8e968005fc9d genesis-generation-config-el +3182cca9d3cd geth-prefunded-keys +8421166e234f prysm-password +d9e6e8d44d99 validator-keystore-0 +23f5ba517394 validator-keystore-1 +4d28dea40b5c validator-keystore-2 + +========================================== User Services ========================================== +UUID Name Ports Status +485e6fde55ae cl-client-0-beacon http: 4000/tcp -> http://127.0.0.1:65010 RUNNING + metrics: 5054/tcp -> http://127.0.0.1:65011 + tcp-discovery: 9000/tcp -> 127.0.0.1:65012 + udp-discovery: 9000/udp -> 127.0.0.1:54455 +73739bd158b2 cl-client-0-validator http: 5042/tcp -> 127.0.0.1:65016 RUNNING + metrics: 5064/tcp -> http://127.0.0.1:65017 +1b0a233cd011 cl-client-1-beacon http: 4000/tcp -> 127.0.0.1:65021 RUNNING + metrics: 8008/tcp -> 127.0.0.1:65023 + tcp-discovery: 9000/tcp -> 127.0.0.1:65024 + udp-discovery: 9000/udp -> 127.0.0.1:56031 + validator-metrics: 5064/tcp -> 127.0.0.1:65022 +949b8220cd53 cl-client-1-validator http: 4000/tcp -> 127.0.0.1:65028 RUNNING + metrics: 8008/tcp -> 127.0.0.1:65030 + tcp-discovery: 9000/tcp -> 127.0.0.1:65031 + udp-discovery: 9000/udp -> 127.0.0.1:60784 + validator-metrics: 5064/tcp -> 127.0.0.1:65029 +c34417bea5fa cl-client-2 http: 4000/tcp -> 127.0.0.1:65037 RUNNING + metrics: 8008/tcp -> 127.0.0.1:65035 + tcp-discovery: 9000/tcp -> 127.0.0.1:65036 + udp-discovery: 9000/udp -> 127.0.0.1:63581 +e19738e6329d el-client-0 engine-rpc: 8551/tcp -> 127.0.0.1:64986 RUNNING + rpc: 8545/tcp -> 127.0.0.1:64988 + tcp-discovery: 30303/tcp -> 127.0.0.1:64987 + udp-discovery: 30303/udp -> 127.0.0.1:55706 + ws: 8546/tcp -> 127.0.0.1:64989 +e904687449d9 el-client-1 engine-rpc: 8551/tcp -> 127.0.0.1:64993 RUNNING + rpc: 8545/tcp -> 127.0.0.1:64995 + tcp-discovery: 30303/tcp -> 127.0.0.1:64994 + udp-discovery: 30303/udp -> 127.0.0.1:58096 + ws: 8546/tcp -> 127.0.0.1:64996 +ad6f401126fa el-client-2 engine-rpc: 8551/tcp -> 127.0.0.1:65003 RUNNING + rpc: 8545/tcp -> 127.0.0.1:65001 + tcp-discovery: 30303/tcp -> 127.0.0.1:65000 + udp-discovery: 30303/udp -> 127.0.0.1:57269 + ws: 8546/tcp -> 127.0.0.1:65002 +12d04a9dbb69 prelaunch-data-generator-1680882122181135513 STOPPED +5b45f9c0504b prelaunch-data-generator-1680882122192182847 STOPPED +3d4aaa75e218 prelaunch-data-generator-1680882122201668972 STOPPED +``` + +축하합니다! 로컬 테스트넷을 1개가 아닌 3개의 노드를 갖도록 성공적으로 구성했습니다. dApp에 대해 이전과 동일한 워크플로(배포 및 테스트)를 실행하려면 `hardhat.config.ts` 구성 파일의 `localnet` 구조체에서 `<$YOUR_PORT>`를 새 3노드 로컬 테스트넷의 `el-client-` 서비스에서 출력된 rpc uri의 포트로 바꾸어 이전과 동일한 작업을 수행하세요. + +## 결론 {#conclusion} + +이것으로 끝입니다! 이 짧은 가이드를 요약하자면 다음과 같습니다. + +- Kurtosis를 사용하여 Docker를 통해 로컬 이더리움 테스트넷을 생성했습니다. +- 로컬 dApp 개발 환경을 로컬 이더리움 네트워크에 연결했습니다. +- 로컬 이더리움 네트워크에서 dApp을 배포하고 간단한 테스트를 실행했습니다. +- 기본 이더리움 네트워크를 3개의 노드를 갖도록 구성했습니다. + +잘된 점, 개선할 점 또는 질문에 대한 답변에 대해 여러분의 의견을 듣고 싶습니다. 주저하지 마시고 [GitHub](https://github.com/kurtosis-tech/kurtosis/issues/new/choose)를 통해 연락하시거나 [이메일을 보내주세요](mailto:feedback@kurtosistech.com)! + +### 기타 예제 및 가이드 {#other-examples-guides} + +[빠른 시작](https://docs.kurtosis.com/quickstart)(Postgres 데이터베이스와 API를 구축)과 [awesome-kurtosis 저장소](https://github.com/kurtosis-tech/awesome-kurtosis)의 다른 예제를 확인해 보시기 바랍니다. 여기에는 다음 패키지를 포함한 훌륭한 예제가 있습니다. + +- 동일한 로컬 이더리움 테스트넷을 가동하지만 트랜잭션 스패머(트랜잭션 시뮬레이션용), 포크 모니터, 연결된 Grafana 및 Prometheus 인스턴스와 같은 추가 서비스가 연결되어 있습니다. +- 동일한 로컬 이더리움 네트워크에 대한 [하위 네트워킹 테스트](https://github.com/kurtosis-tech/awesome-kurtosis/tree/main/ethereum-network-partition-test) 수행 diff --git a/public/content/translations/ko/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/index.md b/public/content/translations/ko/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/index.md new file mode 100644 index 00000000000..890ba1dcdef --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/downsizing-contracts-to-fight-the-contract-size-limit/index.md @@ -0,0 +1,144 @@ +--- +title: "컨트랙트 크기 제한에 맞서 컨트랙트 축소하기" +description: "스마트 계약이 너무 커지는 것을 방지하기 위해 무엇을 할 수 있을까요?" +author: Markus Waas +lang: ko +tags: [ "솔리디티", "스마트 계약", "저장 공간" ] +skill: intermediate +published: 2020-06-26 +source: soliditydeveloper.com +sourceUrl: https://soliditydeveloper.com/max-contract-size +--- + +## 왜 크기 제한이 있나요? {#why-is-there-a-limit} + +[2016년 11월 22일](https://blog.ethereum.org/2016/11/18/hard-fork-no-4-spurious-dragon/)에 스퓨리어스 드래곤(Spurious Dragon) 하드포크는 24.576kb의 스마트 계약 크기 제한을 추가한 [EIP-170](https://eips.ethereum.org/EIPS/eip-170)을 도입했습니다. 솔리디티 개발자로서 컨트랙트에 기능을 점점 더 많이 추가하면 어느 시점에 한계에 도달하게 되며, 배포 시 다음과 같은 오류가 표시됩니다: + +`경고: 컨트랙트 코드 크기가 24576바이트를 초과합니다(스퓨리어스 드래곤(Spurious Dragon)에서 도입된 제한). 이 컨트랙트는 메인넷에 배포되지 않을 수 있습니다. 옵티마이저를 활성화하거나('runs' 값을 낮게 설정!), revert 문자열을 끄거나, 라이브러리를 사용하는 것을 고려해 보세요.` + +이 제한은 서비스 거부(DOS) 공격을 방지하기 위해 도입되었습니다. 컨트랙트에 대한 모든 호출은 가스 측면에서 비교적 저렴합니다. 하지만 이더리움 노드에 대한 컨트랙트 호출의 영향은 호출된 컨트랙트 코드의 크기(디스크에서 코드 읽기, 코드 사전 처리, 머클 증명에 데이터 추가)에 따라 불균형적으로 증가합니다. 공격자가 적은 리소스를 사용하여 다른 사람에게 많은 작업을 유발할 수 있는 상황이 발생하면 DOS 공격의 가능성이 생깁니다. + +원래 블록 가스 한도가 자연스러운 컨트랙트 크기 제한 역할을 했기 때문에 이는 큰 문제가 되지 않았습니다. 당연히, 컨트랙트는 해당 컨트랙트의 모든 바이트코드를 포함하는 트랜잭션 내에서 배포되어야 합니다. 하나의 블록에 해당 트랜잭션 하나만 포함시키면 모든 가스를 사용할 수 있지만, 이는 무한하지 않습니다. [런던 업그레이드](/ethereum-forks/#london) 이후, 블록 가스 한도는 네트워크 수요에 따라 1,500만에서 3,000만 단위 사이에서 변동할 수 있게 되었습니다. + +다음에서는 잠재적 영향력 순으로 몇 가지 방법을 살펴보겠습니다. 체중 감량의 관점에서 생각해 보세요. 목표 체중(이 경우 24kb)에 도달하기 위한 최선의 전략은 영향이 큰 방법에 먼저 집중하는 것입니다. 대부분의 경우 식단을 조절하는 것만으로도 목표에 도달할 수 있지만, 때로는 그 이상이 필요합니다. 그 다음에는 운동(중간 영향)이나 보충제(작은 영향)를 추가할 수도 있습니다. + +## 큰 영향 {#big-impact} + +### 컨트랙트 분리하기 {#separate-your-contracts} + +이 방법이 항상 첫 번째 접근 방식이 되어야 합니다. 컨트랙트를 여러 개의 작은 컨트랙트로 어떻게 분리할 수 있을까요? 일반적으로 이 과정을 통해 컨트랙트에 대한 좋은 아키텍처를 구상하게 됩니다. 코드 가독성 측면에서는 항상 더 작은 컨트랙트가 선호됩니다. 컨트랙트를 분리하려면 다음을 자문해 보세요: + +- 어떤 함수들이 함께 속해 있나요? 각 함수 집합은 자체 컨트랙트에 두는 것이 가장 좋을 수 있습니다. +- 어떤 함수가 컨트랙트 상태를 읽을 필요가 없거나 상태의 특정 하위 집합만 필요로 하나요? +- 저장 공간과 기능을 분리할 수 있나요? + +### 라이브러리 {#libraries} + +저장 공간에서 기능 코드를 분리하는 간단한 방법 중 하나는 [라이브러리](https://solidity.readthedocs.io/en/v0.6.10/contracts.html#libraries)를 사용하는 것입니다. `internal`로 선언하지 마세요. 컴파일 중에 [컨트랙트에 직접 추가](https://ethereum.stackexchange.com/questions/12975/are-internal-functions-in-libraries-not-covered-by-linking)되기 때문입니다. 하지만 `public` 함수를 사용하면 실제로는 별도의 라이브러리 컨트랙트에 있게 됩니다. 라이브러리를 더 편리하게 사용하려면 [`using for`](https://solidity.readthedocs.io/en/v0.6.10/contracts.html#using-for) 사용을 고려해 보세요. + +### 프록시 {#proxies} + +더 발전된 전략은 프록시 시스템을 사용하는 것입니다. 라이브러리는 내부적으로 `DELEGATECALL`을 사용하는데, 이는 호출하는 컨트랙트의 상태로 다른 컨트랙트의 함수를 단순히 실행합니다. 프록시 시스템에 대해 자세히 알아보려면 [이 블로그 게시물](https://hackernoon.com/how-to-make-smart-contracts-upgradable-2612e771d5a2)을 확인하세요. 프록시는 업그레이드 가능성과 같은 더 많은 기능을 제공하지만, 복잡성도 크게 증가시킵니다. 어떤 이유로든 유일한 옵션이 아닌 이상, 단지 컨트랙트 크기를 줄이기 위해 프록시를 추가하는 것은 권장하지 않습니다. + +## 중간 영향 {#medium-impact} + +### 함수 제거하기 {#remove-functions} + +이 방법은 당연하게 들릴 수 있습니다. 함수는 컨트랙트 크기를 상당히 증가시킵니다. + +- **External**: 편의를 위해 `view` 함수를 많이 추가하는 경우가 많습니다. 크기 제한에 도달하기 전까지는 괜찮습니다. 그때는 정말 필수적인 함수를 제외하고 모두 제거하는 것을 고려해야 합니다. +- **Internal**: 함수가 한 번만 호출되는 경우 `internal`/`private` 함수를 제거하고 코드를 인라인으로 처리할 수도 있습니다. + +### 추가 변수 사용 피하기 {#avoid-additional-variables} + +```solidity +function get(uint id) returns (address,address) { + MyStruct memory myStruct = myStructs[id]; + return (myStruct.addr1, myStruct.addr2); +} +``` + +```solidity +function get(uint id) returns (address,address) { + return (myStructs[id].addr1, myStructs[id].addr2); +} +``` + +이처럼 간단한 변경만으로도 **0.28kb**의 차이를 만들 수 있습니다. 컨트랙트에서 이와 유사한 상황을 많이 찾을 수 있으며, 이것들이 모이면 상당한 크기를 줄일 수 있습니다. + +### 오류 메시지 줄이기 {#shorten-error-message} + +긴 revert 메시지, 특히 다양한 revert 메시지는 컨트랙트 크기를 부풀릴 수 있습니다. 대신 짧은 오류 코드를 사용하고 컨트랙트에서 디코딩하세요. 긴 메시지를 다음과 같이 훨씬 짧게 만들 수 있습니다. + +```solidity +require(msg.sender == owner, "이 컨트랙트의 소유자만 이 함수를 호출할 수 있습니다"); +``` + +```solidity +require(msg.sender == owner, "OW1"); +``` + +### 오류 메시지 대신 사용자 정의 오류 사용하기 + +사용자 정의 오류는 [솔리디티 0.8.4](https://blog.soliditylang.org/2021/04/21/custom-errors/)에서 도입되었습니다. 사용자 정의 오류는 함수처럼 선택자로 ABI 인코딩되므로 컨트랙트의 크기를 줄이는 좋은 방법입니다. + +```solidity +error Unauthorized(); + +if (msg.sender != owner) { + revert Unauthorized(); +} +``` + +### 옵티마이저에서 낮은 실행(run) 값 고려하기 {#consider-a-low-run-value-in-the-optimizer} + +옵티마이저 설정을 변경할 수도 있습니다. 기본값인 200은 함수가 200번 호출되는 것처럼 바이트코드를 최적화한다는 의미입니다. 이 값을 1로 변경하면 기본적으로 옵티마이저에게 각 함수를 한 번만 실행하는 경우에 최적화하도록 지시하는 것입니다. 한 번만 실행하도록 최적화된 함수는 배포 자체에 최적화되었음을 의미합니다. **함수를 실행하는 데 드는 [가스 비용](/developers/docs/gas/)이 증가**하므로 이 방법을 사용하지 않는 것이 좋을 수 있습니다. + +## 작은 영향 {#small-impact} + +### 함수에 구조체 전달 피하기 {#avoid-passing-structs-to-functions} + +[ABIEncoderV2](https://solidity.readthedocs.io/en/v0.6.10/layout-of-source-files.html#abiencoderv2)를 사용하고 있다면, 함수에 구조체를 전달하지 않는 것이 도움이 될 수 있습니다. 매개변수를 구조체로 전달하는 대신, 필요한 매개변수를 직접 전달하세요. 이 예시에서는 **0.1kb**를 추가로 절약했습니다. + +```solidity +function get(uint id) returns (address,address) { + return _get(myStruct); +} + +function _get(MyStruct memory myStruct) private view returns(address,address) { + return (myStruct.addr1, myStruct.addr2); +} +``` + +```solidity +function get(uint id) returns(address,address) { + return _get(myStructs[id].addr1, myStructs[id].addr2); +} + +function _get(address addr1, address addr2) private view returns(address,address) { + return (addr1, addr2); +} +``` + +### 함수와 변수에 올바른 가시성 선언하기 {#declare-correct-visibility-for-functions-and-variables} + +- 외부에서만 호출되는 함수나 변수인가요? `public` 대신 `external`로 선언하세요. +- 컨트랙트 내에서만 호출되는 함수나 변수인가요? `public` 대신 `private` 또는 `internal`로 선언하세요. + +### 수식어(modifier) 제거하기 {#remove-modifiers} + +수식어(Modifier)는 특히 많이 사용될 경우 컨트랙트 크기에 상당한 영향을 미칠 수 있습니다. 수식어를 제거하고 대신 함수를 사용하는 것을 고려해 보세요. + +```solidity +modifier checkStuff() {} + +function doSomething() checkStuff {} +``` + +```solidity +function checkStuff() private {} + +function doSomething() { checkStuff(); } +``` + +이 팁들은 컨트랙트 크기를 크게 줄이는 데 도움이 될 것입니다. 다시 한번 강조하지만, 가장 큰 영향을 위해 가능하다면 항상 컨트랙트를 분리하는 데 집중하세요. diff --git a/public/content/translations/ko/developers/tutorials/eip-1271-smart-contract-signatures/index.md b/public/content/translations/ko/developers/tutorials/eip-1271-smart-contract-signatures/index.md new file mode 100644 index 00000000000..b405476b9bc --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/eip-1271-smart-contract-signatures/index.md @@ -0,0 +1,101 @@ +--- +title: "EIP-1271: 스마트 계약 서명 및 확인" +description: "EIP-1271을 사용한 스마트 계약 서명 생성 및 확인에 대한 개요입니다. 또한 스마트 계약 개발자가 참고할 수 있는 구체적인 예시를 제공하기 위해 Safe(이전 Gnosis Safe)에서 사용되는 EIP-1271 구현을 살펴봅니다." +author: Nathan H. Leung +lang: ko +tags: [ "eip-1271", "스마트 컨트랙트", "확인", "서명" ] +skill: intermediate +published: 2023-01-12 +--- + +[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271) 표준을 사용하면 스마트 계약이 서명을 확인할 수 있습니다. + +이 튜토리얼에서는 전자 서명, EIP-1271의 배경 및 [Safe](https://safe.global/)(이전 Gnosis Safe)에서 사용하는 EIP-1271의 특정 구현에 대해 간략히 설명합니다. 이 모든 내용은 여러분의 계약에 EIP-1271을 구현하기 위한 시작점이 될 수 있습니다. + +## 서명이란 무엇인가요? + +이 맥락에서 서명(보다 정확하게는 '전자 서명')은 메시지와 해당 메시지가 특정 개인/보낸 사람/주소로부터 왔다는 일종의 증명을 더한 것입니다. + +예를 들어, 전자 서명은 다음과 같을 수 있습니다. + +1. 메시지: '내 이더리움 지갑으로 이 웹사이트에 로그인하고 싶습니다.' +2. 서명자: 내 주소는 `0x000…`입니다. +3. 증명: 여기 `0x000…`인 제가 실제로 이 전체 메시지를 생성했다는 증거가 있습니다(이것은 일반적으로 암호화된 것입니다). + +전자 서명에는 '메시지'와 '서명'이 모두 포함된다는 점에 유의해야 합니다. + +왜 그럴까요? 예를 들어, 당신이 저에게 서명할 계약서를 주었는데, 제가 서명 페이지만 잘라내고 나머지 계약서 없이 제 서명만 돌려준다면 그 계약은 유효하지 않을 것입니다. + +마찬가지로, 전자 서명은 연관된 메시지 없이는 아무 의미가 없습니다! + +## EIP-1271은 왜 존재하나요? + +이더리움 기반 블록체인에서 사용할 전자 서명을 생성하려면 일반적으로 다른 사람은 아무도 모르는 비밀 개인 키가 필요합니다. 이것이 바로 당신의 서명을 당신의 것으로 만드는 것입니다(비밀 키를 모르면 다른 누구도 동일한 서명을 생성할 수 없습니다). + +이더리움 계정(즉, 외부 소유 계정/EOA)에는 개인 키가 연결되어 있으며, 이 개인 키는 웹사이트나 디앱에서 서명을 요청할 때(예: '이더리움으로 로그인') 일반적으로 사용됩니다. + +앱은 ethers.js와 같은 제3자 라이브러리를 사용하여 사용자가 생성한 [서명을 확인할](https://www.alchemy.com/docs/how-to-verify-a-message-signature-on-ethereum) 수 있으며, [사용자의 개인 키를 몰라도](https://en.wikipedia.org/wiki/Public-key_cryptography) _사용자_가 서명을 생성했음을 확신할 수 있습니다. + +> 사실, EOA 전자 서명은 공개 키 암호학을 사용하기 때문에 **오프체인**에서 생성 및 확인할 수 있습니다! 이것이 바로 가스 없는 DAO 투표가 작동하는 방식입니다. 온체인에서 투표를 제출하는 대신 암호화 라이브러리를 사용하여 오프체인에서 전자 서명을 생성하고 확인할 수 있습니다. + +EOA 계정에는 개인 키가 있지만, 스마트 계약 계정에는 개인 키나 비밀 키가 없습니다(따라서 '이더리움으로 로그인' 등은 스마트 계약 계정에서 기본적으로 작동할 수 없습니다). + +EIP-1271이 해결하려는 문제는 다음과 같습니다. 스마트 계약에 서명에 포함할 수 있는 '비밀'이 없는 경우 스마트 계약 서명이 유효한지 어떻게 알 수 있을까요? + +## EIP-1271은 어떻게 작동하나요? + +스마트 계약에는 메시지에 서명하는 데 사용할 수 있는 개인 키가 없습니다. 그렇다면 서명이 진짜인지 어떻게 알 수 있을까요? + +한 가지 아이디어는 스마트 계약에 서명이 진짜인지 _물어보는_ 것입니다! + +EIP-1271은 주어진 서명이 유효한지 스마트 계약에 '질문'하는 이 아이디어를 표준화합니다. + +EIP-1271을 구현하는 계약에는 메시지와 서명을 입력으로 받는 `isValidSignature`라는 함수가 있어야 합니다. 그런 다음 계약은 일부 유효성 검사 로직을 실행하고(사양에서 여기에서 구체적인 내용을 강제하지는 않음) 서명이 유효한지 여부를 나타내는 값을 반환할 수 있습니다. + +`isValidSignature`가 유효한 결과를 반환하면, 이는 계약이 '네, 이 서명 + 메시지를 승인합니다!'라고 말하는 것과 거의 같습니다. + +### 인터페이스 + +다음은 EIP-1271 사양의 정확한 인터페이스입니다(아래에서 `_hash` 매개변수에 대해 이야기하겠지만, 지금은 확인 중인 메시지로 생각하세요). + +```jsx +pragma solidity ^0.5.0;\n\ncontract ERC1271 {\n\n // bytes4(keccak256(\"isValidSignature(bytes32,bytes)\")\n bytes4 constant internal MAGICVALUE = 0x1626ba7e;\n\n /**\n * @dev 제공된 해시에 대해 제공된 서명이 유효한지 여부를 반환해야 합니다.\n * @param _hash 서명할 데이터의 해시\n * @param _signature _hash와 연관된 서명 바이트 배열\n *\n * 함수가 통과하면 bytes4 매직 값 0x1626ba7e를 반환해야 합니다(MUST).\n * 상태를 수정해서는 안 됩니다(MUST NOT)(solc < 0.5의 경우 STATICCALL 사용, solc > 0.5의 경우 view 한정자 사용).\n * 외부 호출을 허용해야 합니다(MUST).\n */\n function isValidSignature(\n bytes32 _hash,\n bytes memory _signature)\n public\n view\n returns (bytes4 magicValue);\n} +``` + +## 예제 EIP-1271 구현: Safe + +계약은 `isValidSignature`를 여러 가지 방법으로 구현할 수 있습니다. 사양에서는 정확한 구현에 대해 많이 언급하지 않습니다. + +EIP-1271을 구현하는 주목할 만한 계약 중 하나는 Safe(이전 Gnosis Safe)입니다. + +Safe의 코드에서 `isValidSignature`는 [두 가지 방법](https://ethereum.stackexchange.com/questions/122635/signing-messages-as-a-gnosis-safe-eip1271-support)으로 서명을 생성하고 확인할 수 있도록 [구현](https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol)되었습니다. + +1. 온체인 메시지 + 1. 생성: Safe 소유자는 메시지에 '서명'하기 위해 새로운 Safe 트랜잭션을 생성하고 메시지를 데이터로 트랜잭션에 전달합니다. 충분한 수의 소유자가 트랜잭션에 서명하여 멀티시그 임계값에 도달하면 트랜잭션이 브로드캐스트되고 실행됩니다. 트랜잭션에는 메시지를 '승인된' 메시지 목록에 추가하는 `signMessage(bytes calldata _data)`라는 Safe 함수가 있습니다. + 2. 확인: Safe 계약에서 `isValidSignature`를 호출하고, 확인할 메시지를 메시지 매개변수로 전달하고 [서명 매개변수에는 빈 값](https://github.com/safe-global/safe-contracts/blob/main/contracts/handler/CompatibilityFallbackHandler.sol#L32)(즉, `0x`)을 전달합니다. Safe는 서명 매개변수가 비어 있는 것을 보고 서명을 암호학적으로 확인하는 대신, 메시지가 '승인된' 메시지 목록에 있는지 확인합니다. +2. 오프체인 메시지: + 1. 생성: Safe 소유자는 오프체인에서 메시지를 생성한 다음, 멀티시그 승인 임계값을 넘을 만큼 충분한 서명이 모일 때까지 다른 Safe 소유자가 각자 개별적으로 메시지에 서명하도록 합니다. + 2. 확인: `isValidSignature`를 호출합니다. 메시지 매개변수에는 확인할 메시지를 전달합니다. 서명 매개변수에는 각 Safe 소유자의 개별 서명을 모두 차례로 연결하여 전달합니다. Safe는 임계값을 충족하기에 충분한 서명이 있는지 **그리고** 각 서명이 유효한지 확인합니다. 그렇다면 서명 확인이 성공했음을 나타내는 값을 반환합니다. + +## `_hash` 매개변수는 정확히 무엇인가요? 전체 메시지를 전달하지 않는 이유는 무엇인가요? + +[EIP-1271 인터페이스](https://eips.ethereum.org/EIPS/eip-1271)의 `isValidSignature` 함수가 메시지 자체를 받지 않고 대신 `_hash` 매개변수를 받는다는 것을 눈치채셨을 겁니다. 이는 `isValidSignature`에 전체 임의 길이 메시지를 전달하는 대신, 메시지의 32바이트 해시(일반적으로 keccak256)를 전달한다는 것을 의미합니다. + +calldata의 각 바이트, 즉 스마트 계약 함수에 전달되는 함수 매개변수 데이터는 [16 가스(0바이트인 경우 4 가스)의 비용](https://eips.ethereum.org/EIPS/eip-2028)이 들기 때문에 메시지가 길 경우 많은 가스를 절약할 수 있습니다. + +### 이전 EIP-1271 사양 + +실제 사용되는 EIP-1271 사양 중에는 첫 번째 매개변수 유형이 `bytes`(고정 길이 `bytes32` 대신 임의 길이)이고 매개변수 이름이 `message`인 `isValidSignature` 함수를 가진 사양이 있습니다. 이것은 EIP-1271 표준의 [이전 버전](https://github.com/safe-global/safe-contracts/issues/391#issuecomment-1075427206)입니다. + +## 내 계약에 EIP-1271을 어떻게 구현해야 하나요? + +사양은 이 부분에서 매우 개방적입니다. Safe 구현에는 몇 가지 좋은 아이디어가 있습니다. + +- 계약의 '소유자'로부터의 EOA 서명을 유효한 것으로 간주할 수 있습니다. +- 승인된 메시지 목록을 저장하고 해당 메시지만 유효한 것으로 간주할 수 있습니다. + +결국, 계약 개발자인 당신에게 달려 있습니다! + +## 결론 + +[EIP-1271](https://eips.ethereum.org/EIPS/eip-1271)은 스마트 계약이 서명을 확인할 수 있도록 하는 다재다능한 표준입니다. 이 표준은 스마트 계약이 EOA처럼 작동할 수 있는 길을 열어주며, 예를 들어 '이더리움으로 로그인'이 스마트 계약과 함께 작동하는 방법을 제공합니다. 또한 여러 가지 방식으로 구현할 수 있습니다(Safe는 고려해 볼 만한 간단하지 않으면서도 흥미로운 구현을 가지고 있습니다). diff --git a/public/content/translations/ko/developers/tutorials/erc-721-vyper-annotated-code/index.md b/public/content/translations/ko/developers/tutorials/erc-721-vyper-annotated-code/index.md new file mode 100644 index 00000000000..7f10b81bc6a --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/erc-721-vyper-annotated-code/index.md @@ -0,0 +1,637 @@ +--- +title: "Vyper ERC-721 계약 살펴보기" +description: "Ryuya Nakamura의 ERC-721 계약 및 작동 원리" +author: Ori Pomerantz +lang: ko +tags: [ "vyper", "erc-721", "python" ] +skill: beginner +published: 2021-04-01 +--- + +## 소개 {#introduction} + +[ERC-721](/developers/docs/standards/tokens/erc-721/) 표준은 대체 불가능한 토큰(NFT)의 소유권을 보유하는 데 사용됩니다. +[ERC-20](/developers/docs/standards/tokens/erc-20/) 토큰은 개별 토큰 간에 차이가 없으므로 상품처럼 작동합니다. +이와 대조적으로 ERC-721 토큰은 서로 다른 고양이 +만화 또는 서로 다른 부동산 소유권과 같이 유사하지만 동일하지는 않은 자산을 위해 설계되었습니다. + +이 글에서는 [Ryuya Nakamura의 ERC-721 계약](https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC721.vy)을 분석합니다. +이 계약은 파이썬과 유사한 계약 언어인 [Vyper](https://vyper.readthedocs.io/en/latest/index.html)로 작성되었으며, 솔리디티보다 안전하지 않은 코드를 작성하기 어렵게 설계되었습니다. + +## 계약 {#contract} + +```python +# @dev ERC-721 대체 불가능한 토큰 표준 구현 +# @author Ryuya Nakamura (@nrryuya) +# 수정 출처: https://github.com/vyperlang/vyper/blob/de74722bf2d8718cca46902be165f9fe0e3641dd/examples/tokens/ERC721.vy +``` + +Vyper에서는 파이썬에서처럼 주석이 해시(`#`)로 시작하여 줄 끝까지 이어집니다. `@`를 포함하는 주석은 [NatSpec](https://vyper.readthedocs.io/en/latest/natspec.html)에서 사람이 읽을 수 있는 개발문서를 생성하는 데 사용됩니다. + +```python +from vyper.interfaces import ERC721 + +implements: ERC721 +``` + +ERC-721 인터페이스는 Vyper 언어에 내장되어 있습니다. +[여기에서 코드 정의를 볼 수 있습니다](https://github.com/vyperlang/vyper/blob/master/vyper/builtin_interfaces/ERC721.py). +인터페이스 정의는 Vyper가 아닌 파이썬으로 작성되었습니다. 인터페이스는 블록체인 내에서뿐만 아니라 외부 클라이언트에서 블록체인으로 트랜잭션을 보낼 때도 사용되며, 이 클라이언트는 파이썬으로 작성될 수 있기 때문입니다. + +첫 번째 줄은 인터페이스를 가져오고, 두 번째 줄은 여기서 구현하고 있음을 지정합니다. + +### ERC721Receiver 인터페이스 {#receiver-interface} + +```python +# safeTransferFrom()에 의해 호출되는 계약의 인터페이스 +interface ERC721Receiver: + def onERC721Received( +``` + +ERC-721은 두 가지 유형의 전송을 지원합니다. + +- `transferFrom`은 전송자가 모든 대상 주소를 지정하고 전송에 대한 책임을 전송자에게 부여할 수 있도록 합니다. 이는 잘못된 주소로 전송할 수 있음을 의미하며, 이 경우 NFT는 영원히 손실됩니다. +- `safeTransferFrom`은 대상 주소가 계약인지 확인합니다. 만약 그렇다면, ERC-721 계약은 수신 계약에 NFT를 수신할 것인지 묻습니다. + +`safeTransferFrom` 요청에 응답하려면 수신 계약이 `ERC721Receiver`를 구현해야 합니다. + +```python + _operator: address, + _from: address, +``` + +`_from` 주소는 토큰의 현재 소유자입니다. `_operator` 주소는 전송을 요청한 주소입니다(허용량 때문에 이 둘은 같지 않을 수 있습니다). + +```python + _tokenId: uint256, +``` + +ERC-721 토큰 ID는 256비트입니다. 일반적으로 토큰이 나타내는 것에 대한 설명을 해싱하여 생성됩니다. + +```python + _data: Bytes[1024] +``` + +요청은 최대 1024바이트의 사용자 데이터를 가질 수 있습니다. + +```python + ) -> bytes32: view +``` + +계약이 실수로 전송을 수락하는 경우를 방지하기 위해 반환 값은 부울이 아니라 특정 값을 가진 256비트입니다. + +이 함수는 `view`이며, 이는 블록체인의 상태를 읽을 수는 있지만 수정할 수는 없음을 의미합니다. + +### 이벤트 {#events} + +[이벤트](https://media.consensys.net/technical-introduction-to-events-and-logs-in-ethereum-a074d65dd61e)는 블록체인 외부의 사용자와 서버에 이벤트를 알리기 위해 발생됩니다. 이벤트의 내용은 블록체인의 계약에서 사용할 수 없습니다. + +```python +# @dev 모든 NFT의 소유권이 어떤 메커니즘으로든 변경될 때 발생합니다. 이 이벤트는 NFT가 +# 생성(`from` == 0)되거나 소멸(`to` == 0)될 때 발생합니다. 예외: 계약 생성 중에는 +# Transfer를 발생시키지 않고 여러 개의 NFT를 생성하고 할당할 수 있습니다. 전송 시점에 +# 해당 NFT에 대해 승인된 주소(있는 경우)는 없음으로 재설정됩니다. +# @param _from NFT의 전송자(주소가 0 주소인 경우 토큰 생성을 나타냄). +# @param _to NFT의 수신자(주소가 0 주소인 경우 토큰 소멸을 나타냄). +# @param _tokenId 전송된 NFT. +event Transfer: + sender: indexed(address) + receiver: indexed(address) + tokenId: indexed(uint256) +``` + +이는 금액 대신 `tokenId`를 보고한다는 점을 제외하면 ERC-20 전송 이벤트와 유사합니다. +아무도 0 주소를 소유하지 않으므로, 관례적으로 토큰의 생성과 소멸을 보고하는 데 사용합니다. + +```python +# @dev NFT에 대한 승인된 주소가 변경되거나 재확인될 때 발생합니다. 0 주소는 +# 승인된 주소가 없음을 나타냅니다. 전송 이벤트가 발생하면 이는 또한 +# 해당 NFT에 대한 승인된 주소(있는 경우)가 없음으로 재설정됨을 나타냅니다. +# @param _owner NFT의 소유자. +# @param _approved 승인하려는 주소. +# @param _tokenId 승인하려는 NFT. +event Approval: + owner: indexed(address) + approved: indexed(address) + tokenId: indexed(uint256) +``` + +ERC-721 승인은 ERC-20 허용량과 유사합니다. 특정 주소는 특정 토큰을 전송할 수 있습니다. 이는 계약이 토큰을 수락할 때 응답할 수 있는 메커니즘을 제공합니다. 계약은 이벤트를 수신할 수 없으므로, 토큰을 전송하기만 하면 계약은 이를 "알지" 못합니다. 이러한 방식으로 소유자는 먼저 승인을 제출한 다음 계약에 요청을 보냅니다: "토큰 X를 전송하도록 승인했으니, ...해 주세요." + +이는 ERC-721 표준을 ERC-20 표준과 유사하게 만들기 위한 설계상의 선택입니다. ERC-721 토큰은 대체 불가능하기 때문에, 계약은 토큰의 소유권을 보고 특정 토큰을 받았음을 식별할 수도 있습니다. + +```python +# @dev 소유자에 대해 운영자가 활성화 또는 비활성화될 때 발생합니다. 운영자는 +# 소유자의 모든 NFT를 관리할 수 있습니다. +# @param _owner NFT의 소유자. +# @param _operator 운영자 권한을 설정할 주소. +# @param _approved 운영자 권한 상태(운영자 권한이 부여되면 true, 취소되면 false). +event ApprovalForAll: + owner: indexed(address) + operator: indexed(address) + approved: bool +``` + +위임장과 유사하게 특정 유형(특정 계약에 의해 관리되는)의 계정의 모든 토큰을 관리할 수 있는 _운영자_가 있으면 유용할 때가 있습니다. 예를 들어, 6개월 동안 연락이 없으면 자산을 상속인에게 분배하는 계약에 이러한 권한을 부여할 수 있습니다(상속인 중 한 명이 요청하는 경우, 계약은 트랜잭션에 의해 호출되지 않으면 아무것도 할 수 없음). ERC-20에서는 상속 계약에 높은 허용량을 부여할 수 있지만, ERC-721은 토큰이 대체 불가능하기 때문에 작동하지 않습니다. 이것이 그에 상응하는 것입니다. + +`approved` 값은 이벤트가 승인을 위한 것인지, 아니면 승인 철회를 위한 것인지를 알려줍니다. + +### 상태 변수 {#state-vars} + +이 변수에는 토큰의 현재 상태, 즉 어떤 토큰이 사용 가능하고 누가 소유하고 있는지가 포함됩니다. 이들 대부분은 두 유형 사이에 존재하는 단방향 매핑인 `HashMap` 객체입니다[https://vyper.readthedocs.io/en/latest/types.html#mappings]. + +```python +# @dev NFT ID에서 이를 소유한 주소로의 매핑. +idToOwner: HashMap[uint256, address] + +# @dev NFT ID에서 승인된 주소로의 매핑. +idToApprovals: HashMap[uint256, address] +``` + +이더리움에서 사용자와 계약 신원은 160비트 주소로 표시됩니다. 이 두 변수는 토큰 ID에서 소유자 및 전송이 승인된 자(각각 최대 1명)로 매핑됩니다. 이더리움에서 초기화되지 않은 데이터는 항상 0이므로, 소유자나 승인된 전송자가 없는 경우 해당 토큰의 값은 0입니다. + +```python +# @dev 소유자 주소에서 토큰 수로의 매핑. +ownerToNFTokenCount: HashMap[address, uint256] +``` + +이 변수는 각 소유자의 토큰 수를 보유합니다. 소유자에서 토큰으로의 매핑이 없으므로, 특정 소유자가 소유한 토큰을 식별하는 유일한 방법은 블록체인의 이벤트 기록을 되돌아보고 적절한 `Transfer` 이벤트를 보는 것입니다. 이 변수를 사용하여 모든 NFT를 가지고 있고 더 이상 시간을 거슬러 볼 필요가 없을 때를 알 수 있습니다. + +이 알고리즘은 사용자 인터페이스와 외부 서버에서만 작동합니다. 블록체인 자체에서 실행되는 코드는 과거 이벤트를 읽을 수 없습니다. + +```python +# @dev 소유자 주소에서 운영자 주소 매핑으로의 매핑. +ownerToOperators: HashMap[address, HashMap[address, bool]] +``` + +하나의 계정은 여러 운영자를 가질 수 있습니다. 단순한 `HashMap`으로는 각 키가 단일 값으로 연결되기 때문에 이를 추적하기에 충분하지 않습니다. 대신, 값으로 `HashMap[address, bool]`를 사용할 수 있습니다. 기본적으로 각 주소의 값은 `False`이며, 이는 운영자가 아님을 의미합니다. 필요에 따라 값을 `True`로 설정할 수 있습니다. + +```python +# @dev 토큰을 발행할 수 있는 발행자의 주소 +minter: address +``` + +새로운 토큰은 어떻게든 생성되어야 합니다. 이 계약에는 이를 수행할 수 있는 단일 엔티티, 즉 `minter`가 있습니다. 예를 들어, 게임에는 이것으로 충분할 것입니다. 다른 목적을 위해서는 더 복잡한 비즈니스 로직을 생성해야 할 수도 있습니다. + +```python +# @dev 인터페이스 ID에서 지원 여부에 대한 부울로의 매핑 +supportedInterfaces: HashMap[bytes32, bool] + +# @dev ERC165의 ERC165 인터페이스 ID +ERC165_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000001ffc9a7 + +# @dev ERC721의 ERC165 인터페이스 ID +ERC721_INTERFACE_ID: constant(bytes32) = 0x0000000000000000000000000000000000000000000000000000000080ac58cd +``` + +[ERC-165](https://eips.ethereum.org/EIPS/eip-165)는 계약이 애플리케이션과 통신하는 방법, 즉 어떤 ERC를 준수하는지 공개하는 메커니즘을 지정합니다. 이 경우, 계약은 ERC-165와 ERC-721을 준수합니다. + +### 함수 {#functions} + +이들은 실제로 ERC-721을 구현하는 함수입니다. + +#### 생성자 {#constructor} + +```python +@external +def __init__(): +``` + +Vyper에서는 파이썬에서처럼 생성자 함수를 `__init__`이라고 합니다. + +```python + """ + @dev 계약 생성자. + """ +``` + +파이썬과 Vyper에서는 여러 줄 문자열(`"""`로 시작하고 끝나는)을 지정하고 어떤 방식으로도 사용하지 않음으로써 주석을 만들 수도 있습니다. 이러한 주석에는 [NatSpec](https://vyper.readthedocs.io/en/latest/natspec.html)도 포함될 수 있습니다. + +```python + self.supportedInterfaces[ERC165_INTERFACE_ID] = True + self.supportedInterfaces[ERC721_INTERFACE_ID] = True + self.minter = msg.sender +``` + +상태 변수에 액세스하려면 `self.<변수 이름>`을(를) 사용합니다.`(파이썬에서와 동일합니다). + +#### 뷰 함수 {#views} + +이 함수들은 블록체인의 상태를 수정하지 않으므로 외부에서 호출될 경우 무료로 실행될 수 있습니다. 뷰 함수가 계약에 의해 호출되더라도 모든 노드에서 실행되어야 하므로 가스가 소모됩니다. + +```python +@view +@external +``` + +함수 정의 이전에 `@` 기호로 시작하는 이러한 키워드를 _데코레이션_이라고 합니다. 함수가 호출될 수 있는 상황을 지정합니다. + +- `@view`는 이 함수가 뷰임을 지정합니다. +- `@external`은 이 특정 함수가 트랜잭션 및 다른 계약에 의해 호출될 수 있음을 지정합니다. + +```python +def supportsInterface(_interfaceID: bytes32) -> bool: +``` + +파이썬과 달리 Vyper는 [정적 타입 언어](https://wikipedia.org/wiki/Type_system#Static_type_checking)입니다. +[데이터 유형](https://vyper.readthedocs.io/en/latest/types.html)을 식별하지 않고는 변수나 함수 매개변수를 선언할 수 없습니다. 이 경우 입력 매개변수는 256비트 값인 `bytes32`입니다(256비트는 [이더리움 가상 머신](/developers/docs/evm/)의 네이티브 워드 크기입니다). 출력은 부울 값입니다. 관례적으로 함수 매개변수의 이름은 밑줄(`_`)로 시작합니다. + +```python + """ + @dev 인터페이스 식별은 ERC-165에 지정됩니다. + @param _interfaceID 인터페이스의 ID + """ + return self.supportedInterfaces[_interfaceID] +``` + +생성자(`__init__`)에서 설정된 `self.supportedInterfaces` HashMap에서 값을 반환합니다. + +```python +### 뷰 함수 ### + +``` + +이들은 토큰에 대한 정보를 사용자 및 다른 계약에서 사용할 수 있도록 하는 뷰 함수입니다. + +```python +@view +@external +def balanceOf(_owner: address) -> uint256: + """ + @dev `_owner`가 소유한 NFT 수를 반환합니다. + `_owner`가 0 주소인 경우 예외를 발생시킵니다. 0 주소에 할당된 NFT는 유효하지 않은 것으로 간주됩니다. + @param _owner 잔액을 쿼리할 주소. + """ + assert _owner != ZERO_ADDRESS +``` + +이 줄은 `_owner`가 0이 아님을 [주장](https://vyper.readthedocs.io/en/latest/statements.html#assert)합니다. 만약 그렇다면 오류가 발생하고 작업이 되돌려집니다. + +```python + return self.ownerToNFTokenCount[_owner] + +@view +@external +def ownerOf(_tokenId: uint256) -> address: + """ + @dev NFT 소유자의 주소를 반환합니다. + `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. + @param _tokenId NFT의 식별자. + """ + owner: address = self.idToOwner[_tokenId] + # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다 + assert owner != ZERO_ADDRESS + return owner +``` + +이더리움 가상 머신(EVM)에서 값이 저장되지 않은 저장 공간은 모두 0입니다. +`_tokenId`에 토큰이 없으면 `self.idToOwner[_tokenId]`의 값은 0입니다. 이 경우 함수는 되돌려집니다. + +```python +@view +@external +def getApproved(_tokenId: uint256) -> address: + """ + @dev 단일 NFT에 대한 승인된 주소를 가져옵니다. + `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. + @param _tokenId 승인을 쿼리할 NFT의 ID. + """ + # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다 + assert self.idToOwner[_tokenId] != ZERO_ADDRESS + return self.idToApprovals[_tokenId] +``` + +`getApproved`는 0을 반환할 _수_ 있습니다. 토큰이 유효하면 `self.idToApprovals[_tokenId]`를 반환합니다. +승인자가 없으면 그 값은 0입니다. + +```python +@view +@external +def isApprovedForAll(_owner: address, _operator: address) -> bool: + """ + @dev `_operator`가 `_owner`에 대해 승인된 운영자인지 확인합니다. + @param _owner NFT를 소유한 주소. + @param _operator 소유자를 대신하여 행동하는 주소. + """ + return (self.ownerToOperators[_owner])[_operator] +``` + +이 함수는 `_operator`가 이 계약에서 `_owner`의 모든 토큰을 관리할 수 있는지 확인합니다. +여러 운영자가 있을 수 있으므로 이는 2단계 HashMap입니다. + +#### 전송 헬퍼 함수 {#transfer-helpers} + +이 함수는 토큰 전송 또는 관리의 일부인 작업을 구현합니다. + +```python + +### 전송 함수 헬퍼 ### + +@view +@internal +``` + +이 데코레이션, `@internal`은 함수가 동일한 계약 내의 다른 함수에서만 액세스할 수 있음을 의미합니다. 관례적으로 이러한 함수 이름도 밑줄(`_`)로 시작합니다. + +```python +def _isApprovedOrOwner(_spender: address, _tokenId: uint256) -> bool: + """ + @dev 주어진 지출자가 주어진 토큰 ID를 전송할 수 있는지 여부를 반환합니다. + @param spender 쿼리할 지출자의 주소 + @param tokenId 전송될 토큰의 uint256 ID + @return bool msg.sender가 주어진 토큰 ID에 대해 승인되었는지, + 소유자의 운영자인지, 또는 토큰의 소유자인지 여부 + """ + owner: address = self.idToOwner[_tokenId] + spenderIsOwner: bool = owner == _spender + spenderIsApproved: bool = _spender == self.idToApprovals[_tokenId] + spenderIsApprovedForAll: bool = (self.ownerToOperators[owner])[_spender] + return (spenderIsOwner or spenderIsApproved) or spenderIsApprovedForAll +``` + +주소가 토큰을 전송할 수 있도록 허용되는 세 가지 방법이 있습니다. + +1. 주소가 토큰의 소유자입니다 +2. 주소가 해당 토큰을 사용하도록 승인되었습니다 +3. 주소는 토큰 소유자의 운영자입니다 + +위의 함수는 상태를 변경하지 않으므로 뷰가 될 수 있습니다. 운영 비용을 줄이기 위해, 뷰가 될 _수_ 있는 모든 함수는 뷰가 되어야 _합니다_. + +```python +@internal +def _addTokenTo(_to: address, _tokenId: uint256): + """ + @dev 주어진 주소에 NFT 추가 + `_tokenId`가 누군가에 의해 소유된 경우 예외를 발생시킵니다. + """ + # `_tokenId`가 누군가에 의해 소유된 경우 예외를 발생시킵니다 + assert self.idToOwner[_tokenId] == ZERO_ADDRESS + # 소유자 변경 + self.idToOwner[_tokenId] = _to + # 수량 추적 변경 + self.ownerToNFTokenCount[_to] += 1 + + +@internal +def _removeTokenFrom(_from: address, _tokenId: uint256): + """ + @dev 주어진 주소에서 NFT 제거 + `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다. + """ + # `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다 + assert self.idToOwner[_tokenId] == _from + # 소유자 변경 + self.idToOwner[_tokenId] = ZERO_ADDRESS + # 수량 추적 변경 + self.ownerToNFTokenCount[_from] -= 1 +``` + +전송에 문제가 발생하면 호출을 되돌립니다. + +```python +@internal +def _clearApproval(_owner: address, _tokenId: uint256): + """ + @dev 주어진 주소의 승인 취소 + `_owner`가 현재 소유자가 아닌 경우 예외를 발생시킵니다. + """ + # `_owner`가 현재 소유자가 아닌 경우 예외를 발생시킵니다 + assert self.idToOwner[_tokenId] == _owner + if self.idToApprovals[_tokenId] != ZERO_ADDRESS: + # 승인 재설정 + self.idToApprovals[_tokenId] = ZERO_ADDRESS +``` + +필요한 경우에만 값을 변경합니다. 상태 변수는 저장 공간에 있습니다. 저장 공간에 쓰는 것은 EVM(이더리움 가상 머신)이 수행하는 가장 비싼 작업 중 하나입니다([가스](/developers/docs/gas/) 측면에서). 따라서 기존 값을 쓰는 것조차 높은 비용이 들기 때문에 이를 최소화하는 것이 좋습니다. + +```python +@internal +def _transferFrom(_from: address, _to: address, _tokenId: uint256, _sender: address): + """ + @dev NFT 전송 실행. + `msg.sender`가 현재 소유자, 승인된 운영자 또는 이 NFT에 대해 승인된 주소가 아닌 경우 예외를 발생시킵니다. + (참고: `msg.sender`는 비공개 함수에서 허용되지 않으므로 `_sender`를 전달합니다.) + `_to`가 0 주소인 경우 예외를 발생시킵니다. + `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다. + `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. + """ +``` + +토큰을 전송하는 두 가지 방법(일반 및 안전)이 있지만, 감사를 더 쉽게 하기 위해 코드를 한 곳에서만 수행하기 위해 이 내부 함수를 사용합니다. + +```python + # 요구 사항 확인 + assert self._isApprovedOrOwner(_sender, _tokenId) + # `_to`가 0 주소인 경우 예외를 발생시킵니다 + assert _to != ZERO_ADDRESS + # 승인 취소. `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다 + self._clearApproval(_from, _tokenId) + # NFT 제거. `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다 + self._removeTokenFrom(_from, _tokenId) + # NFT 추가 + self._addTokenTo(_to, _tokenId) + # 전송 기록 + log Transfer(_from, _to, _tokenId) +``` + +Vyper에서 이벤트를 발생시키려면 `log` 문을 사용합니다([자세한 내용은 여기 참조](https://vyper.readthedocs.io/en/latest/event-logging.html#event-logging)). + +#### 전송 함수 {#transfer-funs} + +```python + +### 전송 함수 ### + +@external +def transferFrom(_from: address, _to: address, _tokenId: uint256): + """ + @dev `msg.sender`가 현재 소유자, 승인된 운영자 또는 이 NFT에 대해 승인된 주소가 아닌 경우 예외를 발생시킵니다. + `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다. + `_to`가 0 주소인 경우 예외를 발생시킵니다. + `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. + @notice 호출자는 `_to`가 NFT를 수신할 수 있는지 확인해야 할 책임이 있으며, 그렇지 않으면 영구적으로 손실될 수 있습니다. + @param _from NFT의 현재 소유자. + @param _to 새로운 소유자. + @param _tokenId 전송할 NFT. + """ + self._transferFrom(_from, _to, _tokenId, msg.sender) +``` + +이 함수를 사용하면 임의의 주소로 전송할 수 있습니다. 주소가 사용자이거나 토큰을 전송하는 방법을 아는 계약이 아닌 한, 전송하는 모든 토큰은 해당 주소에 갇혀 쓸모없게 됩니다. + +```python +@external +def safeTransferFrom( + _from: address, + _to: address, + _tokenId: uint256, + _data: Bytes[1024]=b"" + ): + """ + @dev 한 주소에서 다른 주소로 NFT의 소유권을 이전합니다. + `msg.sender`가 현재 소유자, 승인된 운영자 또는 이 NFT에 대해 승인된 주소가 아닌 경우 예외를 발생시킵니다. + `_from`이 현재 소유자가 아닌 경우 예외를 발생시킵니다. + `_to`가 0 주소인 경우 예외를 발생시킵니다. + `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. + `_to`가 스마트 계약인 경우 `_to`에서 `onERC721Received`를 호출하고 반환 값이 `bytes4(keccak256("onERC721Received(address,address,uint256,bytes)"))`가 아닌 경우 예외를 발생시킵니다. + 참고: bytes4는 패딩이 있는 bytes32로 표시됩니다. + @param _from NFT의 현재 소유자. + @param _to 새로운 소유자. + @param _tokenId 전송할 NFT. + @param _data 지정된 형식이 없는 추가 데이터, `_to` 호출 시 전송됩니다. + """ + self._transferFrom(_from, _to, _tokenId, msg.sender) +``` + +어쨌든 문제가 발생하면 되돌릴 것이므로 먼저 전송을 수행해도 괜찮습니다. 그러면 호출에서 수행된 모든 작업이 취소됩니다. + +```python + if _to.is_contract: # `_to`가 계약 주소인지 확인 +``` + +먼저 주소가 계약인지(코드가 있는지) 확인합니다. 그렇지 않은 경우 사용자 주소라고 가정하고 사용자는 토큰을 사용하거나 전송할 수 있습니다. 그러나 그것이 당신을 잘못된 안도감에 빠지게 하지는 마십시오. `safeTransferFrom`을 사용하더라도 아무도 개인 키를 모르는 주소로 토큰을 전송하면 토큰을 잃을 수 있습니다. + +```python + returnValue: bytes32 = ERC721Receiver(_to).onERC721Received(msg.sender, _from, _tokenId, _data) +``` + +대상 계약을 호출하여 ERC-721 토큰을 수신할 수 있는지 확인합니다. + +```python + # 전송 대상이 'onERC721Received'를 구현하지 않는 계약인 경우 예외를 발생시킵니다 + assert returnValue == method_id("onERC721Received(address,address,uint256,bytes)", output_type=bytes32) +``` + +대상이 계약이지만 ERC-721 토큰을 허용하지 않거나(또는 이 특정 전송을 허용하지 않기로 결정한) 경우 되돌립니다. + +```python +@external +def approve(_approved: address, _tokenId: uint256): + """ + @dev NFT에 대해 승인된 주소를 설정하거나 재확인합니다. 0 주소는 승인된 주소가 없음을 나타냅니다. + `msg.sender`가 현재 NFT 소유자이거나 현재 소유자의 승인된 운영자가 아닌 경우 예외를 발생시킵니다. + `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. (참고: EIP에 기록되지 않음) + `_approved`가 현재 소유자인 경우 예외를 발생시킵니다. (참고: EIP에 기록되지 않음) + @param _approved 주어진 NFT ID에 대해 승인될 주소. + @param _tokenId 승인될 토큰의 ID. + """ + owner: address = self.idToOwner[_tokenId] + # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다 + assert owner != ZERO_ADDRESS + # `_approved`가 현재 소유자인 경우 예외를 발생시킵니다 + assert _approved != owner +``` + +관례적으로 승인자를 두지 않으려면 자신을 지정하는 것이 아니라 0 주소를 지정합니다. + +```python + # 요구 사항 확인 + senderIsOwner: bool = self.idToOwner[_tokenId] == msg.sender + senderIsApprovedForAll: bool = (self.ownerToOperators[owner])[msg.sender] + assert (senderIsOwner or senderIsApprovedForAll) +``` + +승인을 설정하려면 소유자이거나 소유자가 승인한 운영자여야 합니다. + +```python + # 승인 설정 + self.idToApprovals[_tokenId] = _approved + log Approval(owner, _approved, _tokenId) + + +@external +def setApprovalForAll(_operator: address, _approved: bool): + """ + @dev 제3자("운영자")가 `msg.sender`의 모든 자산을 관리할 수 있도록 승인을 활성화하거나 비활성화합니다. 또한 ApprovalForAll 이벤트를 발생시킵니다. + `_operator`가 `msg.sender`인 경우 예외를 발생시킵니다. (참고: EIP에 기록되지 않음) + @notice 이는 전송자가 당시 토큰을 소유하지 않은 경우에도 작동합니다. + @param _operator 승인된 운영자 집합에 추가할 주소. + @param _approved 운영자가 승인되면 True, 승인을 취소하려면 false입니다. + """ + # `_operator`가 `msg.sender`인 경우 예외를 발생시킵니다 + assert _operator != msg.sender + self.ownerToOperators[msg.sender][_operator] = _approved + log ApprovalForAll(msg.sender, _operator, _approved) +``` + +#### 새 토큰 발행 및 기존 토큰 소멸 {#mint-burn} + +계약을 생성한 계정은 새로운 NFT를 발행할 권한이 있는 슈퍼 사용자인 `minter`입니다. 그러나 기존 토큰을 소각하는 것은 허용되지 않습니다. 소유자 또는 소유자가 승인한 엔티티만 그렇게 할 수 있습니다. + +```python +### 발행 및 소각 함수 ### + +@external +def mint(_to: address, _tokenId: uint256) -> bool: +``` + +이 함수는 작업이 실패하면 되돌려지기 때문에 항상 `True`를 반환합니다. + +```python + """ + @dev 토큰을 발행하는 함수 + `msg.sender`가 발행자가 아닌 경우 예외를 발생시킵니다. + `_to`가 0 주소인 경우 예외를 발생시킵니다. + `_tokenId`가 누군가에 의해 소유된 경우 예외를 발생시킵니다. + @param _to 발행된 토큰을 받을 주소. + @param _tokenId 발행할 토큰 ID. + @return 작업이 성공했는지 여부를 나타내는 부울 값. + """ + # `msg.sender`가 발행자가 아닌 경우 예외를 발생시킵니다 + assert msg.sender == self.minter +``` + +발행자(ERC-721 계약을 생성한 계정)만이 새로운 토큰을 발행할 수 있습니다. 이는 나중에 발행자의 신원을 변경하고자 할 때 문제가 될 수 있습니다. 프로덕션 계약에서는 발행자가 발행자 권한을 다른 사람에게 이전할 수 있도록 하는 함수를 원할 것입니다. + +```python + # `_to`가 0 주소인 경우 예외를 발생시킵니다 + assert _to != ZERO_ADDRESS + # NFT 추가. `_tokenId`가 누군가에 의해 소유된 경우 예외를 발생시킵니다 + self._addTokenTo(_to, _tokenId) + log Transfer(ZERO_ADDRESS, _to, _tokenId) + return True +``` + +관례적으로 새로운 토큰 발행은 0 주소로부터의 전송으로 간주됩니다. + +```python + +@external +def burn(_tokenId: uint256): + """ + @dev 특정 ERC721 토큰을 소각합니다. + `msg.sender`가 현재 소유자, 승인된 운영자 또는 이 NFT에 대해 승인된 주소가 아닌 경우 예외를 발생시킵니다. + `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다. + @param _tokenId 소각될 ERC721 토큰의 uint256 ID. + """ + # 요구 사항 확인 + assert self._isApprovedOrOwner(msg.sender, _tokenId) + owner: address = self.idToOwner[_tokenId] + # `_tokenId`가 유효한 NFT가 아닌 경우 예외를 발생시킵니다 + assert owner != ZERO_ADDRESS + self._clearApproval(owner, _tokenId) + self._removeTokenFrom(owner, _tokenId) + log Transfer(owner, ZERO_ADDRESS, _tokenId) +``` + +토큰을 전송할 수 있는 사람은 누구나 소각할 수 있습니다. 소각은 0 주소로 전송하는 것과 동일하게 보이지만, 0 주소는 실제로 토큰을 받지 않습니다. 이를 통해 토큰에 사용된 모든 저장 공간을 확보할 수 있으므로 트랜잭션의 가스 비용을 줄일 수 있습니다. + +## 이 계약 사용하기 {#using-contract} + +솔리디티와 달리 Vyper는 상속이 없습니다. 이는 코드를 더 명확하게 하여 보안을 더 쉽게 하기 위한 의도적인 설계 선택입니다. 따라서 자신만의 Vyper ERC-721 계약을 만들려면 [이 계약](https://github.com/vyperlang/vyper/blob/master/examples/tokens/ERC721.vy)을 가져와 원하는 비즈니스 로직을 구현하도록 수정합니다. + +## 결론 {#conclusion} + +검토를 위해, 이 계약의 가장 중요한 몇 가지 아이디어는 다음과 같습니다: + +- 안전한 전송으로 ERC-721 토큰을 받으려면 계약이 `ERC721Receiver` 인터페이스를 구현해야 합니다. +- 안전한 전송을 사용하더라도 개인 키를 알 수 없는 주소로 보내면 토큰이 여전히 갇힐 수 있습니다. +- 작업에 문제가 있을 경우 실패 값을 반환하는 것보다 호출을 `revert`하는 것이 좋습니다. +- ERC-721 토큰은 소유자가 있을 때 존재합니다. +- NFT를 전송할 권한을 부여받는 방법은 세 가지가 있습니다. 소유자가 되거나, 특정 토큰에 대해 승인받거나, 소유자의 모든 토큰에 대한 운영자가 될 수 있습니다. +- 과거 이벤트는 블록체인 외부에서만 볼 수 있습니다. 블록체인 내부에서 실행되는 코드는 이를 볼 수 없습니다. + +이제 안전한 Vyper 계약을 구현해 보세요. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). + diff --git a/public/content/translations/ko/developers/tutorials/erc20-annotated-code/index.md b/public/content/translations/ko/developers/tutorials/erc20-annotated-code/index.md new file mode 100644 index 00000000000..75519b5040e --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/erc20-annotated-code/index.md @@ -0,0 +1,809 @@ +--- +title: "ERC-20 계약 살펴보기" +description: "OpenZeppelin ERC-20 계약에는 무엇이 있으며 왜 사용될까요?" +author: Ori Pomerantz +lang: ko +tags: [ "솔리디티", "erc-20" ] +skill: beginner +published: 2021-03-09 +--- + +## 소개 {#introduction} + +이더리움이 사용되는 대표적인 곳 중 하나는 거래 가능한 토큰을 만드는 그룹입니다. 거래 가능한 토큰은 자체 통화라고도 불립니다. 이러한 토큰은 일반적으로 [ERC-20](/developers/docs/standards/tokens/erc-20/) 표준을 따릅니다. 이 표준을 통해 모든 ERC-20 토큰에서 작동하는 유동성 풀이나 지갑 같은 도구를 작성할 수 있습니다. 이 글에서는 [OpenZeppelin 솔리디티 ERC20 구현](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol)과 [인터페이스 정의](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol)에 대해 분석해 보겠습니다. + +이 문서에 나오는 소스 코드는 주석을 포함합니다. ERC-20을 구현하고 싶다면 [이 튜토리얼을 읽어보세요](https://docs.openzeppelin.com/contracts/2.x/erc20-supply). + +## 인터페이스 {#the-interface} + +ERC-20과 같은 표준의 목적은 지갑 및 탈중앙화 거래소와 같은 애플리케이션 전반에서 상호 운용 가능한 많은 토큰 구현을 허용하는 것입니다. 이를 위해 [인터페이스](https://www.geeksforgeeks.org/solidity/solidity-basics-of-interface/)를 만듭니다. 토큰 계약을 사용해야 하는 코드는 인터페이스에서 동일한 정의를 사용하여 이를 사용하는 모든 토큰 계약과 호환될 수 있습니다. MetaMask와 같은 지갑이든, etherscan.io와 같은 탈중앙화앱이든, 유동성 풀과 같은 다른 계약이든 상관없습니다. + +![ERC-20 인터페이스 그림](erc20_interface.png) + +숙련된 프로그래머라면 [Java](https://www.w3schools.com/java/java_interface.asp)나 [C 헤더 파일](https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html)에서 비슷한 구조를 본 기억이 있을 것입니다. + +이것은 OpenZeppelin의 [ERC-20 인터페이스](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol)에 대한 정의입니다. 이는 [사람이 읽을 수 있는 표준](https://eips.ethereum.org/EIPS/eip-20)을 솔리디티 코드로 번역한 것입니다. 물론 인터페이스 자체가 무언가를 _어떻게_ 해야 하는지 정의하지는 않습니다. 이는 아래 계약 소스 코드에 설명되어 있습니다. + +  + +```solidity +// SPDX-License-Identifier: MIT +``` + +솔리디티 파일은 라이선스 식별자를 포함해야 합니다. [여기에서 라이선스 목록을 볼 수 있습니다](https://spdx.org/licenses/). 다른 라이선스가 필요한 경우 주석에 설명하면 됩니다. + +  + +```solidity +pragma solidity >=0.6.0 <0.8.0; +``` + +솔리디티 언어는 여전히 빠르게 발전하고 있으며, 새 버전은 이전 코드와 호환되지 않을 수 있습니다([여기 참조](https://docs.soliditylang.org/en/v0.7.0/070-breaking-changes.html)). 따라서 언어의 최소 버전뿐만 아니라 코드를 테스트한 최신 버전인 최대 버전도 지정하는 것이 좋습니다. + +  + +```solidity +/** + * @dev EIP에 정의된 ERC20 표준의 인터페이스입니다. + */ +``` + +주석의 `@dev`는 소스 코드에서 문서를 생성하는 데 사용되는 [NatSpec 형식](https://docs.soliditylang.org/en/develop/natspec-format.html)의 일부입니다. + +  + +```solidity +interface IERC20 { +``` + +관례적으로 인터페이스 이름은 `I`로 시작합니다. + +  + +```solidity + /** + * @dev 존재하는 토큰의 양을 반환합니다. + */ + function totalSupply() external view returns (uint256); +``` + +이 함수는 `external`이며, 이는 [계약 외부에서만 호출할 수 있음](https://docs.soliditylang.org/en/v0.7.0/cheatsheet.html#index-2)을 의미합니다. +계약에 있는 토큰의 총공급량을 반환합니다. 이 값은 이더리움에서 가장 일반적인 유형인 부호 없는 256비트를 사용하여 반환됩니다(256비트는 EVM의 기본 워드 크기임). 이 함수는 또한 `view` 함수인데, 이는 상태를 변경하지 않으므로 블록체인의 모든 노드에서 실행하는 대신 단일 노드에서 실행할 수 있음을 의미합니다. 이러한 종류의 함수는 트랜잭션을 생성하지 않으며 [가스](/developers/docs/gas/)가 들지 않습니다. + +**참고:** 이론적으로 계약 생성자가 실제 값보다 적은 총공급량을 반환하여 각 토큰이 실제보다 더 가치 있는 것처럼 보이게 속일 수 있는 것처럼 보일 수 있습니다. 그러나 그 두려움은 블록체인의 진정한 본질을 무시하는 것입니다. 블록체인에서 일어나는 모든 일은 모든 노드에서 검증할 수 있습니다. 이를 위해 모든 계약의 기계어 코드와 저장 공간은 모든 노드에서 사용할 수 있습니다. 계약에 대한 솔리디티 코드를 공개할 필요는 없지만, 소스 코드와 컴파일된 솔리디티 버전을 공개하지 않으면 누구도 진지하게 받아들이지 않을 것입니다. 그래야 제공한 기계어 코드와 대조하여 검증할 수 있기 때문입니다. +예를 들어, [이 계약](https://eth.blockscout.com/address/0xa530F85085C6FE2f866E7FdB716849714a89f4CD?tab=contract)을 참조하세요. + +  + +```solidity + /** + * @dev `account`가 소유한 토큰의 양을 반환합니다. + */ + function balanceOf(address account) external view returns (uint256); +``` + +이름에서 알 수 있듯이, `balanceOf`는 계정의 잔액을 반환합니다. 이더리움 계정은 솔리디티에서 160비트를 보유하는 `address` 유형을 사용하여 식별됩니다. +이 함수 또한 `external` 및 `view`입니다. + +  + +```solidity + /** + * @dev 호출자 계정에서 `recipient`로 `amount` 만큼의 토큰을 이동시킵니다. + * + * 작업 성공 여부를 나타내는 불리언 값을 반환합니다. + * + * {Transfer} 이벤트를 발생시킵니다. + */ + function transfer(address recipient, uint256 amount) external returns (bool); +``` + +`transfer` 함수는 호출자로부터 다른 주소로 토큰을 전송합니다. 이는 상태 변경을 포함하므로 `view`가 아닙니다. +사용자가 이 함수를 호출하면 트랜잭션이 생성되고 가스가 소모됩니다. 또한 `Transfer` 이벤트를 발생시켜 블록체인의 모든 사람에게 해당 이벤트를 알립니다. + +이 함수는 두 가지 다른 유형의 호출자에 대해 두 가지 유형의 출력을 가집니다. + +- 사용자 인터페이스에서 직접 함수를 호출하는 사용자. 일반적으로 사용자는 트랜잭션을 제출하고 응답을 기다리지 않는데, 이는 무기한의 시간이 걸릴 수 있습니다. 사용자는 트랜잭션 영수증(트랜잭션 해시로 식별됨)을 찾거나 `Transfer` 이벤트를 찾아 무슨 일이 일어났는지 확인할 수 있습니다. +- 전체 트랜잭션의 일부로 함수를 호출하는 다른 계약. 이러한 계약은 동일한 트랜잭션에서 실행되기 때문에 즉시 결과를 얻으므로 함수 반환 값을 사용할 수 있습니다. + +계약의 상태를 변경하는 다른 함수에 의해서도 동일한 유형의 출력이 생성됩니다. + +  + +허용량을 사용하면 계정이 다른 소유자에게 속한 일부 토큰을 사용할 수 있습니다. +예를 들어 판매자 역할을 하는 계약에 유용합니다. 계약은 이벤트를 모니터링할 수 없으므로 구매자가 판매자 계약으로 직접 토큰을 전송하면 해당 계약은 지불되었는지 알 수 없습니다. 대신, 구매자는 판매자 계약이 특정 금액을 사용하도록 허용하고 판매자는 해당 금액을 전송합니다. +이는 판매자 계약이 호출하는 함수를 통해 수행되므로 판매자 계약은 성공 여부를 알 수 있습니다. + +```solidity + /** + * @dev {transferFrom}을 통해 `spender`가 `owner`를 대신하여 사용할 수 있도록 허용된 나머지 토큰 수를 반환합니다. 기본값은 0입니다. + * + * {approve} 또는 {transferFrom}이 호출되면 이 값이 변경됩니다. + */ + function allowance(address owner, address spender) external view returns (uint256); +``` + +`allowance` 함수를 사용하면 누구나 한 주소(`owner`)가 다른 주소(`spender`)에게 허용한 허용량이 얼마인지 조회할 수 있습니다. + +  + +```solidity + /** + * @dev 호출자의 토큰에 대한 `spender`의 허용량으로 `amount`를 설정합니다. + * + * 작업 성공 여부를 나타내는 불리언 값을 반환합니다. + * + * 중요: 이 방법으로 허용량을 변경하면 불행한 + * 트랜잭션 순서로 인해 누군가 이전 허용량과 새 허용량을 모두 사용할 수 있는 위험이 따릅니다. 이 경쟁 + * 조건을 완화하기 위한 한 가지 가능한 해결책은 먼저 지출자의 허용량을 0으로 줄이고 그 후에 + * 원하는 값을 설정하는 것입니다: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + * + * {Approval} 이벤트를 발생시킵니다. + */ + function approve(address spender, uint256 amount) external returns (bool); +``` + +`approve` 함수는 허용량을 생성합니다. 이 함수가 어떻게 남용될 수 있는지에 대한 메시지를 반드시 읽어보세요. 이더리움에서는 자신의 트랜잭션 순서는 제어할 수 있지만, 상대방의 트랜잭션이 발생한 것을 보기 전까지는 자신의 트랜잭션을 제출하지 않는 한 다른 사람의 트랜잭션이 실행되는 순서는 제어할 수 없습니다. + +  + +```solidity + /** + * @dev 허용량 메커니즘을 사용하여 `sender`에서 `recipient`로 `amount` 토큰을 이동합니다. 그러면 호출자의 + * 허용량에서 `amount`가 차감됩니다. + * + * 작업 성공 여부를 나타내는 불리언 값을 반환합니다. + * + * {Transfer} 이벤트를 발생시킵니다. + */ + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); +``` + +마지막으로 지출자는 `transferFrom`을 사용하여 실제로 허용량을 사용합니다. + +  + +```solidity + + /** + * @dev `value` 토큰이 한 계정(`from`)에서 + * 다른 계정(`to`)으로 이동할 때 발생합니다. + * + * `value`는 0일 수 있다는 점에 유의하세요. + */ + event Transfer(address indexed from, address indexed to, uint256 value); + + /** + * @dev {approve}를 호출하여 `owner`의 `spender`에 대한 허용 한도를 + * 설정할 때 발생합니다. `value`는 새로운 허용 한도입니다. + */ + event Approval(address indexed owner, address indexed spender, uint256 value); +} +``` + +이러한 이벤트는 ERC-20 컨트랙트의 상태가 변경될 때 발생합니다. + +## 실제 컨트랙트 {#the-actual-contract} + +이는 ERC-20 표준을 구현하는 실제 컨트랙트이며, +[여기](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol)에서 가져왔습니다. +있는 그대로 사용하기 위한 것은 아니지만, +여기에서 [상속](https://www.tutorialspoint.com/solidity/solidity_inheritance.htm)하여 사용 가능한 것으로 확장할 수 있습니다. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0 <0.8.0; +``` + +  + +### 가져오기 구문 {#import-statements} + +위의 인터페이스 정의 외에도 컨트랙트 정의는 두 개의 다른 파일을 가져옵니다: + +```solidity + +import "../../GSN/Context.sol"; +import "./IERC20.sol"; +import "../../math/SafeMath.sol"; +``` + +- `GSN/Context.sol`은 이더가 없는 사용자가 블록체인을 사용할 수 있도록 하는 시스템인 [OpenGSN](https://www.opengsn.org/)을 사용하기 위해 필요한 정의입니다. 이것은 이전 버전이므로 OpenGSN과 통합하려면 + [이 튜토리얼](https://docs.opengsn.org/javascript-client/tutorial.html)을 사용하세요. +- [SafeMath 라이브러리](https://ethereumdev.io/using-safe-math-library-to-prevent-from-overflows/)는 **<0.8.0** 버전의 솔리디티에서 산술 오버플로우/언더플로우를 방지합니다. 솔리디티 ≥0.8.0에서는 산술 연산이 오버플로우/언더플로우 시 자동으로 되돌려지므로 SafeMath가 불필요합니다. 이 컨트랙트는 이전 컴파일러 버전과의 하위 호환성을 위해 SafeMath를 사용합니다. + +  + +이 주석은 컨트랙트의 목적을 설명합니다. + +```solidity +/** + * @dev {IERC20} 인터페이스의 구현입니다. + * + * 이 구현은 토큰 생성 방식과 무관합니다. 즉, {_mint}를 사용하여 + * 파생된 컨트랙트에 공급 메커니즘을 추가해야 함을 의미합니다. + * 일반적인 메커니즘은 {ERC20PresetMinterPauser}를 참조하세요. + * + * 팁: 자세한 내용은 가이드 + * https://forum.zeppelin.solutions/t/how-to-implement-erc20-supply-mechanisms/226[공급 메커니즘 구현 방법]을 참조하세요. + * + * 일반적인 OpenZeppelin 가이드라인을 따랐습니다. 함수는 실패 시 `false`를 반환하는 대신 + * 되돌려집니다. 이 동작은 관례적이며 ERC20 애플리케이션의 + * 기대와 충돌하지 않습니다. + * + * 또한 {transferFrom}을 호출하면 {Approval} 이벤트가 발생합니다. + * 이를 통해 애플리케이션은 해당 이벤트를 수신하는 것만으로 모든 계정의 허용량을 + * 재구성할 수 있습니다. EIP의 다른 구현에서는 사양에 요구되지 않으므로 + * 이러한 이벤트를 발생시키지 않을 수 있습니다. + * + * 마지막으로, 허용량 설정과 관련된 잘 알려진 문제를 완화하기 위해 + * 비표준 {decreaseAllowance} 및 {increaseAllowance} + * 함수가 추가되었습니다. {IERC20-approve}를 참조하세요. + */ + +``` + +### 컨트랙트 정의 {#contract-definition} + +```solidity +contract ERC20 is Context, IERC20 { +``` + +이 줄은 상속을 지정하며, 이 경우 OpenGSN을 위해 위에서 설명한 `IERC20` 및 `Context`로부터 상속받습니다. + +  + +```solidity + + using SafeMath for uint256; + +``` + +이 줄은 `SafeMath` 라이브러리를 `uint256` 유형에 연결합니다. 이 라이브러리는 +[여기](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol)에서 찾을 수 있습니다. + +### 변수 정의 {#variable-definitions} + +이 정의는 컨트랙트의 상태 변수를 지정합니다. 이 변수들은 `private`으로 선언되었지만, +이는 블록체인의 다른 컨트랙트가 이를 읽을 수 없다는 것을 의미할 뿐입니다. _블록체인에는 +비밀이 없습니다_. 모든 노드의 소프트웨어는 모든 블록에서 모든 컨트랙트의 상태를 가집니다. 관례적으로, 상태 변수는 `_<무엇인가>`로 명명됩니다. + +처음 두 변수는 [매핑](https://www.tutorialspoint.com/solidity/solidity_mappings.htm)으로, +키가 숫자 값이라는 점을 제외하고 [연관 배열](https://wikipedia.org/wiki/Associative_array)과 거의 동일하게 작동합니다. 저장 공간은 기본값(0)과 다른 값을 가진 항목에 대해서만 할당됩니다. + +```solidity + mapping (address => uint256) private _balances; +``` + +첫 번째 매핑인 `_balances`는 주소와 해당 토큰의 잔액입니다. 잔액에 접근하려면 +`_balances[<주소>]` 구문을 사용합니다. + +  + +```solidity + mapping (address => mapping (address => uint256)) private _allowances; +``` + +이 변수 `_allowances`는 앞서 설명한 허용량을 저장합니다. 첫 번째 인덱스는 토큰의 소유자이며, +두 번째 인덱스는 허용량이 부여된 컨트랙트입니다. 주소 A가 주소 B의 계정에서 사용할 수 있는 +금액에 접근하려면 `_allowances[B][A]`를 사용합니다. + +  + +```solidity + uint256 private _totalSupply; +``` + +이름에서 알 수 있듯이 이 변수는 토큰의 총 공급량을 추적합니다. + +  + +```solidity + string private _name; + string private _symbol; + uint8 private _decimals; +``` + +이 세 가지 변수는 가독성을 높이기 위해 사용됩니다. 처음 두 개는 그 자체로 설명이 되지만, `_decimals`는 그렇지 않습니다. + +한편, 이더리움에는 부동 소수점이나 분수 변수가 없습니다. 반면에 +인간은 토큰을 나눌 수 있기를 원합니다. 사람들이 금을 화폐로 정착시킨 한 가지 이유는 누군가 소 한 마리의 가치로 오리 한 마리를 사려고 할 때 거스름돈을 만들기 어려웠기 때문입니다. + +해결책은 정수를 추적하되, 실제 토큰 대신 거의 가치가 없는 +부분 토큰을 세는 것입니다. 이더의 경우 부분 토큰을 wei라고 하며, 10^18 wei는 1 +ETH와 같습니다. 작성 시점에서 10,000,000,000,000 wei는 대략 미국 또는 유로 1센트입니다. + +애플리케이션은 토큰 잔액을 표시하는 방법을 알아야 합니다. 사용자가 3,141,000,000,000,000,000 wei를 가지고 있다면, 그것은 +3.14 ETH일까요? 31.41 ETH일까요? 3,141 ETH일까요? 이더의 경우 ETH에 대해 10^18 wei로 정의되어 있지만, 여러분의 +토큰에 대해서는 다른 값을 선택할 수 있습니다. 토큰을 나누는 것이 의미가 없다면 +`_decimals` 값을 0으로 사용할 수 있습니다. ETH와 동일한 표준을 사용하고 싶다면 **18** 값을 사용하세요. + +### 생성자 {#the-constructor} + +```solidity + /** + * @dev {name} 및 {symbol} 값을 설정하고, {decimals}를 + * 기본값 18로 초기화합니다. + * + * {decimals}에 다른 값을 선택하려면 {_setupDecimals}를 사용하세요. + * + * 이 세 값은 모두 불변입니다. 즉, 생성 중에 + * 한 번만 설정할 수 있습니다. + */ + constructor (string memory name_, string memory symbol_) public { + // 솔리디티 ≥0.7.0에서는 'public'이 암시적이므로 생략할 수 있습니다. + + _name = name_; + _symbol = symbol_; + _decimals = 18; + } +``` + +생성자는 컨트랙트가 처음 생성될 때 호출됩니다. 관례적으로 함수 매개변수는 `_`으로 명명됩니다. + +### 사용자 인터페이스 함수 {#user-interface-functions} + +```solidity + /** + * @dev 토큰의 이름을 반환합니다. + */ + function name() public view returns (string memory) { + return _name; + } + + /** + * @dev 토큰의 심볼을 반환하며, 일반적으로 이름의 + * 축약 버전입니다. + */ + function symbol() public view returns (string memory) { + return _symbol; + } + + /** + * @dev 사용자 표현을 얻는 데 사용되는 소수점 자릿수를 반환합니다. + * 예를 들어, `decimals`가 `2`이면, `505` 토큰의 잔액은 + * 사용자에게 `5,05`(`505 / 10 ** 2`)로 표시되어야 합니다. + * + * 토큰은 보통 이더와 wei 간의 관계를 모방하여 18이라는 값을 + * 선택합니다. 이는 {_setupDecimals}가 호출되지 않는 한 {ERC20}이 사용하는 + * 값입니다. + * + * 참고: 이 정보는 _표시_ 목적으로만 사용되며, + * {IERC20-balanceOf} 및 {IERC20-transfer}를 포함한 컨트랙트의 어떠한 + * 산술 연산에도 영향을 미치지 않습니다. + */ + function decimals() public view returns (uint8) { + return _decimals; + } +``` + +`name`, `symbol`, `decimals` 함수는 사용자 인터페이스가 컨트랙트에 대해 알 수 있도록 하여 제대로 표시할 수 있도록 돕습니다. + +반환 유형은 `string memory`이며, 이는 메모리에 저장된 문자열을 반환함을 의미합니다. 문자열과 같은 변수는 +세 위치에 저장될 수 있습니다: + +| | 수명 | 컨트랙트 액세스 | 가스 비용 | +| ---- | ------- | -------- | --------------------------------------------- | +| 메모리 | 함수 호출 | 읽기/쓰기 | 수십 또는 수백 (더 높은 위치일수록 더 높음) | +| 콜데이터 | 함수 호출 | 읽기 전용 | 반환 유형으로 사용할 수 없으며, 함수 매개변수 유형으로만 사용 가능 | +| 스토리지 | 변경될 때까지 | 읽기/쓰기 | 높음 (읽기 800, 쓰기 20k) | + +이 경우 `memory`가 가장 좋은 선택입니다. + +### 토큰 정보 읽기 {#read-token-information} + +이 함수들은 총 공급량 또는 계정의 잔액과 같은 토큰에 대한 정보를 제공합니다. + +```solidity + /** + * @dev {IERC20-totalSupply}를 참조하세요. + */ + function totalSupply() public view override returns (uint256) { + return _totalSupply; + } +``` + +`totalSupply` 함수는 토큰의 총 공급량을 반환합니다. + +  + +```solidity + /** + * @dev {IERC20-balanceOf}를 참조하세요. + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } +``` + +계정의 잔액을 읽습니다. 누구나 다른 사람의 계정 잔액을 조회할 수 있음에 유의하세요. 이 정보는 모든 노드에서 확인할 수 있으므로 숨기려고 해도 소용없습니다. _블록체인에는 비밀이 없습니다._ + +### 토큰 전송 {#transfer-tokens} + +```solidity + /** + * @dev {IERC20-transfer}를 참조하세요. + * + * 요구 사항: + * + * - `수신자`는 0 주소일 수 없습니다. + * - 호출자는 최소 `금액`의 잔액을 가지고 있어야 합니다. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { +``` + +`transfer` 함수는 보낸 사람의 계정에서 다른 계정으로 토큰을 전송하기 위해 호출됩니다. 부울 값을 반환하지만 그 값은 항상 **true**라는 점에 유의하세요. 전송이 실패하면 컨트랙트는 호출을 되돌립니다. + +  + +```solidity + _transfer(_msgSender(), recipient, amount); + return true; + } +``` + +`_transfer` 함수가 실제 작업을 수행합니다. 이 함수는 다른 컨트랙트 함수에서만 호출할 수 있는 비공개 함수입니다. 관례적으로 비공개 함수는 상태 변수와 마찬가지로 `_`으로 명명됩니다. + +일반적으로 솔리디티에서는 메시지 발신자에 대해 `msg.sender`를 사용합니다. 그러나 이는 [OpenGSN](http://opengsn.org/)에서 문제를 일으킵니다. 토큰으로 이더 없는 트랜잭션을 허용하려면 `_msgSender()`를 사용해야 합니다. 일반 트랜잭션의 경우 `msg.sender`를 반환하지만 이더가 없는 트랜잭션의 경우 메시지를 중계한 컨트랙트가 아닌 원래 서명자를 반환합니다. + +### 허용량 함수 {#allowance-functions} + +다음은 허용량 기능을 구현하는 함수입니다: `allowance`, `approve`, `transferFrom`, `_approve`. 또한 OpenZeppelin 구현은 기본 표준을 넘어 보안을 개선하는 몇 가지 기능인 `increaseAllowance` 및 `decreaseAllowance`를 포함합니다. + +#### allowance 함수 {#allowance} + +```solidity + /** + * @dev {IERC20-allowance}를 참조하세요. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } +``` + +`allowance` 함수를 사용하면 누구나 허용량을 확인할 수 있습니다. + +#### approve 함수 {#approve} + +```solidity + /** + * @dev {IERC20-approve}를 참조하세요. + * + * 요구 사항: + * + * - `spender`는 0 주소일 수 없습니다. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { +``` + +이 함수는 허용량을 생성하기 위해 호출됩니다. 위의 `transfer` 함수와 유사합니다: + +- 이 함수는 실제 작업을 수행하는 내부 함수(이 경우 `_approve`)를 호출할 뿐입니다. +- 함수는 `true`를 반환하거나(성공 시) 되돌립니다(실패 시). + +  + +```solidity + _approve(_msgSender(), spender, amount); + return true; + } +``` + +상태 변경이 발생하는 곳의 수를 최소화하기 위해 내부 함수를 사용합니다. 상태를 변경하는 _모든_ 함수는 보안 감사가 필요한 잠재적인 보안 위험입니다. 이렇게 하면 실수를 할 가능성이 줄어듭니다. + +#### transferFrom 함수 {#transferFrom} + +지출자가 허용량을 사용하기 위해 호출하는 함수입니다. 여기에는 두 가지 작업이 필요합니다. 사용된 금액을 전송하고 그 금액만큼 허용량을 줄이는 것입니다. + +```solidity + /** + * @dev {IERC20-transferFrom}를 참조하세요. + * + * 업데이트된 허용량을 나타내는 {Approval} 이벤트를 발생시킵니다. 이는 + * EIP에서 요구하지 않습니다. {ERC20}의 시작 부분에 있는 참고 사항을 참조하세요. + * + * 요구 사항: + * + * - `sender` 및 `recipient`는 0 주소일 수 없습니다. + * - `sender`는 최소 `amount`의 잔액을 가지고 있어야 합니다. + * - 호출자는 ``sender``의 토큰에 대해 최소 + * `amount`의 허용량을 가지고 있어야 합니다. + */ + function transferFrom(address sender, address recipient, uint256 amount) public virtual + override returns (bool) { + _transfer(sender, recipient, amount); +``` + +  + +`a.sub(b, "메시지")` 함수 호출은 두 가지 작업을 수행합니다. 첫째, 새로운 허용량인 `a-b`를 계산합니다. +둘째, 이 결과가 음수가 아닌지 확인합니다. 음수이면 제공된 메시지와 함께 호출이 되돌려집니다. 호출이 되돌려질 때 해당 호출 중에 이전에 수행된 모든 처리는 무시되므로 `_transfer`를 되돌릴 필요가 없다는 점에 유의하세요. + +```solidity + _approve(sender, _msgSender(), _allowances[sender][_msgSender()].sub(amount, + "ERC20: 전송 금액이 허용량을 초과합니다")); + return true; + } +``` + +#### OpenZeppelin 안전 추가 사항 {#openzeppelin-safety-additions} + +0이 아닌 허용량을 다른 0이 아닌 값으로 설정하는 것은 위험합니다. 자신의 트랜잭션 순서만 제어할 수 있고 다른 사람의 트랜잭션 순서는 제어할 수 없기 때문입니다. 순진한 앨리스와 부정직한 빌이라는 두 사용자가 있다고 상상해 보세요. 앨리스는 빌에게서 어떤 서비스를 원하는데, 그 비용이 5 토큰이라고 생각해서 빌에게 5 토큰의 허용량을 줍니다. + +그런 다음 상황이 바뀌어 빌의 가격이 10 토큰으로 오릅니다. 여전히 서비스를 원하는 앨리스는 빌의 허용량을 10으로 설정하는 트랜잭션을 보냅니다. 빌은 트랜잭션 풀에서 이 새로운 트랜잭션을 보는 순간 앨리스의 5 토큰을 사용하는 트랜잭션을 보내고 훨씬 더 높은 가스 가격을 설정하여 더 빨리 채굴되도록 합니다. 그렇게 하면 빌은 먼저 5 토큰을 사용하고, 앨리스의 새로운 허용량이 채굴되면 10 토큰을 더 사용하여 총 15 토큰을 사용할 수 있습니다. 이는 앨리스가 승인하려던 것보다 많은 금액입니다. 이 기술을 [선행매매](https://consensysdiligence.github.io/smart-contract-best-practices/attacks/#front-running)라고 합니다. + +| 앨리스의 트랜잭션 | 앨리스 Nonce | 빌 트랜잭션 | 빌 Nonce | 빌의 허용량 | 앨리스로부터의 빌 총수입 | +| ------------------------------------ | --------- | ------------------------------------------------ | ------- | ------ | ------------- | +| approve(Bill, 5) | 10 | | | 5 | 0 | +| | | transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | +| approve(Bill, 10) | 11 | | | 10 | 5 | +| | | transferFrom(Alice, Bill, 10) | 10,124 | 0 | 15 | + +이 문제를 피하기 위해, 이 두 함수(`increaseAllowance`와 `decreaseAllowance`)를 사용하면 허용량을 특정 금액만큼 수정할 수 있습니다. 따라서 빌이 이미 5개의 토큰을 사용했다면, 그는 5개만 더 사용할 수 있을 것입니다. 타이밍에 따라 두 가지 방식으로 작동할 수 있으며, 두 가지 모두 빌이 10개의 토큰만 얻는 것으로 끝납니다: + +A: + +| 앨리스의 트랜잭션 | 앨리스 Nonce | 빌 트랜잭션 | 빌 Nonce | 빌의 허용량 | 앨리스로부터의 빌 총수입 | +| --------------------------------------------- | --------: | ----------------------------------------------- | ------: | ------: | ------------- | +| approve(Bill, 5) | 10 | | | 5 | 0 | +| | | transferFrom(Alice, Bill, 5) | 10,123 | 0 | 5 | +| increaseAllowance(Bill, 5) | 11 | | | 0+5 = 5 | 5 | +| | | transferFrom(Alice, Bill, 5) | 10,124 | 0 | 10 | + +B: + +| 앨리스의 트랜잭션 | 앨리스 Nonce | 빌 트랜잭션 | 빌 Nonce | 빌의 허용량 | 앨리스로부터의 빌 총수입 | +| --------------------------------------------- | --------: | ------------------------------------------------ | ------: | -------: | ------------: | +| approve(Bill, 5) | 10 | | | 5 | 0 | +| increaseAllowance(Bill, 5) | 11 | | | 5+5 = 10 | 0 | +| | | transferFrom(Alice, Bill, 10) | 10,124 | 0 | 10 | + +```solidity + /** + * @dev 호출자에 의해 `spender`에게 부여된 허용량을 원자적으로 증가시킵니다. + * + * 이것은 {IERC20-approve}에 설명된 문제에 대한 완화책으로 사용할 수 있는 {approve}의 대안입니다. + * + * 업데이트된 허용량을 나타내는 {Approval} 이벤트를 발생시킵니다. + * + * 요구 사항: + * + * - `spender`는 0 주소일 수 없습니다. + */ + function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) { + _approve(_msgSender(), spender, _allowances[_msgSender()][spender].add(addedValue)); + return true; + } +``` + +관례적으로 상태 변수는 `_`으로 명명됩니다. 첫 두 변수는 [매핑](https://www.tutorialspoint.com/solidity/solidity_mappings.htm)으로, 키가 숫자 값이라는 점을 제외하고는 [연관 배열](https://wikipedia.org/wiki/Associative_array)과 거의 동일하게 동작합니다. + +저장 공간은 기본값(0)과 다른 값을 가진 항목에만 할당됩니다. + +```solidity + mapping (address => uint256) private _balances; +``` + +첫 번째 매핑인 `_balances`는 주소와 해당 토큰의 잔액입니다. + +#### 잔액에 접근하려면 `_balances[
]` 구문을 사용합니다. + +```solidity + mapping (address => mapping (address => uint256)) private _allowances; +``` + +이 변수 `_allowances`는 앞에서 설명한 허용량을 저장합니다. 첫 번째 인덱스는 토큰의 소유자이고, 두 번째는 허용량이 있는 계약입니다. + +  + +```solidity + uint256 private _totalSupply; +``` + +이름에서 알 수 있듯이, 이 변수는 토큰의 총 공급량을 추적합니다. string private _name; +string private _symbol; +uint8 private _decimals; + +  + +```solidity +처음 두 개는 자명하지만 `_decimals`는 그렇지 않습니다. +``` + +한편으로, 이더리움에는 부동 소수점이나 분수 변수가 없습니다. + +1. 다른 한편으로, 인간은 토큰을 나눌 수 있는 것을 좋아합니다. +2. 사람들이 통화로 금을 선택한 이유 중 하나는 누군가가 소 한 마리 값의 오리를 사려고 할 때 거스름돈을 만들기가 어려웠기 때문입니다. + +해결책은 정수를 추적하는 것이지만, 실제 토큰 대신 거의 가치가 없는 분수 토큰을 세는 것입니다. 이더의 경우, 분수 토큰은 wei라고 불리며, 10^18 wei는 1 ETH와 같습니다. + +글을 쓰는 시점에서 10,000,000,000,000 wei는 대략 1 미국 센트 또는 유로 센트입니다. 애플리케이션은 토큰 잔액을 어떻게 표시해야 하는지 알아야 합니다. 사용자가 3,141,000,000,000,000,000 wei를 가지고 있다면, 그것은 3.14 ETH인가요? 31.41 ETH인가요? + +  + +```solidity +이더의 경우 ETH에 대해 10^18 wei로 정의되지만, 토큰에 대해서는 다른 값을 선택할 수 있습니다. +``` + +토큰을 나누는 것이 의미가 없다면 `_decimals` 값을 0으로 사용할 수 있습니다. ETH와 동일한 표준을 사용하려면 값 **18**을 사용하세요. + +### 생성자 {#the-constructor} + +```solidity + /** + * @dev {name}과 {symbol}의 값을 설정하고, {decimals}를 기본값인 18로 초기화합니다. + * + * {decimals}에 대해 다른 값을 선택하려면 {_setupDecimals}를 사용하세요. + * + * 이 세 가지 값은 모두 불변입니다: 생성 중에 한 번만 설정할 수 있습니다. + */ + constructor (string memory name_, string memory symbol_) public { + // 솔리디티 ≥0.7.0에서는 'public'이 암시적이므로 생략할 수 있습니다. + _name = name_; + _symbol = symbol_; + _decimals = 18; + } +``` + +  + +```solidity +관례적으로 함수 매개변수는 `_`로 명명됩니다. +``` + +### 사용자 인터페이스 함수 {#user-interface-functions} + +```solidity + /** + * @dev 토큰의 이름을 반환합니다. + */ + function name() public view returns (string memory) { + return _name; + } +``` + +```solidity + /** + * @dev 토큰의 기호를 반환하며, 보통 이름의 짧은 버전입니다. + */ + function symbol() public view returns (string memory) { + return _symbol; + } +``` + +```solidity + /** + * @dev 사용자 표현을 얻는 데 사용되는 소수점 자릿수를 반환합니다. + * 예를 들어, `decimals`가 `2`이면, `505` 토큰의 잔액은 + * 사용자에게 `5,05`(`505 / 10 ** 2`)로 표시되어야 합니다. + * + * 토큰은 보통 18의 값을 선택하며, 이는 이더와 wei의 관계를 모방한 것입니다. 이는 {_setupDecimals}가 호출되지 않는 한 + * {ERC20}이 사용하는 값입니다. + * + * 참고: 이 정보는 _표시_ 목적으로만 사용됩니다: 계약의 어떤 + * 산술에도 영향을 미치지 않으며, {IERC20-balanceOf} 및 {IERC20-transfer}를 포함합니다. + */ + function decimals() public view returns (uint8) { + return _decimals; + } +``` + +이 함수들, `name`, `symbol`, `decimals`는 사용자 인터페이스가 계약에 대해 알 수 있도록 도와주어 제대로 표시할 수 있게 합니다. + +#### 반환 유형은 `string memory`로, 메모리에 저장된 문자열을 반환한다는 의미입니다. + +문자열과 같은 변수는 세 곳에 저장될 수 있습니다: +수명 + +계약 액세스 +가스 비용 메모리 + +```solidity +함수 호출 +``` + +읽기/쓰기 + +  + +```solidity +읽기 전용 +``` + +반환 유형으로 사용할 수 없고, 함수 매개변수 유형으로만 사용 가능 + +#### 변경될 때까지 + +높음 (읽기 800, 쓰기 20k) 이 경우, `memory`가 최선의 선택입니다. + +### 토큰 정보 읽기 {#read-token-information} + +이 함수들은 총 공급량이나 계정 잔액과 같은 토큰에 대한 정보를 제공합니다. + +`totalSupply` 함수는 토큰의 총 공급량을 반환합니다. + +  + +```solidity + /** + * @dev {IERC20-balanceOf}를 참조하세요. + */ + function balanceOf(address account) public view override returns (uint256) { + return _balances[account]; + } +``` + +계정의 잔액을 읽습니다. + +### 누구나 다른 사람의 계정 잔액을 가져올 수 있다는 점에 유의하세요. + +이 정보는 어쨌든 모든 노드에서 사용할 수 있으므로 숨기려고 해도 소용이 없습니다. _블록체인에는 비밀이 없습니다._ + +### 토큰 전송하기 {#transfer-tokens} + +```solidity + /** + * @dev {IERC20-transfer}를 참조하세요. + * + * 요구 사항: + * + * - `recipient`는 0 주소일 수 없습니다. + * - 호출자는 최소 `amount`의 잔액을 가지고 있어야 합니다. + */ + function transfer(address recipient, uint256 amount) public virtual override returns (bool) { +``` + +### 훅 {#hooks} + +이 함수는 부울 값을 반환하지만, 그 값은 항상 **true**라는 점에 유의하세요. 전송이 실패하면 계약은 호출을 되돌립니다. + +```solidity + _transfer(_msgSender(), recipient, amount); + return true; + } +``` + +## 결론 {#conclusion} + +이것은 다른 계약 함수에 의해서만 호출될 수 있는 비공개 함수입니다. + +- _이더리움 상에 비공개 정보란 없습니다_. 일반적으로 솔리디티에서는 메시지 발신인으로 `msg.sender`를 사용합니다. +- 하지만 이는 [OpenGSN](http://opengsn.org/)을 깨뜨립니다. 토큰으로 이더 없는 트랜잭션을 허용하려면 `_msgSender()`를 사용해야 합니다. +- 이 함수는 일반 트랜잭션의 경우 `msg.sender`를 반환하지만, 이더 없는 트랜잭션의 경우 메시지를 중계한 계약이 아닌 원래 서명자를 반환합니다. + +### 허용량 함수 {#allowance-functions} + +이 함수들은 허용량 기능을 구현하는 함수들입니다: `allowance`, `approve`, `transferFrom`, 그리고 `_approve`. 또한, OpenZeppelin 구현은 기본 표준을 넘어 보안을 향상시키는 몇 가지 기능, 즉 `increaseAllowance`와 `decreaseAllowance`를 포함합니다. + +#### allowance 함수 {#allowance} + +```solidity + /** + * @dev {IERC20-allowance}를 참조하세요. + */ + function allowance(address owner, address spender) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } +``` + +`allowance` 함수는 누구나 어떤 허용량이든 확인할 수 있게 해줍니다. + +#### approve 함수 {#approve} + +```solidity + /** + * @dev {IERC20-approve}를 참조하세요. + * + * 요구 사항: + * + * - `spender`는 0 주소일 수 없습니다. + */ + function approve(address spender, uint256 amount) public virtual override returns (bool) { +``` + +이 함수는 허용량을 생성하기 위해 호출됩니다. + +위의 `transfer` 함수와 비슷합니다. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). diff --git a/public/content/translations/ko/developers/tutorials/erc20-with-safety-rails/index.md b/public/content/translations/ko/developers/tutorials/erc20-with-safety-rails/index.md new file mode 100644 index 00000000000..b03508299d2 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/erc20-with-safety-rails/index.md @@ -0,0 +1,217 @@ +--- +title: "안전장치가 있는 ERC-20" +description: "사람들이 사소한 실수를 피하도록 돕는 방법" +author: Ori Pomerantz +lang: ko +tags: [ "erc-20" ] +skill: beginner +published: 2022-08-15 +--- + +## 소개 {#introduction} + +이더리움의 가장 큰 장점 중 하나는 트랜잭션을 수정하거나 취소할 수 있는 중앙 기관이 없다는 것입니다. 이더리움의 큰 문제 중 하나는 사용자 실수나 불법적인 트랜잭션을 취소할 권한을 가진 중앙 기관이 없다는 것입니다. 이 글에서는 사용자가 [ERC-20](/developers/docs/standards/tokens/erc-20/) 토큰으로 저지르는 일반적인 실수와 사용자가 이러한 실수를 피하도록 돕거나 중앙 기관에 일부 권한(예: 계정 동결)을 부여하는 ERC-20 계약을 만드는 방법에 대해 알아봅니다. + +[OpenZeppelin ERC-20 토큰 계약](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/token/ERC20)을 사용하지만 이 글에서는 자세히 설명하지 않습니다. 이 정보는 [여기](/developers/tutorials/erc20-annotated-code)에서 찾을 수 있습니다. + +전체 소스 코드를 보려면: + +1. [Remix IDE](https://remix.ethereum.org/)를 엽니다. +2. 깃허브 복제 아이콘(![깃허브 복제 아이콘](icon-clone.png))을 클릭합니다. +3. 깃허브 리포지토리 `https://github.com/qbzzt/20220815-erc20-safety-rails`를 복제합니다. +4. **contracts > erc20-safety-rails.sol**을 엽니다. + +## ERC-20 계약 생성 {#creating-an-erc-20-contract} + +안전장치 기능을 추가하기 전에 ERC-20 계약이 필요합니다. 이 글에서는 [OpenZeppelin 계약 마법사](https://docs.openzeppelin.com/contracts/5.x/wizard)를 사용합니다. 다른 브라우저에서 열고 다음 지침을 따르세요. + +1. **ERC20**을 선택합니다. + +2. 다음 설정을 입력하세요. + + | 매개 변수 | 값 | + | -------- | ---------------- | + | 이름 | SafetyRailsToken | + | 기호 | SAFE | + | Premint | 1000 | + | 기능 | 없음 | + | 액세스 제어 | Ownable | + | 업그레이드 기능 | 없음 | + +3. 위로 스크롤하여 **Remix에서 열기**(Remix용)를 클릭하거나 **다운로드**를 클릭하여 다른 환경을 사용합니다. Remix를 사용한다고 가정하고, 다른 것을 사용한다면 적절하게 변경하세요. + +4. 이제 완전히 작동하는 ERC-20 계약이 있습니다. `.deps` > `npm`을 확장하여 가져온 코드를 볼 수 있습니다. + +5. 계약을 컴파일하고 배포하고 테스트하여 ERC-20 계약으로 작동하는지 확인합니다. Remix 사용법을 배워야 한다면 [이 튜토리얼](https://remix.ethereum.org/?#activate=udapp,solidity,LearnEth)을 사용하세요. + +## 일반적인 실수 {#common-mistakes} + +### 실수 {#the-mistakes} + +사용자는 때때로 잘못된 주소로 토큰을 보냅니다. 사용자가 무엇을 하려고 했는지 마음을 읽을 수는 없지만, 자주 발생하고 쉽게 감지할 수 있는 두 가지 오류 유형이 있습니다. + +1. 계약 자체 주소로 토큰 보내기. 예를 들어, [옵티미즘의 OP 토큰](https://optimism.mirror.xyz/qvd0WfuLKnePm1Gxb9dpGchPf5uDz5NSMEFdgirDS4c)은 두 달도 채 되지 않아 [120,000개 이상의](https://optimism.blockscout.com/address/0x4200000000000000000000000000000000000042) OP 토큰을 축적했습니다. 이는 아마도 사람들이 그냥 잃어버린 상당한 양의 자산을 나타냅니다. + +2. [외부 소유 계정](/developers/docs/accounts/#externally-owned-accounts-and-key-pairs) 또는 [스마트 계약](/developers/docs/smart-contracts)에 해당하지 않는 빈 주소로 토큰을 보냅니다. 이런 일이 얼마나 자주 발생하는지에 대한 통계는 없지만, [한 사건으로 20,000,000개의 토큰 손실이 발생할 뻔했습니다](https://gov.optimism.io/t/message-to-optimism-community-from-wintermute/2595). + +### 전송 방지 {#preventing-transfers} + +OpenZeppelin ERC-20 계약에는 토큰이 전송되기 전에 호출되는 [훅, `_beforeTokenTransfer`](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol#L364-L368)가 포함되어 있습니다. 기본적으로 이 훅은 아무것도 하지 않지만, 문제가 있는 경우 되돌리는 확인과 같은 자체 기능을 추가할 수 있습니다. + +훅을 사용하려면 생성자 뒤에 이 함수를 추가하세요. + +```solidity + function _beforeTokenTransfer(address from, address to, uint256 amount) + internal virtual + override(ERC20) + { + super._beforeTokenTransfer(from, to, amount); + } +``` + +솔리디티에 익숙하지 않다면 이 함수의 일부가 새로울 수 있습니다. + +```solidity + internal virtual +``` + +`virtual` 키워드는 `ERC20`에서 기능을 상속하고 이 함수를 재정의한 것처럼 다른 계약이 우리에게서 상속하고 이 함수를 재정의할 수 있음을 의미합니다. + +```solidity + override(ERC20) +``` + +`_beforeTokenTransfer`의 ERC20 토큰 정의를 [재정의](https://docs.soliditylang.org/en/v0.8.15/contracts.html#function-overriding)하고 있음을 명시적으로 지정해야 합니다. 일반적으로 보안 관점에서는 명시적 정의가 암시적 정의보다 훨씬 낫습니다. 바로 앞에 있으면 무언가를 했다는 사실을 잊을 수 없습니다. 이것이 또한 우리가 재정의하는 슈퍼클래스의 `_beforeTokenTransfer`를 지정해야 하는 이유입니다. + +```solidity + super._beforeTokenTransfer(from, to, amount); +``` + +이 줄은 우리가 상속한 계약의 `_beforeTokenTransfer` 함수를 호출합니다. 이 경우 `ERC20`만 해당하며, `Ownable`에는 이 훅이 없습니다. 현재 `ERC20._beforeTokenTransfer`는 아무것도 하지 않지만, 나중에 기능이 추가될 경우를 대비하여 호출합니다(그리고 계약은 배포 후 변경되지 않기 때문에 계약을 다시 배포하기로 결정합니다). + +### 요구 사항 코딩 {#coding-the-requirements} + +함수에 다음 요구 사항을 추가하려고 합니다. + +- `to` 주소는 ERC-20 계약 자체의 주소인 `address(this)`와 같을 수 없습니다. +- `to` 주소는 비어 있을 수 없으며, 다음 중 하나여야 합니다. + - 외부 소유 계정(EOA). 주소가 EOA인지 직접 확인할 수는 없지만, 주소의 ETH 잔액을 확인할 수 있습니다. EOA는 더 이상 사용되지 않더라도 거의 항상 잔액이 있습니다. 마지막 wei까지 비우기는 어렵습니다. + - 스마트 계약. 주소가 스마트 계약인지 테스트하는 것은 조금 더 어렵습니다. 외부 코드 길이를 확인하는 [`EXTCODESIZE`](https://www.evm.codes/#3b)라는 opcode가 있지만 솔리디티에서 직접 사용할 수는 없습니다. 이를 위해 EVM 어셈블리인 [Yul](https://docs.soliditylang.org/en/v0.8.15/yul.html)을 사용해야 합니다. 솔리디티에서 사용할 수 있는 다른 값([`
.code` 및 `
.codehash`](https://docs.soliditylang.org/en/v0.8.15/units-and-global-variables.html#members-of-address-types))이 있지만, 비용이 더 많이 듭니다. + +새 코드를 한 줄씩 살펴보겠습니다. + +```solidity + require(to != address(this), "토큰을 계약 주소로 보낼 수 없습니다"); +``` + +이것이 첫 번째 요구 사항입니다. `to`와 `this(address)`가 같지 않은지 확인합니다. + +```solidity + bool isToContract; + assembly { + isToContract := gt(extcodesize(to), 0) + } +``` + +이렇게 주소가 계약인지 확인합니다. Yul에서 직접 출력을 받을 수 없으므로 대신 결과를 저장할 변수(`isToContract`)를 정의합니다. Yul의 작동 방식은 모든 opcode가 함수로 간주된다는 것입니다. 따라서 먼저 [`EXTCODESIZE`](https://www.evm.codes/#3b)를 호출하여 계약 크기를 가져온 다음, [`GT`](https://www.evm.codes/#11)를 사용하여 0이 아닌지 확인합니다(부호 없는 정수를 다루므로 물론 음수가 될 수 없습니다). 그런 다음 결과를 `isToContract`에 씁니다. + +```solidity + require(to.balance != 0 || isToContract, "토큰을 빈 주소로 보낼 수 없습니다"); +``` + +그리고 마지막으로, 빈 주소에 대한 실제 확인이 있습니다. + +## 관리자 액세스 {#admin-access} + +때로는 실수를 되돌릴 수 있는 관리자가 있는 것이 유용합니다. 악용 가능성을 줄이기 위해 이 관리자는 [다중서명](https://blog.logrocket.com/security-choices-multi-signature-wallets/)이 될 수 있으므로 여러 사람이 조치에 동의해야 합니다. 이 글에서는 두 가지 관리 기능을 다룹니다. + +1. 계정 동결 및 해제. 예를 들어 계정이 손상되었을 수 있을 때 유용할 수 있습니다. +2. 자산 정리. + + 때로는 사기꾼이 합법성을 얻기 위해 사기 토큰을 실제 토큰의 계약으로 보냅니다. 예를 들어, [여기](https://optimism.blockscout.com/token/0x2348B1a1228DDCd2dB668c3d30207c3E1852fBbe?tab=holders)를 참조하세요. 합법적인 ERC-20 계약은 [0x4200....0042](https://optimism.blockscout.com/token/0x4200000000000000000000000000000000000042)입니다. 그것을 사칭하는 사기는 [0x234....bbe](https://optimism.blockscout.com/token/0x2348B1a1228DDCd2dB668c3d30207c3E1852fBbe)입니다. + + 사람들이 실수로 합법적인 ERC-20 토큰을 우리 계약에 보낼 수도 있는데, 이는 토큰을 꺼낼 방법을 원하는 또 다른 이유입니다. + +OpenZeppelin은 관리자 액세스를 활성화하는 두 가지 메커니즘을 제공합니다. + +- [`Ownable`](https://docs.openzeppelin.com/contracts/5.x/access-control#ownership-and-ownable) 계약에는 단일 소유자가 있습니다. `onlyOwner` [제어자](https://www.tutorialspoint.com/solidity/solidity_function_modifiers.htm)가 있는 함수는 해당 소유자만 호출할 수 있습니다. 소유자는 소유권을 다른 사람에게 이전하거나 완전히 포기할 수 있습니다. 다른 모든 계정의 권한은 일반적으로 동일합니다. +- [`AccessControl`](https://docs.openzeppelin.com/contracts/5.x/access-control#role-based-access-control) 계약에는 [역할 기반 접근 제어(RBAC)](https://en.wikipedia.org/wiki/Role-based_access_control)가 있습니다. + +간단하게 하기 위해 이 글에서는 `Ownable`을 사용합니다. + +### 계약 동결 및 해제 {#freezing-and-thawing-contracts} + +계약을 동결하고 해제하려면 몇 가지 변경이 필요합니다. + +- 어떤 주소가 동결되었는지 추적하기 위한 주소에서 [부울](https://en.wikipedia.org/wiki/Boolean_data_type)로의 [매핑](https://www.tutorialspoint.com/solidity/solidity_mappings.htm). 모든 값은 초기에 0이며, 부울 값의 경우 거짓으로 해석됩니다. 기본적으로 계정은 동결되지 않기 때문에 이것이 우리가 원하는 것입니다. + + ```solidity + mapping(address => bool) public frozenAccounts; + ``` + +- 계정이 동결되거나 해제될 때 관심 있는 사람에게 알리는 [이벤트](https://www.tutorialspoint.com/solidity/solidity_events.htm). 기술적으로 말해서 이러한 작업에는 이벤트가 필요하지 않지만, 오프체인 코드가 이러한 이벤트를 수신하고 무슨 일이 일어나고 있는지 알 수 있도록 도와줍니다. 스마트 계약이 다른 사람과 관련이 있을 수 있는 일이 발생했을 때 이벤트를 내보내는 것은 좋은 매너로 간주됩니다. + + 이벤트는 인덱싱되므로 계정이 동결되거나 해제된 모든 시간을 검색할 수 있습니다. + + ```solidity + // 계정이 동결되거나 해제될 때 + event AccountFrozen(address indexed _addr); + event AccountThawed(address indexed _addr); + ``` + +- 계정 동결 및 해제 기능. 이 두 함수는 거의 동일하므로 동결 함수만 살펴보겠습니다. + + ```solidity + function freezeAccount(address addr) + public + onlyOwner + ``` + + [`public`](https://www.tutorialspoint.com/solidity/solidity_contracts.htm)으로 표시된 함수는 다른 스마트 계약에서 또는 트랜잭션에 의해 직접 호출될 수 있습니다. + + ```solidity + { + require(!frozenAccounts[addr], "계정이 이미 동결되었습니다"); + frozenAccounts[addr] = true; + emit AccountFrozen(addr); + } // freezeAccount + ``` + + 계정이 이미 동결된 경우 되돌립니다. 그렇지 않으면 동결하고 이벤트를 `emit`합니다. + +- 동결된 계정에서 자금이 이동되는 것을 방지하기 위해 `_beforeTokenTransfer`를 변경합니다. 자금은 여전히 동결된 계정으로 이체될 수 있습니다. + + ```solidity + require(!frozenAccounts[from], "계정이 동결되었습니다"); + ``` + +### 자산 정리 {#asset-cleanup} + +이 계약이 보유한 ERC-20 토큰을 해제하려면 해당 토큰이 속한 토큰 계약에서 [`transfer`](https://eips.ethereum.org/EIPS/eip-20#transfer) 또는 [`approve`](https://eips.ethereum.org/EIPS/eip-20#approve) 함수를 호출해야 합니다. 이 경우 허용량에 가스를 낭비할 필요가 없으므로 직접 전송하는 것이 좋습니다. + +```solidity + function cleanupERC20( + address erc20, + address dest + ) + public + onlyOwner + { + IERC20 token = IERC20(erc20); +``` + +이것은 주소를 받았을 때 계약에 대한 객체를 만드는 구문입니다. 소스 코드의 일부로 ERC20 토큰에 대한 정의가 있고(4행 참조), 해당 파일에 OpenZeppelin ERC-20 계약의 인터페이스인 [IERC20에 대한 정의](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/IERC20.sol)가 포함되어 있기 때문에 이 작업을 수행할 수 있습니다. + +```solidity + uint balance = token.balanceOf(address(this)); + token.transfer(dest, balance); + } +``` + +이것은 정리 기능이므로 아마도 토큰을 남기고 싶지 않을 것입니다. 사용자로부터 수동으로 잔액을 가져오는 대신 프로세스를 자동화하는 것이 좋습니다. + +## 결론 {#conclusion} + +이것은 완벽한 해결책이 아닙니다. "사용자가 실수를 했다" 문제에 대한 완벽한 해결책은 없습니다. 그러나 이러한 종류의 확인을 사용하면 적어도 일부 실수를 방지할 수 있습니다. 계정을 동결하는 기능은 위험하지만 해커에게 도난당한 자금을 거부함으로써 특정 해킹의 피해를 제한하는 데 사용할 수 있습니다. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). diff --git a/public/content/translations/ko/developers/tutorials/ethereum-for-web2-auth/index.md b/public/content/translations/ko/developers/tutorials/ethereum-for-web2-auth/index.md new file mode 100644 index 00000000000..7850826d9cc --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/ethereum-for-web2-auth/index.md @@ -0,0 +1,886 @@ +--- +title: "웹2 인증에 이더리움 사용하기" +description: "이 튜토리얼을 읽고 나면, 개발자는 이더리움 로그인(웹3)을 웹2에서 SSO(Single Sign-On) 및 기타 관련 서비스를 제공하는 데 사용되는 표준인 SAML 로그인과 통합할 수 있게 될 것입니다. 이를 통해 이더리움 서명을 통해 웹2 리소스에 대한 액세스를 인증할 수 있으며, 사용자 속성은 인증에서 가져옵니다." +author: Ori Pomerantz +tags: [ "웹2", "인증", "eas" ] +skill: beginner +lang: ko +published: 2025-04-30 +--- + +## 소개 + +[SAML](https://www.onelogin.com/learn/saml)은 웹2에서 [ID 공급자(IdP)](https://en.wikipedia.org/wiki/Identity_provider#SAML_identity_provider)가 [서비스 공급자(SP)](https://en.wikipedia.org/wiki/Service_provider_\(SAML\))에 대한 사용자 정보를 제공할 수 있도록 하는 데 사용되는 표준입니다. + +이 튜토리얼에서는 이더리움 서명을 SAML과 통합하여 사용자가 아직 이더리움을 기본적으로 지원하지 않는 웹2 서비스에 대해 이더리움 지갑을 사용하여 자신을 인증할 수 있도록 하는 방법을 배웁니다. + +이 튜토리얼은 두 가지 별개의 독자 그룹을 위해 작성되었습니다. + +- 이더리움을 이해하고 SAML을 배워야 하는 이더리움 사용자 +- SAML 및 웹2 인증을 이해하고 이더리움을 배워야 하는 웹2 사용자 + +따라서 이미 알고 있는 많은 입문 자료가 포함될 것입니다. 자유롭게 건너뛰어도 좋습니다. + +### 이더리움 사용자를 위한 SAML + +SAML은 중앙화된 프로토콜입니다. 서비스 공급자(SP)는 해당 ID 공급자(IdP) 또는 해당 IdP의 인증서에 서명한 [인증 기관](https://www.ssl.com/article/what-is-a-certificate-authority-ca/)과 사전 신뢰 관계가 있는 경우에만 ID 공급자로부터 어설션(예: "이 사용자는 John이고, 그는 A, B, C를 수행할 수 있는 권한을 가져야 합니다")을 수락합니다. + +예를 들어, SP는 회사에 여행 서비스를 제공하는 여행사일 수 있고 IdP는 회사의 내부 웹사이트일 수 있습니다. 직원들이 출장을 예약해야 할 때, 여행사는 실제로 여행을 예약하기 전에 회사에 인증을 보내도록 합니다. + +![SAML 단계별 프로세스](./fig-01-saml.png) + +이것은 브라우저, SP, IdP 세 엔티티가 액세스를 위해 협상하는 방식입니다. SP는 사전에 브라우저를 사용하는 사용자에 대해 아무것도 알 필요 없이 IdP를 신뢰하기만 하면 됩니다. + +### SAML 사용자를 위한 이더리움 + +이더리움은 탈중앙화된 시스템입니다. + +![이더리움 로그온](./fig-02-eth-logon.png) + +사용자는 개인 키를 가집니다(일반적으로 브라우저 확장 프로그램에 보관됨). 개인 키에서 공개 키를 파생할 수 있고, 공개 키에서 20바이트 주소를 파생할 수 있습니다. 사용자가 시스템에 로그인해야 할 때, 논스(일회용 값)가 포함된 메시지에 서명하도록 요청받습니다. 서버는 해당 주소에 의해 서명이 생성되었는지 확인할 수 있습니다. + +![인증에서 추가 데이터 가져오기](./fig-03-eas-data.png) + +서명은 이더리움 주소만 확인합니다. 다른 사용자 속성을 얻으려면 일반적으로 [인증](https://attest.org/)을 사용합니다. 인증에는 일반적으로 다음과 같은 필드가 있습니다. + +- **인증자**, 인증을 만든 주소 +- **수신자**, 인증이 적용되는 주소 +- **데이터**, 이름, 권한 등과 같이 인증되는 데이터입니다. +- **스키마**, 데이터를 해석하는 데 사용되는 스키마의 ID입니다. + +이더리움의 탈중앙화된 특성 때문에 모든 사용자가 인증을 생성할 수 있습니다. 신뢰할 수 있는 인증을 식별하려면 인증자의 신원이 중요합니다. + +## 설정 + +첫 번째 단계는 SAML SP와 SAML IdP가 서로 통신하도록 하는 것입니다. + +1. 소프트웨어를 다운로드합니다. 이 글의 샘플 소프트웨어는 [github](https://github.com/qbzzt/250420-saml-ethereum)에 있습니다. 여러 단계가 여러 브랜치에 저장되어 있습니다. 이 단계에서는 `saml-only`를 원합니다. + + ```sh + git clone https://github.com/qbzzt/250420-saml-ethereum -b saml-only + cd 250420-saml-ethereum + pnpm install + ``` + +2. 자체 서명된 인증서로 키를 생성합니다. 이는 키가 자체 인증 기관이며 서비스 공급자로 수동으로 가져와야 함을 의미합니다. 자세한 내용은 [OpenSSL 문서](https://docs.openssl.org/master/man1/openssl-req/)를 참조하세요. + + ```sh + mkdir keys + cd keys + openssl req -new -x509 -days 365 -nodes -sha256 -out saml-sp.crt -keyout saml-sp.pem -subj /CN=sp/ + openssl req -new -x509 -days 365 -nodes -sha256 -out saml-idp.crt -keyout saml-idp.pem -subj /CN=idp/ + cd .. + ``` + +3. 서버(SP 및 IdP 모두) 시작 + + ```sh + pnpm start + ``` + +4. URL [http://localhost:3000/](http://localhost:3000/)에서 SP로 이동하여 버튼을 클릭하면 IdP(포트 3001)로 리디렉션됩니다. + +5. IdP에 이메일 주소를 제공하고 **서비스 공급자에 로그인**을 클릭합니다. 서비스 공급자(포트 3000)로 다시 리디렉션되고 이메일 주소로 사용자를 인식하는지 확인합니다. + +### 자세한 설명 + +다음은 단계별로 일어나는 일입니다. + +![이더리움 없는 일반 SAML 로그온](./fig-04-saml-no-eth.png) + +#### src/config.mts + +이 파일에는 ID 공급자와 서비스 공급자 모두에 대한 구성이 포함되어 있습니다. 일반적으로 이 둘은 서로 다른 엔티티이지만 여기서는 단순화를 위해 코드를 공유할 수 있습니다. + +```typescript +const fs = await import("fs") + +const protocol="http" +``` + +지금은 테스트 중이므로 HTTP를 사용해도 괜찮습니다. + +```typescript +export const spCert = fs.readFileSync("keys/saml-sp.crt").toString() +export const idpCert = fs.readFileSync("keys/saml-idp.crt").toString() +``` + +일반적으로 두 구성 요소 모두에서 사용할 수 있는 공개 키를 읽습니다(직접 신뢰하거나 신뢰할 수 있는 인증 기관에서 서명함). + +```typescript +export const spPort = 3000 +export const spHostname = "localhost" +export const spDir = "sp" + +export const idpPort = 3001 +export const idpHostname = "localhost" +export const idpDir = "idp" + +export const spUrl = `${protocol}://${spHostname}:${spPort}/${spDir}` +export const idpUrl = `${protocol}://${idpHostname}:${idpPort}/${idpDir}` +``` + +두 구성 요소의 URL입니다. + +```typescript +export const spPublicData = { +``` + +서비스 공급자의 공개 데이터입니다. + +```typescript + entityID: `${spUrl}/metadata`, +``` + +관례적으로 SAML에서 `entityID`는 엔티티의 메타데이터를 사용할 수 있는 URL입니다. 이 메타데이터는 XML 형식이라는 점을 제외하고 여기의 공개 데이터에 해당합니다. + +```typescript + wantAssertionsSigned: true, + authnRequestsSigned: false, + signingCert: spCert, + allowCreate: true, + assertionConsumerService: [{ + Binding: 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + Location: `${spUrl}/assertion`, + }] + } +``` + +우리의 목적에 가장 중요한 정의는 `assertionConsumerServer`입니다. 이는 서비스 공급자에게 무언가를 주장하기 위해(예: "이 정보를 보내는 사용자는 somebody@example.com입니다") [HTTP POST](https://www.w3schools.com/tags/ref_httpmethods.asp)를 사용하여 URL `http://localhost:3000/sp/assertion`으로 보내야 함을 의미합니다. + +```typescript +export const idpPublicData = { + entityID: `${idpUrl}/metadata`, + signingCert: idpCert, + wantAuthnRequestsSigned: false, + singleSignOnService: [{ + Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + Location: `${idpUrl}/login` + }], + singleLogoutService: [{ + Binding: "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST", + Location: `${idpUrl}/logout` + }], + } +``` + +ID 공급자의 공개 데이터는 비슷합니다. 사용자를 로그인하려면 `http://localhost:3001/idp/login`에 POST하고 사용자를 로그아웃하려면 `http://localhost:3001/idp/logout`에 POST하도록 지정합니다. + +#### src/sp.mts + +서비스 공급자를 구현하는 코드입니다. + +```typescript +import * as config from "./config.mts" +const fs = await import("fs") +const saml = await import("samlify") +``` + +[`samlify`](https://www.npmjs.com/package/samlify) 라이브러리를 사용하여 SAML을 구현합니다. + +```typescript +import * as validator from "@authenio/samlify-node-xmllint" +saml.setSchemaValidator(validator) +``` + +`samlify` 라이브러리는 XML이 올바른지, 예상 공개 키로 서명되었는지 등을 확인하는 패키지가 있어야 합니다. 이 목적을 위해 [`@authenio/samlify-node-xmllint`](https://www.npmjs.com/package/@authenio/samlify-node-xmllint)를 사용합니다. + +```typescript +const express = (await import("express")).default +const spRouter = express.Router() +const app = express() +``` + +[`express`](https://expressjs.com/) [`Router`](https://expressjs.com/en/5x/api.html#router)는 웹 사이트 내에 마운트할 수 있는 "미니 웹 사이트"입니다. 이 경우 모든 서비스 공급자 정의를 함께 그룹화하는 데 사용됩니다. + +```typescript +const spPrivateKey = fs.readFileSync("keys/saml-sp.pem").toString() + +const sp = saml.ServiceProvider({ + privateKey: spPrivateKey, + ...config.spPublicData +}) +``` + +서비스 공급자의 자체 표현은 모든 공개 데이터와 정보를 서명하는 데 사용하는 개인 키입니다. + +```typescript +const idp = saml.IdentityProvider(config.idpPublicData); +``` + +공개 데이터에는 서비스 공급자가 ID 공급자에 대해 알아야 할 모든 것이 포함됩니다. + +```typescript +spRouter.get(`/metadata`, + (req, res) => res.header("Content-Type", "text/xml").send(sp.getMetadata()) +) +``` + +다른 SAML 구성 요소와의 상호 운용성을 활성화하려면 서비스 및 ID 공급자의 공개 데이터(메타데이터라고 함)를 `/metadata`의 XML 형식으로 사용할 수 있어야 합니다. + +```typescript +spRouter.post(`/assertion`, +``` + +이것은 브라우저가 자신을 식별하기 위해 액세스하는 페이지입니다. 어설션에는 사용자 식별자(여기서는 이메일 주소를 사용)가 포함되며 추가 속성을 포함할 수 있습니다. 위 시퀀스 다이어그램의 7단계에 대한 핸들러입니다. + +```typescript + async (req, res) => { + // console.log(`SAML response:\n${Buffer.from(req.body.SAMLResponse, 'base64').toString('utf-8')}`) +``` + +주석 처리된 명령을 사용하여 어설션에 제공된 XML 데이터를 볼 수 있습니다. [base64로 인코딩](https://en.wikipedia.org/wiki/Base64)되어 있습니다. + +```typescript + try { + const loginResponse = await sp.parseLoginResponse(idp, 'post', req); +``` + +ID 서버에서 로그인 요청을 구문 분석합니다. + +```typescript + res.send(` + + +

Hello ${loginResponse.extract.nameID}

+ + + `) + res.send(); +``` + +로그인을 받았음을 사용자에게 보여주기 위해 HTML 응답을 보냅니다. + +```typescript + } catch (err) { + console.error('Error processing SAML response:', err); + res.status(400).send('SAML authentication failed'); + } + } +) +``` + +실패 시 사용자에게 알립니다. + +```typescript +spRouter.get('/login', +``` + +브라우저가 이 페이지를 가져오려고 할 때 로그인 요청을 생성합니다. 위 시퀀스 다이어그램의 1단계에 대한 핸들러입니다. + +```typescript + async (req, res) => { + const loginRequest = await sp.createLoginRequest(idp, "post") +``` + +로그인 요청을 게시할 정보를 가져옵니다. + +```typescript + res.send(` + + + +``` + +이 페이지는 양식(아래 참조)을 자동으로 제출합니다. 이렇게 하면 사용자가 리디렉션되기 위해 아무것도 할 필요가 없습니다. 위 시퀀스 다이어그램의 2단계입니다. + +```typescript +
+``` + +`loginRequest.entityEndpoint`(ID 공급자 엔드포인트의 URL)에 게시합니다. + +```typescript + +``` + +입력 이름은 `loginRequest.type`(`SAMLRequest`)입니다. 해당 필드의 내용은 `loginRequest.context`이며, 다시 base64로 인코딩된 XML입니다. + +```typescript +
+ + + `) + } +) + +app.use(express.urlencoded({extended: true})) +``` + +[이 미들웨어는](https://expressjs.com/en/5x/api.html#express.urlencoded) [HTTP 요청](https://www.tutorialspoint.com/http/http_requests.htm)의 본문을 읽습니다. 기본적으로 express는 대부분의 요청에 필요하지 않기 때문에 무시합니다. POST는 본문을 사용하기 때문에 필요합니다. + +```typescript +app.use(`/${config.spDir}`, spRouter) +``` + +서비스 공급자 디렉터리(`/sp`)에 라우터를 마운트합니다. + +```typescript +app.get("/", (req, res) => { + res.send(` + + + + + + `) +}) +``` + +브라우저가 루트 디렉터리를 가져오려고 하면 로그인 페이지 링크를 제공합니다. + +```typescript +app.listen(config.spPort, () => { + console.log(`service provider is running on http://${config.spHostname}:${config.spPort}`) +}) +``` + +이 express 애플리케이션으로 `spPort`를 수신합니다. + +#### src/idp.mts + +ID 공급자입니다. 서비스 공급자와 매우 유사하며 아래 설명은 다른 부분에 대한 것입니다. + +```typescript +const xmlParser = new (await import("fast-xml-parser")).XMLParser( + { + ignoreAttributes: false, // Preserve attributes + attributeNamePrefix: "@_", // Prefix for attributes + } +) +``` + +서비스 공급자로부터 수신하는 XML 요청을 읽고 이해해야 합니다. + +```typescript +const getLoginPage = requestId => ` +``` + +이 함수는 위 시퀀스 다이어그램의 4단계에서 반환되는 자동 제출 양식이 있는 페이지를 만듭니다. + +```typescript + + + 로그인 페이지 + + +

로그인 페이지

+
+ + 이메일 주소: +
+ +``` + +서비스 공급자에게 보내는 필드는 두 가지입니다. + +1. 응답 중인 `requestId`. +2. 사용자 식별자(지금은 사용자가 제공한 이메일 주소를 사용). + +```typescript +
+ + + +const idpRouter = express.Router() + +idpRouter.post("/loginSubmitted", async (req, res) => { + const loginResponse = await idp.createLoginResponse( +``` + +위 시퀀스 다이어그램의 5단계에 대한 핸들러입니다. [`idp.createLoginResponse`](https://github.com/tngan/samlify/blob/master/src/entity-idp.ts#L73-L125)는 로그인 응답을 생성합니다. + +```typescript + sp, + { + authnContextClassRef: 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport', + audience: sp.entityID, +``` + +대상은 서비스 공급자입니다. + +```typescript + extract: { + request: { + id: req.body.requestId + } + }, +``` + +요청에서 추출한 정보입니다. 요청에서 우리가 신경 쓰는 매개변수 중 하나는 requestId로, 서비스 공급자가 요청과 응답을 일치시킬 수 있게 해줍니다. + +```typescript + signingKey: { privateKey: idpPrivateKey, publicKey: config.idpCert } // Ensure signing +``` + +응답에 서명할 데이터를 가지려면 `signingKey`가 필요합니다. 서비스 공급자는 서명되지 않은 요청을 신뢰하지 않습니다. + +```typescript + }, + "post", + { + email: req.body.email +``` + +서비스 공급자에게 다시 보내는 사용자 정보가 있는 필드입니다. + +```typescript + } + ); + + res.send(` + + + + +
+ +
+ + + `) +}) +``` + +다시 자동 제출 양식을 사용합니다. 위 시퀀스 다이어그램의 6단계입니다. + +```typescript + +// IdP endpoint for login requests +idpRouter.post(`/login`, +``` + +서비스 공급자로부터 로그인 요청을 수신하는 엔드포인트입니다. 위 시퀀스 다이어그램의 3단계에 대한 핸들러입니다. + +```typescript + async (req, res) => { + try { + // Workaround because I couldn't get parseLoginRequest to work. + // const loginRequest = await idp.parseLoginRequest(sp, 'post', req) + const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8')) + res.send(getLoginPage(samlRequest["samlp:AuthnRequest"]["@_ID"])) +``` + +인증 요청의 ID를 읽으려면 [`idp.parseLoginRequest`](https://github.com/tngan/samlify/blob/master/src/entity-idp.ts#L127-L144)를 사용할 수 있어야 합니다. 하지만 작동하게 할 수 없었고 많은 시간을 할애할 가치가 없었기 때문에 [범용 XML 파서](https://www.npmjs.com/package/fast-xml-parser)를 사용했습니다. 필요한 정보는 XML의 최상위 수준에 있는 `` 태그 내의 `ID` 속성입니다. + +## 이더리움 서명 사용하기 + +이제 서비스 공급자에게 사용자 ID를 보낼 수 있으므로 다음 단계는 신뢰할 수 있는 방식으로 사용자 ID를 얻는 것입니다. Viem을 사용하면 지갑에 사용자 주소를 요청할 수 있지만, 이는 브라우저에 정보를 요청하는 것을 의미합니다. 브라우저를 제어할 수 없으므로 브라우저에서 받은 응답을 자동으로 신뢰할 수 없습니다. + +대신 IdP는 브라우저에 서명할 문자열을 보냅니다. 브라우저의 지갑이 이 문자열에 서명하면 실제로 해당 주소라는 것을 의미합니다(즉, 주소에 해당하는 개인 키를 알고 있음). + +이것이 작동하는 것을 보려면 기존 IdP와 SP를 중지하고 다음 명령을 실행하세요. + +```sh +git checkout eth-signatures +pnpm install +pnpm start +``` + +그런 다음 [SP](http://localhost:3000)로 이동하여 지침을 따릅니다. + +이 시점에서는 이더리움 주소에서 이메일 주소를 얻는 방법을 모르므로 대신 `<이더리움 주소>@bad.email.address`를 SP에 보고합니다. + +### 자세한 설명 + +변경 사항은 이전 다이어그램의 4-5단계에 있습니다. + +![이더리움 서명을 사용한 SAML](./fig-05-saml-w-signature.png) + +변경된 유일한 파일은 `idp.mts`입니다. 변경된 부분은 다음과 같습니다. + +```typescript +import { v4 as uuidv4 } from 'uuid' +import { verifyMessage } from 'viem' +``` + +이 두 개의 추가 라이브러리가 필요합니다. [`uuid`](https://www.npmjs.com/package/uuid)를 사용하여 [논스](https://en.wikipedia.org/wiki/Cryptographic_nonce) 값을 생성합니다. 값 자체는 중요하지 않고 한 번만 사용된다는 사실만 중요합니다. + +[`viem`](https://viem.sh/) 라이브러리를 사용하면 이더리움 정의를 사용할 수 있습니다. 여기서는 서명이 실제로 유효한지 확인해야 합니다. + +```typescript +const loginPrompt = "서비스 공급자에 액세스하려면 이 논스에 서명하세요: " +``` + +지갑은 사용자에게 메시지에 서명할 권한을 요청합니다. 논스만 있는 메시지는 사용자를 혼란스럽게 할 수 있으므로 이 프롬프트를 포함합니다. + +```typescript +// Keep requestIDs here +let nonces = {} +``` + +응답하려면 요청 정보가 필요합니다. 요청(4단계)과 함께 보내고 다시 받을 수 있습니다(5단계). 그러나 잠재적으로 적대적인 사용자의 통제하에 있는 브라우저에서 얻는 정보는 신뢰할 수 없습니다. 따라서 여기에 nonce를 키로 저장하는 것이 좋습니다. + +여기서는 단순성을 위해 변수로 수행하고 있음을 참고하세요. 그러나 여기에는 몇 가지 단점이 있습니다. + +- 서비스 거부 공격에 취약합니다. 악의적인 사용자가 여러 번 로그온을 시도하여 메모리를 채울 수 있습니다. +- IdP 프로세스를 다시 시작해야 하는 경우 기존 값을 잃게 됩니다. +- 각각 고유한 변수를 가지므로 여러 프로세스에 걸쳐 부하를 분산할 수 없습니다. + +프로덕션 시스템에서는 데이터베이스를 사용하고 일종의 만료 메커니즘을 구현합니다. + +```typescript +const getSignaturePage = requestId => { + const nonce = uuidv4() + nonces[nonce] = requestId +``` + +논스를 만들고 나중에 사용할 수 있도록 `requestId`를 저장합니다. + +```typescript + return ` + + + + + +

서명하세요

+ +
+ + + +` +} +``` + +나머지는 표준 HTML입니다. + +```typescript +idpRouter.get("/signature/:nonce/:account/:signature", async (req, res) => { +``` + +시퀀스 다이어그램의 5단계에 대한 핸들러입니다. + +```typescript + const requestId = nonces[req.params.nonce] + if (requestId === undefined) { + res.send("Bad nonce") + return ; + } + + nonces[req.params.nonce] = undefined +``` + +요청 ID를 가져오고 재사용할 수 없도록 `nonces`에서 논스를 삭제합니다. + +```typescript + try { +``` + +서명이 유효하지 않을 수 있는 방법이 너무 많기 때문에 `try ...`로 래핑합니다. catch` 블록으로 발생한 오류를 포착합니다. + +```typescript + const validSignature = await verifyMessage({ + address: req.params.account, + message: `${loginPrompt}${req.params.nonce}`, + signature: req.params.signature + }) +``` + +시퀀스 다이어그램의 5.5단계를 구현하려면 [`verifyMessage`](https://viem.sh/docs/actions/public/verifyMessage#verifymessage)를 사용합니다. + +```typescript + if (!validSignature) + throw("Bad signature") + } catch (err) { + res.send("Error:" + err) + return ; + } +``` + +핸들러의 나머지 부분은 한 가지 작은 변경 사항을 제외하고 이전에 `/loginSubmitted` 핸들러에서 수행한 것과 동일합니다. + +```typescript + const loginResponse = await idp.createLoginResponse( + . + . + . + { + email: req.params.account + "@bad.email.address" + } + ); +``` + +실제 이메일 주소는 없으므로(다음 섹션에서 얻을 수 있음) 지금은 이더리움 주소를 반환하고 이메일 주소가 아님을 명확하게 표시합니다. + +```typescript +// IdP endpoint for login requests +idpRouter.post(`/login`, + async (req, res) => { + try { + // Workaround because I couldn't get parseLoginRequest to work. + // const loginRequest = await idp.parseLoginRequest(sp, 'post', req) + const samlRequest = xmlParser.parse(Buffer.from(req.body.SAMLRequest, 'base64').toString('utf-8')) + res.send(getSignaturePage(samlRequest["samlp:AuthnRequest"]["@_ID"])) + } catch (err) { + console.error('Error processing SAML response:', err); + res.status(400).send('SAML authentication failed'); + } + } +) +``` + +3단계 핸들러에서 `getLoginPage` 대신 이제 `getSignaturePage`를 사용합니다. + +## 이메일 주소 받기 + +다음 단계는 서비스 공급자가 요청한 식별자인 이메일 주소를 얻는 것입니다. 이를 위해 [이더리움 증명 서비스(EAS)](https://attest.org/)를 사용합니다. + +인증을 얻는 가장 쉬운 방법은 [GraphQL API](https://docs.attest.org/docs/developer-tools/api)를 사용하는 것입니다. 다음 쿼리를 사용합니다. + +``` +query GetAttestationsByRecipient { + attestations( + where: { + recipient: { equals: "${getAddress(ethAddr)}" } + schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" } + } + take: 1 + ) { + data + id + attester + } +} +``` + +이 [`schemaId`](https://optimism.easscan.org/schema/view/0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977)는 이메일 주소만 포함합니다. 이 쿼리는 이 스키마의 인증을 요청합니다. 인증의 주체는 `recipient`라고 합니다. 항상 이더리움 주소입니다. + +경고: 여기서 인증을 얻는 방식에는 두 가지 보안 문제가 있습니다. + +- 중앙화된 구성 요소인 API 엔드포인트 `https://optimism.easscan.org/graphql`로 이동합니다. `id` 속성을 가져와서 온체인 조회를 통해 인증이 진짜인지 확인할 수 있지만, API 엔드포인트는 인증에 대해 알려주지 않음으로써 여전히 인증을 검열할 수 있습니다. + + 이 문제는 해결 불가능한 것이 아니며, 자체 GraphQL 엔드포인트를 실행하고 체인 로그에서 인증을 얻을 수 있지만, 우리의 목적에는 과도합니다. + +- 인증자 신원을 보지 않습니다. 누구나 거짓 정보를 제공할 수 있습니다. 실제 구현에서는 신뢰할 수 있는 인증자 집합이 있고 그들의 인증만 살펴봅니다. + +이것이 작동하는 것을 보려면 기존 IdP와 SP를 중지하고 다음 명령을 실행하세요. + +```sh +git checkout email-address +pnpm install +pnpm start +``` + +그런 다음 이메일 주소를 제공합니다. 두 가지 방법이 있습니다. + +- 개인 키를 사용하여 지갑을 가져오고 테스트 개인 키 `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`을 사용합니다. + +- 자신의 이메일 주소에 대한 인증을 추가합니다. + + 1. 인증 탐색기에서 [스키마](https://optimism.easscan.org/schema/view/0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977)로 이동합니다. + + 2. **스키마로 인증**을 클릭합니다. + + 3. 이더리움 주소를 수신자로 입력하고, 이메일 주소를 이메일 주소로 입력하고, **온체인**을 선택합니다. 그런 다음 **인증 만들기**를 클릭합니다. + + 4. 지갑에서 트랜잭션을 승인합니다. 가스를 지불하려면 [옵티미즘 블록체인](https://app.optimism.io/bridge/deposit)에 ETH가 필요합니다. + +어느 쪽이든 이 작업을 마친 후 [http://localhost:3000](http://localhost:3000)으로 이동하여 지침을 따릅니다. 테스트 개인 키를 가져온 경우 받는 이메일은 `test_addr_0@example.com`입니다. 자신의 주소를 사용했다면 인증한 주소여야 합니다. + +### 자세한 설명 + +![이더리움 주소에서 이메일로 가져오기](./fig-06-saml-sig-n-email.png) + +새로운 단계는 GraphQL 통신, 5.6 및 5.7단계입니다. + +다시 한 번, `idp.mts`의 변경된 부분은 다음과 같습니다. + +```typescript +import { GraphQLClient } from 'graphql-request' +import { SchemaEncoder } from '@ethereum-attestation-service/eas-sdk' +``` + +필요한 라이브러리를 가져옵니다. + +```typescript +const graphqlEndpointUrl = "https://optimism.easscan.org/graphql" +``` + +[각 블록체인에 대한 별도의 엔드포인트](https://docs.attest.org/docs/developer-tools/api)가 있습니다. + +```typescript +const graphqlClient = new GraphQLClient(graphqlEndpointUrl, { fetch }) +``` + +엔드포인트 쿼리에 사용할 수 있는 새로운 `GraphQLClient` 클라이언트를 만듭니다. + +```typescript +const graphqlSchema = 'string emailAddress' +const graphqlEncoder = new SchemaEncoder(graphqlSchema) +``` + +GraphQL은 바이트가 포함된 불투명한 데이터 객체만 제공합니다. 이를 이해하려면 스키마가 필요합니다. + +```typescript +const ethereumAddressToEmail = async ethAddr => { +``` + +이더리움 주소에서 이메일 주소로 변환하는 함수입니다. + +```typescript + const query = ` + query GetAttestationsByRecipient { +``` + +GraphQL 쿼리입니다. + +```typescript + attestations( +``` + +인증을 찾고 있습니다. + +```typescript + where: { + recipient: { equals: "${getAddress(ethAddr)}" } + schemaId: { equals: "0xfa2eff59a916e3cc3246f9aec5e0ca00874ae9d09e4678e5016006f07622f977" } + } +``` + +원하는 인증은 스키마에 있으며 수신자가 `getAddress(ethAddr)`인 인증입니다. [`getAddress`](https://viem.sh/docs/utilities/getAddress#getaddress) 함수는 주소에 올바른 [체크섬](https://github.com/ethereum/ercs/blob/master/ERCS/erc-55.md)이 있는지 확인합니다. 이는 GraphQL이 대소문자를 구분하기 때문에 필요합니다. "0xBAD060A7", "0xBad060A7", "0xbad060a7"은 서로 다른 값입니다. + +```typescript + take: 1 +``` + +몇 개의 인증을 찾든 첫 번째 것만 원합니다. + +```typescript + ) { + data + id + attester + } + }` +``` + +받고 싶은 필드입니다. + +- `attester`: 인증을 제출한 주소. 일반적으로 인증을 신뢰할지 여부를 결정하는 데 사용됩니다. +- `id`: 인증 ID. 이 값을 사용하여 [온체인에서 인증을 읽어](https://optimism.blockscout.com/address/0x4200000000000000000000000000000000000021?tab=read_proxy&source_address=0x4E0275Ea5a89e7a3c1B58411379D1a0eDdc5b088#0xa3112a64) GraphQL 쿼리의 정보가 올바른지 확인할 수 있습니다. +- `data`: 스키마 데이터(이 경우 이메일 주소). + +```typescript + const queryResult = await graphqlClient.request(query) + + if (queryResult.attestations.length == 0) + return "no_address@available.is" +``` + +인증이 없는 경우 명백히 잘못되었지만 서비스 공급자에게 유효하게 표시되는 값을 반환합니다. + +```typescript + const attestationDataFields = graphqlEncoder.decodeData(queryResult.attestations[0].data) + return attestationDataFields[0].value.value +} +``` + +값이 있으면 `decodeData`를 사용하여 데이터를 디코딩합니다. 제공하는 메타데이터는 필요하지 않고 값 자체만 필요합니다. + +```typescript + const loginResponse = await idp.createLoginResponse( + sp, + { + . + . + . + }, + "post", + { + email: await ethereumAddressToEmail(req.params.account) + } + ); +``` + +새 함수를 사용하여 이메일 주소를 가져옵니다. + +## 탈중앙화는 어떻습니까? + +이 구성에서 사용자는 이더리움-이메일 주소 매핑을 위해 신뢰할 수 있는 인증자에 의존하는 한 다른 사람인 척할 수 없습니다. 그러나 ID 공급자는 여전히 중앙화된 구성 요소입니다. ID 공급자의 개인 키를 가진 사람은 누구나 서비스 공급자에게 거짓 정보를 보낼 수 있습니다. + +[다자간 계산(MPC)](https://en.wikipedia.org/wiki/Secure_multi-party_computation)를 사용하는 솔루션이 있을 수 있습니다. 다음 튜토리얼에서 이에 대해 쓸 수 있기를 바랍니다. + +## 결론 + +이더리움 서명과 같은 로그온 표준을 채택하는 것은 닭과 달걀 문제에 직면합니다. 서비스 공급자는 가능한 가장 넓은 시장에 어필하기를 원합니다. 사용자는 로그온 표준 지원에 대해 걱정하지 않고 서비스에 액세스할 수 있기를 원합니다. +이더리움 IdP와 같은 어댑터를 만들면 이 장애물을 극복하는 데 도움이 될 수 있습니다. + +[여기서 제 작업에 대한 자세한 내용을 확인하세요](https://cryptodocguy.pro/). diff --git a/public/content/translations/ko/developers/tutorials/getting-started-with-ethereum-development-using-alchemy/index.md b/public/content/translations/ko/developers/tutorials/getting-started-with-ethereum-development-using-alchemy/index.md new file mode 100644 index 00000000000..47ad2c87004 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/getting-started-with-ethereum-development-using-alchemy/index.md @@ -0,0 +1,149 @@ +--- +title: "이더리움 개발 시작하기" +description: "이더리움 개발을 시작하는 입문자를 위한 가이드입니다. API 엔드포인트를 구동하는 것부터 명령줄 요청을 보내고, 첫 웹3 스크립트를 작성하는 것까지 안내해 드립니다! 사전 블록체인 개발 경험은 필요 없어요!" +author: "Elan Halpern" +tags: [ "자바스크립트", "ethers.js", "노드", "요청", "alchemy" ] +skill: beginner +lang: ko +published: 2020-10-30 +source: Medium +sourceUrl: https://medium.com/alchemy-api/getting-started-with-ethereum-development-using-alchemy-c3d6a45c567f +--- + +![이더리움과 Alchemy 로고](./ethereum-alchemy.png) + +이더리움 개발을 시작하는 입문자를 위한 가이드입니다. 이번 튜토리얼에서는 Maker, 0x, MyEtherWallet, Dharma, Kyber를 비롯한 상위 블록체인 앱 70%가 사용하는 수백만 명의 사용자를 지원하는 선도적인 블록체인 개발자 플랫폼인 [Alchemy](https://alchemyapi.io/)를 사용합니다. Alchemy는 이더리움 체인의 API 엔드포인트에 대한 액세스를 제공하여 트랜잭션을 읽고 쓸 수 있도록 합니다. + +Alchemy에 가입하는 것부터 첫 웹3 스크립트를 작성하는 것까지 안내해 드립니다! 사전 블록체인 개발 경험은 필요 없어요! + +## 1. 무료 Alchemy 계정 가입하기 {#sign-up-for-a-free-alchemy-account} + +Alchemy 계정 생성은 간단합니다. [여기에서 무료로 가입하세요](https://auth.alchemy.com/). + +## 2. Alchemy 앱 만들기 {#create-an-alchemy-app} + +이더리움 체인과 통신하고 Alchemy 제품을 사용하려면 요청을 인증할 API 키가 필요합니다. + +[대시보드에서 API 키를 생성](https://dashboard.alchemy.com/)할 수 있습니다. 새 키를 생성하려면 아래와 같이 “앱 만들기” 로 이동합니다. + +[_ShapeShift_](https://shapeshift.com/)가 _대시보드를 보여주도록 허락해준 것에 대해 특별히 감사드립니다!_ + +![Alchemy 대시보드](./alchemy-dashboard.png) + +“앱 만들기” 밑의 입력란을 채워 새 키를 얻으세요. 여기에서 이전에 만들었던 앱을 보거나 팀에서 만든 앱을 볼 수 있습니다. 아무 앱에서나 '키 보기'를 클릭하여 기존 키를 가져올 수 있습니다. + +![Alchemy로 앱 만들기 스크린샷](./create-app.png) + +'앱' 위에 마우스를 올리고 하나를 선택하여 기존 API 키를 가져올 수도 있습니다. 여기에서 '키 보기'를 하거나, '앱 편집'에서 특정 도메인을 화이트리스트에 추가하고, 여러 개발자 도구를 확인하며, 분석을 볼 수 있습니다. + +![사용자에게 API 키를 가져오는 방법을 보여주는 Gif](./pull-api-keys.gif) + +## 3. 명령줄에서 요청하기 {#make-a-request-from-the-command-line} + +JSON-RPC와 curl을 사용하여 Alchemy를 통해 이더리움 블록체인과 상호작용합니다. + +수동 요청의 경우 `POST` 요청을 통해 `JSON-RPC`와 상호작용하는 것을 권장합니다. 다음 필드와 함께 `Content-Type: application/json` 헤더와 쿼리를 `POST` 본문으로 전달하기만 하면 됩니다: + +- `jsonrpc`: JSON-RPC 버전—현재는 `2.0`만 지원됩니다. +- `method`: ETH API 메서드. [API 참조 보기](https://docs.alchemyapi.io/documentation/alchemy-api-reference/json-rpc) +- `params`: 메서드에 전달할 파라미터 목록입니다. +- `id`: 요청의 ID입니다. 응답에 의해 반환되므로 어떤 요청에 대한 응답인지 추적할 수 있습니다. + +다음은 명령줄에서 실행하여 현재 가스 가격을 검색하는 예제입니다: + +```bash +curl https://eth-mainnet.alchemyapi.io/v2/demo \ +-X POST \ +-H "Content-Type: application/json" \ +-d '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":73}' +``` + +_**참고:** [https://eth-mainnet.alchemyapi.io/v2/demo](https://eth-mainnet.alchemyapi.io/jsonrpc/demo)를 본인의 API 키 `https://eth-mainnet.alchemyapi.io/v2/**your-api-key`로 바꾸세요._ + +**결과:** + +```json +{ "id": 73,"jsonrpc": "2.0","result": "0x09184e72a000" // 10000000000000 } +``` + +## 4. 웹3 클라이언트 설정하기 {#set-up-your-web3-client} + +**기존 클라이언트가 있는 경우,** 현재 노드 공급자 URL을 API 키가 포함된 Alchemy URL로 변경하세요: `“https://eth-mainnet.alchemyapi.io/v2/your-api-key"` + +**_참고:_** 아래 스크립트는 명령줄에서 실행하는 것이 아니라 **노드 컨텍스트**에서 실행하거나 **파일에 저장**해야 합니다. Node나 npm이 아직 설치되지 않았다면, 이 빠른 [mac용 설정 가이드](https://app.gitbook.com/@alchemyapi/s/alchemy/guides/alchemy-for-macs)를 확인하세요. + +Alchemy와 통합할 수 있는 [웹3 라이브러리](https://docs.alchemyapi.io/guides/getting-started#other-web3-libraries)는 많지만, web3.js를 바로 대체할 수 있으며 Alchemy와 원활하게 작동하도록 빌드 및 구성된 [Alchemy 웹3](https://docs.alchemy.com/reference/api-overview) 사용을 권장합니다. 이는 자동 재시도 및 강력한 WebSocket 지원과 같은 여러 이점을 제공합니다. + +AlchemyWeb3.js를 설치하려면 **프로젝트 디렉터리로 이동**하고 다음을 실행하세요: + +**Yarn 사용 시:** + +``` +yarn add @alch/alchemy-web3 +``` + +**NPM 사용 시:** + +``` +npm install @alch/alchemy-web3 +``` + +Alchemy의 노드 인프라와 상호작용하려면 NodeJS에서 실행하거나 이 코드를 JavaScript 파일에 추가하세요: + +```js +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3( + "https://eth-mainnet.alchemyapi.io/v2/your-api-key" +) +``` + +## 5. 첫 웹3 스크립트 작성하기! {#write-your-first-web3-script} + +이제 웹3 프로그래밍을 본격적으로 시작해보기 위해 이더리움 메인넷에서 최신 블록 번호를 출력하는 간단한 스크립트를 작성해 보겠습니다. + +**1.** 아직 하지 않았다면, 터미널에서 새 프로젝트 디렉터리를 만들고 그 안으로 이동(cd)하세요:\*\* + +``` +mkdir web3-example +cd web3-example +``` + +**2.** 아직 설치하지 않았다면 프로젝트에 Alchemy 웹3(또는 다른 웹3) 의존성을 설치하세요:\*\* + +``` +npm install @alch/alchemy-web3 +``` + +**3.** `index.js`라는 이름의 파일을 만들고 다음 내용을 추가하세요:\*\* + +> `demo`는 본인의 Alchemy HTTP API 키로 교체해야 합니다. + +```js +async function main() { + const { createAlchemyWeb3 } = require("@alch/alchemy-web3") + const web3 = createAlchemyWeb3("https://eth-mainnet.alchemyapi.io/v2/demo") + const blockNumber = await web3.eth.getBlockNumber() + console.log("최신 블록 번호는 " + blockNumber) +} +main() +``` + +비동기와 관련된 것들이 낯선가요? 이 [Medium 게시물](https://medium.com/better-programming/understanding-async-await-in-javascript-1d81bb079b2c)을 확인해보세요. + +**4. node를 사용하여 터미널에서 실행하세요** + +``` +node index.js +``` + +**5. 이제 콘솔에 최신 블록 번호 출력이 표시될 것입니다!** + +``` +최신 블록 번호는 11043912 +``` + +**수고하셨어요!** 축하해요! Alchemy를 사용하여 첫 번째 웹3 스크립트를 작성했습니다 🎉\*\* + +다음으로 해야 될 걸 모르겠나요? [Hello World 스마트 계약 가이드](https://www.alchemy.com/docs/hello-world-smart-contract)에서 첫 스마트 계약을 배포하고 솔리디티 프로그래밍을 직접 해보거나, [대시보드 데모 앱](https://docs.alchemyapi.io/tutorials/demo-app)으로 대시보드 지식을 테스트해보세요! + +_[무료로 Alchemy에 가입](https://auth.alchemy.com/)하고, [개발문서](https://www.alchemy.com/docs/)를 확인하고, 최신 소식을 보려면 [Twitter](https://twitter.com/AlchemyPlatform)에서 저희를 팔로우하세요_. diff --git a/public/content/translations/ko/developers/tutorials/guide-to-smart-contract-security-tools/index.md b/public/content/translations/ko/developers/tutorials/guide-to-smart-contract-security-tools/index.md new file mode 100644 index 00000000000..edb8a635c9a --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/guide-to-smart-contract-security-tools/index.md @@ -0,0 +1,102 @@ +--- +title: "스마트 계약 보안 도구 가이드" +description: "세 가지 다른 테스팅 및 프로그램 분석 기법 개요" +author: "Trailofbits" +lang: ko +tags: [ "솔리디티", "스마트 계약", "보안" ] +skill: intermediate +published: 2020-09-07 +source: Building secure contracts +sourceUrl: https://github.com/crytic/building-secure-contracts/tree/master/program-analysis +--- + +세 가지 독특한 테스팅 및 프로그램 분석 기법을 사용할 것입니다. + +- **[Slither](/developers/tutorials/how-to-use-slither-to-find-smart-contract-bugs/)를 사용한 정적 분석.** 프로그램의 모든 경로를 다양한 프로그램 표현(예: 제어 흐름 그래프)을 통해 동시에 근사화하고 분석합니다. +- **[Echidna](/developers/tutorials/how-to-use-echidna-to-test-smart-contracts/)를 사용한 퍼징.** 트랜잭션을 의사 난수 생성하여 코드를 실행합니다. 퍼저는 주어진 속성을 위반하는 트랜잭션 시퀀스를 찾으려고 시도합니다. +- **[Manticore](/developers/tutorials/how-to-use-manticore-to-find-smart-contract-bugs/)를 사용한 심볼릭 실행.** 각 실행 경로를 수학 공식으로 변환하고 그 위에 제약 조건을 확인할 수 있는 정형 검증 기법입니다. + +각 기법은 장점과 단점이 있으며 [특정 사례](#determining-security-properties)에서 유용합니다. + +| 기법 | 도구 | 사용 방식 | 속도 | 놓친 버그 | 오탐 | +| ------ | --------- | ------------------ | ----- | ----- | -- | +| 정적 분석 | Slither | CLI 및 스크립트 | 초 단위 | 보통 | 낮음 | +| 퍼징 | Echidna | Solidity 속성 | 분 단위 | 낮음 | 없음 | +| 심볼릭 실행 | Manticore | Solidity 속성 및 스크립트 | 시간 단위 | 없음\* | 없음 | + +\* 시간 초과 없이 모든 경로를 탐색하는 경우 + +**Slither**는 몇 초 안에 계약을 분석하지만 정적 분석은 오탐을 유발할 수 있으며 복잡한 검사(예: 산술 검사)에는 덜 적합합니다. API를 통해 Slither를 실행하여 내장된 탐지기에 간편하게 액세스하거나 사용자 정의 검사를 수행할 수 있습니다. + +**Echidna**는 몇 분 동안 실행해야 하며 실제 참(true positive)만 생성합니다. Echidna는 Solidity로 작성된 사용자 제공 보안 속성을 확인합니다. 무작위 탐색을 기반으로 하므로 버그를 놓칠 수 있습니다. + +**Manticore**는 가장 "무거운" 분석을 수행합니다. Echidna와 마찬가지로 Manticore는 사용자가 제공한 속성을 확인합니다. 실행하는 데 더 많은 시간이 필요하지만 속성의 유효성을 증명할 수 있으며 오탐을 보고하지 않습니다. + +## 권장 워크플로 {#suggested-workflow} + +Slither의 내장 탐지기로 시작하여 간단한 버그가 현재 존재하지 않거나 나중에 유입되지 않도록 하세요. Slither를 사용하여 상속, 변수 종속성 및 구조적 문제와 관련된 속성을 확인하세요. 코드베이스가 커짐에 따라 Echidna를 사용하여 상태 머신의 더 복잡한 속성을 테스트하세요. Solidity에서 사용할 수 없는 보호 기능(예: 함수 재정의 방지)을 위해 Slither를 다시 사용하여 맞춤형 검사를 개발하세요. 마지막으로 Manticore를 사용하여 중요한 보안 속성(예: 산술 연산)의 대상 검증을 수행하세요. + +- Slither의 CLI를 사용하여 일반적인 문제 발견 +- Echidna를 사용하여 계약의 상위 수준 보안 속성 테스트 +- Slither를 사용하여 맞춤형 정적 검사 작성 +- 중요한 보안 속성에 대한 심층적인 보증을 원할 때 Manticore 사용 + +**단위 테스트에 대한 참고 사항**. 단위 테스트는 고품질 소프트웨어를 구축하는 데 필요합니다. 그러나 이러한 기법은 보안 결함을 찾는 데 가장 적합하지는 않습니다. 일반적으로 코드의 긍정적인 동작(즉, 코드가 일반적인 컨텍스트에서 예상대로 작동함)을 테스트하는 데 사용되는 반면, 보안 결함은 개발자가 고려하지 않은 엣지 케이스에 있는 경향이 있습니다. 수십 건의 스마트 계약 보안 검토에 대한 저희의 연구에서, [저희가 고객의 코드에서 발견한 보안 결함의 수나 심각도에 단위 테스트 커버리지는 영향을 미치지 않았습니다](https://blog.trailofbits.com/2019/08/08/246-findings-from-our-smart-contract-audits-an-executive-summary/). + +## 보안 속성 결정하기 {#determining-security-properties} + +코드를 효과적으로 테스트하고 검증하려면 주의가 필요한 영역을 식별해야 합니다. 보안에 사용하는 리소스는 제한적이므로, 코드베이스의 취약하거나 가치가 높은 부분의 범위를 정하는 것이 노력을 최적화하는 데 중요합니다. 위협 모델링이 도움이 될 수 있습니다. 다음을 검토해 보세요. + +- [신속한 위험 평가](https://infosec.mozilla.org/guidelines/risk/rapid_risk_assessment.html)(시간이 부족할 때 선호하는 접근 방식) +- [데이터 중심 시스템 위협 모델링 가이드](https://csrc.nist.gov/pubs/sp/800/154/ipd)(일명 NIST 800-154) +- [Shostack 위협 모델링](https://www.amazon.com/Threat-Modeling-Designing-Adam-Shostack/dp/1118809998) +- [STRIDE](https://wikipedia.org/wiki/STRIDE_\(security\)) / [DREAD](https://wikipedia.org/wiki/DREAD_\(risk_assessment_model\)) +- [PASTA](https://wikipedia.org/wiki/Threat_model#P.A.S.T.A.) +- [단언문 사용](https://blog.regehr.org/archives/1091) + +### 구성 요소 {#components} + +확인하려는 내용을 알면 적절한 도구를 선택하는 데도 도움이 됩니다. + +스마트 계약과 자주 관련된 광범위한 영역은 다음과 같습니다. + +- **상태 머신.** 대부분의 계약은 상태 머신으로 표현될 수 있습니다. (1) 유효하지 않은 상태에 도달할 수 없는지, (2) 상태가 유효할 경우 도달할 수 있는지, (3) 어떤 상태도 계약을 가두지 않는지 확인하는 것을 고려해 보세요. + + - Echidna와 Manticore는 상태 머신 사양을 테스트하는 데 선호되는 도구입니다. + +- **액세스 제어.** 시스템에 권한 있는 사용자(예: 소유자, 컨트롤러 등)가 있는 경우 (1) 각 사용자는 승인된 작업만 수행할 수 있고, (2) 어떤 사용자도 더 많은 권한을 가진 사용자의 작업을 차단할 수 없도록 보장해야 합니다. + + - Slither, Echidna, Manticore는 올바른 액세스 제어를 확인할 수 있습니다. 예를 들어, Slither는 화이트리스트에 있는 함수에만 onlyOwner 수정자가 없는지 확인할 수 있습니다. Echidna와 Manticore는 계약이 특정 상태에 도달한 경우에만 권한이 부여되는 것과 같은 더 복잡한 액세스 제어에 유용합니다. + +- **산술 연산.** 산술 연산의 건전성을 확인하는 것은 매우 중요합니다. 모든 곳에서 `SafeMath`를 사용하는 것은 오버플로우/언더플로우를 방지하는 좋은 방법이지만, 반올림 문제와 계약을 가두는 결함을 포함한 다른 산술적 결함도 여전히 고려해야 합니다. + + - 여기서는 Manticore가 최선의 선택입니다. 산술이 SMT 솔버의 범위를 벗어나는 경우 Echidna를 사용할 수 있습니다. + +- **상속 정확성.** Solidity 계약은 다중 상속에 크게 의존합니다. `super` 호출이 누락된 섀도잉 함수, 잘못 해석된 c3 선형화 순서와 같은 실수가 쉽게 발생할 수 있습니다. + + - Slither는 이러한 문제를 확실하게 탐지할 수 있는 도구입니다. + +- **외부 상호작용.** 계약은 서로 상호작용하며, 일부 외부 계약은 신뢰해서는 안 됩니다. 예를 들어, 계약이 외부 오라클에 의존하는 경우 사용 가능한 오라클의 절반이 손상되어도 안전하게 유지될까요? + + - Manticore와 Echidna는 계약과의 외부 상호작용을 테스트하는 데 최선의 선택입니다. Manticore에는 외부 계약을 스텁하는 내장 메커니즘이 있습니다. + +- **표준 준수.** 이더리움 표준(예: ERC20)은 설계에 결함이 있었던 이력이 있습니다. 구축 기반이 되는 표준의 한계를 인지하고 있어야 합니다. + - Slither, Echidna, Manticore는 주어진 표준에서 벗어나는 것을 탐지하는 데 도움이 될 것입니다. + +### 도구 선택 치트 시트 {#tool-selection-cheatsheet} + +| 구성 요소 | 도구 | 예시 | +| ------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 상태 머신 | Echidna, Manticore | | +| 액세스 제어 | Slither, Echidna, Manticore | [Slither 연습 문제 2](https://github.com/crytic/slither/blob/7f54c8b948c34fb35e1d61adaa1bd568ca733253/docs/src/tutorials/exercise2.md), [Echidna 연습 문제 2](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/exercises/Exercise-2.md) | +| 산술 연산 | Manticore, Echidna | [Echidna 연습 문제 1](https://github.com/crytic/building-secure-contracts/blob/master/program-analysis/echidna/exercises/Exercise-1.md), [Manticore 연습 문제 1 - 3](https://github.com/crytic/building-secure-contracts/tree/master/program-analysis/manticore/exercises) | +| 상속 정확성 | Slither | [Slither 연습 문제 1](https://github.com/crytic/slither/blob/7f54c8b948c34fb35e1d61adaa1bd568ca733253/docs/src/tutorials/exercise1.md) | +| 외부 상호작용 | Manticore, Echidna | | +| 표준 준수 | Slither, Echidna, Manticore | [`slither-erc`](https://github.com/crytic/slither/wiki/ERC-Conformance) | + +목표에 따라 다른 영역도 확인해야 하지만, 이러한 대략적인 집중 영역은 모든 스마트 계약 시스템에 좋은 시작입니다. + +저희의 공개 감사에는 검증되거나 테스트된 속성의 예가 포함되어 있습니다. 실제 보안 속성을 검토하려면 다음 보고서의 `자동화된 테스트 및 검증` 섹션을 읽어보세요. + +- [0x](https://github.com/trailofbits/publications/blob/master/reviews/0x-protocol.pdf) +- [Balancer](https://github.com/trailofbits/publications/blob/master/reviews/BalancerCore.pdf) diff --git a/public/content/translations/ko/developers/tutorials/hello-world-smart-contract-fullstack/index.md b/public/content/translations/ko/developers/tutorials/hello-world-smart-contract-fullstack/index.md new file mode 100644 index 00000000000..5cf41cd7555 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/hello-world-smart-contract-fullstack/index.md @@ -0,0 +1,1542 @@ +--- +title: "초보자를 위한 Hello World 스마트 계약 - 풀스택" +description: "이더리움에 간단한 스마트 계약을 작성하고 배포하는 방법에 대한 입문 튜토리얼입니다." +author: "nstrike2" +tags: + [ + "솔리디티", + "hardhat", + "alchemy", + "스마트 계약", + "배포하기", + "블록 탐색기", + "프론트엔드", + "트랜잭션" + ] +skill: beginner +lang: ko +published: 2021-10-25 +--- + +이 가이드는 블록체인 개발을 처음 접하시거나 스마트 계약을 배포하고 상호 작용하는 방법을 모르는 분들을 위한 것입니다. [MetaMask](https://metamask.io), [Solidity](https://docs.soliditylang.org/en/v0.8.0/), [Hardhat](https://hardhat.org), [Alchemy](https://alchemy.com/eth)를 사용하여 Goerli 테스트 네트워크에 간단한 스마트 계약을 생성하고 배포하는 과정을 안내합니다. + +이 튜토리얼을 완료하려면 Alchemy 계정이 필요합니다. [무료 계정으로 가입하세요](https://www.alchemy.com/). + +궁금한 점이 있으면 언제든지 [Alchemy Discord](https://discord.gg/gWuC7zB)에 문의하세요! + +## 1부 - Hardhat을 사용하여 스마트 계약 생성 및 배포하기 {#part-1} + +### 이더리움 네트워크에 연결하기 {#connect-to-the-ethereum-network} + +이더리움 체인에 요청을 보내는 방법은 여러 가지가 있습니다. 간단하게 하기 위해, 자체적으로 노드를 실행하지 않고도 이더리움 체인과 통신할 수 있게 해주는 블록체인 개발자 플랫폼이자 API인 Alchemy의 무료 계정을 사용하겠습니다. Alchemy는 모니터링 및 분석을 위한 개발자 도구도 제공합니다. 이 튜토리얼에서 이러한 도구를 활용하여 스마트 계약 배포 시 내부적으로 어떤 일이 일어나는지 이해해 보겠습니다. + +### 앱과 API 키 만들기 {#create-your-app-and-api-key} + +Alchemy 계정을 생성한 후에는 앱을 만들어 API 키를 생성할 수 있습니다. 이를 통해 Goerli 테스트넷에 요청을 보낼 수 있습니다. 테스트넷에 익숙하지 않은 경우 [네트워크 선택에 대한 Alchemy 가이드](https://www.alchemy.com/docs/choosing-a-web3-network)를 읽어보세요. + +Alchemy 대시보드의 탐색 모음에서 **Apps** 드롭다운을 찾아 **Create App**을 클릭합니다. + +![Hello world create app](./hello-world-create-app.png) + +앱 이름을 '_Hello World_'로 지정하고 간단한 설명을 작성합니다. 환경으로 **Staging**을, 네트워크로 **Goerli**를 선택합니다. + +![create app view hello world](./create-app-view-hello-world.png) + +_참고: **Goerli**를 반드시 선택해야 합니다. 그렇지 않으면 이 튜토리얼이 작동하지 않습니다._ + +**앱 만들기**를 클릭하세요. 생성한 앱이 아래 표에 나타납니다. + +### 이더리움 계정 만들기 {#create-an-ethereum-account} + +트랜잭션을 보내고 받으려면 이더리움 계정이 필요합니다. 브라우저에서 이더리움 계정 주소를 관리할 수 있는 가상 지갑인 MetaMask를 사용하겠습니다. + +[여기](https://metamask.io/download)에서 MetaMask 계정을 무료로 다운로드하고 생성할 수 있습니다. 계정을 생성하거나 이미 계정이 있는 경우, 오른쪽 상단에서 'Goerli 테스트 네트워크'로 전환했는지 확인하세요(실제 돈을 다루지 않기 위함입니다). + +### 4단계: 파우셋에서 이더 추가하기 {#step-4-add-ether-from-a-faucet} + +테스트 네트워크에 스마트 계약을 배포하려면 가짜 ETH가 필요합니다. Goerli 네트워크에서 ETH를 받으려면, Goerli 파우셋으로 이동하여 Goerli 계정 주소를 입력하세요. 최근 Goerli 파우셋이 약간 불안정할 수 있습니다. [테스트 네트워크 페이지](/developers/docs/networks/#goerli)에서 시도해 볼 수 있는 옵션 목록을 확인하세요. + +_참고: 네트워크 혼잡으로 인해 시간이 다소 걸릴 수 있습니다._ +`` + +### 5단계: 잔액 확인하기 {#step-5-check-your-balance} + +지갑에 ETH가 있는지 다시 확인하기 위해 [Alchemy의 컴포저 도구](https://composer.alchemyapi.io/?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D)를 사용하여 [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) 요청을 보냅니다. 이것은 지갑에 있는 ETH의 양을 반환할 것입니다. 더 자세한 내용은 [컴포저 도구 사용법에 대한 Alchemy의 짧은 튜토리얼](https://youtu.be/r6sjRxBZJuU)을 확인하세요. + +MetaMask 계정 주소를 입력하고 **요청 보내기**를 클릭하세요. 아래 코드 스니펫과 같은 응답을 보게 될 것입니다. + +```json +{ "jsonrpc": "2.0", "id": 0, "result": "0x2B5E3AF16B1880000" } +``` + +> _참고: 이 결과는 ETH가 아닌 wei 단위입니다. Wei는 ether의 가장 작은 단위로 사용됩니다._ + +휴! 우리의 가짜 돈이 다 있군요. + +### 6단계: 프로젝트 초기화하기 {#step-6-initialize-our-project} + +먼저 프로젝트를 위한 폴더를 만들어야 합니다. 명령줄로 이동하여 다음을 입력하세요. + +``` +mkdir hello-world +cd hello-world +``` + +이제 프로젝트 폴더 안으로 들어왔으니, `npm init`을 사용하여 프로젝트를 초기화하겠습니다. + +> 아직 npm이 설치되지 않았다면 [Node.js 및 npm 설치 지침](https://docs.alchemyapi.io/alchemy/guides/alchemy-for-macs#1-install-nodejs-and-npm)을 따르세요. + +이 튜토리얼에서는 초기화 질문에 어떻게 대답하든 상관없습니다. 참고를 위해 다음과 같이 진행했습니다. + +``` +패키지 이름: (hello-world) +버전: (1.0.0) +설명: hello world 스마트 계약 +진입점: (index.js) +테스트 명령어: +git 저장소: +키워드: +작성자: +라이선스: (ISC) + +/Users/.../.../.../hello-world/package.json에 쓰기 직전: + +{ + "name": "hello-world", + "version": "1.0.0", + "description": "hello world 스마트 계약", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} +``` + +package.json을 승인하면 준비 완료입니다! + +### 7단계: Hardhat 다운로드하기 {#step-7-download-hardhat} + +Hardhat은 이더리움 소프트웨어를 컴파일, 배포, 테스트 및 디버그하기 위한 개발 환경입니다. 실제 블록체인에 배포하기 전에 로컬에서 스마트 컨트랙트 및 dApp을 구축할 때 사용됩니다. + +`hello-world` 프로젝트 내부에서 다음을 실행하세요: + +``` +npm install --save-dev hardhat +``` + +설치 지침에 대한 자세한 내용은 [이 페이지](https://hardhat.org/getting-started/#overview)를 확인하세요. + +### 8단계: Hardhat 프로젝트 생성하기 {#step-8-create-hardhat-project} + +`hello-world` 프로젝트 폴더 안에서 다음을 실행합니다: + +``` +npx hardhat +``` + +그러면 환영 메시지와 원하는 작업을 선택할 수 있는 옵션이 표시됩니다. "create an empty hardhat.config.js"를 선택합니다. + +``` +888 888 888 888 888 +888 888 888 888 888 +888 888 888 888 888 +8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 +888 888 "88b 888P" d88" 888 888 "88b "88b 888 +888 888 .d888888 888 888 888 888 888 .d888888 888 +888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. +888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 + +👷 Hardhat v2.0.11에 오신 것을 환영합니다 👷‍ + +무엇을 하시겠습니까? … +샘플 프로젝트 생성 +❯ 빈 hardhat.config.js 생성 +종료 +``` + +이렇게 하면 프로젝트에 `hardhat.config.js` 파일이 생성됩니다. 튜토리얼의 뒷부분에서 이 파일을 사용하여 프로젝트 설정을 지정할 것입니다. + +### 9단계: 프로젝트 폴더 추가하기 {#step-9-add-project-folders} + +프로젝트를 체계적으로 정리하기 위해 두 개의 새 폴더를 만들겠습니다. 명령줄에서 `hello-world` 프로젝트의 루트 디렉토리로 이동하여 다음을 입력합니다. + +``` +mkdir contracts +mkdir scripts +``` + +- `contracts/`는 hello world 스마트 계약 코드 파일을 보관할 곳입니다. +- `scripts/`는 계약을 배포하고 상호작용하기 위한 스크립트를 보관할 곳입니다. + +### 10단계: 계약 작성하기 {#step-10-write-our-contract} + +언제 코드를 작성하게 될지 궁금하실 겁니다. 바로 지금입니다! + +선호하는 편집기에서 hello-world 프로젝트를 여세요. 스마트 계약은 대부분 Solidity로 작성되며, 저희도 스마트 계약을 작성하는 데 Solidity를 사용할 것입니다.‌ + +1. `contracts` 폴더로 이동하여 `HelloWorld.sol`이라는 새 파일을 만듭니다. +2. 아래는 이 튜토리얼에서 사용할 샘플 Hello World 스마트 계약입니다. 아래 내용을 `HelloWorld.sol` 파일에 복사하세요. + +_참고: 이 계약이 어떤 역할을 하는지 이해하려면 주석을 꼭 읽어보세요._ + +``` +// 시맨틱 버저닝을 사용하여 Solidity 버전을 지정합니다. +// 자세히 알아보기: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma +pragma solidity >=0.7.3; + +// 'HelloWorld'라는 계약을 정의합니다. +// 계약은 함수와 데이터(그 상태)의 모음입니다. 배포되면, 계약은 이더리움 블록체인의 특정 주소에 위치하게 됩니다. 자세히 알아보기: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html +contract HelloWorld { + + //업데이트 함수가 호출될 때 발생합니다 + //스마트 계약 이벤트는 계약이 블록체인에서 발생한 일을 앱 프론트엔드에 전달하는 방법으로, 특정 이벤트를 '수신'하고 이벤트가 발생할 때 조치를 취할 수 있습니다. + event UpdatedMessages(string oldStr, string newStr); + + // 'string' 타입의 상태 변수 `message`를 선언합니다. + // 상태 변수는 값이 계약 스토리지에 영구적으로 저장되는 변수입니다. `public` 키워드는 변수를 계약 외부에서 액세스할 수 있도록 하고 다른 계약이나 클라이언트가 값을 액세스하기 위해 호출할 수 있는 함수를 생성합니다. + string public message; + + // 많은 클래스 기반 객체 지향 언어와 마찬가지로 생성자는 계약 생성 시에만 실행되는 특수 함수입니다. + // 생성자는 계약의 데이터를 초기화하는 데 사용됩니다. 자세히 알아보기:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors + constructor(string memory initMessage) { + + // 문자열 인수 `initMessage`를 수락하고 계약의 `message` 스토리지 변수에 값을 설정합니다). + message = initMessage; + } + + // 문자열 인수를 허용하고 `message` 스토리지 변수를 업데이트하는 공개 함수입니다. + function update(string memory newMessage) public { + string memory oldMsg = message; + message = newMessage; + emit UpdatedMessages(oldMsg, newMessage); + } +} +``` + +이것은 생성 시 메시지를 저장하는 기본적인 스마트 계약입니다. `update` 함수를 호출하여 업데이트할 수 있습니다. + +### 11단계: MetaMask와 Alchemy를 프로젝트에 연결하기 {#step-11-connect-metamask-alchemy-to-your-project} + +MetaMask 지갑, Alchemy 계정을 만들고 스마트 계약을 작성했습니다. 이제 이 세 가지를 연결할 차례입니다. + +지갑에서 전송되는 모든 트랜잭션은 고유한 개인 키를 사용한 서명이 필요합니다. 프로그램에 이 권한을 제공하기 위해, 개인 키를 환경 파일에 안전하게 저장할 수 있습니다. 여기에 Alchemy용 API 키도 저장할 것입니다. + +> 트랜잭션 전송에 대해 더 알아보려면 web3를 사용한 트랜잭션 전송에 관한 [이 튜토리얼](https://www.alchemy.com/docs/hello-world-smart-contract#step-11-connect-metamask--alchemy-to-your-project)을 확인하세요. + +먼저 프로젝트 디렉터리에 dotenv 패키지를 설치합니다. + +``` +npm install dotenv --save +``` + +그런 다음, 프로젝트의 루트 디렉터리에 `.env` 파일을 생성합니다. 여기에 MetaMask 개인 키와 HTTP Alchemy API URL을 추가합니다. + +환경 파일의 이름은 `.env`여야 합니다. 그렇지 않으면 환경 파일로 인식되지 않습니다. + +`process.env`나 `.env-custom` 또는 다른 이름으로 지정하지 마세요. + +- 개인 키를 내보내려면 [다음 지침](https://metamask.zendesk.com/hc/en-us/articles/360015289632-How-to-Export-an-Account-Private-Key)을 따르세요. +- HTTP Alchemy API URL을 얻으려면 아래를 참조하세요. + +![](./get-alchemy-api-key.gif) + +`.env` 파일은 다음과 같아야 합니다: + +``` +API_URL = "https://eth-goerli.alchemyapi.io/v2/your-api-key" +PRIVATE_KEY = "your-metamask-private-key" +``` + +이를 실제로 코드에 연결하기 위해 13단계의 `hardhat.config.js` 파일에서 이러한 변수를 참조합니다. + +### 12단계: Ethers.js 설치하기 {#step-12-install-ethersjs} + +Ethers.js는 [표준 JSON-RPC 메서드](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc)를 보다 사용자 친화적인 메서드로 래핑하여 이더리움과 더 쉽게 상호 작용하고 요청할 수 있도록 하는 라이브러리입니다. + +Hardhat은 추가적인 툴링 및 확장된 기능을 위해 [플러그인](https://hardhat.org/plugins/)을 통합할 수 있게 해줍니다. 계약 배포를 위해 [Ethers 플러그인](https://hardhat.org/docs/plugins/official-plugins#hardhat-ethers)을 활용할 것입니다. + +프로젝트 디렉토리에 다음을 입력합니다. + +```bash +npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0" +``` + +### 13단계: hardhat.config.js 업데이트하기 {#step-13-update-hardhat-configjs} + +지금까지 여러 종속성과 플러그인을 추가했습니다. 이제 프로젝트가 이 모든 것을 인식할 수 있도록 `hardhat.config.js`를 업데이트해야 합니다. + +`hardhat.config.js`를 다음과 같이 업데이트하세요: + +```javascript +/** + * @type import('hardhat/config').HardhatUserConfig + */ + +require("dotenv").config() +require("@nomiclabs/hardhat-ethers") + +const { API_URL, PRIVATE_KEY } = process.env + +module.exports = { + solidity: "0.7.3", + defaultNetwork: "goerli", + networks: { + hardhat: {}, + goerli: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`], + }, + }, +} +``` + +### 14단계: 계약 컴파일하기 {#step-14-compile-our-contract} + +지금까지 모든 것이 제대로 작동하는지 확인하기 위해 스마트 컨트랙트를 컴파일해 보겠습니다. `compile` 작업은 hardhat에 내장된 작업 중 하나입니다. + +명령줄에서 다음을 실행합니다. + +```bash +npx hardhat compile +``` + +`SPDX license identifier not provided in source file`에 대한 경고가 표시될 수 있지만 걱정할 필요는 없습니다. 다른 모든 것이 잘 보이기를 바랍니다! 그렇지 않은 경우 언제든지 [Alchemy discord](https://discord.gg/u72VCg3)로 메시지를 보내주세요. + +### 15단계: 배포 스크립트 작성하기 {#step-15-write-our-deploy-script} + +이제 스마트 컨트랙트가 작성되고 설정 파일을 사용할 수 있으므로 스마트 컨트랙트 배포 스크립트를 작성할 차례입니다. + +`scripts/` 폴더로 이동하여 `deploy.js`라는 새 파일을 만들고 다음 내용을 추가하세요. + +```javascript +async function main() { + const HelloWorld = await ethers.getContractFactory("HelloWorld") + + // 배포를 시작하고, 계약 객체로 확인되는 프라미스를 반환합니다 + const hello_world = await HelloWorld.deploy("Hello World!") + console.log("계약이 배포된 주소:", hello_world.address) +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error) + process.exit(1) + }) +``` + +Hardhat은 [계약 튜토리얼](https://hardhat.org/tutorial/testing-contracts.html#writing-tests)에서 각 코드 라인이 무엇을 하는지 훌륭하게 설명하고 있으며, 저희는 그 설명을 여기에 채택했습니다. + +```javascript +const HelloWorld = await ethers.getContractFactory("HelloWorld") +``` + +ethers.js의 `ContractFactory`는 새로운 스마트 계약을 배포하는 데 사용되는 추상화이므로, 여기서 `HelloWorld`는 [팩토리](https://en.wikipedia.org/wiki/Factory_\(object-oriented_programming\)) 계약 인스턴스를 위한 팩토리입니다. `hardhat-ethers` 플러그인을 사용할 때 `ContractFactory` 및 `Contract` 인스턴스는 기본적으로 첫 번째 서명자(소유자)에게 연결됩니다. + +```javascript +const hello_world = await HelloWorld.deploy() +``` + +`ContractFactory`에서 `deploy()`를 호출하면 배포가 시작되고, `Contract` 객체로 확인되는 `Promise`를 반환합니다. 이것은 각 스마트 컨트랙트 기능에 대한 메소드가 있는 개체입니다. + +### 16단계: 계약 배포하기 {#step-16-deploy-our-contract} + +마침내 스마트 컨트랙트를 배포할 준비가 되었습니다! 명령줄로 이동하여 다음을 실행하세요: + +```bash +npx hardhat run scripts/deploy.js --network goerli +``` + +그러면 다음과 같은 내용이 표시됩니다. + +```bash +계약이 배포된 주소: 0x6cd7d44516a20882cEa2DE9f205bF401c0d23570 +``` + +**이 주소를 저장해주세요**. 튜토리얼 뒷부분에서 사용할 것입니다. + +[Goerli Etherscan](https://goerli.etherscan.io)으로 이동하여 계약 주소를 검색하면 성공적으로 배포된 것을 볼 수 있습니다. 트랜잭션은 다음과 같습니다. + +![](./etherscan-contract.png) + +`From` 주소는 MetaMask 계정 주소와 일치해야 하며, `To` 주소는 **계약 생성**이라고 표시됩니다. 트랜잭션을 클릭하면 `To` 필드에 계약 주소가 표시됩니다. + +![](./etherscan-transaction.png) + +축하해요! 방금 이더리움 테스트넷에 스마트 계약을 배포했습니다. + +내부적으로 어떤 일이 일어나고 있는지 이해하기 위해 [Alchemy 대시보드](https://dashboard.alchemy.com/explorer)의 Explorer 탭으로 이동해 보겠습니다. 여러 개의 Alchemy 앱이 있는 경우 앱별로 필터링하고 **Hello World**를 선택해야 합니다. + +![](./hello-world-explorer.png) + +여기에서 `.deploy()` 함수를 호출했을 때 Hardhat/Ethers가 내부적으로 수행한 몇 가지 JSON-RPC 메서드를 볼 수 있습니다. 여기서 중요한 두 가지 메서드는 계약을 Goerli 체인에 작성하는 요청인 [`eth_sendRawTransaction`](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_sendrawtransaction)과, 해시가 주어졌을 때 트랜잭션에 대한 정보를 읽는 요청인 [`eth_getTransactionByHash`](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_gettransactionbyhash)입니다. 트랜잭션 전송에 대해 더 자세히 알아보려면 [Web3를 사용한 트랜잭션 전송 튜토리얼](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)을 확인하세요. + +## 2부: 스마트 계약과 상호작용하기 {#part-2-interact-with-your-smart-contract} + +이제 Goerli 네트워크에 스마트 계약을 성공적으로 배포했으니, 이와 상호 작용하는 방법을 알아보겠습니다. + +### interact.js 파일 생성하기 {#create-a-interactjs-file} + +이 파일은 상호 작용 스크립트를 작성할 파일입니다. 1부에서 이전에 설치한 Ethers.js 라이브러리를 사용할 것입니다. + +`scripts/` 폴더 안에 `interact.js`라는 새 파일을 만들고 다음 코드를 추가합니다. + +```javascript +// interact.js + +const API_KEY = process.env.API_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY +const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS +``` + +### .env 파일 업데이트하기 {#update-your-env-file} + +새로운 환경 변수를 사용할 것이므로 [이전에 생성한](#step-11-connect-metamask-&-alchemy-to-your-project) `.env` 파일에 정의해야 합니다. + +Alchemy `API_KEY`와 스마트 계약이 배포된 `CONTRACT_ADDRESS`에 대한 정의를 추가해야 합니다. + +`.env` 파일은 다음과 같아야 합니다. + +```bash +# .env + +API_URL = "https://eth-goerli.alchemyapi.io/v2/" +API_KEY = "" +PRIVATE_KEY = "" +CONTRACT_ADDRESS = "0x" +``` + +### 계약 ABI 가져오기 {#grab-your-contract-ABI} + +계약 [ABI(애플리케이션 바이너리 인터페이스)](/glossary/#abi)는 스마트 계약과 상호 작용하기 위한 인터페이스입니다. Hardhat은 자동으로 ABI를 생성하여 `HelloWorld.json`에 저장합니다. ABI를 사용하려면 `interact.js` 파일에 다음 코드 줄을 추가하여 내용을 구문 분석해야 합니다. + +```javascript +// interact.js +const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json") +``` + +ABI를 보려면 콘솔에 출력할 수 있습니다. + +```javascript +console.log(JSON.stringify(contract.abi)) +``` + +ABI가 콘솔에 출력되는 것을 보려면 터미널로 이동하여 다음을 실행하세요. + +```bash +npx hardhat run scripts/interact.js +``` + +### 계약 인스턴스 생성하기 {#create-an-instance-of-your-contract} + +계약과 상호 작용하려면 코드에서 계약 인스턴스를 생성해야 합니다. Ethers.js로 이를 수행하려면 세 가지 개념을 다루어야 합니다. + +1. 제공자 - 블록체인에 대한 읽기 및 쓰기 접근 권한을 제공하는 노드 제공자 +2. 서명자 - 트랜잭션에 서명할 수 있는 이더리움 계정을 나타냅니다 +3. 계약 - 온체인에 배포된 특정 계약을 나타내는 Ethers.js 객체 + +이전 단계의 계약 ABI를 사용하여 계약 인스턴스를 생성합니다. + +```javascript +// interact.js + +// 제공자 +const alchemyProvider = new ethers.providers.AlchemyProvider( + (network = "goerli"), + API_KEY +) + +// 서명자 +const signer = new ethers.Wallet(PRIVATE_KEY, alchemyProvider) + +// 계약 +const helloWorldContract = new ethers.Contract( + CONTRACT_ADDRESS, + contract.abi, + signer +) +``` + +제공자, 서명자, 계약에 대한 자세한 내용은 [ethers.js 개발문서](https://docs.ethers.io/v5/)에서 알아보세요. + +### 초기 메시지 읽기 {#read-the-init-message} + +`initMessage = "Hello world!"`로 계약을 배포했던 것을 기억하시나요? 이제 스마트 계약에 저장된 메시지를 읽어 콘솔에 출력할 것입니다. + +JavaScript에서는 네트워크와 상호 작용할 때 비동기 함수가 사용됩니다. 비동기 함수에 대해 더 알아보려면 [이 미디엄 기사](https://blog.bitsrc.io/understanding-asynchronous-javascript-the-event-loop-74cd408419ff)를 읽어보세요. + +아래 코드를 사용하여 스마트 계약에서 `message` 함수를 호출하고 초기 메시지를 읽으세요. + +```javascript +// interact.js + +// ... + +async function main() { + const message = await helloWorldContract.message() + console.log("메시지는: " + message) +} +main() +``` + +터미널에서 `npx hardhat run scripts/interact.js`를 사용하여 파일을 실행한 후 다음과 같은 응답을 볼 수 있어야 합니다. + +``` +메시지는: Hello world! +``` + +축하해요! 이더리움 블록체인에서 스마트 계약 데이터를 성공적으로 읽었습니다. 잘하셨습니다! + +### 메시지 업데이트하기 {#update-the-message} + +단순히 메시지를 읽는 대신, `update` 함수를 사용하여 스마트 계약에 저장된 메시지를 업데이트할 수도 있습니다! 꽤 멋지죠? + +메시지를 업데이트하려면 인스턴스화된 계약 객체에서 `update` 함수를 직접 호출할 수 있습니다. + +```javascript +// interact.js + +// ... + +async function main() { + const message = await helloWorldContract.message() + console.log("메시지는: " + message) + + console.log("메시지 업데이트 중...") + const tx = await helloWorldContract.update("This is the new message.") + await tx.wait() +} +main() +``` + +11번째 줄에서 반환된 트랜잭션 객체에 대해 `.wait()`를 호출하는 것을 주목하세요. 이를 통해 스크립트가 함수를 종료하기 전에 트랜잭션이 블록체인에서 채굴될 때까지 기다리게 됩니다. `.wait()` 호출이 포함되지 않으면 스크립트가 계약에서 업데이트된 `message` 값을 보지 못할 수 있습니다. + +### 새 메시지 읽기 {#read-the-new-message} + +[이전 단계](#read-the-init-message)를 반복하여 업데이트된 `message` 값을 읽을 수 있어야 합니다. 잠시 시간을 내어 새 값을 출력하는 데 필요한 변경 사항을 적용해 보세요! + +힌트가 필요하다면, 이 시점에서 `interact.js` 파일은 다음과 같아야 합니다. + +```javascript +// interact.js + +const API_KEY = process.env.API_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY +const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS + +const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json") + +// 제공자 - Alchemy +const alchemyProvider = new ethers.providers.AlchemyProvider( + (network = "goerli"), + API_KEY +) + +// 서명자 - 당신 +const signer = new ethers.Wallet(PRIVATE_KEY, alchemyProvider) + +// 계약 인스턴스 +const helloWorldContract = new ethers.Contract( + CONTRACT_ADDRESS, + contract.abi, + signer +) + +async function main() { + const message = await helloWorldContract.message() + console.log("메시지는: " + message) + + console.log("메시지 업데이트 중...") + const tx = await helloWorldContract.update("this is the new message") + await tx.wait() + + const newMessage = await helloWorldContract.message() + console.log("새 메시지는: " + newMessage) +} + +main() +``` + +이제 스크립트를 실행하면 이전 메시지, 업데이트 상태, 그리고 새 메시지가 터미널에 출력되는 것을 볼 수 있습니다! + +`npx hardhat run scripts/interact.js --network goerli` + +``` +메시지: Hello World! +메시지 업데이트 중... +새 메시지: This is the new message. +``` + +스크립트를 실행하는 동안, 새 메시지가 로드되기 전에 `메시지 업데이트 중...` 단계가 로드되는 데 시간이 걸리는 것을 알 수 있습니다. 이는 채굴 과정 때문입니다. 채굴 중인 트랜잭션을 추적하는 데 관심이 있다면 [Alchemy 멤풀](https://dashboard.alchemyapi.io/mempool)을 방문하여 트랜잭션 상태를 확인하세요. 트랜잭션이 중단된 경우, [Goerli Etherscan](https://goerli.etherscan.io)을 확인하고 트랜잭션 해시를 검색하는 것도 도움이 됩니다. + +## 3부: Etherscan에 스마트 계약 게시하기 {#part-3-publish-your-smart-contract-to-etherscan} + +스마트 계약을 현실로 만드는 힘든 작업을 모두 마쳤습니다. 이제 세상과 공유할 시간입니다! + +Etherscan에서 스마트 계약을 확인하면 누구나 소스 코드를 보고 스마트 계약과 상호 작용할 수 있습니다. 시작해 보겠습니다! + +### 1단계: Etherscan 계정에서 API 키 생성하기 {#step-1-generate-an-api-key-on-your-etherscan-account} + +게시하려는 스마트 계약을 소유하고 있음을 확인하려면 Etherscan API 키가 필요합니다. + +아직 Etherscan 계정이 없다면 [계정을 등록하세요](https://etherscan.io/register). + +로그인한 후 탐색 모음에서 사용자 이름을 찾아 마우스를 올리고 **내 프로필** 버튼을 선택합니다. + +프로필 페이지에 측면 탐색 모음이 표시됩니다. 측면 탐색 모음에서 **API 키**를 선택합니다. 다음으로 '추가' 버튼을 눌러 새 API 키를 만들고, 앱 이름을 **hello-world**로 지정한 다음 **새 API 키 만들기** 버튼을 누릅니다. + +새로운 API 키가 API 키 표에 나타납니다. API 키를 클립보드에 복사합니다. + +다음으로 Etherscan API 키를 `.env` 파일에 추가해야 합니다. + +추가한 후 `.env` 파일은 다음과 같아야 합니다. + +```javascript +API_URL = "https://eth-goerli.alchemyapi.io/v2/your-api-key" +PUBLIC_KEY = "your-public-account-address" +PRIVATE_KEY = "your-private-account-address" +CONTRACT_ADDRESS = "your-contract-address" +ETHERSCAN_API_KEY = "your-etherscan-key" +``` + +### Hardhat 배포 스마트 계약 {#hardhat-deployed-smart-contracts} + +#### hardhat-etherscan 설치하기 {#install-hardhat-etherscan} + +Hardhat을 사용하여 Etherscan에 계약을 게시하는 것은 간단합니다. 시작하려면 먼저 `hardhat-etherscan` 플러그인을 설치해야 합니다. `hardhat-etherscan`은 Etherscan에서 스마트 계약의 소스 코드와 ABI를 자동으로 확인합니다. 이를 추가하려면 `hello-world` 디렉터리에서 다음을 실행합니다. + +```text +npm install --save-dev @nomiclabs/hardhat-etherscan +``` + +설치되면 `hardhat.config.js` 상단에 다음 문을 포함하고 Etherscan 구성 옵션을 추가합니다. + +```javascript +// hardhat.config.js + +require("dotenv").config() +require("@nomiclabs/hardhat-ethers") +require("@nomiclabs/hardhat-etherscan") + +const { API_URL, PRIVATE_KEY, ETHERSCAN_API_KEY } = process.env + +module.exports = { + solidity: "0.7.3", + defaultNetwork: "goerli", + networks: { + hardhat: {}, + goerli: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`], + }, + }, + etherscan: { + // Etherscan용 API 키 + // https://etherscan.io/에서 하나를 얻으십시오. + apiKey: ETHERSCAN_API_KEY, + }, +} +``` + +#### Etherscan에서 스마트 계약 확인하기 {#verify-your-smart-contract-on-etherscan} + +모든 파일이 저장되고 모든 `.env` 변수가 올바르게 구성되었는지 확인하세요. + +`verify` 작업을 실행하여 계약 주소와 배포된 네트워크를 전달합니다. + +```text +npx hardhat verify --network goerli DEPLOYED_CONTRACT_ADDRESS 'Hello World!' +``` + +`DEPLOYED_CONTRACT_ADDRESS`가 Goerli 테스트 네트워크에 배포된 스마트 계약의 주소인지 확인하세요. 또한 마지막 인수(`'Hello World!'`)는 [1부의 배포 단계](#write-our-deploy-script)에서 사용한 문자열 값과 동일해야 합니다. + +모든 것이 순조롭게 진행되면 터미널에 다음 메시지가 표시됩니다. + +```text +계약 소스 코드가 성공적으로 제출되었습니다 +contracts/HelloWorld.sol:HelloWorld at 0xdeployed-contract-address +Etherscan에서 확인을 위해 대기 중... 확인 결과를 기다리는 중... + + +Etherscan에서 HelloWorld 계약을 성공적으로 확인했습니다. +https://goerli.etherscan.io/address/#contracts +``` + +축하해요! 스마트 계약 코드가 Etherscan에 있습니다! + +### Etherscan에서 스마트 계약을 확인하세요! {#check-out-your-smart-contract-on-etherscan} + +터미널에 제공된 링크로 이동하면 Etherscan에 게시된 스마트 계약 코드와 ABI를 볼 수 있습니다! + +**와우, 해내셨군요!** 이제 누구나 당신의 스마트 계약을 호출하거나 쓸 수 있습니다! **다음에 무엇을 만들지 기대됩니다!** + +## 4부 - 스마트 계약을 프런트엔드와 통합하기 {#part-4-integrating-your-smart-contract-with-the-frontend} + +이 튜토리얼을 마치면 다음을 알게 될 것입니다. + +- MetaMask 지갑을 탈중앙화앱에 연결 +- [Alchemy Web3](https://docs.alchemy.com/alchemy/documentation/alchemy-web3) API를 사용하여 스마트 계약에서 데이터 읽기 +- MetaMask를 사용하여 이더리움 트랜잭션 서명하기 + +이 탈중앙화앱에서는 프런트엔드 프레임워크로 [React](https://react.dev/)를 사용할 것입니다. 하지만 저희는 주로 프로젝트에 웹3 기능을 도입하는 데 중점을 둘 것이므로 React의 기초를 자세히 다루지는 않을 것입니다. + +전제 조건으로 React에 대한 초급 수준의 이해가 있어야 합니다. 그렇지 않은 경우 공식 [React 소개 튜토리얼](https://react.dev/learn)을 완료하는 것이 좋습니다. + +### 스타터 파일 복제하기 {#clone-the-starter-files} + +먼저 [hello-world-part-four GitHub 저장소](https://github.com/alchemyplatform/hello-world-part-four-tutorial)로 이동하여 이 프로젝트의 시작 파일을 가져오고 이 저장소를 로컬 컴퓨터에 복제합니다. + +복제된 저장소를 로컬에서 엽니다. 두 개의 폴더, 즉 `starter-files`와 `completed`가 포함되어 있음을 알 수 있습니다. + +- `starter-files` - **이 디렉토리에서 작업할 것입니다**. UI를 이더리움 지갑과 [3부](#part-3)에서 Etherscan에 게시한 스마트 계약에 연결할 것입니다. +- `completed`는 전체 튜토리얼을 포함하며 막혔을 경우에만 참고용으로 사용해야 합니다. + +다음으로, `starter-files` 사본을 즐겨 사용하는 코드 편집기에서 열고 `src` 폴더로 이동합니다. + +우리가 작성할 모든 코드는 `src` 폴더 아래에 위치하게 됩니다. 프로젝트에 웹3 기능을 부여하기 위해 `HelloWorld.js` 구성 요소와 `util/interact.js` JavaScript 파일을 편집할 것입니다. + +### 시작 파일 확인하기 {#check-out-the-starter-files} + +코딩을 시작하기 전에 시작 파일에 무엇이 제공되는지 살펴보겠습니다. + +#### React 프로젝트 실행하기 {#get-your-react-project-running} + +먼저 브라우저에서 React 프로젝트를 실행해 봅시다. React의 장점은 일단 브라우저에서 프로젝트가 실행되면, 저장하는 모든 변경 사항이 브라우저에 실시간으로 업데이트된다는 것입니다. + +프로젝트를 실행하려면 `starter-files` 폴더의 루트 디렉터리로 이동하고 터미널에서 `npm install`을 실행하여 프로젝트의 종속성을 설치합니다. + +```bash +cd starter-files +npm install +``` + +설치가 완료되면 터미널에서 `npm start`를 실행합니다: + +```bash +npm start +``` + +이렇게 하면 브라우저에서 [http://localhost:3000/](http://localhost:3000/)이 열리고 프로젝트의 프런트엔드가 표시됩니다. 하나의 필드(스마트 계약에 저장된 메시지를 업데이트하는 곳), "지갑 연결" 버튼, "업데이트" 버튼으로 구성되어야 합니다. + +두 버튼 중 하나를 클릭해 보면 작동하지 않는 것을 알 수 있습니다. 아직 해당 기능을 프로그래밍해야 하기 때문입니다. + +#### `HelloWorld.js` 구성 요소 {#the-helloworld-js-component} + +편집기에서 `src` 폴더로 다시 돌아가 `HelloWorld.js` 파일을 엽니다. 이 파일은 우리가 작업할 기본 React 컴포넌트이므로 파일의 모든 내용을 이해하는 것이 매우 중요합니다. + +이 파일의 맨 위에는 React 라이브러리, useEffect 및 useState 후크, `./util/interact.js`의 일부 항목(곧 자세히 설명하겠습니다!), 그리고 Alchemy 로고를 포함하여 프로젝트를 실행하는 데 필요한 여러 가져오기 문이 있습니다. + +```javascript +// HelloWorld.js + +import React from "react" +import { useEffect, useState } from "react" +import { + helloWorldContract, + connectWallet, + updateMessage, + loadCurrentMessage, + getCurrentWalletConnected, +} from "./util/interact.js" + +import alchemylogo from "./alchemylogo.svg" +``` + +다음으로, 특정 이벤트 후에 업데이트할 상태 변수가 있습니다. + +```javascript +// HelloWorld.js + +//상태 변수 +const [walletAddress, setWallet] = useState("") +const [status, setStatus] = useState("") +const [message, setMessage] = useState("네트워크에 연결되지 않았습니다.") +const [newMessage, setNewMessage] = useState("") +``` + +각 변수가 나타내는 내용은 다음과 같습니다. + +- `walletAddress` - 사용자의 지갑 주소를 저장하는 문자열 +- `status`- 사용자가 탈중앙화앱과 상호 작용하는 방법을 안내하는 유용한 메시지를 저장하는 문자열 +- `message` - 스마트 계약의 현재 메시지를 저장하는 문자열 +- `newMessage` - 스마트 계약에 작성될 새 메시지를 저장하는 문자열 + +상태 변수 다음에는 아직 구현되지 않은 다섯 개의 함수가 보입니다: `useEffect`, `addSmartContractListener`, `addWalletListener`, `connectWalletPressed`, `onUpdatePressed`. 각 함수의 역할은 다음과 같습니다. + +```javascript +// HelloWorld.js + +//한 번만 호출됨 +useEffect(async () => { + //TODO: 구현 +}, []) + +function addSmartContractListener() { + //TODO: 구현 +} + +function addWalletListener() { + //TODO: 구현 +} + +const connectWalletPressed = async () => { + //TODO: 구현 +} + +const onUpdatePressed = async () => { + //TODO: 구현 +} +``` + +- [`useEffect`](https://legacy.reactjs.org/docs/hooks-effect.html) - 컴포넌트가 렌더링된 후 호출되는 React 후크입니다. 빈 배열 `[]` 속성이 전달되었기 때문에(4번째 줄 참조), 컴포넌트의 _첫 번째_ 렌더링에서만 호출됩니다. 여기에서는 스마트 계약에 저장된 현재 메시지를 로드하고, 스마트 계약 및 지갑 리스너를 호출하며, 지갑이 이미 연결되었는지 여부를 반영하도록 UI를 업데이트합니다. +- `addSmartContractListener` - 이 함수는 HelloWorld 계약의 `UpdatedMessages` 이벤트를 감시하고 스마트 계약의 메시지가 변경될 때 UI를 업데이트하는 리스너를 설정합니다. +- `addWalletListener` - 이 함수는 사용자가 지갑을 연결 해제하거나 주소를 전환하는 등 사용자의 MetaMask 지갑 상태 변경을 감지하는 리스너를 설정합니다. +- `connectWalletPressed` - 이 함수는 사용자의 MetaMask 지갑을 탈중앙화앱에 연결할 때 호출됩니다. +- `onUpdatePressed` - 이 함수는 사용자가 스마트 계약에 저장된 메시지를 업데이트하고 싶을 때 호출됩니다. + +이 파일의 끝부분에는 컴포넌트의 UI가 있습니다. + +```javascript +// HelloWorld.js + +//컴포넌트의 UI +return ( +
+ + + +

현재 메시지:

+

{message}

+ +

새 메시지:

+ +
+ setNewMessage(e.target.value)} + value={newMessage} + /> +

{status}

+ + +
+ +
+) +``` + +이 코드를 자세히 살펴보면 UI에서 다양한 상태 변수를 사용하는 위치를 알 수 있습니다. + +- 6-12줄에서 사용자의 지갑이 연결된 경우(즉, `walletAddress.length > 0`), ID가 "walletButton"인 버튼에 사용자의 `walletAddress`의 축약된 버전을 표시하고, 그렇지 않으면 "지갑 연결"이라고만 표시합니다. +- 17번 줄에는 `message` 문자열에 캡처된 스마트 계약에 저장된 현재 메시지를 표시합니다. +- 23-26줄에서는 [제어된 컴포넌트](https://legacy.reactjs.org/docs/forms.html#controlled-components)를 사용하여 텍스트 필드의 입력이 변경될 때 `newMessage` 상태 변수를 업데이트합니다. + +상태 변수 외에도 `publishButton` 및 `walletButton` ID를 가진 버튼을 각각 클릭하면 `connectWalletPressed` 및 `onUpdatePressed` 함수가 호출되는 것을 볼 수 있습니다. + +마지막으로, 이 `HelloWorld.js` 컴포넌트가 어디에 추가되는지 알아보겠습니다. + +React의 메인 컴포넌트로 다른 모든 컴포넌트의 컨테이너 역할을 하는 `App.js` 파일로 이동하면, 7번째 줄에 `HelloWorld.js` 컴포넌트가 주입된 것을 볼 수 있습니다. + +마지막으로, 제공된 또 다른 파일인 `interact.js` 파일을 확인해 보겠습니다. + +#### `interact.js` 파일 {#the-interact-js-file} + +[M-V-C](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 패러다임을 따르기 위해, 탈중앙화앱의 로직, 데이터, 규칙을 관리하는 모든 함수를 포함하는 별도의 파일을 원할 것입니다. 그리고 이 함수들을 프런트엔드(`HelloWorld.js` 컴포넌트)로 내보낼 수 있어야 합니다. + +👆🏽이것이 바로 `interact.js` 파일의 정확한 목적입니다! + +`src` 디렉터리의 `util` 폴더로 이동하면, 모든 스마트 계약 상호 작용 및 지갑 함수와 변수를 포함할 `interact.js`라는 파일이 포함된 것을 볼 수 있습니다. + +```javascript +// interact.js + +//export const helloWorldContract; + +export const loadCurrentMessage = async () => {} + +export const connectWallet = async () => {} + +const getCurrentWalletConnected = async () => {} + +export const updateMessage = async (message) => {} +``` + +파일 상단에서 `helloWorldContract` 객체가 주석 처리된 것을 볼 수 있습니다. 이 튜토리얼의 뒷부분에서 이 객체의 주석을 해제하고 이 변수에 스마트 계약을 인스턴스화한 다음, `HelloWorld.js` 컴포넌트로 내보낼 것입니다. + +`helloWorldContract` 객체 뒤에 있는 네 개의 미구현 함수는 다음을 수행합니다. + +- `loadCurrentMessage` - 이 함수는 스마트 계약에 저장된 현재 메시지를 로드하는 로직을 처리합니다. [Alchemy Web3 API](https://github.com/alchemyplatform/alchemy-web3)를 사용하여 Hello World 스마트 계약에 _읽기_ 호출을 합니다. +- `connectWallet` - 이 함수는 사용자의 MetaMask를 탈중앙화앱에 연결합니다. +- `getCurrentWalletConnected` - 이 함수는 페이지 로드 시 이더리움 계정이 이미 탈중앙화앱에 연결되어 있는지 확인하고 그에 따라 UI를 업데이트합니다. +- `updateMessage` - 이 함수는 스마트 계약에 저장된 메시지를 업데이트합니다. Hello World 스마트 계약에 _쓰기_ 호출을 하므로, 사용자의 MetaMask 지갑은 메시지를 업데이트하기 위해 이더리움 트랜잭션에 서명해야 합니다. + +이제 무엇을 다루고 있는지 이해했으니, 스마트 계약에서 어떻게 읽어오는지 알아봅시다! + +### 3단계: 스마트 계약에서 데이터 읽기 {#step-3-read-from-your-smart-contract} + +스마트 계약에서 읽으려면 다음을 성공적으로 설정해야 합니다. + +- 이더리움 체인에 대한 API 연결 +- 로드된 스마트 계약 인스턴스 +- 스마트 계약 함수를 호출하는 함수 +- 스마트 계약에서 읽고 있는 데이터가 변경될 때 업데이트를 감시하는 리스너 + +많은 단계처럼 들릴 수 있지만, 걱정하지 마세요! 각 단계를 하나씩 안내해 드리겠습니다! :\) + +#### 이더리움 체인에 API 연결 설정하기 {#establish-an-api-connection-to-the-ethereum-chain} + +이 튜토리얼 2부에서 [Alchemy Web3 키를 사용하여 스마트 계약에서 읽어왔던](https://docs.alchemy.com/alchemy/tutorials/hello-world-smart-contract/interacting-with-a-smart-contract#step-1-install-web3-library) 것을 기억하시나요? 체인에서 읽기 위해 탈중앙화앱에서도 Alchemy 웹3 키가 필요합니다. + +아직 설치하지 않았다면, `starter-files`의 루트 디렉터리로 이동하여 터미널에서 다음을 실행하여 [Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3)를 설치하세요. + +```text +npm install @alch/alchemy-web3 +``` + +[Alchemy Web3](https://github.com/alchemyplatform/alchemy-web3)는 [Web3.js](https://docs.web3js.org/)의 래퍼로, 향상된 API 메서드와 기타 중요한 이점을 제공하여 웹3 개발자로서의 삶을 더 쉽게 만듭니다. 최소한의 구성만으로 바로 앱에서 사용할 수 있도록 설계되었습니다! + +그런 다음 프로젝트 디렉터리에 [dotenv](https://www.npmjs.com/package/dotenv) 패키지를 설치하여 가져온 후 API 키를 안전하게 보관할 수 있도록 합니다. + +```text +npm install dotenv --save +``` + +탈중앙화앱에서는 HTTP API 키 대신 **웹소켓 API 키를 사용할 것**입니다. 이를 통해 스마트 계약에 저장된 메시지가 변경될 때 감지하는 리스너를 설정할 수 있습니다. + +API 키를 받으면 루트 디렉터리에 `.env` 파일을 만들고 Alchemy 웹소켓 URL을 추가하세요. 그 후 `.env` 파일은 다음과 같이 보일 것입니다. + +```javascript +REACT_APP_ALCHEMY_KEY = wss://eth-goerli.ws.alchemyapi.io/v2/ +``` + +이제 탈중앙화앱에서 Alchemy 웹3 엔드포인트를 설정할 준비가 되었습니다! 이제 `util` 폴더 안에 있는 `interact.js`로 돌아가서 파일 상단에 다음 코드를 추가합니다. + +```javascript +// interact.js + +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) + +//export const helloWorldContract; +``` + +위에서는 먼저 `.env` 파일에서 Alchemy 키를 가져온 다음 `alchemyKey`를 `createAlchemyWeb3`에 전달하여 Alchemy 웹3 엔드포인트를 설정했습니다. + +이 엔드포인트가 준비되었으니, 이제 스마트 계약을 로드할 시간입니다! + +#### Hello World 스마트 계약 로드하기 {#loading-your-hello-world-smart-contract} + +Hello World 스마트 계약을 로드하려면 계약 주소와 ABI가 필요하며, [이 튜토리얼 3부](/developers/tutorials/hello-world-smart-contract-fullstack/#part-3-publish-your-smart-contract-to-etherscan-part-3-publish-your-smart-contract-to-etherscan)를 완료했다면 두 가지 모두 Etherscan에서 찾을 수 있습니다. + +#### Etherscan에서 계약 ABI를 가져오는 방법 {#how-to-get-your-contract-abi-from-etherscan} + +이 튜토리얼의 3부를 건너뛰었다면 [0x6f3f635A9762B47954229Ea479b4541eAF402A6A](https://goerli.etherscan.io/address/0x6f3f635a9762b47954229ea479b4541eaf402a6a#code) 주소의 HelloWorld 계약을 사용할 수 있습니다. ABI는 [여기](https://goerli.etherscan.io/address/0x6f3f635a9762b47954229ea479b4541eaf402a6a#code)에서 찾을 수 있습니다. + +계약 ABI는 계약이 어떤 함수를 호출할지 지정하고, 함수가 예상하는 형식으로 데이터를 반환하도록 보장하는 데 필요합니다. 계약 ABI를 복사한 후, `src` 디렉터리에 `contract-abi.json`이라는 JSON 파일로 저장합니다. + +contract-abi.json은 src 폴더에 저장되어야 합니다. + +계약 주소, ABI, Alchemy 웹3 엔드포인트를 사용하여 [contract 메서드](https://docs.web3js.org/api/web3-eth-contract/class/Contract)를 통해 스마트 계약 인스턴스를 로드할 수 있습니다. `interact.js` 파일에 계약 ABI를 가져오고 계약 주소를 추가합니다. + +```javascript +// interact.js + +const contractABI = require("../contract-abi.json") +const contractAddress = "0x6f3f635A9762B47954229Ea479b4541eAF402A6A" +``` + +이제 드디어 `helloWorldContract` 변수의 주석을 해제하고 AlchemyWeb3 엔드포인트를 사용하여 스마트 계약을 로드할 수 있습니다. + +```javascript +// interact.js +export const helloWorldContract = new web3.eth.Contract( + contractABI, + contractAddress +) +``` + +요약하자면, `interact.js`의 첫 12줄은 이제 다음과 같아야 합니다. + +```javascript +// interact.js + +require("dotenv").config() +const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(alchemyKey) + +const contractABI = require("../contract-abi.json") +const contractAddress = "0x6f3f635A9762B47954229Ea479b4541eAF402A6A" + +export const helloWorldContract = new web3.eth.Contract( + contractABI, + contractAddress +) +``` + +이제 계약이 로드되었으니 `loadCurrentMessage` 함수를 구현할 수 있습니다! + +#### `interact.js` 파일에 `loadCurrentMessage` 구현하기 {#implementing-loadCurrentMessage-in-your-interact-js-file} + +이 함수는 매우 간단합니다. 간단한 비동기 웹3 호출을 하여 계약에서 읽어올 것입니다. 함수는 스마트 계약에 저장된 메시지를 반환합니다. + +`interact.js` 파일의 `loadCurrentMessage`를 다음과 같이 업데이트합니다. + +```javascript +// interact.js + +export const loadCurrentMessage = async () => { + const message = await helloWorldContract.methods.message().call() + return message +} +``` + +이 스마트 계약을 UI에 표시하고 싶으므로, `HelloWorld.js` 컴포넌트의 `useEffect` 함수를 다음과 같이 업데이트합시다. + +```javascript +// HelloWorld.js + +//한 번만 호출됨 +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) +}, []) +``` + +컴포넌트의 첫 번째 렌더링 중에 `loadCurrentMessage`가 한 번만 호출되기를 원합니다. 곧 `addSmartContractListener`를 구현하여 스마트 계약의 메시지가 변경된 후 UI를 자동으로 업데이트할 것입니다. + +리스너에 대해 자세히 알아보기 전에, 지금까지의 결과를 확인해 봅시다! `HelloWorld.js`와 `interact.js` 파일을 저장한 다음 [http://localhost:3000/](http://localhost:3000/)으로 이동하세요. + +현재 메시지가 더 이상 "네트워크에 연결되지 않았습니다."라고 표시되지 않는 것을 볼 수 있습니다. 대신 스마트 계약에 저장된 메시지를 반영합니다. 멋져요! + +#### 이제 UI가 스마트 계약에 저장된 메시지를 반영해야 합니다. {#your-UI-should-now-reflect-the-message-stored-in-the-smart-contract} + +이제 그 리스너에 대해 이야기해 봅시다... + +#### `addSmartContractListener` 구현하기 {#implement-addsmartcontractlistener} + +[이 튜토리얼 시리즈의 1부](https://docs.alchemy.com/alchemy/tutorials/hello-world-smart-contract#step-10-write-our-contract)에서 작성한 `HelloWorld.sol` 파일을 다시 생각해 보면, 스마트 계약의 `update` 함수가 호출된 후 `UpdatedMessages`라는 스마트 계약 이벤트가 발생한다는 것을 기억할 것입니다(9번과 27번 줄 참조). + +```javascript +// HelloWorld.sol + +// 시맨틱 버저닝을 사용하여 Solidity 버전을 지정합니다. +// 자세히 알아보기: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma +pragma solidity ^0.7.3; + +// 'HelloWorld'라는 계약을 정의합니다. +// 계약은 함수와 데이터(그 상태)의 모음입니다. 배포되면, 계약은 이더리움 블록체인의 특정 주소에 위치하게 됩니다. 자세히 알아보기: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html +contract HelloWorld { + + //업데이트 함수가 호출될 때 발생합니다 + //스마트 계약 이벤트는 계약이 블록체인에서 발생한 일을 앱 프론트엔드에 전달하는 방법으로, 특정 이벤트를 '수신'하고 이벤트가 발생할 때 조치를 취할 수 있습니다. + event UpdatedMessages(string oldStr, string newStr); + + // 'string' 타입의 상태 변수 `message`를 선언합니다. + // 상태 변수는 값이 계약 스토리지에 영구적으로 저장되는 변수입니다. `public` 키워드는 변수를 계약 외부에서 액세스할 수 있도록 하고 다른 계약이나 클라이언트가 값을 액세스하기 위해 호출할 수 있는 함수를 생성합니다. + string public message; + + // 많은 클래스 기반 객체 지향 언어와 마찬가지로 생성자는 계약 생성 시에만 실행되는 특수 함수입니다. + // 생성자는 계약의 데이터를 초기화하는 데 사용됩니다. 자세히 알아보기:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors + constructor(string memory initMessage) { + + // 문자열 인수 `initMessage`를 수락하고 계약의 `message` 스토리지 변수에 값을 설정합니다). + message = initMessage; + } + + // 문자열 인수를 허용하고 `message` 스토리지 변수를 업데이트하는 공개 함수입니다. + function update(string memory newMessage) public { + string memory oldMsg = message; + message = newMessage; + emit UpdatedMessages(oldMsg, newMessage); + } +} +``` + +스마트 계약 이벤트는 블록체인에서 어떤 일이 일어났음(즉, _이벤트_가 발생했음)을 계약이 프런트엔드 애플리케이션에 전달하는 방법으로, 특정 이벤트를 '수신'하고 해당 이벤트가 발생하면 조치를 취할 수 있습니다. + +`addSmartContractListener` 함수는 특별히 Hello World 스마트 계약의 `UpdatedMessages` 이벤트를 수신하고, UI를 업데이트하여 새 메시지를 표시합니다. + +`addSmartContractListener`를 다음과 같이 수정합니다. + +```javascript +// HelloWorld.js + +function addSmartContractListener() { + helloWorldContract.events.UpdatedMessages({}, (error, data) => { + if (error) { + setStatus("😥 " + error.message) + } else { + setMessage(data.returnValues[1]) + setNewMessage("") + setStatus("🎉 메시지가 업데이트되었습니다!") + } + }) +} +``` + +리스너가 이벤트를 감지할 때 어떤 일이 발생하는지 분석해 보겠습니다. + +- 이벤트가 발생할 때 오류가 발생하면 `status` 상태 변수를 통해 UI에 반영됩니다. +- 그렇지 않으면 반환된 `data` 객체를 사용합니다. `data.returnValues`는 0부터 인덱싱된 배열로, 배열의 첫 번째 요소는 이전 메시지를 저장하고 두 번째 요소는 업데이트된 메시지를 저장합니다. 전체적으로, 성공적인 이벤트 발생 시 `message` 문자열을 업데이트된 메시지로 설정하고, `newMessage` 문자열을 지우고, `status` 상태 변수를 업데이트하여 스마트 계약에 새 메시지가 게시되었음을 반영합니다. + +마지막으로, `HelloWorld.js` 컴포넌트의 첫 렌더링 시 초기화되도록 `useEffect` 함수에서 리스너를 호출합니다. 전체적으로 `useEffect` 함수는 다음과 같아야 합니다. + +```javascript +// HelloWorld.js + +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) + addSmartContractListener() +}, []) +``` + +이제 스마트 계약에서 읽을 수 있게 되었으니, 쓰는 방법도 알아보면 좋겠습니다! 하지만 탈중앙화앱에 쓰려면 먼저 이더리움 지갑이 연결되어 있어야 합니다. + +따라서 다음으로 이더리움 지갑(MetaMask)을 설정하고 탈중앙화앱에 연결하는 방법을 다루겠습니다! + +### 4단계: 이더리움 지갑 설정하기 {#step-4-set-up-your-ethereum-wallet} + +이더리움 체인에 무엇이든 쓰려면 사용자는 가상 지갑의 개인 키를 사용하여 트랜잭션에 서명해야 합니다. 이 튜토리얼에서는 최종 사용자에게 이 트랜잭션 서명을 매우 쉽게 만들어주는 브라우저 내 가상 지갑인 [MetaMask](https://metamask.io/)를 사용하여 이더리움 계정 주소를 관리합니다. + +이더리움의 트랜잭션 작동 방식에 대해 더 자세히 알아보려면 이더리움 재단의 [이 페이지](/developers/docs/transactions/)를 확인하세요. + +#### MetaMask 다운로드하기 {#download-metamask} + +[여기](https://metamask.io/download)에서 MetaMask 계정을 무료로 다운로드하고 생성할 수 있습니다. 계정을 생성하거나 이미 계정이 있는 경우, 오른쪽 상단에서 'Goerli 테스트 네트워크'로 전환했는지 확인하세요(실제 돈을 다루지 않기 위함입니다). + +#### 파우셋에서 이더 추가하기 {#add-ether-from-a-faucet} + +이더리움 블록체인에서 트랜잭션에 서명하려면 가짜 Eth가 필요합니다. Eth를 얻으려면 [FaucETH](https://fauceth.komputing.org)로 이동하여 Goerli 계정 주소를 입력하고 '자금 요청'을 클릭한 다음, 드롭다운에서 '이더리움 테스트넷 Goerli'를 선택하고 마지막으로 '자금 요청' 버튼을 다시 클릭하세요. 곧 MetaMask 계정에서 Eth를 볼 수 있을 것입니다! + +#### 잔액 확인하기 {#check-your-balance} + +잔액이 있는지 다시 확인하기 위해 [Alchemy의 컴포저 도구](https://composer.alchemyapi.io/?composer_state=%7B%22network%22%3A0%2C%22methodName%22%3A%22eth_getBalance%22%2C%22paramValues%22%3A%5B%22%22%2C%22latest%22%5D%7D)를 사용하여 [eth_getBalance](https://docs.alchemyapi.io/alchemy/documentation/alchemy-api-reference/json-rpc#eth_getbalance) 요청을 해봅시다. 그러면 지갑에 있는 Eth의 양이 반환됩니다. MetaMask 계정 주소를 입력하고 "Send Request"를 클릭하면 다음과 같은 응답이 표시됩니다. + +```text +{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"} +``` + +**참고:** 이 결과는 eth가 아닌 wei 단위입니다. Wei는 ether의 최소 단위로 사용됩니다. wei에서 eth로의 변환은 1 eth = 10¹⁸ wei입니다. 따라서 0xde0b6b3a7640000을 10진수로 변환하면 1\*10¹⁸이 되고 이는 1eth와 같습니다. + +휴! 우리의 가짜 돈이 다 있군요! 🤑 + +### 5단계: MetaMask를 UI에 연결하기 {#step-5-connect-metamask-to-your-UI} + +이제 MetaMask 지갑이 설정되었으니, 탈중앙화앱을 연결해 봅시다! + +#### `connectWallet` 함수 {#the-connectWallet-function} + +`interact.js` 파일에서 `connectWallet` 함수를 구현한 다음, `HelloWorld.js` 컴포넌트에서 호출할 수 있습니다. + +`connectWallet`을 다음과 같이 수정합시다. + +```javascript +// interact.js + +export const connectWallet = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_requestAccounts", + }) + const obj = { + status: "👆🏽 위 텍스트 필드에 메시지를 작성하세요.", + address: addressArray[0], + } + return obj + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + 브라우저에 가상 이더리움 지갑인 MetaMask를 설치해야 합니다. + +

+
+ ), + } + } +} +``` + +이 거대한 코드 블록은 정확히 무엇을 하는 걸까요? + +먼저, 브라우저에서 `window.ethereum`이 활성화되어 있는지 확인합니다. + +`window.ethereum`은 MetaMask 및 기타 지갑 제공업체에서 주입하는 글로벌 API로, 웹사이트가 사용자의 이더리움 계정을 요청할 수 있도록 합니다. 승인되면 사용자가 연결된 블록체인에서 데이터를 읽고, 사용자에게 메시지 및 트랜잭션 서명을 제안할 수 있습니다. 자세한 내용은 [MetaMask 문서](https://docs.metamask.io/guide/ethereum-provider.html#table-of-contents)를 확인하세요! + +`window.ethereum`이 _없으면_ MetaMask가 설치되지 않았다는 의미입니다. 그러면 `address`가 빈 문자열로 반환되고 `status` JSX 객체는 사용자가 MetaMask를 설치해야 한다는 것을 전달하는 JSON 객체가 반환됩니다. + +이제 `window.ethereum`이 _있으면_ 상황이 흥미로워집니다. + +try/catch 루프를 사용하여 [`window.ethereum.request({ method: "eth_requestAccounts" });`](https://docs.metamask.io/guide/rpc-api.html#eth-requestaccounts)를 호출하여 MetaMask에 연결을 시도합니다. 이 함수를 호출하면 브라우저에서 MetaMask가 열리고 사용자에게 지갑을 탈중앙화앱에 연결하라는 메시지가 표시됩니다. + +- 사용자가 연결하기로 선택하면 `method: "eth_requestAccounts"`는 탈중앙화앱에 연결된 모든 사용자의 계정 주소를 포함하는 배열을 반환합니다. 전체적으로 `connectWallet` 함수는 이 배열의 _첫 번째_ `address`\(9번째 줄 참조\)와 사용자에게 스마트 계약에 메시지를 작성하라는 `status` 메시지를 포함하는 JSON 객체를 반환합니다. +- 사용자가 연결을 거부하면 JSON 객체는 반환된 `address`에 대해 빈 문자열을 포함하고 사용자가 연결을 거부했음을 반영하는 `status` 메시지를 포함합니다. + +이제 이 `connectWallet` 함수를 작성했으므로 다음 단계는 `HelloWorld.js` 컴포넌트에서 호출하는 것입니다. + +#### `connectWallet` 함수를 `HelloWorld.js` UI 컴포넌트에 추가하기 {#add-the-connectWallet-function-to-your-HelloWorld-js-ui-component} + +`HelloWorld.js`의 `connectWalletPressed` 함수로 이동하여 다음과 같이 업데이트하세요. + +```javascript +// HelloWorld.js + +const connectWalletPressed = async () => { + const walletResponse = await connectWallet() + setStatus(walletResponse.status) + setWallet(walletResponse.address) +} +``` + +`interact.js` 파일에서 대부분의 기능이 `HelloWorld.js` 컴포넌트에서 어떻게 추상화되었는지 주목하세요. 이것은 M-V-C 패러다임을 준수하기 위함입니다! + +`connectWalletPressed`에서 가져온 `connectWallet` 함수를 await 호출하고, 그 응답을 사용하여 상태 훅을 통해 `status` 및 `walletAddress` 변수를 업데이트합니다. + +이제 두 파일(`HelloWorld.js` 및 `interact.js`)을 모두 저장하고 지금까지의 UI를 테스트해 보겠습니다. + +[http://localhost:3000/](http://localhost:3000/) 페이지에서 브라우저를 열고 페이지 오른쪽 상단의 "지갑 연결" 버튼을 누르세요. + +MetaMask가 설치되어 있다면, 지갑을 탈중앙화앱에 연결하라는 메시지가 표시됩니다. 연결 초대를 수락합니다. + +이제 지갑 버튼에 주소가 연결되었음이 반영되는 것을 볼 수 있습니다! 좋아요 🔥 + +다음으로, 페이지를 새로고침해 보세요... 이상하네요. 지갑이 이미 연결되어 있음에도 불구하고 지갑 버튼은 MetaMask에 연결하라는 메시지를 표시합니다... + +하지만 두려워하지 마세요! 이 주소 문제를 쉽게 해결할 수 있습니다(이해하셨나요?) `getCurrentWalletConnected`를 구현하여 주소가 이미 탈중앙화앱에 연결되어 있는지 확인하고 그에 따라 UI를 업데이트하면 됩니다! + +#### `getCurrentWalletConnected` 함수 {#the-getcurrentwalletconnected-function} + +`interact.js` 파일의 `getCurrentWalletConnected` 함수를 다음과 같이 업데이트하세요. + +```javascript +// interact.js + +export const getCurrentWalletConnected = async () => { + if (window.ethereum) { + try { + const addressArray = await window.ethereum.request({ + method: "eth_accounts", + }) + if (addressArray.length > 0) { + return { + address: addressArray[0], + status: "👆🏽 위 텍스트 필드에 메시지를 작성하세요.", + } + } else { + return { + address: "", + status: "🦊 오른쪽 상단 버튼을 사용하여 MetaMask에 연결하세요.", + } + } + } catch (err) { + return { + address: "", + status: "😥 " + err.message, + } + } + } else { + return { + address: "", + status: ( + +

+ {" "} + 🦊 + 브라우저에 가상 이더리움 지갑인 MetaMask를 설치해야 합니다. + +

+
+ ), + } + } +} +``` + +이 코드는 이전 단계에서 작성한 `connectWallet` 함수와 _매우_ 유사합니다. + +주요 차이점은 사용자가 지갑을 연결하기 위해 MetaMask를 여는 `eth_requestAccounts` 메서드를 호출하는 대신, 여기서는 현재 탈중앙화앱에 연결된 MetaMask 주소를 포함하는 배열을 반환하는 `eth_accounts` 메서드를 호출한다는 것입니다. + +이 함수가 작동하는 것을 보려면 `HelloWorld.js` 컴포넌트의 `useEffect` 함수에서 호출해 봅시다. + +```javascript +// HelloWorld.js + +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) + addSmartContractListener() + + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) +}, []) +``` + +`getCurrentWalletConnected` 호출의 응답을 사용하여 `walletAddress`와 `status` 상태 변수를 업데이트하는 것을 확인하세요. + +이제 이 코드를 추가했으니 브라우저 창을 새로고침해 봅시다. + +좋아요! 버튼에는 연결되었다고 표시되고 연결된 지갑 주소의 미리보기가 표시되어야 합니다 - 새로고침 후에도 말이죠! + +#### `addWalletListener` 구현하기 {#implement-addwalletlistener} + +탈중앙화앱 지갑 설정의 마지막 단계는 사용자가 연결을 끊거나 계정을 전환할 때와 같이 지갑의 상태가 변경될 때 UI가 업데이트되도록 지갑 리스너를 구현하는 것입니다. + +`HelloWorld.js` 파일에서 `addWalletListener` 함수를 다음과 같이 수정하세요. + +```javascript +// HelloWorld.js + +function addWalletListener() { + if (window.ethereum) { + window.ethereum.on("accountsChanged", (accounts) => { + if (accounts.length > 0) { + setWallet(accounts[0]) + setStatus("👆🏽 위 텍스트 필드에 메시지를 작성하세요.") + } else { + setWallet("") + setStatus("🦊 오른쪽 상단 버튼을 사용하여 MetaMask에 연결하세요.") + } + }) + } else { + setStatus( +

+ {" "} + 🦊 + 브라우저에 가상 이더리움 지갑인 MetaMask를 설치해야 합니다. + +

+ ) + } +} +``` + +이 시점에서는 여기서 무슨 일이 일어나고 있는지 이해하는 데 도움이 필요 없을 것이라고 확신하지만, 철저함을 위해 간단히 분석해 보겠습니다. + +- 먼저, 함수는 `window.ethereum`이 활성화되어 있는지(즉, MetaMask가 설치되어 있는지) 확인합니다. + - 그렇지 않다면, `status` 상태 변수를 사용자가 MetaMask를 설치하도록 유도하는 JSX 문자열로 설정합니다. + - 활성화된 경우, 3번째 줄에서 `window.ethereum.on("accountsChanged")` 리스너를 설정하여 사용자가 탈중앙화앱에 추가 계정을 연결하거나 계정을 전환하거나 계정 연결을 끊는 등 MetaMask 지갑의 상태 변화를 수신합니다. 연결된 계정이 하나 이상 있는 경우, `walletAddress` 상태 변수는 리스너가 반환한 `accounts` 배열의 첫 번째 계정으로 업데이트됩니다. 그렇지 않으면 `walletAddress`는 빈 문자열로 설정됩니다. + +마지막으로 `useEffect` 함수에서 호출해야 합니다. + +```javascript +// HelloWorld.js + +useEffect(async () => { + const message = await loadCurrentMessage() + setMessage(message) + addSmartContractListener() + + const { address, status } = await getCurrentWalletConnected() + setWallet(address) + setStatus(status) + + addWalletListener() +}, []) +``` + +이것으로 끝입니다! 모든 지갑 기능을 성공적으로 프로그래밍했습니다! 이제 마지막 작업인 스마트 계약에 저장된 메시지 업데이트로 넘어갑시다! + +### 6단계: `updateMessage` 함수 구현하기 {#step-6-implement-the-updateMessage-function} + +자, 이제 마지막 단계에 도착했습니다! `interact.js` 파일의 `updateMessage`에서 다음을 수행할 것입니다. + +1. 스마트 계약에 게시하려는 메시지가 유효한지 확인합니다 +2. MetaMask를 사용하여 트랜잭션에 서명합니다 +3. `HelloWorld.js` 프런트엔드 컴포넌트에서 이 함수를 호출합니다 + +오래 걸리지 않을 것입니다. 이 탈중앙화앱을 완성합시다! + +#### 입력 오류 처리 {#input-error-handling} + +당연히 함수 시작 부분에 일종의 입력 오류 처리를 하는 것이 합리적입니다. + +MetaMask 확장 프로그램이 설치되지 않았거나, 지갑이 연결되지 않았거나(즉, 전달된 `주소`가 빈 문자열인 경우), `메시지`가 빈 문자열인 경우 함수가 조기에 반환되도록 하고 싶을 것입니다. `updateMessage`에 다음 오류 처리를 추가합시다. + +```javascript +// interact.js + +export const updateMessage = async (address, message) => { + if (!window.ethereum || address === null) { + return { + status: + "💡 블록체인에서 메시지를 업데이트하려면 MetaMask 지갑을 연결하세요.", + } + } + + if (message.trim() === "") { + return { + status: "❌ 메시지는 빈 문자열이 될 수 없습니다.", + } + } +} +``` + +이제 적절한 입력 오류 처리가 있으니, MetaMask를 통해 트랜잭션에 서명할 시간입니다! + +#### 트랜잭션 서명하기 {#signing-our-transaction} + +전통적인 웹3 이더리움 트랜잭션에 이미 익숙하다면 다음에 작성할 코드는 매우 친숙할 것입니다. 입력 오류 처리 코드 아래에 `updateMessage`에 다음을 추가합니다. + +```javascript +// interact.js + +//트랜잭션 매개변수 설정 +const transactionParameters = { + to: contractAddress, // 계약 게시 중을 제외하고 필수입니다. + from: address, // 사용자의 활성 주소와 일치해야 합니다. + data: helloWorldContract.methods.update(message).encodeABI(), +} + +//트랜잭션 서명 +try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + status: ( + + ✅{" "} + + Etherscan에서 트랜잭션 상태 보기! + +
+ ℹ️ 트랜잭션이 네트워크에 의해 확인되면 메시지가 + 자동으로 업데이트됩니다. +
+ ), + } +} catch (error) { + return { + status: "😥 " + error.message, + } +} +``` + +어떤 일이 일어나는지 분석해 봅시다. 먼저, 트랜잭션 매개변수를 설정합니다. 여기서: + +- `to`는 수신자 주소(우리의 스마트 계약)를 지정합니다. +- `from`은 트랜잭션의 서명자, 즉 함수에 전달된 `주소` 변수를 지정합니다. +- `data`는 Hello World 스마트 계약의 `update` 메서드에 대한 호출을 포함하며, `message` 문자열 변수를 입력으로 받습니다. + +그런 다음, `window.ethereum.request`라는 await 호출을 통해 MetaMask에 트랜잭션 서명을 요청합니다. 11번과 12번 줄에서 eth 메서드 `eth_sendTransaction`을 지정하고 `transactionParameters`를 전달하는 것을 주목하세요. + +이 시점에서 MetaMask가 브라우저에서 열리고 사용자에게 트랜잭션에 서명하거나 거부하라는 메시지를 표시합니다. + +- 트랜잭션이 성공하면, 함수는 `status` JSX 문자열이 사용자에게 트랜잭션에 대한 자세한 정보를 Etherscan에서 확인하도록 안내하는 JSON 객체를 반환합니다. +- 트랜잭션이 실패하면, 함수는 `status` 문자열이 오류 메시지를 전달하는 JSON 객체를 반환합니다. + +전체적으로 `updateMessage` 함수는 다음과 같아야 합니다. + +```javascript +// interact.js + +export const updateMessage = async (address, message) => { + //입력 오류 처리 + if (!window.ethereum || address === null) { + return { + status: + "💡 블록체인에서 메시지를 업데이트하려면 MetaMask 지갑을 연결하세요.", + } + } + + if (message.trim() === "") { + return { + status: "❌ 메시지는 빈 문자열이 될 수 없습니다.", + } + } + + //트랜잭션 매개변수 설정 + const transactionParameters = { + to: contractAddress, // 계약 게시 중을 제외하고 필수입니다. + from: address, // 사용자의 활성 주소와 일치해야 합니다. + data: helloWorldContract.methods.update(message).encodeABI(), + } + + //트랜잭션 서명 + try { + const txHash = await window.ethereum.request({ + method: "eth_sendTransaction", + params: [transactionParameters], + }) + return { + status: ( + + ✅{" "} + + Etherscan에서 트랜잭션 상태 보기! + +
+ ℹ️ 트랜잭션이 네트워크에 의해 확인되면 메시지가 + 자동으로 업데이트됩니다. +
+ ), + } + } catch (error) { + return { + status: "😥 " + error.message, + } + } +} +``` + +마지막으로, `updateMessage` 함수를 `HelloWorld.js` 컴포넌트에 연결해야 합니다. + +#### `updateMessage`를 `HelloWorld.js` 프런트엔드에 연결하기 {#connect-updatemessage-to-the-helloworld-js-frontend} + +`onUpdatePressed` 함수는 가져온 `updateMessage` 함수에 대한 await 호출을 하고 `status` 상태 변수를 수정하여 트랜잭션이 성공했는지 실패했는지를 반영해야 합니다. + +```javascript +// HelloWorld.js + +const onUpdatePressed = async () => { + const { status } = await updateMessage(walletAddress, newMessage) + setStatus(status) +} +``` + +매우 깔끔하고 간단합니다. 그리고 그거 아시나요... 탈중앙화앱이 완성되었습니다!!! + +**업데이트** 버튼을 테스트해 보세요! + +### 나만의 맞춤형 탈중앙화앱 만들기 {#make-your-own-custom-dapp} + +와, 튜토리얼 끝까지 오셨네요! 요약하자면, 다음을 배웠습니다. + +- MetaMask 지갑을 탈중앙화앱 프로젝트에 연결하기 +- [Alchemy Web3](https://docs.alchemy.com/alchemy/documentation/alchemy-web3) API를 사용하여 스마트 계약에서 데이터 읽기 +- MetaMask를 사용하여 이더리움 트랜잭션 서명하기 + +이제 이 튜토리얼에서 배운 기술을 적용하여 자신만의 맞춤형 탈중앙화앱 프로젝트를 구축할 준비가 되었습니다! 언제나 그렇듯이 질문이 있으시면 [Alchemy Discord](https://discord.gg/gWuC7zB)에서 주저하지 말고 도움을 요청하세요. 🧙‍♂️ + +이 튜토리얼을 완료한 후, Twitter에서 [@alchemyplatform](https://twitter.com/AlchemyPlatform)을 태그하여 경험이 어땠는지 또는 피드백이 있는지 알려주세요! diff --git a/public/content/translations/ko/developers/tutorials/hello-world-smart-contract/index.md b/public/content/translations/ko/developers/tutorials/hello-world-smart-contract/index.md new file mode 100644 index 00000000000..c4683ba45e5 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/hello-world-smart-contract/index.md @@ -0,0 +1,359 @@ +--- +title: "초보자용 Hello World 스마트 컨트랙트" +description: "이더리움에 간단한 스마트 계약을 작성하고 배포하는 방법에 대한 입문 튜토리얼입니다." +author: "elanh" +tags: [ "솔리디티", "hardhat", "alchemy", "스마트 계약", "배포하기" ] +skill: beginner +lang: ko +published: 2021-03-31 +--- + +블록체인 개발이 처음이라 어디서부터 시작해야 할지 모르거나, 스마트 계약을 배포하고 상호작용하는 방법을 이해하고 싶다면 이 가이드가 도움이 될 것입니다. 가상 지갑 [MetaMask](https://metamask.io/), [Solidity](https://docs.soliditylang.org/en/v0.8.0/), [Hardhat](https://hardhat.org/) 및 [Alchemy](https://www.alchemy.com/eth)를 사용하여 Sepolia 테스트 네트워크에 간단한 스마트 계약을 만들고 배포하는 과정을 안내합니다(아직 이 내용이 무엇을 의미하는지 이해하지 못하더라도 걱정하지 마세요. 앞으로 설명해 드릴 것입니다). + +이 튜토리얼의 [2부](https://docs.alchemy.com/docs/interacting-with-a-smart-contract)에서는 스마트 계약이 배포된 후 상호작용하는 방법을 살펴보고, [3부](https://www.alchemy.com/docs/submitting-your-smart-contract-to-etherscan)에서는 Etherscan에 게시하는 방법을 다룹니다. + +궁금한 점이 있으면 언제든지 [Alchemy Discord](https://discord.gg/gWuC7zB)에 문의하세요! + +## 1단계: 이더리움 네트워크에 연결하기 {#step-1} + +이더리움 체인에 요청을 보내는 방법은 여러 가지가 있습니다. 간단하게 설명하기 위해, 자체 노드를 실행하지 않고도 이더리움 체인과 통신할 수 있는 블록체인 개발자 플랫폼 및 API인 Alchemy의 무료 계정을 사용하겠습니다. 이 플랫폼에는 모니터링 및 분석을 위한 개발자 도구도 있습니다. 이 튜토리얼에서는 이 도구를 활용하여 스마트 계약 배포 시 내부적으로 어떤 일이 일어나는지 이해해 보겠습니다. Alchemy 계정이 아직 없다면 [여기서 무료로 가입할 수 있습니다](https://dashboard.alchemy.com/signup). + +## 2단계: 앱 만들기(및 API 키) {#step-2} + +Alchemy 계정을 생성한 후에는 앱을 생성하여 API 키를 생성할 수 있습니다. 이를 통해 Sepolia 테스트넷에 요청을 보낼 수 있습니다. 테스트넷에 익숙하지 않다면 [이 페이지](/developers/docs/networks/)를 확인하세요. + +1. Alchemy 대시보드에서 탐색 모음의 "Select an app"을 선택하고 "Create new app"을 클릭하여 "Create new app" 페이지로 이동하세요. + +![Hello world create app](./hello-world-create-app.png) + +2. 앱 이름을 “Hello World”로 지정하고, 간단한 설명을 제공한 다음, 사용 사례(예: "Infra & Tooling")를 선택하세요. 다음으로 "Ethereum"을 검색하고 네트워크를 선택하세요. + +![create app view hello world](./create-app-view-hello-world.png) + +3. "Next"를 클릭하여 진행한 다음, “Create app”을 클릭하면 완료됩니다! 앱이 탐색 모음 드롭다운 메뉴에 나타나며, API 키를 복사할 수 있습니다. + +## 3단계: 이더리움 계정(주소) 생성하기 {#step-3} + +거래를 보내고 받기 위해서는 이더리움 계정이 필요합니다. 이 튜토리얼에서는 이더리움 계정 주소를 관리하는 데 사용되는 브라우저의 가상 지갑인 MetaMask를 사용합니다. [트랜잭션](/developers/docs/transactions/)에 대해 자세히 알아보기. + +[여기](https://metamask.io/download)에서 MetaMask를 다운로드하고 이더리움 계정을 무료로 만들 수 있습니다. 계정을 만들 때나 이미 계정이 있는 경우, 네트워크 드롭다운 메뉴를 사용하여 "Sepolia" 테스트 네트워크로 전환해야 합니다(실제 돈을 다루지 않도록 하기 위함입니다). + +Sepolia가 목록에 표시되지 않으면 메뉴로 이동한 다음 '고급'으로 가서 아래로 스크롤하여 "테스트 네트워크 표시"를 켜세요. 네트워크 선택 메뉴에서 "사용자 지정" 탭을 선택하여 테스트넷 목록을 찾고 "Sepolia"를 선택하세요. + +![metamask sepolia example](./metamask-sepolia-example.png) + +## 4단계: 파우셋에서 이더 추가하기 {#step-4} + +스마트 계약을 테스트넷에 배포하려면 가짜 Eth가 필요합니다. Sepolia ETH를 얻으려면 [Sepolia 네트워크 세부 정보](/developers/docs/networks/#sepolia)로 이동하여 다양한 파우셋 목록을 볼 수 있습니다. 하나가 작동하지 않으면 다른 것을 시도해 보세요. 때때로 고갈될 수 있습니다. 네트워크 트래픽으로 인해 가짜 ETH를 받는 데 시간이 걸릴 수 있습니다. 곧 MetaMask 계정에서 ETH를 확인할 수 있습니다! + +## 5단계: 잔액 확인하기 {#step-5} + +잔액이 있는지 다시 확인하기 위해 [Alchemy의 컴포저 도구](https://sandbox.alchemy.com/?network=ETH_SEPOLIA&method=eth_getBalance&body.id=1&body.jsonrpc=2.0&body.method=eth_getBalance&body.params%5B0%5D=&body.params%5B1%5D=latest)를 사용하여 [eth_getBalance](/developers/docs/apis/json-rpc/#eth_getbalance) 요청을 해보겠습니다. 이것은 지갑에 있는 ETH의 양을 반환할 것입니다. MetaMask 계정 주소를 입력하고 "Send Request"를 클릭하면 다음과 같은 응답이 표시됩니다. + +```json +{ "jsonrpc": "2.0", "id": 0, "result": "0x2B5E3AF16B1880000" } +``` + +> **참고:** 이 결과는 ETH가 아닌 wei 단위입니다. Wei는 ether의 최소 단위로 사용됩니다. wei를 ETH로 변환하면 다음과 같습니다: 1 eth = 1018 wei. 따라서 0x2B5E3AF16B1880000을 10진수로 변환하면 5\*10¹⁸이 되며 이는 5 ETH와 같습니다. +> +> 휴! 가짜 돈이 모두 준비되었습니다 . + +## 6단계: 프로젝트 초기화하기 {#step-6} + +먼저 프로젝트를 위한 폴더를 만들어야 합니다. 커맨드 라인으로 이동하여 다음을 입력합니다. + +``` +mkdir hello-world +cd hello-world +``` + +이제 프로젝트 폴더 안으로 들어왔으니, `npm init`을 사용하여 프로젝트를 초기화하겠습니다. npm이 아직 설치되어 있지 않다면 [이 지침](https://docs.alchemyapi.io/alchemy/guides/alchemy-for-macs#1-install-nodejs-and-npm)을 따르세요(Node.js도 필요하니 함께 다운로드하세요!). + +``` +npm init +``` + +설치 질문에 어떻게 답하든 크게 상관없습니다. 참고용으로 저희가 진행한 방식은 다음과 같습니다: + +``` +package name: (hello-world) +version: (1.0.0) +description: hello world 스마트 계약 +entry point: (index.js) +test command: +git repository: +keywords: +author: +license: (ISC) +/Users/.../.../.../hello-world/package.json에 쓸 내용: + +{ + "name": "hello-world", + "version": "1.0.0", + "description": "hello world 스마트 계약", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} +``` + +package.json을 승인하면 준비 완료입니다! + +## 7단계: [Hardhat](https://hardhat.org/getting-started/#overview) 다운로드하기 {#step-7} + +Hardhat은 이더리움 소프트웨어를 컴파일, 배포, 테스트 및 디버그하기 위한 개발 환경입니다. 실제 블록체인에 배포하기 전에 로컬에서 스마트 컨트랙트 및 dApp을 구축할 때 사용됩니다. + +`hello-world` 프로젝트 내부에서 다음을 실행하세요: + +``` +npm install --save-dev hardhat +``` + +설치 지침에 대한 자세한 내용은 [이 페이지](https://hardhat.org/getting-started/#overview)를 확인하세요. + +## 8단계: Hardhat 프로젝트 생성하기 {#step-8} + +프로젝트 폴더 내에서 다음을 실행합니다. + +``` +npx hardhat +``` + +그러면 환영 메시지와 원하는 작업을 선택할 수 있는 옵션이 표시됩니다. "create an empty hardhat.config.js"를 선택합니다. + +``` +888 888 888 888 888 +888 888 888 888 888 +888 888 888 888 888 +8888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888 +888 888 "88b 888P" d88" 888 888 "88b "88b 888 +888 888 .d888888 888 888 888 888 888 .d888888 888 +888 888 888 888 888 Y88b 888 888 888 888 888 Y88b. +888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888 + +👷 Hardhat v2.0.11에 오신 것을 환영합니다 👷‍? + +무엇을 하시겠습니까? … +샘플 프로젝트 생성 +❯ 빈 hardhat.config.js 생성 +종료 +``` + +이렇게 하면 `hardhat.config.js` 파일이 생성되며, 여기에 프로젝트에 대한 모든 설정을 지정하게 됩니다(13단계). + +## 9단계: 프로젝트 폴더 추가하기 {#step-9} + +프로젝트를 체계적으로 관리하기 위해 두 개의 새 폴더를 만들겠습니다. 명령줄에서 프로젝트의 루트 디렉터리로 이동하고 다음을 입력합니다. + +``` +mkdir contracts +mkdir scripts +``` + +- `contracts/`는 hello world 스마트 계약 코드 파일을 보관할 곳입니다. +- `scripts/`는 계약을 배포하고 상호작용하기 위한 스크립트를 보관할 곳입니다. + +## 10단계: 스마트 컨트랙트 작성하기 {#step-10} + +도대체 언제 코드를 작성하는 거지? 라고 자문하고 있을지도 모릅니다. 자, 이제 10단계입니다. + +선호하는 편집기(저희는 [VSCode](https://code.visualstudio.com/)를 좋아합니다)에서 hello-world 프로젝트를 여세요. 스마트 계약은 Solidity라는 언어로 작성되며, HelloWorld.sol 스마트 계약을 작성하는 데 이 언어를 사용할 것입니다.‌ + +1. “contracts” 폴더로 이동하여 HelloWorld.sol이라는 새 파일을 만드세요. +2. 아래는 이 튜토리얼에서 사용할 이더리움 재단의 샘플 Hello World 스마트 계약입니다. 아래 내용을 복사하여 HelloWorld.sol 파일에 붙여넣고, 주석을 읽고 이 계약이 어떤 역할을 하는지 이해하세요: + +```solidity +// 시맨틱 버저닝을 사용하여 Solidity 버전을 지정합니다. +// 자세히 알아보기: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma +pragma solidity ^0.7.0; + +// `HelloWorld`라는 이름의 계약을 정의합니다. +// 계약은 함수와 데이터(상태)의 모음입니다. 배포되면 계약은 이더리움 블록체인의 특정 주소에 상주하게 됩니다. 자세히 알아보기: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html +contract HelloWorld { + + // `string` 타입의 상태 변수 `message`를 선언합니다. + // 상태 변수는 그 값이 계약 저장소에 영구적으로 저장되는 변수입니다. `public` 키워드는 변수를 계약 외부에서 접근할 수 있게 만들고, 다른 계약이나 클라이언트가 값을 접근하기 위해 호출할 수 있는 함수를 생성합니다. + string public message; + + // 많은 클래스 기반 객체 지향 언어와 유사하게, 생성자는 계약 생성 시에만 실행되는 특별한 함수입니다. + // 생성자는 계약의 데이터를 초기화하는 데 사용됩니다. 자세히 알아보기:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors + constructor(string memory initMessage) { + + // 문자열 인수 `initMessage`를 받아 계약의 `message` 저장 변수에 값을 설정합니다. + message = initMessage; + } + + // 문자열 인수를 받아 `message` 저장 변수를 업데이트하는 public 함수입니다. + function update(string memory newMessage) public { + message = newMessage; + } +} +``` + +이것은 생성 시 메시지를 저장하고 `update` 함수를 호출하여 업데이트할 수 있는 매우 간단한 스마트 계약입니다. + +## 11단계: MetaMask와 Alchemy를 프로젝트에 연결하기 {#step-11} + +MetaMask 지갑, Alchemy 계정을 만들고 스마트 계약을 작성했습니다. 이제 이 세 가지를 연결할 차례입니다. + +디지털 지갑에서 전송되는 모든 거래에는 고유한 개인 키를 사용하는 서명이 필요합니다. 프로그램에 이 권한을 제공하기 위해 개인 키(및 Alchemy API 키) 를 환경 파일에 안전하게 저장할 수 있습니다. + +> 트랜잭션 전송에 대해 더 자세히 알아보려면 web3를 사용하여 트랜잭션을 전송하는 [이 튜토리얼](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)을 확인하세요. + +먼저 프로젝트 디렉터리에 dotenv 패키지를 설치합니다. + +``` +npm install dotenv --save +``` + +그런 다음 프로젝트의 루트 디렉터리에 `.env` 파일을 만들고 여기에 MetaMask 개인 키와 HTTP Alchemy API URL을 추가합니다. + +- 개인 키를 내보내려면 [이 지침](https://support.metamask.io/configure/accounts/how-to-export-an-accounts-private-key/)을 따르세요. +- HTTP Alchemy API URL을 얻으려면 아래를 참조하세요. + +![get alchemy api key](./get-alchemy-api-key.png) + +Alchemy API URL 복사하세요. + +`.env` 파일은 다음과 같아야 합니다: + +``` +API_URL = "https://eth-sepolia.g.alchemy.com/v2/your-api-key" +PRIVATE_KEY = "your-metamask-private-key" +``` + +이를 실제로 코드에 연결하기 위해 13단계의 `hardhat.config.js` 파일에서 이러한 변수를 참조합니다. + + + + +.env를 커밋하지 마세요! .env 파일을 다른 사람과 공유하거나 노출하지 마세요. 그렇게 하면 민감한 정보가 노출될 수 있습니다. 버전 관리 시스템을 사용하는 경우 .envgitignore 파일에 추가하세요. + + + + +## 12단계: Ethers.js 설치하기 {#step-12-install-ethersjs} + +Ethers.js는 [표준 JSON-RPC 메서드](/developers/docs/apis/json-rpc/)를 더 사용자 친화적인 메서드로 래핑하여 이더리움과 더 쉽게 상호 작용하고 요청할 수 있게 해주는 라이브러리입니다. + +Hardhat을 사용하면 추가 도구 및 확장된 기능을 위해 [플러그인](https://hardhat.org/plugins/)을 매우 쉽게 통합할 수 있습니다. 계약 배포를 위해 [Ethers 플러그인](https://hardhat.org/docs/plugins/official-plugins#hardhat-ethers)을 활용할 것입니다([Ethers.js](https://github.com/ethers-io/ethers.js/)는 매우 깔끔한 계약 배포 방법을 제공합니다). + +프로젝트 디렉토리에 다음을 입력합니다. + +``` +npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0" +``` + +다음 단계에서 `hardhat.config.js`에 ethers를 추가해야 합니다. + +## 13단계: hardhat.config.js 업데이트하기 {#step-13-update-hardhatconfigjs} + +지금까지 여러 종속성과 플러그인을 추가했습니다. 이제 프로젝트가 이 모든 것을 인식할 수 있도록 `hardhat.config.js`를 업데이트해야 합니다. + +`hardhat.config.js`를 다음과 같이 업데이트하세요: + +``` +require('dotenv').config(); + +require("@nomiclabs/hardhat-ethers"); +const { API_URL, PRIVATE_KEY } = process.env; + +/** +* @type import('hardhat/config').HardhatUserConfig +*/ +module.exports = { + solidity: "0.7.3", + defaultNetwork: "sepolia", + networks: { + hardhat: {}, + sepolia: { + url: API_URL, + accounts: [`0x${PRIVATE_KEY}`] + } + }, +} +``` + +## 14단계: 계약 컴파일하기 {#step-14-compile-our-contracts} + +지금까지 모든 것이 제대로 작동하는지 확인하기 위해 스마트 컨트랙트를 컴파일해 보겠습니다. `compile` 작업은 hardhat에 내장된 작업 중 하나입니다. + +명령줄에서 다음을 실행합니다. + +``` +npx hardhat compile +``` + +`SPDX license identifier not provided in source file`에 대한 경고가 표시될 수 있지만, 걱정할 필요는 없습니다. 나머지는 모두 정상일 것입니다! 그렇지 않은 경우 언제든지 [Alchemy discord](https://discord.gg/u72VCg3)로 메시지를 보내주세요. + +## 15단계: 배포 스크립트 작성하기 {#step-15-write-our-deploy-scripts} + +이제 스마트 컨트랙트가 작성되고 설정 파일을 사용할 수 있으므로 스마트 컨트랙트 배포 스크립트를 작성할 차례입니다. + +`scripts/` 폴더로 이동하여 `deploy.js`라는 새 파일을 만들고 다음 내용을 추가하세요. + +``` +async function main() { + const HelloWorld = await ethers.getContractFactory("HelloWorld"); + + // 배포를 시작하고, 계약 객체로 확인되는 프로미스를 반환합니다. + const hello_world = await HelloWorld.deploy("Hello World!"); + console.log("Contract deployed to address:", hello_world.address);} + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); +``` + +Hardhat은 [계약 튜토리얼](https://hardhat.org/tutorial/testing-contracts.html#writing-tests)에서 각 코드 라인이 무엇을 하는지 훌륭하게 설명하고 있으며, 저희는 그 설명을 여기에 채택했습니다. + +``` +const HelloWorld = await ethers.getContractFactory("HelloWorld"); +``` + +ethers.js의 `ContractFactory`는 새 스마트 계약을 배포하는 데 사용되는 추상화이므로, 여기서 `HelloWorld`는 우리의 hello world 계약 인스턴스를 위한 팩토리입니다. `hardhat-ethers` 플러그인을 사용할 때 `ContractFactory` 및 `Contract` 인스턴스는 기본적으로 첫 번째 서명자에 연결됩니다. + +``` +const hello_world = await HelloWorld.deploy(); +``` + +`ContractFactory`에서 `deploy()`를 호출하면 배포가 시작되고, `Contract`로 해석되는 `Promise`가 반환됩니다. 이것은 각 스마트 컨트랙트 기능에 대한 메소드가 있는 개체입니다. + +## 16단계: 계약 배포하기 {#step-16-deploy-our-contract} + +마침내 스마트 컨트랙트를 배포할 준비가 되었습니다! 명령줄로 이동하여 다음을 실행하세요: + +``` +npx hardhat run scripts/deploy.js --network sepolia +``` + +그러면 다음과 같은 내용이 표시됩니다. + +``` +계약이 배포된 주소: 0x6cd7d44516a20882cEa2DE9f205bF401c0d23570 +``` + +[Sepolia etherscan](https://sepolia.etherscan.io/)으로 이동하여 계약 주소를 검색하면 성공적으로 배포되었음을 확인할 수 있습니다. 트랜잭션은 다음과 같습니다. + +![etherscan contract](./etherscan-contract.png) + +`From` 주소는 MetaMask 계정 주소와 일치해야 하며 `To` 주소는 "Contract Creation"이라고 표시됩니다. 하지만 트랜잭션을 클릭하면 `To` 필드에 우리의 계약 주소가 표시됩니다: + +![etherscan transaction](./etherscan-transaction.png) + +축하해요! 이더리움 체인에 스마트 계약을 성공적으로 배포했습니다 🎉 + +내부에서 무슨 일이 일어나고 있는지 이해하기 위해 [Alchemy 대시보드](https://dashboard.alchemyapi.io/explorer)의 Explorer 탭으로 이동해 보겠습니다. Alchemy 앱이 여러 개 있는 경우, 앱별로 필터링하고 “Hello World”를 선택하세요. +![hello world explorer](./hello-world-explorer.png) + +여기서 `.deploy()` 함수를 호출했을 때 Hardhat/Ethers가 내부적으로 수행한 몇 가지 JSON-RPC 호출을 볼 수 있습니다. 여기서 주목해야 할 두 가지 중요한 호출은 실제로 Sepolia 체인에 계약을 작성하는 요청인 [`eth_sendRawTransaction`](https://www.alchemy.com/docs/node/abstract/abstract-api-endpoints/eth-send-raw-transaction)과, 해시가 주어졌을 때 트랜잭션에 대한 정보를 읽는 요청인 [`eth_getTransactionByHash`](https://www.alchemy.com/docs/node/abstract/abstract-api-endpoints/eth-get-transaction-by-hash)입니다(트랜잭션 시 일반적인 패턴). 트랜잭션 전송에 대해 더 알아보려면 [Web3를 사용한 트랜잭션 전송](/developers/tutorials/sending-transactions-using-web3-and-alchemy/)에 대한 이 튜토리얼을 확인하세요. + +이것으로 이 튜토리얼의 1부를 마칩니다. 2부에서는 초기 메시지를 업데이트하여 [스마트 계약과 실제로 상호작용](https://www.alchemy.com/docs/interacting-with-a-smart-contract)하고, 3부에서는 모든 사람이 상호작용 방법을 알 수 있도록 [스마트 계약을 Etherscan에 게시](https://www.alchemy.com/docs/submitting-your-smart-contract-to-etherscan)할 것입니다. + +**Alchemy에 대해 더 자세히 알고 싶으신가요?** 저희 [웹사이트](https://www.alchemy.com/eth)를 확인해 보세요. 업데이트를 놓치고 싶지 않으신가요? [여기](https://www.alchemy.com/newsletter)에서 뉴스레터를 구독하세요! 저희 [Discord](https://discord.gg/u72VCg3)에도 꼭 참여하세요.\*\*. diff --git a/public/content/translations/ko/developers/tutorials/how-to-implement-an-erc721-market/index.md b/public/content/translations/ko/developers/tutorials/how-to-implement-an-erc721-market/index.md new file mode 100644 index 00000000000..15ab408257e --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/how-to-implement-an-erc721-market/index.md @@ -0,0 +1,145 @@ +--- +title: "ERC-721 마켓을 구현하는 방법" +description: "토큰화된 아이템을 탈중앙화된 분류 게시판에 판매용으로 올리는 방법" +author: "Alberto Cuesta Cañada" +tags: [ "스마트 컨트랙트", "erc-721", "솔리디티", "토큰" ] +skill: intermediate +lang: ko +published: 2020-03-19 +source: Hackernoon +sourceUrl: https://hackernoon.com/how-to-implement-an-erc721-market-1e1a32j9 +--- + +이 글에서는 이더리움 블록체인을 위한 Craigslist를 코딩하는 방법을 보여드리겠습니다. + +Gumtree, Ebay, Craigslist 이전에는 분류 게시판이 주로 코르크나 종이로 만들어졌습니다. 학교 복도, 신문, 가로등, 상점 앞에 분류 게시판이 있었습니다. + +인터넷의 등장으로 모든 것이 바뀌었습니다. 특정 분류 게시판을 볼 수 있는 사람들의 수가 몇 배로 증가했습니다. 그로 인해, 그들이 대표하는 시장은 훨씬 더 효율적이 되었고 글로벌 규모로 확장되었습니다. Ebay는 이러한 물리적 분류 게시판에서 기원한 거대한 사업입니다. + +블록체인으로 이 시장들은 다시 한번 바뀔 것이며, 어떻게 바뀌는지 보여드리겠습니다. + +## 수익화 {#monetization} + +공개 블록체인 분류 게시판의 비즈니스 모델은 Ebay 및 그 회사의 모델과는 달라야 합니다. + +첫째, [탈중앙화의 관점](/developers/docs/web2-vs-web3/)이 있습니다. 기존 플랫폼은 자체 서버를 유지해야 합니다. 탈중앙화된 플랫폼은 사용자에 의해 유지되므로, 핵심 플랫폼을 운영하는 비용이 플랫폼 소유자에게는 0으로 떨어집니다. + +그리고 플랫폼에 대한 접근을 제공하는 웹사이트나 인터페이스인 프런트 엔드가 있습니다. 여기에는 많은 옵션이 있습니다. 플랫폼 소유자는 접근을 제한하고 모두가 자신의 인터페이스를 사용하도록 강제하며 수수료를 부과할 수 있습니다. 플랫폼 소유자는 접근을 개방하기로 결정할 수도 있습니다(권력을 민중에게!). 그리고 누구나 플랫폼에 대한 인터페이스를 구축하도록 허용할 수 있습니다. 또는 소유자는 이러한 극단적인 방법 사이의 어떤 접근 방식이든 결정할 수 있습니다. + +_저보다 더 큰 비전을 가진 비즈니스 리더들은 이것을 어떻게 수익화할지 알 것입니다._ _제가 보기에는 이것이 현상 유지와는 다르며 아마도 수익성이 있을 것이라는 점입니다._ + +더욱이, 자동화 및 결제 관점이 있습니다. 어떤 것들은 매우 [효과적으로 토큰화](https://hackernoon.com/tokenization-of-digital-assets-g0ffk3v8s?ref=hackernoon.com)될 수 있으며 분류 게시판에서 거래될 수 있습니다. 토큰화된 자산은 블록체인에서 쉽게 전송됩니다. 매우 복잡한 결제 방법도 블록체인에서 쉽게 구현될 수 있습니다. + +저는 여기서 사업 기회를 보고 있습니다. 운영 비용이 없는 분류 게시판은 각 거래에 복잡한 결제 경로를 포함하여 쉽게 구현될 수 있습니다. 누군가 이것을 무엇에 사용할지에 대한 아이디어를 낼 것이라고 확신합니다. + +저는 그냥 그것을 만드는 것이 행복합니다. 코드를 살펴보겠습니다. + +## 구현 {#implementation} + +얼마 전 우리는 비즈니스 사례 예제 구현 및 기타 유용한 것들이 포함된 [오픈 소스 리포지토리](https://github.com/HQ20/contracts?ref=hackernoon.com)를 시작했으니, 한번 살펴보세요. + +이 [이더리움 분류 게시판](https://github.com/HQ20/contracts/tree/master/contracts/classifieds?ref=hackernoon.com)의 코드가 거기에 있으니, 마음껏 활용하세요. 단, 이 코드는 감사를 받지 않았으므로 자금을 투입하기 전에 직접 실사를 해야 한다는 점을 유념하세요. + +게시판의 기본 사항은 복잡하지 않습니다. 게시판의 모든 광고는 몇 개의 필드를 가진 구조체일 뿐입니다. + +```solidity +struct Trade { + address poster; + uint256 item; + uint256 price; + bytes32 status; // 개시됨, 실행됨, 취소됨 +} +``` + +따라서 광고를 게시하는 사람이 있습니다. 판매용 아이템. 아이템의 가격. 거래의 상태는 개시됨, 실행됨 또는 취소됨일 수 있습니다. + +이 모든 거래는 매핑에 보관될 것입니다. 솔리디티의 모든 것이 매핑인 것 같기 때문입니다. 또한 편리하기 때문이기도 합니다. + +```solidity +mapping(uint256 => Trade) public trades; +``` + +매핑을 사용한다는 것은 각 광고를 게시하기 전에 ID를 만들어야 하며, 해당 광고에 대해 작업을 수행하기 전에 광고의 ID를 알아야 한다는 것을 의미합니다. 스마트 계약이나 프런트엔드에서 이를 처리하는 여러 방법이 있습니다. 조언이 필요하면 물어보세요. + +다음은 우리가 다루는 아이템이 무엇인지, 그리고 거래에 사용되는 통화가 무엇인지에 대한 질문입니다. + +아이템의 경우, 우리는 [ERC-721](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/IERC721.sol?ref=hackernoon.com) 인터페이스를 구현하도록 요구할 것입니다. 이는 실제로 블록체인에서 현실 세계의 아이템을 나타내는 방법일 뿐이지만, [디지털 자산에 가장 잘 작동](https://hackernoon.com/tokenization-of-digital-assets-g0ffk3v8s?ref=hackernoon.com)합니다. 우리는 생성자에서 자체 ERC721 계약을 지정할 것이며, 이는 우리 분류 게시판의 모든 자산이 사전에 토큰화되어야 함을 의미합니다. + +결제의 경우, 우리는 비슷한 작업을 할 것입니다. 대부분의 블록체인 프로젝트는 자체 [ERC-20](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol?ref=hackernoon.com) 암호화폐를 정의합니다. 다른 일부는 DAI와 같은 주류 통화를 사용하는 것을 선호합니다. 이 분류 게시판에서는 구축 시 어떤 통화를 사용할지만 결정하면 됩니다. 쉽습니다. + +```solidity +constructor ( + address _currencyTokenAddress, address _itemTokenAddress +) public { + currencyToken = IERC20(_currencyTokenAddress); + itemToken = IERC721(_itemTokenAddress); + tradeCounter = 0; +} +``` + +거의 다 됐습니다. 광고, 거래할 아이템, 결제할 통화가 있습니다. 광고를 만든다는 것은, 자신이 아이템을 가지고 있다는 것과 다른 게시판 등에 두 번 게시하지 않았다는 것을 모두 보여주기 위해 아이템을 에스크로에 넣는 것을 의미합니다. + +아래 코드는 정확히 그 작업을 수행합니다. 아이템을 에스크로에 넣고, 광고를 생성하며, 몇 가지 정리 작업을 수행합니다. + +```solidity +function openTrade(uint256 _item, uint256 _price) + public +{ + itemToken.transferFrom(msg.sender, address(this), _item); + trades[tradeCounter] = Trade({ + poster: msg.sender, + item: _item, + price: _price, + status: "Open" + }); + tradeCounter += 1; + emit TradeStatusChange(tradeCounter - 1, "Open"); +} +``` + +거래를 수락한다는 것은 광고(거래)를 선택하고, 가격을 지불하며, 아이템을 받는 것을 의미합니다. 아래 코드는 거래를 검색합니다. 이용 가능한지 확인합니다. 아이템 대금을 지불합니다. 아이템을 받습니다. 광고를 업데이트합니다. + +```solidity +function executeTrade(uint256 _trade) + public +{ + Trade memory trade = trades[_trade]; + require(trade.status == "Open", "거래가 개시되지 않았습니다."); + currencyToken.transferFrom(msg.sender, trade.poster, trade.price); + itemToken.transferFrom(address(this), msg.sender, trade.item); + trades[_trade].status = "Executed"; + emit TradeStatusChange(_trade, "Executed"); +} +``` + +마지막으로, 판매자가 구매자가 수락하기 전에 거래를 취소할 수 있는 옵션이 있습니다. 일부 모델에서는 광고가 만료되기 전 일정 기간 동안 활성화됩니다. 시장의 디자인에 따라 선택하면 됩니다. + +코드는 거래를 실행하는 데 사용되는 코드와 매우 유사하며, 단지 통화가 오가지 않고 아이템이 광고 게시자에게 돌아간다는 점만 다릅니다. + +```solidity +function cancelTrade(uint256 _trade) + public +{ + Trade memory trade = trades[_trade]; + require( + msg.sender == trade.poster, + "거래는 게시자만 취소할 수 있습니다." + ); + require(trade.status == "Open", "거래가 개시되지 않았습니다."); + itemToken.transferFrom(address(this), trade.poster, trade.item); + trades[_trade].status = "Cancelled"; + emit TradeStatusChange(_trade, "Cancelled"); +} +``` + +이것으로 끝입니다. 구현의 끝까지 오셨습니다. 일부 비즈니스 개념이 코드로 표현될 때 얼마나 간결해지는지는 꽤 놀라운데, 이것이 바로 그런 경우 중 하나입니다. 전체 계약은 [저희 리포지토리](https://github.com/HQ20/contracts/blob/master/contracts/classifieds/Classifieds.sol)에서 확인하세요. + +## 결론 {#conclusion} + +분류 게시판은 인터넷과 함께 대규모로 확장된 일반적인 시장 구성으로, 소수의 독점적 승자와 함께 매우 인기 있는 비즈니스 모델이 되었습니다. + +또한 분류 게시판은 블록체인 환경에서 복제하기 쉬운 도구이며, 기존의 거대 기업에 대한 도전을 가능하게 하는 매우 특정한 기능을 가지고 있습니다. + +이 글에서 저는 분류 게시판 비즈니스의 비즈니스 현실과 기술적 구현을 연결하려고 시도했습니다. 이 지식은 여러분이 적절한 기술을 가지고 있다면 구현을 위한 비전과 로드맵을 만드는 데 도움이 될 것입니다. + +언제나처럼, 재미있는 것을 만들고 있고 조언이 필요하다면 언제든지 [연락 주세요](https://albertocuesta.es/)! 언제나 기꺼이 도와드리겠습니다. diff --git a/public/content/translations/ko/developers/tutorials/how-to-mint-an-nft/index.md b/public/content/translations/ko/developers/tutorials/how-to-mint-an-nft/index.md new file mode 100644 index 00000000000..1b624a60329 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/how-to-mint-an-nft/index.md @@ -0,0 +1,329 @@ +--- +title: "NFT 발행 방법(NFT 튜토리얼 시리즈 2/3부)" +description: "이 튜토리얼에서는 스마트 계약과 Web3를 사용하여 이더리움 블록체인에서 NFT를 발행하는 방법을 설명합니다." +author: "Sumi Mudgil" +tags: [ "ERC-721", "alchemy", "솔리디티", "스마트 계약" ] +skill: beginner +lang: ko +published: 2021-04-22 +--- + +[Beeple](https://www.nytimes.com/2021/03/11/arts/design/nft-auction-christies-beeple.html): 6,900만 달러 +[3LAU](https://www.forbes.com/sites/abrambrown/2021/03/03/3lau-nft-nonfungible-tokens-justin-blau/?sh=5f72ef64643b): 1,100만 달러 +[Grimes](https://www.theguardian.com/music/2021/mar/02/grimes-sells-digital-art-collection-non-fungible-tokens): 600만 달러 + +이들 모두 Alchemy의 강력한 API를 사용하여 NFT를 발행했습니다. 이 튜토리얼에서는 10분 이내에 동일한 작업을 수행하는 방법을 알려드립니다. + +'NFT 발행'은 블록체인에 ERC-721 토큰의 고유한 인스턴스를 게시하는 행위입니다. [이 NFT 튜토리얼 시리즈의 1부](/developers/tutorials/how-to-write-and-deploy-an-nft/)의 스마트 계약을 사용하여 Web3 기술을 발휘하고 NFT를 발행해 봅시다. 이 튜토리얼이 끝나면 여러분(과 여러분의 지갑)이 원하는 만큼 많은 NFT를 발행할 수 있게 될 것입니다! + +이제 시작하겠습니다! + +## 1단계: Web3 설치하기 {#install-web3} + +NFT 스마트 계약 생성에 대한 첫 번째 튜토리얼을 따라오셨다면, 이미 Ethers.js를 사용해 본 경험이 있으실 것입니다. Web3는 이더리움 블록체인에 대한 요청 생성을 더 쉽게 만들어주는 라이브러리라는 점에서 Ethers와 비슷합니다. 이 튜토리얼에서는 자동 재시도와 강력한 WebSocket 지원을 제공하는 향상된 Web3 라이브러리인 [Alchemy Web3](https://docs.alchemyapi.io/alchemy/documentation/alchemy-web3)를 사용할 것입니다. + +프로젝트 홈 디렉터리에서 다음을 실행하세요. + +``` +npm install @alch/alchemy-web3 +``` + +## 2단계: `mint-nft.js` 파일 생성하기 {#create-mintnftjs} + +scripts 디렉터리 내부에 `mint-nft.js` 파일을 만들고 다음 코드 줄을 추가하세요. + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) +``` + +## 3단계: 계약 ABI 가져오기 {#contract-abi} + +계약 ABI(애플리케이션 바이너리 인터페이스)는 스마트 계약과 상호 작용하기 위한 인터페이스입니다. 계약 ABI에 대해 [여기](https://docs.alchemyapi.io/alchemy/guides/eth_getlogs#what-are-ab-is)에서 더 자세히 알아볼 수 있습니다. Hardhat은 자동으로 ABI를 생성하여 `MyNFT.json` 파일에 저장합니다. 이를 사용하려면 `mint-nft.js` 파일에 다음 코드 줄을 추가하여 내용을 파싱해야 합니다. + +```js +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +``` + +ABI를 보려면 콘솔에 출력할 수 있습니다. + +```js +console.log(JSON.stringify(contract.abi)) +``` + +`mint-nft.js`를 실행하고 ABI가 콘솔에 출력되는 것을 보려면 터미널로 이동하여 다음을 실행하세요. + +```js +node scripts/mint-nft.js +``` + +## 4단계: IPFS를 사용하여 NFT 메타데이터 구성하기 {#config-meta} + +1부 튜토리얼에서 기억하시겠지만, `mintNFT` 스마트 계약 함수는 NFT의 메타데이터를 설명하는 JSON 문서로 확인되어야 하는 tokenURI 매개변수를 받습니다. 이 메타데이터는 이름, 설명, 이미지 및 기타 속성과 같은 구성 가능한 속성을 가질 수 있게 하여 NFT에 생명을 불어넣는 것입니다. + +> _IPFS(Interplanetary File System)는 분산 파일 시스템에서 데이터를 저장하고 공유하기 위한 탈중앙화 프로토콜 및 P2P 네트워크입니다._ + +NFT가 진정으로 탈중앙화되도록 보장하기 위해, 편리한 IPFS API 및 툴킷인 Pinata를 사용하여 NFT 자산과 메타데이터를 저장할 것입니다. Pinata 계정이 없다면, [여기](https://app.pinata.cloud)에서 무료 계정에 가입하고 이메일 인증 절차를 완료하세요. + +계정을 생성한 후: + +- '파일' 페이지로 이동하여 페이지 왼쪽 상단의 파란색 '업로드' 버튼을 클릭합니다. + +- Pinata에 이미지를 업로드합니다. 이 이미지가 NFT의 이미지 자산이 됩니다. 자산 이름은 원하는 대로 지정할 수 있습니다 + +- 업로드 후에는 '파일' 페이지의 표에서 파일 정보를 볼 수 있습니다. CID 열도 표시됩니다. CID 옆에 있는 복사 버튼을 클릭하여 CID를 복사할 수 있습니다. 업로드한 파일은 `https://gateway.pinata.cloud/ipfs/`에서 볼 수 있습니다. 예를 들어, 저희가 사용한 이미지는 IPFS [여기](https://gateway.pinata.cloud/ipfs/QmZdd5KYdCFApWn7eTZJ1qgJu18urJrP9Yh1TZcZrZxxB5)에서 찾을 수 있습니다. + +시각적 학습자를 위해 위 단계는 여기에 요약되어 있습니다. + +![Pinata에 이미지 업로드 방법](./instructionsPinata.gif) + +이제 Pinata에 문서를 하나 더 업로드해야 합니다. 하지만 그 전에 문서를 만들어야 합니다! + +루트 디렉터리에서 `nft-metadata.json`이라는 새 파일을 만들고 다음 JSON 코드를 추가합니다. + +```json +{ + "attributes": [ + { + "trait_type": "Breed", + "value": "Maltipoo" + }, + { + "trait_type": "Eye color", + "value": "Mocha" + } + ], + "description": "The world's most adorable and sensitive pup.", + "image": "ipfs://QmWmvTJmJU3pozR9ZHFmQC2DNDwi2XJtf3QGyYiiagFSWb", + "name": "Ramses" +} +``` + +JSON의 데이터는 자유롭게 변경할 수 있습니다. attributes 섹션을 제거하거나 추가할 수 있습니다. 가장 중요한 것은 `image` 필드가 여러분의 IPFS 이미지 위치를 가리키도록 하는 것입니다. 그렇지 않으면 여러분의 NFT에는 (아주 귀여운!) 강아지 사진이 포함될 것입니다. + +JSON 파일 편집이 끝나면 저장한 다음, 이미지를 업로드할 때와 동일한 단계에 따라 Pinata에 업로드하세요. + +![Pinata에 nft-metadata.json 업로드 방법](./uploadPinata.gif) + +## 5단계: 계약 인스턴스 생성하기 {#instance-contract} + +이제 계약과 상호 작용하려면 코드에서 계약의 인스턴스를 만들어야 합니다. 이를 위해 계약 주소가 필요하며, 이는 배포에서 얻거나 계약을 배포하는 데 사용한 주소를 [Blockscout](https://eth-sepolia.blockscout.com/)에서 조회하여 얻을 수 있습니다. + +![Etherscan에서 계약 주소 보기](./view-contract-etherscan.png) + +위 예시에서 계약 주소는 0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778입니다. + +다음으로 Web3 [contract 메서드](https://docs.web3js.org/api/web3-eth-contract/class/Contract)를 사용하여 ABI와 주소를 이용해 계약을 생성합니다. `mint-nft.js` 파일에 다음을 추가하세요: + +```js +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" + +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) +``` + +## 6단계: `.env` 파일 업데이트하기 {#update-env} + +이제 이더리움 체인에 트랜잭션을 생성하고 전송하기 위해, 여러분의 공개 이더리움 계정 주소를 사용하여 계정 논스(아래에서 설명)를 가져옵니다. + +공개 키를 `.env` 파일에 추가하세요. 튜토리얼 1부를 완료했다면 `.env` 파일은 이제 다음과 같을 것입니다: + +```js +API_URL = "https://eth-sepolia.g.alchemy.com/v2/your-api-key" +PRIVATE_KEY = "your-private-account-address" +PUBLIC_KEY = "your-public-account-address" +``` + +## 7단계: 트랜잭션 생성하기 {#create-txn} + +먼저 `mintNFT(tokenData)`라는 이름의 함수를 정의하고 다음을 수행하여 트랜잭션을 생성해 보겠습니다. + +1. `.env` 파일에서 _PRIVATE_KEY_와 _PUBLIC_KEY_를 가져옵니다. + +2. 다음으로 계정 논스를 알아내야 합니다. 논스 사양은 여러분의 주소에서 보낸 트랜잭션 수를 추적하는 데 사용됩니다. 이는 보안 목적과 [재전송 공격](https://docs.alchemyapi.io/resources/blockchain-glossary#account-nonce)을 방지하기 위해 필요합니다. 주소에서 보낸 트랜잭션 수를 가져오려면 [getTransactionCount](https://docs.alchemyapi.io/documentation/alchemy-api-reference/json-rpc#eth_gettransactioncount)를 사용합니다. + +3. 마지막으로 다음 정보로 트랜잭션을 설정합니다. + +- `'from': PUBLIC_KEY` — 트랜잭션의 발신지는 공개 주소입니다 + +- `'to': contractAddress` — 상호 작용하고 트랜잭션을 보낼 계약입니다 + +- `'nonce': nonce` — 주소에서 보낸 트랜잭션 수가 포함된 계정 논스입니다 + +- `'gas': estimatedGas` — 트랜잭션을 완료하는 데 필요한 예상 가스입니다 + +- `'data': nftContract.methods.mintNFT(PUBLIC_KEY, md).encodeABI()` — 이 트랜잭션에서 수행하려는 계산으로, 이 경우 NFT를 발행하는 것입니다 + +이제 `mint-nft.js` 파일은 다음과 같아야 합니다: + +```js + require('dotenv').config(); + const API_URL = process.env.API_URL; + const PUBLIC_KEY = process.env.PUBLIC_KEY; + const PRIVATE_KEY = process.env.PRIVATE_KEY; + + const { createAlchemyWeb3 } = require("@alch/alchemy-web3"); + const web3 = createAlchemyWeb3(API_URL); + + const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json"); + const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778"; + const nftContract = new web3.eth.Contract(contract.abi, contractAddress); + + async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, 'latest'); //최신 논스 가져오기 + + //트랜잭션 + const tx = { + 'from': PUBLIC_KEY, + 'to': contractAddress, + 'nonce': nonce, + 'gas': 500000, + 'data': nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI() + }; + }​ +``` + +## 8단계: 트랜잭션 서명하기 {#sign-txn} + +이제 트랜잭션을 만들었으므로 전송하기 위해 서명해야 합니다. 여기서 개인 키를 사용합니다. + +`web3.eth.sendSignedTransaction`은 트랜잭션 해시를 제공하며, 이를 사용하여 트랜잭션이 채굴되었는지, 네트워크에서 누락되지 않았는지 확인할 수 있습니다. 트랜잭션 서명 섹션에서 트랜잭션이 성공적으로 통과되었는지 알 수 있도록 몇 가지 오류 확인을 추가한 것을 알 수 있습니다. + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const PUBLIC_KEY = process.env.PUBLIC_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY + +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) + +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) + +async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest") //최신 논스 가져오기 + + //트랜잭션 + const tx = { + from: PUBLIC_KEY, + to: contractAddress, + nonce: nonce, + gas: 500000, + data: nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI(), + } + + const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY) + signPromise + .then((signedTx) => { + web3.eth.sendSignedTransaction( + signedTx.rawTransaction, + function (err, hash) { + if (!err) { + console.log( + "트랜잭션의 해시는 다음과 같습니다: ", + hash, + "\nAlchemy의 Mempool에서 트랜잭션 상태를 확인하세요!" + ) + } else { + console.log( + "트랜잭션을 제출하는 동안 문제가 발생했습니다:", + err + ) + } + } + ) + }) + .catch((err) => { + console.log(" Promise 실패:", err) + }) +} +``` + +## 9단계: `mintNFT` 호출 및 `node mint-nft.js` 실행하기 {#call-mintnft-fn} + +Pinata에 업로드한 `metadata.json`을 기억하시나요? Pinata에서 해시코드를 가져와 `https://gateway.pinata.cloud/ipfs/`를 `mintNFT` 함수에 매개변수로 전달하세요. + +해시코드를 찾는 방법은 아래와 같습니다. + +![Pinata에서 NFT 메타데이터 해시코드 가져오는 방법](./metadataPinata.gif)_Pinata에서 NFT 메타데이터 해시코드를 가져오는 방법_ + +> 복사한 해시코드가 `https://gateway.pinata.cloud/ipfs/`를 별도의 창에 로드하여 **metadata.json**에 연결되는지 다시 확인하세요. 페이지는 아래 스크린샷과 비슷하게 보일 것입니다. + +![페이지에 json 메타데이터가 표시되어야 합니다](./metadataJSON.png)_페이지에 json 메타데이터가 표시되어야 합니다_ + +전체 코드는 다음과 같아야 합니다. + +```js +require("dotenv").config() +const API_URL = process.env.API_URL +const PUBLIC_KEY = process.env.PUBLIC_KEY +const PRIVATE_KEY = process.env.PRIVATE_KEY + +const { createAlchemyWeb3 } = require("@alch/alchemy-web3") +const web3 = createAlchemyWeb3(API_URL) + +const contract = require("../artifacts/contracts/MyNFT.sol/MyNFT.json") +const contractAddress = "0x5a738a5c5fe46a1fd5ee7dd7e38f722e2aef7778" +const nftContract = new web3.eth.Contract(contract.abi, contractAddress) + +async function mintNFT(tokenURI) { + const nonce = await web3.eth.getTransactionCount(PUBLIC_KEY, "latest") //최신 논스 가져오기 + + //트랜잭션 + const tx = { + from: PUBLIC_KEY, + to: contractAddress, + nonce: nonce, + gas: 500000, + data: nftContract.methods.mintNFT(PUBLIC_KEY, tokenURI).encodeABI(), + } + + const signPromise = web3.eth.accounts.signTransaction(tx, PRIVATE_KEY) + signPromise + .then((signedTx) => { + web3.eth.sendSignedTransaction( + signedTx.rawTransaction, + function (err, hash) { + if (!err) { + console.log( + "트랜잭션의 해시는 다음과 같습니다: ", + hash, + "\nAlchemy의 Mempool에서 트랜잭션 상태를 확인하세요!" + ) + } else { + console.log( + "트랜잭션을 제출하는 동안 문제가 발생했습니다:", + err + ) + } + } + ) + }) + .catch((err) => { + console.log("Promise 실패:", err) + }) +} + +mintNFT("ipfs://QmYueiuRNmL4MiA2GwtVMm6ZagknXnSpQnB3z2gWbz36hP") +``` + +이제 `node scripts/mint-nft.js`를 실행하여 NFT를 배포하세요. 몇 초 후 터미널에 다음과 같은 응답이 표시되어야 합니다. + + ``` + 트랜잭션의 해시는 다음과 같습니다: 0x301791fdf492001fcd9d5e5b12f3aa1bbbea9a88ed24993a8ab2cdae2d06e1e8 + + Alchemy의 Mempool에서 트랜잭션 상태를 확인하세요! + ``` + +다음으로 [Alchemy mempool](https://dashboard.alchemyapi.io/mempool)을 방문하여 트랜잭션 상태(대기 중, 채굴됨 또는 네트워크에서 누락됨)를 확인하세요. 트랜잭션이 누락된 경우, [Blockscout](https://eth-sepolia.blockscout.com/)에서 트랜잭션 해시를 검색해 보는 것도 도움이 됩니다. + +![Etherscan에서 NFT 트랜잭션 해시 보기](./view-nft-etherscan.png)_Etherscan에서 NFT 트랜잭션 해시 보기_ + +이것으로 끝입니다! 이제 이더리움 블록체인에서 NFT를 배포하고 발행했습니다 + +`mint-nft.js`를 사용하면 여러분(과 여러분의 지갑)이 원하는 만큼 많은 NFT를 발행할 수 있습니다! NFT의 메타데이터를 설명하는 새로운 tokenURI를 전달해야 합니다(그렇지 않으면 ID만 다른 동일한 NFT를 여러 개 만들게 됩니다). + +아마 여러분의 지갑에 있는 NFT를 자랑하고 싶을 것입니다. 그렇다면 [3부: 지갑에서 NFT를 보는 방법](/developers/tutorials/how-to-view-nft-in-metamask/)을 꼭 확인하세요! diff --git a/public/content/translations/ko/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md b/public/content/translations/ko/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md new file mode 100644 index 00000000000..878fa997912 --- /dev/null +++ b/public/content/translations/ko/developers/tutorials/how-to-mock-solidity-contracts-for-testing/index.md @@ -0,0 +1,102 @@ +--- +title: "테스트를 위해 솔리디티 스마트 계약을 모의하는 방법" +description: "테스트할 때 계약을 놀려야 하는 이유" +author: Markus Waas +lang: ko +tags: [ "솔리디티", "스마트 계약", "테스트", "모킹" ] +skill: intermediate +published: 2020-05-02 +source: soliditydeveloper.com +sourceUrl: https://soliditydeveloper.com/mocking-contracts +--- + +[모의 객체](https://wikipedia.org/wiki/Mock_object)는 객체 지향 프로그래밍에서 흔히 사용되는 디자인 패턴입니다. 이는 '놀리다'라는 의미를 가진 고대 프랑스어 'mocquer'에서 유래했으며, '실제 무언가를 모방하다'라는 의미로 발전했습니다. 이것이 바로 프로그래밍에서 우리가 하는 일입니다. 원한다면 스마트 계약을 놀려도 되지만, 가능하면 언제든지 모의(mock)하세요. 그러면 삶이 더 편해집니다. + +## 모의 객체로 계약 단위 테스트하기 {#unit-testing-contracts-with-mocks} + +계약을 모의한다는 것은 기본적으로 원래 계약과 매우 유사하게 작동하지만 개발자가 쉽게 제어할 수 있는 방식으로 해당 계약의 두 번째 버전을 만드는 것을 의미합니다. 계약이 복잡해서 [계약의 일부만 단위 테스트](/developers/docs/smart-contracts/testing/)하고 싶은 경우가 많습니다. 문제는 이 작은 부분을 테스트하는 데 도달하기 어려운 매우 특정한 계약 상태가 필요한 경우입니다. + +매번 계약을 필요한 상태로 만드는 복잡한 테스트 설정 로직을 작성하거나 모의 객체를 작성할 수 있습니다. 상속을 사용하면 계약을 쉽게 모의할 수 있습니다. 원래 계약에서 상속받는 두 번째 모의 계약을 만들기만 하면 됩니다. 이제 모의 객체에 대한 함수를 재정의할 수 있습니다. 예를 들어 살펴보겠습니다. + +## 예시: 비공개 ERC20 {#example-private-erc20} + +초기 비공개 기간이 있는 ERC-20 계약 예시를 사용합니다. 소유자는 비공개 사용자를 관리할 수 있으며, 초기에는 해당 사용자만 토큰을 받을 수 있습니다. 일정 시간이 지나면 누구나 토큰을 사용할 수 있습니다. 궁금하시다면, 새로운 OpenZeppelin 계약 v3의 [`_beforeTokenTransfer`](https://docs.openzeppelin.com/contracts/5.x/extending-contracts#using-hooks) 훅을 사용하고 있습니다. + +```solidity +pragma solidity ^0.6.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract PrivateERC20 is ERC20, Ownable { + mapping (address => bool) public isPrivateUser; + uint256 private publicAfterTime; + + constructor(uint256 privateERC20timeInSec) ERC20("PrivateERC20", "PRIV") public { + publicAfterTime = now + privateERC20timeInSec; + } + + function addUser(address user) external onlyOwner { + isPrivateUser[user] = true; + } + + function isPublic() public view returns (bool) { + return now >= publicAfterTime; + } + + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + + require(_validRecipient(to), "PrivateERC20: invalid recipient"); + } + + function _validRecipient(address to) private view returns (bool) { + if (isPublic()) { + return true; + } + + return isPrivateUser[to]; + } +} +``` + +이제 모의해 보겠습니다. + +```solidity +pragma solidity ^0.6.0; +import "../PrivateERC20.sol"; + +contract PrivateERC20Mock is PrivateERC20 { + bool isPublicConfig; + + constructor() public PrivateERC20(0) {} + + function setIsPublic(bool isPublic) external { + isPublicConfig = isPublic; + } + + function isPublic() public view returns (bool) { + return isPublicConfig; + } +} +``` + +다음 오류 메시지 중 하나가 표시됩니다. + +- `PrivateERC20Mock.sol: TypeError: Overriding function is missing "override" specifier.` +- `PrivateERC20.sol: TypeError: Trying to override non-virtual function. Did you forget to add "virtual"?.` + +새로운 0.6 솔리디티 버전을 사용하고 있으므로, 재정의할 수 있는 함수에는 `virtual` 키워드를 추가하고 재정의하는 함수에는 `override`를 추가해야 합니다. 따라서 두 `isPublic` 함수에 이를 추가해 보겠습니다. + +이제 단위 테스트에서 대신 `PrivateERC20Mock`을 사용할 수 있습니다. 비공개 사용 시간 동안의 동작을 테스트하려면 `setIsPublic(false)`를 사용하고, 공개 사용 시간을 테스트하려면 `setIsPublic(true)`를 사용하세요. 물론 이 예시에서는 [시간 헬퍼](https://docs.openzeppelin.com/test-helpers/0.5/api#increase)를 사용하여 시간을 적절하게 변경할 수도 있습니다. 하지만 이제 모킹의 개념이 명확해졌을 것이며, 단순히 시간을 앞당기는 것만큼 쉽지 않은 시나리오를 상상할 수 있을 것입니다. + +## 많은 계약 모킹하기 {#mocking-many-contracts} + +모든 단일 모의 객체에 대해 다른 계약을 만들어야 한다면 복잡해질 수 있습니다. 이것이 불편하다면 [MockContract](https://github.com/gnosis/mock-contract) 라이브러리를 살펴볼 수 있습니다. 이를 통해 즉석에서 계약의 동작을 재정의하고 변경할 수 있습니다. 하지만 다른 계약에 대한 호출을 모의하는 경우에만 작동하므로 우리 예시에는 적용되지 않습니다. + +## 모킹은 훨씬 더 강력할 수 있습니다 {#mocking-can-be-even-more-powerful} + +모킹의 힘은 여기서 그치지 않습니다. + +- 함수 추가: 특정 함수를 재정의하는 것뿐만 아니라 추가 함수를 더하는 것도 유용합니다. 토큰의 좋은 예는 모든 사용자가 무료로 새 토큰을 받을 수 있도록 추가적인 `mint` 함수를 두는 것입니다. +- 테스트넷에서 사용: 탈중앙화앱과 함께 테스트넷에서 계약을 배포하고 테스트할 때 모의 버전을 사용하는 것을 고려해 보세요. 꼭 필요한 경우가 아니면 함수 재정의를 피하세요. 결국 실제 로직을 테스트하고 싶을 것입니다. 하지만 예를 들어 계약 상태를 처음으로 간단히 리셋하는 리셋 함수를 추가하면 유용할 수 있으며, 새로운 배포가 필요하지 않습니다. 당연히 메인넷 계약에는 이것을 원하지 않을 것입니다.