diff --git a/blockchain/filedao/blockstore.go b/blockchain/filedao/blockstore.go new file mode 100644 index 0000000000..491fdace00 --- /dev/null +++ b/blockchain/filedao/blockstore.go @@ -0,0 +1,101 @@ +package filedao + +import ( + "fmt" + + "github.com/iotexproject/iotex-proto/golang/iotextypes" + + "github.com/iotexproject/iotex-core/blockchain/block" + "github.com/iotexproject/iotex-core/pkg/util/byteutil" +) + +const ( + blockstoreDefaultVersion = byte(0) + blockstoreHeaderSize = 8 +) + +type ( + // blockstore is a byte slice that contains a block, its receipts and transaction logs + // the first 8 bytes is the size of the block + // the next n bytes is the serialized block and receipts, n is the size of the block + // the rest bytes is the serialized transaction logs + blockstore []byte +) + +var ( + errInvalidBlockstore = fmt.Errorf("invalid blockstore") +) + +func convertToBlockStore(blk *block.Block) (blockstore, error) { + data := make(blockstore, 0) + s := &block.Store{ + Block: blk, + Receipts: blk.Receipts, + } + tmp, err := s.Serialize() + if err != nil { + return nil, err + } + data = append(data, byteutil.Uint64ToBytesBigEndian(uint64(len(tmp)))...) + data = append(data, tmp...) + txLog := blk.TransactionLog() + if txLog != nil { + tmp = txLog.Serialize() + data = append(data, tmp...) + } + return data, nil +} + +func (s blockstore) Block(deser *block.Deserializer) (*block.Block, error) { + size := s.blockSize() + bs, err := deser.DeserializeBlockStore(s[blockstoreHeaderSize : size+blockstoreHeaderSize]) + if err != nil { + return nil, err + } + bs.Block.Receipts = bs.Receipts + return bs.Block, nil +} + +func (s blockstore) TransactionLogs() (*iotextypes.TransactionLogs, error) { + size := s.blockSize() + if uint64(len(s)) == size+blockstoreHeaderSize { + return nil, nil + } + return block.DeserializeSystemLogPb(s[size+blockstoreHeaderSize:]) +} + +func (s blockstore) Serialize() []byte { + return append([]byte{blockstoreDefaultVersion}, s...) +} + +func (s *blockstore) Deserialize(data []byte) error { + if len(data) == 0 { + return errInvalidBlockstore + } + switch data[0] { + case blockstoreDefaultVersion: + bs := blockstore(data[1:]) + if err := bs.Validate(); err != nil { + return err + } + *s = bs + return nil + default: + return errInvalidBlockstore + } +} + +func (s blockstore) blockSize() uint64 { + return byteutil.BytesToUint64BigEndian(s[:blockstoreHeaderSize]) +} + +func (s blockstore) Validate() error { + if len(s) < blockstoreHeaderSize { + return errInvalidBlockstore + } + blkSize := s.blockSize() + if uint64(len(s)) < blkSize+blockstoreHeaderSize { + return errInvalidBlockstore + } + return nil +} diff --git a/blockchain/filedao/blockstore_test.go b/blockchain/filedao/blockstore_test.go new file mode 100644 index 0000000000..f5ce3470ee --- /dev/null +++ b/blockchain/filedao/blockstore_test.go @@ -0,0 +1,40 @@ +package filedao + +import ( + "testing" + + "github.com/iotexproject/go-pkgs/hash" + "github.com/stretchr/testify/require" + + "github.com/iotexproject/iotex-core/blockchain/block" +) + +func TestBlockStore(t *testing.T) { + r := require.New(t) + builder := block.NewTestingBuilder() + blk := createTestingBlock(builder, 1, hash.ZeroHash256) + bs, err := convertToBlockStore(blk) + r.NoError(err) + data := bs.Serialize() + dbs := new(blockstore) + r.NoError(dbs.Deserialize(data)) + r.Equal(bs[:], (*dbs)[:], "serialized block store should be equal to deserialized block store") + // check deserialized block + deser := block.NewDeserializer(0) + dBlk, err := dbs.Block(deser) + r.NoError(err) + dTxLogs, err := dbs.TransactionLogs() + r.NoError(err) + r.NoError(fillTransactionLog(dBlk.Receipts, dTxLogs.Logs)) + r.Equal(blk.Header, dBlk.Header) + r.Equal(blk.Body, dBlk.Body) + r.Equal(blk.Footer, dBlk.Footer) + r.Equal(len(blk.Receipts), len(dBlk.Receipts)) + for i := range blk.Receipts { + r.Equal(blk.Receipts[i].Hash(), dBlk.Receipts[i].Hash()) + r.Equal(len(blk.Receipts[i].TransactionLogs()), len(dBlk.Receipts[i].TransactionLogs())) + for j := range blk.Receipts[i].TransactionLogs() { + r.Equal(blk.Receipts[i].TransactionLogs()[j], dBlk.Receipts[i].TransactionLogs()[j]) + } + } +} diff --git a/blockchain/filedao/sized.go b/blockchain/filedao/sized.go new file mode 100644 index 0000000000..1631aa3d77 --- /dev/null +++ b/blockchain/filedao/sized.go @@ -0,0 +1,372 @@ +package filedao + +import ( + "bytes" + "context" + "math/big" + "os" + "slices" + "sync" + + "github.com/holiman/billy" + "github.com/iotexproject/go-pkgs/hash" + "github.com/iotexproject/iotex-proto/golang/iotextypes" + "github.com/pkg/errors" + "go.uber.org/zap" + + "github.com/iotexproject/iotex-core/action" + "github.com/iotexproject/iotex-core/blockchain/block" + "github.com/iotexproject/iotex-core/db" + "github.com/iotexproject/iotex-core/pkg/log" +) + +type ( + sizedDao struct { + size uint64 + dataDir string + + store billy.Database + + tip uint64 + base uint64 + heightToHash map[uint64]hash.Hash256 + hashToHeight map[hash.Hash256]uint64 + heightToID map[uint64]uint64 + dropCh chan uint64 + lock sync.RWMutex + wg sync.WaitGroup + + deser *block.Deserializer + } +) + +func NewSizedFileDao(size uint64, dataDir string, deser *block.Deserializer) (FileDAO, error) { + sd := &sizedDao{ + size: size, + dataDir: dataDir, + heightToHash: make(map[uint64]hash.Hash256), + hashToHeight: make(map[hash.Hash256]uint64), + heightToID: make(map[uint64]uint64), + deser: deser, + dropCh: make(chan uint64, size), + } + return sd, nil +} + +func (sd *sizedDao) Start(ctx context.Context) error { + sd.lock.Lock() + defer sd.lock.Unlock() + dir := sd.dataDir + if err := os.MkdirAll(dir, 0700); err != nil { + return errors.Wrap(err, "failed to create blob store directory") + } + + index := func(id uint64, size uint32, blob []byte) { + bs := new(blockstore) + err := bs.Deserialize(blob) + if err != nil { + log.L().Warn("Failed to decode block store", zap.Error(err)) + return + } + blk, err := bs.Block(sd.deser) + if err != nil { + log.L().Warn("Failed to decode block", zap.Error(err)) + return + } + h := blk.HashBlock() + height := blk.Height() + sd.hashToHeight[h] = height + sd.heightToHash[height] = h + sd.heightToID[height] = id + if height > sd.tip || sd.tip == 0 { + sd.tip = height + } + if height < sd.base || sd.base == 0 { + sd.base = height + } + } + sd.base = 1 + store, err := billy.Open(billy.Options{Path: dir}, newSlotter(), index) + if err != nil { + return errors.Wrap(err, "failed to open blob store") + } + sd.store = store + // block continous check + for i := sd.tip; i >= sd.base; i-- { + if _, ok := sd.heightToID[i]; !ok { + // remove non-continous blocks[base to i] + for j := sd.base; j < i; j++ { + if id, ok := sd.heightToID[j]; ok { + sd.dropCh <- id + h := sd.heightToHash[j] + delete(sd.heightToHash, j) + delete(sd.hashToHeight, h) + delete(sd.heightToID, j) + } + } + sd.base = i + 1 + break + } + } + // start drop routine + go func() { + sd.wg.Add(1) + defer sd.wg.Done() + for id := range sd.dropCh { + if err := sd.store.Delete(id); err != nil { + log.L().Error("Failed to delete block", zap.Error(err)) + } + } + }() + return nil +} + +func (sd *sizedDao) Stop(ctx context.Context) error { + sd.lock.Lock() + defer sd.lock.Unlock() + close(sd.dropCh) + sd.wg.Wait() + return sd.store.Close() +} + +func (sd *sizedDao) SetStart(height uint64) error { + sd.lock.Lock() + defer sd.lock.Unlock() + if len(sd.hashToHeight) > 0 || len(sd.heightToHash) > 0 || len(sd.heightToID) > 0 { + return errors.New("cannot set start height after start") + } + sd.base = height - 1 + sd.tip = height - 1 + return nil +} + +func (sd *sizedDao) PutBlock(ctx context.Context, blk *block.Block) error { + if blk.Height() != sd.tip+1 { + return ErrInvalidTipHeight + } + bs, err := convertToBlockStore(blk) + if err != nil { + return err + } + + sd.lock.Lock() + defer sd.lock.Unlock() + if blk.Height() != sd.tip+1 { + return ErrInvalidTipHeight + } + id, err := sd.store.Put(bs.Serialize()) + if err != nil { + return err + } + sd.tip++ + hash := blk.HashBlock() + sd.heightToHash[sd.tip] = hash + sd.hashToHeight[hash] = sd.tip + sd.heightToID[sd.tip] = id + + if sd.tip-sd.base >= sd.size { + sd.drop() + } + return nil +} + +func (sd *sizedDao) Height() (uint64, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + return sd.tip, nil +} + +func (sd *sizedDao) GetBlockHash(height uint64) (hash.Hash256, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + h, ok := sd.heightToHash[height] + if !ok { + return hash.ZeroHash256, db.ErrNotExist + } + return h, nil +} + +func (sd *sizedDao) GetBlockHeight(h hash.Hash256) (uint64, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + height, ok := sd.hashToHeight[h] + if !ok { + return 0, db.ErrNotExist + } + return height, nil +} + +func (sd *sizedDao) GetBlock(h hash.Hash256) (*block.Block, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + height, ok := sd.hashToHeight[h] + if !ok { + return nil, db.ErrNotExist + } + return sd.getBlock(height) +} + +func (sd *sizedDao) GetBlockByHeight(height uint64) (*block.Block, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + return sd.getBlock(height) +} + +func (sd *sizedDao) GetReceipts(height uint64) ([]*action.Receipt, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + blk, err := sd.getBlock(height) + if err != nil { + return nil, err + } + return blk.Receipts, nil +} + +func (sd *sizedDao) ContainsTransactionLog() bool { + return true +} + +func (sd *sizedDao) TransactionLogs(height uint64) (*iotextypes.TransactionLogs, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + id, ok := sd.heightToID[height] + if !ok { + return nil, db.ErrNotExist + } + data, err := sd.store.Get(id) + if err != nil { + return nil, err + } + bs := new(blockstore) + err = bs.Deserialize(data) + if err != nil { + return nil, err + } + return bs.TransactionLogs() +} + +func (sd *sizedDao) DeleteTipBlock() error { + panic("not supported") +} + +func (sd *sizedDao) Header(h hash.Hash256) (*block.Header, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + height, ok := sd.hashToHeight[h] + if !ok { + return nil, db.ErrNotExist + } + blk, err := sd.getBlock(height) + if err != nil { + return nil, err + } + return &blk.Header, nil +} + +func (sd *sizedDao) HeaderByHeight(height uint64) (*block.Header, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + blk, err := sd.getBlock(height) + if err != nil { + return nil, err + } + return &blk.Header, nil +} + +func (sd *sizedDao) FooterByHeight(height uint64) (*block.Footer, error) { + sd.lock.RLock() + defer sd.lock.RUnlock() + blk, err := sd.getBlock(height) + if err != nil { + return nil, err + } + return &blk.Footer, nil +} + +func (sd *sizedDao) getBlock(height uint64) (*block.Block, error) { + id, ok := sd.heightToID[height] + if !ok { + return nil, db.ErrNotExist + } + data, err := sd.store.Get(id) + if err != nil { + return nil, err + } + bs := new(blockstore) + err = bs.Deserialize(data) + if err != nil { + return nil, err + } + return bs.Block(sd.deser) +} + +func (sd *sizedDao) drop() { + id, ok := sd.heightToID[sd.base] + if ok { + sd.dropCh <- id + hash := sd.heightToHash[sd.base] + delete(sd.heightToHash, sd.base) + delete(sd.heightToID, sd.base) + delete(sd.hashToHeight, hash) + } + sd.base++ +} + +func newSlotter() func() (uint32, bool) { + sizeList := []uint32{ + 1024 * 4, // empty block + 1024 * 8, // 2 execution + 1024 * 16, + 1024 * 128, // 250 transfer + 1024 * 512, + 1024 * 1024, + 1024 * 1024 * 4, // 5000 transfer + 1024 * 1024 * 8, + 1024 * 1024 * 16, + 1024 * 1024 * 128, + 1024 * 1024 * 512, + 1024 * 1024 * 1024, // max block size + } + i := -1 + return func() (size uint32, done bool) { + i++ + if i >= len(sizeList)-1 { + return sizeList[i], true + } + return sizeList[i], false + } +} + +func fillTransactionLog(receipts []*action.Receipt, txLogs []*iotextypes.TransactionLog) error { + for _, l := range txLogs { + idx := slices.IndexFunc(receipts, func(r *action.Receipt) bool { + return bytes.Equal(r.ActionHash[:], l.ActionHash) + }) + if idx < 0 { + return errors.Errorf("missing receipt for log %x", l.ActionHash) + } + txLogs := make([]*action.TransactionLog, len(l.GetTransactions())) + for j, tx := range l.GetTransactions() { + txlog, err := convertToTxLog(tx) + if err != nil { + return err + } + txLogs[j] = txlog + } + receipts[idx].AddTransactionLogs(txLogs...) + } + return nil +} + +func convertToTxLog(tx *iotextypes.TransactionLog_Transaction) (*action.TransactionLog, error) { + amount, ok := big.NewInt(0).SetString(tx.Amount, 10) + if !ok { + return nil, errors.Errorf("failed to parse amount %s", tx.Amount) + } + return &action.TransactionLog{ + Type: tx.Type, + Amount: amount, + Sender: tx.Sender, + Recipient: tx.Recipient, + }, nil +} diff --git a/blockchain/filedao/sized_test.go b/blockchain/filedao/sized_test.go new file mode 100644 index 0000000000..7a900c1f8e --- /dev/null +++ b/blockchain/filedao/sized_test.go @@ -0,0 +1,184 @@ +package filedao + +import ( + "context" + "crypto/tls" + "testing" + + "github.com/iotexproject/go-pkgs/hash" + "github.com/iotexproject/iotex-proto/golang/iotexapi" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + + "github.com/iotexproject/iotex-core/action" + "github.com/iotexproject/iotex-core/blockchain/block" + "github.com/iotexproject/iotex-core/db" +) + +func TestBlockSize(t *testing.T) { + t.Skip("used for estimating block size") + r := require.New(t) + conn, err := grpc.NewClient("api.mainnet.iotex.one:443", grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{MinVersion: tls.VersionTLS12}))) + r.NoError(err) + defer conn.Close() + + cli := iotexapi.NewAPIServiceClient(conn) + for _, h := range []uint64{29276276} { + resp, err := cli.GetRawBlocks(context.Background(), &iotexapi.GetRawBlocksRequest{ + StartHeight: h, + Count: 1, + WithReceipts: true, + WithTransactionLogs: true, + }) + r.NoError(err) + r.Len(resp.Blocks, 1) + deserializer := block.NewDeserializer(4689) + blk, err := deserializer.FromBlockProto(resp.Blocks[0].Block) + r.NoError(err) + receipts := make([]*action.Receipt, 0, len(resp.Blocks[0].Receipts)) + for _, receiptpb := range resp.Blocks[0].Receipts { + receipt := &action.Receipt{} + receipt.ConvertFromReceiptPb(receiptpb) + receipts = append(receipts, receipt) + } + blk.Receipts = receipts + r.NoError(fillTransactionLog(receipts, resp.Blocks[0].TransactionLogs.Logs)) + data, err := convertToBlockStore(blk) + r.NoError(err) + t.Logf("block %d size= %d", h, len(data)) + } +} + +func TestSizedDaoIntegrity(t *testing.T) { + r := require.New(t) + desr := block.NewDeserializer(4689) + ctx := context.Background() + blocks := make([]*block.Block, 0, 20) + builder := block.NewTestingBuilder() + h := hash.ZeroHash256 + for i := 1; i <= cap(blocks); i++ { + blk := createTestingBlock(builder, uint64(i), h) + blocks = append(blocks, blk) + h = blk.HashBlock() + t.Logf("block height %d hash: %x", i, h) + } + // case: putblocks 1 - 10 + datadir := t.TempDir() + dao, err := NewSizedFileDao(10, datadir, desr) + r.NoError(err) + r.NoError(dao.Start(ctx)) + for i := 0; i < 10; i++ { + r.NoError(dao.PutBlock(ctx, blocks[i])) + } + // verify + testVerifyChainDB(t, dao, 1, 10) + // case: put block not expected + r.ErrorIs(dao.PutBlock(ctx, blocks[8]), ErrInvalidTipHeight) + r.ErrorIs(dao.PutBlock(ctx, blocks[9]), ErrInvalidTipHeight) + r.ErrorIs(dao.PutBlock(ctx, blocks[11]), ErrInvalidTipHeight) + // case: put blocks 11 - 20 + for i := 10; i < 20; i++ { + r.NoError(dao.PutBlock(ctx, blocks[i])) + blk, err := dao.GetBlockByHeight(11) + r.NoError(err) + r.Equal(blocks[10].HashBlock(), blk.HashBlock(), "height %d", i) + } + // verify new blocks added and old blocks removed + testVerifyChainDB(t, dao, 11, 20) + testVerifyChainDBNotExists(t, dao, blocks[:10]) + // case: reload + r.NoError(dao.Stop(ctx)) + dao, err = NewSizedFileDao(10, datadir, desr) + r.NoError(err) + r.NoError(dao.Start(ctx)) + // verify blocks reloaded + testVerifyChainDB(t, dao, 11, 20) + testVerifyChainDBNotExists(t, dao, blocks[:10]) + // case: damaged db + inner := dao.(*sizedDao) + _, err = inner.store.Put([]byte("damaged")) + r.NoError(err) + r.NoError(dao.Stop(ctx)) + dao, err = NewSizedFileDao(10, datadir, desr) + r.NoError(err) + // verify invalid data is ignored + r.NoError(dao.Start(ctx)) + testVerifyChainDB(t, dao, 11, 20) + testVerifyChainDBNotExists(t, dao, blocks[:10]) + // case: remove non-continous blocks + inner = dao.(*sizedDao) + inner.store.Delete(inner.heightToID[15]) + r.NoError(dao.Stop(ctx)) + dao, err = NewSizedFileDao(10, datadir, desr) + r.NoError(err) + // verify blocks 11 - 15 are removed + r.NoError(dao.Start(ctx)) + testVerifyChainDB(t, dao, 16, 20) + testVerifyChainDBNotExists(t, dao, blocks[:15]) + r.NoError(dao.Stop(ctx)) +} + +func TestSizedDaoStorage(t *testing.T) { + r := require.New(t) + desr := block.NewDeserializer(4689) + ctx := context.Background() + + datadir := t.TempDir() + dao, err := NewSizedFileDao(10, datadir, desr) + r.NoError(err) + r.NoError(dao.Start(ctx)) + + // put empty block + height := uint64(1) + builder := block.NewTestingBuilder() + h := hash.ZeroHash256 + blk := createTestingBlock(builder, height, h) + height++ + h = blk.HashBlock() + r.NoError(dao.PutBlock(ctx, blk)) + // put block with 5 actions + blk = createTestingBlock(builder, height, h, withActionNum(5)) + height++ + h = blk.HashBlock() + r.NoError(dao.PutBlock(ctx, blk)) + // put block with 100 actions + blk = createTestingBlock(builder, height, h, withActionNum(100)) + height++ + h = blk.HashBlock() + r.NoError(dao.PutBlock(ctx, blk)) + // put block with 1000 actions + blk = createTestingBlock(builder, height, h, withActionNum(1000)) + height++ + h = blk.HashBlock() + r.NoError(dao.PutBlock(ctx, blk)) + // put block with 5000 actions + blk = createTestingBlock(builder, height, h, withActionNum(5000)) + height++ + h = blk.HashBlock() + r.NoError(dao.PutBlock(ctx, blk)) + // put block with 10000 actions + blk = createTestingBlock(builder, height, h, withActionNum(10000)) + height++ + h = blk.HashBlock() + r.NoError(dao.PutBlock(ctx, blk)) + r.NoError(dao.Stop(ctx)) +} + +func testVerifyChainDBNotExists(t *testing.T, fd FileDAO, blocks []*block.Block) { + r := require.New(t) + for _, blk := range blocks { + _, err := fd.GetBlockHash(blk.Height()) + r.ErrorIs(err, db.ErrNotExist) + _, err = fd.GetBlockHeight(blk.HashBlock()) + r.ErrorIs(err, db.ErrNotExist) + _, err = fd.GetBlockByHeight(blk.Height()) + r.ErrorIs(err, db.ErrNotExist) + _, err = fd.GetBlock(blk.HashBlock()) + r.ErrorIs(err, db.ErrNotExist) + _, err = fd.GetReceipts(blk.Height()) + r.ErrorIs(err, db.ErrNotExist) + _, err = fd.TransactionLogs(blk.Height()) + r.ErrorIs(err, db.ErrNotExist) + } +} diff --git a/blockchain/filedao/testing.go b/blockchain/filedao/testing.go index 3df58a6244..19bc5a28c7 100644 --- a/blockchain/filedao/testing.go +++ b/blockchain/filedao/testing.go @@ -9,6 +9,7 @@ import ( "context" "encoding/hex" "math/big" + "math/rand" "testing" "github.com/pkg/errors" @@ -223,24 +224,64 @@ func testVerifyChainDB(t *testing.T, fd FileDAO, start, end uint64) { } } -func createTestingBlock(builder *block.TestingBuilder, height uint64, h hash.Hash256) *block.Block { +type ( + testBlockCfg struct { + actionNum int + } + testBlockOption func(*testBlockCfg) +) + +func withActionNum(num int) testBlockOption { + return func(cfg *testBlockCfg) { + cfg.actionNum = num + } +} + +func createTestingBlock(builder *block.TestingBuilder, height uint64, h hash.Hash256, opts ...testBlockOption) *block.Block { + cfg := &testBlockCfg{} + for _, opt := range opts { + opt(cfg) + } block.LoadGenesisHash(&genesis.Default) - r := &action.Receipt{ - Status: 1, - BlockHeight: height, - ActionHash: h, + if cfg.actionNum > 0 { + acts := make([]*action.SealedEnvelope, 0) + receipts := make([]*action.Receipt, 0) + for i := 0; i < cfg.actionNum; i++ { + amount := big.NewInt(int64(rand.Intn(100))) + sender := rand.Intn(20) + receipient := rand.Intn(20) + act, _ := action.SignedTransfer(identityset.Address(receipient).String(), identityset.PrivateKey(sender), uint64(rand.Intn(100)), amount, nil, testutil.TestGasLimit, testutil.TestGasPrice) + acts = append(acts, act) + actHash, _ := act.Hash() + r := &action.Receipt{ + Status: 1, + BlockHeight: height, + ActionHash: actHash, + } + receipts = append(receipts, r.AddTransactionLogs(&action.TransactionLog{ + Type: iotextypes.TransactionLogType_NATIVE_TRANSFER, + Amount: amount, + Sender: hex.EncodeToString(identityset.Address(sender).Bytes()), + Recipient: hex.EncodeToString(identityset.Address(receipient).Bytes()), + })) + } + builder.AddActions(acts...).SetReceipts(receipts) + } else { + r := &action.Receipt{ + Status: 1, + BlockHeight: height, + ActionHash: h, + } + builder.SetReceipts([]*action.Receipt{r.AddTransactionLogs(&action.TransactionLog{ + Type: iotextypes.TransactionLogType_NATIVE_TRANSFER, + Amount: big.NewInt(100), + Sender: hex.EncodeToString(h[:]), + Recipient: hex.EncodeToString(h[:]), + })}) } blk, _ := builder. SetHeight(height). SetPrevBlockHash(h). - SetReceipts([]*action.Receipt{ - r.AddTransactionLogs(&action.TransactionLog{ - Type: iotextypes.TransactionLogType_NATIVE_TRANSFER, - Amount: big.NewInt(100), - Sender: hex.EncodeToString(h[:]), - Recipient: hex.EncodeToString(h[:]), - }), - }). SetTimeStamp(testutil.TimestampNow().UTC()). SignAndBuild(identityset.PrivateKey(27)) return &blk