This is the old Algorand Developer Portal. Please head over to dev.algorand.co to explore our newly rebuilt documentation site. Please excuse us as we continue to transition content to the new portal

创建文章

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Tutorial Thumbnail
入门 · 小于15分钟

使用.net进行Algorand开发系列教程之智能合约相关操作

本文会主要讲解使用dotnet-algorand-sdk进行智能合约的编译,及无状态智能合约的使用,最后会介绍一下智能合约的离线调试功能。

需要的工具和环境

本文是系列教程的第五篇,如果您没有任何.net进行Algorand开发的经验,请先阅读本系列教程(一)教程(二)

背景

前一段时间dotnet-algorand-sdk正式升级到了0.2,也支持了Algorand的最新API 2.0。所以在之前的系列教程的基础上,再出几期新的教程。介绍一些dotnet-algorand-sdk的新变化,也介绍一下Algorand API 2.0的一些新变化。

与前面几期教程不同的是,现在的教程都是基于最新的dotnet-algorand-sdk进行的,大家可以放心更新dotnet-algorand-sdk的版本啦。

大家看algorand的官方页面,Algorand最自豪的三个特点分别是智能合约(Smart Contracts)、标准资产(ASA)及原子交易(Atomic Transfers)。关于标准资产的相关操作已经在本系列教程的《3-使用.net进行Algorand开发系列教程之ASA的创建及管理》和《4-使用.net进行Algorand开发系列教程之ASA转账》中进行了详细的说明,本系列的后续主要说明如何使用dotnet进行智能合约及原子交易及如何使用Indexer的相关操作。

image-20210209194639311

图1 Algorand官方网站对Algorand介绍截图

Algorand的智能合约功能被集成到Layer-1中,所以一般被缩写为ASC1。ASC1与Algorand平台一样具有速度快,可扩展性高,不可篡改及很高的安全性,且性价比非常高。 ASC1以一种称为事务执行批准语言(TEAL)的新语言编写。

本文当然不会讲解任何关于TEAL语法的内容。但是当你用TEAL完成智能合约后,dotnet-algorand-sdk就大有可为了。本文会主要讲解使用dotnet-algorand-sdk进行智能合约的编译,及无状态智能合约的使用,最后会介绍一下智能合约的离线调试功能。

步骤

1. 智能合约的编译

智能合约编译是指将TEAL编写的内容编译成Algorand可以识别的二进制的代码的过程。只要完成了智能合约的编写,此过程可以说是相当简单,请使用如下代码即可:

public static void Main(string[] args)
{
    string ALGOD_API_ADDR = args[0];
    if (ALGOD_API_ADDR.IndexOf("//") == -1)
    {
        ALGOD_API_ADDR = "http://" + ALGOD_API_ADDR;
    }

    string ALGOD_API_TOKEN = args[1];            

    AlgodApi algodApiInstance = new AlgodApi(ALGOD_API_ADDR, ALGOD_API_TOKEN);
    // read file - int 1
    byte[] data = File.ReadAllBytes("V2\\contract\\sample.teal");
    var response = algodApiInstance.TealCompile(data);

    Console.WriteLine("response: " + response);
    Console.WriteLine("Hash: " + response.Hash);
    Console.WriteLine("Result: " + response.Result);
    Console.ReadKey();
    //result
    //Hash: 6Z3C3LDVWGMX23BMSYMANACQOSINPFIRF77H7N3AWJZYV6OH6GWTJKVMXY
    //Result: ASABASI=
}

为了使代码更具有代表性,我们使用了从sample.teal文件读取源码。实际上在本例中sample.teal只是普通的文本文件,其内容只有一行:

int 1

这个智能合约非常简单,作用是批准所有内容。这个合约我们会在本教程中反复使用。当然,sample.teal可以是任何TEAL程序。

后面我们所有的操作都是基本编译后的程序的,为了简单其间,后面的程序都不再进行编译这一操作,直接使用编译后的合约。

2. 无状态合约的使用

在进一步深入之前,我们需要更深一步的讨论Algorand的智能合约。相比Ethereum的智能合约,Algorand的智能合约功能更多,也更加强大。

