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 }