Merge pull request #140 from filecoin-project/feat/deal-payments

deals: Wire up client side of payments
This commit is contained in:
Łukasz Magiera 2019-08-16 00:03:05 +02:00 committed by GitHub
commit 6bee253e33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 299 additions and 70 deletions

View File

@ -106,7 +106,7 @@ func (ia InitActor) Exec(act *types.Actor, vmctx types.VMContext, p *ExecParams)
// Set up the actor itself // Set up the actor itself
actor := types.Actor{ actor := types.Actor{
Code: p.Code, Code: p.Code,
Balance: vmctx.Message().Value, Balance: types.NewInt(0),
Head: EmptyCBOR, Head: EmptyCBOR,
Nonce: 0, Nonce: 0,
} }

View File

@ -2,17 +2,15 @@ package actors
import ( import (
"context" "context"
"github.com/filecoin-project/go-lotus/chain/actors/aerrors" "github.com/filecoin-project/go-lotus/chain/actors/aerrors"
"github.com/filecoin-project/go-lotus/chain/address" "github.com/filecoin-project/go-lotus/chain/address"
"github.com/filecoin-project/go-lotus/chain/types" "github.com/filecoin-project/go-lotus/chain/types"
"github.com/filecoin-project/go-lotus/lib/sectorbuilder" "github.com/filecoin-project/go-lotus/lib/sectorbuilder"
"golang.org/x/xerrors" "github.com/ipfs/go-cid"
"github.com/ipfs/go-hamt-ipld"
cid "github.com/ipfs/go-cid"
hamt "github.com/ipfs/go-hamt-ipld"
cbor "github.com/ipfs/go-ipld-cbor" cbor "github.com/ipfs/go-ipld-cbor"
"github.com/libp2p/go-libp2p-core/peer" "github.com/libp2p/go-libp2p-core/peer"
"golang.org/x/xerrors"
) )
func init() { func init() {
@ -21,6 +19,9 @@ func init() {
cbor.RegisterCborType(CommitSectorParams{}) cbor.RegisterCborType(CommitSectorParams{})
cbor.RegisterCborType(MinerInfo{}) cbor.RegisterCborType(MinerInfo{})
cbor.RegisterCborType(SubmitPoStParams{}) cbor.RegisterCborType(SubmitPoStParams{})
cbor.RegisterCborType(PieceInclVoucherData{})
cbor.RegisterCborType(InclusionProof{})
cbor.RegisterCborType(PaymentVerifyParams{})
} }
var ProvingPeriodDuration = uint64(2 * 60) // an hour, for now var ProvingPeriodDuration = uint64(2 * 60) // an hour, for now
@ -98,23 +99,27 @@ type StorageMinerConstructorParams struct {
} }
type maMethods struct { type maMethods struct {
Constructor uint64 Constructor uint64
CommitSector uint64 CommitSector uint64
SubmitPoSt uint64 SubmitPoSt uint64
SlashStorageFault uint64 SlashStorageFault uint64
GetCurrentProvingSet uint64 GetCurrentProvingSet uint64
ArbitrateDeal uint64 ArbitrateDeal uint64
DePledge uint64 DePledge uint64
GetOwner uint64 GetOwner uint64
GetWorkerAddr uint64 GetWorkerAddr uint64
GetPower uint64 GetPower uint64
GetPeerID uint64 GetPeerID uint64
GetSectorSize uint64 GetSectorSize uint64
UpdatePeerID uint64 UpdatePeerID uint64
ChangeWorker uint64 ChangeWorker uint64
IsSlashed uint64
IsLate uint64
PaymentVerifyInclusion uint64
PaymentVerifySector uint64
} }
var MAMethods = maMethods{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14} var MAMethods = maMethods{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}
func (sma StorageMinerActor) Exports() []interface{} { func (sma StorageMinerActor) Exports() []interface{} {
return []interface{}{ return []interface{}{
@ -132,6 +137,10 @@ func (sma StorageMinerActor) Exports() []interface{} {
12: sma.GetSectorSize, 12: sma.GetSectorSize,
13: sma.UpdatePeerID, 13: sma.UpdatePeerID,
//14: sma.ChangeWorker, //14: sma.ChangeWorker,
//15: sma.IsSlashed,
//16: sma.IsLate,
17: sma.PaymentVerifyInclusion,
18: sma.PaymentVerifySector,
} }
} }
@ -376,6 +385,38 @@ func AddToSectorSet(ctx context.Context, cst *hamt.CborIpldStore, ss cid.Cid, se
return ssroot, nil return ssroot, nil
} }
func GetFromSectorSet(ctx context.Context, cst *hamt.CborIpldStore, ss cid.Cid, sectorID types.BigInt) (bool, []byte, []byte, ActorError) {
nd, err := hamt.LoadNode(ctx, cst, ss)
if err != nil {
return false, nil, nil, aerrors.Escalate(err, "could not load HAMT node")
}
infoIf, err := nd.Find(ctx, sectorID.String())
if err == hamt.ErrNotFound {
return false, nil, nil, nil
}
if err != nil {
return false, nil, nil, aerrors.Escalate(err, "failed to find sector in sector set")
}
infoB, ok := infoIf.([]byte)
if !ok {
return false, nil, nil, aerrors.Escalate(xerrors.New("casting infoIf to []byte failed"), "") // TODO: Review: how to create aerrror without retcode?
}
var comms [][]byte // [ [commR], [commD] ]
err = cbor.DecodeInto(infoB, &comms)
if err != nil {
return false, nil, nil, aerrors.Escalate(err, "failed to decode sector set entry")
}
if len(comms) != 2 {
return false, nil, nil, aerrors.Escalate(xerrors.New("sector set entry should only have 2 elements"), "")
}
return true, comms[0], comms[1], nil
}
func ValidatePoRep(maddr address.Address, ssize types.BigInt, params *CommitSectorParams) (bool, ActorError) { func ValidatePoRep(maddr address.Address, ssize types.BigInt, params *CommitSectorParams) (bool, ActorError) {
ok, err := sectorbuilder.VerifySeal(ssize.Uint64(), params.CommR, params.CommD, params.CommRStar, maddr, params.SectorID.Uint64(), params.Proof) ok, err := sectorbuilder.VerifySeal(ssize.Uint64(), params.CommR, params.CommD, params.CommRStar, maddr, params.SectorID.Uint64(), params.Proof)
if err != nil { if err != nil {
@ -493,3 +534,86 @@ func (sma StorageMinerActor) GetSectorSize(act *types.Actor, vmctx types.VMConte
return mi.SectorSize.Bytes(), nil return mi.SectorSize.Bytes(), nil
} }
type PaymentVerifyParams struct {
Extra []byte
Proof []byte
}
type PieceInclVoucherData struct { // TODO: Update spec at https://github.com/filecoin-project/specs/blob/master/actors.md#paymentverify
CommP []byte
PieceSize types.BigInt
}
type InclusionProof struct {
Sector types.BigInt // for CommD, also verifies the sector is in sector set
Proof []byte
}
func (sma StorageMinerActor) PaymentVerifyInclusion(act *types.Actor, vmctx types.VMContext, params *PaymentVerifyParams) ([]byte, ActorError) {
// params.Extra - PieceInclVoucherData
// params.Proof - InclusionProof
_, self, aerr := loadState(vmctx)
if aerr != nil {
return nil, aerr
}
mi, aerr := loadMinerInfo(vmctx, self)
if aerr != nil {
return nil, aerr
}
var voucherData PieceInclVoucherData
if err := cbor.DecodeInto(params.Extra, &voucherData); err != nil {
return nil, aerrors.Escalate(err, "failed to decode storage voucher data for verification")
}
var proof InclusionProof
if err := cbor.DecodeInto(params.Proof, &proof); err != nil {
return nil, aerrors.Escalate(err, "failed to decode storage payment proof")
}
ok, _, commD, aerr := GetFromSectorSet(context.TODO(), vmctx.Ipld(), self.Sectors, proof.Sector)
if aerr != nil {
return nil, aerr
}
if !ok {
return nil, aerrors.New(1, "miner does not have required sector")
}
ok, err := sectorbuilder.VerifyPieceInclusionProof(mi.SectorSize.Uint64(), voucherData.PieceSize.Uint64(), voucherData.CommP, commD, params.Proof)
if err != nil {
return nil, aerrors.Escalate(err, "verify piece inclusion proof failed")
}
if !ok {
return nil, aerrors.New(2, "piece inclusion proof was invalid")
}
return nil, nil
}
func (sma StorageMinerActor) PaymentVerifySector(act *types.Actor, vmctx types.VMContext, params *PaymentVerifyParams) ([]byte, ActorError) {
// params.Extra - BigInt - sector id
// params.Proof - nil
_, self, aerr := loadState(vmctx)
if aerr != nil {
return nil, aerr
}
// TODO: ensure no sector ID reusability within related deal lifetime
sector := types.BigFromBytes(params.Extra)
if len(params.Proof) > 0 {
return nil, aerrors.New(1, "unexpected proof bytes")
}
ok, _, _, aerr := GetFromSectorSet(context.TODO(), vmctx.Ipld(), self.Sectors, sector)
if aerr != nil {
return nil, aerr
}
if !ok {
return nil, aerrors.New(2, "miner does not have required sector")
}
return nil, nil
}

View File

@ -183,43 +183,56 @@ func (c *Client) waitAccept(s inet.Stream, proposal StorageDealProposal, minerID
}, nil }, nil
} }
func (c *Client) Start(ctx context.Context, data cid.Cid, totalPrice types.BigInt, from address.Address, miner address.Address, minerID peer.ID, blocksDuration uint64) (cid.Cid, error) { type ClientDealProposal struct {
Data cid.Cid
TotalPrice types.BigInt
Duration uint64
Payment actors.PaymentInfo
MinerAddress address.Address
ClientAddress address.Address
MinerID peer.ID
}
func (c *Client) VerifyParams(ctx context.Context, data cid.Cid) (*actors.PieceInclVoucherData, error) {
commP, size, err := c.commP(ctx, data) commP, size, err := c.commP(ctx, data)
if err != nil { if err != nil {
return cid.Undef, err return nil, err
} }
dummyCid, _ := cid.Parse("bafkqaaa") return &actors.PieceInclVoucherData{
CommP: commP,
PieceSize: types.NewInt(uint64(size)),
}, nil
}
func (c *Client) Start(ctx context.Context, p ClientDealProposal, vd *actors.PieceInclVoucherData) (cid.Cid, error) {
// TODO: use data // TODO: use data
proposal := StorageDealProposal{ proposal := StorageDealProposal{
PieceRef: data.String(), PieceRef: p.Data.String(),
SerializationMode: SerializationUnixFs, SerializationMode: SerializationUnixFs,
CommP: commP[:], CommP: vd.CommP[:],
Size: uint64(size), Size: vd.PieceSize.Uint64(),
TotalPrice: totalPrice, TotalPrice: p.TotalPrice,
Duration: blocksDuration, Duration: p.Duration,
Payment: actors.PaymentInfo{ Payment: p.Payment,
PayChActor: address.Address{}, MinerAddress: p.MinerAddress,
Payer: address.Address{}, ClientAddress: p.ClientAddress,
ChannelMessage: dummyCid,
Vouchers: nil,
},
MinerAddress: miner,
ClientAddress: from,
} }
s, err := c.h.NewStream(ctx, minerID, ProtocolID) s, err := c.h.NewStream(ctx, p.MinerID, ProtocolID)
if err != nil { if err != nil {
return cid.Undef, err return cid.Undef, err
} }
defer s.Reset() // TODO: handle other updates defer s.Reset() // TODO: handle other updates
if err := c.sendProposal(s, proposal, from); err != nil { if err := c.sendProposal(s, proposal, p.ClientAddress); err != nil {
return cid.Undef, err return cid.Undef, err
} }
deal, err := c.waitAccept(s, proposal, minerID) deal, err := c.waitAccept(s, proposal, p.MinerID)
if err != nil { if err != nil {
return cid.Undef, err return cid.Undef, err
} }

View File

@ -100,6 +100,14 @@ func VerifySeal(sectorSize uint64, commR, commD, commRStar []byte, proverID addr
return sectorbuilder.VerifySeal(sectorSize, commRa, commDa, commRStara, proverIDa, sectorIDa, proof) return sectorbuilder.VerifySeal(sectorSize, commRa, commDa, commRStara, proverIDa, sectorIDa, proof)
} }
func VerifyPieceInclusionProof(sectorSize uint64, pieceSize uint64, commP []byte, commD []byte, proof []byte) (bool, error) {
var commPa, commDa [32]byte
copy(commPa[:], commP)
copy(commDa[:], commD)
return sectorbuilder.VerifyPieceInclusionProof(sectorSize, pieceSize, commPa, commDa, proof)
}
func VerifyPost(sectorSize uint64, sortedCommRs [][CommLen]byte, challengeSeed [CommLen]byte, proofs [][]byte, faults []uint64) (bool, error) { func VerifyPost(sectorSize uint64, sortedCommRs [][CommLen]byte, challengeSeed [CommLen]byte, proofs [][]byte, faults []uint64) (bool, error) {
// sectorbuilder.VerifyPost() // sectorbuilder.VerifyPost()
panic("no") panic("no")

View File

@ -59,7 +59,12 @@ class Address extends React.Component {
addr = <a href="#" onClick={this.openState}>{addr}</a> addr = <a href="#" onClick={this.openState}>{addr}</a>
} }
return <span>{addr}:&nbsp;{this.state.balance}&nbsp;{actInfo}&nbsp;{add1k}</span> let balance = <span>:&nbsp;{this.state.balance}</span>
if(this.props.nobalance) {
balance = <span></span>
}
return <span>{addr}{balance}&nbsp;{actInfo}&nbsp;{add1k}</span>
} }
} }

