mirror of
https://github.com/getAlby/hub.git
synced 2026-08-13 12:33:39 +02:00
feat: enable VSS for new hubs with alby subscription (#780)
* feat: auth with alby account before node setup * fix: redirect to auth page from start page if auth is needed * feat: enable VSS for new hubs with alby subscription (wip) * fix: use correct vss auth token endpoint, pass vss node identifier * chore: update log content * fix: do not log vss token * chore: rename variable in test * fix: add error handling for requesting vss auth token * fix: vss auth_tokens base url * fix: add check to disable unlinking account while vss is activated * fix: ldk vss node identifier algorithm * chore: extract vss token code from launchLNBackend
This commit is contained in:
parent
5a6e9f5383
commit
187b0681da
9 changed files with 216 additions and 17 deletions
|
|
@ -34,3 +34,5 @@ FRONTEND_URL=http://localhost:5173
|
|||
#LND_CERT_FILE=/home/YOUR_USERNAME/.polar/networks/1/volumes/lnd/alice/tls.cert
|
||||
#LND_ADDRESS=127.0.0.1:10001
|
||||
#LND_MACAROON_FILE=/home/YOUR_USERNAME/.polar/networks/1/volumes/lnd/alice/data/chain/bitcoin/regtest/admin.macaroon
|
||||
|
||||
#LDK_VSS_URL="http://localhost:8090/vss"
|
||||
|
|
@ -162,6 +162,7 @@ _To configure via env, the following parameters must be provided:_
|
|||
### LDK Backend parameters
|
||||
|
||||
- `LDK_ESPLORA_SERVER`: By default the optimized Alby esplora is used. You can configure your own esplora server (note: the public blockstream one is slow and can cause onchain syncing and issues with opening channels)
|
||||
- `LDK_VSS_URL`: Use VSS (encrypted remote storage) rather than local sqlite store for lightning and bitcoin data. Currently this feature only works for brand new Alby Hub instances that are connected to Alby Accounts with an active subscription plan.
|
||||
|
||||
#### LDK Network Configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -281,6 +281,68 @@ func (svc *albyOAuthService) GetInfo(ctx context.Context) (*AlbyInfo, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetVssAuthToken(ctx context.Context, nodeIdentifier string) (string, error) {
|
||||
logger.Logger.WithField("node_identifier", nodeIdentifier).Debug("fetching VSS token")
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch user token")
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := svc.oauthConf.Client(ctx, token)
|
||||
|
||||
type vssAuthTokenRequest struct {
|
||||
Identifier string `json:"identifier"`
|
||||
}
|
||||
|
||||
body := bytes.NewBuffer([]byte{})
|
||||
payload := vssAuthTokenRequest{
|
||||
Identifier: nodeIdentifier,
|
||||
}
|
||||
err = json.NewEncoder(body).Encode(&payload)
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to encode request payload")
|
||||
return "", err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/internal/auth_tokens", albyOAuthAPIURL), body)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Error creating request for vss auth token endpoint")
|
||||
return "", err
|
||||
}
|
||||
|
||||
setDefaultRequestHeaders(req)
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch vss auth token endpoint")
|
||||
return "", err
|
||||
}
|
||||
|
||||
if res.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("request to /internal/auth_tokens returned non-success status: %d", res.StatusCode)
|
||||
}
|
||||
|
||||
type vssTokenResponse struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
vssResponse := &vssTokenResponse{}
|
||||
err = json.NewDecoder(res.Body).Decode(vssResponse)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to decode API response")
|
||||
return "", err
|
||||
}
|
||||
|
||||
if vssResponse.Token == "" {
|
||||
logger.Logger.WithField("vssResponse", vssResponse).WithError(err).Error("No token in API response")
|
||||
return "", errors.New("no token in vss response")
|
||||
}
|
||||
|
||||
return vssResponse.Token, nil
|
||||
}
|
||||
|
||||
func (svc *albyOAuthService) GetMe(ctx context.Context) (*AlbyMe, error) {
|
||||
token, err := svc.fetchUserToken(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -482,7 +544,17 @@ func (svc *albyOAuthService) GetAuthUrl() string {
|
|||
}
|
||||
|
||||
func (svc *albyOAuthService) UnlinkAccount(ctx context.Context) error {
|
||||
err := svc.destroyAlbyAccountNWCNode(ctx)
|
||||
ldkVssEnabled, err := svc.cfg.Get("LdkVssEnabled", "")
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch LdkVssEnabled user config")
|
||||
return err
|
||||
}
|
||||
|
||||
if ldkVssEnabled == "true" {
|
||||
return errors.New("alby account cannot be unlinked while VSS is activated")
|
||||
}
|
||||
|
||||
err = svc.destroyAlbyAccountNWCNode(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to destroy Alby Account NWC node")
|
||||
}
|
||||
|
|
@ -740,8 +812,7 @@ func (svc *albyOAuthService) createEncryptedChannelBackup(event *events.StaticCh
|
|||
return nil, fmt.Errorf("failed to encode channels backup data: %w", err)
|
||||
}
|
||||
|
||||
path := []uint32{bip32.FirstHardenedChild}
|
||||
backupKey, err := svc.keys.DeriveKey(path)
|
||||
backupKey, err := svc.keys.DeriveKey([]uint32{bip32.FirstHardenedChild})
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to generate channels backup key")
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type AlbyOAuthService interface {
|
|||
DrainSharedWallet(ctx context.Context, lnClient lnclient.LNClient) error
|
||||
UnlinkAccount(ctx context.Context) error
|
||||
RequestAutoChannel(ctx context.Context, lnClient lnclient.LNClient, isPublic bool) (*AutoChannelResponse, error)
|
||||
GetVssAuthToken(ctx context.Context, nodeIdentifier string) (string, error)
|
||||
}
|
||||
|
||||
type AlbyBalanceResponse struct {
|
||||
|
|
@ -61,16 +62,23 @@ type AlbyInfo struct {
|
|||
type AlbyMeHub struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type AlbyMeSubscription struct {
|
||||
// PlanCode string `json:"plan_code"`
|
||||
Buzz bool `json:"buzz"`
|
||||
}
|
||||
|
||||
type AlbyMe struct {
|
||||
Identifier string `json:"identifier"`
|
||||
NPub string `json:"nostr_pubkey"`
|
||||
LightningAddress string `json:"lightning_address"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
KeysendPubkey string `json:"keysend_pubkey"`
|
||||
SharedNode bool `json:"shared_node"`
|
||||
Hub AlbyMeHub `json:"hub"`
|
||||
Identifier string `json:"identifier"`
|
||||
NPub string `json:"nostr_pubkey"`
|
||||
LightningAddress string `json:"lightning_address"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
KeysendPubkey string `json:"keysend_pubkey"`
|
||||
SharedNode bool `json:"shared_node"`
|
||||
Hub AlbyMeHub `json:"hub"`
|
||||
Subscription AlbyMeSubscription `json:"subscription"`
|
||||
}
|
||||
|
||||
type AlbyBalance struct {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ type AppConfig struct {
|
|||
LDKEsploraServer string `envconfig:"LDK_ESPLORA_SERVER" default:"https://electrs.getalbypro.com"` // TODO: remove LDK prefix
|
||||
LDKGossipSource string `envconfig:"LDK_GOSSIP_SOURCE"`
|
||||
LDKLogLevel string `envconfig:"LDK_LOG_LEVEL" default:"3"`
|
||||
LDKVssUrl string `envconfig:"LDK_VSS_URL"`
|
||||
MempoolApi string `envconfig:"MEMPOOL_API" default:"https://mempool.space/api"`
|
||||
AlbyClientId string `envconfig:"ALBY_OAUTH_CLIENT_ID" default:"J2PbXS1yOf"`
|
||||
AlbyClientSecret string `envconfig:"ALBY_OAUTH_CLIENT_SECRET" default:"rABK2n16IWjLTZ9M1uKU"`
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package ldk
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -15,6 +16,8 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/getAlby/ldk-node-go/ldk_node"
|
||||
"github.com/tyler-smith/go-bip32"
|
||||
|
||||
// "github.com/getAlby/hub/ldk_node"
|
||||
|
||||
"encoding/hex"
|
||||
|
|
@ -28,6 +31,7 @@ import (
|
|||
"github.com/getAlby/hub/lnclient"
|
||||
"github.com/getAlby/hub/logger"
|
||||
"github.com/getAlby/hub/lsp"
|
||||
"github.com/getAlby/hub/service/keys"
|
||||
"github.com/getAlby/hub/utils"
|
||||
)
|
||||
|
||||
|
|
@ -48,7 +52,8 @@ type LDKService struct {
|
|||
|
||||
const resetRouterKey = "ResetRouter"
|
||||
|
||||
func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, network string, staticChannelsBackup *events.StaticChannelsBackupEvent, restoredFromSeed bool) (result lnclient.LNClient, err error) {
|
||||
// TODO: remove staticChannelsBackup *events.StaticChannelsBackupEvent, restoredFromSeed bool (we have a dedicated SCB recovery tool)
|
||||
func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events.EventPublisher, mnemonic, workDir string, network string, staticChannelsBackup *events.StaticChannelsBackupEvent, restoredFromSeed bool, vssToken string) (result lnclient.LNClient, err error) {
|
||||
if mnemonic == "" || workDir == "" {
|
||||
return nil, errors.New("one or more required LDK configuration are missing")
|
||||
}
|
||||
|
|
@ -125,7 +130,17 @@ func NewLDKService(ctx context.Context, cfg config.Config, eventPublisher events
|
|||
builder.RestoreEncodedChannelMonitors(getEncodedChannelMonitorsFromStaticChannelsBackup(staticChannelsBackup))
|
||||
}
|
||||
|
||||
node, err := builder.Build()
|
||||
logger.Logger.WithFields(logrus.Fields{
|
||||
"vss": vssToken != "",
|
||||
}).Info("Creating node")
|
||||
var node *ldk_node.Node
|
||||
if vssToken != "" {
|
||||
node, err = builder.BuildWithVssStoreAndFixedHeaders(cfg.GetEnv().LDKVssUrl, "albyhub", map[string]string{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", vssToken),
|
||||
})
|
||||
} else {
|
||||
node, err = builder.Build()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to create LDK node")
|
||||
|
|
@ -1773,3 +1788,19 @@ func forceCloseChannelsFromStaticChannelsBackup(node *ldk_node.Node, staticChann
|
|||
|
||||
node.ForceCloseAllChannelsWithoutBroadcastingTxn()
|
||||
}
|
||||
|
||||
func GetVssNodeIdentifier(keys keys.Keys) (string, error) {
|
||||
key, err := keys.DeriveKey([]uint32{bip32.FirstHardenedChild + 2})
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// return a 6-character hex string of the hash of a derived key to ensure if same user
|
||||
// runs multiple hubs with different mnemonics, they are all
|
||||
// saved in the VSS under different user_tokens.
|
||||
pubkeyHash256 := sha256.New()
|
||||
pubkeyHash256.Write(key.Key)
|
||||
pubkeyHashBytes := pubkeyHash256.Sum(nil)
|
||||
return hex.EncodeToString(pubkeyHashBytes[0:3]), nil
|
||||
}
|
||||
|
|
|
|||
36
lnclient/ldk/ldk_test.go
Normal file
36
lnclient/ldk/ldk_test.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
package ldk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/getAlby/hub/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetVssNodeIdentifier(t *testing.T) {
|
||||
mnemonic := "thought turkey ask pottery head say catalog desk pledge elbow naive mimic"
|
||||
expectedVssNodeIdentifier := "751636"
|
||||
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestServiceWithMnemonic(mnemonic, "123")
|
||||
require.NoError(t, err)
|
||||
|
||||
vssNodeIdentifier, err := GetVssNodeIdentifier(svc.Keys)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedVssNodeIdentifier, vssNodeIdentifier)
|
||||
}
|
||||
func TestGetVssNodeIdentifier2(t *testing.T) {
|
||||
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
expectedVssNodeIdentifier := "770256"
|
||||
|
||||
defer tests.RemoveTestService()
|
||||
svc, err := tests.CreateTestServiceWithMnemonic(mnemonic, "123")
|
||||
require.NoError(t, err)
|
||||
|
||||
vssNodeIdentifier, err := GetVssNodeIdentifier(svc.Keys)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expectedVssNodeIdentifier, vssNodeIdentifier)
|
||||
}
|
||||
|
|
@ -115,6 +115,9 @@ func (keys *keys) GetAppWalletKey(appID uint) (string, error) {
|
|||
}
|
||||
|
||||
func (keys *keys) DeriveKey(path []uint32) (*bip32.Key, error) {
|
||||
if len(path) == 0 {
|
||||
return nil, errors.New("path must have at least one element")
|
||||
}
|
||||
if keys.appKey == nil {
|
||||
return nil, errors.New("app key not set")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -280,10 +280,16 @@ func (svc *service) launchLNBackend(ctx context.Context, encryptionKey string) e
|
|||
LNDMacaroonHex, _ := svc.cfg.Get("LNDMacaroonHex", encryptionKey)
|
||||
lnClient, err = lnd.NewLNDService(ctx, svc.eventPublisher, LNDAddress, LNDCertHex, LNDMacaroonHex)
|
||||
case config.LDKBackendType:
|
||||
Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey)
|
||||
LDKWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "ldk")
|
||||
mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey)
|
||||
ldkWorkdir := path.Join(svc.cfg.GetEnv().Workdir, "ldk")
|
||||
var vssToken string
|
||||
vssToken, err = svc.requestVssToken(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to request VSS token")
|
||||
return err
|
||||
}
|
||||
|
||||
lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, Mnemonic, LDKWorkdir, svc.cfg.GetEnv().LDKNetwork, nil, false)
|
||||
lnClient, err = ldk.NewLDKService(ctx, svc.cfg, svc.eventPublisher, mnemonic, ldkWorkdir, svc.cfg.GetEnv().LDKNetwork, nil, false, vssToken)
|
||||
case config.GreenlightBackendType:
|
||||
Mnemonic, _ := svc.cfg.Get("Mnemonic", encryptionKey)
|
||||
GreenlightInviteCode, _ := svc.cfg.Get("GreenlightInviteCode", encryptionKey)
|
||||
|
|
@ -363,3 +369,43 @@ func closeRelay(relay *nostr.Relay) {
|
|||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func (svc *service) requestVssToken(ctx context.Context) (string, error) {
|
||||
nodeLastStartTime, _ := svc.cfg.Get("NodeLastStartTime", "")
|
||||
|
||||
// for brand new nodes, consider enabling VSS
|
||||
if nodeLastStartTime == "" && svc.cfg.GetEnv().LDKVssUrl != "" {
|
||||
albyUserIdentifier, err := svc.albyOAuthSvc.GetUserIdentifier()
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch alby user identifier")
|
||||
return "", err
|
||||
}
|
||||
if albyUserIdentifier != "" {
|
||||
me, err := svc.albyOAuthSvc.GetMe(ctx)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch alby user")
|
||||
return "", err
|
||||
}
|
||||
// only activate VSS for Alby paid subscribers
|
||||
if me.Subscription.Buzz {
|
||||
svc.cfg.SetUpdate("LdkVssEnabled", "true", "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vssToken := ""
|
||||
vssEnabled, _ := svc.cfg.Get("LdkVssEnabled", "")
|
||||
if vssEnabled == "true" {
|
||||
vssNodeIdentifier, err := ldk.GetVssNodeIdentifier(svc.keys)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to get VSS node identifier")
|
||||
return "", err
|
||||
}
|
||||
vssToken, err = svc.albyOAuthSvc.GetVssAuthToken(ctx, vssNodeIdentifier)
|
||||
if err != nil {
|
||||
logger.Logger.WithError(err).Error("Failed to fetch VSS JWT token")
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return vssToken, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue