AElf随机数合约标准(ACS6)

本文主要讨论区块链系统中随机数的常见方案,AElf中对于可提供随机数的智能合约提供的标准接口,以及AEDPoS合约对ACS6的实现。算法

关于ACS的说明可见这篇文章的开头。segmentfault

区块链和随机数

区块链系统中,与合约相关的随机数应用大体有几种场景:抽奖、验证码、密码相关等。缓存

而因为区块链本质上是一个分布式系统,他要求各个节点的运算结果是可验证的,传统的随机数生成结果在不一样机器上基本不会一致,让全部的节点产生一样的随机数又不形成过多的延时是不可能的。安全

好在区块链系统中生成一个可用的随机数,咱们已知有几种方案。数据结构

中心化生成随机数。随机数由可信的第三方提供,如RANDOM.ORG
Commitment方案,或者hash-commit-reveal方案。若是读者有读过AElf白皮书会发现AElf主链共识用于肯定每一轮区块生产者肯定生产顺序的in_value和out_value便采用了这种方案:区块生产者在本地生成一个随机的哈希值in_value后,先公布承诺(即out_value),其中out_value = hash(in_value),到了合适的时机再公布随机哈希值in_value,其余节点只须要验证out_value == hash(in_value)就能够了。这里的in_value能够认为是一个随机数。
采集区块链状态信息做为种子,在合约中生成随机数。万一被人知道了随机数的生成算法(智能合约的代码是公开的),再获取到正确的种子,这个方案生成的随机数就能够成功预测的。不敢相信还真有人用这种方式。框架

显然,站在去中心化的角度上考量,Commitment方案至少是一个可用的方案,只须要保证做出承诺(commitment)的人不会本身偷偷提早公开随机数,或者本身利用随机数做弊便可。dom

然而很不幸,其实在区块链系统中,这是没法保证的:咱们没法保证生成随机数的人不会利用信息不对等来作出不公平的事情,好比当这个随机数被做为某次赌局开奖的依据时,随机数的生成者哪怕在赌局开始以前就作出了承诺,依然能够选择性地停止公开这个随机数——这样至关于他获得了“再玩一次”的机会,由于若是他不公开这个随机数,要么赌局会选择其余人公开的随机数,要么这个赌局会做废。async

若是预防随机数生产者的选择停止攻击呢?有一系列成熟的方案,参看Secret Sharing。分布式

简单解释一下:如今有五我的A~E,每人掌握一个公私钥对,此时A产生了一个随机数Random,生成对应的承诺Commitment,同时将随机数Random与B、C、D、E的公钥进行加密获得四个SharingPart,加密SharingPart时便保证只须要凑够B~E中两我的就能够恢复Random,将SharingPart和Commitment一块儿公开。这样哪怕他本身因故没有公开Random的值,B~E中任意两我的用本身的私钥分别对本身收到的SharingPart解密,凑齐两个解密后的数值(要按A加密出SharingPart的顺序),即可以恢复出Random。而万一两个SharingPart没能恢复出Random,只能认为A从一开始就决定做恶——这时候只须要在区块链经济系统的设计中,让A付出代价就好了。好比直接扣除A申请称为随机数生产者缴纳的保证金。(TODO: 画图)ide

此外,咱们还能够选择不依赖某一个个体产生的随机数,而是选择多个个体的随机数进一步计算哈希值做为应用场景中的可用随机数。如此咱们能够比较稳定、安全地在区块链系统中获得随机数。

ACS6 - Random Number Provider Contract Standard

我在以前的一篇文章中解释了AElf区块链关于共识的合约标准,其实是做为AElf主链开发者对合约开发者实现共识机制时推荐实现的接口。而关于随机数生成,咱们制定了ACS6,做为对任何提供随机数的合约推荐要实现的接口。

不出意外地,ACS6是选择对Commitment方案进行抽象,获得的合约标准。

支持使用ACS6的场景以下:

