Skip to content

Commit

Permalink
Merge pull request #72 from PascalCoinDev/master
Browse files Browse the repository at this point in the history
PIP-0044 - Induplicatable NFT
  • Loading branch information
PascalCoinDev committed Mar 14, 2023
2 parents 8f53f3e + 7271546 commit 7ff9a04
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@
- MANDATORY UPGRADE - HARD FORK ACTIVATION WILL OCCUR ON BLOCK **(pending)**
- Upgrade to Protocol 6 (Hard fork)
- Implementation of PIP-0043 (Update OP_RECOVER to recover only non used, not named PASA's) -> https://github.com/PascalCoin/PascalCoin/blob/master/PIP/PIP-0043.md
- Implementation of PIP-0044 (Induplicatable NFT) -> https://github.com/PascalCoin/PascalCoin/blob/master/PIP/PIP-0044.md
- Improvements on downloading Safebox (fresh installation)


Expand Down
64 changes: 64 additions & 0 deletions PIP/PIP-0044.md
@@ -0,0 +1,64 @@
<pre>
PIP: PIP-0044
Title: Induplicatable NFT
Type: Protocol
Impact: Hard-Fork
Author: Albert Molina <bpascalblockchain@gmail.com>
Copyright: Albert Molina, 2023 (All Rights Reserved)
Comments-URI: https://discord.gg/Scr8mcwnrC (Discord channel #pip-44)
Status: Proposed
Created: 2023-03-14
</pre>

## Summary

NFT (Non-fungible-token) is a well known item in the blockchain industry. It's currently based on store the item (usually a HASH of a information) in the blockchain as a Proof-of-ownership of the item.

This means that this HASH of the item is stored in a transaction included in a block, and what really is used for transfers (buy/sell transactions) is a reference of the transaction, so **there is no warranty/prevention that same NFT HASH is stored i other blocks/transactions**

A true NFT must be something that is impossible to be duplicated, so we present a way to store Induplicatable NFT on the blockchain because the HASH will live on the Safebox struct (that is a representation of the ledger balance of the blockchain information)

Also, thanks to Safebox current features, this Induplicatable NFT can be sold using same on-chain transactions mechanism without third party neither single point of failure (PIP-0002 - In-protocol PASA Exchange)

## Proposal

This PIP specifies how to use current Safebox struct and operations to store Induplicatable NFT on the PascalCoin blockchain

- Safebox PASA's has Account Names and Types as described on PIP-0004, that allows to store unique Account Names in the safebox

- Implementation of PIP-0004 limited Account Name to be Null or 3..64 characters long

- Implementation of PIP-0004 prevents first char to be a number ('0'..'9') to not confuse name as an account number

In order to use Account Name as a HASH, we must do one of proposals:

- Without protocol upgrade:

- **Option A**: Use a "encode"/"decode" function to convert first char in a numeric/non-numeric char like convert "`ghijklmnop`" as "`0123456789`" for first char, so hash `9a737f6e41c58935c535fe7b08426006f246986810c21deeb808cc564b8ecdca` will be encoded to `pa737f6e41c58935c535fe7b08426006f246986810c21deeb808cc564b8ecdca` (transform 9 -> p)

- With protocol upgrade (Hard fork):

- **Option B**: Start hash value with a suffix like "`nft_`" because first char cannot be an Hexadecimal numeric number, in this case the 64 characters length is a limitation because we cannot store 32 bytes hash plus suffix length in 64 chars

- **Option C**: Allows usage of numeric first char when name is a representation of a 32 bytes hexadecimal value


This PIP-0044 will implement **Option C** allowing first char as a numberic "0".."9" char when name contains a 32 bytes (64 chars) hexadecimal value, this will prevent to use first number as an account number caused to overflow

## Implementation

```
// Update ValidAccountName function introducing this exception:
...
if (new_name[0] in [Ord('0')..Ord('9')]) then
if (protocol_version>=CT_PROTOCOL_6) and
(length(new_name)=64) and
(IsHexadecimal(new_name))
then continue
else Error('Invalid numeric first char on a non-hash hexadecimal 32 bytes representation');
end;
```

## Backwards Compatibility

This change is not backwards compatible and requires a hard-fork activation.
91 changes: 50 additions & 41 deletions src/core/UAccounts.pas
Expand Up @@ -58,6 +58,7 @@ interface
class procedure GetRewardDistributionForNewBlock(const OperationBlock : TOperationBlock; out acc_0_miner_reward, acc_4_dev_reward : Int64; out acc_4_for_dev : Boolean);
class Function CalcSafeBoxHash(ABlocksHashBuffer : TBytesBuffer; protocol_version : Integer) : TRawBytes;
class Function AllowUseHardcodedRandomHashTable(const AHardcodedFileName : String; const AHardcodedSha256Value : TRawBytes) : Boolean;
class function IsValidAccountName(protocol_version : Integer; const new_name : TRawBytes; var errors : String) : Boolean;
end;

TAccount_Helper = record helper for TAccount
Expand Down Expand Up @@ -290,7 +291,6 @@ TProgressNotifyManyHelper = record helper for TProgressNotifyMany
Procedure SaveSafeBoxToAStream(Stream : TStream; FromBlock, ToBlock : Cardinal);
class Function CopySafeBoxStream(Source,Dest : TStream; FromBlock, ToBlock : Cardinal; var errors : String) : Boolean;
class Function ConcatSafeBoxStream(Source1, Source2, Dest : TStream; var errors : String) : Boolean;
class function ValidAccountName(const new_name : TRawBytes; var errors : String) : Boolean;

Function IsValidNewOperationsBlock(Const newOperationBlock : TOperationBlock; checkSafeBoxHash, checkValidOperationsBlock : Boolean; var errors : String) : Boolean;
class Function IsValidOperationBlock(Const newOperationBlock : TOperationBlock; var errors : String) : Boolean;
Expand Down Expand Up @@ -900,6 +900,53 @@ class procedure TPascalCoinProtocol.CalcProofOfWork(const operationBlock: TOpera
end;
end;

class function TPascalCoinProtocol.IsValidAccountName(protocol_version: Integer; const new_name: TRawBytes; var errors: String): Boolean;
{ Note:
This function is case senstive, and only lower case chars are valid.
Execute a LowerCase() prior to call this function!
}
Const CT_PascalCoin_Base64_Charset : RawByteString = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-+{}[]\_:"|<>,.?/~';
// First char can't start with a number
CT_PascalCoin_FirstChar_Charset : RawByteString = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()-+{}[]\_:"|<>,.?/~';
CT_PascalCoin_name_min_length = 3;
CT_PascalCoin_name_max_length = 64;
var i,j : Integer;
Lraw : TRawBytes;
begin
Result := False; errors := '';
if (length(new_name)<CT_PascalCoin_name_min_length) Or (length(new_name)>CT_PascalCoin_name_max_length) then begin
errors := 'Invalid length:'+IntToStr(Length(new_name))+' (valid from '+Inttostr(CT_PascalCoin_name_max_length)+' to '+IntToStr(CT_PascalCoin_name_max_length)+')';
Exit;
end;
for i:=Low(new_name) to High(new_name) do begin
if (i=Low(new_name)) then begin
j:=Low(CT_PascalCoin_FirstChar_Charset);
// First char can't start with a number
While (j<=High(CT_PascalCoin_FirstChar_Charset)) and (Ord(new_name[i])<>Ord(CT_PascalCoin_FirstChar_Charset[j])) do inc(j);
if (j>High(CT_PascalCoin_FirstChar_Charset)) then begin
// Allow Account Name as an hexadecimal value for a hash on Protocol V6 as proposed on PIP-0044
if Not (
(protocol_version>=CT_PROTOCOL_6) and
(new_name[i] in [Ord('0')..Ord('9')]) and
(length(new_name)=64) and
(TCrypto.HexaToRaw(new_name.ToString,Lraw))
) then begin
errors := 'Invalid char '+Char(new_name[i])+' at first pos';
Exit; // Not found
end;
end;
end else begin
j:=Low(CT_PascalCoin_Base64_Charset);
While (j<=High(CT_PascalCoin_Base64_Charset)) and (Ord(new_name[i])<>Ord(CT_PascalCoin_Base64_Charset[j])) do inc(j);
if j>High(CT_PascalCoin_Base64_Charset) then begin
errors := 'Invalid char '+Char(new_name[i])+' at pos '+IntToStr(i);
Exit; // Not found
end;
end;
end;
Result := True;
end;

class function TPascalCoinProtocol.IsValidMinerBlockPayload(const newBlockPayload: TRawBytes): Boolean;
var i : Integer;
begin
Expand Down Expand Up @@ -3627,7 +3674,7 @@ function TPCSafeBox.LoadSafeBoxChunkFromStream(Stream : TStream; checkAll : Bool
//
// check valid
If (Length(LBlock.accounts[iacc].name)>0) then begin
if Not TPCSafeBox.ValidAccountName(LBlock.accounts[iacc].name,aux_errors) then begin
if Not TPascalCoinProtocol.IsValidAccountName(CurrentProtocol,LBlock.accounts[iacc].name,aux_errors) then begin
errors := errors + ' > Invalid name "'+LBlock.accounts[iacc].name.ToPrintable+'": '+aux_errors;
Exit;
end;
Expand Down Expand Up @@ -4296,44 +4343,6 @@ class function TPCSafeBox.ConcatSafeBoxStream(Source1, Source2, Dest: TStream; v
end;


class function TPCSafeBox.ValidAccountName(const new_name: TRawBytes; var errors : String): Boolean;
{ Note:
This function is case senstive, and only lower case chars are valid.
Execute a LowerCase() prior to call this function!
}
Const CT_PascalCoin_Base64_Charset : RawByteString = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-+{}[]\_:"|<>,.?/~';
// First char can't start with a number
CT_PascalCoin_FirstChar_Charset : RawByteString = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()-+{}[]\_:"|<>,.?/~';
CT_PascalCoin_name_min_length = 3;
CT_PascalCoin_name_max_length = 64;
var i,j : Integer;
begin
Result := False; errors := '';
if (length(new_name)<CT_PascalCoin_name_min_length) Or (length(new_name)>CT_PascalCoin_name_max_length) then begin
errors := 'Invalid length:'+IntToStr(Length(new_name))+' (valid from '+Inttostr(CT_PascalCoin_name_max_length)+' to '+IntToStr(CT_PascalCoin_name_max_length)+')';
Exit;
end;
for i:=Low(new_name) to High(new_name) do begin
if (i=Low(new_name)) then begin
j:=Low(CT_PascalCoin_FirstChar_Charset);
// First char can't start with a number
While (j<=High(CT_PascalCoin_FirstChar_Charset)) and (Ord(new_name[i])<>Ord(CT_PascalCoin_FirstChar_Charset[j])) do inc(j);
if j>High(CT_PascalCoin_FirstChar_Charset) then begin
errors := 'Invalid char '+Char(new_name[i])+' at first pos';
Exit; // Not found
end;
end else begin
j:=Low(CT_PascalCoin_Base64_Charset);
While (j<=High(CT_PascalCoin_Base64_Charset)) and (Ord(new_name[i])<>Ord(CT_PascalCoin_Base64_Charset[j])) do inc(j);
if j>High(CT_PascalCoin_Base64_Charset) then begin
errors := 'Invalid char '+Char(new_name[i])+' at pos '+IntToStr(i);
Exit; // Not found
end;
end;
end;
Result := True;
end;

var _initialSafeboxHash : TRawBytes = Nil;

class function TPCSafeBox.InitialSafeboxHash: TRawBytes;
Expand Down Expand Up @@ -5643,7 +5652,7 @@ function TPCSafeBoxTransaction.UpdateAccountInfo(previous : TAccountPreviousBloc
if (Not TBaseType.Equals(newName,P_target^.name)) then begin
// NEW NAME CHANGE CHECK:
if Length(newName)>0 then begin
If Not TPCSafeBox.ValidAccountName(newName,errors) then begin
If Not TPascalCoinProtocol.IsValidAccountName(FreezedSafeBox.CurrentProtocol,newName,errors) then begin
errors := 'Invalid account name "'+newName.ToPrintable+'" length:'+IntToStr(length(newName))+': '+errors;
Exit;
end;
Expand Down
2 changes: 1 addition & 1 deletion src/core/UOpTransaction.pas
Expand Up @@ -550,7 +550,7 @@ function TOpChangeAccountInfo.DoOperation(AccountPreviousUpdatedBlock : TAccount
end;
If (account_name in FData.changes_type) then begin
If (Length(FData.new_name)>0) then begin
If Not TPCSafeBox.ValidAccountName(FData.new_name,errors) then Exit;
If Not TPascalCoinProtocol.IsValidAccountName(AccountTransaction.FreezedSafeBox.CurrentProtocol,FData.new_name,errors) then Exit;
end;
end else begin
If (Length(FData.new_name)>0) then begin
Expand Down
2 changes: 1 addition & 1 deletion src/core/UPCRPCFindAccounts.pas
Expand Up @@ -206,7 +206,7 @@ class function TRPCFindAccounts.FindAccounts(const ASender: TRPCProcess;
// Validate Parameters
if (Length(LAccountName)>0) And (LSearchByNameType = st_exact) then begin
LRaw.FromString( LAccountName );
if not ASender.Node.Bank.SafeBox.ValidAccountName(LRaw, LErrors) then begin
if not TPascalCoinProtocol.IsValidAccountName(CT_BUILD_PROTOCOL, LRaw, LErrors) then begin
AErrorNum := CT_RPC_ErrNum_InvalidAccountName;
AErrorDesc := LErrors;
exit;
Expand Down
2 changes: 1 addition & 1 deletion src/core/UTxMultiOperation.pas
Expand Up @@ -680,7 +680,7 @@ function TOpMultiOperation.DoOperation(AccountPreviousUpdatedBlock : TAccountPre
end;
If (account_name in chi.changes_type) then begin
If (Length(chi.New_Name)>0) then begin
If Not TPCSafeBox.ValidAccountName(chi.New_Name,errors) then Exit;
If Not TPascalCoinProtocol.IsValidAccountName(AccountTransaction.FreezedSafeBox.CurrentProtocol,chi.New_Name,errors) then Exit;
// Check name not found!
j := AccountTransaction.FindAccountByNameInTransaction(chi.New_Name,newNameWasAdded, newNameWasDeleted);
If (j>=0) Or (newNameWasAdded) or (newNameWasDeleted) then begin
Expand Down
2 changes: 1 addition & 1 deletion src/gui-classic/UFRMOperation.pas
Expand Up @@ -1050,7 +1050,7 @@ function TFRMOperation.UpdateOpChangeInfo(const TargetAccount: TAccount; var Sig
If Not TBaseType.Equals(newName,TargetAccount.name) then begin
changeName:=True;
If Length(newName)>0 then begin
if (Not TPCSafeBox.ValidAccountName(newName,errors)) then begin
if (Not TPascalCoinProtocol.IsValidAccountName(TNode.Node.Bank.SafeBox.CurrentProtocol,newName,errors)) then begin
errors := '"'+newName.ToPrintable+'" is not a valid name: '+errors;
Exit;
end;
Expand Down
2 changes: 1 addition & 1 deletion src/gui-classic/UFRMOperationsExplorer.pas
Expand Up @@ -409,7 +409,7 @@ procedure TFRMOperationsExplorer.bbAddOpChangeClick(Sender: TObject);
aux := new_name;
If Not InputQuery(Caption,Format('New name for account %s:',[TAccountComp.AccountNumberToAccountTxtNumber(nAccount)]),aux) then Break;
aux := LowerCase(aux);
Until (aux='') Or (TPCSafeBox.ValidAccountName(TEncoding.ANSI.GetBytes(aux),errors));
Until (aux='') Or (TPascalCoinProtocol.IsValidAccountName(FSourceNode.Bank.SafeBox.CurrentProtocol,TEncoding.ANSI.GetBytes(aux),errors));
new_name := aux;

aux := IntToStr(new_type);
Expand Down

0 comments on commit 7ff9a04

Please sign in to comment.