Skip to content

Commit

Permalink
feat: catalyst cardano witnesses management (#490)
Browse files Browse the repository at this point in the history
* feat: add shelley address & cardano serialization lib

* fix: address to string

* style: disable cspell for cardano addresses

* fix: address hashcode

* refactor: make coin an extension type, rename magicId to id

* style: use expression syntax

* style: use expressions

* style: typo

* feat: add comparison operators

* feat: add greater or equal, less or equal operators

* feat: add utils for transaction hash

* feat: add transaction serialization

* feat: add algorithm for fee calculation

* docs: improve method

* style: add class modifier to prevent unintented overrides

* feat: catalyst cardano serialization - transactions (#484)

* fix: address hashcode

* feat: add utils for transaction hash

* feat: add transaction serialization

* feat: add algorithm for fee calculation

* docs: improve method

* style: add class modifier to prevent unintented overrides

---------

Co-authored-by: Dominik Toton <[email protected]>

* feat: add witnesses management

* feat: add witnesses

---------

Co-authored-by: Dominik Toton <[email protected]>
  • Loading branch information
dtscalac and Dominik Toton committed May 15, 2024
1 parent f066a26 commit dfbb9f8
Show file tree
Hide file tree
Showing 15 changed files with 278 additions and 14 deletions.
1 change: 1 addition & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,4 @@ lovelace
lovelaces
pinenacl
dtscalac
vkeys
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export 'src/fees.dart';
export 'src/hashes.dart';
export 'src/transaction.dart';
export 'src/types.dart';
export 'src/witness.dart';
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'package:catalyst_cardano_serialization/src/types.dart';

/// Exception thrown when the transaction exceeds the allowed maximum size.
final class MaxTxSizeExceededException implements Exception {
/// The maximum amount of bytes per transaction.
Expand All @@ -14,11 +16,32 @@ final class MaxTxSizeExceededException implements Exception {

@override
String toString() => 'MaxTxSizeExceededException('
'maxTxSize=$maxTxSize'
'maxTxSize:$maxTxSize'
', actualTxSize:$actualTxSize'
')';
}

/// Exception thrown when the transaction outputs exceed the inputs.
final class InsufficientUtxoBalanceException implements Exception {
/// The amount of [Coin] that user has.
final Coin actualAmount;

/// The amount of [Coin] that user wants to spend.
final Coin requiredAmount;

/// The default constructor for [InsufficientUtxoBalanceException].
const InsufficientUtxoBalanceException({
required this.actualAmount,
required this.requiredAmount,
});

@override
String toString() => 'InsufficientUtxoBalanceException('
'actualAmount:$actualAmount'
', requiredAmount:$requiredAmount'
')';
}

/// Exception thrown when building a transaction that doesn't specify the fee.
final class TxFeeNotSpecifiedException implements Exception {
/// The default constructor for [TxFeeNotSpecifiedException].
Expand Down Expand Up @@ -49,3 +72,19 @@ final class InvalidAddressException implements Exception {
@override
String toString() => 'InvalidAddressException: $message';
}

/// Exception thrown when the number of witnesses doesn't match
/// the expected amount.
///
/// When calculating the fee for the transaction the amount of witnesses
/// needs to be specified since they affect the transaction bytes length.
///
/// Thus less or more witnesses than were included when calculating
/// the fee are not allowed.
final class InvalidTransactionWitnessesException implements Exception {
/// The default constructor for [InvalidTransactionWitnessesException].
const InvalidTransactionWitnessesException();

@override
String toString() => 'InvalidTransactionWitnessesException';
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,15 @@ final class TransactionHash extends BaseHash {
/// of [bytes].
TransactionHash.fromHex(super.string) : super.fromHex();

/// Constructs the [TransactionHash] from a [TransactionBody].
TransactionHash.fromTransactionBody(TransactionBody body)
: super.fromBytes(
bytes: Hash.blake2b(
Uint8List.fromList(cbor.encode(body.toCbor())),
digestSize: _length,
),
);

@override
int get length => _length;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'package:catalyst_cardano_serialization/src/address.dart';
import 'package:catalyst_cardano_serialization/src/hashes.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:catalyst_cardano_serialization/src/utils/cbor.dart';
import 'package:catalyst_cardano_serialization/src/witness.dart';
import 'package:cbor/cbor.dart';

/// Represents the signed transaction with a list of witnesses
Expand All @@ -9,6 +11,9 @@ final class Transaction {
/// The transaction body containing the inputs, outputs, fees, etc.
final TransactionBody body;

/// The set of witnesses that have signed given transaction.
final TransactionWitnessSet witnessSet;

/// True if the transaction is valid, false otherwise.
final bool isValid;

Expand All @@ -19,15 +24,15 @@ final class Transaction {
const Transaction({
required this.body,
required this.isValid,
required this.witnessSet,
this.auxiliaryData,
});

/// Serializes the type as cbor.
CborValue toCbor() {
return CborList([
body.toCbor(),
// TODO(dtscalac): implement witnesses
CborMap({}),
witnessSet.toCbor(),
CborBool(isValid),
(auxiliaryData ?? const AuxiliaryData()).toCbor(),
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,3 @@ extension type const SlotBigNum(int value) {
/// Serializes the type as cbor.
CborValue toCbor() => CborSmallInt(value);
}

/// Holds cbor tags not specified by the official cbor package.
final class CborCustomTags {
const CborCustomTags._();

/// A cbor tag describing a key-value pairs data.
static const int map = 259;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/// Holds cbor tags not specified by the official cbor package.
final class CborCustomTags {
const CborCustomTags._();

/// A cbor tag describing a key-value pairs data.
static const int map = 259;
}

/// How many bytes are used in cbor encoding for a major type/length.
enum CborSize {
/// Length/data is encoded inside of the type information.
inline(bytes: 0),

/// Length/data is in 1 byte following the type information.
one(bytes: 1),

/// Length/data is in 2 bytes following the type information.
two(bytes: 2),

/// Length/data is in 4 bytes following the type information.
four(bytes: 4),

/// Length/data is in 8 bytes following the type information.
eight(bytes: 8);

/// The amount of bytes it takes to encode the type in cbor.
final int bytes;

const CborSize({required this.bytes});

/// The max int value that can be inlined in cbor without extra bytes.
static const int maxInlineEncoding = 23;

/// Calculates the [CborSize] for arbitrary [value].
static CborSize ofInt(int value) {
if (value <= maxInlineEncoding) {
return CborSize.inline;
} else if (value < 0x100) {
return CborSize.one;
} else if (value < 0x10000) {
return CborSize.two;
} else if (value < 0x100000000) {
return CborSize.four;
} else {
return CborSize.eight;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'package:cbor/cbor.dart';

/// A set of witnesses that sign the transaction.
class TransactionWitnessSet {
/// The witnesses that sign the transaction.
final Set<VkeyWitness> vkeyWitnesses;

/// The default constructor for [TransactionWitnessSet].
const TransactionWitnessSet({required this.vkeyWitnesses});

/// Serializes the type as cbor.
CborValue toCbor() {
return CborMap({
for (final vkey in vkeyWitnesses.indexed)
CborSmallInt(vkey.$1): vkey.$2.toCbor(),
});
}
}

/// The transaction witness with a [signature] of the transaction.
class VkeyWitness {
/// The public key of the witness.
final Vkey vkey;

/// The witness signature of the transaction.
final Ed25519Signature signature;

/// The default constructor for [VkeyWitness].
const VkeyWitness({
required this.vkey,
required this.signature,
});

/// Builds a fake [VkeyWitness] that helps to measure target transaction
/// size when the transaction hasn't been signed yet.
factory VkeyWitness.seeded(int byte) {
return VkeyWitness(
vkey: Vkey.seeded(byte),
signature: Ed25519Signature.seeded(byte),
);
}

/// Serializes the type as cbor.
CborValue toCbor() {
return CborList([
vkey.toCbor(),
signature.toCbor(),
]);
}
}

/// The public key of the witness.
extension type Vkey._(List<int> bytes) {
/// The length of the [Vkey].
static const int length = 32;

/// The default constructor for [Vkey].
Vkey.fromBytes(this.bytes) {
if (bytes.length != length) {
throw ArgumentError('Vkey length does not match: ${bytes.length}');
}
}

/// Returns the [Vkey] filled with [byte] that can be
/// used to reserve size to calculate the final transaction bytes size.
factory Vkey.seeded(int byte) => Vkey.fromBytes(List.filled(length, byte));

/// Serializes the type as cbor.
CborValue toCbor() => CborBytes(bytes);
}

/// The witness signature of the transaction.
extension type Ed25519Signature._(List<int> bytes) {
/// The length of the [Ed25519Signature].
static const int length = 64;

/// The default constructor for [Ed25519Signature].
Ed25519Signature.fromBytes(this.bytes) {
if (bytes.length != length) {
throw ArgumentError(
'Ed25519Signature length does not match: ${bytes.length}',
);
}
}

/// Returns the [Ed25519Signature] filled with [byte] that can be
/// used to reserve size to calculate the final transaction bytes size.
factory Ed25519Signature.seeded(int byte) =>
Ed25519Signature.fromBytes(List.filled(length, byte));

/// Serializes the type as cbor.
CborValue toCbor() => CborBytes(bytes);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:catalyst_cardano_serialization/src/address.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:test/test.dart';

import 'utils/test_data.dart';
import 'test_utils/test_data.dart';

void main() {
group(ShelleyAddress, () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'package:catalyst_cardano_serialization/src/fees.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:test/test.dart';

import 'utils/test_data.dart';
import 'test_utils/test_data.dart';

void main() {
group(LinearFee, () {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:catalyst_cardano_serialization/src/hashes.dart';
import 'package:catalyst_cardano_serialization/src/transaction.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:cbor/cbor.dart';
import 'package:convert/convert.dart';
import 'package:test/test.dart';
Expand All @@ -20,6 +21,24 @@ void main() {
expect(hash.bytes, equals(bytes));
});

test('from transaction body', () {
const body = TransactionBody(
inputs: {},
outputs: [],
fee: Coin(0),
);

final hash = TransactionHash.fromTransactionBody(body);
expect(
hash,
equals(
TransactionHash.fromHex(
'36fdff68dfe3660f1ceea60f018a0fd7a83da13def229108794c397a879b0436',
),
),
);
});

test('toCbor returns bytes', () {
final hash = TransactionHash.fromBytes(bytes: bytes);
final encodedCbor = cbor.encode(hash.toCbor());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:catalyst_cardano_serialization/src/address.dart';
import 'package:catalyst_cardano_serialization/src/hashes.dart';
import 'package:catalyst_cardano_serialization/src/transaction.dart';
import 'package:catalyst_cardano_serialization/src/types.dart';
import 'package:catalyst_cardano_serialization/src/witness.dart';
import 'package:cbor/cbor.dart';

/* cSpell:disable */
Expand Down Expand Up @@ -43,6 +44,7 @@ Transaction minimalTestTransaction() {
fee: const Coin(10000000),
),
isValid: true,
witnessSet: const TransactionWitnessSet(vkeyWitnesses: {}),
);
}

Expand Down Expand Up @@ -74,6 +76,10 @@ Transaction fullTestTransaction() {
networkId: NetworkId.testnet,
),
isValid: true,
// TODO(dtscalac): provide witness
witnessSet: const TransactionWitnessSet(
vkeyWitnesses: {},
),
auxiliaryData: auxiliaryData,
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'package:cbor/cbor.dart';
import 'package:convert/convert.dart';
import 'package:test/test.dart';

import 'utils/test_data.dart';
import 'test_utils/test_data.dart';

void main() {
group(Transaction, () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'package:catalyst_cardano_serialization/src/utils/cbor.dart';
import 'package:test/test.dart';

void main() {
group(CborSize, () {
test('of int value', () {
expect(CborSize.ofInt(0), equals(CborSize.inline));
expect(
CborSize.ofInt(CborSize.maxInlineEncoding),
equals(CborSize.inline),
);
expect(CborSize.ofInt(0xff), equals(CborSize.one));
expect(CborSize.ofInt(0x100), equals(CborSize.two));
expect(CborSize.ofInt(0xffff), equals(CborSize.two));
expect(CborSize.ofInt(0x10000), equals(CborSize.four));
expect(CborSize.ofInt(0xffffffff), equals(CborSize.four));
expect(CborSize.ofInt(0x100000000), equals(CborSize.eight));
expect(CborSize.ofInt(0xfffffffffffff), equals(CborSize.eight));
});

test('bytes length', () {
expect(CborSize.inline.bytes, equals(0));
expect(CborSize.one.bytes, equals(1));
expect(CborSize.two.bytes, equals(2));
expect(CborSize.four.bytes, equals(4));
expect(CborSize.eight.bytes, equals(8));
});
});
}
Loading

0 comments on commit dfbb9f8

Please sign in to comment.