Algorand官方文档https://developer.algorand.org/zh-hans/docs/features/asc1/将智能合约分为无状态合约(Stateless Smart Contracts)和状态合约(Stateful Smart Contracts)。状态合约比较类似与Ethereum中的合约,一般需要向区块链上存储一定的变量以跟踪某一状态的变化。而无状态合约合约设计的就十分精妙,其不需要在区块链上存储任何东西,却能实现非常强大的功能。

Algorand中的所有交易必须由一个帐户或多签名帐户对进行签名。而无状态智能合约,使一定的逻辑也具备签名的能力。也就是说一个交易只要达到某种条件,就可以得到该逻辑签名(LogicSignature)的确认。

无状态智能合约可以进一步分为两种主要使用方式:逻辑账户(也叫合约账号,Contract Account)和逻辑签名(也叫签名委托,signature delegation)。

逻辑账户的生成及使用

逻辑账户指的是一个由逻辑程序控制的账户。这种账户和普通账户一样可以存储 ALGO 和 ASA 资产,但不一样的是,从逻辑账户发出的交易并不是由个人帐户的签名来批准的,而是基于该账户对应的逻辑程序,由区块链来自行判断的。

开发者可以通过编写一段逻辑程序,并对其进行编译来创建一个逻辑账户,该账户会包含一个逻辑账户的地址和一个对应的逻辑账户签名。用户可以向该逻辑账户地址存入代币,由逻辑程序来控制如何能够从中取出代币,如下图所示:

image-20210209194658605

图2 逻辑账户交易流程

那么我们如何创建一个合约账号呢?具体创建和使用逻辑账户的流程如下:

  • 使用 TEAL 语言编写脚本实现逻辑
  • 使用工具对脚本进行编译,可以得到一个逻辑账户地址和逻辑签名
  • 从个人帐户或多签账户向该逻辑账户地址充值
  • 若是想从该账户中转出一些资产,则需要构造一笔使用该逻辑账户地址作为发送方的交易,并使用逻辑签名进行签名
  • 最后再将交易发送上链,区块链就会依据事先编写的脚本逻辑来判断这笔交易的合法性。

下面的示例中仍旧使用前文所提到的最简单的智能合约,不过这里不再赘述其编译过程,直接使用编译后代码(BASE64编码):“ASABASI=”

public static void Main(params string[] args)
{
    string ALGOD_API_ADDR = args[0];
    if (ALGOD_API_ADDR.IndexOf("//") == -1)
    {
        ALGOD_API_ADDR = "http://" + ALGOD_API_ADDR;
    }

    string ALGOD_API_TOKEN = args[1];
    //string toAddressMnemonic = "typical permit hurdle hat song detail cattle merge oxygen crowd arctic cargo smooth fly rice vacuum lounge yard frown predict west wife latin absent cup";
    var toAddress = new Address("PVT67ZSBADU5ATXRIYBRIDBWSOIJOJJR73FJPCUFSKPHXI4M7PIRS5SRRI");
    var algodApiInstance = new AlgodApi(ALGOD_API_ADDR, ALGOD_API_TOKEN);
    Algorand.V2.Model.TransactionParametersResponse transParams;
    try
    {
        transParams = algodApiInstance.TransactionParams();
    }
    catch (ApiException e)
    {
        throw new Exception("Could not get params", e);
    }
    // format and send logic sig
    byte[] program = Convert.FromBase64String("ASABASI=");
    LogicsigSignature lsig = new LogicsigSignature(program, null);
    Console.WriteLine("Escrow address: " + lsig.Address.ToString());

    var tx = Utils.GetPaymentTransaction(lsig.Address, toAddress, 100000, "draw algo from contract", transParams);

    if (!lsig.Verify(tx.sender))
    {
        string msg = "Verification failed";
        Console.WriteLine(msg);
    }
    else
    {
        try
        {
            //签名操作
            SignedTransaction signedTx = Account.SignLogicsigTransaction(lsig, tx);
            var id = Utils.SubmitTransaction(algodApiInstance, signedTx);
            Console.WriteLine("Successfully sent tx logic sig tx id: " + id);
        }
        catch (ApiException e)
        {
            // This is generally expected, but should give us an informative error message.
            Console.WriteLine("Exception when calling algod#rawTransaction: " + e.Message);
        }
    }
    Console.WriteLine("You have successefully arrived the end of this test, please press and key to exist.");
}

