Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

suggestion for address type #11

Merged
merged 13 commits into from
Jul 29, 2021

Conversation

tweakch
Copy link
Collaborator

@tweakch tweakch commented Jul 27, 2021

I saw #5 and have a suggestions for Address Type.

I added some comments because I think it's important for new people to have some context.

I'd do more if this kind of contribution is welcome.

@nothingalike
Copy link
Member

I definitely like where your heads at. This is my main motivation for Issue #5.

return (_bytes[0] >> 4) switch
{
0x01 => AddressType.Base, //@TODO: derive all AddressTypes
0x06 => AddressType.Enterprise,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe Reward type is the same as Enterprise. I haven't done a lot of testing around this, but thats what i came up with from reading the Rust code

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after reviewing the AddressService, i believe im wrong

CardanoSharp.Wallet/Models/Addresses/Address.cs Outdated Show resolved Hide resolved
Comment on lines +11 to +12
private byte[] _bytes;
private string _address;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There have been times when having access to both of would have been very useful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ToString() gives you the Address
GetBytes() gives you the Bytes

the rationale for using methods instead of properties is that properties don't support formatting or endianness.... maybe that is a feature for the future...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that we need to do is make a string encoder so people can query by hash. https://github.com/CardanoSharp/cardanosharp-demo/blob/CreateTransactionExplorer/src/Infastructure/Persistence/CardanoEFCore/Queries.cs If you look here at line 70, I did a raw SQL query from the passed in hash. The Postgres hash type is a 32 byte array I believe, and when trying to convert a string to the proper Postgres type it was a pain that I couldn't get. So I think it would be super useful to have an extenstion method that could turn strings into that postgres types.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ill dive into how the tx id is created. I know the cardano cli can tell you want it will be before you even submit so im sure we can figure it out. Im sure its something along the lines of byte[] -> blake hash

@nothingalike
Copy link
Member

I feel like a type like this could replace the AddressService but we would need to handle the main method

public string GetAddress(byte[] payment, byte[] stake, NetworkType networkType, AddressType addressType)

@tweakch
Copy link
Collaborator Author

tweakch commented Jul 28, 2021

I feel like a type like this could replace the AddressService but we would need to handle the main method

public string GetAddress(byte[] payment, byte[] stake, NetworkType networkType, AddressType addressType)

In my opinion AddressService has legitimate use in providing an API to create Address from RootKey. I think the types should serialize and deserialize to string and byte[] and help users work with the services.

I suggest a repurposing of services for use in DI containers.

interface IAddressService : IDisposable
{
    // default Network
    NetworkType NetworkType { get; }

    // legacy support
    string CreateAddress(byte[] payment, byte[] stake, AddressType addressType);
    string CreateAddress(byte[] payment, byte[] stake, NetworkType networkType, AddressType addressType);

    // new methods
    AddressBase CreateAddress(RootKey rootKey, AddressType addressType);
    AddressBase CreateAddress(RootKey rootKey, AddressType addressType, NetworkType networkType);

    PaymentAddress CreatePaymentAddress(RootKey rootKey);
    PaymentAddress CreatePaymentAddress(RootKey rootKey, NetworkType networkType);

    ICollection<PaymentAddress> CreatePaymentAddresses(int count, RootKey rootKey);
    ICollection<PaymentAddress> CreatePaymentAddresses(int count, RootKey rootKey, NetworkType networkType);
}

To use it with DI containers

public void ConfigureServices(IServiceCollection services) 
{
    services.Configure<WalletConfig>(Configuration.GetSection("Wallet"));
    services.AddSingleton<IAddressService, AddressService>();
}

And provide the ability to configure services through appsetting.json

{
  "Wallet": {
    "Network" : "Testnet",
    "RootDirectory" : "/some/path/on/localhost",
    "KeysDirectory" : "keys",
  }
}
public class AddressService : IAddressService
{
    public AddressService(WalletConfig options)
    {
        NetworkType = options.Network;
        //....
    }
}

@nothingalike
Copy link
Member

I feel like a type like this could replace the AddressService but we would need to handle the main method

public string GetAddress(byte[] payment, byte[] stake, NetworkType networkType, AddressType addressType)

In my opinion AddressService has legitimate use in providing an API to create Address from RootKey. I think the types should serialize and deserialize to string and byte[] and help users work with the services.

Fair point. I don't know if I'm sold on a type so opinionated as RootKey but I definitely had plans to support PrivateKey and PublicKey

My current thoughts around Types:

public class PrivateKey
{
	public byte[] Key { get; set; }
	public byte[] ChainCode { get; set; }

	//one option, could be redone with extensions
	private PublicKey _publicKey { get; set; }
	public PublicKey PublicKey {
		get {
			if(_publicKey == null) {
	            _publicKey = new PublicKey(Ed25519.GetPublicKey(Key), ChainCode);
			}
			return _publicKey;
		}
	}
}

public class PublicKey
{
	public PublicKey(byte[] key, byte[] chaincode)
	{
	    Key = key;
	    ChainCode = chaincode;
	}
	public byte[] Key { get; set; }
	public byte[] ChainCode { get; set; }
}

I could definitely see some very useful bit around having child types such as RootKey, PurposeKey, AccountKey, ChangeKey, and IndexKey. This would provide full derivation context for the key you are referring too. They might be more inclusive of the PrivateKey and PublicKey while also allowing us to correctly generate the Bech32 addresses with correct prefix declared in CIP005.

interface IAddressService : IDisposable
{
    // default Network
    NetworkType NetworkType { get; }

    // legacy support
    string CreateAddress(byte[] payment, byte[] stake, AddressType addressType);
    string CreateAddress(byte[] payment, byte[] stake, NetworkType networkType, AddressType addressType);

    // new methods
    AddressBase CreateAddress(RootKey rootKey, AddressType addressType);
    AddressBase CreateAddress(RootKey rootKey, AddressType addressType, NetworkType networkType);

    PaymentAddress CreatePaymentAddress(RootKey rootKey);
    PaymentAddress CreatePaymentAddress(RootKey rootKey, NetworkType networkType);

    ICollection<PaymentAddress> CreatePaymentAddresses(int count, RootKey rootKey);
    ICollection<PaymentAddress> CreatePaymentAddresses(int count, RootKey rootKey, NetworkType networkType);
}

Honestly I would just updated the "legacy support" to return the AddressBase type. Inputs would be updated as well to support PublicKey types instead of the byte[]

PaymentAddress CreatePaymentAddress(RootKey rootKey);
PaymentAddress CreatePaymentAddress(RootKey rootKey, NetworkType networkType);

These more opinionated methods are very "developer" friendly. This approach was more or less my thoughts for the empty WalletService. The WalletService was to bridge the gap of KeyService and AddressService to the more common use cases, such as CreatePaymentAddress, CreateStakeAddress etc.

To use it with DI containers

public void ConfigureServices(IServiceCollection services) 
{
    services.Configure<WalletConfig>(Configuration.GetSection("Wallet"));
    services.AddSingleton<IAddressService, AddressService>();
}

And provide the ability to configure services through appsetting.json

{
  "Wallet": {
    "Network" : "Testnet",
    "RootDirectory" : "/some/path/on/localhost",
    "KeysDirectory" : "keys",
  }
}
public class AddressService : IAddressService
{
    public AddressService(WalletConfig options)
    {
        NetworkType = options.Network;
        //....
    }
}

I like the idea of DI support but I think the WalletConfig approach is a bit out of scope for this library. I don't think the library should worry about managing wallets specifically but rather focus on the operations against them. I can see that being something an application would do to load wallets, then use this library to act on those wallets. Or maybe a different child library called CardanoSharp.Wallet.Manager (or something along those lines) that acts more like cardano-wallet where it has context of wallets and can leverage CardanoSharp.Wallet

@Daniel-SchaeferJ @safestak-keith If either of you two have an opinion on the matter, please chime in.

@tweakch
Copy link
Collaborator Author

tweakch commented Jul 28, 2021

I could definitely see some very useful bit around having child types such as RootKey, PurposeKey, AccountKey, ChangeKey, and IndexKey. This would provide full derivation context for the key you are referring too. They might be more inclusive of the PrivateKey and PublicKey while also allowing us to correctly generate the Bech32 addresses with correct prefix declared in CIP005.

...and, furthers the understanding for devs new to the matter.

PaymentAddress CreatePaymentAddress(RootKey rootKey);
PaymentAddress CreatePaymentAddress(RootKey rootKey, NetworkType networkType);

These more opinionated methods are very "developer" friendly. This approach was more or less my thoughts for the empty WalletService. The WalletService was to bridge the gap of KeyService and AddressService to the more common use cases, such as CreatePaymentAddress, CreateStakeAddress etc.

Agreed, IWalletService is a better place for these methods.

I like the idea of DI support but I think the WalletConfig approach is a bit out of scope for this library.... I don't think the library should worry about managing wallets specifically but rather focus on the operations against them. I can see that being something an application would do to load wallets, then use this library to act on those wallets. Or maybe a different child library called CardanoSharp.Wallet.Manager (or something along those lines) that acts more like cardano-wallet where it has context of wallets and can leverage CardanoSharp.Wallet

I like this separation, makes total sense. And you are right, WalletConfig is not the correct approach and was a bad example for what I mean. The point I am trying make is that a lot of projects that will use this library also use DI containers . So the library should adhere to IoC principles and make it easy to use in DI environments. Plus we have the added benefit of very modular and testable units and are able to write smaller tests, which again, helps in understanding what the components of the library are doing.

tweakch and others added 2 commits July 28, 2021 19:51
@nothingalike
Copy link
Member

...and, furthers the understanding for devs new to the matter.

yeah I think after the conversation I'm on board with this opinionated types.

...the library should adhere to IoC principles and make it easy to use in DI environments.

100% agree. main reason why every service has an interface accompanying it.


Alright so I'm down to start the type conversion process with Address. I updated all instances of address in the tests to use the model instead of the basic string or byte[] instances.

One thing I'm thinking is just turning the Bech32 class into a static class. seems unnecessary to new something up just to encode or decode something. thoughts?

@tweakch
Copy link
Collaborator Author

tweakch commented Jul 29, 2021

One thing I'm thinking is just turning the Bech32 class into a static class. seems unnecessary to new something up just to encode or decode something. thoughts?

I agree.

tweakch added 2 commits July 29, 2021 14:00
* move encoding and decoding into partials to avoid large class
@nothingalike
Copy link
Member

haha you beat me to the Bech32 static change. I was about to post a PR and wanted to reference the comment here, but say you already committed it

Copy link
Collaborator Author

@tweakch tweakch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought that might happen.... sorry. ^^
I also split encoding and decoding into partials to avoid large class and added a Bech32Test

CardanoSharp.Wallet.Test/Bech32Tests.cs Show resolved Hide resolved
Comment on lines 34 to 75
static byte[] ConvertBitsFast(ReadOnlySpan<byte> data, int fromBits, int toBits, bool pad = true)
{
// TODO: Optimize Looping
// We can use a method similar to BIP39 here to avoid the nested loop, usage of List, increase the speed,
// and shorten this function to 3 lines.
// Or convert to ulong[], loop through it (3 times) take 5 bits at a time or 8 bits at a time...
int acc = 0;
int bits = 0;
int maxv = (1 << toBits) - 1;
int maxacc = (1 << (fromBits + toBits - 1)) - 1;

List<byte> result = new List<byte>();
foreach (var b in data)
{
// Speed doesn't matter for this class but we can skip this check for 8 to 5 conversion.
if ((b >> fromBits) > 0)
{
return null;
}
acc = ((acc << fromBits) | b) & maxacc;
bits += fromBits;
while (bits >= toBits)
{
bits -= toBits;
result.Add((byte)((acc >> bits) & maxv));
}
}
if (pad)
{
if (bits > 0)
{
result.Add((byte)((acc << (toBits - bits)) & maxv));
}
}
else if (bits >= fromBits || (byte)((acc << (toBits - bits)) & maxv) != 0)
{
return null;
}
return result.ToArray();

}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was this copied from the Bech32 class? if so, should we just make it public so we dont duplicate

Copy link
Collaborator Author

@tweakch tweakch Jul 29, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes i copied it.
I just put it there to play around with it (i.e. ReadOnlySpan<byte>) and saw the comment and added #15
but we can remove it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets keep for now. after some experimenting/testing we can removed and update main method

@nothingalike
Copy link
Member

Alright so unless you have any more ideas for this PR, I think we can look to merge it. I was speaking with @safestak-keith about the base types vs opinionated types, and I think we should move forward with base types then make a pass to add the opinionated types. you two made the point that it could help the developer understand the key/address derivations and keep them from implementing faulty processes. I agree with this but first want to establish the base types.

@tweakch
Copy link
Collaborator Author

tweakch commented Jul 29, 2021

Alright so unless you have any more ideas for this PR, I think we can look to merge it. I was speaking with @safestak-keith about the base types vs opinionated types, and I think we should move forward with base types then make a pass to add the opinionated types. you two made the point that it could help the developer understand the key/address derivations and keep them from implementing faulty processes. I agree with this but first want to establish the base types.

Just one more idea that I want to raise.
when testing the BIP0173 test vectors with the current Bech32 class there are some failed tests. I will remove them for now and open a new PR on the main repo (thanks for the invite btw) and we can look at this there

@tweakch tweakch marked this pull request as ready for review July 29, 2021 14:00
@nothingalike
Copy link
Member

Just one more idea that I want to raise.
when testing the BIP0173 test vectors with the current Bech32 class there are some failed tests. I will remove them for now and open a new PR on the main repo (thanks for the invite btw) and we can look at this there

yeah that sounds good to me

@nothingalike nothingalike merged commit 04c90b8 into CardanoSharp:main Jul 29, 2021
Comment on lines -263 to -266
if (data == null || data.Length == 0)
throw new ArgumentNullException(nameof(data), "Data can not be null or empty.");
if (!IsValidHrp(hrp))
throw new FormatException("Invalid HRP.");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to think about Custom Exceptions. There will be numerous time in our application that a witness, metadata, etc could be null so making some custom domain logic via Exceptions might work well for us

Comment on lines +20 to +27
if (b32Arr.Length < CheckSumSize)
{
throw new FormatException("Invalid data length.");
}
if (!VerifyChecksum(hrp, b32Arr))
{
throw new FormatException("Invalid checksum.");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should really make things like this into a Try/Parse pattern.
https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/exceptions-and-performance

Copy link
Member

@Daniel-SchaeferJ Daniel-SchaeferJ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall great work! I have submitted my few thoughts throughout the changes via single line comments. Thank you for your work!!!!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants