创建文章

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。