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

Add support for partially signed tx, closes #38 #42

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion tx_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type TxBuilder struct {
pkeys []crypto.PrvKey

changeReceiver *Address

additionalWitnesses uint
}

// NewTxBuilder returns a new instance of TxBuilder.
Expand Down Expand Up @@ -48,6 +50,12 @@ func (tb *TxBuilder) SetFee(fee Coin) {
tb.tx.Body.Fee = fee
}

// SetAdditionalWitnesses sets future witnesses for a partially signed transction.
// This is useful to compute the real length and so fee in advance
func (tb *TxBuilder) SetAdditionalWitnesses(witnesses uint) {
tb.additionalWitnesses = witnesses
}

// AddAuxiliaryData adds auxiliary data to the transaction.
func (tb *TxBuilder) AddAuxiliaryData(data *AuxiliaryData) {
tb.tx.AuxiliaryData = data
Expand Down Expand Up @@ -141,7 +149,24 @@ func (tb *TxBuilder) MinCoinsForTxOut(txOut *TxOutput) Coin {

// calculateMinFee computes the minimal fee required for the transaction.
func (tb *TxBuilder) calculateMinFee() Coin {
if tb.additionalWitnesses > 0 {
// we can asssume the list of VKeyWitnessSet is not a nil value, as `build()` method is always allocating a slice
additionalVKeyWitnessSet := make([]VKeyWitness, tb.additionalWitnesses)
for i := uint(0); i < tb.additionalWitnesses; i++ {
additionalVKeyWitnessSet[i] = VKeyWitness{
VKey: crypto.PubKey(make([]byte, 32)),
Signature: make([]byte, 64),
}
}
tb.tx.WitnessSet.VKeyWitnessSet = append(tb.tx.WitnessSet.VKeyWitnessSet, additionalVKeyWitnessSet...)
}

txBytes := tb.tx.Bytes()

if tb.additionalWitnesses > 0 {
tb.tx.WitnessSet.VKeyWitnessSet = tb.tx.WitnessSet.VKeyWitnessSet[:len(tb.tx.WitnessSet.VKeyWitnessSet)-int(tb.additionalWitnesses)]
}

txLength := uint64(len(txBytes))
return tb.protocol.MinFeeA*Coin(txLength) + tb.protocol.MinFeeB
}
Expand Down Expand Up @@ -208,7 +233,7 @@ func (tb *TxBuilder) addChangeIfNeeded(inputAmount, outputAmount *Value) error {

if inputOutputCmp := inputAmount.Cmp(outputAmount); inputOutputCmp == -1 || inputOutputCmp == 2 {
return fmt.Errorf(
"insuficient input in transaction, got %v want atleast %v",
"insufficient input in transaction, got %v want atleast %v",
inputAmount,
outputAmount,
)
Expand Down
154 changes: 154 additions & 0 deletions tx_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,3 +593,157 @@ func TestAddChangeIfNeeded(t *testing.T) {
})
}
}

func TestCalculateMinFee(t *testing.T) {
key := crypto.NewXPrvKeyFromEntropy([]byte("receiver address"), "foo")
payment, err := NewKeyCredential(key.PubKey())
if err != nil {
t.Fatal(err)
}
receiver, err := NewEnterpriseAddress(Testnet, payment)
if err != nil {
t.Fatal(err)
}

type fields struct {
tx Tx
protocol *ProtocolParams
inputs []*TxInput
outputs []*TxOutput
certs []Certificate
// ttl uint64
// fee uint64
}

testcases := []struct {
name string
fields fields
expectedFee Coin
additionalWitnesses uint
}{
{
name: "input == output + fee",
fields: fields{
protocol: alonzoProtocol,
inputs: []*TxInput{
{
TxHash: make([]byte, 32),
Index: uint64(0),
Amount: NewValue(20000000),
},
},
outputs: []*TxOutput{
{
Address: receiver,
Amount: NewValue(18831991),
},
},
},
expectedFee: Coin(165413), //Coin(168009),
},
{
name: "with only change address",
fields: fields{
protocol: alonzoProtocol,
inputs: []*TxInput{
{
TxHash: make([]byte, 32),
Index: uint64(0),
Amount: NewValue(20000000),
},
},
},
expectedFee: Coin(163785), // cardano-cli would propose Coin(165149) as fee for a tx with only change address and no output,
},
{
name: "input == output + fee (two additional witness)",
fields: fields{
protocol: alonzoProtocol,
inputs: []*TxInput{
{
TxHash: make([]byte, 32),
Index: uint64(0),
Amount: NewValue(20000000),
},
},
outputs: []*TxOutput{
{
Address: receiver,
// Amount: NewValue(19823103),
Amount: NewValue(1982310),
},
},
},
expectedFee: Coin(174301), //Coin(176897),
additionalWitnesses: 2,
},
{
name: "input == output + fee, one delegation certificate, implies one additional witness",
fields: fields{
protocol: alonzoProtocol,
inputs: []*TxInput{
{
TxHash: make([]byte, 32),
Index: uint64(0),
Amount: NewValue(20000000),
},
},
outputs: []*TxOutput{
{
Address: receiver,
Amount: NewValue(19827547),
},
},
certs: []Certificate{
func() Certificate {
c, _ := NewStakeDelegationCertificate(crypto.PubKey(make([]byte, 32)), Hash28(make([]byte, 28)))
return c
}(),
},
},
expectedFee: Coin(172453),
additionalWitnesses: 1,
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
key := crypto.NewXPrvKeyFromEntropy([]byte("change address"), "foo")
payment, err := NewKeyCredential(key.PubKey())
if err != nil {
t.Fatal(err)
}
changeAddr, err := NewEnterpriseAddress(Testnet, payment)
if err != nil {
t.Fatal(err)
}
txBuilder := NewTxBuilder(alonzoProtocol)
txBuilder.AddInputs(tc.fields.inputs...)
txBuilder.AddOutputs(tc.fields.outputs...)
for _, cert := range tc.fields.certs {
txBuilder.AddCertificate(cert)
}
txBuilder.AddChangeIfNeeded(changeAddr)
txBuilder.SetAdditionalWitnesses(tc.additionalWitnesses)
txBuilder.Sign(key.PrvKey())
tx, err := txBuilder.Build()
if err != nil {
t.Fatal(err)
}
var totalIn Coin
for _, input := range tx.Body.Inputs {
totalIn += input.Amount.Coin
}
var totalOut Coin
for _, output := range tx.Body.Outputs {
totalOut += output.Amount.Coin
}
if got, want := tx.Body.Fee+totalOut, totalIn; got != want {
t.Errorf("invalid fee+totalOut: got %v want %v", got, want)
}
if got, want := tx.Body.Fee, tc.expectedFee; got != want {
t.Errorf("calculated fee is not the expected one: got %v want %v", got, want)
}
})
}
}