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

2022-11-07 11:05:35 浏览数 (1)

译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]

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

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

以下是这篇文章的大纲

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

ERC-721--NFT 标准

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

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

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

流行的 Art Blocks 集合: Ringers, Chromie Squiggle, Fidenza.

为了理解 Art Blocks 智能合约,我们首先需要了解 ERC-721。ERC-721 是一个用于实现 NFT 智能合约的标准。为了兼容ERC-721[4],一个合约需要实现这些功能:

代码语言:javascript复制
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 智能合约分解[5],了解更多关于 ERC-721 标准。

现在让我们来了解一下 Art Blocks 是如何实现这个标准并创建生成 NFT 的。

Art Blocks 合约源代码

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

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

GenArt721Core.sol继承自 ERC-721 合约。源代码可以在Etherscan[6]和Github[7]找到。

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

ERC-721 的实现

Art Blocks 使用一个现成的OpenZeppelin 的实现[8]来实现 ERC-721 接口。OpenZeppelin 是一个最常用标准的实现库。

  • 使用映射来管理代币的所有权。
代码语言:javascript复制
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;
}
  • 所有权的转移:
代码语言:javascript复制
pragma solidity ^ 0.5 .0;

function transferFrom(address from, address to, uint256 tokenId) public {
  // ...
  _ownedTokensCount[from].decrement();
  _ownedTokensCount[to].increment();
  _tokenOwner[tokenId] = to;
  // ...
}
  • 以及如何管理授权:
代码语言:javascript复制
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功能。
代码语言:javascript复制
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 函数将被省略)。
代码语言:javascript复制
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 的其他函数。
代码语言:javascript复制
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 等)。主合约定义了一个项目的数据结构。

代码语言:javascript复制
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,...)。

代码语言:javascript复制
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);
}

从上面的截图中你可能已经注意到,合约还使用了一些数据结构来跟踪所有的东西。

代码语言:javascript复制
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值是在铸币函数中计算的。

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

代码语言:javascript复制
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的实现。

代码语言:javascript复制
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[9]获得这些信息。只要向下翻到到 "tokenURI"。如果我们导航到这个 HTTP 路径,我们会得到这个 JSON 文件。

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

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

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

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

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

实际的脚本在projectScriptByIndex中。

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

随机性是如何产生的?

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

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

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

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

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


本翻译由 Duet Protocol[11] 赞助支持。

原文:https://betterprogramming.pub/why-art-blocks-uses-javascript-in-its-smart-contract-e252ceb4cf93

参考资料

[1]

登链翻译计划: https://github.com/lbc-team/Pioneer

[2]

翻译小组: https://learnblockchain.cn/people/412

[3]

Tiny 熊: https://learnblockchain.cn/people/15

[4]

ERC-721: https://eips.ethereum.org/EIPS/eip-721

[5]

BoredApeYachtClub智能合约分解: https://ilamanov.medium.com/bored-ape-yacht-club-smart-contract-breakdown-6c254c774394

[6]

Etherscan: https://etherscan.io/address/0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270#code

[7]

Github: https://gist.github.com/ilamanov/e4241e5b8afd0cb2341c544363899f8b

[8]

OpenZeppelin的实现: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC721/ERC721.sol

[9]

Etherscan: https://etherscan.io/address/0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270#readContract

[10]

Etherscan: https://etherscan.io/address/0xa7d8d9ef8d8ce8992df33d8b8cf4aebabd5bd270#readContract

[11]

Duet Protocol: https://duet.finance/?utm_souce=learnblockchain

0 人点赞