github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/fvm/environment/uuids.go (about)

     1  package environment
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"encoding/binary"
     6  	"fmt"
     7  
     8  	"github.com/rs/zerolog"
     9  
    10  	"github.com/onflow/flow-go/fvm/storage/state"
    11  	"github.com/onflow/flow-go/fvm/tracing"
    12  	"github.com/onflow/flow-go/model/flow"
    13  	"github.com/onflow/flow-go/module/trace"
    14  	"github.com/onflow/flow-go/utils/slices"
    15  )
    16  
    17  // uuid is partitioned with 3rd byte for compatibility reasons.
    18  // (database types and Javascript safe integer limits)
    19  //
    20  // counter(C) is 7 bytes, paritition(P) is 1 byte
    21  // uuid is assembled by first reading the counter from the register value of the partitioned register,
    22  // and then left shifting the 6th and 7th byte, and placing the partition byte at 6th byte:
    23  // C7 C6 P C5 C4 C3 C2 C1
    24  //
    25  // Until resource ids start filling the bits above the 48th one, dapps will have enough time
    26  // to switch to a larger data type.
    27  
    28  const (
    29  	// The max value for any is uuid partition is MaxUint56, since one byte
    30  	// in the uuid is used for partitioning.
    31  	MaxUint56 = (uint64(1) << 56) - 1
    32  
    33  	// Start warning when there's only a single high bit left.  This should give
    34  	// us plenty of time to migrate to larger counters.
    35  	Uint56OverflowWarningThreshold = (uint64(1) << 55) - 1
    36  )
    37  
    38  type UUIDGenerator interface {
    39  	GenerateUUID() (uint64, error)
    40  }
    41  
    42  type ParseRestrictedUUIDGenerator struct {
    43  	txnState state.NestedTransactionPreparer
    44  	impl     UUIDGenerator
    45  }
    46  
    47  func NewParseRestrictedUUIDGenerator(
    48  	txnState state.NestedTransactionPreparer,
    49  	impl UUIDGenerator,
    50  ) UUIDGenerator {
    51  	return ParseRestrictedUUIDGenerator{
    52  		txnState: txnState,
    53  		impl:     impl,
    54  	}
    55  }
    56  
    57  func (generator ParseRestrictedUUIDGenerator) GenerateUUID() (uint64, error) {
    58  	return parseRestrict1Ret(
    59  		generator.txnState,
    60  		trace.FVMEnvGenerateUUID,
    61  		generator.impl.GenerateUUID)
    62  }
    63  
    64  type uUIDGenerator struct {
    65  	tracer tracing.TracerSpan
    66  	log    zerolog.Logger
    67  	meter  Meter
    68  
    69  	txnState state.NestedTransactionPreparer
    70  
    71  	blockHeader *flow.Header
    72  	txnIndex    uint32
    73  
    74  	initialized bool
    75  	partition   byte
    76  	registerId  flow.RegisterID
    77  }
    78  
    79  func uuidPartition(blockId flow.Identifier, txnIndex uint32) byte {
    80  	// Partitioning by txnIndex ensures temporally neighboring transactions do
    81  	// not share registers / conflict with each other.
    82  	//
    83  	// Since all blocks will have a transaction at txnIndex 0 but not
    84  	// necessarily a transaction at txnIndex 255, if we assign partition based
    85  	// only on txnIndex, partition 0's counter (and other low-valued
    86  	// partitions' counters) will fill up much more quickly than high-valued
    87  	// partitions' counters.  Therefore, a deterministically random offset is
    88  	// used to ensure the partitioned counters are roughly balanced.  Any byte
    89  	// in the sha hash is sufficiently random/uniform for this purpose (Note that
    90  	// block Id is already a sha hash, but its hash implementation may change
    91  	// underneath us).
    92  	//
    93  	// Note that since partition 0 reuses the legacy counter, its counter is
    94  	// much	further ahead than the other partitions.  If partition 0's counter
    95  	// is in danager of overflowing, use variants of "the power of two random
    96  	// choices" to shed load to other counters.
    97  	//
    98  	// The explicit mod is not really needed, but is there for completeness.
    99  	partitionOffset := sha256.Sum256(blockId[:])[0]
   100  	return byte((uint32(partitionOffset) + txnIndex) % 256)
   101  }
   102  
   103  func NewUUIDGenerator(
   104  	tracer tracing.TracerSpan,
   105  	log zerolog.Logger,
   106  	meter Meter,
   107  	txnState state.NestedTransactionPreparer,
   108  	blockHeader *flow.Header,
   109  	txnIndex uint32,
   110  ) *uUIDGenerator {
   111  	return &uUIDGenerator{
   112  		tracer:      tracer,
   113  		log:         log,
   114  		meter:       meter,
   115  		txnState:    txnState,
   116  		blockHeader: blockHeader,
   117  		txnIndex:    txnIndex,
   118  		initialized: false,
   119  	}
   120  }
   121  
   122  // getCounter reads the uint64 value from the partitioned uuid register.
   123  func (generator *uUIDGenerator) getCounter() (uint64, error) {
   124  	stateBytes, err := generator.txnState.Get(generator.registerId)
   125  	if err != nil {
   126  		return 0, fmt.Errorf(
   127  			"cannot get uuid partition %d byte from state: %w",
   128  			generator.partition,
   129  			err)
   130  	}
   131  	bytes := slices.EnsureByteSliceSize(stateBytes, 8)
   132  
   133  	return binary.BigEndian.Uint64(bytes), nil
   134  }
   135  
   136  // setCounter sets a new uint56 value into the partitioned uuid register.
   137  func (generator *uUIDGenerator) setCounter(
   138  	value uint64,
   139  ) error {
   140  	if value > Uint56OverflowWarningThreshold {
   141  		if value > MaxUint56 {
   142  			return fmt.Errorf(
   143  				"uuid partition %d overflowed",
   144  				generator.partition)
   145  		}
   146  
   147  		generator.log.Warn().
   148  			Int("partition", int(generator.partition)).
   149  			Uint64("value", value).
   150  			Msg("uuid partition is running out of bits")
   151  	}
   152  
   153  	bytes := make([]byte, 8)
   154  	binary.BigEndian.PutUint64(bytes, value)
   155  	err := generator.txnState.Set(generator.registerId, bytes)
   156  	if err != nil {
   157  		return fmt.Errorf(
   158  			"cannot set uuid %d byte to state: %w",
   159  			generator.partition,
   160  			err)
   161  	}
   162  	return nil
   163  }
   164  
   165  func (generator *uUIDGenerator) maybeInitializePartition() {
   166  	if generator.initialized {
   167  		return
   168  	}
   169  	generator.initialized = true
   170  
   171  	// NOTE: block header is not set for scripts.  We'll just use partition 0 in
   172  	// this case.
   173  	if generator.blockHeader == nil {
   174  		generator.partition = 0
   175  	} else {
   176  		generator.partition = uuidPartition(
   177  			generator.blockHeader.ID(),
   178  			generator.txnIndex)
   179  	}
   180  
   181  	generator.registerId = flow.UUIDRegisterID(generator.partition)
   182  }
   183  
   184  // GenerateUUID generates a new uuid and persist the data changes into state
   185  func (generator *uUIDGenerator) GenerateUUID() (uint64, error) {
   186  	defer generator.tracer.StartExtensiveTracingChildSpan(
   187  		trace.FVMEnvGenerateUUID).End()
   188  
   189  	err := generator.meter.MeterComputation(
   190  		ComputationKindGenerateUUID,
   191  		1)
   192  	if err != nil {
   193  		return 0, fmt.Errorf("generate uuid failed: %w", err)
   194  	}
   195  
   196  	generator.maybeInitializePartition()
   197  
   198  	counter, err := generator.getCounter()
   199  	if err != nil {
   200  		return 0, fmt.Errorf("cannot generate UUID: %w", err)
   201  	}
   202  
   203  	err = generator.setCounter(counter + 1)
   204  	if err != nil {
   205  		return 0, fmt.Errorf("cannot generate UUID: %w", err)
   206  	}
   207  
   208  	// Since the partition counter only goes up to MaxUint56, we can use the
   209  	// assemble a UUID value with the partition (P) and the counter (C).
   210  	// Note: partition (P) is represented by the 6th byte
   211  	// (C7 C6) | P | (C5 C4 C3 C2 C1)
   212  	return ((counter & 0xFF_FF00_0000_0000) << 8) | (uint64(generator.partition) << 40) | (counter & 0xFF_FFFF_FFFF), nil
   213  
   214  }