Art Blocks合约要点分析 - 利用 JavaScript 动态生成图片

Art Blocks智能合约分解

img

Art Blocks是一个创建链上生成NFT的平台。但是你知道在链上和链下究竟保留了什么吗?为什么他们的智能合约中需要JavaScript?

我们将通过分解Art Blocks的智能合约找到答案。我们还将了解图片是如何生成/渲染的,以及Art Blocks从哪里获得生成图片所需的随机性。

以下是这篇文章的大纲

  • ERC-721的背景 -- NFT标准
  • Art Blocks合约源代码
  • 生成艺术图片

ERC-721--NFT标准

首先,介绍一下Art Blocks的背景。

Art Blocks 是一个平台(实际上只是一个智能合约),在这里你可以创建生成NFT。艺术家提交可以生成图像的脚本。Art Blocks存储这些脚本,当有人想铸造一个NFT时,它会创建一个独特的哈希值。这个哈希值被用作图像生成算法的种子,生成的图像对挖掘者来说是独一无二的。

下面是一些生成图像的例子:

img

流行的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;
}
  • namesymbol是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合约源代码

Art Blocks的区块链后端只包括一个大的智能合约,叫做GenArt721Core.sol。这个智能合约被分解成2块。

  1. 实现ERC-721标准的合约
  2. 主合约GenArt721Core.sol,负责存储渲染NFT所需的数据。

GenArt721Core.sol继承自ERC-721合约。源代码可以在EtherscanGithub找到。

Art Blocks还有两个轻量级合约: GenArt721Minter (铸造代币和接受付款)和 Randomizer (生成伪随机数)。但这些将不会在本文中涉及。

ERC-721的实现

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);
}
  • 虽然,不是ERC-721标准的一部分,但OpenZeppelin的ERC-721实现包括mintburn功能。
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);
}
  • 该实现还有一些映射来存储额外的信息(为简洁起见,这些映射的setter/getter函数将被省略)。
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;
  • 最后,这里是ERC-721的其他函数。
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];
        }
  • ERC-721规范中剩下的一个函数,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值是在铸币函数中计算的。

img

艺术家可以通过一堆设置函数来改变项目参数,比如这些:

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)有以下元数据路径。

img

你可以从Etherscan获得这些信息。只要向下翻到到 "tokenURI"。如果我们导航到这个HTTP路径,我们会得到这个JSON文件。

img

注意,这个JSON文件有一堆不同的特征类型和项目描述的信息。它也有一个指向实际图像的链接。

img

那么,当你购买NFT时,你真正拥有什么?在此案例中,你只是拥有tokenIdtokenUri函数然后将tokenId映射到IPFS或HTTP链接,取决于项目设置。这个链接要么直接指向图片,要么指向一个有属性的JSON和一个嵌套的图片链接。

但是图像是如何生成/渲染的呢?不幸的是,图片不是在链上生成的。智能合约只存储了一个渲染图片所需的JavaScript脚本。然后,Art Blocks的前端查询这个脚本,并在其传统的后端,而不是区块链后端按需生成图像。

为什么图像不是在链上生成/渲染的?这是因为脚本有库的依赖性。脚本依赖常见的JavaScript库,如p5.jsprocessing,这些库通常被设计师用来创建生成图像。把这些依赖库放在链上会非常昂贵,这就是为什么图像是在链外生成的。

不过,渲染图像的指令(渲染脚本)是存储在链上的。你可以通过浏览Etherscan上的projectScriptInfo来检查存储的脚本。这将告诉你项目脚本需要依赖什么库,以及它有多少个脚本(如果脚本太长,它将被分解成许多部分)。

img

实际的脚本在projectScriptByIndex中。

img

脚本以普通字符串的形式存储在项目数据结构中。

img

随机性是如何产生的?

你可能想知道NFT集合中的随机模式是如何产生的。在生成图像时,前端并不只是从智能合约中提取脚本。它还获取了哈希字符串。还记得哈希字符串吗?

img

这个哈希值可以从合约中的tokenIdToHash映射中读出。在图像生成过程中,该哈希字符串被用作输入/种子。哈希字符串控制着图像的参数(例如,Chromie Squiggle的斜线变得如何)。

img

大量的信息被组合起来产生哈希值。其中一个输入是挖掘者的地址。这样,挖掘者就参与了图像的生成过程,NFT对矿工来说是独一无二的。(如果其他人在相同的条件下铸造相同的代币,他将得到一个不同的图像,因为他的地址是不同的)。

哈希的另一个输入是 "随机化合约 "的 "返回值"。这个合约似乎不是开源的(没有在Etherscan上验证过),所以我们无法看到它的代码。但它很可能是一个伪随机数生成器,在链上生成随机数,来源包括最后一个铸造的区块高度。

这就是Art Blocks合约的要点! 希望对你有所帮助。


本翻译由 Duet Protocol 赞助支持。

点赞 2
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
翻译小组
翻译小组
0x9e64...7c84
大家看到好的文章可以在 GitHub 提 Issue: https://github.com/lbc-team/Pioneer/issues 欢迎关注我的 Twitter: https://twitter.com/UpchainDAO