Skip to content

Commit

Permalink
TxBuilder: Remove hard dependency to WellKnown keys
Browse files Browse the repository at this point in the history
The last step missing to allow the TxBuilder to be truly independent
from the Well-Known keys, yet offer a similar API.
  • Loading branch information
Geod24 authored and hewison-chris committed Apr 8, 2022
1 parent 1502758 commit 3656186
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 41 deletions.
1 change: 1 addition & 0 deletions source/agora/utils/PrettyPrinter.d
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,7 @@ Outputs (1): boa1xzgenes5...gm67(59,499,999.9,920,9)<Payment>
// need reproducible unlocks for test (signing generates unique nonces)
import agora.script.Lock;
import agora.utils.Test;
import agora.utils.TxBuilder;
static Unlock unlocker (in Transaction, in OutputRef) @safe nothrow
{
return Unlock.init;
Expand Down
20 changes: 19 additions & 1 deletion source/agora/utils/Test.d
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import agora.crypto.Key;
import agora.crypto.Schnorr;
import agora.script.Lock;
import agora.serialization.Serializer;
public import agora.utils.TxBuilder;
import agora.utils.TxBuilder;
public import agora.utils.Utility : retryFor;

import std.algorithm;
Expand Down Expand Up @@ -314,6 +314,24 @@ unittest
assert(WK.PreImages.at(Height(1), only_node2) == preimages_height_1);
}

// Uses a random nonce when signing (non-determenistic signature),
// and defaults to LockType.Key
private static Unlock WKUnlocker (in Transaction tx, in OutputRef out_ref)
@safe nothrow
{
import agora.script.Signature : getChallenge;

auto ownerKP = WK.Keys[out_ref.output.address];
assert(ownerKP !is KeyPair.init,
"Address not found in Well-Known keypairs: "
~ out_ref.output.address.toString());

return genKeyUnlock(ownerKP.sign(tx.getChallenge()));
}

///
public alias TxBuilder = StaticTransactionBuilder!WKUnlocker;

/***************************************************************************
Takes a block object and filters the payment outputs
Expand Down
145 changes: 105 additions & 40 deletions source/agora/utils/TxBuilder.d
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@
transactions are valid. Generating invalid `Transaction`s is not supported,
however one can generate a valid `Transaction` and mutate it afterwards.
Usage_recommendation:
The `TransactionBuilder` needs to access keys (more precisely, unlocker)
in order to generate valid transaction. However, supplying those keys
every time the `TransactionBuilder` is to be instantiated greatly reduce
usability. For this reason, we recommmend something along the following:
```
private KeyPair[PublicKey] allKeys;
private Unlock keyUnlocker (in Transaction tx, in OutputRef out_ref)
@safe nothrow
{
auto ownerKP = allKeys[out_ref.output.address];
assert(ownerKP !is KeyPair.init)
return genKeyUnlock(ownerKP.sign(tx.getChallenge()));
}
/// Publicly exposed alias used by other modules
public alias TxBuilder = StaticTransactionBuilder!keyUnlocker;
```
The following sections assume such a usage and thus reference `TxBuilder`.
Basics:
When building a transaction, one must first attach an `Output`,
or a `Transaction`, using either the constructors or `attach`.
Expand Down Expand Up @@ -40,14 +60,6 @@
will be the refund address, and if a `Transaction` is provided, the owner
of the first output will be the refund address.
Well_Known_addresses:
This utility relies on the signing keys used for the inputs to be part
of well-known address (see `WK.Keys`).
Error_handling:
Since this is an utility inteded purely for testing, passing invalid data
or inability to perform an operation will result in an assertion failure.
Chaining:
As can be seen in the example, operations which modify the state will
return a reference to the `TxBuilder` to allow for easy chaining.
Expand Down Expand Up @@ -80,15 +92,26 @@ import agora.script.Lock;
import agora.script.Opcodes;
import agora.script.Script: toPushOpcode;
import agora.script.Signature;
/* version (unittest) */ import agora.utils.Test;
version (unittest) import agora.utils.Test;

import std.algorithm;
import std.format;
import std.range;

