diff --git a/.gitignore b/.gitignore index c556d1e590..410da12571 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,5 @@ osxcross/ target/ Cargo.lock **/*.rs.bk + +*.code-workspace \ No newline at end of file diff --git a/chain/block.go b/chain/block.go index 4aab647fe4..3a42c1a1c5 100644 --- a/chain/block.go +++ b/chain/block.go @@ -23,6 +23,7 @@ import ( "github.com/ava-labs/hypersdk/codec" "github.com/ava-labs/hypersdk/consts" + "github.com/ava-labs/hypersdk/merkle" "github.com/ava-labs/hypersdk/state" "github.com/ava-labs/hypersdk/utils" "github.com/ava-labs/hypersdk/window" @@ -40,7 +41,8 @@ type StatefulBlock struct { Tmstmp int64 `json:"timestamp"` Hght uint64 `json:"height"` - Txs []*Transaction `json:"txs"` + Txs []*Transaction `json:"txs"` + TxsRoot ids.ID `json:"txsRoot"` // StateRoot is the root of the post-execution state // of [Prnt]. @@ -290,6 +292,27 @@ func (b *StatelessBlock) initializeBuilt( b.containsWarp = true } } + + // transaction hash generation + // [len(b.Txs)] should be equal to [b.results] + merkleItems := make([][]byte, 0, len(b.Txs)) + for i := 0; i < len(b.Txs); i++ { + txID := b.Txs[i].ID() + resultOutput := b.results[i].Output + // [txID + resultOutput] + // txID is a fixed length array, hence [append] will always allocate new memory and copy + // so slice with new address will be returned and no reflect on txID, then later + // we consume those bytes + merkleItems = append(merkleItems, append(txID[:], resultOutput...)) + } + + // consume bytes to avoid extra copying + root, _, err := merkle.GenerateMerkleRoot(ctx, b.vm.Tracer(), merkleItems, true) + if err != nil { + return err + } + b.TxsRoot = root + return nil } @@ -994,6 +1017,8 @@ func (b *StatefulBlock) Marshal() ([]byte, error) { p.PackID(b.StateRoot) p.PackUint64(uint64(b.WarpResults)) + p.PackID(b.TxsRoot) + bytes := p.Bytes() if err := p.Err(); err != nil { return nil, err @@ -1029,6 +1054,7 @@ func UnmarshalBlock(raw []byte, parser Parser) (*StatefulBlock, error) { p.UnpackID(false, &b.StateRoot) b.WarpResults = set.Bits64(p.UnpackUint64(false)) + p.UnpackID(false, &b.TxsRoot) // Ensure no leftover bytes if !p.Empty() { diff --git a/merkle/merkle.go b/merkle/merkle.go new file mode 100644 index 0000000000..e76e7efbad --- /dev/null +++ b/merkle/merkle.go @@ -0,0 +1,52 @@ +package merkle + +import ( + "context" + + "github.com/ava-labs/avalanchego/database" + "github.com/ava-labs/avalanchego/database/memdb" + "github.com/ava-labs/avalanchego/ids" + "github.com/ava-labs/avalanchego/trace" + "github.com/ava-labs/avalanchego/utils/units" + "github.com/ava-labs/avalanchego/x/merkledb" + "github.com/ava-labs/hypersdk/utils" +) + +// Generate merkle root for a set of items +func GenerateMerkleRoot(ctx context.Context, tracer trace.Tracer, merkleItems [][]byte, consumeBytes bool) (ids.ID, merkledb.MerkleDB, error) { + batchOps := make([]database.BatchOp, 0, len(merkleItems)) + + for _, item := range merkleItems { + key := utils.ToID(item) + batchOps = append(batchOps, database.BatchOp{ + Key: key[:], + Value: item, + }) + } + + db, err := merkledb.New(ctx, memdb.New(), merkledb.Config{ + BranchFactor: merkledb.BranchFactor16, + HistoryLength: 100, + IntermediateNodeCacheSize: units.MiB, + ValueNodeCacheSize: units.MiB, + Tracer: tracer, + }) + if err != nil { + return ids.Empty, nil, err + } + + view, err := db.NewView(ctx, merkledb.ViewChanges{BatchOps: batchOps, ConsumeBytes: consumeBytes}) + if err != nil { + return ids.Empty, nil, err + } + if err := view.CommitToDB(ctx); err != nil { + return ids.Empty, nil, err + } + + root, err := db.GetMerkleRoot(ctx) + if err != nil { + return ids.Empty, nil, err + } + + return root, db, nil +}