github.com/hyperledger/burrow@v0.34.5-0.20220512172541-77f09336001d/execution/contexts/name_context.go (about)

     1  package contexts
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"regexp"
     7  
     8  	"github.com/hyperledger/burrow/acm/acmstate"
     9  	"github.com/hyperledger/burrow/execution/engine"
    10  	"github.com/hyperledger/burrow/execution/errors"
    11  	"github.com/hyperledger/burrow/execution/exec"
    12  	"github.com/hyperledger/burrow/execution/names"
    13  	"github.com/hyperledger/burrow/logging"
    14  	"github.com/hyperledger/burrow/txs/payload"
    15  )
    16  
    17  // Name should be file system like
    18  // Data should be anything permitted in JSON
    19  var regexpAlphaNum = regexp.MustCompile("^[a-zA-Z0-9._/-@]*$")
    20  var regexpJSON = regexp.MustCompile(`^[a-zA-Z0-9_/ \-+"':,\n\t.{}()\[\]]*$`)
    21  
    22  type NameContext struct {
    23  	Blockchain engine.Blockchain
    24  	State      acmstate.ReaderWriter
    25  	NameReg    names.ReaderWriter
    26  	Logger     *logging.Logger
    27  	tx         *payload.NameTx
    28  }
    29  
    30  func (ctx *NameContext) Execute(txe *exec.TxExecution, p payload.Payload) error {
    31  	var ok bool
    32  	ctx.tx, ok = p.(*payload.NameTx)
    33  	if !ok {
    34  		return fmt.Errorf("payload must be NameTx, but is: %v", txe.Envelope.Tx.Payload)
    35  	}
    36  	// Validate input
    37  	inAcc, err := ctx.State.GetAccount(ctx.tx.Input.Address)
    38  	if err != nil {
    39  		return err
    40  	}
    41  	if inAcc == nil {
    42  		ctx.Logger.InfoMsg("Cannot find input account",
    43  			"tx_input", ctx.tx.Input)
    44  		return errors.Codes.InvalidAddress
    45  	}
    46  	// check permission
    47  	if !hasNamePermission(ctx.State, inAcc, ctx.Logger) {
    48  		return fmt.Errorf("account %s does not have Name permission", ctx.tx.Input.Address)
    49  	}
    50  	if ctx.tx.Input.Amount < ctx.tx.Fee {
    51  		ctx.Logger.InfoMsg("Sender did not send enough to cover the fee",
    52  			"tx_input", ctx.tx.Input)
    53  		return errors.Codes.InsufficientFunds
    54  	}
    55  
    56  	// validate the input strings
    57  	if err := validateStrings(ctx.tx); err != nil {
    58  		return err
    59  	}
    60  
    61  	value := ctx.tx.Input.Amount - ctx.tx.Fee
    62  
    63  	// let's say cost of a name for one block is len(data) + 32
    64  	costPerBlock := names.NameCostPerBlock(names.NameBaseCost(ctx.tx.Name, ctx.tx.Data))
    65  	expiresIn := value / uint64(costPerBlock)
    66  	lastBlockHeight := ctx.Blockchain.LastBlockHeight()
    67  
    68  	ctx.Logger.TraceMsg("New NameTx",
    69  		"value", value,
    70  		"cost_per_block", costPerBlock,
    71  		"expires_in", expiresIn,
    72  		"last_block_height", lastBlockHeight)
    73  
    74  	// check if the name exists
    75  	entry, err := ctx.NameReg.GetName(ctx.tx.Name)
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	if entry != nil {
    81  		var expired bool
    82  
    83  		// if the entry already exists, and hasn't expired, we must be owner
    84  		if entry.Expires > lastBlockHeight {
    85  			// ensure we are owner
    86  			if entry.Owner != ctx.tx.Input.Address {
    87  				return fmt.Errorf("permission denied: sender %s is trying to update a name (%s) for "+
    88  					"which they are not an owner", ctx.tx.Input.Address, ctx.tx.Name)
    89  			}
    90  		} else {
    91  			expired = true
    92  		}
    93  
    94  		// no value and empty data means delete the entry
    95  		if value == 0 && len(ctx.tx.Data) == 0 {
    96  			// maybe we reward you for telling us we can delete this crap
    97  			// (owners if not expired, anyone if expired)
    98  			ctx.Logger.TraceMsg("Removing NameReg entry (no value and empty data in tx requests this)",
    99  				"name", entry.Name)
   100  			err := ctx.NameReg.RemoveName(entry.Name)
   101  			if err != nil {
   102  				return err
   103  			}
   104  		} else {
   105  			// update the entry by bumping the expiry
   106  			// and changing the data
   107  			if expired {
   108  				if expiresIn < names.MinNameRegistrationPeriod {
   109  					return fmt.Errorf("names must be registered for at least %d blocks", names.MinNameRegistrationPeriod)
   110  				}
   111  				entry.Expires = lastBlockHeight + expiresIn
   112  				entry.Owner = ctx.tx.Input.Address
   113  				ctx.Logger.TraceMsg("An old NameReg entry has expired and been reclaimed",
   114  					"name", entry.Name,
   115  					"expires_in", expiresIn,
   116  					"owner", entry.Owner)
   117  			} else {
   118  				// since the size of the data may have changed
   119  				// we use the total amount of "credit"
   120  				oldCredit := (entry.Expires - lastBlockHeight) * names.NameBaseCost(entry.Name, entry.Data)
   121  				credit := oldCredit + value
   122  				expiresIn = uint64(credit / costPerBlock)
   123  				if expiresIn < names.MinNameRegistrationPeriod {
   124  					return fmt.Errorf("names must be registered for at least %d blocks", names.MinNameRegistrationPeriod)
   125  				}
   126  				entry.Expires = lastBlockHeight + expiresIn
   127  				ctx.Logger.TraceMsg("Updated NameReg entry",
   128  					"name", entry.Name,
   129  					"expires_in", expiresIn,
   130  					"old_credit", oldCredit,
   131  					"value", value,
   132  					"credit", credit)
   133  			}
   134  			entry.Data = ctx.tx.Data
   135  			err := ctx.NameReg.UpdateName(entry)
   136  			if err != nil {
   137  				return err
   138  			}
   139  		}
   140  	} else {
   141  		if expiresIn < names.MinNameRegistrationPeriod {
   142  			return fmt.Errorf("names must be registered for at least %d blocks", names.MinNameRegistrationPeriod)
   143  		}
   144  		// entry does not exist, so create it
   145  		entry = &names.Entry{
   146  			Name:    ctx.tx.Name,
   147  			Owner:   ctx.tx.Input.Address,
   148  			Data:    ctx.tx.Data,
   149  			Expires: lastBlockHeight + expiresIn,
   150  		}
   151  		ctx.Logger.TraceMsg("Creating NameReg entry",
   152  			"name", entry.Name,
   153  			"expires_in", expiresIn)
   154  		err := ctx.NameReg.UpdateName(entry)
   155  		if err != nil {
   156  			return err
   157  		}
   158  	}
   159  
   160  	// TODO: something with the value sent?
   161  
   162  	// Good!
   163  	ctx.Logger.TraceMsg("Incrementing sequence number for NameTx",
   164  		"tag", "sequence",
   165  		"account", inAcc.Address,
   166  		"old_sequence", inAcc.Sequence,
   167  		"new_sequence", inAcc.Sequence+1)
   168  
   169  	err = inAcc.SubtractFromBalance(value)
   170  	if err != nil {
   171  		return errors.Errorf(errors.Codes.InsufficientFunds,
   172  			"Input account does not have sufficient balance to cover input amount: %v", ctx.tx.Input)
   173  	}
   174  	err = ctx.State.UpdateAccount(inAcc)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	// TODO: maybe we want to take funds on error and allow txs in that don't do anything?
   180  
   181  	txe.Input(ctx.tx.Input.Address, nil)
   182  	txe.Name(entry)
   183  	return nil
   184  }
   185  
   186  func validateStrings(tx *payload.NameTx) error {
   187  	if len(tx.Name) == 0 {
   188  		return errors.Errorf(errors.Codes.InvalidString, "name must not be empty")
   189  	}
   190  	if len(tx.Name) > names.MaxNameLength {
   191  		return errors.Errorf(errors.Codes.InvalidString, "Name is too long. Max %d bytes", names.MaxNameLength)
   192  	}
   193  	if len(tx.Data) > names.MaxDataLength {
   194  		return errors.Errorf(errors.Codes.InvalidString, "Data is too long. Max %d bytes", names.MaxDataLength)
   195  	}
   196  
   197  	if !validateNameRegEntryName(tx.Name) {
   198  		return errors.Errorf(errors.Codes.InvalidString,
   199  			"Invalid characters found in NameTx.Name (%s). Only alphanumeric, underscores, dashes, forward slashes, and @ are allowed", tx.Name)
   200  	}
   201  
   202  	if !validateNameRegEntryData(tx.Data) {
   203  		return errors.Errorf(errors.Codes.InvalidString,
   204  			"Invalid characters found in NameTx.Data (%s). Only the kind of things found in a JSON file are allowed", tx.Data)
   205  	}
   206  
   207  	return nil
   208  }
   209  
   210  // filter strings
   211  func validateNameRegEntryName(name string) bool {
   212  	return regexpAlphaNum.Match([]byte(name))
   213  }
   214  
   215  func validateNameRegEntryData(data string) bool {
   216  	return regexpJSON.Match([]byte(data))
   217  }