用户对实现了ACS6的合约申请一个随机数,相似于发送了一个定单;
实现了ACS6的合约给用户返回一些信息,这些信息包括用户能够在哪一个区块高度(H)获取获得一个随机数,以及用户获取随机数可用的凭据T(也是一个哈希值);
等待区块链高度到达指定高度H后,用户发送交易尝试获取随机数,凭据T须要做为该交易的参数;
实现了ACS6的合约根据凭据T返回一个随机数。
若是用户尝试在高度H以前获取随机数,本次获取随机数的交易会执行失败并抛出一个AssertionException,提示高度还没到。

基于以上场景,咱们设计的ACS6以下:

service RandomNumberProviderContract {

rpc RequestRandomNumber (google.protobuf.Empty) returns (RandomNumberOrder) {
}
rpc GetRandomNumber (aelf.Hash) returns (aelf.Hash) {
}

}

message RandomNumberOrder {

sint64 block_height = 1;// Orderer can get a random number after this height.
aelf.Hash token_hash = 2;

}
用户发送RequestRandomNumber交易来申请一个随机数,合约须要为本次请求生成一个凭据(token_hash),而后把该凭据和用户可以获取该随机数的区块高度一块儿返回给用户。高度达到之后,用户利用收到的凭据(token_hash)发送GetRandomNumber交易便可获得一个可用的随机数。做为合约,在实现该方法的时候应该缓存为用户生成的凭据,做为一个Map的key,这个Map的value则应该根据合约本身对随机数的实现自行定义数据结构。

好比,AEDPoS合约在实现ACS6的时候,能够将该Map的value定义为:

message RandomNumberRequestInformation {

sint64 round_number = 1;
sint64 order = 2;
sint64 expected_block_height = 3;

}
其中round_number指示为了生成该用户申请的随机数,应该使用哪一轮(及以后)各个CDC公布的previous_in_value值;order为这个用户申请随机数的RandomNumberProviderContract交易被该轮第几个CDC打包(因此须要使用该轮该次序之后公布的previous_in_value做为随机数生成的“原材料”);expected_block_height则是要告知给用户的须要等待到的区块高度。

AEDPoS合约对ACS6的一个实现

因为AEDPoS共识自己的推动过程当中就采用了hash-commit-reveal的方式,能够直接使用每一个CDC的产生普通区块(区别于额外区块)时公布的previous_in_value来做为生成随机数的原材料。新公布的previous_in_value的验证(即验证hash(previous_in_value) == previous_out_value)发生于任何节点执行新的区块以前,只要该区块被成功同步上best chain,就无需担忧已经公布的previous_in_value存在弄虚做假的行为。

注意,本节的实现只是在刚刚定义好ACS6后的一个尝试,以后随时可能会有大幅度改动。

惟一要担忧的是CDC共谋。所以AEDPoS在实现随机数生成的时候,不会仅仅采用一个CDC的previous_in_value来生成随机数,而是设置了五个:

public static class AEDPoSContractConstants
{

...
public const int RandomNumberRequestMinersCount = 5;

}
当一个用户向AEDPoS请求随机数时,他只能等到当前轮的最后一个时间槽(额外区块时间槽),才能够百分之百保证本轮可公布的previous_in_value都已经公布了。由于可能存在本轮刚刚加入(也许是刚解决完分叉)的CDC,他没有previous_in_value可公布;也可能存在一些CDC因为本地缓存问题,未能公布previous_in_value,而到了额外区块的时间槽时,他的previous_in_value随着secret sharing的reveal阶段完成被恢复——若是咱们容许用户在reveal阶段以前发送GetRandomNumber交易获取随机数,那么在reveal阶段以前和以后获取到的随机数可能会不一致,这会很使人困扰。