在上面的代码中lsig完全像一个普通账号一样,向地址PVT67ZSBADU5ATXRIYBRIDBWSOIJOJJR73FJPCUFSKPHXI4M7PIRS5SRRI 进行了一笔转账交易。实际上这个转账需要满足智能合约中的逻辑。

逻辑签名的生成及使用

逻辑签名指的是一个由逻辑程序来对交易进行校验的签名。该签名与普通账户的私钥签名类似,可以用来授权交易的执行。但不同的是,普通账户的签名可以用来授权从其对应的账户中发出的所有交易;而逻辑签名则是基于逻辑来判断交易的有效性,只有当逻辑正确时,才能批准交易。

开发者可以通过编写一段逻辑程序,并使用私钥对该程序签名来生成一个对应的逻辑签名,然后将该签名公布出来,从而使得其他人可以自行使用逻辑签名来发送从该账户发出的交易。用户可以通过逻辑签名来授权交易,由逻辑程序来对交易进行授权,如下图所示:

image-20210209194712361

图3 逻辑签名使用流程

逻辑签名的使用可以参照以下代码,代码中对部分逻辑进行了注释说明:

public static void Main(params string[] args)
{
    string ALGOD_API_ADDR = args[0];
    if (ALGOD_API_ADDR.IndexOf("//") == -1)
    {
        ALGOD_API_ADDR = "http://" + ALGOD_API_ADDR;
    }
    string ALGOD_API_TOKEN = args[1];
    //第一个账号用于给智能合约签名,并把签名发布出去
    string SRC_ACCOUNT = "typical permit hurdle hat song detail cattle merge oxygen crowd arctic cargo smooth fly rice vacuum lounge yard frown predict west wife latin absent cup";
    Account acct1 = new Account(SRC_ACCOUNT);            
    byte[] program = Convert.FromBase64String("ASABASI=");

    LogicsigSignature lsig = new LogicsigSignature(program, null);            

    // sign the logic signaure with an account sk
    // 这里操作的意义是账号1批准逻辑签名可以操纵我的账号
    acct1.SignLogicsig(lsig);
    var contractSig = Convert.ToBase64String(lsig.sig.Bytes);
    var acct1Address = acct1.Address.ToString();

    //第二步,另一个账号,只用contractSig就可以对acct1中进行符合智能合约逻辑的操作
    //注:此例中是全部返回1,也就是说任何操作,这在实际生产中是非常危险的操作,请注意
    //注:也需要用到acct1的地址(公钥)

    //实际上,在本例中并不需要acct2的私钥,只要有公钥就足够了
    //string acct2_mnemonic = "place blouse sad pigeon wing warrior wild script"
    //                   + " problem team blouse camp soldier breeze twist mother"
    //                   + " vanish public glass code arrow execute convince ability"
    //                   + " there";
    //Account acct2 = new Account(acct2_mnemonic);
    var acct2Address = "AJNNFQN7DSR7QEY766V7JDG35OPM53ZSNF7CU264AWOOUGSZBMLMSKCRIU";

    //为了表示与账号1全部脱离,所以新建一个LogicsigSignature
    LogicsigSignature lsig2 = new LogicsigSignature(program, null, Convert.FromBase64String(contractSig));
    var algodApiInstance = new AlgodApi(ALGOD_API_ADDR, ALGOD_API_TOKEN);
    Algorand.V2.Model.TransactionParametersResponse transParams;
    try
    {
        transParams = algodApiInstance.TransactionParams();
    }
    catch (ApiException e)
    {
        throw new Exception("Could not get params", e);
    }

    Transaction tx = Utils.GetPaymentTransaction(new Address(acct1Address), new Address(acct2Address), 1000000, 
        "draw algo with logic signature", transParams);            

    try
    {
        //bypass verify for non-lsig
        SignedTransaction signedTx = Account.SignLogicsigTransaction(lsig2, tx);

        var id = Utils.SubmitTransaction(algodApiInstance, signedTx);
        Console.WriteLine("Successfully sent tx logic sig tx id: " + id);
    }
    catch (ApiException e)
    {
        // This is generally expected, but should give us an informative error message.
        Console.WriteLine("Exception when calling algod#rawTransaction: " + e.Message);
    }

    Console.WriteLine("You have successefully arrived the end of this test, please press and key to exist.");
    Console.ReadKey();
}