/// Ditto
public struct TxBuilder
public struct StaticTransactionBuilder (alias KeyUnlocker)
{
static assert(is(typeof(&KeyUnlocker) : TransactionBuilder.Unlocker),
"Expected `KeyUnlocker` template argument to `TransactionBuilder` " ~
"to be of type `" ~ TransactionBuilder.Unlocker.stringof ~ "`, not `" ~
typeof(&KeyUnlocker).stringof ~ "`");

/// Actual object
public TransactionBuilder builder;

///
public alias builder this;

/***************************************************************************
Construct a new transaction builder with the provided refund address
Expand All @@ -105,42 +128,102 @@ public struct TxBuilder

public this (in PublicKey refundMe) @safe pure nothrow
{
this.leftover = Output(Amount(0), refundMe);
this.unlocker = &TxBuilder.keyUnlocker;
this.builder = TransactionBuilder(&KeyUnlocker, refundMe);
}

/// Ditto
public this (in Lock lock) @safe pure nothrow
{
this.leftover = Output(Amount(0), lock);
this.unlocker = &TxBuilder.keyUnlocker;
this.builder = TransactionBuilder(&KeyUnlocker, lock);
}

/// Ditto
public this (const Transaction tx) @safe nothrow
{
this(tx.outputs[0].lock);
this.attach(tx);
this.builder = TransactionBuilder(&KeyUnlocker, tx);
}

/// Ditto
public this (const Transaction tx, uint index) @safe nothrow
{
this(tx.outputs[index].lock);
this.builder = TransactionBuilder(&KeyUnlocker, tx, index);
}

/// Ditto
public this (const Transaction tx, uint index, in Lock lock)
@safe nothrow
{
this.builder = TransactionBuilder(&KeyUnlocker, tx, index, lock);
}

/// Convenience constructor that calls `this.attach(Output, Hash)`
public this (in Output utxo, in Hash hash) @safe nothrow
{
this.builder = TransactionBuilder(&KeyUnlocker, utxo, hash);
}
}

///
public struct TransactionBuilder
{
/// Define Unlocker function to sign the inputs
public alias Unlocker = Unlock function (in Transaction tx, in OutputRef out_ref)
@safe nothrow;

/***************************************************************************
Construct a new transaction builder with the provided refund address
Params:
unlocker = The function to use for unlocking
refundMe = The address to receive the funds by default.
lock = the lock to use in place of an address
tx = The transaction to attach to. If the `index` overload is used,
only the specified `index` will be attached, and it will be
the refund address. Otherwise, the first output is used.
index = Index of the sole output to use from the transaction.
***************************************************************************/

public this (Unlocker unlocker, in PublicKey refundMe) @safe pure nothrow
{
this.unlocker = unlocker;
this.leftover = Output(Amount(0), refundMe);
}

/// Ditto
public this (Unlocker unlocker, in Lock lock) @safe pure nothrow
{
this.unlocker = unlocker;
this.leftover = Output(Amount(0), lock);
}

/// Ditto
public this (Unlocker unlocker, const Transaction tx) @safe nothrow
{
this(unlocker, tx.outputs[0].lock);
this.attach(tx);
}

/// Ditto
public this (Unlocker unlocker, const Transaction tx, uint index) @safe nothrow
{
this(unlocker, tx.outputs[index].lock);
this.attach(tx, index);
}

/// Ditto
public this (const Transaction tx, uint index, in Lock lock) @safe nothrow
public this (Unlocker unlocker, const Transaction tx, uint index, in Lock lock)
@safe nothrow
{
this(lock);
this(unlocker, lock);
this.attach(tx, index);
}

/// Convenience constructor that calls `this.attach(Output, Hash)`
public this (in Output utxo, in Hash hash) @safe nothrow
public this (Unlocker unlocker, in Output utxo, in Hash hash) @safe nothrow
{
this(utxo.address);
this(unlocker, utxo.address);
this.attach(utxo, hash);
}

Expand Down Expand Up @@ -239,8 +322,6 @@ public struct TxBuilder
Sets the unlocker function to sign the inputs
If not set then the default unlocker using WellKnownKeys is used.
Params:
unlocker = function to sign the inputs of the transaction
Expand All @@ -266,19 +347,6 @@ public struct TxBuilder
return Unlock(toPushOpcode(pair[]));
}

// Uses a random nonce when signing (non-determenistic signature),
// and defaults to LockType.Key
private static Unlock keyUnlocker (in Transaction tx, in OutputRef out_ref)
@safe nothrow
{
auto ownerKP = WK.Keys[out_ref.output.address];
assert(ownerKP !is KeyPair.init,
"Address not found in Well-Known keypairs: "
~ out_ref.output.address.toString());

return genKeyUnlock(ownerKP.sign(tx.getChallenge()));
}

/***************************************************************************
Set the payload used by the Transaction
Expand Down Expand Up @@ -340,6 +408,7 @@ public struct TxBuilder
in OutputType outputs_type = OutputType.Payment, uint unlock_age = 0,
Amount freeze_fee = 10_000.coins) @safe nothrow
{
assert(this.unlocker !is null, "unlocker not defined");
assert(this.inputs.length, "Cannot sign input-less transaction");
assert(this.data.outputs.length || this.leftover.value > Amount(0),
"Output-less transactions are not valid");
Expand Down Expand Up @@ -523,10 +592,6 @@ public struct TxBuilder
return this;
}

/// Define Unlocker function to sign the inputs
public alias Unlocker = Unlock function (in Transaction tx, in OutputRef out_ref)
@safe nothrow;

/// The actual function that will sign the inputs
private Unlocker unlocker;

Expand Down

0 comments on commit 3656186

Please sign in to comment.