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 }