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