ACS2 - 并行执行标准

ACS2用于提供交易并行执行的信息。

接口

继承了ACS2的合约仅需要实现一个接口:

  • GetResourceInfo

参数为Transaction类型,返回值为定义在acs2.proto中的ResourceInfo类型:

message ResourceInfo {
    repeated aelf.ScopedStatePath paths = 1;
    bool non_parallelizable = 2;
}

其中,aelf.ScopedStatePath定义在aelfcore.proto中:

message ScopedStatePath {
    Address address = 1;
    StatePath path = 2;
}
message StatePath {
    repeated string parts = 1;
}

应用

AElf使用key-value数据库来存储数据,对于合约执行过程中产生的数据,使用一种名为State Path的机制来确定某一条有意义的数据的key。

例如在Token合约的State文件中中定义一个类型为MappedState<Address, string, long>、名为Balances的属性,我们可以通过该属性来访问、修改某一个Address所代表的账户下的某种token的余额。

假设Token合约的地址为Nmjj7noTpMqZ522j76SDsFLhiKkThv1u3d4TxqJMD8v89tWmE,想要知道地址为2EM5uV6bSJh6xJfZTUa1pZpYsYcCUAdPvZvFUJzMDJEx3rbioz的账户的ELF余额,可以直接使用这个key来访问redis/ssdb:

Nmjj7noTpMqZ522j76SDsFLhiKkThv1u3d4TxqJMD8v89tWmE/Balances/2EM5uV6bSJh6xJfZTUa1pZpYsYcCUAdPvZvFUJzMDJEx3rbioz/ELF

而在AElf中,交易并行执行的实现也是基于以上key的组织逻辑,开发者需要提供某个方法可能访问到的StatePath,那么这个方法对应的交易在执行之前,会得到适当的分组:如果两个方法不会访问同样的StatePath,那么就可以放心地放在不同的组中。

注意,如果该方法提供的StatePath与实际执行过程使用到的StatePath不一致,会在实际执行的过程中取消并行,并且合约会被标记为不能并行。

如果对相关的逻辑感兴趣,可以查看AElf代码中ITransactionGrouper这个接口的实现,以及IParallelTransactionExecutingService接口的实现。

接口实现

一个例子:Token合约中的Transfer方法,其核心逻辑为修改地址A和地址B的ELF的余额,会访问两次上面提到的Balances属性。

此时,我们就需要把地址A和地址B的ELF的余额的key通过GetResourceInfo方法通知ITransactionGrouper:

var args = TransferInput.Parser.ParseFrom(txn.Params);
var resourceInfo = new ResourceInfo
{
    Paths =
    {
        GetPath(nameof(TokenContractState.Balances), txn.From.ToString(), args.Symbol),
        GetPath(nameof(TokenContractState.Balances), args.To.ToString(), args.Symbol),
    }
};
return resourceInfo;

其中,GetPath就是把组成key的几段数据构成一个ScopedStatePath类型:

private ScopedStatePath GetPath(params string[] parts)
{
    return new ScopedStatePath
    {
        Address = Context.Self,
        Path = new StatePath
        {
            Parts =
            {
                parts
            }
        }
    }
}

测试

可以自行构造两个交易,直接将交易ITransactionGrouper的实现实例的GroupAsync方法,来得知这两个交易能否并行。

我们准备两个实现了ACS2的合约的Stub,它们的地址不同,即模拟不同的地址做Transfer:

var keyPair1 = SampleECKeyPairs.KeyPairs[0];
var acs2DemoContractStub1 = GetACS2DemoContractStub(keyPair1);
var keyPair2 = SampleECKeyPairs.KeyPairs[1];
var acs2DemoContractStub2 = GetACS2DemoContractStub(keyPair2);

然后从Application中取出测试所需要的一些服务和数据:

var transactionGrouper = Application.ServiceProvider.GetRequiredService<ITransactionGrouper>();
var blockchainService = Application.ServiceProvider.GetRequiredService<IBlockchainService>();
var chain = await blockchainService.GetChainAsync();

最后通过transactionGrouper做检验:

// Situation can be parallel executed.
{
    var groupedTransactions = await transactionGrouper.GroupAsync(new ChainContext
    {
        BlockHash = chain.BestChainHash,
        BlockHeight = chain.BestChainHeight
    }, new List<Transaction>
    {
        acs2DemoContractStub1.TransferCredits.GetTransaction(new TransferCreditsInput
        {
            To = Address.FromPublicKey(SampleECKeyPairs.KeyPairs[2].PublicKey),
            Symbol = "ELF",
            Amount = 1
        }),
        acs2DemoContractStub2.TransferCredits.GetTransaction(new TransferCreditsInput
        {
            To = Address.FromPublicKey(SampleECKeyPairs.KeyPairs[3].PublicKey),
            Symbol = "ELF",
            Amount = 1
        }),
    });
    groupedTransactions.Parallelizables.Count.ShouldBe(2);
}
// Situation cannot.
{
    var groupedTransactions = await transactionGrouper.GroupAsync(new ChainContext
    {
        BlockHash = chain.BestChainHash,
        BlockHeight = chain.BestChainHeight
    }, new List<Transaction>
    {
        acs2DemoContractStub1.TransferCredits.GetTransaction(new TransferCreditsInput
        {
            To = Address.FromPublicKey(SampleECKeyPairs.KeyPairs[2].PublicKey),
            Symbol = "ELF",
            Amount = 1
        }),
        acs2DemoContractStub2.TransferCredits.GetTransaction(new TransferCreditsInput
        {
            To = Address.FromPublicKey(SampleECKeyPairs.KeyPairs[2].PublicKey),
            Symbol = "ELF",
            Amount = 1
        }),
    });
    groupedTransactions.Parallelizables.Count.ShouldBe(1);
}

示例

可以参考MultiToken合约对GetResourceInfo的实现,注意其中对于Tranfer方法提供的Resource Info,除了本文提到的两个key,还需要考虑收取交易费。