/// <summary>
/// In AEDPoS, we calculate next several continual previous_in_values to provide random hash.
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public override RandomNumberOrder RequestRandomNumber(Empty input)
{

var tokenHash = Context.TransactionId;
if (TryToGetCurrentRoundInformation(out var currentRound))
{
    var lastMinedBlockMinerInformation = currentRound.RealTimeMinersInformation.Values.OrderBy(i => i.Order)
        .LastOrDefault(i => i.OutValue != null);

    var lastMinedBlockSlotOrder = lastMinedBlockMinerInformation?.Order ?? 0;

    var minersCount = currentRound.RealTimeMinersInformation.Count;
    // At most need to wait one round.
    var waitingBlocks = minersCount.Sub(lastMinedBlockSlotOrder).Add(1).Mul(AEDPoSContractConstants.TinyBlocksNumber);
    var expectedBlockHeight = Context.CurrentHeight.Add(waitingBlocks);
    State.RandomNumberInformationMap[tokenHash] = new RandomNumberRequestInformation
    {
        RoundNumber = currentRound.RoundNumber,
        Order = lastMinedBlockSlotOrder,
        ExpectedBlockHeight = expectedBlockHeight
    };
    return new RandomNumberOrder
    {
        BlockHeight = expectedBlockHeight,
        TokenHash = tokenHash
    };
}

Assert(false, "Failed to get current round information");

// Won't reach here.
return new RandomNumberOrder
{
    BlockHeight = long.MaxValue
};

}
用户使用凭据试图获取随机数时,AEDPoS会收集用户申请随机数后五个由CDC公布的previous_in_value,用这五个哈希值计算出来一个哈希值,做为一个可用的随机数返回给用户。

public override Hash GetRandomNumber(Hash input)
{

var roundNumberRequestInformation = State.RandomNumberInformationMap[input];
if (roundNumberRequestInformation == null)
{
    Assert(false, "Random number token not found.");
    // Won't reach here.
    return Hash.Empty;
}

if (roundNumberRequestInformation.ExpectedBlockHeight > Context.CurrentHeight)
{
    Assert(false, "Still preparing random number.");
}

var targetRoundNumber = roundNumberRequestInformation.RoundNumber;
if (TryToGetRoundInformation(targetRoundNumber, out var targetRound))
{
    var neededParticipatorCount = Math.Min(AEDPoSContractConstants.RandomNumberRequestMinersCount,
        targetRound.RealTimeMinersInformation.Count);
    var participators = targetRound.RealTimeMinersInformation.Values.Where(i =>
        i.Order > roundNumberRequestInformation.Order && i.PreviousInValue != null).ToList();
    var roundNumber = targetRoundNumber;
    TryToGetRoundNumber(out var currentRoundNumber);
    while (participators.Count < neededParticipatorCount && roundNumber <= currentRoundNumber)
    {
        roundNumber++;
        if (TryToGetRoundInformation(roundNumber, out var round))
        {
            var newParticipators = round.RealTimeMinersInformation.Values.OrderBy(i => i.Order)
                .Where(i => i.PreviousInValue != null).ToList();
            var stillNeed = neededParticipatorCount - participators.Count;
            participators.AddRange(newParticipators.Count > stillNeed
                ? newParticipators.Take(stillNeed)
                : newParticipators);
        }
        else
        {
            Assert(false, "Still preparing random number, try later.");
        }
    }
    
    // Now we can delete this token_hash from RandomNumberInformationMap
    // TODO: Set null if deleting key supported.
    State.RandomNumberInformationMap[input] = new RandomNumberRequestInformation();

    var inValues = participators.Select(i => i.PreviousInValue).ToList();
    var randomHash = inValues.First();
    randomHash = inValues.Skip(1).Aggregate(randomHash, Hash.FromTwoHashes);
    return randomHash;
}

Assert(false, "Still preparing random number, try later.");

// Won't reach here.
return Hash.Empty;

}
接下来对这两个方法添加BVT测试用例。(关于AElf合约的测试有一个框架来着,能够用代码生成器生成一个可以调用某个合约方法的Stub,该Stub就能够模拟一个区块链上用户发交易,每一个交易会单独打包为一个区块。以后有时间写文章详细介绍一下。)