image-20210209194724895

图4 示例代码的交易逻辑

图4和上图3的逻辑基本是一致的,只不过针对示例代码对逻辑签名的交易过程进行了细化。细心的用户可能会发现,其实此部分的示例代码是将两个人的操作统合到同一段代码中去了,所以读者看着操作逻辑可能会有点混乱。

我们可以把关于acct1作为一个角色,他的操作结果是得到了contractSig,然后把contractSig以其他方式(如邮件,即时聊天软件等)传送给了另一个角色(不必要是acct2),由另一个角色进行后续操作。这样你就能更清晰的这段代码的逻辑了。

逻辑账户与逻辑签名的异同

经过上面的实践,我们会发现逻辑账号和逻辑签名还有非常大的不同的

逻辑账户是一个第三方的账户,该账户有地址,但并不存在对应的公私钥对,从该账户发送交易的唯一办法是是经过其对应的逻辑程序的验证;

逻辑签名是使用某一个普通账户(由某个公私钥对控制)的私钥对一个逻辑程序进行签名而生成的,该签名可以用来授权该账户发出的交易,除此以外该账户自己的公私钥对依然可以授权交易。

换言之,逻辑账号其实是逻辑操纵自身的过程。而逻辑签名是逻辑操纵其他账号的过程。逻辑签名有点像Ethereum上ERC20代币的approve功能,也就是能把自身的一些权限(智能合约编写的权限)授权给持有逻辑签名的人。虽然我们上面的代码实现了非常相似的功能,但其内存逻辑还是有很大不同的。在实际使用时也会有很大差异。

离线调试(dryrun debug)

相信如果你进行过智能合约的开发,都会有非常难以调试的感觉。因为每次测试都需要把智能合约发布到区块链上,再进行调用等。如果你在主网进行相关开发,这个费用是非常非常高的。为了解决这个问题,大多数区块链的做法是部署测试网。在测试网上进行调试费用相对就会低一些。当然algorand也提供了测试网。但algorand在测试网的基础上,又提出了另外一种解决方案。那种就是网络上试运行一下,先看看结果。这种调试方法特别适合于无状态的合约,如上面所说的逻辑账号或者逻辑合约。

我们还是先看一下如果使用代码进行离线调试:

