Art Blocks智能合约分解
Art Blocks是一个创建链上生成NFT的平台。但是你知道在链上和链下究竟保留了什么吗?为什么他们的智能合约中需要JavaScript?
我们将通过分解Art Blocks的智能合约找到答案。我们还将了解图片是如何生成/渲染的,以及Art Blocks从哪里获得生成图片所需的随机性。
以下是这篇文章的大纲
首先,介绍一下Art Blocks的背景。
Art Blocks 是一个平台(实际上只是一个智能合约),在这里你可以创建生成NFT。艺术家提交可以生成图像的脚本。Art Blocks存储这些脚本,当有人想铸造一个NFT时,它会创建一个独特的哈希值。这个哈希值被用作图像生成算法的种子,生成的图像对挖掘者来说是独一无二的。
下面是一些生成图像的例子:
流行的Art Blocks集合: Ringers, Chromie Squiggle, Fidenza.
为了理解Art Blocks智能合约,我们首先需要了解ERC-721。ERC-721是一个用于实现NFT智能合约的标准。为了兼容ERC-721,一个合约需要实现这些功能:
pragma solidity ^ 0.4 .20;
interface ERC721 {
function name() public view returns(string);
function symbol() public view returns(string);
function tokenURI(uint256 _tokenId) public view returns(string);
function totalSupply() public view returns(uint256);
function tokenByIndex(uint256 _index) public view returns(uint256);
function tokenOfOwnerByIndex(address _owner, uint256 _index) public view
returns(uint256);
function balanceOf(address _owner) public view returns(uint256);
function ownerOf(uint256 _tokenId) public view returns(address);
function approve(address _approved, uint256 _tokenId) public payable;
function transferFrom(address _from, address _to, uint256 _tokenId) public
payable;
}
name
和symbol
是NFT描述符。例如,对于Art Blocks,它们是 "Art Blocks "和 "BLOCKS"。tokenUri
- 代币元数据的路径(图像网址,稀有度属性等)totalSupply
- 该合约跟踪的NFT数量tokenByIndex
- 返回指定索引的tokenId,索引为[0, totalSupply]。tokenOfOwnerByIndex
- 枚举所有者的代币并返回索引处的tokenId。balanceOf
- 所有者拥有的NFT的数量ownerOf
- 指定代币的所有者approve
- 允许其他人管理(转让、出售等)自己的代币。(有一个类似的函数setApprovalForAll(address _operator, bool _approved)
,它和approval一样,但给予所有代币的权限,而不仅仅是一个。为简洁起见,跳过)。transferFrom
- 转账代币。调用者需要是一个预先授权的地址。所有NFT智能合约都需要实现ERC-721标准。这允许像OpenSea这样的第三方以标准化的方式与NFT合约交互(例如,所有的合约将有相同的ownerOf
功能)。请看我的文章BoredApeYachtClub智能合约分解,了解更多关于ERC-721标准。
现在让我们来了解一下Art Blocks是如何实现这个标准并创建生成NFT的。
Art Blocks的区块链后端只包括一个大的智能合约,叫做GenArt721Core.sol
。这个智能合约被分解成2块。
GenArt721Core.sol
,负责存储渲染NFT所需的数据。GenArt721Core.sol
继承自ERC-721合约。源代码可以在Etherscan和Github找到。
Art Blocks还有两个轻量级合约:
GenArt721Minter
(铸造代币和接受付款)和Randomizer
(生成伪随机数)。但这些将不会在本文中涉及。
Art Blocks使用一个现成的OpenZeppelin的实现来实现ERC-721接口。OpenZeppelin是一个最常用标准的实现库。
pragma solidity ^ 0.5 .0;
// Mapping from token ID to owner
mapping(uint256 => address) private _tokenOwner;
// Mapping from owner to number of owned token
mapping(address => Counters.Counter) private _ownedTokensCount;
function balanceOf(address owner) public view returns(uint256) {
require(owner != address(0), "ERC721: balance query for the zero address");
return _ownedTokensCount[owner].current();
}
function ownerOf(uint256 tokenId) public view returns(address) {
address owner = _tokenOwner[tokenId];
require(owner != address(0), "ERC721: owner query for nonexistent token");
return owner;
}
pragma solidity ^ 0.5 .0;
function transferFrom(address from, address to, uint256 tokenId) public {
// ...
_ownedTokensCount[from].decrement();
_ownedTokensCount[to].increment();
_tokenOwner[tokenId] = to;
// ...
}
pragma solidity ^ 0.5 .0;
// Mapping from token ID to approved address
mapping(uint256 => address) private _tokenApprovals;
function approve(address to, uint256 tokenId) public {
address owner = ownerOf(tokenId);
require(to != owner, "ERC721: approval to current owner");
require(msg.sender == owner | isApprovedForAll(owner, msg.sender),
"ERC721: approve caller is not owner nor approved for all"
);
_tokenApprovals[tokenId] = to;
emit Approval(owner, to, tokenId);
}
mint
和burn
功能。pragma solidity ^ 0.5 .0;
function _mint(address to, uint256 tokenId) internal {
_tokenOwner[tokenId] = to;
_ownedTokensCount[to].increment();
}
function _burn(address owner, uint256 tokenId) internal {
_ownedTokensCount[owner].decrement();
_tokenOwner[tokenId] = address(0);
}
pragma solidity ^ 0.5 .0;
// Mapping from owner to list of owned token IDs
mapping(address => uint256[]) private _ownedTokens;
// Mapping from token ID to index of the owner tokens list
mapping(uint256 => uint256) private _ownedTokensIndex;
// Array with all token ids, used for enumeration
uint256[] private _allTokens;
// Mapping from token id to position in the allTokens array
mapping(uint256 => uint256) private _allTokensIndex;
pragma solidity ^ 0.5 .0;
function totalSupply() public view returns(uint256) {
return _allTokens.length;
}
function tokenByIndex(uint256 index) public view returns(uint256) {
require(index < totalSupply(), "ERC721Enumerable: global index out of
bounds ");
return _allTokens[index];
}
function tokenOfOwnerByIndex(address owner, uint256 index) public view
returns(uint256) {
require(index < balanceOf(owner), "ERC721Enumerable: owner index out of
bounds ");
return _ownedTokens[owner][index];
}
tokenUri
,将在文章后面解释。GenArt721Core.sol
该主合约扩展了ERC-721合约,增加了Art Blocks的特定功能:"存储项目信息 "和 "生成NFT"。让我们从存储项目信息部分开始。
每个NFT集合都被认为是一个独立的项目(如Chromie Squiggle、Ringers等)。主合约定义了一个项目的数据结构。
pragma solidity ^ 0.5 .0;
struct Project {
string name;
string artist;
string description;
string website;
string license;
bool active;
bool locked;
bool paused;
// number of NFTs minted for this project
uint256 invocations;
uint256 maxInvocations;
// Javascript scripts used to generate the images
uint scriptCount; // number of scripts
mapping(uint256 => string) scripts; // store each script as a string
string scriptJSON; // script metadata such as what libraries it depends on
bool useHashString; // if true, hash is used as an input to generate the image
// whether project dynamic or static
bool dynamic;
// if project is dynamic, tokenUri will be "{projectBaseUri}/{tokenId}"
string projectBaseURI;
// if project is static, will use IPFS
bool useIpfs;
// tokenUri will be "{projectBaseIpfsURI}/{ipfsHash}"
string projectBaseIpfsURI;
string ipfsHash;
}
所有项目的NFT都存储在一个大的智能合约中--我们不会为每个集合创建一个新的合约。所有的项目都存储在一个大的映射中,称为projects
,其中的键只是项目的索引(0,1,2,...)。
pragma solidity ^ 0.5 .0;
mapping(uint256 => Project) projects;
uint256 public nextProjectId = 3;
function addProject(
string memory _projectName,
address _artistAddress,
uint256 _pricePerTokenInWei,
bool _dynamic) public onlyWhitelisted {
uint256 projectId = nextProjectId;
projectIdToArtistAddress[projectId] = _artistAddress;
projects[projectId].name = _projectName;
projectIdToCurrencySymbol[projectId] = "ETH";
projectIdToPricePerTokenInWei[projectId] = _pricePerTokenInWei;
projects[projectId].paused = true;
projects[projectId].dynamic = _dynamic;
projects[projectId].maxInvocations = ONE_MILLION;
if (!_dynamic) {
projects[projectId].useHashString = false;
} else {
projects[projectId].useHashString = true;
}
nextProjectId = nextProjectId.add(1);
}
从上面的截图中你可能已经注意到,合约还使用了一些数据结构来跟踪所有的东西。
pragma solidity ^ 0.5 .0;
//All financial functions are stripped from Project struct for visibility
mapping(uint256 => address) public projectIdToArtistAddress;
mapping(uint256 => string) public projectIdToCurrencySymbol;
mapping(uint256 => address) public projectIdToCurrencyAddress;
mapping(uint256 => uint256) public projectIdToPricePerTokenInWei;
mapping(uint256 => address) public projectIdToAdditionalPayee;
mapping(uint256 => uint256) public projectIdToAdditionalPayeePercentage;
mapping(uint256 => uint256) public projectIdToSecondaryMarketRoyaltyPercentage;
mapping(uint256 => string) public staticIpfsImageLink;
mapping(uint256 => uint256) public tokenIdToProjectId;
mapping(uint256 => uint256[]) internal projectIdToTokenIds;
mapping(uint256 => bytes32) public tokenIdToHash;
mapping(bytes32 => uint256) public hashToTokenId;
让我解释一下最后4行。
tokenId
是一个NFT的ID,projectId
是项目的ID。合约记录了这两者之间的双向映射。hash
是 [ 1)NFT的索引,2)区块编号,3)前一个区块的区块哈希,4)矿工的地址,5)随机器合约的随机值] 的组合的keccak256哈希值。我们将在稍后讨论随机器合约。hash
值是在铸币函数中计算的。艺术家可以通过一堆设置函数来改变项目参数,比如这些:
pragma solidity ^ 0.5 .0;
function updateProjectName(
uint256 _projectId,
string memory _projectName) onlyUnlocked(_projectId) onlyArtistOrWhitelisted(_projectId) public {
projects[_projectId].name = _projectName;
}
function updateProjectDescription(
uint256 _projectId,
string memory _projectDescription) onlyArtist(_projectId) public {
projects[_projectId].description = _projectDescription;
}
function toggleProjectIsLocked(uint256 _projectId) public onlyWhitelisted onlyUnlocked(_projectId) {
projects[_projectId].locked = true;
}
但是一旦项目被锁定,许多变量就永远不能被改变。
关于 "存储项目信息"的功能就到此为止。让我们来看看由GenArt721Core.sol
合约实现的下一个功能。
生成艺术图的入口是 "tokenUri "函数。它是ERC-721标准中的一个函数,应该是返回NFT的元数据(如图片或属性)。下面是tokenUri
的实现。
pragma solidity ^ 0.5 .0;
function tokenURI(uint256 _tokenId) external view onlyValidTokenId(_tokenId) returns(string memory) {
// if staticIpfsImageLink is present,
// then return "{projectBaseIpfsURI}/{staticIpfsImageLink}"
if (bytes(staticIpfsImageLink[_tokenId]).length > 0) {
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseIpfsURI,
staticIpfsImageLink[_tokenId]);
}
// if project is not dynamic and useIpfs is true,
// then return "{projectBaseIpfsURI}/{ipfsHash}"
if (!projects[tokenIdToProjectId[_tokenId]].dynamic &&
projects[tokenIdToProjectId[_tokenId]].useIpfs) {
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseIpfsURI,
projects[tokenIdToProjectId[_tokenId]].ipfsHash);
}
// else return "{projectBaseURI}/{_tokenId}"
return Strings.strConcat(
projects[tokenIdToProjectId[_tokenId]].projectBaseURI,
Strings.uint2str(_tokenId));
}
它有很多if条件,但本质上只是有条件地构建元数据路径。项目可以选择将元数据存储在IPFS上(作为图像或JSON文件),或者,如果项目是动态的,元数据可以从传统的HTTP API提供。大多数项目都是动态的,所以我们将专注于这种情况。
例如,Fidenza集合(projectId
=78)有以下元数据路径。
你可以从Etherscan获得这些信息。只要向下翻到到 "tokenURI"。如果我们导航到这个HTTP路径,我们会得到这个JSON文件。
注意,这个JSON文件有一堆不同的特征类型和项目描述的信息。它也有一个指向实际图像的链接。
那么,当你购买NFT时,你真正拥有什么?在此案例中,你只是拥有tokenId
。tokenUri
函数然后将tokenId
映射到IPFS或HTTP链接,取决于项目设置。这个链接要么直接指向图片,要么指向一个有属性的JSON和一个嵌套的图片链接。
但是图像是如何生成/渲染的呢?不幸的是,图片不是在链上生成的。智能合约只存储了一个渲染图片所需的JavaScript脚本。然后,Art Blocks的前端查询这个脚本,并在其传统的后端,而不是区块链后端按需生成图像。
为什么图像不是在链上生成/渲染的?这是因为脚本有库的依赖性。脚本依赖常见的JavaScript库,如p5.js和processing,这些库通常被设计师用来创建生成图像。把这些依赖库放在链上会非常昂贵,这就是为什么图像是在链外生成的。
不过,渲染图像的指令(渲染脚本)是存储在链上的。你可以通过浏览Etherscan上的projectScriptInfo
来检查存储的脚本。这将告诉你项目脚本需要依赖什么库,以及它有多少个脚本(如果脚本太长,它将被分解成许多部分)。
实际的脚本在projectScriptByIndex
中。
脚本以普通字符串的形式存储在项目数据结构中。
你可能想知道NFT集合中的随机模式是如何产生的。在生成图像时,前端并不只是从智能合约中提取脚本。它还获取了哈希字符串。还记得哈希字符串吗?
这个哈希值可以从合约中的tokenIdToHash
映射中读出。在图像生成过程中,该哈希字符串被用作输入/种子。哈希字符串控制着图像的参数(例如,Chromie Squiggle的斜线变得如何)。
大量的信息被组合起来产生哈希值。其中一个输入是挖掘者的地址。这样,挖掘者就参与了图像的生成过程,NFT对矿工来说是独一无二的。(如果其他人在相同的条件下铸造相同的代币,他将得到一个不同的图像,因为他的地址是不同的)。
哈希的另一个输入是 "随机化合约 "的 "返回值"。这个合约似乎不是开源的(没有在Etherscan上验证过),所以我们无法看到它的代码。但它很可能是一个伪随机数生成器,在链上生成随机数,来源包括最后一个铸造的区块高度。
这就是Art Blocks合约的要点! 希望对你有所帮助。
本翻译由 Duet Protocol 赞助支持。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!