在链刚刚启动的时候,由任意一个模拟用户的Stub发送RequestRandomNumber交易,检查能不能获得一个可用的RandomNumberOrder便可。因为在测试用例执行以前经过大量交易(目前是19个)部署必备的合约,所以在执行该RequestRandomNumber时,区块高度已经达到了20。

[Fact]
internal async Task<Hash> AEDPoSContract_RequestRandomNumber()
{

var randomNumberOrder = (await AEDPoSContractStub.RequestRandomNumber.SendAsync(new Empty())).Output;
randomNumberOrder.TokenHash.ShouldNotBeNull();
randomNumberOrder.BlockHeight.ShouldBeGreaterThan(
    AEDPoSContractTestConstants.InitialMinersCount.Mul(AEDPoSContractTestConstants.TinySlots));
return randomNumberOrder.TokenHash;

}
等到必定的高度后(正好一轮时间),模拟用户发送GetRandomNumber交易来获取随机数。如下的测试用例模拟第一轮的CDC进行正常生产区块的操做,也分别在目标高度(ExpectedBlockHeight)先后尝试发送GetRandomNumber。在高度没有达到时,交易执行结果为失败,错误信息中包含“Still preparing random number.”;第二次发送GetRandomNumber时,交易执行成功并返回可用的随机数值;第三次发送GetRandomNumber,由于相关随机数的信息已经被AEDPoS合约删除(节省空间),所以返回一个空的哈希值。

[Fact]
internal async Task AEDPoSContract_GetRandomNumber()
{

var tokenHash = await AEDPoSContract_RequestRandomNumber();

var currentRound = await BootMiner.GetCurrentRoundInformation.CallAsync(new Empty());

var randomHashes = Enumerable.Range(0, AEDPoSContractTestConstants.InitialMinersCount).Select(_ => Hash.Generate()).ToList();
var triggers = Enumerable.Range(0, AEDPoSContractTestConstants.InitialMinersCount).Select(i => new AElfConsensusTriggerInformation
{
    PublicKey = ByteString.CopyFrom(InitialMinersKeyPairs[i].PublicKey),
    RandomHash = randomHashes[i]
}).ToDictionary(t => t.PublicKey.ToHex(), t => t);

// Exactly one round except extra block time slot.
foreach (var minerInRound in currentRound.RealTimeMinersInformation.Values.OrderBy(m => m.Order))
{
    var currentKeyPair = InitialMinersKeyPairs.First(p => p.PublicKey.ToHex() == minerInRound.PublicKey);

    KeyPairProvider.SetKeyPair(currentKeyPair);

    BlockTimeProvider.SetBlockTime(minerInRound.ExpectedMiningTime);

    var tester = GetAEDPoSContractStub(currentKeyPair);
    var headerInformation =
        (await tester.GetInformationToUpdateConsensus.CallAsync(triggers[minerInRound.PublicKey]
            .ToBytesValue())).ToConsensusHeaderInformation();

    // Update consensus information.
    var toUpdate = headerInformation.Round.ExtractInformationToUpdateConsensus(minerInRound.PublicKey);
    await tester.UpdateValue.SendAsync(toUpdate);

    for (var i = 0; i < 8; i++)
    {
        await tester.UpdateTinyBlockInformation.SendAsync(new TinyBlockInput
        {
            ActualMiningTime = TimestampHelper.GetUtcNow(),
            RoundId = currentRound.RoundId
        });
    }
}

// Not enough.
{
    var transactionResult = (await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash)).TransactionResult;
    transactionResult.Error.ShouldContain("Still preparing random number.");
}

// In test framework, the execution of every transaction will generate a new block no matter the execution succeeded.
await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash);

// Now it's enough.
{
    var randomNumber = (await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash)).Output;
    randomNumber.Value.ShouldNotBeEmpty();
}

// Then this order deleted from state.
{
    var randomNumber = (await AEDPoSContractStub.GetRandomNumber.SendAsync(tokenHash)).Output;
    randomNumber.Value.ShouldBeEmpty();
}

}

欢迎来找茬。

相关文章
相关标签/搜索