package batchmap
import (
"context"
"crypto"
"fmt"
"github.com/apache/beam/sdks/v2/go/pkg/beam"
"github.com/apache/beam/sdks/v2/go/pkg/beam/register"
"github.com/google/trillian/merkle/coniks"
"github.com/google/trillian/merkle/smt"
"github.com/google/trillian/merkle/smt/node"
)
var (
cntTilesHashed = beam.NewCounter("batchmap", "tiles-hashed")
cntTilesCopied = beam.NewCounter("batchmap", "tiles-copied")
cntTilesCreated = beam.NewCounter("batchmap", "tiles-created")
cntTilesUpdated = beam.NewCounter("batchmap", "tiles-updated")
)
func init() {
register.DoFn1x2[nodeHash, []byte, nodeHash](&leafShardFn{})
register.DoFn3x2[context.Context, []byte, func(*nodeHash) bool, *Tile, error](&tileHashFn{})
register.DoFn4x2[context.Context, []byte, func(**Tile) bool, func(*nodeHash) bool, *Tile, error](&tileUpdateFn{})
register.Function5x1(createStratum)
register.Function6x1(updateStratum)
register.Function1x2(tilePathFn)
}
func Create(s beam.Scope, entries beam.PCollection, treeID int64, hash crypto.Hash, prefixStrata int) (beam.PCollection, error) {
s = s.Scope("batchmap.Create")
if prefixStrata < 0 || prefixStrata >= 32 {
return beam.PCollection{}, fmt.Errorf("prefixStrata must be in [0, 32), got %d", prefixStrata)
}
nodeHashes := beam.ParDo(s, entryToNodeHashFn, entries)
lastStratum := createStratum(s, nodeHashes, treeID, hash, prefixStrata)
allTiles := make([]beam.PCollection, 0, prefixStrata+1)
allTiles = append(allTiles, lastStratum)
for d := prefixStrata - 1; d >= 0; d-- {
nodeHashes = beam.ParDo(s, tileToNodeHashFn, lastStratum)
lastStratum = createStratum(s, nodeHashes, treeID, hash, d)
allTiles = append(allTiles, lastStratum)
}
return beam.Flatten(s, allTiles...), nil
}
func Update(s beam.Scope, base, delta beam.PCollection, treeID int64, hash crypto.Hash, prefixStrata int) (beam.PCollection, error) {
s = s.Scope("batchmap.Update")
if prefixStrata < 0 || prefixStrata >= 32 {
return beam.PCollection{}, fmt.Errorf("prefixStrata must be in [0, 32), got %d", prefixStrata)
}
baseStrata := beam.Partition(s, prefixStrata+1, partitionByPrefixLenFn, base)
nodeHashes := beam.ParDo(s, entryToNodeHashFn, delta)
lastStratum := updateStratum(s, baseStrata[prefixStrata], nodeHashes, treeID, hash, prefixStrata)
allTiles := make([]beam.PCollection, 0, prefixStrata+1)
allTiles = append(allTiles, lastStratum)
for d := prefixStrata - 1; d >= 0; d-- {
nodeHashes = beam.ParDo(s, tileToNodeHashFn, lastStratum)
lastStratum = updateStratum(s, baseStrata[d], nodeHashes, treeID, hash, d)
allTiles = append(allTiles, lastStratum)
}
return beam.Flatten(s, allTiles...), nil
}
func createStratum(s beam.Scope, leaves beam.PCollection, treeID int64, hash crypto.Hash, rootDepth int) beam.PCollection {
s = s.Scope(fmt.Sprintf("createStratum-%d", rootDepth))
shardedLeaves := beam.ParDo(s, &leafShardFn{RootDepthBytes: rootDepth}, leaves)
return beam.ParDo(s, &tileHashFn{TreeID: treeID, Hash: hash}, beam.GroupByKey(s, shardedLeaves))
}
func updateStratum(s beam.Scope, base, deltas beam.PCollection, treeID int64, hash crypto.Hash, rootDepth int) beam.PCollection {
s = s.Scope(fmt.Sprintf("updateStratum-%d", rootDepth))
shardedBase := beam.ParDo(s, tilePathFn, base)
shardedDelta := beam.ParDo(s, &leafShardFn{RootDepthBytes: rootDepth}, deltas)
return beam.ParDo(s, &tileUpdateFn{TreeID: treeID, Hash: hash}, beam.CoGroupByKey(s, shardedBase, shardedDelta))
}
func tilePathFn(t *Tile) ([]byte, *Tile) { return t.Path, t }
type nodeHash struct {
Path []byte
Hash []byte
}
func partitionByPrefixLenFn(t *Tile) int {
return len(t.Path)
}
func tileToNodeHashFn(t *Tile) nodeHash {
return nodeHash{Path: t.Path, Hash: t.RootHash}
}
func entryToNodeHashFn(e *Entry) nodeHash {
return nodeHash{Path: e.HashKey, Hash: e.HashValue}
}
type leafShardFn struct {
RootDepthBytes int
}
func (fn *leafShardFn) ProcessElement(leaf nodeHash) ([]byte, nodeHash) {
return leaf.Path[:fn.RootDepthBytes], leaf
}
type tileHashFn struct {
TreeID int64
Hash crypto.Hash
th *tileHasher
}
func (fn *tileHashFn) Setup() {
fn.th = &tileHasher{fn.TreeID, coniks.New(fn.Hash)}
}
func (fn *tileHashFn) ProcessElement(ctx context.Context, rootPath []byte, leaves func(*nodeHash) bool) (*Tile, error) {
nodes, err := convertNodes(leaves)
if err != nil {
return nil, err
}
cntTilesHashed.Inc(ctx, 1)
return fn.th.construct(rootPath, nodes)
}
func convertNodes(leaves func(*nodeHash) bool) ([]smt.Node, error) {
nodes := []smt.Node{}
var leaf nodeHash
for leaves(&leaf) {
lid, err := nodeID2Decode(leaf.Path)
if err != nil {
return nil, fmt.Errorf("failed to decode leaf ID: %v", err)
}
nodes = append(nodes, smt.Node{ID: lid, Hash: leaf.Hash})
}
return nodes, nil
}
type tileUpdateFn struct {
TreeID int64
Hash crypto.Hash
th *tileHasher
}
func (fn *tileUpdateFn) Setup() {
fn.th = &tileHasher{fn.TreeID, coniks.New(fn.Hash)}
}
func (fn *tileUpdateFn) ProcessElement(ctx context.Context, rootPath []byte, bases func(**Tile) bool, deltas func(*nodeHash) bool) (*Tile, error) {
base, err := getOptionalTile(bases)
if err != nil {
return nil, fmt.Errorf("failed precondition getOptionalTile at %x: %v", rootPath, err)
}
nodes, err := convertNodes(deltas)
if err != nil {
return nil, err
}
if len(nodes) == 0 {
cntTilesCopied.Inc(ctx, 1)
return base, nil
}
if base == nil {
cntTilesCreated.Inc(ctx, 1)
return fn.th.construct(rootPath, nodes)
}
cntTilesUpdated.Inc(ctx, 1)
return fn.updateTile(rootPath, base, nodes)
}
func (fn *tileUpdateFn) updateTile(rootPath []byte, base *Tile, deltas []smt.Node) (*Tile, error) {
baseNodes := make([]smt.Node, 0, len(base.Leaves))
for _, l := range base.Leaves {
leafPath := append(rootPath, l.Path...)
lidx, err := nodeID2Decode(leafPath)
if err != nil {
return nil, fmt.Errorf("failed to decode leaf ID: %v", err)
}
baseNodes = append(baseNodes, smt.Node{ID: lidx, Hash: l.Hash})
}
return fn.th.update(rootPath, baseNodes, deltas)
}
type tileHasher struct {
treeID int64
h *coniks.Hasher
}
func (th *tileHasher) construct(rootPath []byte, nodes []smt.Node) (*Tile, error) {
rootDepthBytes := len(rootPath)
if err := smt.Prepare(nodes, nodes[0].ID.BitLen()); err != nil {
return nil, fmt.Errorf("smt.Prepare: %v", err)
}
tls := make([]*TileLeaf, len(nodes))
for i, n := range nodes {
nPath, err := nodeID2Encode(n.ID)
if err != nil {
return nil, fmt.Errorf("failed to encode leaf ID: %v", err)
}
tls[i] = &TileLeaf{
Path: nPath[rootDepthBytes:],
Hash: n.Hash,
}
}
rootHash, err := th.hashTile(uint(8*rootDepthBytes), nodes)
if err != nil {
return nil, fmt.Errorf("failed to hash tile: %v", err)
}
return &Tile{
Path: rootPath,
Leaves: tls,
RootHash: rootHash,
}, nil
}
func (th *tileHasher) update(rootPath []byte, baseNodes, deltaNodes []smt.Node) (*Tile, error) {
m := make(map[node.ID]smt.Node)
for _, leaf := range deltaNodes {
if v, found := m[leaf.ID]; found {
return nil, fmt.Errorf("found duplicate values at leaf tile position %s: {%x, %x}", leaf.ID, v.Hash, leaf.Hash)
}
m[leaf.ID] = leaf
}
for _, leaf := range baseNodes {
if _, found := m[leaf.ID]; !found {
m[leaf.ID] = leaf
}
}
nodes := make([]smt.Node, 0, len(m))
for _, v := range m {
nodes = append(nodes, v)
}
return th.construct(rootPath, nodes)
}
func (th *tileHasher) hashTile(depthBits uint, leaves []smt.Node) ([]byte, error) {
h, err := smt.NewHStar3(leaves, th.h.HashChildren, uint(leaves[0].ID.BitLen()), depthBits)
if err != nil {
return nil, err
}
r, err := h.Update(th)
if err != nil {
return nil, err
}
if len(r) != 1 {
return nil, fmt.Errorf("expected single root but got %d", len(r))
}
return r[0].Hash, nil
}
func (th tileHasher) Get(id node.ID) ([]byte, error) {
return th.h.HashEmpty(th.treeID, id), nil
}
func (th tileHasher) Set(id node.ID, hash []byte) {}
func nodeID2Encode(n node.ID) ([]byte, error) {
b, c := n.LastByte()
if c == 0 {
return []byte{}, nil
}
if c == 8 {
return append([]byte(n.FullBytes()), b), nil
}
return nil, fmt.Errorf("node ID bit length is not aligned to bytes: %d", n.BitLen())
}
func nodeID2Decode(bs []byte) (node.ID, error) {
return node.NewID(string(bs), 8*uint(len(bs))), nil
}
func getOptionalTile(iter func(**Tile) bool) (*Tile, error) {
var t1, t2 *Tile
if !iter(&t1) || !iter(&t2) {
return t1, nil
}
return nil, fmt.Errorf("unexpectedly found multiple tiles at %x", t1.Path)
}