public static void Main(params string[] args)
{
    string ALGOD_API_ADDR = args[0];
    if (ALGOD_API_ADDR.IndexOf("//") == -1)
    {
        ALGOD_API_ADDR = "http://" + ALGOD_API_ADDR;
    }
    string ALGOD_API_TOKEN = args[1];
    //第一个账号用于给智能合约签名,并把签名发布出去
    string SRC_ACCOUNT = "buzz genre work meat fame favorite rookie stay tennis demand panic busy hedgehog snow morning acquire ball grain grape member blur armor foil ability seminar";
    Account acct1 = new Account(SRC_ACCOUNT);
    var acct2Address = "QUDVUXBX4Q3Y2H5K2AG3QWEOMY374WO62YNJFFGUTMOJ7FB74CMBKY6LPQ";

    //byte[] source = File.ReadAllBytes("V2\\contract\\sample.teal");
    byte[] program = Convert.FromBase64String("ASABASI=");

    LogicsigSignature lsig = new LogicsigSignature(program, null);

    // sign the logic signaure with an account sk
    acct1.SignLogicsig(lsig);

    var algodApiInstance = new AlgodApi(ALGOD_API_ADDR, ALGOD_API_TOKEN);
    Algorand.V2.Model.TransactionParametersResponse transParams;
    try
    {
        transParams = algodApiInstance.TransactionParams();
    }
    catch (ApiException e)
    {
        throw new Exception("Could not get params", e);
    }

    Transaction tx = Utils.GetPaymentTransaction(acct1.Address, new Address(acct2Address), 1000000,
        "tx using in dryrun", transParams);

    try
    {
        //bypass verify for non-lsig
        SignedTransaction signedTx = Account.SignLogicsigTransaction(lsig, tx);
        //一切准备就绪,本可以直接发送到网络,也可使得Dryrun的方法来进行调试
        //var id = Utils.SubmitTransaction(algodApiInstance, signedTx);
        //Console.WriteLine("Successfully sent tx logic sig tx id: " + id);
        // dryrun source
        //var dryrunResponse = Utils.GetDryrunResponse(algodApiInstance, signedTx, source);                
        //Console.WriteLine("Dryrun compiled repsonse : " + dryrunResponse.ToJson()); // pretty print

        // dryrun logic sig transaction
        var dryrunResponse2 = Utils.GetDryrunResponse(algodApiInstance, signedTx);                
        Console.WriteLine("Dryrun source repsonse : " + dryrunResponse2.ToJson()); // pretty print
    }
    catch (ApiException e)
    {
        // This is generally expected, but should give us an informative error message.
        Console.WriteLine("Exception when calling algod#rawTransaction: " + e.Message);
    }

    Console.WriteLine("You have successefully arrived the end of this test, please press and key to exist.");
}

需要注意的是,一个节点需要进行以下设置才能支持离线调试:

$ algocfg set -p EnableDeveloperAPI -v true 
$ goal node restart

另外,purestake也是不支持离线调试的,也就是说如果你用purestake作为接入点,连接testnet,以上代码是没有办法正常运行的。可能Purestake不支持该项功能(但我没有在Purestake的任何文档中发现他有对此加以说明,如果读者发现相关的内容,请和我说一下)。所以运行此部分内容需要运行一个独立节点。鉴于运行独立节点可能需要一点时间,所以将程序运行结果展示如下:

image-20210209194743533

图5 离线调试运行结果

虽然向看似发送离线调试和发送普通的交易没有什么区别,但如果你从Algorand的浏览器上查看离线调试交易你就会发现有很大不同。浏览器并不会记录你的离线调试记录,也就是说其实你的交易并没有真正发送到网络,只是在网络环境下“试运行”了一个,看看能得到一个什么结果。如下图展示了我在运行离线调试后相应账号的状态,并没有产生新的交易。

image-20210209194754366

图6 离线调试后相应账号状态

其他的区块链项目是没有此功能的。如果需要一点类比,离线调试像生活中的预审功能。提交材料前先预审一遍,看能不能被接受,如果不能被接受,我们可以选择不提交项目,也可以选择修改材料后再提交。

结语

到这里,用c#进行智能合约交互的内容就基本讲完了。其实大家可以发现,Algorand的智能合约设计在有超越Ethereum之处的。但对于我来说(可能也是对于很多开发者来说),其不便之处在于其使用了一套全新的语言TEAL。TEAL语言的语法和使用逻辑完全不同于一般的面向对象的语言,所以入门的难度就会相对高一点。虽然这不能说明TEAL不如solidity,但现在绝大多数的开发者都有面向对象的开发经验,入门solidity的难度要低于入门TEAL的。另一方面,得益于Ethereum的盛行,现在多数区块链开发者都是有使用过solidity的经历的。但TEAL,相比solidity还有很长的一段路要走。虽然也由社区开发了pyTeal这样的语法类似于python的工具,但对一个新进入Algorand的开发者来说,学习曲线还是比较陡峭。

后期如果有时间,我也会更新一点pyTeal或者TEAL的教程,到时也希望大家捧场。最后希望大家能用Algorand的智能合约开发出自己理想的Dapp。