View File

@ -16,10 +16,14 @@
background: #f9be77; background: #f9be77;
user-select: text; user-select: text;
font-family: monospace; font-family: monospace;
min-width: 40em; min-width: 50em;
display: inline-block; display: inline-block;
} }
.FullNode-voucher {
padding-left: 1em;
}
.StorageNode { .StorageNode {
background: #f9be77; background: #f9be77;
user-select: text; user-select: text;

View File

@ -36,21 +36,17 @@ class FullNode extends React.Component {
const tipset = await this.props.client.call("Filecoin.ChainHead", []) const tipset = await this.props.client.call("Filecoin.ChainHead", [])
const addrs = await this.props.client.call('Filecoin.WalletList', []) let addrs = await this.props.client.call('Filecoin.WalletList', [])
let defaultAddr = "" let defaultAddr = ""
if (addrs.length > 0) { if (addrs.length > 0) {
defaultAddr = await this.props.client.call('Filecoin.WalletDefaultAddress', []) defaultAddr = await this.props.client.call('Filecoin.WalletDefaultAddress', [])
} }
let paychs = await this.props.client.call('Filecoin.PaychList', [])
/* const balances = await addrss.map(async addr => { if(!paychs)
let balance = 0 paychs = []
try { const vouchers = await Promise.all(paychs.map(paych => {
balance = await this.props.client.call('Filecoin.WalletBalance', [addr]) return this.props.client.call('Filecoin.PaychVoucherList', [paych])
} catch { }))
balance = -1
}
return [addr, balance]
}).reduce(awaitListReducer, Promise.resolve([]))*/
this.setState(() => ({ this.setState(() => ({
id: id, id: id,
@ -59,6 +55,9 @@ class FullNode extends React.Component {
tipset: tipset, tipset: tipset,
addrs: addrs, addrs: addrs,
paychs: paychs,
vouchers: vouchers,
defaultAddr: defaultAddr})) defaultAddr: defaultAddr}))
} }
@ -118,6 +117,23 @@ class FullNode extends React.Component {
} }
return <div key={addr}>{line}</div> return <div key={addr}>{line}</div>
}) })
let paychannels = this.state.paychs.map((addr, ak) => {
const line = <Address client={this.props.client} add1k={this.add1k} addr={addr} mountWindow={this.props.mountWindow}/>
const vouchers = this.state.vouchers[ak].map(voucher => {
let extra = <span></span>
if(voucher.Extra) {
extra = <span>Verif: &lt;<b><Address nobalance={true} client={this.props.client} addr={voucher.Extra.Actor} mountWindow={this.props.mountWindow}/>M{voucher.Extra.Method}</b>&gt;</span>
}
return <div key={voucher.Nonce} className="FullNode-voucher">
Voucher Nonce:<b>{voucher.Nonce}</b> Lane:<b>{voucher.Lane}</b> Amt:<b>{voucher.Amount}</b> TL:<b>{voucher.TimeLock}</b> MinCl:<b>{voucher.MinCloseHeight}</b> {extra}
</div>
})
return <div key={addr}>
{line}
{vouchers}
</div>
})
runtime = ( runtime = (
<div> <div>
@ -130,6 +146,7 @@ class FullNode extends React.Component {
<div> <div>
<div>Balances: [New <a href="#" onClick={this.newScepAddr}>[Secp256k1]</a>]</div> <div>Balances: [New <a href="#" onClick={this.newScepAddr}>[Secp256k1]</a>]</div>
<div>{addresses}</div> <div>{addresses}</div>
<div>{paychannels}</div>
</div> </div>
</div> </div>

View File

@ -47,11 +47,13 @@ type FullNodeAPI struct {
} }
func (a *FullNodeAPI) ClientStartDeal(ctx context.Context, data cid.Cid, miner address.Address, price types.BigInt, blocksDuration uint64) (*cid.Cid, error) { func (a *FullNodeAPI) ClientStartDeal(ctx context.Context, data cid.Cid, miner address.Address, price types.BigInt, blocksDuration uint64) (*cid.Cid, error) {
// TODO: make this a param
self, err := a.WalletDefaultAddress(ctx) self, err := a.WalletDefaultAddress(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// get miner peerID
msg := &types.Message{ msg := &types.Message{
To: miner, To: miner,
From: miner, From: miner,
@ -67,8 +69,59 @@ func (a *FullNodeAPI) ClientStartDeal(ctx context.Context, data cid.Cid, miner a
return nil, err return nil, err
} }
vd, err := a.DealClient.VerifyParams(ctx, data)
if err != nil {
return nil, err
}
voucherData, err := cbor.DumpObject(vd)
if err != nil {
return nil, err
}
// setup payments
total := types.BigMul(price, types.NewInt(blocksDuration)) total := types.BigMul(price, types.NewInt(blocksDuration))
c, err := a.DealClient.Start(ctx, data, total, self, miner, pid, blocksDuration)
// TODO: at least ping the miner before creating paych / locking the money
paych, paychMsg, err := a.paychCreate(ctx, self, miner, total)
if err != nil {
return nil, err
}
voucher := types.SignedVoucher{
// TimeLock: 0, // TODO: do we want to use this somehow?
Extra: &types.ModVerifyParams{
Actor: miner,
Method: actors.MAMethods.PaymentVerifyInclusion,
Data: voucherData,
},
Lane: 0,
Amount: total,
MinCloseHeight: blocksDuration, // TODO: some way to start this after initial piece inclusion by actor? (also, at least add current height)
}
sv, err := a.paychVoucherCreate(ctx, paych, voucher)
if err != nil {
return nil, err
}
proposal := deals.ClientDealProposal{
Data: data,
TotalPrice: total,
Duration: blocksDuration,
Payment: actors.PaymentInfo{
PayChActor: paych,
Payer: self,
ChannelMessage: paychMsg,
Vouchers: []types.SignedVoucher{*sv},
},
MinerAddress: miner,
ClientAddress: self,
MinerID: pid,
}
c, err := a.DealClient.Start(ctx, proposal, vd)
// TODO: send updated voucher with PaymentVerifySector for cheaper validation (validate the sector the miner sent us first!)
return &c, err return &c, err
} }
@ -420,15 +473,19 @@ func (a *FullNodeAPI) StateMinerProvingSet(ctx context.Context, addr address.Add
} }
func (a *FullNodeAPI) PaychCreate(ctx context.Context, from, to address.Address, amt types.BigInt) (address.Address, error) { func (a *FullNodeAPI) PaychCreate(ctx context.Context, from, to address.Address, amt types.BigInt) (address.Address, error) {
act, _, err := a.paychCreate(ctx, from, to, amt)
return act, err
}
func (a *FullNodeAPI) paychCreate(ctx context.Context, from, to address.Address, amt types.BigInt) (address.Address, cid.Cid, error) {
params, aerr := actors.SerializeParams(&actors.PCAConstructorParams{To: to}) params, aerr := actors.SerializeParams(&actors.PCAConstructorParams{To: to})
if aerr != nil { if aerr != nil {
return address.Undef, aerr return address.Undef, cid.Undef, aerr
} }
nonce, err := a.MpoolGetNonce(ctx, from) nonce, err := a.MpoolGetNonce(ctx, from)
if err != nil { if err != nil {
return address.Undef, err return address.Undef, cid.Undef, err
} }
enc, err := actors.SerializeParams(&actors.ExecParams{ enc, err := actors.SerializeParams(&actors.ExecParams{
@ -449,12 +506,12 @@ func (a *FullNodeAPI) PaychCreate(ctx context.Context, from, to address.Address,
ser, err := msg.Serialize() ser, err := msg.Serialize()
if err != nil { if err != nil {
return address.Undef, err return address.Undef, cid.Undef, err
} }
sig, err := a.WalletSign(ctx, from, ser) sig, err := a.WalletSign(ctx, from, ser)
if err != nil { if err != nil {
return address.Undef, err return address.Undef, cid.Undef, err
} }
smsg := &types.SignedMessage{ smsg := &types.SignedMessage{
@ -463,28 +520,28 @@ func (a *FullNodeAPI) PaychCreate(ctx context.Context, from, to address.Address,
} }
if err := a.MpoolPush(ctx, smsg); err != nil { if err := a.MpoolPush(ctx, smsg); err != nil {
return address.Undef, err return address.Undef, cid.Undef, err
} }
mwait, err := a.ChainWaitMsg(ctx, smsg.Cid()) mwait, err := a.ChainWaitMsg(ctx, smsg.Cid())
if err != nil { if err != nil {
return address.Undef, err return address.Undef, cid.Undef, err
} }
if mwait.Receipt.ExitCode != 0 { if mwait.Receipt.ExitCode != 0 {
return address.Undef, fmt.Errorf("payment channel creation failed (exit code %d)", mwait.Receipt.ExitCode) return address.Undef, cid.Undef, fmt.Errorf("payment channel creation failed (exit code %d)", mwait.Receipt.ExitCode)
} }
paychaddr, err := address.NewFromBytes(mwait.Receipt.Return) paychaddr, err := address.NewFromBytes(mwait.Receipt.Return)
if err != nil { if err != nil {
return address.Undef, err return address.Undef, cid.Undef, err
} }
if err := a.PaychMgr.TrackOutboundChannel(ctx, paychaddr); err != nil { if err := a.PaychMgr.TrackOutboundChannel(ctx, paychaddr); err != nil {
return address.Undef, err return address.Undef, cid.Undef, err
} }
return paychaddr, nil return paychaddr, msg.Cid(), nil
} }
func (a *FullNodeAPI) PaychList(ctx context.Context) ([]address.Address, error) { func (a *FullNodeAPI) PaychList(ctx context.Context) ([]address.Address, error) {
@ -551,21 +608,22 @@ func (a *FullNodeAPI) PaychVoucherAdd(ctx context.Context, ch address.Address, s
// actual additional value of this voucher will only be the difference between // actual additional value of this voucher will only be the difference between
// the two. // the two.
func (a *FullNodeAPI) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*types.SignedVoucher, error) { func (a *FullNodeAPI) PaychVoucherCreate(ctx context.Context, pch address.Address, amt types.BigInt, lane uint64) (*types.SignedVoucher, error) {
return a.paychVoucherCreate(ctx, pch, types.SignedVoucher{Amount: amt, Lane: lane})
}
func (a *FullNodeAPI) paychVoucherCreate(ctx context.Context, pch address.Address, voucher types.SignedVoucher) (*types.SignedVoucher, error) {
ci, err := a.PaychMgr.GetChannelInfo(pch) ci, err := a.PaychMgr.GetChannelInfo(pch)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nonce, err := a.PaychMgr.NextNonceForLane(ctx, pch, lane) nonce, err := a.PaychMgr.NextNonceForLane(ctx, pch, voucher.Lane)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sv := &types.SignedVoucher{ sv := &voucher
Lane: lane, sv.Nonce = nonce
Nonce: nonce,
Amount: amt,
}
vb, err := sv.SigningBytes() vb, err := sv.SigningBytes()
if err != nil { if err != nil {